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 pub fn to_metadata(&self) -> Result<Metadata, DeserializeError> {
19 use std::convert::TryInto;
20 self.try_into()
21 }
22
23 pub fn from_metadata(metadata: &Metadata) -> Result<CIP25Metadata, DeserializeError> {
26 Self::try_from(metadata)
27 }
28
29 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#[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 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 .or_else(|| {
142 map.get(&TransactionMetadatum::new_text("Name".to_owned()).unwrap())
143 })
144 .or_else(|| {
146 map.get(&TransactionMetadatum::new_text("title".to_owned()).unwrap())
147 })
148 .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#[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 V1,
222 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 pub fn new(version: CIP25Version) -> Self {
236 Self {
237 nfts: BTreeMap::new(),
238 version,
239 }
240 }
241
242 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
281impl 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 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 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 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 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 let initial_position = raw.as_mut_ref().stream_position().unwrap();
335
336 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 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 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 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 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 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 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 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 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 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 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 read_len.read_elems(1)?;
573 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 read_len.read_elems(1)?;
589 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 Ok(data)
613 })(raw)
614 .map_err(|e| e.annotate("LabelMetadataV2"));
615 match deser_variant {
616 Ok(label_metadata_v2) => {
617 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 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 let bytes = "a569617277656176654964782b36737270585a4f54664b5f36324b55724a4b68345664434647305953323731707132304f4d52704535547365696d6167657835697066733a2f2f516d5557503678474875636742557635313467776762743479696a673336615551756e455036317a354438524b53646e616d656e53706163654275642023313530376674726169747385695374617220537569746a4368657374706c6174656442656c7464466c616766506973746f6c647479706565416c69656e";
696 CIP25MetadataDetails::from_bytes(hex::decode(bytes).unwrap()).unwrap();
697 }
698 {
699 let bytes = "a365636f6c6f72672345433937423665696d616765783a697066733a2f2f697066732f516d557662463273694846474752745a357a613156774e51387934396262746a6d59664659686745383968437132646e616d656a426572727920416c6261";
705 CIP25MetadataDetails::from_bytes(hex::decode(bytes).unwrap()).unwrap();
706 }
707 }
708
709 #[test]
710 fn just_name() {
711 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 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 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 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 let bytes = "bf1902d1a36464617461a2581cbaadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00dbaadf00da344cafed00da6646e616d656d4d65746164617461204e616d656566696c657382a4637372636473726331646e616d656966696c656e616d6531696d65646961547970656966696c657479706531816864736b6a66616b7381a1403864a3637372636473726332646e616d656966696c656e616d6532696d65646961547970656966696c65747970653265696d6167657821687474733a2f2f736f6d652e776562736974652e636f6d2f696d6167652e706e67696d656469615479706567696d6167652f2a6b6465736372697074696f6e776465736372697074696f6e206f662074686973204e4654a14038641832a1403864a140386481a1403864816864736b6a66616b73a1403864a14038646776657273696f6e02a1403864a14038641905398144baadf00dff";
806 let _ = CIP25Metadata::from_bytes(hex::decode(bytes).unwrap()).unwrap();
807 }
808}