cardano_serialization_lib/protocol_types/
metadata.rs

1use crate::*;
2use hashlink::LinkedHashMap;
3
4const MD_MAX_LEN: usize = 64;
5
6#[wasm_bindgen]
7#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct MetadataMap(pub(crate) LinkedHashMap<TransactionMetadatum, TransactionMetadatum>);
9
10to_from_bytes!(MetadataMap);
11
12#[wasm_bindgen]
13impl MetadataMap {
14    pub fn new() -> Self {
15        Self(LinkedHashMap::new())
16    }
17
18    pub fn len(&self) -> usize {
19        self.0.len()
20    }
21
22    pub fn insert(
23        &mut self,
24        key: &TransactionMetadatum,
25        value: &TransactionMetadatum,
26    ) -> Option<TransactionMetadatum> {
27        self.0.insert(key.clone(), value.clone())
28    }
29
30    // convenience function for inserting as a string key
31    pub fn insert_str(
32        &mut self,
33        key: &str,
34        value: &TransactionMetadatum,
35    ) -> Result<Option<TransactionMetadatum>, JsError> {
36        Ok(self.insert(&TransactionMetadatum::new_text(key.to_owned())?, value))
37    }
38
39    // convenience function for inserting 32-bit integers - for higher-precision integers use insert() with an Int struct
40    pub fn insert_i32(
41        &mut self,
42        key: i32,
43        value: &TransactionMetadatum,
44    ) -> Option<TransactionMetadatum> {
45        self.insert(&TransactionMetadatum::new_int(&Int::new_i32(key)), value)
46    }
47
48    pub fn get(&self, key: &TransactionMetadatum) -> Result<TransactionMetadatum, JsError> {
49        self.0
50            .get(key)
51            .map(|v| v.clone())
52            .ok_or_else(|| JsError::from_str(&format!("key {:?} not found", key)))
53    }
54
55    // convenience function for retrieving a string key
56    pub fn get_str(&self, key: &str) -> Result<TransactionMetadatum, JsError> {
57        self.get(&TransactionMetadatum::new_text(key.to_owned())?)
58    }
59
60    // convenience function for retrieving 32-bit integer keys - for higher-precision integers use get() with an Int struct
61    pub fn get_i32(&self, key: i32) -> Result<TransactionMetadatum, JsError> {
62        self.get(&TransactionMetadatum::new_int(&Int::new_i32(key)))
63    }
64
65    pub fn has(&self, key: &TransactionMetadatum) -> bool {
66        self.0.contains_key(key)
67    }
68
69    pub fn keys(&self) -> MetadataList {
70        MetadataList(
71            self.0
72                .iter()
73                .map(|(k, _v)| k.clone())
74                .collect::<Vec<TransactionMetadatum>>(),
75        )
76    }
77}
78
79#[wasm_bindgen]
80#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub struct MetadataList(pub(crate) Vec<TransactionMetadatum>);
82
83to_from_bytes!(MetadataList);
84
85#[wasm_bindgen]
86impl MetadataList {
87    pub fn new() -> Self {
88        Self(Vec::new())
89    }
90
91    pub fn len(&self) -> usize {
92        self.0.len()
93    }
94
95    pub fn get(&self, index: usize) -> TransactionMetadatum {
96        self.0[index].clone()
97    }
98
99    pub fn add(&mut self, elem: &TransactionMetadatum) {
100        self.0.push(elem.clone());
101    }
102}
103
104#[wasm_bindgen]
105#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
106pub enum TransactionMetadatumKind {
107    MetadataMap,
108    MetadataList,
109    Int,
110    Bytes,
111    Text,
112}
113
114#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub(crate) enum TransactionMetadatumEnum {
116    MetadataMap(MetadataMap),
117    MetadataList(MetadataList),
118    Int(Int),
119    Bytes(Vec<u8>),
120    Text(String),
121}
122
123#[wasm_bindgen]
124#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
125pub struct TransactionMetadatum(pub(crate) TransactionMetadatumEnum);
126
127to_from_bytes!(TransactionMetadatum);
128
129#[wasm_bindgen]
130impl TransactionMetadatum {
131    pub fn new_map(map: &MetadataMap) -> Self {
132        Self(TransactionMetadatumEnum::MetadataMap(map.clone()))
133    }
134
135    pub fn new_list(list: &MetadataList) -> Self {
136        Self(TransactionMetadatumEnum::MetadataList(list.clone()))
137    }
138
139    pub fn new_int(int: &Int) -> Self {
140        Self(TransactionMetadatumEnum::Int(int.clone()))
141    }
142
143    pub fn new_bytes(bytes: Vec<u8>) -> Result<TransactionMetadatum, JsError> {
144        if bytes.len() > MD_MAX_LEN {
145            Err(JsError::from_str(&format!(
146                "Max metadata bytes too long: {}, max = {}",
147                bytes.len(),
148                MD_MAX_LEN
149            )))
150        } else {
151            Ok(Self(TransactionMetadatumEnum::Bytes(bytes)))
152        }
153    }
154
155    pub fn new_text(text: String) -> Result<TransactionMetadatum, JsError> {
156        if text.len() > MD_MAX_LEN {
157            Err(JsError::from_str(&format!(
158                "Max metadata string too long: {}, max = {}",
159                text.len(),
160                MD_MAX_LEN
161            )))
162        } else {
163            Ok(Self(TransactionMetadatumEnum::Text(text)))
164        }
165    }
166
167    pub fn kind(&self) -> TransactionMetadatumKind {
168        match &self.0 {
169            TransactionMetadatumEnum::MetadataMap(_) => TransactionMetadatumKind::MetadataMap,
170            TransactionMetadatumEnum::MetadataList(_) => TransactionMetadatumKind::MetadataList,
171            TransactionMetadatumEnum::Int(_) => TransactionMetadatumKind::Int,
172            TransactionMetadatumEnum::Bytes(_) => TransactionMetadatumKind::Bytes,
173            TransactionMetadatumEnum::Text(_) => TransactionMetadatumKind::Text,
174        }
175    }
176
177    pub fn as_map(&self) -> Result<MetadataMap, JsError> {
178        match &self.0 {
179            TransactionMetadatumEnum::MetadataMap(x) => Ok(x.clone()),
180            _ => Err(JsError::from_str("not a map")),
181        }
182    }
183
184    pub fn as_list(&self) -> Result<MetadataList, JsError> {
185        match &self.0 {
186            TransactionMetadatumEnum::MetadataList(x) => Ok(x.clone()),
187            _ => Err(JsError::from_str("not a list")),
188        }
189    }
190
191    pub fn as_int(&self) -> Result<Int, JsError> {
192        match &self.0 {
193            TransactionMetadatumEnum::Int(x) => Ok(x.clone()),
194            _ => Err(JsError::from_str("not an int")),
195        }
196    }
197
198    pub fn as_bytes(&self) -> Result<Vec<u8>, JsError> {
199        match &self.0 {
200            TransactionMetadatumEnum::Bytes(x) => Ok(x.clone()),
201            _ => Err(JsError::from_str("not bytes")),
202        }
203    }
204
205    pub fn as_text(&self) -> Result<String, JsError> {
206        match &self.0 {
207            TransactionMetadatumEnum::Text(x) => Ok(x.clone()),
208            _ => Err(JsError::from_str("not text")),
209        }
210    }
211}
212
213impl serde::Serialize for TransactionMetadatum {
214    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
215    where
216        S: serde::Serializer,
217    {
218        let json_str = decode_metadatum_to_json_str(self, MetadataJsonSchema::DetailedSchema)
219            .map_err(|e| serde::ser::Error::custom(&format!("{:?}", e)))?;
220        serializer.serialize_str(&json_str)
221    }
222}
223
224impl<'de> serde::de::Deserialize<'de> for TransactionMetadatum {
225    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
226    where
227        D: serde::de::Deserializer<'de>,
228    {
229        let s = <String as serde::de::Deserialize>::deserialize(deserializer)?;
230        encode_json_str_to_metadatum(s.clone(), MetadataJsonSchema::DetailedSchema).map_err(|e| {
231            serde::de::Error::invalid_value(
232                serde::de::Unexpected::Str(&s),
233                &format!("{:?}", e).as_str(),
234            )
235        })
236    }
237}
238
239// just for now we'll do json-in-json until I can figure this out better
240// TODO: maybe not generate this? or how do we do this?
241impl JsonSchema for TransactionMetadatum {
242    fn schema_name() -> String {
243        String::from("TransactionMetadatum")
244    }
245    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
246        String::json_schema(gen)
247    }
248    fn is_referenceable() -> bool {
249        String::is_referenceable()
250    }
251}
252
253pub type TransactionMetadatumLabel = BigNum;
254
255#[wasm_bindgen]
256#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
257pub struct TransactionMetadatumLabels(pub(crate) Vec<TransactionMetadatumLabel>);
258
259to_from_bytes!(TransactionMetadatumLabels);
260
261#[wasm_bindgen]
262impl TransactionMetadatumLabels {
263    pub fn new() -> Self {
264        Self(Vec::new())
265    }
266
267    pub fn len(&self) -> usize {
268        self.0.len()
269    }
270
271    pub fn get(&self, index: usize) -> TransactionMetadatumLabel {
272        self.0[index].clone()
273    }
274
275    pub fn add(&mut self, elem: &TransactionMetadatumLabel) {
276        self.0.push(elem.clone());
277    }
278}
279
280#[wasm_bindgen]
281#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
282pub struct GeneralTransactionMetadata(
283    pub(crate) LinkedHashMap<TransactionMetadatumLabel, TransactionMetadatum>,
284);
285
286impl_to_from!(GeneralTransactionMetadata);
287
288#[wasm_bindgen]
289impl GeneralTransactionMetadata {
290    pub fn new() -> Self {
291        Self(LinkedHashMap::new())
292    }
293
294    pub fn len(&self) -> usize {
295        self.0.len()
296    }
297
298    pub fn insert(
299        &mut self,
300        key: &TransactionMetadatumLabel,
301        value: &TransactionMetadatum,
302    ) -> Option<TransactionMetadatum> {
303        self.0.insert(key.clone(), value.clone())
304    }
305
306    pub fn get(&self, key: &TransactionMetadatumLabel) -> Option<TransactionMetadatum> {
307        self.0.get(key).map(|v| v.clone())
308    }
309
310    pub fn keys(&self) -> TransactionMetadatumLabels {
311        TransactionMetadatumLabels(
312            self.0
313                .iter()
314                .map(|(k, _v)| k.clone())
315                .collect::<Vec<TransactionMetadatumLabel>>(),
316        )
317    }
318}
319
320impl serde::Serialize for GeneralTransactionMetadata {
321    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
322    where
323        S: serde::Serializer,
324    {
325        let map = self.0.iter().collect::<std::collections::BTreeMap<_, _>>();
326        map.serialize(serializer)
327    }
328}
329
330impl<'de> serde::de::Deserialize<'de> for GeneralTransactionMetadata {
331    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332    where
333        D: serde::de::Deserializer<'de>,
334    {
335        let map = <std::collections::BTreeMap<_, _> as serde::de::Deserialize>::deserialize(
336            deserializer,
337        )?;
338        Ok(Self(map.into_iter().collect()))
339    }
340}
341
342impl JsonSchema for GeneralTransactionMetadata {
343    fn schema_name() -> String {
344        String::from("GeneralTransactionMetadata")
345    }
346    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
347        std::collections::BTreeMap::<TransactionMetadatumLabel, TransactionMetadatum>::json_schema(
348            gen,
349        )
350    }
351    fn is_referenceable() -> bool {
352        std::collections::BTreeMap::<TransactionMetadatumLabel, TransactionMetadatum>::is_referenceable()
353    }
354}
355
356#[wasm_bindgen]
357#[derive(Clone, Debug, Ord, PartialOrd, serde::Serialize, serde::Deserialize, JsonSchema)]
358pub struct AuxiliaryData {
359    pub(crate) metadata: Option<GeneralTransactionMetadata>,
360    pub(crate) native_scripts: Option<NativeScripts>,
361    pub(crate) plutus_scripts: Option<PlutusScripts>,
362    pub(crate) prefer_alonzo_format: bool,
363}
364
365impl std::cmp::PartialEq<Self> for AuxiliaryData {
366    fn eq(&self, other: &Self) -> bool {
367        self.metadata.eq(&other.metadata)
368            && self.native_scripts.eq(&other.native_scripts)
369            && self.plutus_scripts.eq(&other.plutus_scripts)
370    }
371}
372
373impl std::cmp::Eq for AuxiliaryData {}
374
375impl_to_from!(AuxiliaryData);
376
377#[wasm_bindgen]
378impl AuxiliaryData {
379    pub fn new() -> Self {
380        Self {
381            metadata: None,
382            native_scripts: None,
383            plutus_scripts: None,
384            prefer_alonzo_format: false,
385        }
386    }
387
388    pub fn metadata(&self) -> Option<GeneralTransactionMetadata> {
389        self.metadata.clone()
390    }
391
392    pub fn set_metadata(&mut self, metadata: &GeneralTransactionMetadata) {
393        self.metadata = Some(metadata.clone());
394    }
395
396    pub fn native_scripts(&self) -> Option<NativeScripts> {
397        self.native_scripts.clone()
398    }
399
400    pub fn set_native_scripts(&mut self, native_scripts: &NativeScripts) {
401        self.native_scripts = Some(native_scripts.clone())
402    }
403
404    pub fn plutus_scripts(&self) -> Option<PlutusScripts> {
405        self.plutus_scripts.clone()
406    }
407
408    pub fn set_plutus_scripts(&mut self, plutus_scripts: &PlutusScripts) {
409        self.plutus_scripts = Some(plutus_scripts.clone())
410    }
411
412    pub fn prefer_alonzo_format(&self) -> bool {
413        self.prefer_alonzo_format.clone()
414    }
415
416    pub fn set_prefer_alonzo_format(&mut self, prefer: bool) {
417        self.prefer_alonzo_format = prefer
418    }
419}
420
421// encodes arbitrary bytes into chunks of 64 bytes (the limit for bytes) as a list to be valid Metadata
422#[wasm_bindgen]
423pub fn encode_arbitrary_bytes_as_metadatum(bytes: &[u8]) -> TransactionMetadatum {
424    let mut list = MetadataList::new();
425    for chunk in bytes.chunks(MD_MAX_LEN) {
426        // this should never fail as we are already chunking it
427        list.add(&TransactionMetadatum::new_bytes(chunk.to_vec()).unwrap());
428    }
429    TransactionMetadatum::new_list(&list)
430}
431
432// decodes from chunks of bytes in a list to a byte vector if that is the metadata format, otherwise returns None
433#[wasm_bindgen]
434pub fn decode_arbitrary_bytes_from_metadatum(
435    metadata: &TransactionMetadatum,
436) -> Result<Vec<u8>, JsError> {
437    let mut bytes = Vec::new();
438    for elem in metadata.as_list()?.0 {
439        bytes.append(&mut elem.as_bytes()?);
440    }
441    Ok(bytes)
442}
443
444#[wasm_bindgen]
445#[derive(Copy, Clone, Eq, PartialEq)]
446// Different schema methods for mapping between JSON and the metadata CBOR.
447// This conversion should match TxMetadataJsonSchema in cardano-node defined (at time of writing) here:
448// https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/MetaData.hs
449// but has 2 additional schemas for more or less conversionse
450// Note: Byte/Strings (including keys) in any schema must be at most 64 bytes in length
451pub enum MetadataJsonSchema {
452    // Does zero implicit conversions.
453    // Round-trip conversions are 100% consistent
454    // Treats maps DIRECTLY as maps in JSON in a natural way e.g. {"key1": 47, "key2": [0, 1]]}
455    // From JSON:
456    // * null/true/false NOT supported.
457    // * keys treated as strings only
458    // To JSON
459    // * Bytes, non-string keys NOT supported.
460    // Stricter than any TxMetadataJsonSchema in cardano-node but more natural for JSON -> Metadata
461    NoConversions,
462    // Does some implicit conversions.
463    // Round-trip conversions MD -> JSON -> MD is NOT consistent, but JSON -> MD -> JSON is.
464    // Without using bytes
465    // Maps are treated as an array of k-v pairs as such: [{"key1": 47}, {"key2": [0, 1]}, {"key3": "0xFFFF"}]
466    // From JSON:
467    // * null/true/false NOT supported.
468    // * Strings parseable as bytes (0x starting hex) or integers are converted.
469    // To JSON:
470    // * Non-string keys partially supported (bytes as 0x starting hex string, integer converted to string).
471    // * Bytes are converted to hex strings starting with 0x for both values and keys.
472    // Corresponds to TxMetadataJsonSchema's TxMetadataJsonNoSchema in cardano-node
473    BasicConversions,
474    // Supports the annotated schema presented in cardano-node with tagged values e.g. {"int": 7}, {"list": [0, 1]}
475    // Round-trip conversions are 100% consistent
476    // Maps are treated as an array of k-v pairs as such: [{"key1": {"int": 47}}, {"key2": {"list": [0, 1]}}, {"key3": {"bytes": "0xFFFF"}}]
477    // From JSON:
478    // * null/true/false NOT supported.
479    // * Strings parseable as bytes (hex WITHOUT 0x prefix) or integers converted.
480    // To JSON:
481    // * Non-string keys are supported. Any key parseable as JSON is encoded as metadata instead of a string
482    // Corresponds to TxMetadataJsonSchema's TxMetadataJsonDetailedSchema in cardano-node
483    DetailedSchema,
484}
485
486fn supports_tagged_values(schema: MetadataJsonSchema) -> bool {
487    match schema {
488        MetadataJsonSchema::NoConversions | MetadataJsonSchema::BasicConversions => false,
489        MetadataJsonSchema::DetailedSchema => true,
490    }
491}
492
493fn hex_string_to_bytes(hex: &str) -> Option<Vec<u8>> {
494    if hex.starts_with("0x") {
495        hex::decode(&hex[2..]).ok()
496    } else {
497        None
498    }
499}
500
501fn bytes_to_hex_string(bytes: &[u8]) -> String {
502    format!("0x{}", hex::encode(bytes))
503}
504
505// Converts JSON to Metadata according to MetadataJsonSchema
506#[wasm_bindgen]
507pub fn encode_json_str_to_metadatum(
508    json: String,
509    schema: MetadataJsonSchema,
510) -> Result<TransactionMetadatum, JsError> {
511    let value = serde_json::from_str(&json).map_err(|e| JsError::from_str(&e.to_string()))?;
512    encode_json_value_to_metadatum(value, schema)
513}
514
515pub fn encode_json_value_to_metadatum(
516    value: serde_json::Value,
517    schema: MetadataJsonSchema,
518) -> Result<TransactionMetadatum, JsError> {
519    use serde_json::Value;
520    fn encode_number(x: serde_json::Number) -> Result<TransactionMetadatum, JsError> {
521        if let Some(x) = x.as_u64() {
522            Ok(TransactionMetadatum::new_int(&Int::new(&x.into())))
523        } else if let Some(x) = x.as_i64() {
524            Ok(TransactionMetadatum::new_int(&Int::new_negative(
525                &(-x as u64).into(),
526            )))
527        } else {
528            Err(JsError::from_str("floats not allowed in metadata"))
529        }
530    }
531    fn encode_string(
532        s: String,
533        schema: MetadataJsonSchema,
534    ) -> Result<TransactionMetadatum, JsError> {
535        if schema == MetadataJsonSchema::BasicConversions {
536            match hex_string_to_bytes(&s) {
537                Some(bytes) => TransactionMetadatum::new_bytes(bytes),
538                None => TransactionMetadatum::new_text(s),
539            }
540        } else {
541            TransactionMetadatum::new_text(s)
542        }
543    }
544    fn encode_array(
545        json_arr: Vec<Value>,
546        schema: MetadataJsonSchema,
547    ) -> Result<TransactionMetadatum, JsError> {
548        let mut arr = MetadataList::new();
549        for value in json_arr {
550            arr.add(&encode_json_value_to_metadatum(value, schema)?);
551        }
552        Ok(TransactionMetadatum::new_list(&arr))
553    }
554    match schema {
555        MetadataJsonSchema::NoConversions | MetadataJsonSchema::BasicConversions => match value {
556            Value::Null => Err(JsError::from_str("null not allowed in metadata")),
557            Value::Bool(_) => Err(JsError::from_str("bools not allowed in metadata")),
558            Value::Number(x) => encode_number(x),
559            Value::String(s) => encode_string(s, schema),
560            Value::Array(json_arr) => encode_array(json_arr, schema),
561            Value::Object(json_obj) => {
562                let mut map = MetadataMap::new();
563                for (raw_key, value) in json_obj {
564                    let key = if schema == MetadataJsonSchema::BasicConversions {
565                        match raw_key.parse::<i128>() {
566                            Ok(x) => TransactionMetadatum::new_int(&Int(x)),
567                            Err(_) => encode_string(raw_key, schema)?,
568                        }
569                    } else {
570                        TransactionMetadatum::new_text(raw_key)?
571                    };
572                    map.insert(&key, &encode_json_value_to_metadatum(value, schema)?);
573                }
574                Ok(TransactionMetadatum::new_map(&map))
575            }
576        },
577        // we rely on tagged objects to control parsing here instead
578        MetadataJsonSchema::DetailedSchema => match value {
579            Value::Object(obj) if obj.len() == 1 => {
580                let (k, v) = obj.into_iter().next().unwrap();
581                fn tag_mismatch() -> JsError {
582                    JsError::from_str("key does not match type")
583                }
584                match k.as_str() {
585                    "int" => match v {
586                        Value::Number(x) => encode_number(x),
587                        _ => Err(tag_mismatch()),
588                    },
589                    "string" => {
590                        encode_string(v.as_str().ok_or_else(tag_mismatch)?.to_owned(), schema)
591                    }
592                    "bytes" => match hex::decode(v.as_str().ok_or_else(tag_mismatch)?) {
593                        Ok(bytes) => TransactionMetadatum::new_bytes(bytes),
594                        Err(_) => Err(JsError::from_str(
595                            "invalid hex string in tagged byte-object",
596                        )),
597                    },
598                    "list" => encode_array(v.as_array().ok_or_else(tag_mismatch)?.clone(), schema),
599                    "map" => {
600                        let mut map = MetadataMap::new();
601                        fn map_entry_err() -> JsError {
602                            JsError::from_str("entry format in detailed schema map object not correct. Needs to be of form {\"k\": \"key\", \"v\": value}")
603                        }
604                        for entry in v.as_array().ok_or_else(tag_mismatch)? {
605                            let entry_obj = entry.as_object().ok_or_else(map_entry_err)?;
606                            let raw_key = entry_obj.get("k").ok_or_else(map_entry_err)?;
607                            let value = entry_obj.get("v").ok_or_else(map_entry_err)?;
608                            let key = encode_json_value_to_metadatum(raw_key.clone(), schema)?;
609                            map.insert(
610                                &key,
611                                &encode_json_value_to_metadatum(value.clone(), schema)?,
612                            );
613                        }
614                        Ok(TransactionMetadatum::new_map(&map))
615                    }
616                    invalid_key => Err(JsError::from_str(&format!(
617                        "key '{}' in tagged object not valid",
618                        invalid_key
619                    ))),
620                }
621            }
622            _ => Err(JsError::from_str(
623                "DetailedSchema requires types to be tagged objects",
624            )),
625        },
626    }
627}
628
629// Converts Metadata to JSON according to MetadataJsonSchema
630#[wasm_bindgen]
631pub fn decode_metadatum_to_json_str(
632    metadatum: &TransactionMetadatum,
633    schema: MetadataJsonSchema,
634) -> Result<String, JsError> {
635    let value = decode_metadatum_to_json_value(metadatum, schema)?;
636    serde_json::to_string(&value).map_err(|e| JsError::from_str(&e.to_string()))
637}
638
639pub fn decode_metadatum_to_json_value(
640    metadatum: &TransactionMetadatum,
641    schema: MetadataJsonSchema,
642) -> Result<serde_json::Value, JsError> {
643    use serde_json::Value;
644    use std::convert::TryFrom;
645    fn decode_key(
646        key: &TransactionMetadatum,
647        schema: MetadataJsonSchema,
648    ) -> Result<String, JsError> {
649        match &key.0 {
650            TransactionMetadatumEnum::Text(s) => Ok(s.clone()),
651            TransactionMetadatumEnum::Bytes(b) if schema != MetadataJsonSchema::NoConversions => {
652                Ok(bytes_to_hex_string(b.as_ref()))
653            }
654            TransactionMetadatumEnum::Int(i) if schema != MetadataJsonSchema::NoConversions => {
655                let int_str = if i.0 >= 0 {
656                    u64::try_from(i.0).map(|x| x.to_string())
657                } else {
658                    i64::try_from(i.0).map(|x| x.to_string())
659                };
660                int_str.map_err(|e| JsError::from_str(&e.to_string()))
661            }
662            TransactionMetadatumEnum::MetadataList(list)
663                if schema == MetadataJsonSchema::DetailedSchema =>
664            {
665                decode_metadatum_to_json_str(&TransactionMetadatum::new_list(&list), schema)
666            }
667            TransactionMetadatumEnum::MetadataMap(map)
668                if schema == MetadataJsonSchema::DetailedSchema =>
669            {
670                decode_metadatum_to_json_str(&TransactionMetadatum::new_map(&map), schema)
671            }
672            _ => Err(JsError::from_str(&format!(
673                "key type {:?} not allowed in JSON under specified schema",
674                key.0
675            ))),
676        }
677    }
678    let (type_key, value) = match &metadatum.0 {
679        TransactionMetadatumEnum::MetadataMap(map) => match schema {
680            MetadataJsonSchema::NoConversions | MetadataJsonSchema::BasicConversions => {
681                // treats maps directly as JSON maps
682                let mut json_map = serde_json::map::Map::with_capacity(map.len());
683                for (key, value) in map.0.iter() {
684                    json_map.insert(
685                        decode_key(key, schema)?,
686                        decode_metadatum_to_json_value(value, schema)?,
687                    );
688                }
689                ("map", Value::from(json_map))
690            }
691
692            MetadataJsonSchema::DetailedSchema => (
693                "map",
694                Value::from(
695                    map.0
696                        .iter()
697                        .map(|(key, value)| {
698                            // must encode maps as JSON lists of objects with k/v keys
699                            // also in these schemas we support more key types than strings
700                            let k = decode_metadatum_to_json_value(key, schema)?;
701                            let v = decode_metadatum_to_json_value(value, schema)?;
702                            let mut kv_obj = serde_json::map::Map::with_capacity(2);
703                            kv_obj.insert(String::from("k"), Value::from(k));
704                            kv_obj.insert(String::from("v"), v);
705                            Ok(Value::from(kv_obj))
706                        })
707                        .collect::<Result<Vec<_>, JsError>>()?,
708                ),
709            ),
710        },
711        TransactionMetadatumEnum::MetadataList(arr) => (
712            "list",
713            Value::from(
714                arr.0
715                    .iter()
716                    .map(|e| decode_metadatum_to_json_value(e, schema))
717                    .collect::<Result<Vec<_>, JsError>>()?,
718            ),
719        ),
720        TransactionMetadatumEnum::Int(x) => (
721            "int",
722            if x.0 >= 0 {
723                Value::from(u64::try_from(x.0).map_err(|e| JsError::from_str(&e.to_string()))?)
724            } else {
725                Value::from(i64::try_from(x.0).map_err(|e| JsError::from_str(&e.to_string()))?)
726            },
727        ),
728        TransactionMetadatumEnum::Bytes(bytes) => (
729            "bytes",
730            match schema {
731                MetadataJsonSchema::NoConversions => Err(JsError::from_str(
732                    "bytes not allowed in JSON in specified schema",
733                )),
734                // 0x prefix
735                MetadataJsonSchema::BasicConversions => {
736                    Ok(Value::from(bytes_to_hex_string(bytes.as_ref())))
737                }
738                // no prefix
739                MetadataJsonSchema::DetailedSchema => Ok(Value::from(hex::encode(bytes))),
740            }?,
741        ),
742        TransactionMetadatumEnum::Text(s) => ("string", Value::from(s.clone())),
743    };
744    // potentially wrap value in a keyed map to represent more types
745    if supports_tagged_values(schema) {
746        let mut wrapper = serde_json::map::Map::with_capacity(1);
747        wrapper.insert(String::from(type_key), value);
748        Ok(Value::from(wrapper))
749    } else {
750        Ok(value)
751    }
752}