1use std::borrow::Cow;
2
3use crate::{AccountId, ParseAccountError};
4#[cfg(feature = "schemars")]
5use crate::schemars_exports::schemars;
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")]
458 fn test_schemars() {
459 let schema = schemars::schema_for!(AccountIdRef);
460 let json_schema = serde_json::to_value(&schema).unwrap();
461 assert_eq!(
462 json_schema,
463 serde_json::json!({
464 "$schema": "http://json-schema.org/draft-07/schema#",
465 "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",
466 "title": "AccountIdRef",
467 "type": "string"
468 }
469 )
470 );
471 }
472
473 #[test]
474 fn test_err_kind_classification() {
475 let id = AccountIdRef::new("ErinMoriarty.near");
476 debug_assert!(
477 matches!(
478 id,
479 Err(ParseAccountError {
480 kind: ParseErrorKind::InvalidChar,
481 char: Some((0, 'E'))
482 })
483 ),
484 "{:?}",
485 id
486 );
487
488 let id = AccountIdRef::new("-KarlUrban.near");
489 debug_assert!(
490 matches!(
491 id,
492 Err(ParseAccountError {
493 kind: ParseErrorKind::RedundantSeparator,
494 char: Some((0, '-'))
495 })
496 ),
497 "{:?}",
498 id
499 );
500
501 let id = AccountIdRef::new("anthonystarr.");
502 debug_assert!(
503 matches!(
504 id,
505 Err(ParseAccountError {
506 kind: ParseErrorKind::RedundantSeparator,
507 char: Some((12, '.'))
508 })
509 ),
510 "{:?}",
511 id
512 );
513
514 let id = AccountIdRef::new("jack__Quaid.near");
515 debug_assert!(
516 matches!(
517 id,
518 Err(ParseAccountError {
519 kind: ParseErrorKind::RedundantSeparator,
520 char: Some((5, '_'))
521 })
522 ),
523 "{:?}",
524 id
525 );
526 }
527
528 #[test]
529 fn test_is_valid_top_level_account_id() {
530 let ok_top_level_account_ids = &[
531 "aa",
532 "a-a",
533 "a-aa",
534 "100",
535 "0o",
536 "com",
537 "near",
538 "bowen",
539 "b-o_w_e-n",
540 "0o0ooo00oo00o",
541 "alex-skidanov",
542 "b-o_w_e-n",
543 "no_lols",
544 "0xb794f5ea0ba39494ce839613fffba74279579268",
546 "0123456789012345678901234567890123456789012345678901234567890123",
548 ];
549 for account_id in ok_top_level_account_ids {
550 assert!(
551 AccountIdRef::new(account_id).map_or(false, |account_id| account_id.is_top_level()),
552 "Valid top level account id {:?} marked invalid",
553 account_id
554 );
555 }
556
557 let bad_top_level_account_ids = &[
558 "ƒelicia.near", "near.a",
560 "b.owen",
561 "bro.wen",
562 "a.ha",
563 "a.b-a.ra",
564 "some-complex-address@gmail.com",
565 "sub.buy_d1gitz@atata@b0-rg.c_0_m",
566 "over.9000",
567 "google.com",
568 "illia.cheapaccounts.near",
569 "10-4.8-2",
570 "a",
571 "A",
572 "Abc",
573 "-near",
574 "near-",
575 "-near-",
576 "near.",
577 ".near",
578 "near@",
579 "@near",
580 "неар",
581 "@@@@@",
582 "0__0",
583 "0_-_0",
584 "0_-_0",
585 "..",
586 "a..near",
587 "nEar",
588 "_bowen",
589 "hello world",
590 "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz",
591 "01234567890123456789012345678901234567890123456789012345678901234",
592 "system",
594 ];
595 for account_id in bad_top_level_account_ids {
596 assert!(
597 !AccountIdRef::new(account_id)
598 .map_or(false, |account_id| account_id.is_top_level()),
599 "Invalid top level account id {:?} marked valid",
600 account_id
601 );
602 }
603 }
604
605 #[test]
606 fn test_is_valid_sub_account_id() {
607 let ok_pairs = &[
608 ("test", "a.test"),
609 ("test-me", "abc.test-me"),
610 ("gmail.com", "abc.gmail.com"),
611 ("gmail.com", "abc-lol.gmail.com"),
612 ("gmail.com", "abc_lol.gmail.com"),
613 ("gmail.com", "bro-abc_lol.gmail.com"),
614 ("g0", "0g.g0"),
615 ("1g", "1g.1g"),
616 ("5-3", "4_2.5-3"),
617 ];
618 for (signer_id, sub_account_id) in ok_pairs {
619 assert!(
620 matches!(
621 (AccountIdRef::new(signer_id), AccountIdRef::new(sub_account_id)),
622 (Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(signer_id)
623 ),
624 "Failed to create sub-account {:?} by account {:?}",
625 sub_account_id,
626 signer_id
627 );
628 }
629
630 let bad_pairs = &[
631 ("test", ".test"),
632 ("test", "test"),
633 ("test", "a1.a.test"),
634 ("test", "est"),
635 ("test", ""),
636 ("test", "st"),
637 ("test5", "ббб"),
638 ("test", "a-test"),
639 ("test", "etest"),
640 ("test", "a.etest"),
641 ("test", "retest"),
642 ("test-me", "abc-.test-me"),
643 ("test-me", "Abc.test-me"),
644 ("test-me", "-abc.test-me"),
645 ("test-me", "a--c.test-me"),
646 ("test-me", "a_-c.test-me"),
647 ("test-me", "a-_c.test-me"),
648 ("test-me", "_abc.test-me"),
649 ("test-me", "abc_.test-me"),
650 ("test-me", "..test-me"),
651 ("test-me", "a..test-me"),
652 ("gmail.com", "a.abc@gmail.com"),
653 ("gmail.com", ".abc@gmail.com"),
654 ("gmail.com", ".abc@gmail@com"),
655 ("gmail.com", "abc@gmail@com"),
656 ("test", "a@test"),
657 ("test_me", "abc@test_me"),
658 ("gmail.com", "abc@gmail.com"),
659 ("gmail@com", "abc.gmail@com"),
660 ("gmail.com", "abc-lol@gmail.com"),
661 ("gmail@com", "abc_lol.gmail@com"),
662 ("gmail@com", "bro-abc_lol.gmail@com"),
663 (
664 "gmail.com",
665 "123456789012345678901234567890123456789012345678901234567890@gmail.com",
666 ),
667 (
668 "123456789012345678901234567890123456789012345678901234567890",
669 "1234567890.123456789012345678901234567890123456789012345678901234567890",
670 ),
671 (
672 "b794f5ea0ba39494ce839613fffba74279579268",
673 "0xb794f5ea0ba39494ce839613fffba74279579268",
675 ),
676 ("aa", "ъ@aa"),
677 ("aa", "ъ.aa"),
678 ];
679 for (signer_id, sub_account_id) in bad_pairs {
680 assert!(
681 !matches!(
682 (AccountIdRef::new(signer_id), AccountIdRef::new(sub_account_id)),
683 (Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(&signer_id)
684 ),
685 "Invalid sub-account {:?} created by account {:?}",
686 sub_account_id,
687 signer_id
688 );
689 }
690 }
691
692 #[test]
693 fn test_is_account_id_near_implicit() {
694 let valid_near_implicit_account_ids = &[
695 "0000000000000000000000000000000000000000000000000000000000000000",
696 "6174617461746174617461746174617461746174617461746174617461746174",
697 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
698 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
699 "20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667",
700 ];
701 for valid_account_id in valid_near_implicit_account_ids {
702 assert!(
703 matches!(
704 AccountIdRef::new(valid_account_id),
705 Ok(account_id) if account_id.get_account_type() == AccountType::NearImplicitAccount
706 ),
707 "Account ID {} should be valid 64-len hex",
708 valid_account_id
709 );
710 }
711
712 let invalid_near_implicit_account_ids = &[
713 "000000000000000000000000000000000000000000000000000000000000000",
714 "6.74617461746174617461746174617461746174617461746174617461746174",
715 "012-456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
716 "fffff_ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
717 "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
718 "00000000000000000000000000000000000000000000000000000000000000",
719 ];
720 for invalid_account_id in invalid_near_implicit_account_ids {
721 assert!(
722 !matches!(
723 AccountIdRef::new(invalid_account_id),
724 Ok(account_id) if account_id.get_account_type() == AccountType::NearImplicitAccount
725 ),
726 "Account ID {} is not a NEAR-implicit account",
727 invalid_account_id
728 );
729 }
730 }
731
732 #[test]
733 fn test_is_account_id_eth_implicit() {
734 let valid_eth_implicit_account_ids = &[
735 "0x0000000000000000000000000000000000000000",
736 "0x6174617461746174617461746174617461746174",
737 "0x0123456789abcdef0123456789abcdef01234567",
738 "0xffffffffffffffffffffffffffffffffffffffff",
739 "0x20782e20662e64666420482123494b6b6c677573",
740 ];
741 for valid_account_id in valid_eth_implicit_account_ids {
742 assert!(
743 matches!(
744 valid_account_id.parse::<AccountId>(),
745 Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
746 ),
747 "Account ID {} should be valid 42-len hex, starting with 0x",
748 valid_account_id
749 );
750 }
751
752 let invalid_eth_implicit_account_ids = &[
753 "04b794f5ea0ba39494ce839613fffba74279579268",
754 "0x000000000000000000000000000000000000000",
755 "0x6.74617461746174617461746174617461746174",
756 "0x012-456789abcdef0123456789abcdef01234567",
757 "0xfffff_ffffffffffffffffffffffffffffffffff",
758 "0xoooooooooooooooooooooooooooooooooooooooo",
759 "0x00000000000000000000000000000000000000000",
760 "0000000000000000000000000000000000000000000000000000000000000000",
761 ];
762 for invalid_account_id in invalid_eth_implicit_account_ids {
763 assert!(
764 !matches!(
765 invalid_account_id.parse::<AccountId>(),
766 Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
767 ),
768 "Account ID {} is not an ETH-implicit account",
769 invalid_account_id
770 );
771 }
772 }
773
774 #[test]
775 #[cfg(feature = "arbitrary")]
776 fn test_arbitrary() {
777 let corpus = [
778 ("a|bcd", None),
779 ("ab|cde", Some("ab")),
780 ("a_-b", None),
781 ("ab_-c", Some("ab")),
782 ("a", None),
783 ("miraclx.near", Some("miraclx.near")),
784 (
785 "01234567890123456789012345678901234567890123456789012345678901234",
786 None,
787 ),
788 ];
789
790 for (input, expected_output) in corpus {
791 assert!(input.len() <= u8::MAX as usize);
792 let data = [input.as_bytes(), &[input.len() as _]].concat();
793 let mut u = arbitrary::Unstructured::new(&data);
794
795 assert_eq!(
796 u.arbitrary::<&AccountIdRef>()
797 .ok()
798 .map(AsRef::<str>::as_ref),
799 expected_output
800 );
801 }
802 }
803}