1use std::borrow::Cow;
2
3#[cfg(feature = "schemars")]
4use crate::schemars_exports::schemars;
5use crate::{AccountId, ParseAccountError};
6
7#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
31#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
32#[cfg_attr(feature = "abi", derive(borsh::BorshSchema))]
33pub struct AccountIdRef(pub(crate) str);
34
35#[derive(PartialEq)]
42pub enum AccountType {
43 NamedAccount,
45 NearImplicitAccount,
47 EthImplicitAccount,
49}
50
51impl AccountType {
52 pub fn is_implicit(&self) -> bool {
53 match &self {
54 Self::NearImplicitAccount => true,
55 Self::EthImplicitAccount => true,
56 Self::NamedAccount => false,
57 }
58 }
59}
60
61impl AccountIdRef {
62 pub const MIN_LEN: usize = crate::validation::MIN_LEN;
64 pub const MAX_LEN: usize = crate::validation::MAX_LEN;
66
67 pub fn new<S: AsRef<str> + ?Sized>(id: &S) -> Result<&Self, ParseAccountError> {
71 let id = id.as_ref();
72 crate::validation::validate(id)?;
73
74 Ok(unsafe { &*(id as *const str as *const Self) })
78 }
79
80 pub const fn new_or_panic(id: &str) -> &Self {
87 crate::validation::validate_const(id);
88
89 unsafe { &*(id as *const str as *const Self) }
90 }
91
92 pub(crate) fn new_unvalidated<S: AsRef<str> + ?Sized>(id: &S) -> &Self {
97 let id = id.as_ref();
98 #[cfg(not(feature = "internal_unstable"))]
103 debug_assert!(crate::validation::validate(id).is_ok());
104
105 unsafe { &*(id as *const str as *const Self) }
107 }
108
109 pub fn as_bytes(&self) -> &[u8] {
111 self.0.as_bytes()
112 }
113
114 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127
128 pub fn is_top_level(&self) -> bool {
145 !self.is_system() && !self.0.contains('.')
146 }
147
148 pub fn is_sub_account_of(&self, parent: &AccountIdRef) -> bool {
171 self.0
172 .strip_suffix(parent.as_str())
173 .and_then(|s| s.strip_suffix('.'))
174 .map_or(false, |s| !s.contains('.'))
175 }
176
177 pub fn get_account_type(&self) -> AccountType {
202 if crate::validation::is_eth_implicit(self.as_str()) {
203 return AccountType::EthImplicitAccount;
204 }
205 if crate::validation::is_near_implicit(self.as_str()) {
206 return AccountType::NearImplicitAccount;
207 }
208 AccountType::NamedAccount
209 }
210
211 pub fn is_system(&self) -> bool {
227 self == "system"
228 }
229
230 pub const fn len(&self) -> usize {
232 self.0.len()
233 }
234
235 pub fn get_parent_account_id(&self) -> Option<&AccountIdRef> {
255 let parent_str = self.as_str().split_once('.')?.1;
256 Some(AccountIdRef::new_unvalidated(parent_str))
257 }
258}
259
260impl std::fmt::Display for AccountIdRef {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 std::fmt::Display::fmt(&self.0, f)
263 }
264}
265
266impl ToOwned for AccountIdRef {
267 type Owned = AccountId;
268
269 fn to_owned(&self) -> Self::Owned {
270 AccountId(self.0.into())
271 }
272}
273
274impl<'a> From<&'a AccountIdRef> for AccountId {
275 fn from(id: &'a AccountIdRef) -> Self {
276 id.to_owned()
277 }
278}
279
280impl<'s> TryFrom<&'s str> for &'s AccountIdRef {
281 type Error = ParseAccountError;
282
283 fn try_from(value: &'s str) -> Result<Self, Self::Error> {
284 AccountIdRef::new(value)
285 }
286}
287
288impl AsRef<str> for AccountIdRef {
289 fn as_ref(&self) -> &str {
290 &self.0
291 }
292}
293
294impl PartialEq<AccountIdRef> for String {
295 fn eq(&self, other: &AccountIdRef) -> bool {
296 self == &other.0
297 }
298}
299
300impl PartialEq<String> for AccountIdRef {
301 fn eq(&self, other: &String) -> bool {
302 &self.0 == other
303 }
304}
305
306impl PartialEq<AccountIdRef> for str {
307 fn eq(&self, other: &AccountIdRef) -> bool {
308 self == &other.0
309 }
310}
311
312impl PartialEq<str> for AccountIdRef {
313 fn eq(&self, other: &str) -> bool {
314 &self.0 == other
315 }
316}
317
318impl<'a> PartialEq<AccountIdRef> for &'a str {
319 fn eq(&self, other: &AccountIdRef) -> bool {
320 *self == &other.0
321 }
322}
323
324impl<'a> PartialEq<&'a str> for AccountIdRef {
325 fn eq(&self, other: &&'a str) -> bool {
326 &self.0 == *other
327 }
328}
329
330impl<'a> PartialEq<&'a AccountIdRef> for str {
331 fn eq(&self, other: &&'a AccountIdRef) -> bool {
332 self == &other.0
333 }
334}
335
336impl<'a> PartialEq<str> for &'a AccountIdRef {
337 fn eq(&self, other: &str) -> bool {
338 &self.0 == other
339 }
340}
341
342impl<'a> PartialEq<&'a AccountIdRef> for String {
343 fn eq(&self, other: &&'a AccountIdRef) -> bool {
344 self == &other.0
345 }
346}
347
348impl<'a> PartialEq<String> for &'a AccountIdRef {
349 fn eq(&self, other: &String) -> bool {
350 &self.0 == other
351 }
352}
353
354impl PartialOrd<AccountIdRef> for String {
355 fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
356 self.as_str().partial_cmp(&other.0)
357 }
358}
359
360impl PartialOrd<String> for AccountIdRef {
361 fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
362 self.0.partial_cmp(other.as_str())
363 }
364}
365
366impl PartialOrd<AccountIdRef> for str {
367 fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
368 self.partial_cmp(other.as_str())
369 }
370}
371
372impl PartialOrd<str> for AccountIdRef {
373 fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
374 self.as_str().partial_cmp(other)
375 }
376}
377
378impl<'a> PartialOrd<AccountIdRef> for &'a str {
379 fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
380 self.partial_cmp(&other.as_str())
381 }
382}
383
384impl<'a> PartialOrd<&'a str> for AccountIdRef {
385 fn partial_cmp(&self, other: &&'a str) -> Option<std::cmp::Ordering> {
386 self.as_str().partial_cmp(*other)
387 }
388}
389
390impl<'a> PartialOrd<&'a AccountIdRef> for String {
391 fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
392 self.as_str().partial_cmp(&other.0)
393 }
394}
395
396impl<'a> PartialOrd<String> for &'a AccountIdRef {
397 fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
398 self.0.partial_cmp(other.as_str())
399 }
400}
401
402impl<'a> PartialOrd<&'a AccountIdRef> for str {
403 fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
404 self.partial_cmp(other.as_str())
405 }
406}
407
408impl<'a> PartialOrd<str> for &'a AccountIdRef {
409 fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
410 self.as_str().partial_cmp(other)
411 }
412}
413
414impl<'a> From<&'a AccountIdRef> for Cow<'a, AccountIdRef> {
415 fn from(value: &'a AccountIdRef) -> Self {
416 Cow::Borrowed(value)
417 }
418}
419
420#[cfg(feature = "arbitrary")]
421impl<'a> arbitrary::Arbitrary<'a> for &'a AccountIdRef {
422 fn size_hint(_depth: usize) -> (usize, Option<usize>) {
423 (crate::validation::MIN_LEN, Some(crate::validation::MAX_LEN))
424 }
425
426 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
427 let mut s = u.arbitrary::<&str>()?;
428
429 loop {
430 match AccountIdRef::new(s) {
431 Ok(account_id) => break Ok(account_id),
432 Err(ParseAccountError {
433 char: Some((idx, _)),
434 ..
435 }) => {
436 s = &s[..idx];
437 continue;
438 }
439 _ => break Err(arbitrary::Error::IncorrectFormat),
440 }
441 }
442 }
443
444 fn arbitrary_take_rest(u: arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
445 let s = <&str as arbitrary::Arbitrary>::arbitrary_take_rest(u)?;
446 AccountIdRef::new(s).map_err(|_| arbitrary::Error::IncorrectFormat)
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use crate::ParseErrorKind;
453
454 use super::*;
455
456 #[test]
457 #[cfg(feature = "schemars-v1")]
458 fn test_schemars() {
459 let schema = schemars::schema_for!(AccountIdRef);
460 let json_schema = serde_json::to_value(&schema).unwrap();
461 dbg!(&json_schema);
462 assert_eq!(
463 json_schema,
464 serde_json::json!({
465 "$schema": "https://json-schema.org/draft/2020-12/schema",
466 "description": "Account identifier. This is the human readable UTF-8 string which is used internally to index\naccounts on the network and their respective state.\n\nThis is the \"referenced\" version of the account ID. It is to [`AccountId`] what [`str`] is to [`String`],\nand works quite similarly to [`Path`]. Like with [`str`] and [`Path`], you\ncan't have a value of type `AccountIdRef`, but you can have a reference like `&AccountIdRef` or\n`&mut AccountIdRef`.\n\nThis type supports zero-copy deserialization offered by [`serde`](https://docs.rs/serde/), but cannot\ndo the same for [`borsh`](https://docs.rs/borsh/) since the latter does not support zero-copy.\n\n# Examples\n```\nuse near_account_id::{AccountId, AccountIdRef};\nuse std::convert::{TryFrom, TryInto};\n\n// Construction\nlet alice = AccountIdRef::new(\"alice.near\").unwrap();\nassert!(AccountIdRef::new(\"invalid.\").is_err());\n```\n\n[`FromStr`]: std::str::FromStr\n[`Path`]: std::path::Path",
467 "title": "AccountIdRef",
468 "type": "string"
469 }
470 )
471 );
472 }
473
474 #[test]
475 #[cfg(feature = "schemars-v0_8")]
476 fn test_schemars() {
477 let schema = schemars::schema_for!(AccountIdRef);
478 let json_schema = serde_json::to_value(&schema).unwrap();
479 dbg!(&json_schema);
480 assert_eq!(
481 json_schema,
482 serde_json::json!({
483 "$schema": "http://json-schema.org/draft-07/schema#",
484 "description": "Account identifier. This is the human readable UTF-8 string which is used internally to index accounts on the network and their respective state.\n\nThis is the \"referenced\" version of the account ID. It is to [`AccountId`] what [`str`] is to [`String`], and works quite similarly to [`Path`]. Like with [`str`] and [`Path`], you can't have a value of type `AccountIdRef`, but you can have a reference like `&AccountIdRef` or `&mut AccountIdRef`.\n\nThis type supports zero-copy deserialization offered by [`serde`](https://docs.rs/serde/), but cannot do the same for [`borsh`](https://docs.rs/borsh/) since the latter does not support zero-copy.\n\n# Examples ``` use near_account_id::{AccountId, AccountIdRef}; use std::convert::{TryFrom, TryInto};\n\n// Construction let alice = AccountIdRef::new(\"alice.near\").unwrap(); assert!(AccountIdRef::new(\"invalid.\").is_err()); ```\n\n[`FromStr`]: std::str::FromStr [`Path`]: std::path::Path",
485 "title": "AccountIdRef",
486 "type": "string"
487 }
488 )
489 );
490 }
491
492 #[test]
493 fn test_err_kind_classification() {
494 let id = AccountIdRef::new("ErinMoriarty.near");
495 debug_assert!(
496 matches!(
497 id,
498 Err(ParseAccountError {
499 kind: ParseErrorKind::InvalidChar,
500 char: Some((0, 'E'))
501 })
502 ),
503 "{:?}",
504 id
505 );
506
507 let id = AccountIdRef::new("-KarlUrban.near");
508 debug_assert!(
509 matches!(
510 id,
511 Err(ParseAccountError {
512 kind: ParseErrorKind::RedundantSeparator,
513 char: Some((0, '-'))
514 })
515 ),
516 "{:?}",
517 id
518 );
519
520 let id = AccountIdRef::new("anthonystarr.");
521 debug_assert!(
522 matches!(
523 id,
524 Err(ParseAccountError {
525 kind: ParseErrorKind::RedundantSeparator,
526 char: Some((12, '.'))
527 })
528 ),
529 "{:?}",
530 id
531 );
532
533 let id = AccountIdRef::new("jack__Quaid.near");
534 debug_assert!(
535 matches!(
536 id,
537 Err(ParseAccountError {
538 kind: ParseErrorKind::RedundantSeparator,
539 char: Some((5, '_'))
540 })
541 ),
542 "{:?}",
543 id
544 );
545 }
546
547 #[test]
548 fn test_is_valid_top_level_account_id() {
549 let ok_top_level_account_ids = &[
550 "aa",
551 "a-a",
552 "a-aa",
553 "100",
554 "0o",
555 "com",
556 "near",
557 "bowen",
558 "b-o_w_e-n",
559 "0o0ooo00oo00o",
560 "alex-skidanov",
561 "b-o_w_e-n",
562 "no_lols",
563 "0xb794f5ea0ba39494ce839613fffba74279579268",
565 "0123456789012345678901234567890123456789012345678901234567890123",
567 ];
568 for account_id in ok_top_level_account_ids {
569 assert!(
570 AccountIdRef::new(account_id).map_or(false, |account_id| account_id.is_top_level()),
571 "Valid top level account id {:?} marked invalid",
572 account_id
573 );
574 }
575
576 let bad_top_level_account_ids = &[
577 "ƒelicia.near", "near.a",
579 "b.owen",
580 "bro.wen",
581 "a.ha",
582 "a.b-a.ra",
583 "some-complex-address@gmail.com",
584 "sub.buy_d1gitz@atata@b0-rg.c_0_m",
585 "over.9000",
586 "google.com",
587 "illia.cheapaccounts.near",
588 "10-4.8-2",
589 "a",
590 "A",
591 "Abc",
592 "-near",
593 "near-",
594 "-near-",
595 "near.",
596 ".near",
597 "near@",
598 "@near",
599 "неар",
600 "@@@@@",
601 "0__0",
602 "0_-_0",
603 "0_-_0",
604 "..",
605 "a..near",
606 "nEar",
607 "_bowen",
608 "hello world",
609 "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz",
610 "01234567890123456789012345678901234567890123456789012345678901234",
611 "system",
613 ];
614 for account_id in bad_top_level_account_ids {
615 assert!(
616 !AccountIdRef::new(account_id)
617 .map_or(false, |account_id| account_id.is_top_level()),
618 "Invalid top level account id {:?} marked valid",
619 account_id
620 );
621 }
622 }
623
624 #[test]
625 fn test_is_valid_sub_account_id() {
626 let ok_pairs = &[
627 ("test", "a.test"),
628 ("test-me", "abc.test-me"),
629 ("gmail.com", "abc.gmail.com"),
630 ("gmail.com", "abc-lol.gmail.com"),
631 ("gmail.com", "abc_lol.gmail.com"),
632 ("gmail.com", "bro-abc_lol.gmail.com"),
633 ("g0", "0g.g0"),
634 ("1g", "1g.1g"),
635 ("5-3", "4_2.5-3"),
636 ];
637 for (signer_id, sub_account_id) in ok_pairs {
638 assert!(
639 matches!(
640 (AccountIdRef::new(signer_id), AccountIdRef::new(sub_account_id)),
641 (Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(signer_id)
642 ),
643 "Failed to create sub-account {:?} by account {:?}",
644 sub_account_id,
645 signer_id
646 );
647 }
648
649 let bad_pairs = &[
650 ("test", ".test"),
651 ("test", "test"),
652 ("test", "a1.a.test"),
653 ("test", "est"),
654 ("test", ""),
655 ("test", "st"),
656 ("test5", "ббб"),
657 ("test", "a-test"),
658 ("test", "etest"),
659 ("test", "a.etest"),
660 ("test", "retest"),
661 ("test-me", "abc-.test-me"),
662 ("test-me", "Abc.test-me"),
663 ("test-me", "-abc.test-me"),
664 ("test-me", "a--c.test-me"),
665 ("test-me", "a_-c.test-me"),
666 ("test-me", "a-_c.test-me"),
667 ("test-me", "_abc.test-me"),
668 ("test-me", "abc_.test-me"),
669 ("test-me", "..test-me"),
670 ("test-me", "a..test-me"),
671 ("gmail.com", "a.abc@gmail.com"),
672 ("gmail.com", ".abc@gmail.com"),
673 ("gmail.com", ".abc@gmail@com"),
674 ("gmail.com", "abc@gmail@com"),
675 ("test", "a@test"),
676 ("test_me", "abc@test_me"),
677 ("gmail.com", "abc@gmail.com"),
678 ("gmail@com", "abc.gmail@com"),
679 ("gmail.com", "abc-lol@gmail.com"),
680 ("gmail@com", "abc_lol.gmail@com"),
681 ("gmail@com", "bro-abc_lol.gmail@com"),
682 (
683 "gmail.com",
684 "123456789012345678901234567890123456789012345678901234567890@gmail.com",
685 ),
686 (
687 "123456789012345678901234567890123456789012345678901234567890",
688 "1234567890.123456789012345678901234567890123456789012345678901234567890",
689 ),
690 (
691 "b794f5ea0ba39494ce839613fffba74279579268",
692 "0xb794f5ea0ba39494ce839613fffba74279579268",
694 ),
695 ("aa", "ъ@aa"),
696 ("aa", "ъ.aa"),
697 ];
698 for (signer_id, sub_account_id) in bad_pairs {
699 assert!(
700 !matches!(
701 (AccountIdRef::new(signer_id), AccountIdRef::new(sub_account_id)),
702 (Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(&signer_id)
703 ),
704 "Invalid sub-account {:?} created by account {:?}",
705 sub_account_id,
706 signer_id
707 );
708 }
709 }
710
711 #[test]
712 fn test_is_account_id_near_implicit() {
713 let valid_near_implicit_account_ids = &[
714 "0000000000000000000000000000000000000000000000000000000000000000",
715 "6174617461746174617461746174617461746174617461746174617461746174",
716 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
717 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
718 "20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667",
719 ];
720 for valid_account_id in valid_near_implicit_account_ids {
721 assert!(
722 matches!(
723 AccountIdRef::new(valid_account_id),
724 Ok(account_id) if account_id.get_account_type() == AccountType::NearImplicitAccount
725 ),
726 "Account ID {} should be valid 64-len hex",
727 valid_account_id
728 );
729 }
730
731 let invalid_near_implicit_account_ids = &[
732 "000000000000000000000000000000000000000000000000000000000000000",
733 "6.74617461746174617461746174617461746174617461746174617461746174",
734 "012-456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
735 "fffff_ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
736 "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
737 "00000000000000000000000000000000000000000000000000000000000000",
738 ];
739 for invalid_account_id in invalid_near_implicit_account_ids {
740 assert!(
741 !matches!(
742 AccountIdRef::new(invalid_account_id),
743 Ok(account_id) if account_id.get_account_type() == AccountType::NearImplicitAccount
744 ),
745 "Account ID {} is not a NEAR-implicit account",
746 invalid_account_id
747 );
748 }
749 }
750
751 #[test]
752 fn test_is_account_id_eth_implicit() {
753 let valid_eth_implicit_account_ids = &[
754 "0x0000000000000000000000000000000000000000",
755 "0x6174617461746174617461746174617461746174",
756 "0x0123456789abcdef0123456789abcdef01234567",
757 "0xffffffffffffffffffffffffffffffffffffffff",
758 "0x20782e20662e64666420482123494b6b6c677573",
759 ];
760 for valid_account_id in valid_eth_implicit_account_ids {
761 assert!(
762 matches!(
763 valid_account_id.parse::<AccountId>(),
764 Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
765 ),
766 "Account ID {} should be valid 42-len hex, starting with 0x",
767 valid_account_id
768 );
769 }
770
771 let invalid_eth_implicit_account_ids = &[
772 "04b794f5ea0ba39494ce839613fffba74279579268",
773 "0x000000000000000000000000000000000000000",
774 "0x6.74617461746174617461746174617461746174",
775 "0x012-456789abcdef0123456789abcdef01234567",
776 "0xfffff_ffffffffffffffffffffffffffffffffff",
777 "0xoooooooooooooooooooooooooooooooooooooooo",
778 "0x00000000000000000000000000000000000000000",
779 "0000000000000000000000000000000000000000000000000000000000000000",
780 ];
781 for invalid_account_id in invalid_eth_implicit_account_ids {
782 assert!(
783 !matches!(
784 invalid_account_id.parse::<AccountId>(),
785 Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
786 ),
787 "Account ID {} is not an ETH-implicit account",
788 invalid_account_id
789 );
790 }
791 }
792
793 #[test]
794 #[cfg(feature = "arbitrary")]
795 fn test_arbitrary() {
796 let corpus = [
797 ("a|bcd", None),
798 ("ab|cde", Some("ab")),
799 ("a_-b", None),
800 ("ab_-c", Some("ab")),
801 ("a", None),
802 ("miraclx.near", Some("miraclx.near")),
803 (
804 "01234567890123456789012345678901234567890123456789012345678901234",
805 None,
806 ),
807 ];
808
809 for (input, expected_output) in corpus {
810 assert!(input.len() <= u8::MAX as usize);
811 let data = [input.as_bytes(), &[input.len() as _]].concat();
812 let mut u = arbitrary::Unstructured::new(&data);
813
814 assert_eq!(
815 u.arbitrary::<&AccountIdRef>()
816 .ok()
817 .map(AsRef::<str>::as_ref),
818 expected_output
819 );
820 }
821 }
822}