1#[cfg(feature = "cell")]
3mod cell;
4
5use defuse_crypto::{Curve, ed25519::Ed25519};
6use defuse_digest::{Digest, sha2::Sha256};
7use defuse_time::Timestamp;
8#[cfg(feature = "arbitrary")]
9use defuse_time::arbitrary::RangeNanos;
10#[cfg(feature = "cell")]
11pub use tlb_ton::Cell;
12pub use tlb_ton::MsgAddress;
13
14pub struct TonConnect;
16
17impl TonConnect {
18 #[must_use = "check if verification passed"]
19 #[inline]
20 pub fn verify(
21 public_key: &<Ed25519 as Curve>::PublicKey,
22 payload: &TonConnectPayload,
23 signature: &<Ed25519 as Curve>::Signature,
24 ) -> bool {
25 let Some(prehash) = payload.try_prehash() else {
26 return false;
27 };
28
29 Ed25519::verify(public_key, &prehash, signature)
30 }
31}
32
33#[cfg_attr(
35 feature = "serde",
36 ::cfg_eval::cfg_eval,
37 ::serde_with::serde_as,
38 derive(::serde::Serialize, ::serde::Deserialize),
39 cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema))
40)]
41#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct TonConnectPayload {
44 pub address: MsgAddress,
47
48 pub domain: String,
50
51 #[cfg_attr(
53 feature = "arbitrary",
54 arbitrary(with = ::arbitrary_with::As::<RangeNanos::<0>>::arbitrary)
55 )]
56 #[cfg_attr(
57 feature = "serde",
58 serde_as(as = "::serde_with::PickFirst<(
59 _,
60 ::defuse_time::serde::TimestampSeconds<::serde_with::DisplayFromStr>,
61 ::defuse_time::serde::TimestampSeconds,
62 )>")
63 )]
64 pub timestamp: Timestamp,
65
66 pub payload: TonConnectPayloadSchema,
68}
69
70impl TonConnectPayload {
71 pub fn try_prehash(&self) -> Option<[u8; 32]> {
72 let timestamp: u64 = self.timestamp.as_secs().try_into().ok()?;
73
74 let (prefix, payload) = match &self.payload {
75 TonConnectPayloadSchema::Text { text } => (b"txt", text.as_bytes()),
76 TonConnectPayloadSchema::Binary { bytes } => (b"bin", bytes.as_slice()),
77 #[cfg(feature = "cell")]
78 TonConnectPayloadSchema::Cell { schema_crc, cell } => {
79 return self::cell::TonConnectCellMessage {
80 schema_crc: *schema_crc,
81 timestamp,
82 user_address: &self.address,
83 app_domain: &self.domain,
84 payload: &cell,
85 }
86 .hash();
87 }
88 };
89
90 let domain_len: u32 = self.domain.len().try_into().ok()?;
91 let payload_len: u32 = payload.len().try_into().ok()?;
92
93 let prehash = Sha256::new_with_prefix(b"\xFF\xFFton-connect/sign-data/")
95 .chain_update(self.address.workchain_id.to_be_bytes())
96 .chain_update(self.address.address)
97 .chain_update(domain_len.to_be_bytes())
98 .chain_update(self.domain.as_bytes())
99 .chain_update(timestamp.to_be_bytes())
100 .chain_update(prefix)
101 .chain_update(payload_len.to_be_bytes())
102 .chain_update(payload)
103 .finalize()
104 .into();
105
106 Some(prehash)
107 }
108}
109
110#[cfg_attr(
114 feature = "serde",
115 ::cfg_eval::cfg_eval,
116 ::serde_with::serde_as,
117 derive(::serde::Serialize, ::serde::Deserialize),
118 cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema)),
119 serde(tag = "type", rename_all = "snake_case")
120)]
121#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum TonConnectPayloadSchema {
124 Text { text: String },
128
129 Binary {
134 #[cfg_attr(feature = "serde", serde_as(as = "::serde_with::base64::Base64"))]
135 bytes: Vec<u8>,
136 },
137
138 #[cfg(feature = "cell")]
143 Cell {
144 schema_crc: u32,
146
147 #[cfg_attr(
149 feature = "serde",
150 serde_as(as = "defuse_serde_utils::tlb::AsBoC<serde_with::base64::Base64>")
151 )]
152 cell: Cell,
153 },
154}
155
156impl TonConnectPayloadSchema {
157 pub fn text(txt: impl Into<String>) -> Self {
158 Self::Text { text: txt.into() }
159 }
160
161 pub fn binary(bytes: impl Into<Vec<u8>>) -> Self {
162 Self::Binary {
163 bytes: bytes.into(),
164 }
165 }
166
167 #[cfg(feature = "cell")]
168 pub const fn cell(schema_crc: u32, cell: Cell) -> Self {
169 Self::Cell { schema_crc, cell }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 use defuse_crypto::ed25519::ed25519_dalek::{
178 PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH, Signature, VerifyingKey,
179 };
180 use hex_literal::hex;
181 use rstest::rstest;
182 use tlb_ton::BagOfCells;
183
184 #[rstest]
185 #[case::text(
186 hex!("22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"),
187 TonConnectPayload {
188 address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
189 .parse()
190 .unwrap(),
191 domain: "ton-connect.github.io".to_string(),
192 timestamp: Timestamp::from_secs(1747759882).unwrap(),
193 payload: TonConnectPayloadSchema::text("Hello, TON!".repeat(100)),
194 },
195 hex!("7bc628f6d634ab6ddaf10463742b13f0ede3cb828737d9ce1962cc808fbfe7035e77c1a3d0b682acf02d645cc1a244992b276552c0e1c57d30b03c2820d73d01"),
196 )]
197 #[case::binary(
198 hex!("22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"),
199 TonConnectPayload {
200 address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
201 .parse()
202 .unwrap(),
203 domain: "ton-connect.github.io".to_string(),
204 timestamp: Timestamp::from_secs(1747760435).unwrap(),
205 payload: TonConnectPayloadSchema::binary(hex!("48656c6c6f2c20544f4e21")),
206 },
207 hex!("9cf4c1c16b47afce46940eb9cd410894f31544b74206c2254bb1651f9b32cf5b0e482b78a2e8251e54d3517fae4b06c6f23546667d63ff62dccce70451698d01"),
208 )]
209 #[cfg_attr(feature = "cell", case::cell(
210 hex!("22e795a07e832fc9084ca35a488a711f1dbedef637d4e886a6997d93ee2c2e37"),
211 TonConnectPayload {
212 address: "0:f4809e5ffac9dc42a6b1d94c5e74ad5fd86378de675c805f2274d0055cbc9378"
213 .parse()
214 .unwrap(),
215 domain: "ton-connect.github.io".to_string(),
216 timestamp: Timestamp::from_secs(1747772412).unwrap(),
217 payload: TonConnectPayloadSchema::cell(
218 0x2eccd0c1,
219 BagOfCells::parse_base64("te6cckEBAQEAEQAAHgAAAABIZWxsbywgVE9OIb7WCx4=")
220 .unwrap()
221 .into_single_root()
222 .unwrap()
223 .as_ref()
224 .clone(),
225 ),
226 },
227 hex!("6ad083855374c201c2acb14aa4e7eef44603c8d356624c8fd3b6be3babd84bd8bc7390f0ed4484ab58a535b3088681e0006839eb07136470985b3a33bfa17c05"),
228 ))]
229 fn verify_ok(
230 #[case] public_key: [u8; PUBLIC_KEY_LENGTH],
231 #[case] payload: TonConnectPayload,
232 #[case] signature: [u8; SIGNATURE_LENGTH],
233 ) {
234 let public_key = VerifyingKey::from_bytes(&public_key).unwrap();
235 let signature = Signature::from_bytes(&signature);
236
237 assert!(
238 TonConnect::verify(&public_key, &payload, &signature),
239 "invalid signature"
240 );
241 }
242}