1mod 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 pub address: MsgAddress,
27 pub domain: String,
29 #[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#[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 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}