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