cml_chain/json/
metadatums.rs

1#[cfg(not(feature = "used_from_wasm"))]
2use noop_proc_macro::wasm_bindgen;
3#[cfg(feature = "used_from_wasm")]
4use wasm_bindgen::prelude::wasm_bindgen;
5
6use crate::{
7    auxdata::{MetadatumMap, TransactionMetadatum},
8    json::json_serialize::{JsonParseError, Value as JSONValue},
9    utils::BigInteger,
10};
11
12use cml_core::{DeserializeError, Int};
13
14use std::collections::BTreeMap;
15use std::convert::TryFrom;
16
17#[wasm_bindgen]
18#[derive(Copy, Clone, Eq, PartialEq)]
19// Different schema methods for mapping between JSON and the metadata CBOR.
20// This conversion should match TxMetadataJsonSchema in cardano-node defined (at time of writing) here:
21// https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/MetaData.hs
22// but has 2 additional schemas for more or less conversionse
23// Note: Byte/Strings (including keys) in any schema must be at most 64 bytes in length
24pub enum MetadataJsonSchema {
25    // Does zero implicit conversions.
26    // Round-trip conversions are 100% consistent
27    // Treats maps DIRECTLY as maps in JSON in a natural way e.g. {"key1": 47, "key2": [0, 1]]}
28    // From JSON:
29    // * null/true/false NOT supported.
30    // * keys treated as strings only
31    // To JSON
32    // * Bytes, non-string keys NOT supported.
33    // Stricter than any TxMetadataJsonSchema in cardano-node but more natural for JSON -> Metadata
34    NoConversions,
35    // Does some implicit conversions.
36    // Round-trip conversions MD -> JSON -> MD is NOT consistent, but JSON -> MD -> JSON is.
37    // Without using bytes
38    // Maps are treated as an array of k-v pairs as such: [{"key1": 47}, {"key2": [0, 1]}, {"key3": "0xFFFF"}]
39    // From JSON:
40    // * null/true/false NOT supported.
41    // * Strings parseable as bytes (0x starting hex) or integers are converted.
42    // To JSON:
43    // * Non-string keys partially supported (bytes as 0x starting hex string, integer converted to string).
44    // * Bytes are converted to hex strings starting with 0x for both values and keys.
45    // Corresponds to TxMetadataJsonSchema's TxMetadataJsonNoSchema in cardano-node
46    BasicConversions,
47    // Supports the annotated schema presented in cardano-node with tagged values e.g. {"int": 7}, {"list": [0, 1]}
48    // Round-trip conversions are 100% consistent
49    // Maps are treated as an array of k-v pairs as such: [{"key1": {"int": 47}}, {"key2": {"list": [0, 1]}}, {"key3": {"bytes": "0xFFFF"}}]
50    // From JSON:
51    // * null/true/false NOT supported.
52    // * Strings parseable as bytes (hex WITHOUT 0x prefix) or integers converted.
53    // To JSON:
54    // * Non-string keys are supported. Any key parseable as JSON is encoded as metadata instead of a string
55    // Corresponds to TxMetadataJsonSchema's TxMetadataJsonDetailedSchema in cardano-node
56    DetailedSchema,
57}
58
59#[derive(Debug, thiserror::Error)]
60pub enum MetadataJsonError {
61    #[error("JSON Parsing: {0}")]
62    JsonParse(#[from] JsonParseError),
63    #[error("JSON printing: {0}")]
64    JsonPrinting(#[from] serde_json::Error),
65    #[error("null not allowed in metadatums")]
66    NullFound,
67    #[error("bools not allowed in metadatums")]
68    BoolFound,
69    #[error("DetailedSchema key {0} does not match type {1:?}")]
70    DetailedKeyMismatch(String, JSONValue),
71    #[error("entry format in detailed schema map object not correct. Needs to be of form {{\"k\": \"key\", \"v\": value}}")]
72    InvalidMapEntry,
73    #[error("key '{0}' in tagged object not valid")]
74    InvalidTag(String),
75    #[error(
76        "DetailedSchema requires ALL JSON to be tagged objects, found: {:?}",
77        0
78    )]
79    DetailedNonObject(JSONValue),
80    #[error("Invalid hex string: {0}")]
81    InvalidHex(#[from] hex::FromHexError),
82    #[error("Bytes not allowed in BasicConversions schema")]
83    BytesInNoConversions,
84    #[error("Metadatum ints must fit in 8 bytes: {0}")]
85    IntTooBig(BigInteger),
86    #[error("key type {0:?} not allowed in JSON under specified schema")]
87    InvalidKeyType(TransactionMetadatum),
88    #[error("Metadatum structure error (e.g. too big for bounds): {0}")]
89    InvalidStructure(#[from] DeserializeError),
90}
91
92fn supports_tagged_values(schema: MetadataJsonSchema) -> bool {
93    match schema {
94        MetadataJsonSchema::NoConversions | MetadataJsonSchema::BasicConversions => false,
95        MetadataJsonSchema::DetailedSchema => true,
96    }
97}
98
99fn hex_string_to_bytes(hex: &str) -> Option<Vec<u8>> {
100    if let Some(stripped) = hex.strip_prefix("0x") {
101        hex::decode(stripped).ok()
102    } else {
103        None
104    }
105}
106
107fn bytes_to_hex_string(bytes: &[u8]) -> String {
108    format!("0x{}", hex::encode(bytes))
109}
110
111/// Converts JSON to Metadata according to MetadataJsonSchema
112pub fn encode_json_str_to_metadatum(
113    json: &str,
114    schema: MetadataJsonSchema,
115) -> Result<TransactionMetadatum, MetadataJsonError> {
116    let value = JSONValue::from_string(json)?;
117    encode_json_value_to_metadatum(value, schema)
118}
119
120pub fn encode_json_value_to_metadatum(
121    value: JSONValue,
122    schema: MetadataJsonSchema,
123) -> Result<TransactionMetadatum, MetadataJsonError> {
124    fn encode_string(
125        s: String,
126        schema: MetadataJsonSchema,
127    ) -> Result<TransactionMetadatum, DeserializeError> {
128        if schema == MetadataJsonSchema::BasicConversions {
129            match hex_string_to_bytes(&s) {
130                Some(bytes) => TransactionMetadatum::new_bytes(bytes),
131                None => TransactionMetadatum::new_text(s),
132            }
133        } else {
134            TransactionMetadatum::new_text(s)
135        }
136    }
137    fn encode_array(
138        json_arr: Vec<JSONValue>,
139        schema: MetadataJsonSchema,
140    ) -> Result<TransactionMetadatum, MetadataJsonError> {
141        json_arr
142            .into_iter()
143            .map(|value| encode_json_value_to_metadatum(value, schema))
144            .collect::<Result<Vec<_>, MetadataJsonError>>()
145            .map(TransactionMetadatum::new_list)
146    }
147    match schema {
148        MetadataJsonSchema::NoConversions | MetadataJsonSchema::BasicConversions => match value {
149            JSONValue::Null => Err(MetadataJsonError::NullFound),
150            JSONValue::Bool(_) => Err(MetadataJsonError::BoolFound),
151            JSONValue::Number(x) => Ok(TransactionMetadatum::new_int(
152                x.as_int().ok_or(MetadataJsonError::IntTooBig(x.clone()))?,
153            )),
154            JSONValue::String(s) => encode_string(s, schema).map_err(Into::into),
155            JSONValue::Array(json_arr) => encode_array(json_arr, schema),
156            JSONValue::Object(json_obj) => {
157                let mut map = MetadatumMap::new();
158                for (raw_key, value) in json_obj {
159                    let key =
160                        if schema == MetadataJsonSchema::BasicConversions {
161                            match raw_key.parse::<i128>() {
162                                Ok(x) => TransactionMetadatum::new_int(Int::try_from(x).map_err(
163                                    |_e| MetadataJsonError::IntTooBig(BigInteger::from(x)),
164                                )?),
165                                Err(_) => encode_string(raw_key, schema)?,
166                            }
167                        } else {
168                            TransactionMetadatum::new_text(raw_key)?
169                        };
170                    map.set(key, encode_json_value_to_metadatum(value, schema)?);
171                }
172                Ok(TransactionMetadatum::new_map(map))
173            }
174        },
175        // we rely on tagged objects to control parsing here instead
176        MetadataJsonSchema::DetailedSchema => match value {
177            JSONValue::Object(obj) if obj.len() == 1 => {
178                let (k, v) = obj.into_iter().next().unwrap();
179                match k.as_str() {
180                    "int" => match v {
181                        JSONValue::Number(x) => Ok(TransactionMetadatum::new_int(
182                            x.as_int().ok_or(MetadataJsonError::IntTooBig(x.clone()))?,
183                        )),
184                        _ => Err(MetadataJsonError::DetailedKeyMismatch(k, v)),
185                    },
186                    "string" => match v {
187                        JSONValue::String(string) => {
188                            encode_string(string, schema).map_err(Into::into)
189                        }
190                        _ => Err(MetadataJsonError::DetailedKeyMismatch(k, v)),
191                    },
192                    "bytes" => match v {
193                        JSONValue::String(string) => hex::decode(string)
194                            .map_err(Into::into)
195                            .and_then(|b| TransactionMetadatum::new_bytes(b).map_err(Into::into)),
196                        _ => Err(MetadataJsonError::DetailedKeyMismatch(k, v)),
197                    },
198                    "list" => match v {
199                        JSONValue::Array(array) => encode_array(array, schema),
200                        _ => Err(MetadataJsonError::DetailedKeyMismatch(k, v)),
201                    },
202                    "map" => {
203                        let mut map = MetadatumMap::new();
204
205                        let array = match v {
206                            JSONValue::Array(array) => Ok(array),
207                            _ => Err(MetadataJsonError::DetailedKeyMismatch(k, v)),
208                        }?;
209                        for entry in array {
210                            let entry_obj = match entry {
211                                JSONValue::Object(obj) => Ok(obj),
212                                _ => Err(MetadataJsonError::InvalidMapEntry),
213                            }?;
214                            let raw_key = entry_obj
215                                .get("k")
216                                .ok_or(MetadataJsonError::InvalidMapEntry)?;
217                            let value = entry_obj
218                                .get("v")
219                                .ok_or(MetadataJsonError::InvalidMapEntry)?;
220                            let key = encode_json_value_to_metadatum(raw_key.clone(), schema)?;
221                            map.set(key, encode_json_value_to_metadatum(value.clone(), schema)?);
222                        }
223                        Ok(TransactionMetadatum::new_map(map))
224                    }
225                    _invalid_key => Err(MetadataJsonError::InvalidTag(k)),
226                }
227            }
228            _ => Err(MetadataJsonError::DetailedNonObject(value)),
229        },
230    }
231}
232
233/// Converts Metadata to JSON according to MetadataJsonSchema
234pub fn decode_metadatum_to_json_str(
235    metadatum: &TransactionMetadatum,
236    schema: MetadataJsonSchema,
237) -> Result<String, MetadataJsonError> {
238    let value = decode_metadatum_to_json_value(metadatum, schema)?;
239    value.to_string().map_err(Into::into)
240}
241
242pub fn decode_metadatum_to_json_value(
243    metadatum: &TransactionMetadatum,
244    schema: MetadataJsonSchema,
245) -> Result<JSONValue, MetadataJsonError> {
246    fn decode_key(
247        key: &TransactionMetadatum,
248        schema: MetadataJsonSchema,
249    ) -> Result<String, MetadataJsonError> {
250        match key {
251            TransactionMetadatum::Text { text, .. } => Ok(text.clone()),
252            TransactionMetadatum::Bytes { bytes, .. }
253                if schema != MetadataJsonSchema::NoConversions =>
254            {
255                Ok(bytes_to_hex_string(bytes.as_ref()))
256            }
257            TransactionMetadatum::Int(i) if schema != MetadataJsonSchema::NoConversions => {
258                Ok(i.to_string())
259            }
260            TransactionMetadatum::List { elements, .. }
261                if schema == MetadataJsonSchema::DetailedSchema =>
262            {
263                decode_metadatum_to_json_str(
264                    &TransactionMetadatum::new_list(elements.clone()),
265                    schema,
266                )
267            }
268            TransactionMetadatum::Map(map) if schema == MetadataJsonSchema::DetailedSchema => {
269                decode_metadatum_to_json_str(&TransactionMetadatum::new_map(map.clone()), schema)
270            }
271            _ => Err(MetadataJsonError::InvalidKeyType(key.clone())),
272        }
273    }
274    let (type_key, value) = match metadatum {
275        TransactionMetadatum::Map(map) => match schema {
276            MetadataJsonSchema::NoConversions | MetadataJsonSchema::BasicConversions => {
277                // treats maps directly as JSON maps
278                let mut json_map = BTreeMap::new();
279                for (key, value) in map.entries.iter() {
280                    json_map.insert(
281                        decode_key(key, schema)?,
282                        decode_metadatum_to_json_value(value, schema)?,
283                    );
284                }
285                ("map", JSONValue::from(json_map))
286            }
287
288            MetadataJsonSchema::DetailedSchema => (
289                "map",
290                JSONValue::from(
291                    map.entries
292                        .iter()
293                        .map(|(key, value)| {
294                            // must encode maps as JSON lists of objects with k/v keys
295                            // also in these schemas we support more key types than strings
296                            let k = decode_metadatum_to_json_value(key, schema)?;
297                            let v = decode_metadatum_to_json_value(value, schema)?;
298                            let mut kv_obj = BTreeMap::new();
299                            kv_obj.insert(String::from("k"), k);
300                            kv_obj.insert(String::from("v"), v);
301                            Ok(JSONValue::from(kv_obj))
302                        })
303                        .collect::<Result<Vec<_>, MetadataJsonError>>()?,
304                ),
305            ),
306        },
307        TransactionMetadatum::List { elements, .. } => (
308            "list",
309            JSONValue::from(
310                elements
311                    .iter()
312                    .map(|e| decode_metadatum_to_json_value(e, schema))
313                    .collect::<Result<Vec<_>, MetadataJsonError>>()?,
314            ),
315        ),
316        TransactionMetadatum::Int(x) => ("int", JSONValue::Number(BigInteger::from_int(x))),
317        TransactionMetadatum::Bytes { bytes, .. } => (
318            "bytes",
319            match schema {
320                MetadataJsonSchema::NoConversions => Err(MetadataJsonError::BytesInNoConversions),
321                // 0x prefix
322                MetadataJsonSchema::BasicConversions => {
323                    Ok(JSONValue::from(bytes_to_hex_string(bytes.as_ref())))
324                }
325                // no prefix
326                MetadataJsonSchema::DetailedSchema => Ok(JSONValue::from(hex::encode(bytes))),
327            }?,
328        ),
329        TransactionMetadatum::Text { text, .. } => ("string", JSONValue::from(text.clone())),
330    };
331    // potentially wrap value in a keyed map to represent more types
332    if supports_tagged_values(schema) {
333        let mut wrapper = BTreeMap::new();
334        wrapper.insert(String::from(type_key), value);
335        Ok(JSONValue::from(wrapper))
336    } else {
337        Ok(value)
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn json_encoding_no_conversions() {
347        let input_str = "{\"receiver_id\": \"SJKdj34k3jjKFDKfjFUDfdjkfd\",\"sender_id\": \"jkfdsufjdk34h3Sdfjdhfduf873\",\"comment\": \"happy birthday\",\"tags\": [0, 264, -1024, 32]}";
348        let metadata = encode_json_str_to_metadatum(input_str, MetadataJsonSchema::NoConversions)
349            .expect("encode failed");
350        let map = metadata.as_map().unwrap();
351        assert_eq!(
352            map.get_str("receiver_id").unwrap().as_text().unwrap(),
353            "SJKdj34k3jjKFDKfjFUDfdjkfd"
354        );
355        assert_eq!(
356            map.get_str("sender_id").unwrap().as_text().unwrap(),
357            "jkfdsufjdk34h3Sdfjdhfduf873"
358        );
359        assert_eq!(
360            map.get_str("comment").unwrap().as_text().unwrap(),
361            "happy birthday"
362        );
363        let tags = map.get_str("tags").unwrap().as_list().unwrap();
364        let tags_i32 = tags
365            .iter()
366            .map(|md| md.as_int().unwrap().into())
367            .collect::<Vec<i128>>();
368        assert_eq!(tags_i32, vec![0, 264, -1024, 32]);
369        let output_str = decode_metadatum_to_json_str(&metadata, MetadataJsonSchema::NoConversions)
370            .expect("decode failed");
371        let input_json: serde_json::Value = serde_json::from_str(input_str).unwrap();
372        let output_json: serde_json::Value = serde_json::from_str(&output_str).unwrap();
373        assert_eq!(input_json, output_json);
374    }
375
376    #[test]
377    fn json_encoding_basic() {
378        let input_str =
379            "{\"0x8badf00d\": \"0xdeadbeef\",\"9\": 5,\"obj\": {\"a\":[{\"5\": 2},{}]}}";
380        let metadata =
381            encode_json_str_to_metadatum(input_str, MetadataJsonSchema::BasicConversions)
382                .expect("encode failed");
383        json_encoding_check_example_metadatum(&metadata);
384        let output_str =
385            decode_metadatum_to_json_str(&metadata, MetadataJsonSchema::BasicConversions)
386                .expect("decode failed");
387        let input_json: serde_json::Value = serde_json::from_str(input_str).unwrap();
388        let output_json: serde_json::Value = serde_json::from_str(&output_str).unwrap();
389        assert_eq!(input_json, output_json);
390    }
391
392    #[test]
393    fn json_encoding_detailed() {
394        let input_str = "{\"map\":[
395            {
396                \"k\":{\"bytes\":\"8badf00d\"},
397                \"v\":{\"bytes\":\"deadbeef\"}
398            },
399            {
400                \"k\":{\"int\":9},
401                \"v\":{\"int\":5}
402            },
403            {
404                \"k\":{\"string\":\"obj\"},
405                \"v\":{\"map\":[
406                    {
407                        \"k\":{\"string\":\"a\"},
408                        \"v\":{\"list\":[
409                        {\"map\":[
410                            {
411                                \"k\":{\"int\":5},
412                                \"v\":{\"int\":2}
413                            }
414                            ]},
415                            {\"map\":[
416                            ]}
417                        ]}
418                    }
419                ]}
420            }
421        ]}";
422        let metadata = encode_json_str_to_metadatum(input_str, MetadataJsonSchema::DetailedSchema)
423            .expect("encode failed");
424        json_encoding_check_example_metadatum(&metadata);
425        let output_str =
426            decode_metadatum_to_json_str(&metadata, MetadataJsonSchema::DetailedSchema)
427                .expect("decode failed");
428        let input_json: serde_json::Value = serde_json::from_str(input_str).unwrap();
429        let output_json: serde_json::Value = serde_json::from_str(&output_str).unwrap();
430        assert_eq!(input_json, output_json);
431    }
432
433    fn json_encoding_check_example_metadatum(metadata: &TransactionMetadatum) {
434        let map = metadata.as_map().unwrap();
435        assert_eq!(
436            *map.get(&TransactionMetadatum::new_bytes(hex::decode("8badf00d").unwrap()).unwrap())
437                .unwrap()
438                .as_bytes()
439                .unwrap(),
440            hex::decode("deadbeef").unwrap()
441        );
442        assert_eq!(
443            i128::from(
444                map.get(&TransactionMetadatum::new_int(Int::from(9u64)))
445                    .unwrap()
446                    .as_int()
447                    .unwrap()
448            ),
449            5
450        );
451        let inner_map = map.get_str("obj").unwrap().as_map().unwrap();
452        let a = inner_map.get_str("a").unwrap().as_list().unwrap();
453        let a1 = a[0].as_map().unwrap();
454        assert_eq!(
455            i128::from(
456                a1.get(&TransactionMetadatum::new_int(Int::from(5u64)))
457                    .unwrap()
458                    .as_int()
459                    .unwrap()
460            ),
461            2
462        );
463        let a2 = a[1].as_map().unwrap();
464        assert_eq!(a2.len(), 0);
465    }
466
467    #[test]
468    fn json_encoding_detailed_complex_key() {
469        let input_str = "{\"map\":[
470            {
471            \"k\":{\"list\":[
472                {\"map\": [
473                    {
474                        \"k\": {\"int\": 5},
475                        \"v\": {\"int\": -7}
476                    },
477                    {
478                        \"k\": {\"string\": \"hello\"},
479                        \"v\": {\"string\": \"world\"}
480                    }
481                ]},
482                {\"bytes\": \"ff00ff00\"}
483            ]},
484            \"v\":{\"int\":5}
485            }
486        ]}";
487        let metadata = encode_json_str_to_metadatum(input_str, MetadataJsonSchema::DetailedSchema)
488            .expect("encode failed");
489
490        let map = metadata.as_map().unwrap();
491        let (k, v) = map.entries.first().unwrap();
492        assert_eq!(i128::from(v.as_int().unwrap()), 5i128);
493        let key_list = k.as_list().unwrap();
494        assert_eq!(key_list.len(), 2);
495        let key_map = key_list[0].as_map().unwrap();
496        assert_eq!(
497            i128::from(
498                key_map
499                    .get(&TransactionMetadatum::new_int(Int::from(5u64)))
500                    .unwrap()
501                    .as_int()
502                    .unwrap()
503            ),
504            -7i128
505        );
506        assert_eq!(
507            key_map.get_str("hello").unwrap().as_text().unwrap(),
508            "world"
509        );
510        let key_bytes = key_list[1].as_bytes().unwrap();
511        assert_eq!(*key_bytes, hex::decode("ff00ff00").unwrap());
512
513        let output_str =
514            decode_metadatum_to_json_str(&metadata, MetadataJsonSchema::DetailedSchema)
515                .expect("decode failed");
516        let input_json: serde_json::Value = serde_json::from_str(input_str).unwrap();
517        let output_json: serde_json::Value = serde_json::from_str(&output_str).unwrap();
518        assert_eq!(input_json, output_json);
519    }
520}