defuse_token_id/
lib.rs

1mod error;
2
3#[cfg(feature = "imt")]
4pub mod imt;
5#[cfg(feature = "nep141")]
6pub mod nep141;
7#[cfg(feature = "nep171")]
8pub mod nep171;
9#[cfg(feature = "nep245")]
10pub mod nep245;
11
12#[cfg(not(any(
13    feature = "nep141",
14    feature = "nep171",
15    feature = "nep245",
16    feature = "imt"
17)))]
18compile_error!(
19    r#"At least one of these features should be enabled:
20- "nep141"
21- "nep171"
22- "nep245"
23- "imt"
24"#
25);
26
27use core::{
28    fmt::{self, Debug, Display},
29    str::FromStr,
30};
31use near_sdk::near;
32use serde_with::{DeserializeFromStr, SerializeDisplay};
33use strum::{EnumDiscriminants, EnumIter, EnumString};
34
35pub use self::error::TokenIdError;
36
37#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))]
38#[derive(
39    Clone,
40    PartialEq,
41    Eq,
42    PartialOrd,
43    Ord,
44    Hash,
45    EnumDiscriminants,
46    SerializeDisplay,
47    DeserializeFromStr,
48    derive_more::From,
49)]
50#[strum_discriminants(
51    name(TokenIdType),
52    derive(
53        strum::Display,
54        EnumString,
55        EnumIter,
56        SerializeDisplay,
57        DeserializeFromStr
58    ),
59    strum(serialize_all = "snake_case"),
60    cfg_attr(
61        all(feature = "abi", not(target_arch = "wasm32")),
62        derive(::near_sdk::NearSchema),
63        schemars(with = "String"),
64    ),
65    vis(pub)
66)]
67#[near(serializers = [borsh(use_discriminant=true)])]
68#[repr(u8)]
69// Private: Because we need construction to go through the TokenId struct to check for length
70pub enum TokenId {
71    #[cfg(feature = "nep141")]
72    Nep141(crate::nep141::Nep141TokenId) = 0,
73    #[cfg(feature = "nep171")]
74    Nep171(crate::nep171::Nep171TokenId) = 1,
75    #[cfg(feature = "nep245")]
76    Nep245(crate::nep245::Nep245TokenId) = 2,
77    #[cfg(feature = "imt")]
78    Imt(crate::imt::ImtTokenId) = 3,
79}
80
81impl Debug for TokenId {
82    #[inline]
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            #[cfg(feature = "nep141")]
86            Self::Nep141(token_id) => {
87                write!(f, "{}:{}", TokenIdType::Nep141, token_id)
88            }
89            #[cfg(feature = "nep171")]
90            Self::Nep171(token_id) => {
91                write!(f, "{}:{}", TokenIdType::Nep171, token_id)
92            }
93            #[cfg(feature = "nep245")]
94            Self::Nep245(token_id) => {
95                write!(f, "{}:{}", TokenIdType::Nep245, token_id)
96            }
97            #[cfg(feature = "imt")]
98            Self::Imt(token_id) => {
99                write!(f, "{}:{}", TokenIdType::Imt, token_id)
100            }
101        }
102    }
103}
104
105impl Display for TokenId {
106    #[inline]
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        fmt::Debug::fmt(&self, f)
109    }
110}
111
112impl FromStr for TokenId {
113    type Err = TokenIdError;
114
115    #[inline]
116    fn from_str(s: &str) -> Result<Self, Self::Err> {
117        let (typ, data) = s
118            .split_once(':')
119            .ok_or(strum::ParseError::VariantNotFound)?;
120        match typ.parse()? {
121            #[cfg(feature = "nep141")]
122            TokenIdType::Nep141 => data.parse().map(Self::Nep141),
123            #[cfg(feature = "nep171")]
124            TokenIdType::Nep171 => data.parse().map(Self::Nep171),
125            #[cfg(feature = "nep245")]
126            TokenIdType::Nep245 => data.parse().map(Self::Nep245),
127            #[cfg(feature = "imt")]
128            TokenIdType::Imt => data.parse().map(Self::Imt),
129        }
130    }
131}
132
133#[cfg(all(feature = "abi", not(target_arch = "wasm32")))]
134const _: () = {
135    use near_sdk::schemars::{
136        JsonSchema,
137        r#gen::SchemaGenerator,
138        schema::{InstanceType, Schema, SchemaObject},
139    };
140
141    impl JsonSchema for TokenId {
142        fn schema_name() -> String {
143            stringify!(TokenId).to_string()
144        }
145
146        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
147            use near_sdk::AccountId;
148
149            SchemaObject {
150                instance_type: Some(InstanceType::String.into()),
151                extensions: [(
152                    "examples",
153                    [
154                        #[cfg(feature = "nep141")]
155                        TokenId::Nep141(crate::nep141::Nep141TokenId::new(
156                            "ft.near".parse::<AccountId>().unwrap(),
157                        )),
158                        #[cfg(feature = "nep171")]
159                        TokenId::Nep171(crate::nep171::Nep171TokenId::new(
160                            "nft.near".parse::<AccountId>().unwrap(),
161                            "token_id1",
162                        )),
163                        #[cfg(feature = "nep245")]
164                        TokenId::Nep245(crate::nep245::Nep245TokenId::new(
165                            "mt.near".parse::<AccountId>().unwrap(),
166                            "token_id1",
167                        )),
168                        #[cfg(feature = "imt")]
169                        TokenId::Imt(crate::imt::ImtTokenId::new(
170                            "imt.near".parse::<AccountId>().unwrap(),
171                            "token_id1",
172                        )),
173                    ]
174                    .map(|s| s.to_string())
175                    .to_vec()
176                    .into(),
177                )]
178                .into_iter()
179                .map(|(k, v)| (k.to_string(), v))
180                .collect(),
181                ..Default::default()
182            }
183            .into()
184        }
185    }
186};
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use defuse_test_utils::random::make_arbitrary;
192    use near_sdk::{borsh, serde_json};
193    use rstest::rstest;
194
195    #[rstest]
196    #[trace]
197    #[cfg_attr(feature = "nep141", case("nep141:abc", "0003000000616263"))]
198    #[cfg_attr(
199        feature = "nep171",
200        case("nep171:abc:xyz", "01030000006162630300000078797a")
201    )]
202    #[cfg_attr(
203        feature = "nep245",
204        case("nep245:abc:xyz", "02030000006162630300000078797a")
205    )]
206    #[cfg_attr(feature = "imt", case("imt:abc:xyz", "03030000006162630300000078797a"))]
207    fn roundtrip_fixed(#[case] token_id_str: &str, #[case] borsh_expected_hex: &str) {
208        let token_id: TokenId = token_id_str.parse().unwrap();
209        let borsh_expected = hex::decode(borsh_expected_hex).unwrap();
210
211        let borsh_ser = borsh::to_vec(&token_id).unwrap();
212        assert_eq!(borsh_ser, borsh_expected);
213
214        let got: TokenId = borsh::from_slice(&borsh_ser).unwrap();
215        assert_eq!(got, token_id);
216        assert_eq!(got.to_string(), token_id_str);
217    }
218
219    #[rstest]
220    #[trace]
221    fn borsh_roundtrip(#[from(make_arbitrary)] token_id: TokenId) {
222        let ser = borsh::to_vec(&token_id).unwrap();
223        let got: TokenId = borsh::from_slice(&ser).unwrap();
224        assert_eq!(got, token_id);
225    }
226
227    #[rstest]
228    #[trace]
229    fn display_from_str_roundtrip(#[from(make_arbitrary)] token_id: TokenId) {
230        let s = token_id.to_string();
231        let got: TokenId = s.parse().unwrap();
232        assert_eq!(got, token_id);
233    }
234
235    #[rstest]
236    #[trace]
237    fn serde_roundtrip(#[from(make_arbitrary)] token_id: TokenId) {
238        let ser = serde_json::to_vec(&token_id).unwrap();
239        let got: TokenId = serde_json::from_slice(&ser).unwrap();
240        assert_eq!(got, token_id);
241    }
242}