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