From e848f4bb3679aa8e6ca78f149d0d725db739094f Mon Sep 17 00:00:00 2001 From: rooooooooob Date: Wed, 2 Feb 2022 11:39:07 -0800 Subject: [PATCH 1/4] cardano-cli Plutus Datum JSON format support this commit is just for the detailed schema (but has some parts of the basic/no-schema format) as I couldn't find test cases. Further commits will be made once I can get some test cases. Based on: https://github.com/input-output-hk/cardano-node/blob/c1efb2f97134c0607c982246a36e3da7266ac194/cardano-api/src/Cardano/Api/ScriptData.hs#L254 + using `cardano-cli`. --- rust/src/plutus.rs | 250 +++++++++++++++++++++++++++++++++++++++++++++ rust/src/utils.rs | 7 ++ 2 files changed, 257 insertions(+) diff --git a/rust/src/plutus.rs b/rust/src/plutus.rs index fcb9f70c..7792d078 100644 --- a/rust/src/plutus.rs +++ b/rust/src/plutus.rs @@ -616,7 +616,207 @@ impl Strings { +// json +#[wasm_bindgen] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PlutusDatumSchema { + BasicConversions, + DetailedSchema, +} + +#[wasm_bindgen] +pub fn encode_json_str_to_plutus_datum(json: &str, schema: PlutusDatumSchema) -> Result { + if schema == PlutusDatumSchema::BasicConversions { + todo!("will fully implement once some test cases can be found"); + } + let value = serde_json::from_str(json).map_err(|e| JsError::from_str(&e.to_string()))?; + encode_json_value_to_plutus_datum(value, schema) +} + +pub fn encode_json_value_to_plutus_datum(value: serde_json::Value, schema: PlutusDatumSchema) -> Result { + use serde_json::Value; + fn encode_number(x: serde_json::Number) -> Result { + if let Some(x) = x.as_u64() { + Ok(PlutusData::new_integer(&BigInt::from(x))) + } else if let Some(x) = x.as_i64() { + Ok(PlutusData::new_integer(&BigInt::from(x))) + } else { + Err(JsError::from_str("floats not allowed in plutus datums")) + } + } + fn encode_hex_bytes(s: &str, schema: PlutusDatumSchema) -> Result { + let bytes = if schema == PlutusDatumSchema::BasicConversions { + if s.starts_with("0x") { + hex::decode(&s[2..]).map_err(|e| JsError::from_str(&e.to_string())) + } else { + Err(JsError::from_str("Hex byte strings in schemaless mode MUST start with \"0x\" before the hex characters")) + } + } else { + if s.starts_with("0x") { + Err(JsError::from_str("Hex byte strings in detailed schema should NOT start with 0x and should just contain the hex characters")) + } else { + hex::decode(s).map_err(|e| JsError::from_str(&e.to_string())) + } + }; + Ok(PlutusData::new_bytes(bytes?)) + } + fn encode_array(json_arr: Vec, schema: PlutusDatumSchema) -> Result { + let mut arr = PlutusList::new(); + for value in json_arr { + arr.add(&encode_json_value_to_plutus_datum(value, schema)?); + } + Ok(PlutusData::new_list(&arr)) + } + match schema { + PlutusDatumSchema::BasicConversions => match value { + Value::Null => Err(JsError::from_str("null not allowed in plutus datums")), + Value::Bool(_) => Err(JsError::from_str("bools not allowed in plutus datums")), + Value::Number(x) => encode_number(x), + // actually only for byte strings + Value::String(s) => encode_hex_bytes(&s, schema), + Value::Array(json_arr) => encode_array(json_arr, schema), + Value::Object(json_obj) => { + // here keys are JSON-in-JSON + let mut map = PlutusMap::new(); + for (raw_key, value) in json_obj { + //let key = encode_json_str_to_plutus_datum(raw_key, schema) + // .map_err(|e| JsError::from_str("Key \"{}\" is invalid JSON: {}", raw_key, e))?; + + todo!(); + } + Ok(PlutusData::new_map(&map)) + }, + }, + PlutusDatumSchema::DetailedSchema => match value { + Value::Object(obj) => { + if obj.len() == 1 { + // all variants except tagged constructors + let (k, v) = obj.into_iter().next().unwrap(); + fn tag_mismatch() -> JsError { + JsError::from_str("key does not match type") + } + match k.as_str() { + "int" => match v { + Value::Number(x) => encode_number(x), + _ => Err(tag_mismatch()), + }, + "bytes" => encode_hex_bytes(v.as_str().ok_or_else(tag_mismatch)?, schema), + "list" => encode_array(v.as_array().ok_or_else(tag_mismatch)?.clone(), schema), + "map" => { + let mut map = PlutusMap::new(); + fn map_entry_err() -> JsError { + JsError::from_str("entry format in detailed schema map object not correct. Needs to be of form {\"k\": {\"key_type\": key}, \"v\": {\"value_type\", value}}") + } + for entry in v.as_array().ok_or_else(tag_mismatch)? { + let entry_obj = entry.as_object().ok_or_else(map_entry_err)?; + let raw_key = entry_obj + .get("k") + .ok_or_else(map_entry_err)?; + let value = entry_obj + .get("v") + .ok_or_else(map_entry_err)?; + let key = encode_json_value_to_plutus_datum(raw_key.clone(), schema)?; + map.insert(&key, &encode_json_value_to_plutus_datum(value.clone(), schema)?); + } + Ok(PlutusData::new_map(&map)) + }, + invalid_key => Err(JsError::from_str(&format!("key '{}' in tagged object not valid", invalid_key))), + } + } else { + // constructor with tagged variant + if obj.len() != 2 { + return Err(JsError::from_str("detailed schemas must either have only one of the following keys: \"int\", \"bytes\", \"list\" or \"map\", or both of these 2 keys: \"constructor\" + \"fields\"")); + } + let variant: BigNum = obj + .get("constructor") + .and_then(|v| Some(to_bignum(v.as_u64()?))) + .ok_or_else(|| JsError::from_str("tagged constructors must contain an unsigned integer called \"constructor\""))?; + let fields_json = obj + .get("fields") + .and_then(|f| f.as_array()) + .ok_or_else(|| JsError::from_str("tagged constructors must contian a list called \"fields\""))?; + let mut fields = PlutusList::new(); + for field_json in fields_json { + let field = encode_json_value_to_plutus_datum(field_json.clone(), schema)?; + fields.add(&field); + } + Ok(PlutusData::new_constr_plutus_data(&ConstrPlutusData::new(&variant, &fields))) + } + }, + _ => Err(JsError::from_str(&format!("DetailedSchema requires types to be tagged objects, found: {}", value))), + }, + } +} + +#[wasm_bindgen] +pub fn decode_plutus_datum_to_json_str(datum: &PlutusData, schema: PlutusDatumSchema) -> Result { + let value = decode_plutus_datum_to_json_value(datum, schema)?; + serde_json::to_string(&value).map_err(|e| JsError::from_str(&e.to_string())) +} + +pub fn decode_plutus_datum_to_json_value(datum: &PlutusData, schema: PlutusDatumSchema) -> Result { + use serde_json::Value; + use std::convert::TryFrom; + let (type_tag, json_value) = match &datum.datum { + PlutusDataEnum::ConstrPlutusData(constr) => { + let mut obj = serde_json::map::Map::with_capacity(2); + obj.insert( + String::from("constructor"), + Value::from(from_bignum(&constr.alternative)) + ); + let mut fields = Vec::new(); + for field in constr.data.elems.iter() { + fields.push(decode_plutus_datum_to_json_value(field, schema)?); + } + obj.insert( + String::from("fields"), + Value::from(fields) + ); + (None, Value::from(obj)) + }, + PlutusDataEnum::Map(map) => match schema { + PlutusDatumSchema::BasicConversions => { + // I need to look at how they handle these to be certain. + todo!(); + }, + PlutusDatumSchema::DetailedSchema => (Some("map"), Value::from(map.0.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 = serde_json::map::Map::with_capacity(2); + kv_obj.insert(String::from("k"), k); + kv_obj.insert(String::from("v"), v); + Ok(Value::from(kv_obj)) + }).collect::, JsError>>()?)), + }, + PlutusDataEnum::List(list) => { + let mut elems = Vec::new(); + for elem in list.elems.iter() { + elems.push(decode_plutus_datum_to_json_value(elem, schema)?); + } + (Some("list"), Value::from(elems)) + }, + PlutusDataEnum::Integer(x) => ( + Some("int"), + Value::from( + x + .as_u64() + .as_ref() + .map(from_bignum) + .ok_or_else(|| JsError::from_str(&format!("Integer {} too big for our JSON support", x.to_str())))? + ) + ), + PlutusDataEnum::Bytes(bytes) => (Some("bytes"), Value::from(hex::encode(bytes))), + _ => todo!(), + }; + if type_tag.is_none() || schema != PlutusDatumSchema::DetailedSchema { + Ok(json_value) + } else { + let mut wrapper = serde_json::map::Map::with_capacity(1); + wrapper.insert(String::from(type_tag.unwrap()), json_value); + Ok(Value::from(wrapper)) + } +} @@ -1287,4 +1487,54 @@ mod tests { let new_bytes = datums.to_bytes(); assert_eq!(orig_bytes, new_bytes); } + + #[test] + pub fn plutus_datum_from_json_detailed() { + let json = "{\"list\": [ + {\"map\": [ + {\"k\": {\"bytes\": \"DEADBEEF\"}, \"v\": {\"int\": 42}}, + {\"k\": {\"map\" : [ + {\"k\": {\"int\": 9}, \"v\": {\"int\": 5}} + ]}, \"v\": {\"list\": []}} + ]}, + {\"bytes\": \"CAFED00D\"}, + {\"constructor\": 0, \"fields\": [ + {\"map\": []}, + {\"int\": 23} + ]} + ]}"; + let datum = encode_json_str_to_plutus_datum(json, PlutusDatumSchema::DetailedSchema).unwrap(); + + let list = datum.as_list().unwrap(); + assert_eq!(3, list.len()); + // map + let map = list.get(0).as_map().unwrap(); + assert_eq!(map.len(), 2); + let map_deadbeef = map.get(&PlutusData::new_bytes(vec![222, 173, 190, 239])).unwrap(); + assert_eq!(map_deadbeef.as_integer(), BigInt::from_str("42").ok()); + let mut long_key = PlutusMap::new(); + long_key.insert( + &PlutusData::new_integer(&BigInt::from_str("9").unwrap()), + &PlutusData::new_integer(&BigInt::from_str("5").unwrap()) + ); + let map_9_to_5 = map.get(&PlutusData::new_map(&long_key)).unwrap().as_list().unwrap(); + assert_eq!(map_9_to_5.len(), 0); + // bytes + let bytes = list.get(1).as_bytes().unwrap(); + assert_eq!(bytes, [202, 254, 208, 13]); + // constr data + let constr = list.get(2).as_constr_plutus_data().unwrap(); + assert_eq!(to_bignum(0), constr.alternative()); + let fields = constr.data(); + assert_eq!(fields.len(), 2); + let field0 = fields.get(0).as_map().unwrap(); + assert_eq!(field0.len(), 0); + let field1 = fields.get(1); + assert_eq!(field1.as_integer(), BigInt::from_str("23").ok()); + + // test round-trip via generated JSON + let json2 = decode_plutus_datum_to_json_str(&datum, PlutusDatumSchema::DetailedSchema).unwrap(); + let datum2 = encode_json_str_to_plutus_datum(&json2, PlutusDatumSchema::DetailedSchema).unwrap(); + assert_eq!(datum, datum2); + } } diff --git a/rust/src/utils.rs b/rust/src/utils.rs index 3f82f19e..46f958df 100644 --- a/rust/src/utils.rs +++ b/rust/src/utils.rs @@ -675,6 +675,7 @@ impl BigInt { return None; } match u64_digits.len() { + 0 => Some(to_bignum(0)), 1 => Some(to_bignum(*u64_digits.first().unwrap())), _ => None, } @@ -760,6 +761,12 @@ impl Deserialize for BigInt { } } +impl std::convert::From for BigInt where T: std::convert::Into { + fn from(x: T) -> Self { + Self(x.into()) + } +} + // we use the cbor_event::Serialize trait directly // This is only for use for plain cddl groups who need to be embedded within outer groups. From b20668bddc1ad835eb8da24815319ea51f3c82fd Mon Sep 17 00:00:00 2001 From: rooooooooob Date: Thu, 3 Feb 2022 15:52:36 -0800 Subject: [PATCH 2/4] Plutus Datum JSON implementation for BasicConversions --- rust/pkg/cardano_serialization_lib.js.flow | 186 +++++++++++++-------- rust/src/plutus.rs | 173 ++++++++++++++----- rust/src/utils.rs | 29 ++++ 3 files changed, 278 insertions(+), 110 deletions(-) diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index 0a9eb0e2..c43c2600 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -5,42 +5,6 @@ * @flow */ -/** - * @param {Uint8Array} bytes - * @returns {TransactionMetadatum} - */ -declare export function encode_arbitrary_bytes_as_metadatum( - bytes: Uint8Array -): TransactionMetadatum; - -/** - * @param {TransactionMetadatum} metadata - * @returns {Uint8Array} - */ -declare export function decode_arbitrary_bytes_from_metadatum( - metadata: TransactionMetadatum -): Uint8Array; - -/** - * @param {string} json - * @param {number} schema - * @returns {TransactionMetadatum} - */ -declare export function encode_json_str_to_metadatum( - json: string, - schema: number -): TransactionMetadatum; - -/** - * @param {TransactionMetadatum} metadatum - * @param {number} schema - * @returns {string} - */ -declare export function decode_metadatum_to_json_str( - metadatum: TransactionMetadatum, - schema: number -): string; - /** * @param {TransactionHash} tx_body_hash * @param {ByronAddress} addr @@ -165,6 +129,49 @@ declare export function encode_json_str_to_native_script( schema: number ): NativeScript; +/** + * @param {Uint8Array} bytes + * @returns {TransactionMetadatum} + */ +declare export function encode_arbitrary_bytes_as_metadatum( + bytes: Uint8Array +): TransactionMetadatum; + +/** + * @param {TransactionMetadatum} metadata + * @returns {Uint8Array} + */ +declare export function decode_arbitrary_bytes_from_metadatum( + metadata: TransactionMetadatum +): Uint8Array; + +/** + * @param {string} json + * @param {number} schema + * @returns {TransactionMetadatum} + */ +declare export function encode_json_str_to_metadatum( + json: string, + schema: number +): TransactionMetadatum; + +/** + * @param {TransactionMetadatum} metadatum + * @param {number} schema + * @returns {string} + */ +declare export function decode_metadatum_to_json_str( + metadatum: TransactionMetadatum, + schema: number +): string; + +/** + * @param {Transaction} tx + * @param {LinearFee} linear_fee + * @returns {BigNum} + */ +declare export function min_fee(tx: Transaction, linear_fee: LinearFee): BigNum; + /** * @param {string} password * @param {string} salt @@ -190,11 +197,24 @@ declare export function decrypt_with_password( ): string; /** - * @param {Transaction} tx - * @param {LinearFee} linear_fee - * @returns {BigNum} + * @param {string} json + * @param {number} schema + * @returns {PlutusData} */ -declare export function min_fee(tx: Transaction, linear_fee: LinearFee): BigNum; +declare export function encode_json_str_to_plutus_datum( + json: string, + schema: number +): PlutusData; + +/** + * @param {PlutusData} datum + * @param {number} schema + * @returns {string} + */ +declare export function decode_plutus_datum_to_json_str( + datum: PlutusData, + schema: number +): string; /** */ @@ -274,31 +294,12 @@ declare export var StakeCredKind: {| |}; /** + * Used to choosed the schema for a script JSON string */ -declare export var LanguageKind: {| - +PlutusV1: 0, // 0 -|}; - -/** - */ - -declare export var PlutusDataKind: {| - +ConstrPlutusData: 0, // 0 - +Map: 1, // 1 - +List: 2, // 2 - +Integer: 3, // 3 - +Bytes: 4, // 4 -|}; - -/** - */ - -declare export var RedeemerTagKind: {| - +Spend: 0, // 0 - +Mint: 1, // 1 - +Cert: 2, // 2 - +Reward: 3, // 3 +declare export var ScriptSchema: {| + +Wallet: 0, // 0 + +Node: 1, // 1 |}; /** @@ -321,15 +322,6 @@ declare export var MetadataJsonSchema: {| +DetailedSchema: 2, // 2 |}; -/** - * Used to choosed the schema for a script JSON string - */ - -declare export var ScriptSchema: {| - +Wallet: 0, // 0 - +Node: 1, // 1 -|}; - /** */ @@ -340,6 +332,51 @@ declare export var CoinSelectionStrategyCIP2: {| +RandomImproveMultiAsset: 3, // 3 |}; +/** + */ + +declare export var LanguageKind: {| + +PlutusV1: 0, // 0 +|}; + +/** + */ + +declare export var PlutusDataKind: {| + +ConstrPlutusData: 0, // 0 + +Map: 1, // 1 + +List: 2, // 2 + +Integer: 3, // 3 + +Bytes: 4, // 4 +|}; + +/** + */ + +declare export var RedeemerTagKind: {| + +Spend: 0, // 0 + +Mint: 1, // 1 + +Cert: 2, // 2 + +Reward: 3, // 3 +|}; + +/** + * 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 + */ + +declare export var PlutusDatumSchema: {| + +BasicConversions: 0, // 0 + +DetailedSchema: 1, // 1 +|}; + /** */ declare export class Address { @@ -652,6 +689,11 @@ declare export class BigInt { */ as_u64(): BigNum | void; + /** + * @returns {Int | void} + */ + as_int(): Int | void; + /** * @param {string} text * @returns {BigInt} diff --git a/rust/src/plutus.rs b/rust/src/plutus.rs index 7792d078..b0ecd734 100644 --- a/rust/src/plutus.rs +++ b/rust/src/plutus.rs @@ -618,18 +618,60 @@ impl Strings { // json + +/// 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 PlutusDatumSchema { + /// 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" + /// Integer'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, } #[wasm_bindgen] pub fn encode_json_str_to_plutus_datum(json: &str, schema: PlutusDatumSchema) -> Result { - if schema == PlutusDatumSchema::BasicConversions { - todo!("will fully implement once some test cases can be found"); - } let value = serde_json::from_str(json).map_err(|e| JsError::from_str(&e.to_string()))?; encode_json_value_to_plutus_datum(value, schema) } @@ -645,21 +687,32 @@ pub fn encode_json_value_to_plutus_datum(value: serde_json::Value, schema: Plutu Err(JsError::from_str("floats not allowed in plutus datums")) } } - fn encode_hex_bytes(s: &str, schema: PlutusDatumSchema) -> Result { - let bytes = if schema == PlutusDatumSchema::BasicConversions { + fn encode_string(s: &str, schema: PlutusDatumSchema, is_key: bool) -> Result { + if schema == PlutusDatumSchema::BasicConversions { if s.starts_with("0x") { - hex::decode(&s[2..]).map_err(|e| JsError::from_str(&e.to_string())) + // this must be a valid hex bytestring after + hex::decode(&s[2..]) + .map(|bytes| PlutusData::new_bytes(bytes)) + .map_err(|err| JsError::from_str(&format!("Error decoding {}: {}", s, err))) + } else if is_key { + // try as an integer + BigInt::from_str(s) + .map(|x| PlutusData::new_integer(&x)) + // if not, we use the utf8 bytes of the string instead directly + .or_else(|_err| Ok(PlutusData::new_bytes(s.as_bytes().to_vec()))) } else { - Err(JsError::from_str("Hex byte strings in schemaless mode MUST start with \"0x\" before the hex characters")) + // 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(JsError::from_str("Hex byte strings in detailed schema should NOT start with 0x and should just contain the hex characters")) } else { - hex::decode(s).map_err(|e| JsError::from_str(&e.to_string())) + hex::decode(s) + .map(|bytes| PlutusData::new_bytes(bytes)) + .map_err(|e| JsError::from_str(&e.to_string())) } - }; - Ok(PlutusData::new_bytes(bytes?)) + } } fn encode_array(json_arr: Vec, schema: PlutusDatumSchema) -> Result { let mut arr = PlutusList::new(); @@ -673,17 +726,15 @@ pub fn encode_json_value_to_plutus_datum(value: serde_json::Value, schema: Plutu Value::Null => Err(JsError::from_str("null not allowed in plutus datums")), Value::Bool(_) => Err(JsError::from_str("bools not allowed in plutus datums")), Value::Number(x) => encode_number(x), - // actually only for byte strings - Value::String(s) => encode_hex_bytes(&s, schema), + // no strings in plutus so it's all bytes (as hex or utf8 printable) + Value::String(s) => encode_string(&s, schema, false), Value::Array(json_arr) => encode_array(json_arr, schema), Value::Object(json_obj) => { - // here keys are JSON-in-JSON let mut map = PlutusMap::new(); - for (raw_key, value) in json_obj { - //let key = encode_json_str_to_plutus_datum(raw_key, schema) - // .map_err(|e| JsError::from_str("Key \"{}\" is invalid JSON: {}", raw_key, e))?; - - todo!(); + 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.insert(&key, &value); } Ok(PlutusData::new_map(&map)) }, @@ -701,7 +752,7 @@ pub fn encode_json_value_to_plutus_datum(value: serde_json::Value, schema: Plutu Value::Number(x) => encode_number(x), _ => Err(tag_mismatch()), }, - "bytes" => encode_hex_bytes(v.as_str().ok_or_else(tag_mismatch)?, schema), + "bytes" => encode_string(v.as_str().ok_or_else(tag_mismatch)?, schema, false), "list" => encode_array(v.as_array().ok_or_else(tag_mismatch)?.clone(), schema), "map" => { let mut map = PlutusMap::new(); @@ -744,7 +795,7 @@ pub fn encode_json_value_to_plutus_datum(value: serde_json::Value, schema: Plutu Ok(PlutusData::new_constr_plutus_data(&ConstrPlutusData::new(&variant, &fields))) } }, - _ => Err(JsError::from_str(&format!("DetailedSchema requires types to be tagged objects, found: {}", value))), + _ => Err(JsError::from_str(&format!("DetailedSchema requires ALL JSON to be tagged objects, found: {}", value))), }, } } @@ -776,10 +827,17 @@ pub fn decode_plutus_datum_to_json_value(datum: &PlutusData, schema: PlutusDatum (None, Value::from(obj)) }, PlutusDataEnum::Map(map) => match schema { - PlutusDatumSchema::BasicConversions => { - // I need to look at how they handle these to be certain. - todo!(); - }, + PlutusDatumSchema::BasicConversions => (None, Value::from(map.0.iter().map(|(key, value)| { + let json_key: String = match &key.datum { + PlutusDataEnum::ConstrPlutusData(_) => Err(JsError::from_str("plutus data constructors are not allowed as keys in this schema. Use DetailedSchema.")), + PlutusDataEnum::Map(_) => Err(JsError::from_str("plutus maps are not allowed as keys in this schema. Use DetailedSchema.")), + PlutusDataEnum::List(_) => Err(JsError::from_str("plutus lists are not allowed as keys in this schema. Use DetailedSchema.")), + PlutusDataEnum::Integer(x) => Ok(x.to_str()), + PlutusDataEnum::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, Value::from(json_value))) + }).collect::, JsError>>()?)), PlutusDatumSchema::DetailedSchema => (Some("map"), Value::from(map.0.iter().map(|(key, value)| { let k = decode_plutus_datum_to_json_value(key, schema)?; let v = decode_plutus_datum_to_json_value(value, schema)?; @@ -796,18 +854,25 @@ pub fn decode_plutus_datum_to_json_value(datum: &PlutusData, schema: PlutusDatum } (Some("list"), Value::from(elems)) }, - PlutusDataEnum::Integer(x) => ( + PlutusDataEnum::Integer(bigint) => ( Some("int"), - Value::from( - x - .as_u64() - .as_ref() - .map(from_bignum) - .ok_or_else(|| JsError::from_str(&format!("Integer {} too big for our JSON support", x.to_str())))? - ) + bigint + .as_int() + .as_ref() + .map(|int| if int.0 >= 0 { Value::from(int.0 as u64) } else { Value::from(int.0 as i64) }) + .ok_or_else(|| JsError::from_str(&format!("Integer {} too big for our JSON support", bigint.to_str())))? ), - PlutusDataEnum::Bytes(bytes) => (Some("bytes"), Value::from(hex::encode(bytes))), - _ => todo!(), + PlutusDataEnum::Bytes(bytes) => (Some("bytes"), Value::from(match schema { + PlutusDatumSchema::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))) + }, + PlutusDatumSchema::DetailedSchema => hex::encode(bytes), + })), }; if type_tag.is_none() || schema != PlutusDatumSchema::DetailedSchema { Ok(json_value) @@ -868,8 +933,6 @@ impl Deserialize for PlutusScripts { } } - -// TODO: write tests for this hand-coded implementation? impl cbor_event::se::Serialize for ConstrPlutusData { fn serialize<'se, W: Write>(&self, serializer: &'se mut Serializer) -> cbor_event::Result<&'se mut Serializer> { if let Some(compact_tag) = Self::alternative_to_compact_cbor_tag(from_bignum(&self.alternative)) { @@ -1488,13 +1551,47 @@ mod tests { assert_eq!(orig_bytes, new_bytes); } + #[test] + pub fn plutus_datum_from_json_basic() { + let json = "{ + \"5\": \"some utf8 string\", + \"0xDEADBEEF\": [ + {\"reg string\": {}}, + -9 + ] + }"; + + let datum = encode_json_str_to_plutus_datum(json, PlutusDatumSchema::BasicConversions).unwrap(); + + let map = datum.as_map().unwrap(); + let map_5 = map.get(&PlutusData::new_integer(&BigInt::from_str("5").unwrap())).unwrap(); + let utf8_bytes = "some utf8 string".as_bytes(); + assert_eq!(map_5.as_bytes().unwrap(), utf8_bytes); + let map_deadbeef: PlutusList = map + .get(&PlutusData::new_bytes(vec![222, 173, 190, 239])) + .expect("DEADBEEF key not found") + .as_list() + .expect("must be a map"); + assert_eq!(map_deadbeef.len(), 2); + let inner_map = map_deadbeef.get(0).as_map().unwrap(); + assert_eq!(inner_map.len(), 1); + let reg_string = inner_map.get(&PlutusData::new_bytes("reg string".as_bytes().to_vec())).unwrap(); + assert_eq!(reg_string.as_map().expect("reg string: {}").len(), 0); + assert_eq!(map_deadbeef.get(1).as_integer(), BigInt::from_str("-9").ok()); + + // test round-trip via generated JSON + let json2 = decode_plutus_datum_to_json_str(&datum, PlutusDatumSchema::BasicConversions).unwrap(); + let datum2 = encode_json_str_to_plutus_datum(&json2, PlutusDatumSchema::BasicConversions).unwrap(); + assert_eq!(datum, datum2); + } + #[test] pub fn plutus_datum_from_json_detailed() { let json = "{\"list\": [ {\"map\": [ {\"k\": {\"bytes\": \"DEADBEEF\"}, \"v\": {\"int\": 42}}, {\"k\": {\"map\" : [ - {\"k\": {\"int\": 9}, \"v\": {\"int\": 5}} + {\"k\": {\"int\": 9}, \"v\": {\"int\": -5}} ]}, \"v\": {\"list\": []}} ]}, {\"bytes\": \"CAFED00D\"}, @@ -1515,7 +1612,7 @@ mod tests { let mut long_key = PlutusMap::new(); long_key.insert( &PlutusData::new_integer(&BigInt::from_str("9").unwrap()), - &PlutusData::new_integer(&BigInt::from_str("5").unwrap()) + &PlutusData::new_integer(&BigInt::from_str("-5").unwrap()) ); let map_9_to_5 = map.get(&PlutusData::new_map(&long_key)).unwrap().as_list().unwrap(); assert_eq!(map_9_to_5.len(), 0); diff --git a/rust/src/utils.rs b/rust/src/utils.rs index 46f958df..2f78ed5b 100644 --- a/rust/src/utils.rs +++ b/rust/src/utils.rs @@ -681,6 +681,20 @@ impl BigInt { } } + pub fn as_int(&self) -> Option { + let (sign, u64_digits) = self.0.to_u64_digits(); + let u64_digit = match u64_digits.len() { + 0 => Some(to_bignum(0)), + 1 => Some(to_bignum(*u64_digits.first().unwrap())), + _ => None, + }?; + match sign { + num_bigint::Sign::NoSign | + num_bigint::Sign::Plus => Some(Int::new(&u64_digit)), + num_bigint::Sign::Minus => Some(Int::new_negative(&u64_digit)), + } + } + pub fn from_str(text: &str) -> Result { use std::str::FromStr; num_bigint::BigInt::from_str(text) @@ -2306,4 +2320,19 @@ mod tests { assert_eq!(Int::new_i32(42).as_i32_or_fail().unwrap(), 42); assert_eq!(Int::new_i32(-42).as_i32_or_fail().unwrap(), -42); } + + #[test] + fn bigint_as_int() { + let zero = BigInt::from_str("0").unwrap(); + let zero_int = zero.as_int().unwrap(); + assert_eq!(zero_int.0, 0i128); + + let pos = BigInt::from_str("1024").unwrap(); + let pos_int = pos.as_int().unwrap(); + assert_eq!(pos_int.0, 1024i128); + + let neg = BigInt::from_str("-1024").unwrap(); + let neg_int = neg.as_int().unwrap(); + assert_eq!(neg_int.0, -1024i128); + } } From 482a5f6c62d7a91e6e49c15fb8c772d02105a8d6 Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 24 May 2022 20:41:58 +0400 Subject: [PATCH 3/4] fix warning --- rust/src/plutus.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/src/plutus.rs b/rust/src/plutus.rs index 6159614c..fcb54eca 100644 --- a/rust/src/plutus.rs +++ b/rust/src/plutus.rs @@ -844,7 +844,6 @@ pub fn decode_plutus_datum_to_json_str(datum: &PlutusData, schema: PlutusDatumSc pub fn decode_plutus_datum_to_json_value(datum: &PlutusData, schema: PlutusDatumSchema) -> Result { use serde_json::Value; - use std::convert::TryFrom; let (type_tag, json_value) = match &datum.datum { PlutusDataEnum::ConstrPlutusData(constr) => { let mut obj = serde_json::map::Map::with_capacity(2); From 9ef76d84f8e6cb283546f9cf793e6758d70a638a Mon Sep 17 00:00:00 2001 From: lisicky Date: Tue, 24 May 2022 20:44:30 +0400 Subject: [PATCH 4/4] update js.flow --- rust/pkg/cardano_serialization_lib.js.flow | 35 ++++++---------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index 602d609a..335c2d88 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -296,8 +296,8 @@ declare export var ScriptSchema: {| |}; /** - * Used to choosed the schema for a script JSON string */ + declare export var TransactionMetadatumKind: {| +MetadataMap: 0, // 0 +MetadataList: 1, // 1 @@ -325,31 +325,6 @@ declare export var CoinSelectionStrategyCIP2: {| +RandomImproveMultiAsset: 3, // 3 |}; -/** - */ - -declare export var StakeCredKind: {| - +Key: 0, // 0 - +Script: 1, // 1 -|}; - -/** - */ - -declare export var LanguageKind: {| - +PlutusV1: 0, // 0 -|}; - -/** - */ - -declare export var RedeemerTagKind: {| - +Spend: 0, // 0 - +Mint: 1, // 1 - +Cert: 2, // 2 - +Reward: 3, // 3 -|}; - /** */ @@ -395,6 +370,14 @@ declare export var PlutusDatumSchema: {| +DetailedSchema: 1, // 1 |}; +/** + */ + +declare export var StakeCredKind: {| + +Key: 0, // 0 + +Script: 1, // 1 +|}; + /** */ declare export class Address {