Skip to main content

defuse_ton_connect/
lib.rs

1//! TON Connect [signData](https://docs.tonconsole.com/academy/sign-data)
2#[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
14/// [TON Connect](https://docs.tonconsole.com/academy/sign-data) signature schema.
15pub 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/// [TON Connect](https://docs.tonconsole.com/academy/sign-data) signable payload.
34#[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    /// Wallet address in either [Raw](https://docs.ton.org/v3/documentation/smart-contracts/addresses/address-formats#raw-address) representation
45    /// or [user-friendly](https://docs.ton.org/v3/documentation/smart-contracts/addresses/address-formats#user-friendly-address) format
46    pub address: MsgAddress,
47
48    /// dApp domain
49    pub domain: String,
50
51    /// UNIX timestamp (RFC3339 or in seconds) at the time of singing
52    #[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    /// Typed payload schema
67    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        // 0xffff ++ "ton-connect/sign-data/" ++ Address ++ AppDomain ++ Timestamp ++ Payload
94        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/// [`TonConnectPayload`](TonConnectPayload) schema.
111///
112/// See <https://docs.tonconsole.com/academy/sign-data#choosing-the-right-format>
113#[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 payload. Use this when the data is human-readable.
125    ///
126    /// See <https://docs.tonconsole.com/academy/sign-data#1-text>
127    Text { text: String },
128
129    /// Binary payload. Use this when signing a hash, arbitrary bytes,
130    /// or a file.
131    ///
132    /// See <https://docs.tonconsole.com/academy/sign-data#2-binary>
133    Binary {
134        #[cfg_attr(feature = "serde", serde_as(as = "::serde_with::base64::Base64"))]
135        bytes: Vec<u8>,
136    },
137
138    /// Cell payload. Use this if the signed data should be verifiable and
139    /// restorable inside a smart contract.
140    ///
141    /// See <https://docs.tonconsole.com/academy/sign-data#3-cell>
142    #[cfg(feature = "cell")]
143    Cell {
144        /// Schema CRC: `crc32(schema)`
145        schema_crc: u32,
146
147        /// Data serialized to [`Cell`] according to schema
148        #[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}