cml_cip25/
utils.rs

1use std::{collections::BTreeMap, convert::TryFrom, string::FromUtf8Error};
2
3use cbor_event::{de::Deserializer, se::Serializer};
4pub use cml_chain::{
5    assets::AssetName,
6    auxdata::{Metadata, TransactionMetadatum},
7    PolicyId,
8};
9pub use cml_core::{error::*, serialization::*};
10use std::io::{BufRead, Seek, SeekFrom, Write};
11
12use crate::{CIP25ChunkableString, CIP25Metadata, CIP25MetadataDetails, CIP25String64};
13
14pub static CIP25_METADATA_LABEL: u64 = 721;
15
16impl CIP25Metadata {
17    /// Create a Metadata containing only the CIP25 schema
18    pub fn to_metadata(&self) -> Result<Metadata, DeserializeError> {
19        use std::convert::TryInto;
20        self.try_into()
21    }
22
23    /// Read the CIP25 schema from a Metadata. Ignores all other data besides CIP25
24    /// Can fail if the Metadata does not conform to CIP25
25    pub fn from_metadata(metadata: &Metadata) -> Result<CIP25Metadata, DeserializeError> {
26        Self::try_from(metadata)
27    }
28
29    /// Add to an existing metadata (could be empty) the full CIP25 metadata
30    pub fn add_to_metadata(&self, metadata: &mut Metadata) -> Result<(), DeserializeError> {
31        let cip25_metadatum = TransactionMetadatum::from_cbor_bytes(&self.key_721.to_bytes())?;
32        metadata.set(CIP25_METADATA_LABEL, cip25_metadatum);
33        Ok(())
34    }
35}
36
37impl std::convert::TryFrom<&Metadata> for CIP25Metadata {
38    type Error = DeserializeError;
39
40    fn try_from(metadata: &Metadata) -> Result<Self, Self::Error> {
41        let cip25_metadatum = metadata.get(CIP25_METADATA_LABEL).ok_or_else(|| {
42            DeserializeFailure::MandatoryFieldMissing(Key::Uint(CIP25_METADATA_LABEL))
43        })?;
44        Ok(Self {
45            key_721: CIP25LabelMetadata::from_cbor_bytes(&cip25_metadatum.to_cbor_bytes())?,
46        })
47    }
48}
49
50impl std::convert::TryInto<Metadata> for &CIP25Metadata {
51    type Error = DeserializeError;
52
53    fn try_into(self) -> Result<Metadata, Self::Error> {
54        let mut metadata = Metadata::new();
55        self.add_to_metadata(&mut metadata)?;
56        Ok(metadata)
57    }
58}
59
60impl CIP25String64 {
61    pub fn new_str(inner: &str) -> Result<Self, DeserializeError> {
62        if inner.len() > 64 {
63            return Err(DeserializeError::new(
64                "CIP25String64",
65                DeserializeFailure::RangeCheck {
66                    found: inner.len() as isize,
67                    min: Some(0),
68                    max: Some(64),
69                },
70            ));
71        }
72        Ok(Self(inner.to_owned()))
73    }
74
75    pub fn to_str(&self) -> &str {
76        &self.0
77    }
78}
79
80impl TryFrom<&str> for CIP25String64 {
81    type Error = DeserializeError;
82
83    fn try_from(inner: &str) -> Result<Self, Self::Error> {
84        CIP25String64::new_str(inner)
85    }
86}
87
88impl From<&str> for CIP25ChunkableString {
89    fn from(s: &str) -> Self {
90        CIP25String64::new_str(s)
91            .map(Self::Single)
92            .unwrap_or_else(|_err| {
93                let mut chunks = Vec::with_capacity(s.len() / 64);
94                for i in (0..s.len()).step_by(64) {
95                    let j = std::cmp::min(s.len(), i + 64);
96                    chunks.push(CIP25String64::new_str(&s[i..j]).unwrap());
97                }
98                Self::Chunked(chunks)
99            })
100    }
101}
102
103impl From<&CIP25ChunkableString> for String {
104    fn from(chunkable: &CIP25ChunkableString) -> Self {
105        match chunkable {
106            CIP25ChunkableString::Single(chunk) => chunk.to_str().to_owned(),
107            CIP25ChunkableString::Chunked(chunks) => chunks
108                .iter()
109                .map(|chunk| chunk.to_str().to_owned())
110                .collect(),
111        }
112    }
113}
114
115/// A subset of CIP25MetadataDetails where the keys are optional
116/// Useful to extract the key fields (name & image) of incorrectly formatted cip25
117#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
118pub struct CIP25MiniMetadataDetails {
119    pub name: Option<CIP25String64>,
120    pub image: Option<CIP25ChunkableString>,
121}
122
123impl CIP25MiniMetadataDetails {
124    pub fn new(name: Option<CIP25String64>, image: Option<CIP25ChunkableString>) -> Self {
125        Self { name, image }
126    }
127
128    /// loose parsing of CIP25 metadata to allow for common exceptions to the format
129    /// `metadatum` should represent the data where the `CIP25MetadataDetails` is in the cip25 structure
130    /// TODO: this is not an ideal solution
131    ///       ideally: we would have a function that takes in a policy ID
132    ///       and would have a lookup map to know which lambda to call to get the name & image depending on the policy ID
133    ///       with a fallback to the standard CIP25 definition
134    ///       however, since this is a lot of work, we use this temporary solution instead
135    pub fn loose_parse(metadatum: &TransactionMetadatum) -> Result<Self, DeserializeError> {
136        match metadatum {
137            TransactionMetadatum::Map(map) => {
138                let name: Option<CIP25String64> = map
139                    .get(&TransactionMetadatum::new_text("name".to_owned()).unwrap())
140                    // for some reason, 1% of NFTs seem to use the wrong case
141                    .or_else(|| {
142                        map.get(&TransactionMetadatum::new_text("Name".to_owned()).unwrap())
143                    })
144                    // for some reason, 0.5% of NFTs use "title" instead of name
145                    .or_else(|| {
146                        map.get(&TransactionMetadatum::new_text("title".to_owned()).unwrap())
147                    })
148                    // for some reason, 0.3% of NFTs use "id" instead of name
149                    .or_else(|| map.get(&TransactionMetadatum::new_text("id".to_owned()).unwrap()))
150                    .and_then(|result| match result {
151                        TransactionMetadatum::Text { text, .. } => {
152                            CIP25String64::new_str(text).ok()
153                        }
154                        _ => None,
155                    });
156
157                let image_base =
158                    map.get(&TransactionMetadatum::new_text("image".to_owned()).unwrap());
159                let image = match image_base {
160                    None => None,
161                    Some(base) => match base {
162                        TransactionMetadatum::Text { text, .. } => {
163                            match CIP25String64::new_str(text) {
164                                Ok(str64) => Some(CIP25ChunkableString::Single(str64)),
165                                Err(_) => None,
166                            }
167                        }
168                        TransactionMetadatum::List { elements, .. } => (|| {
169                            let mut chunks: Vec<CIP25String64> = vec![];
170                            for i in 0..elements.len() {
171                                match elements.get(i) {
172                                    Some(TransactionMetadatum::Text { text, .. }) => {
173                                        match CIP25String64::new_str(text) {
174                                            Ok(str64) => chunks.push(str64),
175                                            Err(_) => return None,
176                                        }
177                                    }
178                                    _ => return None,
179                                };
180                            }
181                            Some(CIP25ChunkableString::Chunked(chunks))
182                        })(),
183                        _ => None,
184                    },
185                };
186
187                Ok(CIP25MiniMetadataDetails::new(name, image))
188            }
189            _ => Err(DeserializeError::new(
190                "CIP25MiniMetadataDetails",
191                DeserializeFailure::NoVariantMatched,
192            )),
193        }
194    }
195}
196
197#[derive(Debug, thiserror::Error)]
198pub enum CIP25Error {
199    #[error("Version 1 Asset Name must be string. Asset: {0:?}, Err: {1}")]
200    Version1NonStringAsset(AssetName, FromUtf8Error),
201}
202
203/// Which version of the CIP25 spec to use. See CIP25 for details.
204/// This will change how things are encoded but for the most part contains
205/// the same information.
206#[wasm_bindgen::prelude::wasm_bindgen]
207#[derive(
208    Copy,
209    Clone,
210    Debug,
211    PartialEq,
212    PartialOrd,
213    Eq,
214    Ord,
215    serde::Deserialize,
216    serde::Serialize,
217    schemars::JsonSchema,
218)]
219pub enum CIP25Version {
220    /// Initial version of CIP25 with only string (utf8) asset names allowed.
221    V1,
222    /// Second version of CIP25. Supports any type of asset names.
223    V2,
224}
225
226#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
227pub struct CIP25LabelMetadata {
228    nfts: BTreeMap<PolicyId, BTreeMap<AssetName, CIP25MetadataDetails>>,
229    version: CIP25Version,
230}
231
232impl CIP25LabelMetadata {
233    /// Note that Version 1 can only support utf8 string asset names.
234    /// Version 2 can support any asset name.
235    pub fn new(version: CIP25Version) -> Self {
236        Self {
237            nfts: BTreeMap::new(),
238            version,
239        }
240    }
241
242    /// If this is version 1 and the asset name is not a utf8 asset name
243    /// then this will return an error.
244    /// This function will never return an error for version 2.
245    /// On success, returns the previous details that were overwritten, or None otherwise.
246    pub fn set(
247        &mut self,
248        policy_id: PolicyId,
249        asset_name: AssetName,
250        details: CIP25MetadataDetails,
251    ) -> Result<Option<CIP25MetadataDetails>, CIP25Error> {
252        if self.version == CIP25Version::V1 {
253            if let Err(e) = String::from_utf8(asset_name.to_raw_bytes().to_vec()) {
254                return Err(CIP25Error::Version1NonStringAsset(asset_name, e));
255            }
256        }
257        Ok(self
258            .nfts
259            .entry(policy_id)
260            .or_default()
261            .insert(asset_name, details))
262    }
263
264    pub fn get(
265        &self,
266        policy_id: &PolicyId,
267        asset_name: &AssetName,
268    ) -> Option<&CIP25MetadataDetails> {
269        self.nfts.get(policy_id)?.get(asset_name)
270    }
271
272    pub fn nfts(&self) -> &BTreeMap<PolicyId, BTreeMap<AssetName, CIP25MetadataDetails>> {
273        &self.nfts
274    }
275
276    pub fn version(&self) -> CIP25Version {
277        self.version
278    }
279}
280
281// serialization:
282
283impl cbor_event::se::Serialize for CIP25LabelMetadata {
284    fn serialize<'se, W: Write>(
285        &self,
286        serializer: &'se mut Serializer<W>,
287    ) -> cbor_event::Result<&'se mut Serializer<W>> {
288        match self.version {
289            CIP25Version::V1 => {
290                serializer.write_map(cbor_event::Len::Len(self.nfts.len() as u64))?;
291                for (policy_id, assets) in self.nfts.iter() {
292                    // hand-edit: write hex string
293                    serializer.write_text(policy_id.to_hex())?;
294                    serializer.write_map(cbor_event::Len::Len(assets.len() as u64))?;
295                    for (asset_name, details) in assets.iter() {
296                        // hand-edit: write as string
297                        // note: this invariant is checked during setting and data is private
298                        let asset_name_str =
299                            String::from_utf8(asset_name.to_raw_bytes().to_vec()).unwrap();
300                        serializer.write_text(asset_name_str)?;
301                        details.serialize(serializer)?;
302                    }
303                }
304            }
305            CIP25Version::V2 => {
306                serializer.write_map(cbor_event::Len::Len(2))?;
307                serializer.write_text("data")?;
308                serializer.write_map(cbor_event::Len::Len(self.nfts.len() as u64))?;
309                for (policy_id, assets) in self.nfts.iter() {
310                    // hand-edit: write bytes
311                    serializer.write_bytes(policy_id.to_raw_bytes())?;
312
313                    serializer.write_map(cbor_event::Len::Len(assets.len() as u64))?;
314                    for (asset_name, details) in assets.iter() {
315                        // hand-edit: write bytes
316                        serializer.write_bytes(asset_name.to_raw_bytes())?;
317
318                        details.serialize(serializer)?;
319                    }
320                }
321                serializer.write_text("version")?;
322                serializer.write_unsigned_integer(2u64)?;
323            }
324        }
325        Ok(serializer)
326    }
327}
328
329impl Deserialize for CIP25LabelMetadata {
330    fn deserialize<R: BufRead + Seek>(raw: &mut Deserializer<R>) -> Result<Self, DeserializeError> {
331        (|| -> Result<_, DeserializeError> {
332            // largely taken from result of generating the original CDDL then modifying to merge v1/v2
333            // this has to be modified anyway to allow for permissive parsing in the first place.
334            let initial_position = raw.as_mut_ref().stream_position().unwrap();
335
336            // Try parsing V1
337            let deser_variant = (|raw: &mut Deserializer<_>| -> Result<_, DeserializeError> {
338                let mut label_metadata_v1_table = BTreeMap::new();
339                let mut label_metadata_v1_table_len = 0;
340                let label_metadata_v1_len = raw.map()?;
341                while match label_metadata_v1_len {
342                    cbor_event::Len::Len(n) => label_metadata_v1_table_len < n as usize,
343                    cbor_event::Len::Indefinite => true,
344                } {
345                    match raw.cbor_type()? {
346                        cbor_event::Type::Text => {
347                            // hand-edit: read as hex text
348                            let label_metadata_v1_key = PolicyId::from_hex(&raw.text()?)
349                                .map_err(|e| DeserializeFailure::InvalidStructure(Box::new(e)))?;
350
351                            let mut label_metadata_v1_value_table = BTreeMap::new();
352                            let mut label_metadata_v1_value_table_len = 0;
353                            let label_metadata_v1_value_len = raw.map()?;
354                            while match label_metadata_v1_value_len {
355                                cbor_event::Len::Len(n) => {
356                                    label_metadata_v1_value_table_len < n as usize
357                                }
358                                cbor_event::Len::Indefinite => true,
359                            } {
360                                match raw.cbor_type()? {
361                                    cbor_event::Type::Text => {
362                                        // hand-edit: read as text
363                                        let label_metadata_v1_value_key = AssetName::new(raw.text()?.as_bytes().to_vec())?;
364
365                                        let label_metadata_v1_value_value =
366                                            CIP25MetadataDetails::deserialize(raw)?;
367                                        if label_metadata_v1_value_table
368                                            .insert(
369                                                label_metadata_v1_value_key.clone(),
370                                                label_metadata_v1_value_value,
371                                            )
372                                            .is_some()
373                                        {
374                                            return Err(DeserializeFailure::DuplicateKey(
375                                                Key::Str(String::from(
376                                                    "some complicated/unsupported type",
377                                                )),
378                                            )
379                                            .into());
380                                        }
381                                        label_metadata_v1_value_table_len += 1;
382                                    }
383                                    cbor_event::Type::Special => {
384                                        assert_eq!(raw.special()?, cbor_event::Special::Break);
385                                        break;
386                                    }
387                                    _other_type => {
388                                        // we still need to read the data to move on to the CBOR after it
389                                        let _other_key =
390                                            cml_chain::auxdata::TransactionMetadatum::deserialize(
391                                                raw,
392                                            )?;
393                                        let _other_value =
394                                            cml_chain::auxdata::TransactionMetadatum::deserialize(
395                                                raw,
396                                            )?;
397                                        label_metadata_v1_value_table_len += 1;
398                                    }
399                                }
400                            }
401                            let label_metadata_v1_value = label_metadata_v1_value_table;
402                            if label_metadata_v1_table
403                                .insert(label_metadata_v1_key, label_metadata_v1_value)
404                                .is_some()
405                            {
406                                return Err(DeserializeFailure::DuplicateKey(Key::Str(
407                                    String::from("some complicated/unsupported type"),
408                                ))
409                                .into());
410                            }
411                            label_metadata_v1_table_len += 1;
412                        }
413                        cbor_event::Type::Special => {
414                            assert_eq!(raw.special()?, cbor_event::Special::Break);
415                            break;
416                        }
417                        _other_type => {
418                            // we still need to read the data to move on to the CBOR after it
419                            let _other_key =
420                                cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
421                            let _other_value =
422                                cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
423                            label_metadata_v1_table_len += 1;
424                        }
425                    }
426                }
427                Ok(label_metadata_v1_table)
428            })(raw);
429            match deser_variant {
430                Ok(label_metadata_v1) => {
431                    // hand-edit: construct merged type
432                    return Ok(Self {
433                        nfts: label_metadata_v1,
434                        version: CIP25Version::V1,
435                    });
436                },
437                Err(_) => raw
438                    .as_mut_ref()
439                    .seek(SeekFrom::Start(initial_position))
440                    .unwrap(),
441            };
442
443            // Try paring V2
444            let deser_variant = (|raw: &mut Deserializer<_>| -> Result<_, DeserializeError> {
445                let len = raw.map()?;
446                let mut read_len = CBORReadLen::new(match len {
447                    cbor_event::Len::Len(n) => cbor_event::LenSz::Len(n, cbor_event::Sz::canonical(n)),
448                    cbor_event::Len::Indefinite => cbor_event::LenSz::Indefinite,
449                });
450                read_len.read_elems(2)?;
451                let mut data = None;
452                let mut version_present = false;
453                let mut read = 0;
454                while match len {
455                    cbor_event::Len::Len(n) => read < n as usize,
456                    cbor_event::Len::Indefinite => true,
457                } {
458                    match raw.cbor_type()? {
459                        cbor_event::Type::Text => match raw.text()?.as_str() {
460                            "data" => {
461                                if data.is_some() {
462                                    return Err(DeserializeFailure::DuplicateKey(Key::Str(
463                                        "data".into(),
464                                    ))
465                                    .into());
466                                }
467                                data = Some(
468                                    (|| -> Result<_, DeserializeError> {
469                                        let mut data_table = BTreeMap::new();
470                                        let data_len = raw.map()?;
471                                        let mut data_table_len = 0;
472                                        while match data_len {
473                                            cbor_event::Len::Len(n) => data_table_len < n as usize,
474                                            cbor_event::Len::Indefinite => true,
475                                        } {
476                                            match raw.cbor_type()? {
477                                                cbor_event::Type::Bytes => {
478                                                    // hand-edit: read as bytes
479                                                    let data_key = PolicyId::from_raw_bytes(&raw.bytes()?)
480                                                        .map_err(|e| DeserializeFailure::InvalidStructure(Box::new(e)))?;
481
482                                                    let mut data_value_table_len = 0;
483                                                    let mut data_value_table = BTreeMap::new();
484                                                    let data_value_len = raw.map()?;
485                                                    while match data_value_len {
486                                                        cbor_event::Len::Len(n) => data_value_table_len < n as usize,
487                                                        cbor_event::Len::Indefinite => true,
488                                                    } {
489                                                        match raw.cbor_type()? {
490                                                            cbor_event::Type::Bytes => {
491                                                                // hand-edit: read as bytes
492                                                                let data_value_key = AssetName::new(raw.bytes()?)?;
493
494                                                                let data_value_value =
495                                                                    CIP25MetadataDetails::deserialize(raw)?;
496                                                                if data_value_table
497                                                                    .insert(data_value_key.clone(), data_value_value)
498                                                                    .is_some()
499                                                                {
500                                                                    return Err(DeserializeFailure::DuplicateKey(
501                                                                        Key::Str(String::from(
502                                                                            "some complicated/unsupported type",
503                                                                        )),
504                                                                    )
505                                                                    .into());
506                                                                }
507                                                                data_value_table_len += 1;
508                                                            },
509                                                            cbor_event::Type::Special => {
510                                                                assert_eq!(raw.special()?, cbor_event::Special::Break);
511                                                                break;
512                                                            },
513                                                            _other_type => {
514                                                                // we still need to read the data to move on to the CBOR after it
515                                                                let _other_key = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
516                                                                let _other_value = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
517                                                                data_value_table_len += 1;
518                                                            },
519                                                        }
520                                                    }
521                                                    let data_value = data_value_table;
522                                                    if data_table.insert(data_key, data_value).is_some()
523                                                    {
524                                                        return Err(DeserializeFailure::DuplicateKey(
525                                                            Key::Str(String::from(
526                                                                "some complicated/unsupported type",
527                                                            )),
528                                                        )
529                                                        .into());
530                                                    }
531                                                    data_table_len += 1;
532                                                },
533                                                cbor_event::Type::Special => {
534                                                    assert_eq!(raw.special()?, cbor_event::Special::Break);
535                                                    break;
536                                                },
537                                                _other_type => {
538                                                    // we still need to read the data to move on to the CBOR after it
539                                                    let _other_key = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
540                                                    let _other_value = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
541                                                    data_table_len += 1;
542                                                },
543                                            }
544                                        }
545                                        Ok(data_table)
546                                    })()
547                                    .map_err(|e| e.annotate("data"))?,
548                                );
549                            }
550                            "version" => {
551                                if version_present {
552                                    return Err(DeserializeFailure::DuplicateKey(Key::Str(
553                                        "version".into(),
554                                    ))
555                                    .into());
556                                }
557                                version_present = (|| -> Result<_, DeserializeError> {
558                                    let version_value = raw.unsigned_integer()?;
559                                    if version_value != 2 {
560                                        return Err(DeserializeFailure::FixedValueMismatch {
561                                            found: Key::Uint(version_value),
562                                            expected: Key::Uint(2),
563                                        }
564                                        .into());
565                                    }
566                                    Ok(true)
567                                })()
568                                .map_err(|e| e.annotate("version"))?;
569                            }
570                            _unknown_key => {
571                                // CIP-25 allows permissive parsing
572                                read_len.read_elems(1)?;
573                                // we still need to read the data to move on to the CBOR after it
574                                let _other_metadatum = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
575                            }
576                        },
577                        cbor_event::Type::Special => match len {
578                            cbor_event::Len::Len(_) => {
579                                return Err(DeserializeFailure::BreakInDefiniteLen.into())
580                            }
581                            cbor_event::Len::Indefinite => match raw.special()? {
582                                cbor_event::Special::Break => break,
583                                _ => return Err(DeserializeFailure::EndingBreakMissing.into()),
584                            },
585                        },
586                        _other_type => {
587                            // CIP-25 allows permissive parsing
588                            read_len.read_elems(1)?;
589                            // we still need to read the data to move on to the CBOR after it
590                            let _other_key = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
591                            let _other_value = cml_chain::auxdata::TransactionMetadatum::deserialize(raw)?;
592                        }
593                    }
594                    read += 1;
595                }
596                let data = match data {
597                    Some(x) => x,
598                    None => {
599                        return Err(
600                            DeserializeFailure::MandatoryFieldMissing(Key::Str(String::from("data")))
601                                .into(),
602                        )
603                    }
604                };
605                if !version_present {
606                    return Err(
607                        DeserializeFailure::MandatoryFieldMissing(Key::Str(String::from("version")))
608                            .into(),
609                    );
610                }
611                // hand-edit: expression only here, no Self wrapper
612                Ok(data)
613            })(raw)
614            .map_err(|e| e.annotate("LabelMetadataV2"));
615            match deser_variant {
616                Ok(label_metadata_v2) => {
617                    // hand-edit: construct merged type
618                    return Ok(Self {
619                        nfts: label_metadata_v2,
620                        version: CIP25Version::V2,
621                    });
622                },
623                Err(_) => raw
624                    .as_mut_ref()
625                    .seek(SeekFrom::Start(initial_position))
626                    .unwrap(),
627            };
628
629            // Neither worked
630            Err(DeserializeError::new(
631                "CIP25LabelMetadata",
632                DeserializeFailure::NoVariantMatched,
633            ))
634        })()
635        .map_err(|e| e.annotate("CIP25LabelMetadata"))
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use crate::{CIP25FilesDetails, CIP25MetadataDetails};
642
643    use super::*;
644
645    #[test]
646    fn create() {
647        let mut details = CIP25MetadataDetails::new(
648            CIP25String64::try_from("Metadata Name").unwrap(),
649            CIP25ChunkableString::from("htts://some.website.com/image.png"),
650        );
651        details.description = Some(CIP25ChunkableString::from("description of this NFT"));
652        details.media_type = Some(CIP25String64::try_from("image/*").unwrap());
653        details.files = Some(vec![
654            CIP25FilesDetails::new(
655                CIP25String64::new_str("filename1").unwrap(),
656                CIP25String64::new_str("filetype1").unwrap(),
657                CIP25ChunkableString::from("src1"),
658            ),
659            CIP25FilesDetails::new(
660                CIP25String64::new_str("filename2").unwrap(),
661                CIP25String64::new_str("filetype2").unwrap(),
662                CIP25ChunkableString::from("src2"),
663            ),
664        ]);
665        let policy_id_bytes = [
666            0xBA, 0xAD, 0xF0, 0x0D, 0xBA, 0xAD, 0xF0, 0x0D, 0xBA, 0xAD, 0xF0, 0x0D, 0xBA, 0xAD,
667            0xF0, 0x0D, 0xBA, 0xAD, 0xF0, 0x0D, 0xBA, 0xAD, 0xF0, 0x0D, 0xBA, 0xAD, 0xF0, 0x0D,
668        ];
669        let mut v2 = CIP25LabelMetadata::new(CIP25Version::V2);
670        v2.set(
671            PolicyId::from_raw_bytes(&policy_id_bytes).unwrap(),
672            AssetName::new(vec![0xCA, 0xFE, 0xD0, 0x0D]).unwrap(),
673            details,
674        )
675        .unwrap();
676        let metadata = CIP25Metadata::new(v2);
677        let metadata_bytes = metadata.to_bytes();
678        let roundtrip = CIP25Metadata::from_cbor_bytes(&metadata_bytes).unwrap();
679        assert_eq!(metadata_bytes, roundtrip.to_bytes());
680        let as_metadata = metadata.to_metadata().unwrap();
681        let from_metadata = CIP25Metadata::from_metadata(&as_metadata).unwrap();
682        assert_eq!(metadata_bytes, from_metadata.to_bytes());
683    }
684
685    #[test]
686    fn parse_metadata_details() {
687        {
688            // {
689            //  "arweaveId": "6srpXZOTfK_62KUrJKh4VdCFG0YS271pq20OMRpE5Ts",
690            //  "image": "ipfs://QmUWP6xGHucgBUv514gwgbt4yijg36aUQunEP61z5D8RKS",
691            //  "name": "SpaceBud #1507",
692            //  "traits": ["Star Suit", "Chestplate", "Belt", "Flag", "Pistol"],
693            //  "type": "Alien",
694            // }
695            let bytes = "a569617277656176654964782b36737270585a4f54664b5f36324b55724a4b68345664434647305953323731707132304f4d52704535547365696d6167657835697066733a2f2f516d5557503678474875636742557635313467776762743479696a673336615551756e455036317a354438524b53646e616d656e53706163654275642023313530376674726169747385695374617220537569746a4368657374706c6174656442656c7464466c616766506973746f6c647479706565416c69656e";
696            CIP25MetadataDetails::from_bytes(hex::decode(bytes).unwrap()).unwrap();
697        }
698        {
699            // {
700            //     "color": "#EC97B6",
701            //     "image": "ipfs://ipfs/QmUvbF2siHFGGRtZ5za1VwNQ8y49bbtjmYfFYhgE89hCq2",
702            //     "name": "Berry Alba",
703            // }
704            let bytes = "a365636f6c6f72672345433937423665696d616765783a697066733a2f2f697066732f516d557662463273694846474752745a357a613156774e51387934396262746a6d59664659686745383968437132646e616d656a426572727920416c6261";
705            CIP25MetadataDetails::from_bytes(hex::decode(bytes).unwrap()).unwrap();
706        }
707    }
708
709    #[test]
710    fn just_name() {
711        // {"name":"Metaverse"}
712        let details = CIP25MiniMetadataDetails::loose_parse(
713            &TransactionMetadatum::from_bytes(
714                hex::decode("a1646e616d65694d6574617665727365").unwrap(),
715            )
716            .unwrap(),
717        )
718        .unwrap();
719        assert_eq!(details.name.unwrap().0, "Metaverse");
720    }
721
722    #[test]
723    fn uppercase_name() {
724        // {"Date":"9 May 2021","Description":"Happy Mother's Day to all the Cardano Moms!","Image":"ipfs.io/ipfs/Qmah6QPKUKvp6K9XQB2SA42Q3yrffCbYBbk8EoRrB7FN2g","Name":"Mother's Day 2021","Ticker":"MOM21","URL":"ipfs.io/ipfs/Qmah6QPKUKvp6K9XQB2SA42Q3yrffCbYBbk8EoRrB7FN2g"}
725        let details = CIP25MiniMetadataDetails::loose_parse(&TransactionMetadatum::from_bytes(hex::decode("a664446174656a39204d617920323032316b4465736372697074696f6e782b4861707079204d6f7468657227732044617920746f20616c6c207468652043617264616e6f204d6f6d732165496d616765783b697066732e696f2f697066732f516d61683651504b554b7670364b39585142325341343251337972666643625942626b38456f52724237464e3267644e616d65714d6f746865722773204461792032303231665469636b6572654d4f4d32316355524c783b697066732e696f2f697066732f516d61683651504b554b7670364b39585142325341343251337972666643625942626b38456f52724237464e3267").unwrap()).unwrap()).unwrap();
726        assert_eq!(details.name.unwrap().0, "Mother's Day 2021");
727    }
728
729    #[test]
730    fn id_no_name() {
731        // {"id":"00","image":"ipfs://QmSfYTF8B4ua6hFdr6URdRDZBZ9FjCQNUdDcLr2f7P8xn3"}
732        let details = CIP25MiniMetadataDetails::loose_parse(&TransactionMetadatum::from_bytes(hex::decode("a262696462303065696d6167657835697066733a2f2f516d5366595446384234756136684664723655526452445a425a39466a43514e556444634c723266375038786e33").unwrap()).unwrap()).unwrap();
733        assert_eq!(details.name.unwrap().0, "00");
734    }
735
736    #[test]
737    fn just_image() {
738        // {"image":"ipfs://QmSfYTF8B4ua6hFdr6URdRDZBZ9FjCQNUdDcLr2f7P8xn3"}
739        let details = CIP25MiniMetadataDetails::loose_parse(&TransactionMetadatum::from_bytes(hex::decode("a165696d6167657835697066733a2f2f516d5366595446384234756136684664723655526452445a425a39466a43514e556444634c723266375038786e33").unwrap()).unwrap()).unwrap();
740        assert_eq!(
741            String::from(&details.image.unwrap()),
742            "ipfs://QmSfYTF8B4ua6hFdr6URdRDZBZ9FjCQNUdDcLr2f7P8xn3"
743        );
744    }
745
746    #[test]
747    fn noisy_metadata() {
748        // generated by adding this to the create() test case at the bottom:
749
750        // as_metadata.insert(1337, TransactionMetadatum::new_list(vec![
751        //     TransactionMetadatum::new_bytes(vec![0xBA, 0xAD, 0xF0, 0x0D]),
752        // ]));
753        // let label_metadatum_entries: &mut _ = match as_metadata.get_mut(&721).unwrap() {
754        //     TransactionMetadatum::Map(map) => map.entries,
755        //     _ => panic!(),
756        // };
757        // let mut filler_map = OrderedHashMap::new();
758        // filler_map.insert(
759        //     TransactionMetadatum::new_bytes(vec![]),
760        //     TransactionMetadatum::new_int(cml_core::Int::new_nint(100))
761        // );
762        // label_metadatum_entries.insert(TransactionMetadatum::new_map(filler_map.clone()), TransactionMetadatum::new_map(filler_map.clone()));
763        // let data_entries: &mut _ = match label_metadatum_entries.get_mut(&TransactionMetadatum::new_text("data".to_owned())).unwrap() {
764        //     TransactionMetadatum::Map{ map.entries, .. } => map.entries,
765        //     _ => panic!(),
766        // };
767        // data_entries.insert(TransactionMetadatum::new_map(filler_map.clone()), TransactionMetadatum::new_map(filler_map.clone()));
768        // let policy_entries: &mut _ = match data_entries.get_mut(&TransactionMetadatum::new_bytes(policy_id_bytes.to_vec())).unwrap() {
769        //     TransactionMetadatum::Map{ map.entries, .. } => map.entries,
770        //     _ => panic!(),
771        // };
772        // policy_entries.insert(TransactionMetadatum::new_map(filler_map.clone()), TransactionMetadatum::new_map(filler_map.clone()));
773        // policy_entries.insert(
774        //     TransactionMetadatum::new_list(vec![TransactionMetadatum::new_map(filler_map.clone())]),
775        //     TransactionMetadatum::new_list(vec![TransactionMetadatum::new_text("dskjfaks".to_owned())])
776        // );
777        // let details: &mut _ = match policy_entries.get_mut(&TransactionMetadatum::new_bytes(vec![0xCA, 0xFE, 0xD0, 0x0D])).unwrap() {
778        //     TransactionMetadatum::Map(map) => map.entries,
779        //     _ => panic!(),
780        // };
781        // details.insert(
782        //     TransactionMetadatum::new_map(filler_map.clone()),
783        //     TransactionMetadatum::new_int(cml_core::Int::new_uint(50))
784        // );
785        // let file_details: &mut _ = match details.get_mut(&TransactionMetadatum::new_text("files".to_owned())).unwrap() {
786        //     TransactionMetadatum::List{ elements, .. } => match elements.get_mut(0).unwrap() {
787        //         TransactionMetadatum::Map{ map.entries, .. } => map.entries,
788        //         _ => panic!(),
789        //     },
790        //     _ => panic!(),
791        // };
792        // file_details.insert(
793        //     TransactionMetadatum::new_list(vec![TransactionMetadatum::new_text("dskjfaks".to_owned())]),
794        //     TransactionMetadatum::new_list(vec![TransactionMetadatum::new_map(filler_map.clone())])
795        // );
796        // let mut buf = cbor_event::se::Serializer::new_vec();
797        // buf.write_map(cbor_event::Len::Indefinite).unwrap();
798        // for (label, datum) in as_metadata.iter() {
799        //     buf.write_unsigned_integer(*label).unwrap();
800        //     datum.serialize(&mut buf, false).unwrap();
801        // }
802        // buf.write_special(cbor_event::Special::Break).unwrap();
803        // panic!("{}", hex::encode(buf.finalize()));
804
805        let bytes = "bf1902d1a36464617461a2581cbaadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00da344cafed00da6646e616d656d4d65746164617461204e616d656566696c657382a4637372636473726331646e616d656966696c656e616d6531696d65646961547970656966696c657479706531816864736b6a66616b7381a1403864a3637372636473726332646e616d656966696c656e616d6532696d65646961547970656966696c65747970653265696d6167657821687474733a2f2f736f6d652e776562736974652e636f6d2f696d6167652e706e67696d656469615479706567696d6167652f2a6b6465736372697074696f6e776465736372697074696f6e206f662074686973204e4654a14038641832a1403864a140386481a1403864816864736b6a66616b73a1403864a14038646776657273696f6e02a1403864a14038641905398144baadf00dff";
806        let _ = CIP25Metadata::from_bytes(hex::decode(bytes).unwrap()).unwrap();
807    }
808}