Skip to main content

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
53impl TonConnectPayload {
54    pub fn try_hash(&self) -> Result<defuse_crypto::CryptoHash, tlb_ton::StringError> {
55        use crate::schema::{PayloadSchema, TonConnectPayloadContext};
56        use std::borrow::Cow;
57        use tlb_ton::Error;
58
59        let timestamp: u64 = self
60            .timestamp
61            .timestamp()
62            .try_into()
63            .map_err(|_| Error::custom("negative timestamp"))?;
64
65        let context = TonConnectPayloadContext {
66            address: self.address,
67            domain: Cow::Borrowed(self.domain.as_str()),
68            timestamp,
69        };
70
71        self.payload.hash_with_context(context)
72    }
73
74    pub fn hash(&self) -> defuse_crypto::CryptoHash {
75        self.try_hash().expect("ton-connect hash")
76    }
77}
78
79impl Payload for TonConnectPayload {
80    #[inline]
81    fn hash(&self) -> defuse_crypto::CryptoHash {
82        Self::hash(self)
83    }
84}
85
86#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
87#[cfg_attr(
88    feature = "serde",
89    ::cfg_eval::cfg_eval,
90    ::serde_with::serde_as,
91    derive(::serde::Serialize, ::serde::Deserialize),
92    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
93)]
94#[autoimpl(Deref using self.payload)]
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct SignedTonConnectPayload {
97    #[cfg_attr(feature = "serde", serde(flatten))]
98    pub payload: TonConnectPayload,
99
100    #[cfg_attr(
101        feature = "serde",
102        serde_as(as = "defuse_crypto::serde::AsCurve<Ed25519>")
103    )]
104    pub public_key: <Ed25519 as defuse_crypto::Curve>::PublicKey,
105    #[cfg_attr(
106        feature = "serde",
107        serde_as(as = "defuse_crypto::serde::AsCurve<Ed25519>")
108    )]
109    pub signature: <Ed25519 as defuse_crypto::Curve>::Signature,
110}
111
112impl Payload for SignedTonConnectPayload {
113    #[inline]
114    fn hash(&self) -> defuse_crypto::CryptoHash {
115        self.payload.hash()
116    }
117}
118
119#[cfg(feature = "near-contract")]
120impl defuse_crypto::SignedPayload for SignedTonConnectPayload {
121    type PublicKey = <Ed25519 as defuse_crypto::Curve>::PublicKey;
122
123    #[inline]
124    fn verify(&self) -> Option<Self::PublicKey> {
125        use defuse_crypto::VerifiableCurve;
126        Ed25519::verify(&self.signature, &self.hash(), &self.public_key)
127    }
128}
129
130#[cfg(test)]
131#[allow(clippy::unreadable_literal)]
132mod tests {
133    use super::*;
134
135    use arbitrary::{Arbitrary, Unstructured};
136    use defuse_crypto::SignedPayload;
137    use defuse_test_utils::random::random_bytes;
138    use hex_literal::hex;
139    use rstest::rstest;
140    use tlb_ton::UnixTimestamp;
141
142    #[cfg(all(feature = "text", feature = "serde"))]
143    #[rstest]
144    fn verify_text(random_bytes: Vec<u8>) {
145        verify(
146            &SignedTonConnectPayload {
147                payload: TonConnectPayload {
148                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
149                        .parse()
150                        .unwrap(),
151                    domain: "ton-connect.github.io".to_string(),
152                    timestamp: DateTime::from_timestamp(1747759882, 0).unwrap(),
153                    payload: TonConnectPayloadSchema::text("Hello, TON!".repeat(100)),
154                },
155                public_key: hex!(
156                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
157                ),
158                signature: hex!(
159                    "7bc628f6d634ab6ddaf10463742b13f0ede3cb828737d9ce1962cc808fbfe7035e77c1a3d0b682acf02d645cc1a244992b276552c0e1c57d30b03c2820d73d01"
160                ),
161            },
162            &random_bytes,
163        );
164    }
165
166    #[cfg(all(feature = "binary", feature = "serde"))]
167    #[rstest]
168    fn verify_binary(random_bytes: Vec<u8>) {
169        verify(
170            &SignedTonConnectPayload {
171                payload: TonConnectPayload {
172                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
173                        .parse()
174                        .unwrap(),
175                    domain: "ton-connect.github.io".to_string(),
176                    timestamp: DateTime::from_timestamp(1747760435, 0).unwrap(),
177                    payload: TonConnectPayloadSchema::binary(hex!("48656c6c6f2c20544f4e21")),
178                },
179                public_key: hex!(
180                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
181                ),
182                signature: hex!(
183                    "9cf4c1c16b47afce46940eb9cd410894f31544b74206c2254bb1651f9b32cf5b0e482b78a2e8251e54d3517fae4b06c6f23546667d63ff62dccce70451698d01"
184                ),
185            },
186            &random_bytes,
187        );
188    }
189
190    #[cfg(all(feature = "cell", feature = "serde"))]
191    #[rstest]
192    fn verify_cell(random_bytes: Vec<u8>) {
193        use tlb_ton::BagOfCells;
194
195        verify(
196            &SignedTonConnectPayload {
197                payload: TonConnectPayload {
198                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
199                        .parse()
200                        .unwrap(),
201                    domain: "ton-connect.github.io".to_string(),
202                    timestamp: DateTime::from_timestamp(1747772412, 0).unwrap(),
203                    payload: TonConnectPayloadSchema::cell(
204                        0x2eccd0c1,
205                        BagOfCells::parse_base64("te6cckEBAQEAEQAAHgAAAABIZWxsbywgVE9OIb7WCx4=")
206                            .unwrap()
207                            .into_single_root()
208                            .unwrap()
209                            .as_ref()
210                            .clone(),
211                    ),
212                },
213                public_key: hex!(
214                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
215                ),
216                signature: hex!(
217                    "6ad083855374c201c2acb14aa4e7eef44603c8d356624c8fd3b6be3babd84bd8bc7390f0ed4484ab58a535b3088681e0006839eb07136470985b3a33bfa17c05"
218                ),
219            },
220            &random_bytes,
221        );
222    }
223
224    #[cfg(feature = "serde")]
225    fn verify(signed: &SignedTonConnectPayload, random_bytes: &[u8]) {
226        verify_ok(signed, true);
227
228        // tampering
229        let mut u = Unstructured::new(random_bytes);
230        {
231            let mut t = signed.clone();
232            t.payload.address = Arbitrary::arbitrary(&mut u).unwrap();
233            dbg!(&t.payload.address);
234            verify_ok(&t, false);
235        }
236        {
237            let mut t = signed.clone();
238            t.payload.domain = Arbitrary::arbitrary(&mut u).unwrap();
239            dbg!(&t.payload.domain);
240            verify_ok(&t, false);
241        }
242        {
243            let mut t = signed.clone();
244            t.payload.timestamp = UnixTimestamp::arbitrary(&mut u).unwrap();
245            dbg!(&t.payload.timestamp);
246            verify_ok(&t, false);
247        }
248        {
249            let mut t = signed.clone();
250            t.payload.payload = Arbitrary::arbitrary(&mut u).unwrap();
251            dbg!(&t.payload.payload);
252            verify_ok(&t, false);
253        }
254    }
255
256    #[cfg(all(feature = "arbitrary", feature = "serde"))]
257    #[rstest]
258    fn arbitrary(random_bytes: Vec<u8>) {
259        verify_ok(
260            &Unstructured::new(&random_bytes).arbitrary().unwrap(),
261            false,
262        );
263    }
264
265    #[cfg(feature = "serde")]
266    fn verify_ok(signed: &SignedTonConnectPayload, ok: bool) {
267        let serialized = serde_json::to_string_pretty(signed).unwrap();
268        println!("{}", &serialized);
269        let deserialized: SignedTonConnectPayload = serde_json::from_str(&serialized).unwrap();
270
271        assert_eq!(&deserialized, signed);
272        assert_eq!(deserialized.verify(), ok.then_some(deserialized.public_key));
273    }
274}