defuse_ton_connect/
lib.rs

1//! TON Connect [signData](https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#sign-data)
2mod schema;
3
4use chrono::{DateTime, Utc};
5use defuse_crypto::Ed25519;
6use impl_tools::autoimpl;
7use tlb_ton::MsgAddress;
8
9use defuse_crypto::Payload;
10
11pub use schema::TonConnectPayloadSchema;
12pub use tlb_ton;
13
14#[cfg_attr(
15    feature = "serde",
16    ::cfg_eval::cfg_eval,
17    ::serde_with::serde_as,
18    derive(::serde::Serialize, ::serde::Deserialize),
19    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
20)]
21#[autoimpl(Deref using self.payload)]
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct TonConnectPayload {
24    /// Wallet address in either [Raw](https://docs.ton.org/v3/documentation/smart-contracts/addresses/address-formats#raw-address) representation
25    /// or [user-friendly](https://docs.ton.org/v3/documentation/smart-contracts/addresses/address-formats#user-friendly-address) format
26    pub address: MsgAddress,
27    /// dApp domain
28    pub domain: String,
29    /// UNIX timestamp (in seconds or RFC3339) at the time of singing
30    #[cfg_attr(
31        feature = "serde",
32        serde_as(as = "::serde_with::PickFirst<(_, ::serde_with::TimestampSeconds)>")
33    )]
34    pub timestamp: DateTime<Utc>,
35    pub payload: TonConnectPayloadSchema,
36}
37
38// `serde_as` requires `cfg_eval`, which pre-expands field `cfg_attr`s during derive
39// pre-processing — before the compiler resolves derive helper attributes in the re-emitted
40// item. This breaks fields that combine `serde_as` and `#[arbitrary(with = ...)]`.
41#[cfg(feature = "arbitrary")]
42impl<'a> arbitrary::Arbitrary<'a> for TonConnectPayload {
43    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
44        Ok(Self {
45            address: u.arbitrary()?,
46            domain: u.arbitrary()?,
47            timestamp: ::tlb_ton::UnixTimestamp::arbitrary(u)?,
48            payload: u.arbitrary()?,
49        })
50    }
51}
52
53#[cfg(any(feature = "near-contract", feature = "sha2"))]
54impl TonConnectPayload {
55    pub fn try_hash(&self) -> Result<defuse_crypto::CryptoHash, tlb_ton::StringError> {
56        use crate::schema::{PayloadSchema, TonConnectPayloadContext};
57        use std::borrow::Cow;
58        use tlb_ton::Error;
59
60        let timestamp: u64 = self
61            .timestamp
62            .timestamp()
63            .try_into()
64            .map_err(|_| Error::custom("negative timestamp"))?;
65
66        let context = TonConnectPayloadContext {
67            address: self.address,
68            domain: Cow::Borrowed(self.domain.as_str()),
69            timestamp,
70        };
71
72        self.payload.hash_with_context(context)
73    }
74
75    pub fn hash(&self) -> defuse_crypto::CryptoHash {
76        self.try_hash().expect("ton-connect hash")
77    }
78}
79
80impl Payload for TonConnectPayload {
81    #[inline]
82    fn hash(&self) -> defuse_crypto::CryptoHash {
83        Self::hash(self)
84    }
85}
86
87#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
88#[cfg_attr(
89    feature = "serde",
90    ::cfg_eval::cfg_eval,
91    ::serde_with::serde_as,
92    derive(::serde::Serialize, ::serde::Deserialize),
93    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
94)]
95#[autoimpl(Deref using self.payload)]
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct SignedTonConnectPayload {
98    #[cfg_attr(feature = "serde", serde(flatten))]
99    pub payload: TonConnectPayload,
100
101    #[cfg_attr(
102        feature = "serde",
103        serde_as(as = "defuse_crypto::serde::AsCurve<Ed25519>")
104    )]
105    pub public_key: <Ed25519 as defuse_crypto::Curve>::PublicKey,
106    #[cfg_attr(
107        feature = "serde",
108        serde_as(as = "defuse_crypto::serde::AsCurve<Ed25519>")
109    )]
110    pub signature: <Ed25519 as defuse_crypto::Curve>::Signature,
111}
112
113impl Payload for SignedTonConnectPayload {
114    #[inline]
115    fn hash(&self) -> defuse_crypto::CryptoHash {
116        self.payload.hash()
117    }
118}
119
120#[cfg(feature = "near-contract")]
121impl defuse_crypto::SignedPayload for SignedTonConnectPayload {
122    type PublicKey = <Ed25519 as defuse_crypto::Curve>::PublicKey;
123
124    #[inline]
125    fn verify(&self) -> Option<Self::PublicKey> {
126        use defuse_crypto::VerifiableCurve;
127        Ed25519::verify(&self.signature, &self.hash(), &self.public_key)
128    }
129}
130
131#[cfg(test)]
132#[allow(clippy::unreadable_literal)]
133mod tests {
134    use super::*;
135
136    use arbitrary::{Arbitrary, Unstructured};
137    use defuse_crypto::SignedPayload;
138    use defuse_test_utils::random::random_bytes;
139    use hex_literal::hex;
140    use rstest::rstest;
141    use tlb_ton::UnixTimestamp;
142
143    #[cfg(all(feature = "text", feature = "serde"))]
144    #[rstest]
145    fn verify_text(random_bytes: Vec<u8>) {
146        verify(
147            &SignedTonConnectPayload {
148                payload: TonConnectPayload {
149                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
150                        .parse()
151                        .unwrap(),
152                    domain: "ton-connect.github.io".to_string(),
153                    timestamp: DateTime::from_timestamp(1747759882, 0).unwrap(),
154                    payload: TonConnectPayloadSchema::text("Hello, TON!".repeat(100)),
155                },
156                public_key: hex!(
157                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
158                ),
159                signature: hex!(
160                    "7bc628f6d634ab6ddaf10463742b13f0ede3cb828737d9ce1962cc808fbfe7035e77c1a3d0b682acf02d645cc1a244992b276552c0e1c57d30b03c2820d73d01"
161                ),
162            },
163            &random_bytes,
164        );
165    }
166
167    #[cfg(all(feature = "binary", feature = "serde"))]
168    #[rstest]
169    fn verify_binary(random_bytes: Vec<u8>) {
170        verify(
171            &SignedTonConnectPayload {
172                payload: TonConnectPayload {
173                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
174                        .parse()
175                        .unwrap(),
176                    domain: "ton-connect.github.io".to_string(),
177                    timestamp: DateTime::from_timestamp(1747760435, 0).unwrap(),
178                    payload: TonConnectPayloadSchema::binary(hex!("48656c6c6f2c20544f4e21")),
179                },
180                public_key: hex!(
181                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
182                ),
183                signature: hex!(
184                    "9cf4c1c16b47afce46940eb9cd410894f31544b74206c2254bb1651f9b32cf5b0e482b78a2e8251e54d3517fae4b06c6f23546667d63ff62dccce70451698d01"
185                ),
186            },
187            &random_bytes,
188        );
189    }
190
191    #[cfg(all(feature = "cell", feature = "serde"))]
192    #[rstest]
193    fn verify_cell(random_bytes: Vec<u8>) {
194        use tlb_ton::BagOfCells;
195
196        verify(
197            &SignedTonConnectPayload {
198                payload: TonConnectPayload {
199                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
200                        .parse()
201                        .unwrap(),
202                    domain: "ton-connect.github.io".to_string(),
203                    timestamp: DateTime::from_timestamp(1747772412, 0).unwrap(),
204                    payload: TonConnectPayloadSchema::cell(
205                        0x2eccd0c1,
206                        BagOfCells::parse_base64("te6cckEBAQEAEQAAHgAAAABIZWxsbywgVE9OIb7WCx4=")
207                            .unwrap()
208                            .into_single_root()
209                            .unwrap()
210                            .as_ref()
211                            .clone(),
212                    ),
213                },
214                public_key: hex!(
215                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
216                ),
217                signature: hex!(
218                    "6ad083855374c201c2acb14aa4e7eef44603c8d356624c8fd3b6be3babd84bd8bc7390f0ed4484ab58a535b3088681e0006839eb07136470985b3a33bfa17c05"
219                ),
220            },
221            &random_bytes,
222        );
223    }
224
225    #[cfg(feature = "serde")]
226    fn verify(signed: &SignedTonConnectPayload, random_bytes: &[u8]) {
227        verify_ok(signed, true);
228
229        // tampering
230        let mut u = Unstructured::new(random_bytes);
231        {
232            let mut t = signed.clone();
233            t.payload.address = Arbitrary::arbitrary(&mut u).unwrap();
234            dbg!(&t.payload.address);
235            verify_ok(&t, false);
236        }
237        {
238            let mut t = signed.clone();
239            t.payload.domain = Arbitrary::arbitrary(&mut u).unwrap();
240            dbg!(&t.payload.domain);
241            verify_ok(&t, false);
242        }
243        {
244            let mut t = signed.clone();
245            t.payload.timestamp = UnixTimestamp::arbitrary(&mut u).unwrap();
246            dbg!(&t.payload.timestamp);
247            verify_ok(&t, false);
248        }
249        {
250            let mut t = signed.clone();
251            t.payload.payload = Arbitrary::arbitrary(&mut u).unwrap();
252            dbg!(&t.payload.payload);
253            verify_ok(&t, false);
254        }
255    }
256
257    #[cfg(all(feature = "arbitrary", feature = "serde"))]
258    #[rstest]
259    fn arbitrary(random_bytes: Vec<u8>) {
260        verify_ok(
261            &Unstructured::new(&random_bytes).arbitrary().unwrap(),
262            false,
263        );
264    }
265
266    #[cfg(feature = "serde")]
267    fn verify_ok(signed: &SignedTonConnectPayload, ok: bool) {
268        let serialized = serde_json::to_string_pretty(signed).unwrap();
269        println!("{}", &serialized);
270        let deserialized: SignedTonConnectPayload = serde_json::from_str(&serialized).unwrap();
271
272        assert_eq!(&deserialized, signed);
273        assert_eq!(deserialized.verify(), ok.then_some(deserialized.public_key));
274    }
275}