cml_chain/json/
plutus_datums.rs

1use crate::{
2    json::json_serialize::{JsonParseError, Value as JSONValue},
3    plutus::{ConstrPlutusData, PlutusData, PlutusMap},
4    utils::BigInteger,
5};
6use std::collections::BTreeMap;
7use std::str::FromStr;
8
9#[cfg(not(feature = "used_from_wasm"))]
10use noop_proc_macro::wasm_bindgen;
11#[cfg(feature = "used_from_wasm")]
12use wasm_bindgen::prelude::wasm_bindgen;
13
14/// JSON <-> PlutusData conversion schemas.
15/// Follows ScriptDataJsonSchema in cardano-cli defined at:
16/// https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/ScriptData.hs#L254
17///
18/// All methods here have the following restrictions due to limitations on dependencies:
19/// * JSON numbers above u64::MAX (positive) or below i64::MIN (negative) will throw errors
20/// * Hex strings for bytes don't accept odd-length (half-byte) strings.
21///      cardano-cli seems to support these however but it seems to be different than just 0-padding
22///      on either side when tested so proceed with caution
23#[wasm_bindgen]
24#[derive(Copy, Clone, Debug, Eq, PartialEq)]
25pub enum CardanoNodePlutusDatumSchema {
26    /// ScriptDataJsonNoSchema in cardano-node.
27    ///
28    /// This is the format used by --script-data-value in cardano-cli
29    /// This tries to accept most JSON but does not support the full spectrum of Plutus datums.
30    /// From JSON:
31    /// * null/true/false/floats NOT supported
32    /// * strings starting with 0x are treated as hex bytes. All other strings are encoded as their utf8 bytes.
33    /// To JSON:
34    /// * ConstrPlutusData not supported in ANY FORM (neither keys nor values)
35    /// * Lists not supported in keys
36    /// * Maps not supported in keys
37    ////
38    BasicConversions,
39    /// ScriptDataJsonDetailedSchema in cardano-node.
40    ///
41    /// This is the format used by --script-data-file in cardano-cli
42    /// This covers almost all (only minor exceptions) Plutus datums, but the JSON must conform to a strict schema.
43    /// The schema specifies that ALL keys and ALL values must be contained in a JSON map with 2 cases:
44    /// 1. For ConstrPlutusData there must be two fields "constructor" contianing a number and "fields" containing its fields
45    ///    e.g. { "constructor": 2, "fields": [{"int": 2}, {"list": [{"bytes": "CAFEF00D"}]}]}
46    /// 2. For all other cases there must be only one field named "int", "bytes", "list" or "map"
47    ///    BigInteger's value is a JSON number e.g. {"int": 100}
48    ///    Bytes' value is a hex string representing the bytes WITHOUT any prefix e.g. {"bytes": "CAFEF00D"}
49    ///    Lists' value is a JSON list of its elements encoded via the same schema e.g. {"list": [{"bytes": "CAFEF00D"}]}
50    ///    Maps' value is a JSON list of objects, one for each key-value pair in the map, with keys "k" and "v"
51    ///          respectively with their values being the plutus datum encoded via this same schema
52    ///          e.g. {"map": [
53    ///              {"k": {"int": 2}, "v": {"int": 5}},
54    ///              {"k": {"map": [{"k": {"list": [{"int": 1}]}, "v": {"bytes": "FF03"}}]}, "v": {"list": []}}
55    ///          ]}
56    /// From JSON:
57    /// * null/true/false/floats NOT supported
58    /// * the JSON must conform to a very specific schema
59    /// To JSON:
60    /// * all Plutus datums should be fully supported outside of the integer range limitations outlined above.
61    ////
62    DetailedSchema,
63}
64
65#[derive(Debug, thiserror::Error)]
66pub enum PlutusJsonError {
67    #[error("JSON Parsing: {0}")]
68    JsonParse(#[from] JsonParseError),
69    #[error("JSON printing: {0}")]
70    JsonPrinting(#[from] serde_json::Error),
71    #[error("null not allowed in plutus datums")]
72    NullFound,
73    #[error("bools not allowed in plutus datums")]
74    BoolFound,
75    #[error(
76        "DetailedSchema requires ALL JSON to be tagged objects, found: {:?}",
77        0
78    )]
79    DetailedNonObject(JSONValue),
80    #[error("Hex byte strings in detailed schema should NOT start with 0x and should just contain the hex characters")]
81    DetailedHexWith0x,
82    #[error("DetailedSchema key {0} does not match type {1:?}")]
83    DetailedKeyMismatch(String, JSONValue),
84    #[error("Invalid hex string: {0}")]
85    InvalidHex(#[from] hex::FromHexError),
86    #[error("entry format in detailed schema map object not correct. Needs to be of form {{\"k\": {{\"key_type\": key}}, \"v\": {{\"value_type\", value}}}}")]
87    InvalidMapEntry,
88    #[error("key '{0}' in tagged object not valid")]
89    InvalidTag(String),
90    #[error("Key requires DetailedSchema: {:?}", 0)]
91    DetailedKeyInBasicSchema(PlutusData),
92    #[error("detailed schemas must either have only one of the following keys: \"int\", \"bytes\", \"list\" or \"map\", or both of these 2 keys: \"constructor\" + \"fields\"")]
93    InvalidTaggedConstructor,
94}
95
96pub fn encode_json_str_to_plutus_datum(
97    json: &str,
98    schema: CardanoNodePlutusDatumSchema,
99) -> Result<PlutusData, PlutusJsonError> {
100    let value = JSONValue::from_string(json)?;
101    encode_json_value_to_plutus_datum(value, schema)
102}
103
104pub fn encode_json_value_to_plutus_datum(
105    value: JSONValue,
106    schema: CardanoNodePlutusDatumSchema,
107) -> Result<PlutusData, PlutusJsonError> {
108    fn encode_string(
109        s: &str,
110        schema: CardanoNodePlutusDatumSchema,
111        is_key: bool,
112    ) -> Result<PlutusData, PlutusJsonError> {
113        if schema == CardanoNodePlutusDatumSchema::BasicConversions {
114            if let Some(stripped) = s.strip_prefix("0x") {
115                // this must be a valid hex bytestring after
116                hex::decode(stripped)
117                    .map(PlutusData::new_bytes)
118                    .map_err(Into::into)
119            } else if is_key {
120                // try as an integer
121                match BigInteger::from_str(s) {
122                    Ok(x) => Ok(PlutusData::new_integer(x)),
123                    // if not, we use the utf8 bytes of the string instead directly
124                    Err(_err) => Ok(PlutusData::new_bytes(s.as_bytes().to_vec())),
125                }
126            } else {
127                // can only be UTF bytes if not in a key and not prefixed by 0x
128                Ok(PlutusData::new_bytes(s.as_bytes().to_vec()))
129            }
130        } else if s.starts_with("0x") {
131            Err(PlutusJsonError::DetailedHexWith0x)
132        } else {
133            hex::decode(s)
134                .map(PlutusData::new_bytes)
135                .map_err(Into::into)
136        }
137    }
138    fn encode_array(
139        json_arr: Vec<JSONValue>,
140        schema: CardanoNodePlutusDatumSchema,
141    ) -> Result<PlutusData, PlutusJsonError> {
142        let mut arr = Vec::new();
143        for value in json_arr {
144            arr.push(encode_json_value_to_plutus_datum(value, schema)?);
145        }
146        Ok(PlutusData::new_list(arr))
147    }
148    match schema {
149        CardanoNodePlutusDatumSchema::BasicConversions => match value {
150            JSONValue::Null => Err(PlutusJsonError::NullFound),
151            JSONValue::Bool(_) => Err(PlutusJsonError::BoolFound),
152            JSONValue::Number(x) => Ok(PlutusData::new_integer(x)),
153            // no strings in plutus so it's all bytes (as hex or utf8 printable)
154            JSONValue::String(s) => encode_string(&s, schema, false),
155            JSONValue::Array(json_arr) => encode_array(json_arr, schema),
156            JSONValue::Object(json_obj) => {
157                let mut map = PlutusMap::new();
158                for (raw_key, raw_value) in json_obj {
159                    let key = encode_string(&raw_key, schema, true)?;
160                    let value = encode_json_value_to_plutus_datum(raw_value, schema)?;
161                    map.set(key, value);
162                }
163                Ok(PlutusData::new_map(map))
164            }
165        },
166        CardanoNodePlutusDatumSchema::DetailedSchema => match value {
167            JSONValue::Object(obj) => {
168                if obj.len() == 1 {
169                    // all variants except tagged constructors
170                    let (k, v) = obj.into_iter().next().unwrap();
171                    match k.as_str() {
172                        "int" => match v {
173                            JSONValue::Number(x) => Ok(PlutusData::new_integer(x)),
174                            _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
175                        },
176                        "bytes" => match v {
177                            JSONValue::String(s) => encode_string(&s, schema, false),
178                            _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
179                        },
180                        "list" => match v {
181                            JSONValue::Array(arr) => encode_array(arr, schema),
182                            _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
183                        },
184                        "map" => {
185                            let mut map = PlutusMap::new();
186                            let array = match v {
187                                JSONValue::Array(array) => Ok(array),
188                                _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
189                            }?;
190
191                            for entry in array {
192                                let entry_obj = match entry {
193                                    JSONValue::Object(obj) => Ok(obj),
194                                    _ => Err(PlutusJsonError::InvalidMapEntry),
195                                }?;
196                                let raw_key =
197                                    entry_obj.get("k").ok_or(PlutusJsonError::InvalidMapEntry)?;
198                                let value =
199                                    entry_obj.get("v").ok_or(PlutusJsonError::InvalidMapEntry)?;
200                                let key =
201                                    encode_json_value_to_plutus_datum(raw_key.clone(), schema)?;
202                                map.set(
203                                    key,
204                                    encode_json_value_to_plutus_datum(value.clone(), schema)?,
205                                );
206                            }
207                            Ok(PlutusData::new_map(map))
208                        }
209                        _invalid_key => Err(PlutusJsonError::InvalidTag(k)),
210                    }
211                } else {
212                    // constructor with tagged variant
213                    let variant = obj.get("constructor").and_then(|v| match v {
214                        JSONValue::Number(number) => number.as_u64(),
215                        _ => None,
216                    });
217                    let fields_json = obj.get("fields").and_then(|f| match f {
218                        JSONValue::Array(arr) => Some(arr),
219                        _ => None,
220                    });
221                    match (obj.len(), variant, fields_json) {
222                        (2, Some(variant), Some(fields_json)) => {
223                            let mut fields = Vec::new();
224                            for field_json in fields_json {
225                                let field =
226                                    encode_json_value_to_plutus_datum(field_json.clone(), schema)?;
227                                fields.push(field);
228                            }
229                            Ok(PlutusData::new_constr_plutus_data(ConstrPlutusData::new(
230                                variant, fields,
231                            )))
232                        }
233                        _ => Err(PlutusJsonError::InvalidTaggedConstructor),
234                    }
235                }
236            }
237            _ => Err(PlutusJsonError::DetailedNonObject(value)),
238        },
239    }
240}
241
242pub fn decode_plutus_datum_to_json_str(
243    datum: &PlutusData,
244    schema: CardanoNodePlutusDatumSchema,
245) -> Result<String, PlutusJsonError> {
246    decode_plutus_datum_to_json_value(datum, schema).and_then(|v| v.to_string().map_err(Into::into))
247}
248
249pub fn decode_plutus_datum_to_json_value(
250    datum: &PlutusData,
251    schema: CardanoNodePlutusDatumSchema,
252) -> Result<JSONValue, PlutusJsonError> {
253    let (type_tag, json_value) = match datum {
254        PlutusData::ConstrPlutusData(constr) => {
255            let mut obj = BTreeMap::new();
256            obj.insert(
257                String::from("constructor"),
258                JSONValue::from(constr.alternative),
259            );
260            let mut fields = Vec::new();
261            for field in constr.fields.iter() {
262                fields.push(decode_plutus_datum_to_json_value(field, schema)?);
263            }
264            obj.insert(String::from("fields"), JSONValue::from(fields));
265            (None, JSONValue::from(obj))
266        }
267        PlutusData::Map(map) => match schema {
268            CardanoNodePlutusDatumSchema::BasicConversions => (
269                None,
270                JSONValue::from(
271                    map.entries
272                        .iter()
273                        .map(|(key, value)| {
274                            let json_key: String = match key {
275                                PlutusData::ConstrPlutusData(_)
276                                | PlutusData::Map(_)
277                                | PlutusData::List { .. } => {
278                                    Err(PlutusJsonError::DetailedKeyInBasicSchema(key.clone()))
279                                }
280                                PlutusData::Integer(x) => Ok(x.to_string()),
281                                PlutusData::Bytes { bytes, .. } => String::from_utf8(bytes.clone())
282                                    .or_else(|_err| Ok(format!("0x{}", hex::encode(bytes)))),
283                            }?;
284                            let json_value = decode_plutus_datum_to_json_value(value, schema)?;
285                            Ok((json_key, json_value))
286                        })
287                        .collect::<Result<BTreeMap<String, JSONValue>, PlutusJsonError>>()?,
288                ),
289            ),
290            CardanoNodePlutusDatumSchema::DetailedSchema => (
291                Some("map"),
292                JSONValue::from(
293                    map.entries
294                        .iter()
295                        .map(|(key, value)| {
296                            let k = decode_plutus_datum_to_json_value(key, schema)?;
297                            let v = decode_plutus_datum_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<_>, PlutusJsonError>>()?,
304                ),
305            ),
306        },
307        PlutusData::List { list, .. } => {
308            let mut elems = Vec::new();
309            for elem in list.iter() {
310                elems.push(decode_plutus_datum_to_json_value(elem, schema)?);
311            }
312            (Some("list"), JSONValue::from(elems))
313        }
314        PlutusData::Integer(bigint) => (Some("int"), JSONValue::from(bigint.clone())),
315        PlutusData::Bytes { bytes, .. } => (
316            Some("bytes"),
317            JSONValue::from(match schema {
318                CardanoNodePlutusDatumSchema::BasicConversions => {
319                    // cardano-cli converts to a string only if bytes are utf8 and all characters are printable
320                    String::from_utf8(bytes.clone())
321                        .ok()
322                        .filter(|utf8| utf8.chars().all(|c| !c.is_control()))
323                        // otherwise we hex-encode the bytes with a 0x prefix
324                        .unwrap_or_else(|| format!("0x{}", hex::encode(bytes)))
325                }
326                CardanoNodePlutusDatumSchema::DetailedSchema => hex::encode(bytes),
327            }),
328        ),
329    };
330    match (type_tag, schema) {
331        (Some(type_tag), CardanoNodePlutusDatumSchema::DetailedSchema) => {
332            let mut wrapper = BTreeMap::new();
333            wrapper.insert(String::from(type_tag), json_value);
334            Ok(JSONValue::from(wrapper))
335        }
336        _ => Ok(json_value),
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use crate::plutus::PlutusData;
343
344    #[test]
345    fn plutus_datum_json() {
346        let json = "{\"map\":[{\"k\":{\"int\":100},\"v\":{\"list\":[{\"map\":[{\"k\":{\"bytes\":\"78\"},\"v\":{\"bytes\":\"30\"}},{\"k\":{\"bytes\":\"79\"},\"v\":{\"int\":1}}]}]}},{\"k\":{\"bytes\":\"666f6f\"},\"v\":{\"bytes\":\"0000baadf00d0000cafed00d0000deadbeef0000\"}}]}";
347        // let datum = encode_json_str_to_plutus_datum(json, crate::json::plutus_datums::CardanoNodePlutusDatumSchema::DetailedSchema).unwrap();
348        let datum: PlutusData = serde_json::from_str(json).unwrap();
349        assert_eq!(json, serde_json::to_string(&datum).unwrap());
350    }
351}