defuse_ton_connect/
lib.rs1mod 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 pub address: MsgAddress,
29 pub domain: String,
31 #[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 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}