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
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 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}