near_account_id/
account_id_ref.rs

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