1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
use crate::{
    json::json_serialize::{JsonParseError, Value as JSONValue},
    plutus::{ConstrPlutusData, PlutusData, PlutusMap},
    utils::BigInteger,
};
use std::collections::BTreeMap;
use std::str::FromStr;

use wasm_bindgen::prelude::wasm_bindgen;

/// JSON <-> PlutusData conversion schemas.
/// Follows ScriptDataJsonSchema in cardano-cli defined at:
/// https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/ScriptData.hs#L254
///
/// All methods here have the following restrictions due to limitations on dependencies:
/// * JSON numbers above u64::MAX (positive) or below i64::MIN (negative) will throw errors
/// * Hex strings for bytes don't accept odd-length (half-byte) strings.
///      cardano-cli seems to support these however but it seems to be different than just 0-padding
///      on either side when tested so proceed with caution
#[wasm_bindgen]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum CardanoNodePlutusDatumSchema {
    /// ScriptDataJsonNoSchema in cardano-node.
    ///
    /// This is the format used by --script-data-value in cardano-cli
    /// This tries to accept most JSON but does not support the full spectrum of Plutus datums.
    /// From JSON:
    /// * null/true/false/floats NOT supported
    /// * strings starting with 0x are treated as hex bytes. All other strings are encoded as their utf8 bytes.
    /// To JSON:
    /// * ConstrPlutusData not supported in ANY FORM (neither keys nor values)
    /// * Lists not supported in keys
    /// * Maps not supported in keys
    ////
    BasicConversions,
    /// ScriptDataJsonDetailedSchema in cardano-node.
    ///
    /// This is the format used by --script-data-file in cardano-cli
    /// This covers almost all (only minor exceptions) Plutus datums, but the JSON must conform to a strict schema.
    /// The schema specifies that ALL keys and ALL values must be contained in a JSON map with 2 cases:
    /// 1. For ConstrPlutusData there must be two fields "constructor" contianing a number and "fields" containing its fields
    ///    e.g. { "constructor": 2, "fields": [{"int": 2}, {"list": [{"bytes": "CAFEF00D"}]}]}
    /// 2. For all other cases there must be only one field named "int", "bytes", "list" or "map"
    ///    BigInteger's value is a JSON number e.g. {"int": 100}
    ///    Bytes' value is a hex string representing the bytes WITHOUT any prefix e.g. {"bytes": "CAFEF00D"}
    ///    Lists' value is a JSON list of its elements encoded via the same schema e.g. {"list": [{"bytes": "CAFEF00D"}]}
    ///    Maps' value is a JSON list of objects, one for each key-value pair in the map, with keys "k" and "v"
    ///          respectively with their values being the plutus datum encoded via this same schema
    ///          e.g. {"map": [
    ///              {"k": {"int": 2}, "v": {"int": 5}},
    ///              {"k": {"map": [{"k": {"list": [{"int": 1}]}, "v": {"bytes": "FF03"}}]}, "v": {"list": []}}
    ///          ]}
    /// From JSON:
    /// * null/true/false/floats NOT supported
    /// * the JSON must conform to a very specific schema
    /// To JSON:
    /// * all Plutus datums should be fully supported outside of the integer range limitations outlined above.
    ////
    DetailedSchema,
}

