near_account_id/
account_id_ref.rs

1use std::borrow::Cow;
2
3use crate::{AccountId, ParseAccountError};
4
5/// Account identifier. This is the human readable UTF-8 string which is used internally to index
6/// accounts on the network and their respective state.
7///
8/// This is the "referenced" version of the account ID. It is to [`AccountId`] what [`str`] is to [`String`],
9/// and works quite similarly to [`Path`]. Like with [`str`] and [`Path`], you
10/// can't have a value of type `AccountIdRef`, but you can have a reference like `&AccountIdRef` or
11/// `&mut AccountIdRef`.
12///
13/// This type supports zero-copy deserialization offered by [`serde`](https://docs.rs/serde/), but cannot
14/// do the same for [`borsh`](https://docs.rs/borsh/) since the latter does not support zero-copy.
15///
16/// # Examples
17/// ```
18/// use near_account_id::{AccountId, AccountIdRef};
19/// use std::convert::{TryFrom, TryInto};
20///
21/// // Construction
22/// let alice = AccountIdRef::new("alice.near").unwrap();
23/// assert!(AccountIdRef::new("invalid.").is_err());
24/// ```
25///
26/// [`FromStr`]: std::str::FromStr
27/// [`Path`]: std::path::Path
28#[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/// Enum representing possible types of accounts.
34/// This `enum` is returned by the [`get_account_type`] method on [`AccountIdRef`].
35/// See its documentation for more.
36///
37/// [`get_account_type`]: AccountIdRef::get_account_type
38/// [`AccountIdRef`]: struct.AccountIdRef.html
39#[derive(PartialEq)]
40pub enum AccountType {
41    /// Any valid account, that is neither NEAR-implicit nor ETH-implicit.
42    NamedAccount,
43    /// An account with 64 characters long hexadecimal address.
44    NearImplicitAccount,
45    /// An account which address starts with '0x', followed by 40 hex characters.
46    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    /// Shortest valid length for a NEAR Account ID.
61    pub const MIN_LEN: usize = crate::validation::MIN_LEN;
62    /// Longest valid length for a NEAR Account ID.
63    pub const MAX_LEN: usize = crate::validation::MAX_LEN;
64
65    /// Construct a [`&AccountIdRef`](AccountIdRef) from a string reference.
66    ///
67    /// This constructor validates the provided ID, and will produce an error when validation fails.
68    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        // Safety:
73        // - a newtype struct is guaranteed to have the same memory layout as its only field
74        // - the borrow checker will enforce its rules appropriately on the resulting reference
75        Ok(unsafe { &*(id as *const str as *const Self) })
76    }
77
78    /// Construct a [`&AccountIdRef`](AccountIdRef) from with validation at compile time.
79    /// This constructor will panic if validation fails.
80    /// ```rust
81    /// use near_account_id::AccountIdRef;
82    /// const ALICE: &AccountIdRef = AccountIdRef::new_or_panic("alice.near");
83    /// ```
84    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    /// Construct a [`&AccountIdRef`](AccountIdRef) from a string reference without validating the address.
91    /// It is the responsibility of the caller to ensure the account ID is valid.
92    ///
93    /// For more information, read: <https://docs.near.org/docs/concepts/account#account-id-rules>
94    pub(crate) fn new_unvalidated<S: AsRef<str> + ?Sized>(id: &S) -> &Self {
95        let id = id.as_ref();
96        // In nearcore, due to legacy reasons, AccountId construction and validation are separated.
97        // In order to avoid protocol change, `internal_unstable` feature was implemented and it is
98        // expected that AccountId might be invalid and it will be explicitly validated at the
99        // later stage.
100        #[cfg(not(feature = "internal_unstable"))]
101        debug_assert!(crate::validation::validate(id).is_ok());
102
103        // Safety: see `AccountIdRef::new`
104        unsafe { &*(id as *const str as *const Self) }
105    }
106
107    /// Returns a reference to the account ID bytes.
108    pub fn as_bytes(&self) -> &[u8] {
109        self.0.as_bytes()
110    }
111
112    /// Returns a string slice of the entire Account ID.
113    ///
114    /// ## Examples
115    ///
116    /// ```
117    /// use near_account_id::AccountIdRef;
118    ///
119    /// let carol = AccountIdRef::new("carol.near").unwrap();
120    /// assert_eq!("carol.near", carol.as_str());
121    /// ```
122    pub fn as_str(&self) -> &str {
123        &self.0
124    }
125
126    /// Returns `true` if the account ID is a top-level NEAR Account ID.
127    ///
128    /// See [Top-level Accounts](https://docs.near.org/docs/concepts/account#top-level-accounts).
129    ///
130    /// ## Examples
131    ///
132    /// ```
133    /// use near_account_id::AccountIdRef;
134    ///
135    /// let near_tla = AccountIdRef::new("near").unwrap();
136    /// assert!(near_tla.is_top_level());
137    ///
138    /// // "alice.near" is a sub account of "near" account
139    /// let alice = AccountIdRef::new("alice.near").unwrap();
140    /// assert!(!alice.is_top_level());
141    /// ```
142    pub fn is_top_level(&self) -> bool {
143        !self.is_system() && !self.0.contains('.')
144    }
145
146    /// Returns `true` if the `AccountId` is a direct sub-account of the provided parent account.
147    ///
148    /// See [Subaccounts](https://docs.near.org/docs/concepts/account#subaccounts).
149    ///
150    /// ## Examples
151    ///
152    /// ```
153    /// use near_account_id::AccountId;
154    ///
155    /// let near_tla: AccountId = "near".parse().unwrap();
156    /// assert!(near_tla.is_top_level());
157    ///
158    /// let alice: AccountId = "alice.near".parse().unwrap();
159    /// assert!(alice.is_sub_account_of(&near_tla));
160    ///
161    /// let alice_app: AccountId = "app.alice.near".parse().unwrap();
162    ///
163    /// // While app.alice.near is a sub account of alice.near,
164    /// // app.alice.near is not a sub account of near
165    /// assert!(alice_app.is_sub_account_of(&alice));
166    /// assert!(!alice_app.is_sub_account_of(&near_tla));
167    /// ```
168    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    /// Returns `AccountType::EthImplicitAccount` if the `AccountId` is a 40 characters long hexadecimal prefixed with '0x'.
176    /// Returns `AccountType::NearImplicitAccount` if the `AccountId` is a 64 characters long hexadecimal.
177    /// Otherwise, returns `AccountType::NamedAccount`.
178    ///
179    /// See [Implicit-Accounts](https://docs.near.org/docs/concepts/account#implicit-accounts).
180    ///
181    /// ## Examples
182    ///
183    /// ```
184    /// use near_account_id::{AccountId, AccountType};
185    ///
186    /// let alice: AccountId = "alice.near".parse().unwrap();
187    /// assert!(alice.get_account_type() == AccountType::NamedAccount);
188    ///
189    /// let eth_rando = "0xb794f5ea0ba39494ce839613fffba74279579268"
190    ///     .parse::<AccountId>()
191    ///     .unwrap();
192    /// assert!(eth_rando.get_account_type() == AccountType::EthImplicitAccount);
193    ///
194    /// let near_rando = "98793cd91a3f870fb126f66285808c7e094afcfc4eda8a970f6648cdf0dbd6de"
195    ///     .parse::<AccountId>()
196    ///     .unwrap();
197    /// assert!(near_rando.get_account_type() == AccountType::NearImplicitAccount);
198    /// ```
199    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    /// Returns `true` if this `AccountId` is the system account.
210    ///
211    /// See [System account](https://nomicon.io/DataStructures/Account.html?highlight=system#system-account).
212    ///
213    /// ## Examples
214    ///
215    /// ```
216    /// use near_account_id::AccountId;
217    ///
218    /// let alice: AccountId = "alice.near".parse().unwrap();
219    /// assert!(!alice.is_system());
220    ///
221    /// let system: AccountId = "system".parse().unwrap();
222    /// assert!(system.is_system());
223    /// ```
224    pub fn is_system(&self) -> bool {
225        self == "system"
226    }
227
228    /// Returns the length of the underlying account id string.
229    pub const fn len(&self) -> usize {
230        self.0.len()
231    }
232
233    /// Returns parent's account id reference
234    ///
235    /// ## Examples
236    /// ```
237    /// use near_account_id::AccountIdRef;
238    ///
239    /// let alice: &AccountIdRef = AccountIdRef::new_or_panic("alice.near");
240    /// let parent: &AccountIdRef = alice.get_parent_account_id().unwrap();
241    ///
242    /// assert!(alice.is_sub_account_of(parent));
243    ///
244    /// let near: &AccountIdRef = AccountIdRef::new_or_panic("near");
245    ///
246    /// assert!(near.get_parent_account_id().is_none());
247    ///
248    /// let implicit: &AccountIdRef = AccountIdRef::new_or_panic("248e104d1d4764d713c4211c13808c8fc887869c580f4178e60538ac5c2a0b26");
249    ///
250    /// assert!(implicit.get_parent_account_id().is_none());
251    /// ```
252    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            // ETH-implicit account
543            "0xb794f5ea0ba39494ce839613fffba74279579268",
544            // NEAR-implicit account
545            "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", // fancy ƒ!
557            "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            // Valid regex and length, but reserved
591            "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                // ETH-implicit account
672                "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}