unc_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 unc_account_id::{AccountId, AccountIdRef};
19/// use std::convert::{TryFrom, TryInto};
20///
21/// // Construction
22/// let alice = AccountIdRef::new("alice.unc").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    /// An account with 64 characters long hexadecimal address.
42    UtilityAccount,
43    /// An account which address starts with '0x', followed by 40 hex characters.
44    EthAccount,
45    Reserved,
46}
47
48impl AccountType {
49    pub fn is_valid(&self) -> bool {
50        match &self {
51            Self::UtilityAccount => true,
52            Self::EthAccount => true,
53            Self::Reserved => false,
54        }
55    }
56}
57
58impl AccountIdRef {
59    /// Shortest valid length for a Utility Account ID.
60    pub const MIN_LEN: usize = crate::validation::MIN_LEN;
61    /// Longest valid length for a Utility Account ID.
62    pub const MAX_LEN: usize = crate::validation::MAX_LEN;
63
64    /// Construct a [`&AccountIdRef`](AccountIdRef) from a string reference.
65    ///
66    /// This constructor validates the provided ID, and will produce an error when validation fails.
67    pub fn new<S: AsRef<str> + ?Sized>(id: &S) -> Result<&Self, ParseAccountError> {
68        let id = id.as_ref();
69        crate::validation::validate(id)?;
70
71        // Safety:
72        // - a newtype struct is guaranteed to have the same memory layout as its only field
73        // - the borrow checker will enforce its rules appropriately on the resulting reference
74        Ok(unsafe { &*(id as *const str as *const Self) })
75    }
76
77    /// Construct a [`&AccountIdRef`](AccountIdRef) from with validation at compile time.
78    /// This constructor will panic if validation fails.
79    /// ```rust
80    /// use unc_account_id::AccountIdRef;
81    /// const ALICE: &AccountIdRef = AccountIdRef::new_or_panic("alice.unc");
82    /// ```
83    pub const fn new_or_panic(id: &str) -> &Self {
84        crate::validation::validate_const(id);
85
86        unsafe { &*(id as *const str as *const Self) }
87    }
88
89    /// Construct a [`&AccountIdRef`](AccountIdRef) from a string reference without validating the address.
90    /// It is the responsibility of the caller to ensure the account ID is valid.
91    ///
92    /// For more information, read: <https://docs.unc.org/docs/concepts/account#account-id-rules>
93    pub(crate) fn new_unvalidated<S: AsRef<str> + ?Sized>(id: &S) -> &Self {
94        let id = id.as_ref();
95        // In unccore, due to legacy reasons, AccountId construction and validation are separated.
96        // In order to avoid protocol change, `internal_unstable` feature was implemented and it is
97        // expected that AccountId might be invalid and it will be explicitly validated at the
98        // later stage.
99        #[cfg(not(feature = "internal_unstable"))]
100        debug_assert!(crate::validation::validate(id).is_ok());
101
102        // Safety: see `AccountIdRef::new`
103        unsafe { &*(id as *const str as *const Self) }
104    }
105
106    /// Returns a reference to the account ID bytes.
107    pub fn as_bytes(&self) -> &[u8] {
108        self.0.as_bytes()
109    }
110
111    /// Returns a string slice of the entire Account ID.
112    ///
113    /// ## Examples
114    ///
115    /// ```
116    /// use unc_account_id::AccountIdRef;
117    ///
118    /// let carol = AccountIdRef::new("carol.unc").unwrap();
119    /// assert_eq!("carol.unc", carol.as_str());
120    /// ```
121    pub fn as_str(&self) -> &str {
122        &self.0
123    }
124
125    /// Returns `AccountType::EthAccount` if the `AccountId` is a 40 characters long hexadecimal prefixed with '0x'.
126    /// Returns `AccountType::Un cImplicitAccount` if the `AccountId` is a 64 characters long hexadecimal.
127    /// Otherwise, returns `AccountType::Reserved`.
128    ///
129    /// See [Implicit-Accounts](https://docs.unc.org/docs/concepts/account#implicit-accounts).
130    ///
131    /// ## Examples
132    ///
133    /// ```
134    /// use unc_account_id::{AccountId, AccountType};
135    ///
136    /// let alice: AccountId = "alice.unc".parse().unwrap();
137    /// assert!(alice.get_account_type() == AccountType::Reserved);
138    ///
139    /// let eth_rando = "0xb794f5ea0ba39494ce839613fffba74279579268"
140    ///     .parse::<AccountId>()
141    ///     .unwrap();
142    /// assert!(eth_rando.get_account_type() == AccountType::EthAccount);
143    ///
144    /// let unc_rando = "98793cd91a3f870fb126f66285808c7e094afcfc4eda8a970f6648cdf0dbd6de"
145    ///     .parse::<AccountId>()
146    ///     .unwrap();
147    /// assert!(unc_rando.get_account_type() == AccountType::UtilityAccount);
148    /// ```
149    pub fn get_account_type(&self) -> AccountType {
150        if crate::validation::is_eth_implicit(self.as_str()) {
151            return AccountType::EthAccount;
152        }
153        if crate::validation::is_valid_implicit(self.as_str()) {
154            return AccountType::UtilityAccount;
155        }
156        
157        AccountType::Reserved
158    }
159
160    /// Returns `true` if this `AccountId` is the system account.
161    ///
162    /// See [System account](https://nomicon.io/DataStructures/Account.html?highlight=system#system-account).
163    ///
164    /// ## Examples
165    ///
166    /// ```
167    /// use unc_account_id::AccountId;
168    ///
169    /// let alice: AccountId = "alice.unc".parse().unwrap();
170    /// assert!(!alice.is_system());
171    ///
172    /// let system: AccountId = "system".parse().unwrap();
173    /// assert!(system.is_system());
174    /// ```
175    pub fn is_system(&self) -> bool {
176        self == "system"
177    }
178
179    /// Returns the length of the underlying account id string.
180    pub const fn len(&self) -> usize {
181        self.0.len()
182    }
183
184    /// Returns parent's account id reference
185    ///
186    /// ## Examples
187    /// ```
188    /// use unc_account_id::{AccountIdRef, AccountType};
189    /// 
190    /// let unc: &AccountIdRef = AccountIdRef::new_or_panic("unc");
191    ///
192    /// assert!(unc.get_parent_account_id().is_none());
193    ///
194    /// let implicit: &AccountIdRef = AccountIdRef::new_or_panic("248e104d1d4764d713c4211c13808c8fc887869c580f4178e60538ac5c2a0b26");
195    ///
196    /// assert!(implicit.get_parent_account_id().is_none());
197    /// ```
198    pub fn get_parent_account_id(&self) -> Option<&AccountIdRef> {
199        let parent_str = self.as_str().split_once('.')?.1;
200        Some(AccountIdRef::new_unvalidated(parent_str))
201    }
202}
203
204impl std::fmt::Display for AccountIdRef {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        std::fmt::Display::fmt(&self.0, f)
207    }
208}
209
210impl ToOwned for AccountIdRef {
211    type Owned = AccountId;
212
213    fn to_owned(&self) -> Self::Owned {
214        AccountId(self.0.into())
215    }
216}
217
218impl<'a> From<&'a AccountIdRef> for AccountId {
219    fn from(id: &'a AccountIdRef) -> Self {
220        id.to_owned()
221    }
222}
223
224impl<'s> TryFrom<&'s str> for &'s AccountIdRef {
225    type Error = ParseAccountError;
226
227    fn try_from(value: &'s str) -> Result<Self, Self::Error> {
228        AccountIdRef::new(value)
229    }
230}
231
232impl AsRef<str> for AccountIdRef {
233    fn as_ref(&self) -> &str {
234        &self.0
235    }
236}
237
238impl PartialEq<AccountIdRef> for String {
239    fn eq(&self, other: &AccountIdRef) -> bool {
240        self == &other.0
241    }
242}
243
244impl PartialEq<String> for AccountIdRef {
245    fn eq(&self, other: &String) -> bool {
246        &self.0 == other
247    }
248}
249
250impl PartialEq<AccountIdRef> for str {
251    fn eq(&self, other: &AccountIdRef) -> bool {
252        self == &other.0
253    }
254}
255
256impl PartialEq<str> for AccountIdRef {
257    fn eq(&self, other: &str) -> bool {
258        &self.0 == other
259    }
260}
261
262impl<'a> PartialEq<AccountIdRef> for &'a str {
263    fn eq(&self, other: &AccountIdRef) -> bool {
264        *self == &other.0
265    }
266}
267
268impl<'a> PartialEq<&'a str> for AccountIdRef {
269    fn eq(&self, other: &&'a str) -> bool {
270        &self.0 == *other
271    }
272}
273
274impl<'a> PartialEq<&'a AccountIdRef> for str {
275    fn eq(&self, other: &&'a AccountIdRef) -> bool {
276        self == &other.0
277    }
278}
279
280impl<'a> PartialEq<str> for &'a AccountIdRef {
281    fn eq(&self, other: &str) -> bool {
282        &self.0 == other
283    }
284}
285
286impl<'a> PartialEq<&'a AccountIdRef> for String {
287    fn eq(&self, other: &&'a AccountIdRef) -> bool {
288        self == &other.0
289    }
290}
291
292impl<'a> PartialEq<String> for &'a AccountIdRef {
293    fn eq(&self, other: &String) -> bool {
294        &self.0 == other
295    }
296}
297
298impl PartialOrd<AccountIdRef> for String {
299    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
300        self.as_str().partial_cmp(&other.0)
301    }
302}
303
304impl PartialOrd<String> for AccountIdRef {
305    fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
306        self.0.partial_cmp(other.as_str())
307    }
308}
309
310impl PartialOrd<AccountIdRef> for str {
311    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
312        self.partial_cmp(other.as_str())
313    }
314}
315
316impl PartialOrd<str> for AccountIdRef {
317    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
318        self.as_str().partial_cmp(other)
319    }
320}
321
322impl<'a> PartialOrd<AccountIdRef> for &'a str {
323    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
324        self.partial_cmp(&other.as_str())
325    }
326}
327
328impl<'a> PartialOrd<&'a str> for AccountIdRef {
329    fn partial_cmp(&self, other: &&'a str) -> Option<std::cmp::Ordering> {
330        self.as_str().partial_cmp(*other)
331    }
332}
333
334impl<'a> PartialOrd<&'a AccountIdRef> for String {
335    fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
336        self.as_str().partial_cmp(&other.0)
337    }
338}
339
340impl<'a> PartialOrd<String> for &'a AccountIdRef {
341    fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
342        self.0.partial_cmp(other.as_str())
343    }
344}
345
346impl<'a> PartialOrd<&'a AccountIdRef> for str {
347    fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
348        self.partial_cmp(other.as_str())
349    }
350}
351
352impl<'a> PartialOrd<str> for &'a AccountIdRef {
353    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
354        self.as_str().partial_cmp(other)
355    }
356}
357
358impl<'a> From<&'a AccountIdRef> for Cow<'a, AccountIdRef> {
359    fn from(value: &'a AccountIdRef) -> Self {
360        Cow::Borrowed(value)
361    }
362}
363
364#[cfg(feature = "arbitrary")]
365impl<'a> arbitrary::Arbitrary<'a> for &'a AccountIdRef {
366    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
367        (crate::validation::MIN_LEN, Some(crate::validation::MAX_LEN))
368    }
369
370    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
371        let mut s = u.arbitrary::<&str>()?;
372
373        loop {
374            match AccountIdRef::new(s) {
375                Ok(account_id) => break Ok(account_id),
376                Err(ParseAccountError {
377                    char: Some((idx, _)),
378                    ..
379                }) => {
380                    s = &s[..idx];
381                    continue;
382                }
383                _ => break Err(arbitrary::Error::IncorrectFormat),
384            }
385        }
386    }
387
388    fn arbitrary_take_rest(u: arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
389        let s = <&str as arbitrary::Arbitrary>::arbitrary_take_rest(u)?;
390        AccountIdRef::new(s).map_err(|_| arbitrary::Error::IncorrectFormat)
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use crate::ParseErrorKind;
397
398    use super::*;
399
400    #[test]
401    #[cfg(feature = "schemars")]
402    fn test_schemars() {
403        let schema = schemars::schema_for!(AccountIdRef);
404        let json_schema = serde_json::to_value(&schema).unwrap();
405        assert_eq!(
406            json_schema,
407            serde_json::json!({
408                    "$schema": "http://json-schema.org/draft-07/schema#",
409                    "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 unc_account_id::{AccountId, AccountIdRef}; use std::convert::{TryFrom, TryInto};\n\n// Construction let alice = AccountIdRef::new(\"alice.unc\").unwrap(); assert!(AccountIdRef::new(\"invalid.\").is_err()); ```\n\n[`FromStr`]: std::str::FromStr [`Path`]: std::path::Path",
410                    "title": "AccountIdRef",
411                    "type": "string"
412                }
413            )
414        );
415    }
416
417    #[test]
418    fn test_err_kind_classification() {
419        let id = AccountIdRef::new("ErinMoriarty.unc");
420        debug_assert!(
421            matches!(
422                id,
423                Err(ParseAccountError {
424                    kind: ParseErrorKind::InvalidChar,
425                    char: Some((0, 'E'))
426                })
427            ),
428            "{:?}",
429            id
430        );
431
432        let id = AccountIdRef::new("-KarlUrban.unc");
433        debug_assert!(
434            matches!(
435                id,
436                Err(ParseAccountError {
437                    kind: ParseErrorKind::RedundantSeparator,
438                    char: Some((0, '-'))
439                })
440            ),
441            "{:?}",
442            id
443        );
444
445        let id = AccountIdRef::new("anthonystarr.");
446        debug_assert!(
447            matches!(
448                id,
449                Err(ParseAccountError {
450                    kind: ParseErrorKind::RedundantSeparator,
451                    char: Some((12, '.'))
452                })
453            ),
454            "{:?}",
455            id
456        );
457
458        let id = AccountIdRef::new("jack__Quaid.unc");
459        debug_assert!(
460            matches!(
461                id,
462                Err(ParseAccountError {
463                    kind: ParseErrorKind::RedundantSeparator,
464                    char: Some((5, '_'))
465                })
466            ),
467            "{:?}",
468            id
469        );
470    }
471
472    #[test]
473    fn test_is_account_id_unc_implicit() {
474        let valid_unc_implicit_account_ids = &[
475            "0000000000000000000000000000000000000000000000000000000000000000",
476            "6174617461746174617461746174617461746174617461746174617461746174",
477            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
478            "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
479            "20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667",
480        ];
481        for valid_account_id in valid_unc_implicit_account_ids {
482            assert!(
483                matches!(
484                    AccountIdRef::new(valid_account_id),
485                    Ok(account_id) if account_id.get_account_type() == AccountType::UtilityAccount
486                ),
487                "Account ID {} should be valid 64-len hex",
488                valid_account_id
489            );
490        }
491
492        let invalid_unc_implicit_account_ids = &[
493            "000000000000000000000000000000000000000000000000000000000000000",
494            "6.74617461746174617461746174617461746174617461746174617461746174",
495            "012-456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
496            "fffff_ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
497            "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
498            "00000000000000000000000000000000000000000000000000000000000000",
499        ];
500        for invalid_account_id in invalid_unc_implicit_account_ids {
501            assert!(
502                !matches!(
503                    AccountIdRef::new(invalid_account_id),
504                    Ok(account_id) if account_id.get_account_type() == AccountType::UtilityAccount
505                ),
506                "Account ID {} is not a Utility-implicit account",
507                invalid_account_id
508            );
509        }
510    }
511
512    #[test]
513    fn test_is_account_id_eth_implicit() {
514        let valid_eth_implicit_account_ids = &[
515            "0x0000000000000000000000000000000000000000",
516            "0x6174617461746174617461746174617461746174",
517            "0x0123456789abcdef0123456789abcdef01234567",
518            "0xffffffffffffffffffffffffffffffffffffffff",
519            "0x20782e20662e64666420482123494b6b6c677573",
520        ];
521        for valid_account_id in valid_eth_implicit_account_ids {
522            assert!(
523                matches!(
524                    valid_account_id.parse::<AccountId>(),
525                    Ok(account_id) if account_id.get_account_type() == AccountType::EthAccount
526                ),
527                "Account ID {} should be valid 42-len hex, starting with 0x",
528                valid_account_id
529            );
530        }
531
532        let invalid_eth_implicit_account_ids = &[
533            "04b794f5ea0ba39494ce839613fffba74279579268",
534            "0x000000000000000000000000000000000000000",
535            "0x6.74617461746174617461746174617461746174",
536            "0x012-456789abcdef0123456789abcdef01234567",
537            "0xfffff_ffffffffffffffffffffffffffffffffff",
538            "0xoooooooooooooooooooooooooooooooooooooooo",
539            "0x00000000000000000000000000000000000000000",
540            "0000000000000000000000000000000000000000000000000000000000000000",
541        ];
542        for invalid_account_id in invalid_eth_implicit_account_ids {
543            assert!(
544                !matches!(
545                    invalid_account_id.parse::<AccountId>(),
546                    Ok(account_id) if account_id.get_account_type() == AccountType::EthAccount
547                ),
548                "Account ID {} is not an ETH-implicit account",
549                invalid_account_id
550            );
551        }
552    }
553
554    #[test]
555    #[cfg(feature = "arbitrary")]
556    fn test_arbitrary() {
557        let corpus = [
558            ("a|bcd", None),
559            ("ab|cde", Some("ab")),
560            ("a_-b", None),
561            ("ab_-c", Some("ab")),
562            ("a", None),
563            ("miraclx.unc", Some("miraclx.unc")),
564            (
565                "01234567890123456789012345678901234567890123456789012345678901234",
566                None,
567            ),
568        ];
569
570        for (input, expected_output) in corpus {
571            assert!(input.len() <= u8::MAX as usize);
572            let data = [input.as_bytes(), &[input.len() as _]].concat();
573            let mut u = arbitrary::Unstructured::new(&data);
574
575            assert_eq!(
576                u.arbitrary::<&AccountIdRef>()
577                    .ok()
578                    .map(AsRef::<str>::as_ref),
579                expected_output
580            );
581        }
582    }
583}