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