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 std::borrow::Cow;
5
6use chrono::{DateTime, Utc};
7use defuse_crypto::{Curve, Ed25519, Payload, SignedPayload, serde::AsCurve};
8use defuse_near_utils::UnwrapOrPanicError;
9use impl_tools::autoimpl;
10use near_sdk::near;
11use serde_with::{PickFirst, TimestampSeconds, serde_as};
12use tlb_ton::{Error, MsgAddress, StringError};
13
14pub use schema::TonConnectPayloadSchema;
15pub use tlb_ton;
16
17use crate::schema::{PayloadSchema, TonConnectPayloadContext};
18
19#[cfg_attr(test, derive(arbitrary::Arbitrary))]
20#[near(serializers = [json])]
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(test, arbitrary(with = ::tlb_ton::UnixTimestamp::arbitrary))]
31    #[serde_as(as = "PickFirst<(_, TimestampSeconds)>")]
32    pub timestamp: DateTime<Utc>,
33    pub payload: TonConnectPayloadSchema,
34}
35
36impl TonConnectPayload {
37    fn try_hash(&self) -> Result<near_sdk::CryptoHash, StringError> {
38        let timestamp: u64 = self
39            .timestamp
40            .timestamp()
41            .try_into()
42            .map_err(|_| Error::custom("negative timestamp"))?;
43
44        let context = TonConnectPayloadContext {
45            address: self.address,
46            domain: Cow::Borrowed(self.domain.as_str()),
47            timestamp,
48        };
49
50        self.payload.hash_with_context(context)
51    }
52}
53
54impl Payload for TonConnectPayload {
55    #[inline]
56    fn hash(&self) -> near_sdk::CryptoHash {
57        self.try_hash().unwrap_or_panic_str()
58    }
59}
60
61#[cfg_attr(test, derive(arbitrary::Arbitrary))]
62#[near(serializers = [json])]
63#[autoimpl(Deref using self.payload)]
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct SignedTonConnectPayload {
66    #[serde(flatten)]
67    pub payload: TonConnectPayload,
68
69    #[serde_as(as = "AsCurve<Ed25519>")]
70    pub public_key: <Ed25519 as Curve>::PublicKey,
71    #[serde_as(as = "AsCurve<Ed25519>")]
72    pub signature: <Ed25519 as Curve>::Signature,
73}
74
75impl Payload for SignedTonConnectPayload {
76    #[inline]
77    fn hash(&self) -> near_sdk::CryptoHash {
78        self.payload.hash()
79    }
80}
81
82impl SignedPayload for SignedTonConnectPayload {
83    type PublicKey = <Ed25519 as Curve>::PublicKey;
84
85    #[inline]
86    fn verify(&self) -> Option<Self::PublicKey> {
87        Ed25519::verify(&self.signature, &self.hash(), &self.public_key)
88    }
89}
90
91#[cfg(test)]
92#[allow(clippy::unreadable_literal)]
93mod tests {
94    use super::*;
95
96    use arbitrary::{Arbitrary, Unstructured};
97    use defuse_test_utils::random::random_bytes;
98    use hex_literal::hex;
99    use near_sdk::serde_json;
100    use rstest::rstest;
101    use tlb_ton::UnixTimestamp;
102
103    #[cfg(feature = "text")]
104    #[rstest]
105    fn verify_text(random_bytes: Vec<u8>) {
106        verify(
107            &SignedTonConnectPayload {
108                payload: TonConnectPayload {
109                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
110                        .parse()
111                        .unwrap(),
112                    domain: "ton-connect.github.io".to_string(),
113                    timestamp: DateTime::from_timestamp(1747759882, 0).unwrap(),
114                    payload: TonConnectPayloadSchema::text("Hello, TON!".repeat(100)),
115                },
116                public_key: hex!(
117                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
118                ),
119                signature: hex!(
120                    "7bc628f6d634ab6ddaf10463742b13f0ede3cb828737d9ce1962cc808fbfe7035e77c1a3d0b682acf02d645cc1a244992b276552c0e1c57d30b03c2820d73d01"
121                ),
122            },
123            &random_bytes,
124        );
125    }
126
127    #[cfg(feature = "binary")]
128    #[rstest]
129    fn verify_binary(random_bytes: Vec<u8>) {
130        verify(
131            &SignedTonConnectPayload {
132                payload: TonConnectPayload {
133                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
134                        .parse()
135                        .unwrap(),
136                    domain: "ton-connect.github.io".to_string(),
137                    timestamp: DateTime::from_timestamp(1747760435, 0).unwrap(),
138                    payload: TonConnectPayloadSchema::binary(hex!("48656c6c6f2c20544f4e21")),
139                },
140                public_key: hex!(
141                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
142                ),
143                signature: hex!(
144                    "9cf4c1c16b47afce46940eb9cd410894f31544b74206c2254bb1651f9b32cf5b0e482b78a2e8251e54d3517fae4b06c6f23546667d63ff62dccce70451698d01"
145                ),
146            },
147            &random_bytes,
148        );
149    }
150
151    #[cfg(feature = "cell")]
152    #[rstest]
153    fn verify_cell(random_bytes: Vec<u8>) {
154        use tlb_ton::BagOfCells;
155
156        verify(
157            &SignedTonConnectPayload {
158                payload: TonConnectPayload {
159                    address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
160                        .parse()
161                        .unwrap(),
162                    domain: "ton-connect.github.io".to_string(),
163                    timestamp: DateTime::from_timestamp(1747772412, 0).unwrap(),
164                    payload: TonConnectPayloadSchema::cell(
165                        0x2eccd0c1,
166                        BagOfCells::parse_base64("te6cckEBAQEAEQAAHgAAAABIZWxsbywgVE9OIb7WCx4=")
167                            .unwrap()
168                            .into_single_root()
169                            .unwrap()
170                            .as_ref()
171                            .clone(),
172                    ),
173                },
174                public_key: hex!(
175                    "22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"
176                ),
177                signature: hex!(
178                    "6ad083855374c201c2acb14aa4e7eef44603c8d356624c8fd3b6be3babd84bd8bc7390f0ed4484ab58a535b3088681e0006839eb07136470985b3a33bfa17c05"
179                ),
180            },
181            &random_bytes,
182        );
183    }
184
185    fn verify(signed: &SignedTonConnectPayload, random_bytes: &[u8]) {
186        verify_ok(signed, true);
187
188        // tampering
189        let mut u = Unstructured::new(random_bytes);
190        {
191            let mut t = signed.clone();
192            t.payload.address = Arbitrary::arbitrary(&mut u).unwrap();
193            dbg!(&t.payload.address);
194            verify_ok(&t, false);
195        }
196        {
197            let mut t = signed.clone();
198            t.payload.domain = Arbitrary::arbitrary(&mut u).unwrap();
199            dbg!(&t.payload.domain);
200            verify_ok(&t, false);
201        }
202        {
203            let mut t = signed.clone();
204            t.payload.timestamp = UnixTimestamp::arbitrary(&mut u).unwrap();
205            dbg!(&t.payload.timestamp);
206            verify_ok(&t, false);
207        }
208        {
209            let mut t = signed.clone();
210            t.payload.payload = Arbitrary::arbitrary(&mut u).unwrap();
211            dbg!(&t.payload.payload);
212            verify_ok(&t, false);
213        }
214    }
215
216    #[rstest]
217    fn arbitrary(random_bytes: Vec<u8>) {
218        verify_ok(
219            &Unstructured::new(&random_bytes).arbitrary().unwrap(),
220            false,
221        );
222    }
223
224    fn verify_ok(signed: &SignedTonConnectPayload, ok: bool) {
225        let serialized = serde_json::to_string_pretty(signed).unwrap();
226        println!("{}", &serialized);
227        let deserialized: SignedTonConnectPayload = serde_json::from_str(&serialized).unwrap();
228
229        assert_eq!(&deserialized, signed);
230        assert_eq!(deserialized.verify(), ok.then_some(deserialized.public_key));
231    }
232}