#[derive(Debug, thiserror::Error)]
pub enum PlutusJsonError {
    #[error("JSON Parsing: {0}")]
    JsonParse(#[from] JsonParseError),
    #[error("JSON printing: {0}")]
    JsonPrinting(#[from] serde_json::Error),
    #[error("null not allowed in plutus datums")]
    NullFound,
    #[error("bools not allowed in plutus datums")]
    BoolFound,
    #[error(
        "DetailedSchema requires ALL JSON to be tagged objects, found: {:?}",
        0
    )]
    DetailedNonObject(JSONValue),
    #[error("Hex byte strings in detailed schema should NOT start with 0x and should just contain the hex characters")]
    DetailedHexWith0x,
    #[error("DetailedSchema key {0} does not match type {1:?}")]
    DetailedKeyMismatch(String, JSONValue),
    #[error("Invalid hex string: {0}")]
    InvalidHex(#[from] hex::FromHexError),
    #[error("entry format in detailed schema map object not correct. Needs to be of form {{\"k\": {{\"key_type\": key}}, \"v\": {{\"value_type\", value}}}}")]
    InvalidMapEntry,
    #[error("key '{0}' in tagged object not valid")]
    InvalidTag(String),
    #[error("Key requires DetailedSchema: {:?}", 0)]
    DetailedKeyInBasicSchema(PlutusData),
    #[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\"")]
    InvalidTaggedConstructor,
}

pub fn encode_json_str_to_plutus_datum(
    json: &str,
    schema: CardanoNodePlutusDatumSchema,
) -> Result<PlutusData, PlutusJsonError> {
    let value = JSONValue::from_string(json)?;
    encode_json_value_to_plutus_datum(value, schema)
}

pub fn encode_json_value_to_plutus_datum(
    value: JSONValue,
    schema: CardanoNodePlutusDatumSchema,
) -> Result<PlutusData, PlutusJsonError> {
    fn encode_string(
        s: &str,
        schema: CardanoNodePlutusDatumSchema,
        is_key: bool,
    ) -> Result<PlutusData, PlutusJsonError> {
        if schema == CardanoNodePlutusDatumSchema::BasicConversions {
            if let Some(stripped) = s.strip_prefix("0x") {
                // this must be a valid hex bytestring after
                hex::decode(stripped)
                    .map(PlutusData::new_bytes)
                    .map_err(Into::into)
            } else if is_key {
                // try as an integer
                match BigInteger::from_str(s) {
                    Ok(x) => Ok(PlutusData::new_integer(x)),
                    // if not, we use the utf8 bytes of the string instead directly
                    Err(_err) => Ok(PlutusData::new_bytes(s.as_bytes().to_vec())),
                }
            } else {
                // can only be UTF bytes if not in a key and not prefixed by 0x
                Ok(PlutusData::new_bytes(s.as_bytes().to_vec()))
            }
        } else if s.starts_with("0x") {
            Err(PlutusJsonError::DetailedHexWith0x)
        } else {
            hex::decode(s)
                .map(PlutusData::new_bytes)
                .map_err(Into::into)
        }
    }
    fn encode_array(
        json_arr: Vec<JSONValue>,
        schema: CardanoNodePlutusDatumSchema,
    ) -> Result<PlutusData, PlutusJsonError> {
        let mut arr = Vec::new();
        for value in json_arr {
            arr.push(encode_json_value_to_plutus_datum(value, schema)?);
        }
        Ok(PlutusData::new_list(arr))
    }
    match schema {
        CardanoNodePlutusDatumSchema::BasicConversions => match value {
            JSONValue::Null => Err(PlutusJsonError::NullFound),
            JSONValue::Bool(_) => Err(PlutusJsonError::BoolFound),
            JSONValue::Number(x) => Ok(PlutusData::new_integer(x)),
            // no strings in plutus so it's all bytes (as hex or utf8 printable)
            JSONValue::String(s) => encode_string(&s, schema, false),
            JSONValue::Array(json_arr) => encode_array(json_arr, schema),
            JSONValue::Object(json_obj) => {
                let mut map = PlutusMap::new();
                for (raw_key, raw_value) in json_obj {
                    let key = encode_string(&raw_key, schema, true)?;
                    let value = encode_json_value_to_plutus_datum(raw_value, schema)?;
                    map.set(key, value);
                }
                Ok(PlutusData::new_map(map))
            }
        },
        CardanoNodePlutusDatumSchema::DetailedSchema => match value {
            JSONValue::Object(obj) => {
                if obj.len() == 1 {
                    // all variants except tagged constructors
                    let (k, v) = obj.into_iter().next().unwrap();
                    match k.as_str() {
                        "int" => match v {
                            JSONValue::Number(x) => Ok(PlutusData::new_integer(x)),
                            _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
                        },
                        "bytes" => match v {
                            JSONValue::String(s) => encode_string(&s, schema, false),
                            _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
                        },
                        "list" => match v {
                            JSONValue::Array(arr) => encode_array(arr, schema),
                            _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
                        },
                        "map" => {
                            let mut map = PlutusMap::new();
                            let array = match v {
                                JSONValue::Array(array) => Ok(array),
                                _ => Err(PlutusJsonError::DetailedKeyMismatch(k, v)),
                            }?;

                            for entry in array {
                                let entry_obj = match entry {
                                    JSONValue::Object(obj) => Ok(obj),
                                    _ => Err(PlutusJsonError::InvalidMapEntry),
                                }?;
                                let raw_key =
                                    entry_obj.get("k").ok_or(PlutusJsonError::InvalidMapEntry)?;
                                let value =
                                    entry_obj.get("v").ok_or(PlutusJsonError::InvalidMapEntry)?;
                                let key =
                                    encode_json_value_to_plutus_datum(raw_key.clone(), schema)?;
                                map.set(
                                    key,
                                    encode_json_value_to_plutus_datum(value.clone(), schema)?,
                                );
                            }
                            Ok(PlutusData::new_map(map))
                        }
                        _invalid_key => Err(PlutusJsonError::InvalidTag(k)),
                    }
                } else {
                    // constructor with tagged variant
                    let variant = obj.get("constructor").and_then(|v| match v {
                        JSONValue::Number(number) => number.as_u64(),
                        _ => None,
                    });
                    let fields_json = obj.get("fields").and_then(|f| match f {
                        JSONValue::Array(arr) => Some(arr),
                        _ => None,
                    });
                    match (obj.len(), variant, fields_json) {
                        (2, Some(variant), Some(fields_json)) => {
                            let mut fields = Vec::new();
                            for field_json in fields_json {
                                let field =
                                    encode_json_value_to_plutus_datum(field_json.clone(), schema)?;
                                fields.push(field);
                            }
                            Ok(PlutusData::new_constr_plutus_data(ConstrPlutusData::new(
                                variant, fields,
                            )))
                        }
                        _ => Err(PlutusJsonError::InvalidTaggedConstructor),
                    }
                }
            }
            _ => Err(PlutusJsonError::DetailedNonObject(value)),
        },
    }
}

pub fn decode_plutus_datum_to_json_str(
    datum: &PlutusData,
    schema: CardanoNodePlutusDatumSchema,
) -> Result<String, PlutusJsonError> {
    decode_plutus_datum_to_json_value(datum, schema).and_then(|v| v.to_string().map_err(Into::into))
}

pub fn decode_plutus_datum_to_json_value(
    datum: &PlutusData,
    schema: CardanoNodePlutusDatumSchema,
) -> Result<JSONValue, PlutusJsonError> {
    let (type_tag, json_value) = match datum {
        PlutusData::ConstrPlutusData(constr) => {
            let mut obj = BTreeMap::new();
            obj.insert(
                String::from("constructor"),
                JSONValue::from(constr.alternative),
            );
            let mut fields = Vec::new();
            for field in constr.fields.iter() {
                fields.push(decode_plutus_datum_to_json_value(field, schema)?);
            }
            obj.insert(String::from("fields"), JSONValue::from(fields));
            (None, JSONValue::from(obj))
        }
        PlutusData::Map(map) => match schema {
            CardanoNodePlutusDatumSchema::BasicConversions => (
                None,
                JSONValue::from(
                    map.entries
                        .iter()
                        .map(|(key, value)| {
                            let json_key: String = match key {
                                PlutusData::ConstrPlutusData(_)
                                | PlutusData::Map(_)
                                | PlutusData::List { .. } => {
                                    Err(PlutusJsonError::DetailedKeyInBasicSchema(key.clone()))
                                }
                                PlutusData::Integer(x) => Ok(x.to_string()),
                                PlutusData::Bytes { bytes, .. } => String::from_utf8(bytes.clone())
                                    .or_else(|_err| Ok(format!("0x{}", hex::encode(bytes)))),
                            }?;
                            let json_value = decode_plutus_datum_to_json_value(value, schema)?;
                            Ok((json_key, json_value))
                        })
                        .collect::<Result<BTreeMap<String, JSONValue>, PlutusJsonError>>()?,
                ),
            ),
            CardanoNodePlutusDatumSchema::DetailedSchema => (
                Some("map"),
                JSONValue::from(
                    map.entries
                        .iter()
                        .map(|(key, value)| {
                            let k = decode_plutus_datum_to_json_value(key, schema)?;
                            let v = decode_plutus_datum_to_json_value(value, schema)?;
                            let mut kv_obj = BTreeMap::new();
                            kv_obj.insert(String::from("k"), k);
                            kv_obj.insert(String::from("v"), v);
                            Ok(JSONValue::from(kv_obj))
                        })
                        .collect::<Result<Vec<_>, PlutusJsonError>>()?,
                ),
            ),
        },
        PlutusData::List { list, .. } => {
            let mut elems = Vec::new();
            for elem in list.iter() {
                elems.push(decode_plutus_datum_to_json_value(elem, schema)?);
            }
            (Some("list"), JSONValue::from(elems))
        }
        PlutusData::Integer(bigint) => (Some("int"), JSONValue::from(bigint.clone())),
        PlutusData::Bytes { bytes, .. } => (
            Some("bytes"),
            JSONValue::from(match schema {
                CardanoNodePlutusDatumSchema::BasicConversions => {
                    // cardano-cli converts to a string only if bytes are utf8 and all characters are printable
                    String::from_utf8(bytes.clone())
                        .ok()
                        .filter(|utf8| utf8.chars().all(|c| !c.is_control()))
                        // otherwise we hex-encode the bytes with a 0x prefix
                        .unwrap_or_else(|| format!("0x{}", hex::encode(bytes)))
                }
                CardanoNodePlutusDatumSchema::DetailedSchema => hex::encode(bytes),
            }),
        ),
    };
    match (type_tag, schema) {
        (Some(type_tag), CardanoNodePlutusDatumSchema::DetailedSchema) => {
            let mut wrapper = BTreeMap::new();
            wrapper.insert(String::from(type_tag), json_value);
            Ok(JSONValue::from(wrapper))
        }
        _ => Ok(json_value),
    }
}

#[cfg(test)]
mod tests {
    use crate::plutus::PlutusData;

    #[test]
    fn plutus_datum_json() {
        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\"}}]}";
        // let datum = encode_json_str_to_plutus_datum(json, crate::json::plutus_datums::CardanoNodePlutusDatumSchema::DetailedSchema).unwrap();
        let datum: PlutusData = serde_json::from_str(json).unwrap();
        assert_eq!(json, serde_json::to_string(&datum).unwrap());
    }
}