ibc_app_nft_transfer_types/
token.rs

1//! Defines Non-Fungible Token Transfer (ICS-721) token types.
2use core::fmt::{self, Display};
3use core::str::FromStr;
4
5use http::Uri;
6use ibc_core::host::types::error::DecodingError;
7use ibc_core::primitives::prelude::*;
8#[cfg(feature = "serde")]
9use ibc_core::primitives::serializers;
10
11use crate::data::Data;
12
13/// Token ID for an NFT
14#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
15#[cfg_attr(
16    feature = "parity-scale-codec",
17    derive(
18        parity_scale_codec::Encode,
19        parity_scale_codec::Decode,
20        scale_info::TypeInfo
21    )
22)]
23#[cfg_attr(
24    feature = "borsh",
25    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
26)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
30pub struct TokenId(String);
31
32impl AsRef<str> for TokenId {
33    fn as_ref(&self) -> &str {
34        &self.0
35    }
36}
37
38impl Display for TokenId {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "{}", self.0)
41    }
42}
43
44impl FromStr for TokenId {
45    type Err = DecodingError;
46
47    fn from_str(token_id: &str) -> Result<Self, Self::Err> {
48        if token_id.trim().is_empty() {
49            Err(DecodingError::missing_raw_data("empty token ID"))
50        } else {
51            Ok(Self(token_id.to_string()))
52        }
53    }
54}
55
56#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
57#[cfg_attr(
58    feature = "parity-scale-codec",
59    derive(
60        parity_scale_codec::Encode,
61        parity_scale_codec::Decode,
62        scale_info::TypeInfo
63    )
64)]
65#[cfg_attr(
66    feature = "borsh",
67    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
68)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct TokenIds(pub Vec<TokenId>);
73
74impl TokenIds {
75    pub fn as_ref(&self) -> Vec<&TokenId> {
76        self.0.iter().collect()
77    }
78}
79
80impl Display for TokenIds {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(
83            f,
84            "{}",
85            self.0
86                .iter()
87                .map(|t| t.to_string())
88                .collect::<Vec<String>>()
89                .join(",")
90        )
91    }
92}
93
94impl TryFrom<Vec<String>> for TokenIds {
95    type Error = DecodingError;
96
97    fn try_from(token_ids: Vec<String>) -> Result<Self, Self::Error> {
98        if token_ids.is_empty() {
99            return Err(DecodingError::missing_raw_data("empty token IDs"));
100        }
101
102        let ids: Result<Vec<TokenId>, _> = token_ids.iter().map(|t| t.parse()).collect();
103        let mut ids = ids?;
104
105        ids.sort();
106        ids.dedup();
107
108        if ids.len() != token_ids.len() {
109            return Err(DecodingError::invalid_raw_data(format!(
110                "mismatched number of token IDs: expected {}, actual {}",
111                token_ids.len(),
112                ids.len()
113            )));
114        }
115
116        Ok(Self(ids))
117    }
118}
119
120/// Token URI for an NFT
121#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
122#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct TokenUri(
126    #[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arb_uri))]
127    #[cfg_attr(feature = "serde", serde(with = "serializers"))]
128    #[cfg_attr(feature = "schema", schemars(with = "String"))]
129    Uri,
130);
131
132#[cfg(feature = "borsh")]
133impl borsh::BorshSerialize for TokenUri {
134    fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
135        borsh::BorshSerialize::serialize(&self.to_string(), writer)
136    }
137}
138
139#[cfg(feature = "borsh")]
140impl borsh::BorshDeserialize for TokenUri {
141    fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
142        let uri = String::deserialize_reader(reader)?;
143        Ok(TokenUri::from_str(&uri).map_err(|_| borsh::io::ErrorKind::Other)?)
144    }
145}
146
147#[cfg(feature = "parity-scale-codec")]
148impl parity_scale_codec::Encode for TokenUri {
149    fn encode_to<T: parity_scale_codec::Output + ?Sized>(&self, writer: &mut T) {
150        self.to_string().encode_to(writer);
151    }
152}
153
154#[cfg(feature = "parity-scale-codec")]
155impl parity_scale_codec::Decode for TokenUri {
156    fn decode<I: parity_scale_codec::Input>(
157        input: &mut I,
158    ) -> Result<Self, parity_scale_codec::Error> {
159        let uri = String::decode(input)?;
160        TokenUri::from_str(&uri).map_err(|_| parity_scale_codec::Error::from("from str error"))
161    }
162}
163
164#[cfg(feature = "parity-scale-codec")]
165impl scale_info::TypeInfo for TokenUri {
166    type Identity = Self;
167
168    fn type_info() -> scale_info::Type {
169        scale_info::Type::builder()
170            .path(scale_info::Path::new("TokenUri", module_path!()))
171            .composite(
172                scale_info::build::Fields::unnamed()
173                    .field(|f| f.ty::<String>().type_name("String")),
174            )
175    }
176}
177
178impl Display for TokenUri {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "{}", self.0)
181    }
182}
183
184impl FromStr for TokenUri {
185    type Err = DecodingError;
186
187    fn from_str(token_uri: &str) -> Result<Self, Self::Err> {
188        let token_uri = Uri::from_str(token_uri).map_err(DecodingError::invalid_raw_data)?;
189        Ok(Self(token_uri))
190    }
191}
192
193/// Token data for an NFT
194#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
195#[cfg_attr(
196    feature = "parity-scale-codec",
197    derive(
198        parity_scale_codec::Encode,
199        parity_scale_codec::Decode,
200        scale_info::TypeInfo
201    )
202)]
203#[cfg_attr(
204    feature = "borsh",
205    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
206)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
208#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209#[derive(Clone, Debug, PartialEq, Eq, derive_more::AsRef)]
210pub struct TokenData(Data);
211
212impl Display for TokenData {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(f, "{}", self.0)
215    }
216}
217
218impl FromStr for TokenData {
219    type Err = DecodingError;
220
221    fn from_str(token_data: &str) -> Result<Self, Self::Err> {
222        let data = Data::from_str(token_data)?;
223        Ok(Self(data))
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[cfg(feature = "serde")]
232    #[test]
233    fn test_serde_json_roundtrip() {
234        fn serde_roundtrip(token_uri: TokenUri) {
235            let serialized =
236                serde_json::to_string(&token_uri).expect("failed to serialize TokenUri");
237            let deserialized = serde_json::from_str::<TokenUri>(&serialized)
238                .expect("failed to deserialize TokenUri");
239
240            assert_eq!(deserialized, token_uri);
241        }
242
243        let uri = "/foo/bar?baz".parse::<Uri>().unwrap();
244        serde_roundtrip(TokenUri(uri));
245
246        let uri = "https://www.rust-lang.org/install.html"
247            .parse::<Uri>()
248            .unwrap();
249        serde_roundtrip(TokenUri(uri));
250    }
251
252    #[cfg(feature = "borsh")]
253    #[test]
254    fn test_borsh_roundtrip() {
255        fn borsh_roundtrip(token_uri: TokenUri) {
256            let token_uri_bytes = borsh::to_vec(&token_uri).unwrap();
257            let res = borsh::from_slice::<TokenUri>(&token_uri_bytes).unwrap();
258
259            assert_eq!(token_uri, res);
260        }
261
262        let uri = "/foo/bar?baz".parse::<Uri>().unwrap();
263        borsh_roundtrip(TokenUri(uri));
264
265        let uri = "https://www.rust-lang.org/install.html"
266            .parse::<Uri>()
267            .unwrap();
268        borsh_roundtrip(TokenUri(uri));
269    }
270}