diff --git a/chain/rust/Cargo.toml b/chain/rust/Cargo.toml index 5b966fbb..241b9e9f 100644 --- a/chain/rust/Cargo.toml +++ b/chain/rust/Cargo.toml @@ -29,6 +29,7 @@ num-integer = "0.1.45" #rand_os = "0.1" thiserror = "1.0.37" num = "0.4" +unicode-segmentation = "1.10.1" # These can be removed if we make wasm bindings for ALL functionality here. # This was not done right now as there is a lot of existing legacy code e.g. # for Byron that might need to be used from WASM and might not. diff --git a/chain/rust/src/builders/redeemer_builder.rs b/chain/rust/src/builders/redeemer_builder.rs index 284c142d..d743fdad 100644 --- a/chain/rust/src/builders/redeemer_builder.rs +++ b/chain/rust/src/builders/redeemer_builder.rs @@ -330,7 +330,7 @@ mod tests { let script = PlutusScript::PlutusV1(PlutusV1Script::new(vec![0])); PartialPlutusWitness { script: PlutusScriptWitness::Script(script), - redeemer: PlutusData::new_big_int(0u64.into()), + redeemer: PlutusData::new_integer(0u64.into()), } }; let missing_signers = vec![fake_raw_key_public(0).hash()]; diff --git a/chain/rust/src/builders/witness_builder.rs b/chain/rust/src/builders/witness_builder.rs index 1d85031f..aa290c99 100644 --- a/chain/rust/src/builders/witness_builder.rs +++ b/chain/rust/src/builders/witness_builder.rs @@ -624,7 +624,7 @@ mod tests { let script = PlutusScript::PlutusV1(PlutusV1Script::new(vec![0])); PartialPlutusWitness { script: PlutusScriptWitness::Script(script), - redeemer: PlutusData::new_big_int(0u64.into()), + redeemer: PlutusData::new_integer(0u64.into()), } }; let missing_signers = vec![fake_raw_key_public(0).hash()]; @@ -648,7 +648,7 @@ mod tests { let script = PlutusScript::PlutusV1(PlutusV1Script::new(vec![0])); PartialPlutusWitness { script: PlutusScriptWitness::Script(script), - redeemer: PlutusData::new_big_int(0u64.into()), + redeemer: PlutusData::new_integer(0u64.into()), } }; let missing_signers = vec![hash]; diff --git a/chain/rust/src/json/json_serialize.rs b/chain/rust/src/json/json_serialize.rs new file mode 100644 index 00000000..6a8d0fb8 --- /dev/null +++ b/chain/rust/src/json/json_serialize.rs @@ -0,0 +1,1433 @@ +use std::collections::{BTreeMap, VecDeque}; +use std::fmt::{Display, Formatter}; +use std::iter::FromIterator; +use std::str::FromStr; + +use itertools::Itertools; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::utils::BigInt; + +/** + * Value replaces traditional serde_json::Value in some places. + * + * Main reason for custom type is the fact that serde_json::Value doesn't support big integers, + * while we need them in metadata and plutus structs. + * + * If we move from integers to String we will no longer support the JSON format that cardano-node uses. + */ +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Value { + Null, + Bool(bool), + Number(BigInt), + String(String), + Array(Vec), + Object(BTreeMap), +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub enum JsonToken { + ArrayStart, + ArrayEnd, + + ObjectStart, + ObjectEnd, + + Colon, + Comma, + Quote, + + String { raw: String }, + + LeftQuotedString { raw: String }, + + ParsedValue { value: Value }, + // This is an object's key when [Quote, String, Quote, Colon] or [Quote, Quote, Colon] are parsed + ParsedKey { key: String }, +} + +impl JsonToken { + fn is_quote(&self) -> bool { + matches!(self, JsonToken::Quote) + } + + fn is_string(&self) -> bool { + matches!(self, JsonToken::String { .. }) + } +} + +impl Serialize for Value { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let string = self + .clone() + .to_string() + .map_err(|err| serde::ser::Error::custom(format!("{:?}", err)))?; + + serializer.serialize_str(string.as_str()) + } +} + +impl<'de> Deserialize<'de> for Value { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + Value::from_string(s).map_err(|err| serde::de::Error::custom(format!("{:?}", err))) + } +} + +#[derive(Debug, Clone)] +pub enum JsonParseError { + // general + InvalidToken(JsonToken), + InvalidParseResult(Vec), + + // array and object + InvalidTokenBeforeArrayOrObjectStart(JsonToken), + + // array + NotAllowedInArray(JsonToken), + NoArrayStartFound, + ArrayCommaError, + + // object + NotAllowedInObject(JsonToken), + NoObjectStartFound, + ObjectStructureError, + NoValueForKey(JsonToken), + NoKeyForValue(JsonToken), + + // quote + InvalidTokenBeforeQuote(JsonToken), + // colon + InvalidTokenBeforeColon(Option), + // comma + InvalidTokenBeforeComma(Option), + // string + InvalidTokenBeforeString(JsonToken), + InvalidRawString(String), +} + +impl Display for JsonParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.serialize_str(format!("{:?}", self).as_str()) + } +} + +impl std::error::Error for JsonParseError {} + +fn tokenize_string(string: String) -> Vec { + fn are_we_inside_string(tokens: &Vec) -> bool { + if tokens.is_empty() { + return false; + } + if tokens.len() == 1 { + let is_last_token_quote = tokens.last().map(|t| t.is_quote()).unwrap_or(false); + return is_last_token_quote; + } + let last_token = tokens.last(); + let token_before_last_token = tokens.get(tokens.len() - 2); + let is_token_before_last_string_or_quote = token_before_last_token + .map(|t| t.is_quote() || t.is_string()) + .unwrap_or(false); + let is_last_token_quote = last_token.map(|t| t.is_quote()).unwrap_or(false); + + !is_token_before_last_string_or_quote && is_last_token_quote + /* + + This works because of the following: + We either have: + - string without quote on right (which could be invalid as well) + - string with quote on right + + If we had a string with quote on right before current position, the tokens will be: + [.., String, Quote, ] + or in case of empty string: + [.., Quote, Quote, ]. + + We never have 2 strings in a row [.., String, String, ..], so we won't face situation + when we consider we're in / not in the string incorrectly + + If we had a string without quote on right than it's just current string and it's not pushed. + This way we will have [.., Quote, ] + + if before quote there was another string - this is invalid json + + */ + } + + let mut tokens = Vec::::new(); + let mut current_string: String = String::new(); + for char in string.graphemes(true) { + let (reset_string, token) = match char { + "\"" => { + // if we have backslashed quotes in a string they're in the string already + if !current_string.is_empty() + && current_string.graphemes(true).last().unwrap() == "\\" + { + let graphemes_count = current_string.graphemes(true).count(); + current_string = current_string + .graphemes(true) + .take(graphemes_count - 1) + .collect(); + current_string += "\""; + (false, None) + } else { + (true, Some(JsonToken::Quote)) + } + } + "{" | "}" | "[" | "]" | ":" | "," if are_we_inside_string(&tokens) => { + current_string += char; + (false, None) + } + "{" => (true, Some(JsonToken::ObjectStart)), + "}" => (true, Some(JsonToken::ObjectEnd)), + "[" => (true, Some(JsonToken::ArrayStart)), + "]" => (true, Some(JsonToken::ArrayEnd)), + ":" => (true, Some(JsonToken::Colon)), + "," => (true, Some(JsonToken::Comma)), + _ => { + let splitted: Vec<&str> = char.split_whitespace().collect(); + let is_whitespace = splitted.is_empty() + || (splitted.len() == 1 && splitted.first().cloned().unwrap_or("").is_empty()); + if !is_whitespace || are_we_inside_string(&tokens) { + current_string += char; + } + (false, None) + } + }; + + if reset_string && !current_string.is_empty() { + tokens.push(JsonToken::String { + raw: current_string.clone(), + }); + current_string = String::new(); + } + + if let Some(token) = token { + tokens.push(token); + } + } + + if !current_string.is_empty() { + tokens.push(JsonToken::String { + raw: current_string.clone(), + }); + } + + tokens +} + +fn parse_json(tokens: Vec) -> Result { + let mut stack: VecDeque = VecDeque::new(); + + for token in tokens.into_iter() { + match token { + JsonToken::ArrayStart | JsonToken::ObjectStart => { + handle_array_or_object_open(token, &mut stack)?; // done + } + JsonToken::ArrayEnd => { + parse_array(&mut stack)?; // done + } + JsonToken::Colon => { + handle_colon(&mut stack)?; // done + } + JsonToken::Comma => { + handle_comma(&mut stack)?; // done + } + JsonToken::Quote => { + handle_quote(&mut stack)?; // done + } + JsonToken::ObjectEnd => { + parse_object(&mut stack)?; + } + JsonToken::String { raw } => { + handle_string(raw, &mut stack)?; + } + JsonToken::ParsedKey { .. } + | JsonToken::ParsedValue { .. } + | JsonToken::LeftQuotedString { .. } => { + return Err(JsonParseError::InvalidToken(token)); + } + } + } + + if stack.len() > 1 { + return Err(JsonParseError::InvalidParseResult(Vec::from_iter(stack))); + } + + match stack.pop_back() { + None => Err(JsonParseError::InvalidParseResult(vec![])), + Some(JsonToken::ParsedValue { value }) => Ok(value), + Some(other) => Err(JsonParseError::InvalidParseResult(vec![other])), + } +} + +fn handle_array_or_object_open( + token: JsonToken, + stack: &mut VecDeque, +) -> Result<(), JsonParseError> { + match stack.back() { + None + | Some(JsonToken::ArrayStart) + | Some(JsonToken::ParsedKey { .. }) + | Some(JsonToken::Comma) => { + stack.push_back(token); + Ok(()) + } + back => Err(JsonParseError::InvalidTokenBeforeArrayOrObjectStart( + back.cloned().unwrap(), + )), + } +} + +fn handle_colon(stack: &mut VecDeque) -> Result<(), JsonParseError> { + let back = stack.pop_back(); + match &back { + Some(JsonToken::ParsedValue { + value: Value::String(string), + }) => { + stack.push_back(JsonToken::ParsedKey { + key: string.clone(), + }); + Ok(()) + } + _ => Err(JsonParseError::InvalidTokenBeforeColon(back)), + } +} + +fn handle_comma(stack: &mut VecDeque) -> Result<(), JsonParseError> { + let back = stack.back(); + match back { + Some(JsonToken::ParsedValue { .. }) => { + stack.push_back(JsonToken::Comma); + Ok(()) + } + _ => Err(JsonParseError::InvalidTokenBeforeComma(back.cloned())), + } +} + +fn handle_quote(stack: &mut VecDeque) -> Result<(), JsonParseError> { + let back = stack.pop_back(); + match back { + None => { + stack.push_back(JsonToken::Quote); + Ok(()) + } + Some(JsonToken::ArrayStart) + | Some(JsonToken::ObjectStart) + | Some(JsonToken::Comma) + | Some(JsonToken::ParsedKey { .. }) => { + stack.push_back(back.unwrap()); + stack.push_back(JsonToken::Quote); + Ok(()) + } + Some(JsonToken::Quote) => { + stack.push_back(JsonToken::ParsedValue { + value: Value::String(String::new()), + }); + Ok(()) + } + Some(JsonToken::LeftQuotedString { raw }) => { + stack.push_back(JsonToken::ParsedValue { + value: Value::String(raw), + }); + Ok(()) + } + _ => Err(JsonParseError::InvalidTokenBeforeQuote(back.unwrap())), + } +} + +fn parse_raw_string(string: String) -> Result { + match string.as_str() { + "null" => Ok(Value::Null), + "false" => Ok(Value::Bool(false)), + "true" => Ok(Value::Bool(true)), + string => { + let number = BigInt::from_str(string); + match number { + Ok(number) => Ok(Value::Number(number)), + Err(_) => Err(JsonParseError::InvalidRawString(String::from(string))), + } + } + } +} + +fn handle_string(string: String, stack: &mut VecDeque) -> Result<(), JsonParseError> { + let back = stack.pop_back(); + match &back { + None => { + let event = parse_raw_string(string)?; + stack.push_back(JsonToken::ParsedValue { value: event }); + Ok(()) + } + Some(JsonToken::Quote) => { + stack.push_back(JsonToken::LeftQuotedString { raw: string }); + Ok(()) + } + Some(JsonToken::ParsedKey { .. }) + | Some(JsonToken::Comma) + | Some(JsonToken::ArrayStart) => { + stack.push_back(back.unwrap()); + + let event = parse_raw_string(string)?; + stack.push_back(JsonToken::ParsedValue { value: event }); + Ok(()) + } + _ => Err(JsonParseError::InvalidTokenBeforeString(back.unwrap())), + } +} + +fn parse_array(stack: &mut VecDeque) -> Result<(), JsonParseError> { + let mut array = Vec::::new(); + let mut opening_brace_found = false; + while !stack.is_empty() && !opening_brace_found { + let current_token = stack.pop_back().unwrap(); + + match ¤t_token { + JsonToken::ArrayStart => { + opening_brace_found = true; + break; + } + JsonToken::ParsedValue { .. } | JsonToken::Comma => { + array.push(current_token); + } + _ => { + return Err(JsonParseError::NotAllowedInArray(current_token)); + } + } + } + + if !opening_brace_found { + return Err(JsonParseError::NoArrayStartFound); + } + + array.reverse(); + + let mut result = Vec::::new(); + let total_tokens = array.len(); + for (number, token) in array.into_iter().enumerate() { + match token { + JsonToken::Comma => { + if number % 2 != 1 || number + 1 == total_tokens { + return Err(JsonParseError::ArrayCommaError); + } + } + JsonToken::ParsedValue { value } => { + if number % 2 != 0 { + return Err(JsonParseError::ArrayCommaError); + } + result.push(value); + } + _ => return Err(JsonParseError::NotAllowedInArray(token)), + } + } + + stack.push_back(JsonToken::ParsedValue { + value: Value::Array(result), + }); + + Ok(()) +} + +fn parse_object(stack: &mut VecDeque) -> Result<(), JsonParseError> { + let mut array = Vec::::new(); + let mut opening_brace_found = false; + while !stack.is_empty() && !opening_brace_found { + let current_token = stack.pop_back().unwrap(); + + match ¤t_token { + JsonToken::ObjectStart => { + opening_brace_found = true; + break; + } + JsonToken::ParsedValue { .. } | JsonToken::Comma | JsonToken::ParsedKey { .. } => { + array.push(current_token); + } + _ => { + return Err(JsonParseError::NotAllowedInObject(current_token)); + } + } + } + + if !opening_brace_found { + return Err(JsonParseError::NoObjectStartFound); + } + + array.reverse(); + + let mut result = BTreeMap::::new(); + let total_tokens = array.len(); + + let mut current_key: Option = None; + + for (number, token) in array.into_iter().enumerate() { + match &token { + JsonToken::ParsedKey { key } => { + if number + 1 == total_tokens { + return Err(JsonParseError::NoValueForKey(token.clone())); + } + if number % 3 != 0 { + return Err(JsonParseError::ObjectStructureError); + } + current_key = Some(key.clone()); + } + JsonToken::ParsedValue { value } => { + let key = match current_key.clone() { + None => { + return Err(JsonParseError::NoKeyForValue(token.clone())); + } + Some(key) => { + current_key = None; + key + } + }; + if number % 3 != 1 { + return Err(JsonParseError::ObjectStructureError); + } + result.insert(key, value.clone()); + } + JsonToken::Comma => { + if number % 3 != 2 || number + 1 == total_tokens { + return Err(JsonParseError::ObjectStructureError); + } + } + _ => return Err(JsonParseError::NotAllowedInObject(token)), + } + } + + stack.push_back(JsonToken::ParsedValue { + value: Value::Object(result), + }); + + Ok(()) +} + +impl Value { + pub fn to_string(&self) -> Result { + match self { + Value::Null => serde_json::to_string(&serde_json::Value::Null), + Value::Bool(b) => serde_json::to_string(&serde_json::Value::Bool(*b)), + Value::Number(bigint) => Ok(bigint.to_string()), + Value::String(text) => serde_json::to_string(&serde_json::Value::String(text.clone())), + Value::Array(arr) => { + let mut arr_serialized = vec![String::new(); arr.len()]; + for (i, item) in arr.iter().enumerate() { + arr_serialized[i] = item.to_string()?; + } + Ok(format!("[{}]", arr_serialized.iter().join(","))) + } + Value::Object(items) => { + let mut items_serialized = vec![String::new(); items.len()]; + for (i, (key, value)) in items.iter().enumerate() { + items_serialized[i] = format!("\"{}\":{}", key, value.to_string()?); + } + Ok(format!("{{{}}}", items_serialized.iter().join(","))) + } + } + } + + pub fn from_string(from: String) -> Result { + let tokens = tokenize_string(from); + parse_json(tokens) + } +} + +impl From> for Value { + fn from(vec: Vec) -> Self { + Value::Array(vec) + } +} + +impl From for Value { + fn from(string: String) -> Self { + Value::String(string) + } +} + +impl From for Value { + fn from(number: u64) -> Self { + Value::Number(BigInt::from(number)) + } +} + +impl From for Value { + fn from(number: BigInt) -> Self { + Value::Number(number) + } +} + +impl From> for Value { + fn from(from: BTreeMap) -> Self { + Value::Object(from) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::iter::FromIterator; + use std::str::FromStr; + + use super::{parse_json, tokenize_string, JsonToken, Value}; + use crate::utils::BigInt; + + #[test] + fn run_primitives() { + assert_eq!( + Value::Null.to_string().unwrap(), + serde_json::Value::Null.to_string() + ); + for b in vec![true, false].into_iter() { + assert_eq!( + Value::Bool(b).to_string().unwrap(), + serde_json::Value::Bool(b).to_string() + ); + } + // supported uints + for integer in vec![0, u64::MAX].into_iter() { + assert_eq!( + Value::Number(BigInt::from(integer)).to_string().unwrap(), + serde_json::Value::from(integer).to_string() + ); + } + // supported ints + for integer in vec![0, i64::MAX, i64::MIN].into_iter() { + assert_eq!( + Value::Number(BigInt::from(integer)).to_string().unwrap(), + serde_json::Value::from(integer).to_string() + ); + } + // unsupported ints + assert_eq!( + Value::Number(BigInt::from_str("980949788381070983313748912887").unwrap()) + .to_string() + .unwrap(), + "980949788381070983313748912887" + ); + // string + assert_eq!( + Value::String(String::from("supported string")) + .to_string() + .unwrap(), + serde_json::Value::from(String::from("supported string")).to_string() + ); + } + + #[test] + fn run_array_supported_primitives() { + let cml_arr = generate_array_of_primitives(); + let serde_arr = generate_array_of_primitives_serde_json(); + assert_eq!( + Value::Array(cml_arr).to_string().unwrap(), + serde_json::Value::Array(serde_arr).to_string() + ) + } + + fn generate_array_of_primitives() -> Vec { + let mut cml_arr = vec![Value::Null]; + cml_arr.extend(vec![true, false].into_iter().map(Value::Bool)); + cml_arr.extend( + vec![0, u64::MAX] + .into_iter() + .map(|integer| Value::Number(BigInt::from(integer))), + ); + cml_arr.extend( + vec![0, i64::MAX, i64::MIN] + .into_iter() + .map(|integer| Value::Number(BigInt::from(integer))), + ); + cml_arr.extend( + vec!["supported_string", ""] + .into_iter() + .map(|str| Value::String(String::from(str))), + ); + cml_arr + } + + fn generate_array_of_primitives_serde_json() -> Vec { + let mut serde_arr = vec![serde_json::Value::Null]; + serde_arr.extend(vec![true, false].into_iter().map(serde_json::Value::Bool)); + serde_arr.extend(vec![0, u64::MAX].into_iter().map(serde_json::Value::from)); + serde_arr.extend( + vec![0, i64::MAX, i64::MIN] + .into_iter() + .map(serde_json::Value::from), + ); + serde_arr.extend( + vec!["supported_string", ""] + .into_iter() + .map(|str| serde_json::Value::from(String::from(str))), + ); + serde_arr + } + + // serde_json::Value didn't support big integers like that + fn generate_array_of_primitives_with_unsupported() -> Vec { + let mut cml_arr = vec![Value::Null]; + cml_arr.extend(vec![true, false].into_iter().map(Value::Bool)); + cml_arr.extend( + vec![0, u64::MAX] + .into_iter() + .map(|integer| Value::Number(BigInt::from(integer))), + ); + cml_arr.extend( + vec![0, i64::MAX, i64::MIN] + .into_iter() + .map(|integer| Value::Number(BigInt::from(integer))), + ); + cml_arr.extend( + vec![ + BigInt::from_str("980949788381070983313748912887").unwrap(), + BigInt::from_str("-980949788381070983313748912887").unwrap(), + ] + .into_iter() + .map(Value::Number), + ); + cml_arr.extend( + vec!["supported_string", ""] + .into_iter() + .map(|str| Value::String(String::from(str))), + ); + cml_arr + } + + #[test] + fn run_array_unsupported_primitives() { + let cml_arr = generate_array_of_primitives_with_unsupported(); + assert_eq!( + Value::Array(cml_arr).to_string().unwrap(), + "[null,true,false,0,18446744073709551615,0,9223372036854775807,-9223372036854775808,980949788381070983313748912887,-980949788381070983313748912887,\"supported_string\",\"\"]" + ); + } + + fn generate_map() -> BTreeMap { + let mut index = 0; + let mut cml_map = vec![(index.to_string(), Value::Null)]; + index += 1; + + cml_map.extend(vec![true, false].into_iter().map(|b| { + let local = index; + index += 1; + (local.to_string(), Value::Bool(b)) + })); + cml_map.extend(vec![0, u64::MAX].into_iter().map(|integer| { + let local = index; + index += 1; + (local.to_string(), Value::Number(BigInt::from(integer))) + })); + cml_map.extend(vec![0, i64::MAX, i64::MIN].into_iter().map(|integer| { + let local = index; + index += 1; + (local.to_string(), Value::Number(BigInt::from(integer))) + })); + cml_map.extend(vec!["supported_string", ""].into_iter().map(|str| { + let local = index; + index += 1; + (local.to_string(), Value::String(String::from(str))) + })); + cml_map.extend(vec![generate_array_of_primitives()].into_iter().map(|arr| { + let local = index; + index += 1; + (local.to_string(), Value::Array(arr)) + })); + + BTreeMap::from_iter(cml_map) + } + + fn generate_map_unsupported() -> BTreeMap { + let mut map = generate_map(); + let mut index = map + .keys() + .map(|key| u64::from_str(key).unwrap()) + .max() + .unwrap() + + 1; + + map.insert( + index.to_string(), + Value::Number(BigInt::from_str("980949788381070983313748912887").unwrap()), + ); + index += 1; + + let arr = generate_array_of_primitives_with_unsupported(); + map.insert(index.to_string(), Value::Array(arr)); + index += 1; + + let arr = generate_map(); + map.insert(index.to_string(), Value::Object(arr)); + + map + } + + fn generate_map_serde_json() -> serde_json::Value { + let mut index = 0; + let mut serde_map = vec![(index.to_string(), serde_json::Value::Null)]; + index += 1; + + serde_map.extend(vec![true, false].into_iter().map(|b| { + let local = index; + index += 1; + (local.to_string(), serde_json::Value::Bool(b)) + })); + serde_map.extend(vec![0, u64::MAX].into_iter().map(|integer| { + let local = index; + index += 1; + (local.to_string(), serde_json::Value::from(integer)) + })); + serde_map.extend(vec![0, i64::MAX, i64::MIN].into_iter().map(|integer| { + let local = index; + index += 1; + (local.to_string(), serde_json::Value::from(integer)) + })); + serde_map.extend(vec!["supported_string", ""].into_iter().map(|str| { + let local = index; + index += 1; + (local.to_string(), serde_json::Value::from(str)) + })); + serde_map.extend( + vec![generate_array_of_primitives_serde_json()] + .into_iter() + .map(|arr| { + let local = index; + index += 1; + (local.to_string(), serde_json::Value::Array(arr)) + }), + ); + + serde_json::Value::Object(serde_json::Map::from_iter(serde_map)) + } + + #[test] + fn run_map() { + let cml_map = generate_map(); + let serde_map = generate_map_serde_json(); + + assert_eq!( + Value::Object(cml_map).to_string().unwrap(), + serde_map.to_string() + ); + } + + #[test] + fn run_map_unsupported() { + let cml_map = generate_map_unsupported(); + + assert_eq!( + Value::Object(cml_map).to_string().unwrap(), + "{\"0\":null,\"1\":true,\"10\":[null,true,false,0,18446744073709551615,0,9223372036854775807,-9223372036854775808,\"supported_string\",\"\"],\"11\":980949788381070983313748912887,\"12\":[null,true,false,0,18446744073709551615,0,9223372036854775807,-9223372036854775808,980949788381070983313748912887,-980949788381070983313748912887,\"supported_string\",\"\"],\"13\":{\"0\":null,\"1\":true,\"10\":[null,true,false,0,18446744073709551615,0,9223372036854775807,-9223372036854775808,\"supported_string\",\"\"],\"2\":false,\"3\":0,\"4\":18446744073709551615,\"5\":0,\"6\":9223372036854775807,\"7\":-9223372036854775808,\"8\":\"supported_string\",\"9\":\"\"},\"2\":false,\"3\":0,\"4\":18446744073709551615,\"5\":0,\"6\":9223372036854775807,\"7\":-9223372036854775808,\"8\":\"supported_string\",\"9\":\"\"}" + ); + } + + fn easy_cases() -> Vec<(String, Vec, Value)> { + vec![ + ( + "false".to_string(), + vec![JsonToken::String { + raw: "false".to_string(), + }], + Value::Bool(false), + ), + ( + "true".to_string(), + vec![JsonToken::String { + raw: "true".to_string(), + }], + Value::Bool(true), + ), + ( + "null".to_string(), + vec![JsonToken::String { + raw: "null".to_string(), + }], + Value::Null, + ), + ( + "\"string\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "string".to_string(), + }, + JsonToken::Quote, + ], + Value::String("string".to_string()), + ), + ( + "\"str\\\"ing\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "str\"ing".to_string(), + }, + JsonToken::Quote, + ], + Value::String("str\"ing".to_string()), + ), + ( + "\"\\\"\\\"\\\"\\\"\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "\"\"\"\"".to_string(), + }, + JsonToken::Quote, + ], + Value::String("\"\"\"\"".to_string()), + ), + ( + "\"\\\"\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "\"".to_string(), + }, + JsonToken::Quote, + ], + Value::String("\"".to_string()), + ), + ( + "\"\\\"\\\"\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "\"\"".to_string(), + }, + JsonToken::Quote, + ], + Value::String("\"\"".to_string()), + ), + ( + "\"y̆\\\"\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "y̆\"".to_string(), + }, + JsonToken::Quote, + ], + Value::String("y̆\"".to_string()), + ), + ( + "\"y̆\\\"y̆\\\"y̆\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "y̆\"y̆\"y̆".to_string(), + }, + JsonToken::Quote, + ], + Value::String("y̆\"y̆\"y̆".to_string()), + ), + ( + "\"y̆\\\"y̆y̆\\\"y̆\"".to_string(), + vec![ + JsonToken::Quote, + JsonToken::String { + raw: "y̆\"y̆y̆\"y̆".to_string(), + }, + JsonToken::Quote, + ], + Value::String("y̆\"y̆y̆\"y̆".to_string()), + ), + ( + "1234".to_string(), + vec![JsonToken::String { + raw: "1234".to_string(), + }], + Value::Number(BigInt::from_str("1234").unwrap()), + ), + ( + "-1234".to_string(), + vec![JsonToken::String { + raw: "-1234".to_string(), + }], + Value::Number(BigInt::from_str("-1234").unwrap()), + ), + ( + "123456789876543212345678900000000000000000000".to_string(), + vec![JsonToken::String { + raw: "123456789876543212345678900000000000000000000".to_string(), + }], + Value::Number( + BigInt::from_str("123456789876543212345678900000000000000000000").unwrap(), + ), + ), + ( + "-123456789876543212345678900000000000000000000".to_string(), + vec![JsonToken::String { + raw: "-123456789876543212345678900000000000000000000".to_string(), + }], + Value::Number( + BigInt::from_str("-123456789876543212345678900000000000000000000").unwrap(), + ), + ), + ( + "0".to_string(), + vec![JsonToken::String { + raw: "0".to_string(), + }], + Value::Number(BigInt::from_str("0").unwrap()), + ), + ( + "-0".to_string(), + vec![JsonToken::String { + raw: "-0".to_string(), + }], + Value::Number(BigInt::from_str("-0").unwrap()), + ), + ] + } + + fn run_cases(cases: Vec<(String, Vec, Value)>) { + for (case, correct_tokens, correct) in cases { + let computed_tokens = tokenize_string(case.clone()); + assert_eq!( + computed_tokens, + correct_tokens, + "Can't tokenize case: {}\n tokens: {}\n correct: {}\n", + case, + serde_json::to_string(&computed_tokens).unwrap(), + serde_json::to_string(&correct_tokens).unwrap() + ); + + let parsed = parse_json(computed_tokens); + assert!( + parsed.is_ok(), + "Can't parse case: {}\n error: {:?}\n correct: {:?}\n", + case, + parsed.err(), + correct + ); + assert_eq!( + parsed.clone().unwrap(), + correct, + "Mismatch case: {}\n parsed: {:?}\n correct: {:?}\n", + case, + parsed, + correct + ); + } + } + + #[test] + fn deserialize_easy() { + let cases = easy_cases(); + run_cases(cases); + } + + fn generate_array( + cases: Vec<(String, Vec, Value)>, + ) -> (String, Vec, Value) { + let mut test_string = String::from("["); + let mut correct_tokens = vec![JsonToken::ArrayStart]; + let mut correct_value = vec![]; + + let count = cases.len(); + for (number, (test, tokens, parsed)) in cases.into_iter().enumerate() { + test_string += test.as_str(); + correct_tokens.extend(tokens); + correct_value.push(parsed); + if number + 1 != count { + test_string += ","; + correct_tokens.push(JsonToken::Comma); + } + } + test_string += "]"; + correct_tokens.push(JsonToken::ArrayEnd); + (test_string, correct_tokens, Value::Array(correct_value)) + } + + fn generate_arrays() -> Vec<(String, Vec, Value)> { + vec![ + generate_array(easy_cases()), + generate_array(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()), + )), + generate_array(Vec::from_iter( + vec![generate_array(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()), + ))] + .into_iter() + .chain(easy_cases().into_iter()), + )), + ] + } + + #[test] + fn deserialize_array() { + let cases = generate_arrays(); + + run_cases(cases); + } + + fn generate_object( + cases: Vec<(String, Vec, Value)>, + ) -> (String, Vec, Value) { + let mut test_string = String::from("{"); + let mut correct_tokens = vec![JsonToken::ObjectStart]; + let mut correct_value = BTreeMap::new(); + + let count = cases.len(); + for (number, (test, tokens, parsed)) in cases.into_iter().enumerate() { + test_string += &format!("\"{}\":", number); + test_string += &test; + correct_tokens.extend(vec![ + JsonToken::Quote, + JsonToken::String { + raw: number.to_string(), + }, + JsonToken::Quote, + JsonToken::Colon, + ]); + correct_tokens.extend(tokens); + correct_value.insert(number.to_string(), parsed); + if number + 1 != count { + test_string += ","; + correct_tokens.push(JsonToken::Comma); + } + } + test_string += "}"; + correct_tokens.push(JsonToken::ObjectEnd); + (test_string, correct_tokens, Value::Object(correct_value)) + } + + fn generate_objects() -> Vec<(String, Vec, Value)> { + vec![ + generate_object(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()), + )), + generate_object(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()) + .chain( + vec![generate_object(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()), + ))] + .into_iter(), + ), + )), + generate_object(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()) + .chain( + vec![generate_object(Vec::from_iter( + vec![generate_array(easy_cases())] + .into_iter() + .chain(easy_cases().into_iter()) + .chain(generate_arrays().into_iter()), + ))] + .into_iter() + .chain(generate_arrays().into_iter()), + ), + )), + ] + } + + #[test] + fn deserialize_object() { + let cases = generate_objects(); + + run_cases(cases); + } + + #[test] + fn mix() { + let cases = vec![ + generate_array(generate_objects()), + generate_array(generate_arrays()), + generate_object(generate_arrays()), + generate_object(generate_objects()), + ]; + + run_cases(cases); + } + + #[test] + fn deserialize_errors() { + let cases = vec![ + ",", + "[],", + "{},", + "{,}", + // commas + "{\"1\":\"kek\",}", + "{,\"1\":\"kek\"}", + "{\"1\":\"kek\",\"1\":\"kek\",}", + "{\"1\":\"kek\",,\"1\":\"kek\"}", + "{\"1\",:\"kek\",\"1\":\"kek\"}", + "{\"1\":,\"kek\",\"1\":\"kek\"}", + "{\"1\",\"kek\",\"1\":\"kek\"}", + "{\"1\":\"kek\",\"1\":\"kek\"},", + ",{\"1\":\"kek\",\"1\":\"kek\"}", + "{\"1\"\"kek\",\"1\":\"kek\"}", + "{:\"kek\",\"1\":\"kek\"}", + "{\"1:\"kek\",\"1\":\"kek\"}", + "{1\":\"kek\",\"1\":\"kek\"}", + "{1:\"kek\",\"1\":\"kek\"}", + "{\"1\"kek\",\"1\":\"kek\"}", + "{\"1kek\",\"1\":\"kek\"}", + "[1,2,3,]", + "[1,2,,3]", + "[,1,2,3]", + // array + "[\"lel\":,2,3]", + "[\"lel\":1,2,3]", + "[1,2,3", + "1,2,3]", + "[", + "]", + "{", + "}", + "[[1,2,3]", + "[1,2,3]]", + "{\"\":1}}", + "{{\"\":1}", + "{\"\":[1,]}", + "{\"\":[1,2}", + "{\"\":[1,2,[1,]]}", + "{\"\":[1,2,[1,[]]}", + "{\"\":[1,2,[1,[]]]]}", + // empty + "[][]", + "{}[]", + "[]{}", + "{}{}", + "nul", + "\"\"\"", + "\"\\\"", + "\\\"\\\"", + "\\\" + \\\"", + ]; + for case in cases.into_iter() { + let computed_tokens = tokenize_string(case.to_string()); + let parsed = parse_json(computed_tokens.clone()); + assert!( + parsed.is_err(), + "False parse case: {}\n result: {:?}\n", + case, + parsed.unwrap() + ); + } + } + + #[test] + fn deserialize_ok() { + let cases = vec![ + ("[]", Value::Array(vec![])), + ("{}", Value::Object(BTreeMap::new())), + ("\"{}[]:,\"", Value::String("{}[]:,".to_string())), + ("null", Value::Null), + ("null ", Value::Null), + (" null ", Value::Null), + ( + " \ + [\ + \"\ + \ + \"] \ + ", + Value::Array(vec![Value::String( + "\ + \ + " + .to_string(), + )]), + ), + ( + " \ + [\" \" \ + , \" \" ] \ + ", + Value::Array(vec![ + Value::String(" ".to_string()), + Value::String(" ".to_string()), + ]), + ), + ( + "{\"kek\":1}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Number(BigInt::from(1)), + )])), + ), + ( + "{\"kek\": 1}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Number(BigInt::from(1)), + )])), + ), + ( + "{\"kek\":false}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Bool(false), + )])), + ), + ( + "{\"kek\":true}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Bool(true), + )])), + ), + ( + "{\"kek\":null}", + Value::Object(BTreeMap::from_iter(vec![("kek".to_string(), Value::Null)])), + ), + ( + "{\"kek\":{}}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Object(BTreeMap::new()), + )])), + ), + ( + "{\"kek\":[]}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![]), + )])), + ), + ( + "{\"kek\":[ ]}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![]), + )])), + ), + ( + " {\"kek\": []}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![]), + )])), + ), + ( + "{\"kek\":[{\"\":[{}]}]}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Object(BTreeMap::new())]), + )]))]), + )])), + ), + ( + "{\"kek\":[{\"\":[1]}]}", + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Number(BigInt::from(1))]), + )]))]), + )])), + ), + ( + "[{\"kek\":[{\"\":[1, \"{}[]:,\\\"{}[]:,\\\"\"\ + ]}]},{\"kek\":[{\"\":[1]}]}]", + Value::Array(vec![ + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![ + Value::Number(BigInt::from(1)), + Value::String("{}[]:,\"{}[]:,\"".to_string()), + ]), + )]))]), + )])), + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Number(BigInt::from(1))]), + )]))]), + )])), + ]), + ), + ( + "[{\"kek\":[{\"\":[1]}]},{\"kek\":[{\"\":[1]}]}]", + Value::Array(vec![ + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Number(BigInt::from(1))]), + )]))]), + )])), + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Number(BigInt::from(1))]), + )]))]), + )])), + ]), + ), + ( + "[\ + {\ + \"kek\": [\ + {\"\":[1]}]},{\ + \"kek\":\ + [{\"\":[1]}]\ + }]", + Value::Array(vec![ + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Number(BigInt::from(1))]), + )]))]), + )])), + Value::Object(BTreeMap::from_iter(vec![( + "kek".to_string(), + Value::Array(vec![Value::Object(BTreeMap::from_iter(vec![( + String::new(), + Value::Array(vec![Value::Number(BigInt::from(1))]), + )]))]), + )])), + ]), + ), + ]; + + for (case, correct) in cases { + let computed_tokens = tokenize_string(case.to_string()); + let parsed = parse_json(computed_tokens); + assert!( + parsed.is_ok(), + "Can't parse case: {}\n error: {:?}\n correct: {:?}\n", + case, + parsed.err(), + correct + ); + assert_eq!( + parsed.clone().unwrap(), + correct, + "Mismatch case: {}\n parsed: {:?}\n correct: {:?}\n", + case, + parsed, + correct + ); + } + } +} diff --git a/chain/rust/src/json/mod.rs b/chain/rust/src/json/mod.rs new file mode 100644 index 00000000..8e588728 --- /dev/null +++ b/chain/rust/src/json/mod.rs @@ -0,0 +1,2 @@ +mod json_serialize; +pub mod plutus_datums; diff --git a/chain/rust/src/json/plutus_datums.rs b/chain/rust/src/json/plutus_datums.rs new file mode 100644 index 00000000..12435f9e --- /dev/null +++ b/chain/rust/src/json/plutus_datums.rs @@ -0,0 +1,335 @@ +use crate::{ + json::json_serialize::{JsonParseError, Value as JSONValue}, + plutus::{ConstrPlutusData, PlutusData, PlutusMap}, + utils::BigInt, +}; +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" + /// BigInt'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 { + let value = JSONValue::from_string(json.to_string())?; + encode_json_value_to_plutus_datum(value, schema) +} + +pub fn encode_json_value_to_plutus_datum( + value: JSONValue, + schema: CardanoNodePlutusDatumSchema, +) -> Result { + fn encode_string( + s: &str, + schema: CardanoNodePlutusDatumSchema, + is_key: bool, + ) -> Result { + 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 BigInt::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, + schema: CardanoNodePlutusDatumSchema, + ) -> Result { + 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 { + 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 { + 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::, 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::, 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), + } +} diff --git a/chain/rust/src/lib.rs b/chain/rust/src/lib.rs index a8bef07a..64f2688a 100644 --- a/chain/rust/src/lib.rs +++ b/chain/rust/src/lib.rs @@ -22,6 +22,7 @@ pub mod deposit; pub mod fees; pub mod genesis; pub mod governance; +pub mod json; pub mod min_ada; pub mod plutus; pub mod serialization; diff --git a/chain/rust/src/plutus/mod.rs b/chain/rust/src/plutus/mod.rs index 2837e834..1288cdbb 100644 --- a/chain/rust/src/plutus/mod.rs +++ b/chain/rust/src/plutus/mod.rs @@ -128,7 +128,7 @@ pub enum PlutusData { #[serde(skip)] list_encoding: LenEncoding, }, - BigInt(BigInt), + Integer(BigInt), Bytes { bytes: Vec, #[derivative( @@ -158,8 +158,8 @@ impl PlutusData { } } - pub fn new_big_int(big_int: BigInt) -> Self { - Self::BigInt(big_int) + pub fn new_integer(integer: BigInt) -> Self { + Self::Integer(integer) } pub fn new_bytes(bytes: Vec) -> Self { diff --git a/chain/rust/src/plutus/serialization.rs b/chain/rust/src/plutus/serialization.rs index 1e34e250..aee33c5c 100644 --- a/chain/rust/src/plutus/serialization.rs +++ b/chain/rust/src/plutus/serialization.rs @@ -473,7 +473,7 @@ impl Serialize for PlutusData { } list_encoding.end(serializer, force_canonical) } - PlutusData::BigInt(big_int) => big_int.serialize(serializer, force_canonical), + PlutusData::Integer(big_int) => big_int.serialize(serializer, force_canonical), // hand-written PlutusData::Bytes { bytes, @@ -499,8 +499,8 @@ impl Deserialize for PlutusData { .unwrap(); if tag == 2 || tag == 3 { BigInt::deserialize(raw) - .map(Self::BigInt) - .map_err(|e| e.annotate("BigInt")) + .map(Self::Integer) + .map_err(|e| e.annotate("Integer")) } else { ConstrPlutusData::deserialize(raw) .map(Self::ConstrPlutusData) @@ -534,8 +534,8 @@ impl Deserialize for PlutusData { } cbor_event::Type::UnsignedInteger | cbor_event::Type::NegativeInteger => { BigInt::deserialize(raw) - .map(Self::BigInt) - .map_err(|e| e.annotate("BigInt")) + .map(Self::Integer) + .map_err(|e| e.annotate("Integer")) } // hand-written 100% since the format is not just arbitrary CBOR bytes cbor_event::Type::Bytes => read_bounded_bytes(raw) diff --git a/chain/wasm/src/json/mod.rs b/chain/wasm/src/json/mod.rs new file mode 100644 index 00000000..bbdd66d6 --- /dev/null +++ b/chain/wasm/src/json/mod.rs @@ -0,0 +1 @@ +pub mod plutus_datums; diff --git a/chain/wasm/src/json/plutus_datums.rs b/chain/wasm/src/json/plutus_datums.rs new file mode 100644 index 00000000..82fb4e5f --- /dev/null +++ b/chain/wasm/src/json/plutus_datums.rs @@ -0,0 +1,24 @@ +pub use cml_chain::json::plutus_datums::CardanoNodePlutusDatumSchema; + +use crate::plutus::PlutusData; + +use wasm_bindgen::prelude::{wasm_bindgen, JsError}; + +#[wasm_bindgen] +pub fn encode_json_str_to_plutus_datum( + json: &str, + schema: CardanoNodePlutusDatumSchema, +) -> Result { + cml_chain::json::plutus_datums::encode_json_str_to_plutus_datum(json, schema) + .map(Into::into) + .map_err(Into::into) +} + +#[wasm_bindgen] +pub fn decode_plutus_datum_to_json_str( + datum: &PlutusData, + schema: CardanoNodePlutusDatumSchema, +) -> Result { + cml_chain::json::plutus_datums::decode_plutus_datum_to_json_str(datum.as_ref(), schema) + .map_err(Into::into) +} diff --git a/chain/wasm/src/lib.rs b/chain/wasm/src/lib.rs index 0e4034b5..b0a9f46b 100644 --- a/chain/wasm/src/lib.rs +++ b/chain/wasm/src/lib.rs @@ -20,6 +20,7 @@ pub mod certs; pub mod crypto; pub mod fees; pub mod governance; +pub mod json; pub mod plutus; pub mod transaction; pub mod utils; diff --git a/chain/wasm/src/plutus/mod.rs b/chain/wasm/src/plutus/mod.rs index 4720ab83..cbcbd2d9 100644 --- a/chain/wasm/src/plutus/mod.rs +++ b/chain/wasm/src/plutus/mod.rs @@ -123,8 +123,8 @@ impl PlutusData { Self(cml_chain::plutus::PlutusData::new_list(list.clone().into())) } - pub fn new_big_int(big_int: &BigInt) -> Self { - Self(cml_chain::plutus::PlutusData::new_big_int( + pub fn new_integer(big_int: &BigInt) -> Self { + Self(cml_chain::plutus::PlutusData::new_integer( big_int.clone().into(), )) } @@ -138,7 +138,7 @@ impl PlutusData { cml_chain::plutus::PlutusData::ConstrPlutusData(_) => PlutusDataKind::ConstrPlutusData, cml_chain::plutus::PlutusData::Map { .. } => PlutusDataKind::Map, cml_chain::plutus::PlutusData::List { .. } => PlutusDataKind::List, - cml_chain::plutus::PlutusData::BigInt(_) => PlutusDataKind::BigInt, + cml_chain::plutus::PlutusData::Integer(_) => PlutusDataKind::Integer, cml_chain::plutus::PlutusData::Bytes { .. } => PlutusDataKind::Bytes, } } @@ -166,9 +166,9 @@ impl PlutusData { } } - pub fn as_big_int(&self) -> Option { + pub fn as_integer(&self) -> Option { match &self.0 { - cml_chain::plutus::PlutusData::BigInt(big_int) => Some(big_int.clone().into()), + cml_chain::plutus::PlutusData::Integer(big_int) => Some(big_int.clone().into()), _ => None, } } @@ -186,7 +186,7 @@ pub enum PlutusDataKind { ConstrPlutusData, Map, List, - BigInt, + Integer, Bytes, } diff --git a/specs/conway/plutus.cddl b/specs/conway/plutus.cddl index 38ef4a8e..f9902b3d 100644 --- a/specs/conway/plutus.cddl +++ b/specs/conway/plutus.cddl @@ -18,7 +18,7 @@ plutus_data = constr_plutus_data / { * plutus_data => plutus_data } ; @name map / [ * plutus_data ] ; @name list - / big_int + / big_int ; @name integer / bounded_bytes ; @name bytes big_int = _CDDL_CODEGEN_EXTERN_TYPE_