defuse_token_id/
lib.rs

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