defuse_core/token_id/
mod.rs

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