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 = "abi", derive(borsh::BorshSchema))]
30pub struct AccountIdRef(pub(crate) str);
31
32/// Enum representing possible types of accounts.
33/// This `enum` is returned by the [`get_account_type`] method on [`AccountIdRef`].
34/// See its documentation for more.
35///
36/// [`get_account_type`]: AccountIdRef::get_account_type
37/// [`AccountIdRef`]: struct.AccountIdRef.html
38#[derive(PartialEq)]
39pub enum AccountType {
40    /// Any valid account, that is neither NEAR-implicit nor ETH-implicit.
41    NamedAccount,
42    /// An account with 64 characters long hexadecimal address.
43    NearImplicitAccount,
44    /// An account which address starts with '0x', followed by 40 hex characters.
45    EthImplicitAccount,
46    /// An account which address starts with '0s', followed by 40 hex characters.
47    ///
48    /// See [NEP-616](https://github.com/near/NEPs/pull/616/) for more details.
49    NearDeterministicAccount,
50}
51
52impl AccountType {
53    pub fn is_implicit(&self) -> bool {
54        match &self {
55            Self::NearImplicitAccount => true,
56            Self::EthImplicitAccount => true,
57            Self::NearDeterministicAccount => true,
58            Self::NamedAccount => false,
59        }
60    }
61}
62
63impl AccountIdRef {
64    /// Shortest valid length for a NEAR Account ID.
65    pub const MIN_LEN: usize = crate::validation::MIN_LEN;
66    /// Longest valid length for a NEAR Account ID.
67    pub const MAX_LEN: usize = crate::validation::MAX_LEN;
68
69    /// Construct a [`&AccountIdRef`](AccountIdRef) from a string reference.
70    ///
71    /// This constructor validates the provided ID, and will produce an error when validation fails.
72    pub fn new<S: AsRef<str> + ?Sized>(id: &S) -> Result<&Self, ParseAccountError> {
73        let id = id.as_ref();
74        crate::validation::validate(id)?;
75
76        // Safety:
77        // - a newtype struct is guaranteed to have the same memory layout as its only field
78        // - the borrow checker will enforce its rules appropriately on the resulting reference
79        Ok(unsafe { &*(id as *const str as *const Self) })
80    }
81
82    /// Construct a [`&AccountIdRef`](AccountIdRef) from with validation at compile time.
83    /// This constructor will panic if validation fails.
84    /// ```rust
85    /// use near_account_id::AccountIdRef;
86    /// const ALICE: &AccountIdRef = AccountIdRef::new_or_panic("alice.near");
87    /// ```
88    pub const fn new_or_panic(id: &str) -> &Self {
89        crate::validation::validate_const(id);
90
91        unsafe { &*(id as *const str as *const Self) }
92    }
93
94    /// Construct a [`&AccountIdRef`](AccountIdRef) from a string reference without validating the address.
95    /// It is the responsibility of the caller to ensure the account ID is valid.
96    ///
97    /// For more information, read: <https://docs.near.org/docs/concepts/account#account-id-rules>
98    pub(crate) fn new_unvalidated<S: AsRef<str> + ?Sized>(id: &S) -> &Self {
99        let id = id.as_ref();
100        // In nearcore, due to legacy reasons, AccountId construction and validation are separated.
101        // In order to avoid protocol change, `internal_unstable` feature was implemented and it is
102        // expected that AccountId might be invalid and it will be explicitly validated at the
103        // later stage.
104        #[cfg(not(feature = "internal_unstable"))]
105        debug_assert!(crate::validation::validate(id).is_ok());
106
107        // Safety: see `AccountIdRef::new`
108        unsafe { &*(id as *const str as *const Self) }
109    }
110
111    /// Returns a reference to the account ID bytes.
112    pub fn as_bytes(&self) -> &[u8] {
113        self.0.as_bytes()
114    }
115
116    /// Returns a string slice of the entire Account ID.
117    ///
118    /// ## Examples
119    ///
120    /// ```
121    /// use near_account_id::AccountIdRef;
122    ///
123    /// let carol = AccountIdRef::new("carol.near").unwrap();
124    /// assert_eq!("carol.near", carol.as_str());
125    /// ```
126    pub fn as_str(&self) -> &str {
127        &self.0
128    }
129
130    /// Returns `true` if the account ID is a top-level NEAR Account ID.
131    ///
132    /// See [Top-level Accounts](https://docs.near.org/docs/concepts/account#top-level-accounts).
133    ///
134    /// ## Examples
135    ///
136    /// ```
137    /// use near_account_id::AccountIdRef;
138    ///
139    /// let near_tla = AccountIdRef::new("near").unwrap();
140    /// assert!(near_tla.is_top_level());
141    ///
142    /// // "alice.near" is a sub account of "near" account
143    /// let alice = AccountIdRef::new("alice.near").unwrap();
144    /// assert!(!alice.is_top_level());
145    /// ```
146    pub fn is_top_level(&self) -> bool {
147        !self.is_system() && !self.0.contains('.')
148    }
149
150    /// Returns `true` if the `AccountId` is a direct sub-account of the provided parent account.
151    ///
152    /// See [Subaccounts](https://docs.near.org/docs/concepts/account#subaccounts).
153    ///
154    /// ## Examples
155    ///
156    /// ```
157    /// use near_account_id::AccountId;
158    ///
159    /// let near_tla: AccountId = "near".parse().unwrap();
160    /// assert!(near_tla.is_top_level());
161    ///
162    /// let alice: AccountId = "alice.near".parse().unwrap();
163    /// assert!(alice.is_sub_account_of(&near_tla));
164    ///
165    /// let alice_app: AccountId = "app.alice.near".parse().unwrap();
166    ///
167    /// // While app.alice.near is a sub account of alice.near,
168    /// // app.alice.near is not a sub account of near
169    /// assert!(alice_app.is_sub_account_of(&alice));
170    /// assert!(!alice_app.is_sub_account_of(&near_tla));
171    /// ```
172    pub fn is_sub_account_of(&self, parent: &AccountIdRef) -> bool {
173        self.0
174            .strip_suffix(parent.as_str())
175            .and_then(|s| s.strip_suffix('.'))
176            .map_or(false, |s| !s.contains('.'))
177    }
178
179    /// Returns `AccountType::EthImplicitAccount` if the `AccountId` is a 40 characters long hexadecimal prefixed with '0x'.
180    /// Returns `AccountType::NearImplicitAccount` if the `AccountId` is a 64 characters long hexadecimal.
181    /// Otherwise, returns `AccountType::NamedAccount`.
182    ///
183    /// See [Implicit-Accounts](https://docs.near.org/docs/concepts/account#implicit-accounts).
184    ///
185    /// ## Examples
186    ///
187    /// ```
188    /// use near_account_id::{AccountId, AccountType};
189    ///
190    /// let alice: AccountId = "alice.near".parse().unwrap();
191    /// assert!(alice.get_account_type() == AccountType::NamedAccount);
192    ///
193    /// let eth_rando = "0xb794f5ea0ba39494ce839613fffba74279579268"
194    ///     .parse::<AccountId>()
195    ///     .unwrap();
196    /// assert!(eth_rando.get_account_type() == AccountType::EthImplicitAccount);
197    ///
198    /// let near_rando = "98793cd91a3f870fb126f66285808c7e094afcfc4eda8a970f6648cdf0dbd6de"
199    ///     .parse::<AccountId>()
200    ///     .unwrap();
201    /// assert!(near_rando.get_account_type() == AccountType::NearImplicitAccount);
202    /// ```
203    pub fn get_account_type(&self) -> AccountType {
204        if crate::validation::is_eth_implicit(self.as_str()) {
205            return AccountType::EthImplicitAccount;
206        }
207        if crate::validation::is_near_implicit(self.as_str()) {
208            return AccountType::NearImplicitAccount;
209        }
210        if crate::validation::is_near_deterministic(self.as_str()) {
211            return AccountType::NearDeterministicAccount;
212        }
213        AccountType::NamedAccount
214    }
215
216    /// Returns `true` if this `AccountId` is the system account.
217    ///
218    /// See [System account](https://nomicon.io/DataStructures/Account.html?highlight=system#system-account).
219    ///
220    /// ## Examples
221    ///
222    /// ```
223    /// use near_account_id::AccountId;
224    ///
225    /// let alice: AccountId = "alice.near".parse().unwrap();
226    /// assert!(!alice.is_system());
227    ///
228    /// let system: AccountId = "system".parse().unwrap();
229    /// assert!(system.is_system());
230    /// ```
231    pub fn is_system(&self) -> bool {
232        self == "system"
233    }
234
235    /// Returns the length of the underlying account id string.
236    pub const fn len(&self) -> usize {
237        self.0.len()
238    }
239
240    /// Returns parent's account id reference
241    ///
242    /// ## Examples
243    /// ```
244    /// use near_account_id::AccountIdRef;
245    ///
246    /// let alice: &AccountIdRef = AccountIdRef::new_or_panic("alice.near");
247    /// let parent: &AccountIdRef = alice.get_parent_account_id().unwrap();
248    ///
249    /// assert!(alice.is_sub_account_of(parent));
250    ///
251    /// let near: &AccountIdRef = AccountIdRef::new_or_panic("near");
252    ///
253    /// assert!(near.get_parent_account_id().is_none());
254    ///
255    /// let implicit: &AccountIdRef = AccountIdRef::new_or_panic("248e104d1d4764d713c4211c13808c8fc887869c580f4178e60538ac5c2a0b26");
256    ///
257    /// assert!(implicit.get_parent_account_id().is_none());
258    /// ```
259    pub fn get_parent_account_id(&self) -> Option<&AccountIdRef> {
260        let parent_str = self.as_str().split_once('.')?.1;
261        Some(AccountIdRef::new_unvalidated(parent_str))
262    }
263}
264
265impl std::fmt::Display for AccountIdRef {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        std::fmt::Display::fmt(&self.0, f)
268    }
269}
270
271impl ToOwned for AccountIdRef {
272    type Owned = AccountId;
273
274    fn to_owned(&self) -> Self::Owned {
275        AccountId(self.0.into())
276    }
277}
278
279impl<'a> From<&'a AccountIdRef> for AccountId {
280    fn from(id: &'a AccountIdRef) -> Self {
281        id.to_owned()
282    }
283}
284
285impl<'s> TryFrom<&'s str> for &'s AccountIdRef {
286    type Error = ParseAccountError;
287
288    fn try_from(value: &'s str) -> Result<Self, Self::Error> {
289        AccountIdRef::new(value)
290    }
291}
292
293impl AsRef<str> for AccountIdRef {
294    fn as_ref(&self) -> &str {
295        &self.0
296    }
297}
298
299impl PartialEq<AccountIdRef> for String {
300    fn eq(&self, other: &AccountIdRef) -> bool {
301        self == &other.0
302    }
303}
304
305impl PartialEq<String> for AccountIdRef {
306    fn eq(&self, other: &String) -> bool {
307        &self.0 == other
308    }
309}
310
311impl PartialEq<AccountIdRef> for str {
312    fn eq(&self, other: &AccountIdRef) -> bool {
313        self == &other.0
314    }
315}
316
317impl PartialEq<str> for AccountIdRef {
318    fn eq(&self, other: &str) -> bool {
319        &self.0 == other
320    }
321}
322
323impl<'a> PartialEq<AccountIdRef> for &'a str {
324    fn eq(&self, other: &AccountIdRef) -> bool {
325        *self == &other.0
326    }
327}
328
329impl<'a> PartialEq<&'a str> for AccountIdRef {
330    fn eq(&self, other: &&'a str) -> bool {
331        &self.0 == *other
332    }
333}
334
335impl<'a> PartialEq<&'a AccountIdRef> for str {
336    fn eq(&self, other: &&'a AccountIdRef) -> bool {
337        self == &other.0
338    }
339}
340
341impl<'a> PartialEq<str> for &'a AccountIdRef {
342    fn eq(&self, other: &str) -> bool {
343        &self.0 == other
344    }
345}
346
347impl<'a> PartialEq<&'a AccountIdRef> for String {
348    fn eq(&self, other: &&'a AccountIdRef) -> bool {
349        self == &other.0
350    }
351}
352
353impl<'a> PartialEq<String> for &'a AccountIdRef {
354    fn eq(&self, other: &String) -> bool {
355        &self.0 == other
356    }
357}
358
359impl PartialOrd<AccountIdRef> for String {
360    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
361        self.as_str().partial_cmp(&other.0)
362    }
363}
364
365impl PartialOrd<String> for AccountIdRef {
366    fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
367        self.0.partial_cmp(other.as_str())
368    }
369}
370
371impl PartialOrd<AccountIdRef> for str {
372    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
373        self.partial_cmp(other.as_str())
374    }
375}
376
377impl PartialOrd<str> for AccountIdRef {
378    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
379        self.as_str().partial_cmp(other)
380    }
381}
382
383impl<'a> PartialOrd<AccountIdRef> for &'a str {
384    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
385        self.partial_cmp(&other.as_str())
386    }
387}
388
389impl<'a> PartialOrd<&'a str> for AccountIdRef {
390    fn partial_cmp(&self, other: &&'a str) -> Option<std::cmp::Ordering> {
391        self.as_str().partial_cmp(*other)
392    }
393}
394
395impl<'a> PartialOrd<&'a AccountIdRef> for String {
396    fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
397        self.as_str().partial_cmp(&other.0)
398    }
399}
400
401impl<'a> PartialOrd<String> for &'a AccountIdRef {
402    fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
403        self.0.partial_cmp(other.as_str())
404    }
405}
406
407impl<'a> PartialOrd<&'a AccountIdRef> for str {
408    fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
409        self.partial_cmp(other.as_str())
410    }
411}
412
413impl<'a> PartialOrd<str> for &'a AccountIdRef {
414    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
415        self.as_str().partial_cmp(other)
416    }
417}
418
419impl<'a> From<&'a AccountIdRef> for Cow<'a, AccountIdRef> {
420    fn from(value: &'a AccountIdRef) -> Self {
421        Cow::Borrowed(value)
422    }
423}
424
425#[cfg(feature = "schemars-v0_8")]
426impl schemars_v0_8::JsonSchema for AccountIdRef {
427    fn is_referenceable() -> bool {
428        false
429    }
430
431    fn schema_name() -> String {
432        "AccountIdRef".to_string()
433    }
434
435    fn json_schema(_: &mut schemars_v0_8::gen::SchemaGenerator) -> schemars_v0_8::schema::Schema {
436        use schemars_v0_8::schema::{InstanceType, Metadata, Schema, SchemaObject, SingleOrVec};
437        Schema::Object(SchemaObject {
438            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
439            metadata: Some(Box::new(Metadata {
440                description: Some("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".to_string()),
441                ..Default::default()
442            })),
443            ..Default::default()
444        })
445    }
446}
447
448#[cfg(feature = "schemars-v1")]
449impl schemars_v1::JsonSchema for AccountIdRef {
450    fn schema_name() -> std::borrow::Cow<'static, str> {
451        "AccountIdRef".to_string().into()
452    }
453
454    fn json_schema(_: &mut schemars_v1::SchemaGenerator) -> schemars_v1::Schema {
455        schemars_v1::json_schema!({
456            "$schema": "https://json-schema.org/draft/2020-12/schema",
457            "description": "Account identifier. This is the human readable UTF-8 string which is used internally to index\naccounts on the network and their respective state.\n\nThis is the \"referenced\" version of the account ID. It is to [`AccountId`] what [`str`] is to [`String`],\nand works quite similarly to [`Path`]. Like with [`str`] and [`Path`], you\ncan't have a value of type `AccountIdRef`, but you can have a reference like `&AccountIdRef` or\n`&mut AccountIdRef`.\n\nThis type supports zero-copy deserialization offered by [`serde`](https://docs.rs/serde/), but cannot\ndo the same for [`borsh`](https://docs.rs/borsh/) since the latter does not support zero-copy.\n\n# Examples\n```\nuse near_account_id::{AccountId, AccountIdRef};\nuse std::convert::{TryFrom, TryInto};\n\n// Construction\nlet alice = AccountIdRef::new(\"alice.near\").unwrap();\nassert!(AccountIdRef::new(\"invalid.\").is_err());\n```\n\n[`FromStr`]: std::str::FromStr\n[`Path`]: std::path::Path",
458            "title": "AccountIdRef",
459            "type": "string"
460        })
461    }
462}
463
464#[cfg(feature = "arbitrary")]
465impl<'a> arbitrary::Arbitrary<'a> for &'a AccountIdRef {
466    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
467        (crate::validation::MIN_LEN, Some(crate::validation::MAX_LEN))
468    }
469
470    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
471        let mut s = u.arbitrary::<&str>()?;
472
473        loop {
474            match AccountIdRef::new(s) {
475                Ok(account_id) => break Ok(account_id),
476                Err(ParseAccountError {
477                    char: Some((idx, _)),
478                    ..
479                }) => {
480                    s = &s[..idx];
481                    continue;
482                }
483                _ => break Err(arbitrary::Error::IncorrectFormat),
484            }
485        }
486    }
487
488    fn arbitrary_take_rest(u: arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
489        let s = <&str as arbitrary::Arbitrary>::arbitrary_take_rest(u)?;
490        AccountIdRef::new(s).map_err(|_| arbitrary::Error::IncorrectFormat)
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use crate::ParseErrorKind;
497
498    use super::*;
499
500    #[test]
501    #[cfg(feature = "schemars-v1")]
502    fn test_schemars_v1() {
503        let schema = schemars_v1::schema_for!(AccountIdRef);
504        let json_schema = serde_json::to_value(&schema).unwrap();
505        dbg!(&json_schema);
506        assert_eq!(
507            json_schema,
508            serde_json::json!({
509                    "$schema": "https://json-schema.org/draft/2020-12/schema",
510                    "description": "Account identifier. This is the human readable UTF-8 string which is used internally to index\naccounts on the network and their respective state.\n\nThis is the \"referenced\" version of the account ID. It is to [`AccountId`] what [`str`] is to [`String`],\nand works quite similarly to [`Path`]. Like with [`str`] and [`Path`], you\ncan't have a value of type `AccountIdRef`, but you can have a reference like `&AccountIdRef` or\n`&mut AccountIdRef`.\n\nThis type supports zero-copy deserialization offered by [`serde`](https://docs.rs/serde/), but cannot\ndo the same for [`borsh`](https://docs.rs/borsh/) since the latter does not support zero-copy.\n\n# Examples\n```\nuse near_account_id::{AccountId, AccountIdRef};\nuse std::convert::{TryFrom, TryInto};\n\n// Construction\nlet alice = AccountIdRef::new(\"alice.near\").unwrap();\nassert!(AccountIdRef::new(\"invalid.\").is_err());\n```\n\n[`FromStr`]: std::str::FromStr\n[`Path`]: std::path::Path",
511                    "title": "AccountIdRef",
512                    "type": "string"
513                }
514            )
515        );
516    }
517
518    #[test]
519    #[cfg(feature = "schemars-v0_8")]
520    fn test_schemars_v0_8() {
521        let schema = schemars_v0_8::schema_for!(AccountIdRef);
522        let json_schema = serde_json::to_value(&schema).unwrap();
523        dbg!(&json_schema);
524        assert_eq!(
525            json_schema,
526            serde_json::json!({
527                    "$schema": "http://json-schema.org/draft-07/schema#",
528                    "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",
529                    "title": "AccountIdRef",
530                    "type": "string"
531                }
532            )
533        );
534    }
535
536    #[test]
537    fn test_err_kind_classification() {
538        let id = AccountIdRef::new("ErinMoriarty.near");
539        debug_assert!(
540            matches!(
541                id,
542                Err(ParseAccountError {
543                    kind: ParseErrorKind::InvalidChar,
544                    char: Some((0, 'E'))
545                })
546            ),
547            "{:?}",
548            id
549        );
550
551        let id = AccountIdRef::new("-KarlUrban.near");
552        debug_assert!(
553            matches!(
554                id,
555                Err(ParseAccountError {
556                    kind: ParseErrorKind::RedundantSeparator,
557                    char: Some((0, '-'))
558                })
559            ),
560            "{:?}",
561            id
562        );
563
564        let id = AccountIdRef::new("anthonystarr.");
565        debug_assert!(
566            matches!(
567                id,
568                Err(ParseAccountError {
569                    kind: ParseErrorKind::RedundantSeparator,
570                    char: Some((12, '.'))
571                })
572            ),
573            "{:?}",
574            id
575        );
576
577        let id = AccountIdRef::new("jack__Quaid.near");
578        debug_assert!(
579            matches!(
580                id,
581                Err(ParseAccountError {
582                    kind: ParseErrorKind::RedundantSeparator,
583                    char: Some((5, '_'))
584                })
585            ),
586            "{:?}",
587            id
588        );
589    }
590
591    #[test]
592    fn test_is_valid_top_level_account_id() {
593        let ok_top_level_account_ids = &[
594            "aa",
595            "a-a",
596            "a-aa",
597            "100",
598            "0o",
599            "com",
600            "near",
601            "bowen",
602            "b-o_w_e-n",
603            "0o0ooo00oo00o",
604            "alex-skidanov",
605            "b-o_w_e-n",
606            "no_lols",
607            // ETH-implicit account
608            "0xb794f5ea0ba39494ce839613fffba74279579268",
609            // NEAR-implicit account
610            "0123456789012345678901234567890123456789012345678901234567890123",
611        ];
612        for account_id in ok_top_level_account_ids {
613            assert!(
614                AccountIdRef::new(account_id).map_or(false, |account_id| account_id.is_top_level()),
615                "Valid top level account id {:?} marked invalid",
616                account_id
617            );
618        }
619
620        let bad_top_level_account_ids = &[
621            "ƒelicia.near", // fancy ƒ!
622            "near.a",
623            "b.owen",
624            "bro.wen",
625            "a.ha",
626            "a.b-a.ra",
627            "some-complex-address@gmail.com",
628            "sub.buy_d1gitz@atata@b0-rg.c_0_m",
629            "over.9000",
630            "google.com",
631            "illia.cheapaccounts.near",
632            "10-4.8-2",
633            "a",
634            "A",
635            "Abc",
636            "-near",
637            "near-",
638            "-near-",
639            "near.",
640            ".near",
641            "near@",
642            "@near",
643            "неар",
644            "@@@@@",
645            "0__0",
646            "0_-_0",
647            "0_-_0",
648            "..",
649            "a..near",
650            "nEar",
651            "_bowen",
652            "hello world",
653            "abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz",
654            "01234567890123456789012345678901234567890123456789012345678901234",
655            // Valid regex and length, but reserved
656            "system",
657        ];
658        for account_id in bad_top_level_account_ids {
659            assert!(
660                !AccountIdRef::new(account_id)
661                    .map_or(false, |account_id| account_id.is_top_level()),
662                "Invalid top level account id {:?} marked valid",
663                account_id
664            );
665        }
666    }
667
668    #[test]
669    fn test_is_valid_sub_account_id() {
670        let ok_pairs = &[
671            ("test", "a.test"),
672            ("test-me", "abc.test-me"),
673            ("gmail.com", "abc.gmail.com"),
674            ("gmail.com", "abc-lol.gmail.com"),
675            ("gmail.com", "abc_lol.gmail.com"),
676            ("gmail.com", "bro-abc_lol.gmail.com"),
677            ("g0", "0g.g0"),
678            ("1g", "1g.1g"),
679            ("5-3", "4_2.5-3"),
680        ];
681        for (signer_id, sub_account_id) in ok_pairs {
682            assert!(
683                matches!(
684                    (AccountIdRef::new(signer_id), AccountIdRef::new(sub_account_id)),
685                    (Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(signer_id)
686                ),
687                "Failed to create sub-account {:?} by account {:?}",
688                sub_account_id,
689                signer_id
690            );
691        }
692
693        let bad_pairs = &[
694            ("test", ".test"),
695            ("test", "test"),
696            ("test", "a1.a.test"),
697            ("test", "est"),
698            ("test", ""),
699            ("test", "st"),
700            ("test5", "ббб"),
701            ("test", "a-test"),
702            ("test", "etest"),
703            ("test", "a.etest"),
704            ("test", "retest"),
705            ("test-me", "abc-.test-me"),
706            ("test-me", "Abc.test-me"),
707            ("test-me", "-abc.test-me"),
708            ("test-me", "a--c.test-me"),
709            ("test-me", "a_-c.test-me"),
710            ("test-me", "a-_c.test-me"),
711            ("test-me", "_abc.test-me"),
712            ("test-me", "abc_.test-me"),
713            ("test-me", "..test-me"),
714            ("test-me", "a..test-me"),
715            ("gmail.com", "a.abc@gmail.com"),
716            ("gmail.com", ".abc@gmail.com"),
717            ("gmail.com", ".abc@gmail@com"),
718            ("gmail.com", "abc@gmail@com"),
719            ("test", "a@test"),
720            ("test_me", "abc@test_me"),
721            ("gmail.com", "abc@gmail.com"),
722            ("gmail@com", "abc.gmail@com"),
723            ("gmail.com", "abc-lol@gmail.com"),
724            ("gmail@com", "abc_lol.gmail@com"),
725            ("gmail@com", "bro-abc_lol.gmail@com"),
726            (
727                "gmail.com",
728                "123456789012345678901234567890123456789012345678901234567890@gmail.com",
729            ),
730            (
731                "123456789012345678901234567890123456789012345678901234567890",
732                "1234567890.123456789012345678901234567890123456789012345678901234567890",
733            ),
734            (
735                "b794f5ea0ba39494ce839613fffba74279579268",
736                // ETH-implicit account
737                "0xb794f5ea0ba39494ce839613fffba74279579268",
738            ),
739            ("aa", "ъ@aa"),
740            ("aa", "ъ.aa"),
741        ];
742        for (signer_id, sub_account_id) in bad_pairs {
743            assert!(
744                !matches!(
745                    (AccountIdRef::new(signer_id), AccountIdRef::new(sub_account_id)),
746                    (Ok(signer_id), Ok(sub_account_id)) if sub_account_id.is_sub_account_of(&signer_id)
747                ),
748                "Invalid sub-account {:?} created by account {:?}",
749                sub_account_id,
750                signer_id
751            );
752        }
753    }
754
755    #[test]
756    fn test_is_account_id_near_implicit() {
757        let valid_near_implicit_account_ids = &[
758            "0000000000000000000000000000000000000000000000000000000000000000",
759            "6174617461746174617461746174617461746174617461746174617461746174",
760            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
761            "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
762            "20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667",
763        ];
764        for valid_account_id in valid_near_implicit_account_ids {
765            assert!(
766                matches!(
767                    AccountIdRef::new(valid_account_id),
768                    Ok(account_id) if account_id.get_account_type() == AccountType::NearImplicitAccount
769                ),
770                "Account ID {} should be valid 64-len hex",
771                valid_account_id
772            );
773        }
774
775        let invalid_near_implicit_account_ids = &[
776            "000000000000000000000000000000000000000000000000000000000000000",
777            "6.74617461746174617461746174617461746174617461746174617461746174",
778            "012-456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
779            "fffff_ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
780            "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
781            "00000000000000000000000000000000000000000000000000000000000000",
782        ];
783        for invalid_account_id in invalid_near_implicit_account_ids {
784            assert!(
785                !matches!(
786                    AccountIdRef::new(invalid_account_id),
787                    Ok(account_id) if account_id.get_account_type() == AccountType::NearImplicitAccount
788                ),
789                "Account ID {} is not a NEAR-implicit account",
790                invalid_account_id
791            );
792        }
793    }
794
795    #[test]
796    fn test_is_account_id_eth_implicit() {
797        let valid_eth_implicit_account_ids = &[
798            "0x0000000000000000000000000000000000000000",
799            "0x6174617461746174617461746174617461746174",
800            "0x0123456789abcdef0123456789abcdef01234567",
801            "0xffffffffffffffffffffffffffffffffffffffff",
802            "0x20782e20662e64666420482123494b6b6c677573",
803        ];
804        for valid_account_id in valid_eth_implicit_account_ids {
805            assert!(
806                matches!(
807                    valid_account_id.parse::<AccountId>(),
808                    Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
809                ),
810                "Account ID {} should be valid 42-len hex, starting with 0x",
811                valid_account_id
812            );
813        }
814
815        let invalid_eth_implicit_account_ids = &[
816            "04b794f5ea0ba39494ce839613fffba74279579268",
817            "0x000000000000000000000000000000000000000",
818            "0x6.74617461746174617461746174617461746174",
819            "0x012-456789abcdef0123456789abcdef01234567",
820            "0xfffff_ffffffffffffffffffffffffffffffffff",
821            "0xoooooooooooooooooooooooooooooooooooooooo",
822            "0x00000000000000000000000000000000000000000",
823            "0000000000000000000000000000000000000000000000000000000000000000",
824        ];
825        for invalid_account_id in invalid_eth_implicit_account_ids {
826            assert!(
827                !matches!(
828                    invalid_account_id.parse::<AccountId>(),
829                    Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
830                ),
831                "Account ID {} is not an ETH-implicit account",
832                invalid_account_id
833            );
834        }
835    }
836
837    #[test]
838    fn test_is_account_id_near_deterministic() {
839        let valid_near_deterministic_account_ids = &[
840            "0s0000000000000000000000000000000000000000",
841            "0s6174617461746174617461746174617461746174",
842            "0s0123456789abcdef0123456789abcdef01234567",
843            "0sffffffffffffffffffffffffffffffffffffffff",
844            "0s20782e20662e64666420482123494b6b6c677573",
845        ];
846        for valid_account_id in valid_near_deterministic_account_ids {
847            assert!(
848                matches!(
849                    valid_account_id.parse::<AccountId>(),
850                    Ok(account_id) if account_id.get_account_type() == AccountType::NearDeterministicAccount
851                ),
852                "Account ID {} should be valid 42-len hex, starting with 0s",
853                valid_account_id
854            );
855        }
856
857        let invalid_near_deterministic_account_ids = &[
858            "00s000000000000000000000000000000000000000",
859            "0s000000000000000000000000000000000000000",
860            "0s00000000000000000000000000000000000000.0",
861            "04b794f5ea0ba39494ce839613fffba74279579268",
862            "0x000000000000000000000000000000000000000",
863            "alice.near",
864            "0s.alice.near",
865            "0s.match_total_len_42_with_a_named_account",
866        ];
867        for invalid_account_id in invalid_near_deterministic_account_ids {
868            assert!(
869                !matches!(
870                    invalid_account_id.parse::<AccountId>(),
871                    Ok(account_id) if account_id.get_account_type() == AccountType::EthImplicitAccount
872                ),
873                "Account ID {} is not a NEAR deterministic account",
874                invalid_account_id
875            );
876        }
877    }
878
879    #[test]
880    #[cfg(feature = "arbitrary")]
881    fn test_arbitrary() {
882        let corpus = [
883            ("a|bcd", None),
884            ("ab|cde", Some("ab")),
885            ("a_-b", None),
886            ("ab_-c", Some("ab")),
887            ("a", None),
888            ("miraclx.near", Some("miraclx.near")),
889            (
890                "01234567890123456789012345678901234567890123456789012345678901234",
891                None,
892            ),
893        ];
894
895        for (input, expected_output) in corpus {
896            assert!(input.len() <= u8::MAX as usize);
897            let data = [input.as_bytes(), &[input.len() as _]].concat();
898            let mut u = arbitrary::Unstructured::new(&data);
899
900            assert_eq!(
901                u.arbitrary::<&AccountIdRef>()
902                    .ok()
903                    .map(AsRef::<str>::as_ref),
904                expected_output
905            );
906        }
907    }
908}