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