defuse_core/token_id/
nep171.rs

1use std::{fmt, str::FromStr};
2
3use crate::token_id::{MAX_ALLOWED_TOKEN_ID_LEN, error::TokenIdError};
4use near_contract_standards::non_fungible_token;
5use near_sdk::{AccountId, AccountIdRef, near};
6use serde_with::{DeserializeFromStr, SerializeDisplay};
7
8#[cfg(any(feature = "arbitrary", test))]
9use arbitrary_with::{Arbitrary, As, LimitLen};
10#[cfg(any(feature = "arbitrary", test))]
11use defuse_near_utils::arbitrary::ArbitraryAccountId;
12
13#[cfg_attr(any(feature = "arbitrary", test), derive(Arbitrary))]
14#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, SerializeDisplay, DeserializeFromStr)]
15#[near(serializers = [borsh])]
16pub struct Nep171TokenId {
17    #[cfg_attr(
18        any(feature = "arbitrary", test),
19        arbitrary(with = As::<ArbitraryAccountId>::arbitrary),
20    )]
21    contract_id: AccountId,
22
23    #[cfg_attr(
24        any(feature = "arbitrary", test),
25        arbitrary(with = As::<LimitLen<MAX_ALLOWED_TOKEN_ID_LEN>>::arbitrary),
26    )]
27    nft_token_id: near_contract_standards::non_fungible_token::TokenId,
28}
29
30impl Nep171TokenId {
31    pub fn new(
32        contract_id: AccountId,
33        nft_token_id: non_fungible_token::TokenId,
34    ) -> Result<Self, TokenIdError> {
35        if nft_token_id.len() > MAX_ALLOWED_TOKEN_ID_LEN {
36            return Err(TokenIdError::TokenIdTooLarge(nft_token_id.len()));
37        }
38
39        Ok(Self {
40            contract_id,
41            nft_token_id,
42        })
43    }
44
45    #[allow(clippy::missing_const_for_fn)]
46    pub fn contract_id(&self) -> &AccountIdRef {
47        &self.contract_id
48    }
49
50    pub const fn nft_token_id(&self) -> &non_fungible_token::TokenId {
51        &self.nft_token_id
52    }
53
54    pub fn into_contract_id_and_nft_token_id(self) -> (AccountId, non_fungible_token::TokenId) {
55        (self.contract_id, self.nft_token_id)
56    }
57}
58
59impl std::fmt::Debug for Nep171TokenId {
60    #[inline]
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{}:{}", self.contract_id(), self.nft_token_id())
63    }
64}
65
66impl std::fmt::Display for Nep171TokenId {
67    #[inline]
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        fmt::Debug::fmt(&self, f)
70    }
71}
72
73impl FromStr for Nep171TokenId {
74    type Err = TokenIdError;
75
76    fn from_str(data: &str) -> Result<Self, Self::Err> {
77        let (contract_id, token_id) = data
78            .split_once(':')
79            .ok_or(strum::ParseError::VariantNotFound)?;
80        Self::new(contract_id.parse()?, token_id.to_string())
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use arbitrary::Unstructured;
88    use arbitrary_with::UnstructuredExt;
89    use defuse_test_utils::random::{make_arbitrary, random_bytes};
90    use rstest::rstest;
91
92    #[rstest]
93    #[trace]
94    fn display_from_str_roundtrip(#[from(make_arbitrary)] token_id: Nep171TokenId) {
95        let s = token_id.to_string();
96        let got: Nep171TokenId = s.parse().unwrap();
97        assert_eq!(got, token_id);
98    }
99
100    #[rstest]
101    fn token_id_length(random_bytes: Vec<u8>) {
102        let mut u = Unstructured::new(&random_bytes);
103        let contract_id = u.arbitrary_as::<_, ArbitraryAccountId>().unwrap();
104        let token_id: String = u.arbitrary().unwrap();
105
106        let r = Nep171TokenId::new(contract_id, token_id.clone());
107        if token_id.len() > MAX_ALLOWED_TOKEN_ID_LEN {
108            assert!(matches!(r.unwrap_err(), TokenIdError::TokenIdTooLarge(_)));
109        } else {
110            r.unwrap();
111        }
112    }
113}