ton_address/
lib.rs

1#![forbid(unsafe_code)]
2
3use base64::prelude::{BASE64_STANDARD_NO_PAD, BASE64_URL_SAFE_NO_PAD};
4use base64::Engine;
5use crc::Crc;
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8
9pub type Workchain = i32;
10pub type HashPart = [u8; 32];
11
12/// A quick alias for converting an [`Address`] structure to
13/// a Base64 Standard string representation of an address.
14pub const BASE64_STD_DEFAULT: Base64Encoder = Base64Encoder::Standard {
15    bounceable: true,
16    production: true,
17};
18
19/// A quick alias for converting an [`Address`] structure to
20/// a Base64 Url Safe string representation of an address.
21pub const BASE64_URL_DEFAULT: Base64Encoder = Base64Encoder::UrlSafe {
22    bounceable: true,
23    production: true,
24};
25
26#[inline]
27fn crc16(slice: &[u8]) -> u16 {
28    Crc::<u16>::new(&crc::CRC_16_XMODEM).checksum(slice)
29}
30
31#[derive(Debug, thiserror::Error, PartialEq)]
32#[error("Error parsing TON address: {reason}")]
33pub struct ParseError {
34    pub address: String,
35    pub reason: &'static str,
36}
37
38/// A decoder used to encrypt and decrypt Base64 addresses
39/// on The Open Network (TON).
40#[derive(Debug, PartialEq)]
41pub enum Base64Decoder {
42    /// [`STANDARD`]: base64::alphabet::STANDARD
43    /// [`NO_PAD`]: base64::engine::general_purpose::NO_PAD
44    ///
45    /// Using the [`STANDARD`] base64 alphabet and [`NO_PAD`] config.
46    Standard,
47
48    /// [`URL_SAFE`]: base64::alphabet::URL_SAFE
49    /// [`NO_PAD`]: base64::engine::general_purpose::NO_PAD
50    ///
51    /// Using the [`URL_SAFE`] base64 alphabet and [`NO_PAD`] config.
52    UrlSafe,
53}
54
55impl Base64Decoder {
56    /// Decodes a Base64 encoded string depending on the selected algorithm.
57    #[inline]
58    fn decode(&self, str: &str) -> Result<Vec<u8>, ParseError> {
59        let res = match self {
60            Self::Standard => BASE64_STANDARD_NO_PAD.decode(str),
61            Self::UrlSafe => BASE64_URL_SAFE_NO_PAD.decode(str),
62        };
63
64        match res {
65            Ok(v) => Ok(v),
66            Err(_) => Err(ParseError {
67                address: str.to_owned(),
68                reason: "Invalid base64 address string: base64 decode error",
69            }),
70        }
71    }
72
73    /// Guesses the Base64 alphabet from the `str` argument.
74    #[inline]
75    fn guess(str: &str) -> Base64Decoder {
76        if str.contains('+') || str.contains('/') {
77            return Base64Decoder::Standard;
78        } else if str.contains('-') || str.contains('_') {
79            return Base64Decoder::UrlSafe;
80        }
81
82        // If there are no control characters in the encoded string,
83        // then it is compatible with both types of alphabets.
84        // So it's 100% safe.
85        Base64Decoder::Standard
86    }
87}
88
89/// An encoder that converts the Address structure to a Base64 string representation.
90#[derive(Debug, Copy, Clone)]
91pub enum Base64Encoder {
92    Standard { bounceable: bool, production: bool },
93    UrlSafe { bounceable: bool, production: bool },
94}
95
96impl Base64Encoder {
97    fn encode(&self, workchain: Workchain, hash_part: &HashPart) -> String {
98        let (bounceable, production) = match self {
99            Self::Standard {
100                bounceable,
101                production,
102            } => (bounceable, production),
103            Self::UrlSafe {
104                bounceable,
105                production,
106            } => (bounceable, production),
107        };
108
109        let mut buffer = [0u8; 36];
110
111        buffer[0] = match (bounceable, production) {
112            (true, true) => 0x11,
113            (true, false) => 0x51,
114            (false, true) => 0x91,
115            (false, false) => 0xD1,
116        };
117
118        buffer[1] = (workchain & 0xFF) as u8;
119        buffer[2..34].clone_from_slice(hash_part);
120
121        let crc = crc16(&buffer[0..34]);
122
123        buffer[34] = ((crc >> 8) & 0xFF) as u8;
124        buffer[35] = (crc & 0xFF) as u8;
125
126        match self {
127            Self::Standard { .. } => BASE64_STANDARD_NO_PAD.encode(buffer),
128            Self::UrlSafe { .. } => BASE64_URL_SAFE_NO_PAD.encode(buffer),
129        }
130    }
131}
132
133/// An intermediate structure that should not be used explicitly,
134/// and represents the result of decoding an address through
135/// the [`Address`] structure.
136#[derive(Debug)]
137pub struct EncoderResult {
138    pub address: Address,
139    pub non_bounceable: bool,
140    pub non_production: bool,
141    #[allow(dead_code)]
142    pub decoder: Base64Decoder,
143}
144
145impl EncoderResult {
146    pub fn is_non_bounceable(&self) -> bool {
147        self.non_bounceable
148    }
149
150    pub fn is_non_production(&self) -> bool {
151        self.non_production
152    }
153
154    pub fn is_bounceable(&self) -> bool {
155        !self.non_bounceable
156    }
157
158    pub fn is_production(&self) -> bool {
159        !self.non_production
160    }
161}
162
163impl PartialEq for EncoderResult {
164    /// The logic of the comparison in this case is such that regardless of the bounceable and
165    /// production, encoder flags, the result will be positive only if the workchain and
166    /// hash_part of both addresses are equal.
167    fn eq(&self, other: &Self) -> bool {
168        self.address == other.address
169    }
170}
171
172/// A structure representing the internals of an address
173/// in a Ton network.
174///
175/// Regardless of the address type, its `workchain` and `hash_part`
176/// always remain the same.
177#[derive(Debug, PartialEq)]
178pub struct Address {
179    workchain: Workchain,
180    hash_part: HashPart,
181}
182
183impl Address {
184    /// Creates a new [`Address`] structure from workchain and hash_part.
185    pub fn new(workchain: Workchain, hash_part: &HashPart) -> Self {
186        Self {
187            workchain,
188            hash_part: *hash_part,
189        }
190    }
191
192    /// Creates a new [`Address`] structure using the null values of workchain
193    /// and hash_part.
194    pub const fn empty() -> Self {
195        Self {
196            workchain: 0,
197            hash_part: [0u8; 32],
198        }
199    }
200
201    /// Returns the number of the workchain.
202    pub fn get_workchain(&self) -> i32 {
203        self.workchain
204    }
205
206    /// Returns a reference to the hash part.
207    pub fn get_hash_part(&self) -> &HashPart {
208        &self.hash_part
209    }
210
211    /// Attempt to create an [`Address`] structure from the
212    /// string representation of the raw address.
213    pub fn from_raw_address<S>(raw: S) -> Result<Self, ParseError>
214    where
215        S: AsRef<str>,
216    {
217        let raw = raw.as_ref();
218
219        let parts = raw.split(':').collect::<Vec<&str>>();
220
221        if parts.len() != 2 {
222            return Err(ParseError {
223                address: raw.to_owned(),
224                reason: "Invalid raw address string: wrong address format",
225            });
226        }
227
228        let wc = match parts[0].parse::<i32>() {
229            Ok(wc) => wc,
230            Err(_) => {
231                return Err(ParseError {
232                    address: raw.to_owned(),
233                    reason: "Invalid raw address string: workchain number is not a 32-bit integer",
234                });
235            }
236        };
237
238        let hash_part = match hex::decode(parts[1]) {
239            Ok(part) => part,
240            Err(_) => {
241                return Err(ParseError {
242                    address: raw.to_owned(),
243                    reason: "Invalid raw address string: failed to decode hash part",
244                });
245            }
246        };
247
248        if hash_part.len() != 32 {
249            return Err(ParseError {
250                address: raw.to_owned(),
251                reason: "Invalid raw address string: hash part length must be 32 bytes",
252            });
253        }
254
255        Ok(Self {
256            workchain: wc,
257            hash_part: hash_part.as_slice().try_into().expect(
258                "checking for hash part length ensures that the slice is safely cast to an array",
259            ),
260        })
261    }
262
263    /// Decodes the base64 address of the Ton network into an [`Address`] structure.
264    ///
265    /// If the `encoder` argument is specified, the method decodes the address “strictly”
266    /// according to the specified algorithm.
267    /// Otherwise, the address algorithm will be guessed by the presence of base64 control
268    /// characters.
269    pub fn from_base64<S>(
270        address: S,
271        encoder: Option<Base64Decoder>,
272    ) -> Result<EncoderResult, ParseError>
273    where
274        S: AsRef<str>,
275    {
276        let address = address.as_ref();
277
278        if address.len() != 48 {
279            return Err(ParseError {
280                address: address.to_owned(),
281                reason: "Invalid base64 address string: length must be 48 characters",
282            });
283        }
284
285        let encoder = encoder.unwrap_or_else(|| Base64Decoder::guess(address));
286        let bytes = encoder.decode(address)?;
287
288        if bytes.len() != 36 {
289            return Err(ParseError {
290                address: address.to_owned(),
291                reason: "Invalid base64 address string: length of decoded bytes must be 36",
292            });
293        }
294
295        let (non_production, non_bounceable) = match bytes[0] {
296            0x11 => (false, false),
297            0x51 => (false, true),
298            0x91 => (true, false),
299            0xD1 => (true, true),
300            _ => {
301                return Err(ParseError {
302                    address: address.to_owned(),
303                    reason: "Invalid base64 address string: invalid flag",
304                });
305            }
306        };
307
308        let workchain = bytes[1] as i32;
309
310        let server_crc = crc16(&bytes[0..34]);
311        let client_crc = ((bytes[34] as u16) << 8) | (bytes[35] as u16);
312
313        if server_crc != client_crc {
314            return Err(ParseError {
315                address: address.to_owned(),
316                reason: "Invalid base64 address string: CRC16 hashes do not match",
317            });
318        }
319
320        let mut hash_part: HashPart = [0u8; 32];
321        hash_part.clone_from_slice(&bytes[2..34]);
322
323        Ok(EncoderResult {
324            address: Address {
325                workchain,
326                hash_part,
327            },
328            non_bounceable,
329            non_production,
330            decoder: encoder,
331        })
332    }
333
334    /// Converts the current structure to a string of the form “0:fa16bc...”
335    /// also known as the “raw address”.
336    pub fn to_raw_address(&self) -> String {
337        format!("{}:{}", self.workchain, hex::encode(self.hash_part))
338    }
339
340    /// Converts the current structure to a Base64 string according to
341    /// the specified preferences in the `encoder` argument.
342    ///
343    /// Use the [`BASE64_STD_DEFAULT`] and [`BASE64_URL_DEFAULT`] constants for fast conversion.
344    pub fn to_base64(&self, encoder: Base64Encoder) -> String {
345        encoder.encode(self.workchain, &self.hash_part)
346    }
347}
348
349impl FromStr for Address {
350    type Err = ParseError;
351
352    fn from_str(s: &str) -> Result<Self, Self::Err> {
353        if s.contains(':') {
354            Address::from_raw_address(s)
355        } else {
356            Ok(Address::from_base64(s, None)?.address)
357        }
358    }
359}
360
361impl TryFrom<String> for Address {
362    type Error = ParseError;
363
364    fn try_from(value: String) -> Result<Self, Self::Error> {
365        if value.contains(':') {
366            Address::from_raw_address(&value)
367        } else {
368            Ok(Address::from_base64(&value, None)?.address)
369        }
370    }
371}
372
373impl Display for Address {
374    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
375        f.write_str(self.to_base64(BASE64_URL_DEFAULT).as_str())
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_new_address() {
385        let bytes = hex::decode("e4d954ef9f4e1250a26b5bbad76a1cdd17cfd08babad6f4c23e372270aef6f76")
386            .unwrap();
387        let hash_part: HashPart = bytes.as_slice().try_into().unwrap();
388        let workchain = 0;
389
390        let address = Address::new(workchain, &hash_part);
391        assert_eq!(address.get_workchain(), workchain);
392        assert_eq!(
393            address.get_hash_part(),
394            &[
395                0xe4, 0xd9, 0x54, 0xef, 0x9f, 0x4e, 0x12, 0x50, 0xa2, 0x6b, 0x5b, 0xba, 0xd7, 0x6a,
396                0x1c, 0xdd, 0x17, 0xcf, 0xd0, 0x8b, 0xab, 0xad, 0x6f, 0x4c, 0x23, 0xe3, 0x72, 0x27,
397                0x0a, 0xef, 0x6f, 0x76
398            ]
399        );
400    }
401
402    #[test]
403    fn test_new_address_empty() {
404        let address = Address::empty();
405
406        assert_eq!(address.get_workchain(), 0);
407        assert_eq!(address.get_hash_part(), &[0u8; 32]);
408    }
409
410    #[test]
411    fn test_new_address_from_raw_adress() {
412        // main case
413        {
414            let raw_address = "0:e4d954ef9f4e1250a26b5bbad76a1cdd17cfd08babad6f4c23e372270aef6f76";
415            let address = Address::from_raw_address(raw_address);
416
417            assert_eq!(
418                address,
419                Ok(Address::new(
420                    0,
421                    &[
422                        0xe4, 0xd9, 0x54, 0xef, 0x9f, 0x4e, 0x12, 0x50, 0xa2, 0x6b, 0x5b, 0xba,
423                        0xd7, 0x6a, 0x1c, 0xdd, 0x17, 0xcf, 0xd0, 0x8b, 0xab, 0xad, 0x6f, 0x4c,
424                        0x23, 0xe3, 0x72, 0x27, 0x0a, 0xef, 0x6f, 0x76
425                    ]
426                ))
427            );
428        }
429
430        // error cases
431        {
432            let raw_address = "bad_string";
433            let address = Address::from_raw_address(raw_address);
434
435            assert_eq!(
436                address,
437                Err(ParseError {
438                    address: raw_address.to_owned(),
439                    reason: "Invalid raw address string: wrong address format",
440                })
441            );
442        }
443
444        {
445            let raw_address = "fdfd:fdfd";
446            let address = Address::from_raw_address(raw_address);
447
448            assert_eq!(
449                address,
450                Err(ParseError {
451                    address: raw_address.to_owned(),
452                    reason: "Invalid raw address string: workchain number is not a 32-bit integer",
453                })
454            );
455        }
456
457        {
458            let raw_address = "0:][p][;cr3244";
459            let address = Address::from_raw_address(raw_address);
460
461            assert_eq!(
462                address,
463                Err(ParseError {
464                    address: raw_address.to_owned(),
465                    reason: "Invalid raw address string: failed to decode hash part",
466                })
467            );
468        }
469
470        {
471            let raw_address = "0:ABCDE012";
472            let address = Address::from_raw_address(raw_address);
473
474            assert_eq!(
475                address,
476                Err(ParseError {
477                    address: raw_address.to_owned(),
478                    reason: "Invalid raw address string: hash part length must be 32 bytes",
479                })
480            );
481        }
482    }
483
484    #[test]
485    fn test_from_base64() {
486        // main case (1): [bounceable] + [production] + [encoder guessing]
487        {
488            let result =
489                Address::from_base64("EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR", None)
490                    .unwrap();
491
492            // Encoder result
493            assert_eq!(result.is_bounceable(), true);
494            assert_eq!(result.is_production(), true);
495            assert_eq!(result.decoder, Base64Decoder::UrlSafe);
496
497            // Address
498            assert_eq!(result.address.get_workchain(), 0);
499            assert_eq!(
500                result.address.get_hash_part(),
501                &[
502                    228, 217, 84, 239, 159, 78, 18, 80, 162, 107, 91, 186, 215, 106, 28, 221, 23,
503                    207, 208, 139, 171, 173, 111, 76, 35, 227, 114, 39, 10, 239, 111, 118
504                ]
505            );
506        }
507
508        // main case (2): [non bounceable] + [production] + [encoder guessing]
509        {
510            let result =
511                Address::from_base64("UQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgZ8t", None)
512                    .unwrap();
513
514            // Encoder result
515            assert_eq!(result.is_bounceable(), false);
516            assert_eq!(result.is_production(), true);
517            assert_eq!(result.decoder, Base64Decoder::Standard);
518
519            // Address
520            assert_eq!(result.address.get_workchain(), 0);
521            assert_eq!(
522                result.address.get_hash_part(),
523                &[
524                    22u8, 204, 66, 156, 118, 124, 164, 189, 119, 212, 54, 139, 170, 117, 46, 182,
525                    182, 250, 233, 223, 102, 194, 198, 226, 146, 233, 228, 43, 75, 162, 18, 129
526                ]
527            );
528        }
529
530        // error case (1): bad length
531        {
532            let result = Address::from_base64("bad length", None);
533            assert_eq!(
534                result,
535                Err(ParseError {
536                    address: "bad length".to_owned(),
537                    reason: "Invalid base64 address string: length must be 48 characters"
538                })
539            );
540        }
541
542        // error case (2): byte length
543        {
544            let result =
545                Address::from_base64("EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrRIyM", None);
546            assert_eq!(
547                result,
548                Err(ParseError {
549                    address: "EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrRIyM".to_owned(),
550                    reason: "Invalid base64 address string: length must be 48 characters"
551                })
552            );
553        }
554
555        // error case (3): invalid flag
556        {
557            let result =
558                Address::from_base64("VQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR", None);
559            assert_eq!(
560                result,
561                Err(ParseError {
562                    address: "VQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR".to_owned(),
563                    reason: "Invalid base64 address string: invalid flag"
564                })
565            );
566        }
567
568        // error case (3): bad CRC16
569        {
570            let result =
571                Address::from_base64("EQDkqlTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR", None);
572            assert_eq!(
573                result,
574                Err(ParseError {
575                    address: "EQDkqlTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR".to_owned(),
576                    reason: "Invalid base64 address string: CRC16 hashes do not match"
577                })
578            );
579        }
580    }
581
582    #[test]
583    fn test_compare_addresses() {
584        // case (1): same addresses
585        {
586            let address1 =
587                Address::from_base64("UQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgZ8t", None)
588                    .unwrap()
589                    .address;
590
591            let address2 =
592                Address::from_base64("UQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgZ8t", None)
593                    .unwrap()
594                    .address;
595
596            assert_eq!(address1, address2);
597        }
598
599        // case (2): not same
600        {
601            let address1 =
602                Address::from_base64("UQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgZ8t", None)
603                    .unwrap()
604                    .address;
605
606            let address2 =
607                Address::from_base64("EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR", None)
608                    .unwrap()
609                    .address;
610
611            assert_ne!(address1, address2);
612        }
613    }
614
615    #[test]
616    fn test_multi_converts() {
617        // case (1): from base64 url safe
618        {
619            let addr = "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
620                .parse::<Address>()
621                .unwrap();
622
623            assert_eq!(
624                addr.to_raw_address(),
625                "0:0e97797708411c29a3cb1f3f810ef4f83f41d990838f7f93ce7082c4ff9aa026"
626            );
627            assert_eq!(
628                addr.to_base64(BASE64_STD_DEFAULT),
629                "EQAOl3l3CEEcKaPLHz+BDvT4P0HZkIOPf5POcILE/5qgJuR2"
630            );
631            assert_eq!(
632                addr.to_base64(BASE64_URL_DEFAULT),
633                "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
634            );
635            assert_eq!(
636                addr.to_string(),
637                "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
638            );
639        }
640
641        // case (2): from base64 url std
642        {
643            let addr = "EQAOl3l3CEEcKaPLHz+BDvT4P0HZkIOPf5POcILE/5qgJuR2"
644                .parse::<Address>()
645                .unwrap();
646
647            assert_eq!(
648                addr.to_raw_address(),
649                "0:0e97797708411c29a3cb1f3f810ef4f83f41d990838f7f93ce7082c4ff9aa026"
650            );
651            assert_eq!(
652                addr.to_base64(BASE64_STD_DEFAULT),
653                "EQAOl3l3CEEcKaPLHz+BDvT4P0HZkIOPf5POcILE/5qgJuR2"
654            );
655            assert_eq!(
656                addr.to_base64(BASE64_URL_DEFAULT),
657                "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
658            );
659            assert_eq!(
660                addr.to_string(),
661                "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
662            );
663        }
664
665        // case (2): from raw address
666        {
667            let addr = "0:0e97797708411c29a3cb1f3f810ef4f83f41d990838f7f93ce7082c4ff9aa026"
668                .parse::<Address>()
669                .unwrap();
670
671            assert_eq!(
672                addr.to_raw_address(),
673                "0:0e97797708411c29a3cb1f3f810ef4f83f41d990838f7f93ce7082c4ff9aa026"
674            );
675            assert_eq!(
676                addr.to_base64(BASE64_STD_DEFAULT),
677                "EQAOl3l3CEEcKaPLHz+BDvT4P0HZkIOPf5POcILE/5qgJuR2"
678            );
679            assert_eq!(
680                addr.to_base64(BASE64_URL_DEFAULT),
681                "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
682            );
683            assert_eq!(
684                addr.to_string(),
685                "EQAOl3l3CEEcKaPLHz-BDvT4P0HZkIOPf5POcILE_5qgJuR2"
686            );
687        }
688    }
689}