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
12pub const BASE64_STD_DEFAULT: Base64Encoder = Base64Encoder::Standard {
15 bounceable: true,
16 production: true,
17};
18
19pub 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#[derive(Debug, PartialEq)]
41pub enum Base64Decoder {
42 Standard,
47
48 UrlSafe,
53}
54
55impl Base64Decoder {
56 #[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 #[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 Base64Decoder::Standard
86 }
87}
88
89#[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#[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 fn eq(&self, other: &Self) -> bool {
168 self.address == other.address
169 }
170}
171
172#[derive(Debug, PartialEq)]
178pub struct Address {
179 workchain: Workchain,
180 hash_part: HashPart,
181}
182
183impl Address {
184 pub fn new(workchain: Workchain, hash_part: &HashPart) -> Self {
186 Self {
187 workchain,
188 hash_part: *hash_part,
189 }
190 }
191
192 pub const fn empty() -> Self {
195 Self {
196 workchain: 0,
197 hash_part: [0u8; 32],
198 }
199 }
200
201 pub fn get_workchain(&self) -> i32 {
203 self.workchain
204 }
205
206 pub fn get_hash_part(&self) -> &HashPart {
208 &self.hash_part
209 }
210
211 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 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 pub fn to_raw_address(&self) -> String {
337 format!("{}:{}", self.workchain, hex::encode(self.hash_part))
338 }
339
340 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 {
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 {
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 {
488 let result =
489 Address::from_base64("EQDk2VTvn04SUKJrW7rXahzdF8_Qi6utb0wj43InCu9vdjrR", None)
490 .unwrap();
491
492 assert_eq!(result.is_bounceable(), true);
494 assert_eq!(result.is_production(), true);
495 assert_eq!(result.decoder, Base64Decoder::UrlSafe);
496
497 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 {
510 let result =
511 Address::from_base64("UQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgZ8t", None)
512 .unwrap();
513
514 assert_eq!(result.is_bounceable(), false);
516 assert_eq!(result.is_production(), true);
517 assert_eq!(result.decoder, Base64Decoder::Standard);
518
519 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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}