diff --git a/Cargo.lock b/Cargo.lock index b865897548..fd2304ce10 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,9 +392,9 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.15" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "shlex", ] @@ -452,9 +452,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.16" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -462,9 +462,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -572,9 +572,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -1480,9 +1480,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", @@ -2038,6 +2038,14 @@ dependencies = [ "syn", ] +[[package]] +name = "multiversx-sc-codec-human-readable" +version = "0.1.0" +dependencies = [ + "multiversx-sc-scenario", + "serde_json", +] + [[package]] name = "multiversx-sc-derive" version = "0.53.0" @@ -2136,7 +2144,6 @@ dependencies = [ "multiversx-sc-scenario", "multiversx-sdk", "serde_json", - "tokio", ] [[package]] @@ -2939,9 +2946,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.35" +version = "0.38.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" dependencies = [ "bitflags", "errno", @@ -3158,9 +3165,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "indexmap", "itoa", @@ -3531,9 +3538,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 651456eebd..5a1a1951c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "data/codec", "data/codec-derive", + "data/human-readable", "framework/base", "framework/derive", diff --git a/data/codec/src/single/top_en.rs b/data/codec/src/single/top_en.rs index dd131ffc1f..9b5ddf10a4 100644 --- a/data/codec/src/single/top_en.rs +++ b/data/codec/src/single/top_en.rs @@ -5,7 +5,7 @@ use crate::{ use alloc::vec::Vec; use unwrap_infallible::UnwrapInfallible; -pub trait TopEncode: Sized { +pub trait TopEncode { /// Attempt to serialize the value to ouput. fn top_encode(&self, output: O) -> Result<(), EncodeError> where diff --git a/data/human-readable/Cargo.toml b/data/human-readable/Cargo.toml new file mode 100644 index 0000000000..9ceacbac6a --- /dev/null +++ b/data/human-readable/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "multiversx-sc-codec-human-readable" +version = "0.1.0" +edition = "2018" +publish = false + +authors = ["MultiversX "] +license = "GPL-3.0-only" +readme = "README.md" +repository = "https://github.com/multiversx/mx-sdk-rs" +homepage = "https://multiversx.com/" +documentation = "https://docs.multiversx.com/" +description = "Conversions from a human readable format to the multiversx-sc-codec" +keywords = ["multiversx", "wasm", "webassembly", "blockchain", "contract"] +categories = ["cryptography::cryptocurrencies", "development-tools"] + +[dependencies] +serde_json = { version = "1.0.68" } + +[dependencies.multiversx-sc-scenario] +version = "=0.53.0" +path = "../../framework/scenario" diff --git a/data/human-readable/src/decode.rs b/data/human-readable/src/decode.rs new file mode 100644 index 0000000000..5a0e0b850d --- /dev/null +++ b/data/human-readable/src/decode.rs @@ -0,0 +1,240 @@ +use std::{error::Error, fmt::Display}; + +use crate::{ + format::HumanReadableValue, + multiversx_sc::abi::{TypeContents, TypeDescription}, + SingleValue, StructField, StructValue, +}; +use multiversx_sc_scenario::{ + bech32, + multiversx_sc::abi::{ContractAbi, EnumVariantDescription, StructFieldDescription}, + num_bigint::{BigInt, BigUint}, +}; + +use crate::AnyValue; + +pub fn decode_human_readable_value( + input: &HumanReadableValue, + type_name: &str, + contract_abi: &ContractAbi, +) -> Result> { + let type_description = contract_abi.type_descriptions.find_or_default(type_name); + decode_any_value(input, &type_description, contract_abi) +} + +pub fn decode_any_value( + input: &HumanReadableValue, + type_description: &TypeDescription, + contract_abi: &ContractAbi, +) -> Result> { + match &type_description.contents { + TypeContents::NotSpecified => { + decode_single_value(input, type_description.names.abi.as_str()) + }, + TypeContents::Enum(variants) => decode_enum(input, variants, contract_abi), + TypeContents::Struct(fields) => decode_struct(input, fields, contract_abi), + TypeContents::ExplicitEnum(_) => panic!("not supported"), + } +} + +fn decode_single_value( + input: &HumanReadableValue, + type_name: &str, +) -> Result> { + match type_name { + "BigUint" | "u64" | "u32" | "u16" | "usize" | "u8" => { + let number_value = input + .get_value() + .as_number() + .ok_or_else(|| Box::new(DecodeError("expected unsigned number value")))?; + + let value = number_value.to_string().parse::()?; + Ok(AnyValue::SingleValue(SingleValue::UnsignedNumber(value))) + }, + "BigInt" | "i64" | "i32" | "i16" | "isize" | "i8" => { + let number_value = input + .get_value() + .as_number() + .ok_or_else(|| Box::new(DecodeError("expected number value")))?; + + let value = number_value.to_string().parse::()?; + Ok(AnyValue::SingleValue(SingleValue::SignedNumber(value))) + }, + "ManagedBuffer" => { + let array_value = input + .get_value() + .as_array() + .ok_or_else(|| Box::new(DecodeError("expected bytes value")))?; + + let mut bytes = vec![0u8; array_value.len()]; + for (i, value) in array_value.iter().enumerate() { + let number_value = value + .as_u64() + .ok_or_else(|| Box::new(DecodeError("expected byte value")))?; + if number_value > 255 { + return Err(Box::new(DecodeError("expected byte value"))); + } + bytes[i] = number_value as u8; + } + + Ok(AnyValue::SingleValue(SingleValue::Bytes(bytes.into()))) + }, + "string" | "utf-8 string" => { + let str_value = input + .get_value() + .as_str() + .ok_or_else(|| Box::new(DecodeError("expected string value")))?; + + Ok(AnyValue::SingleValue(SingleValue::String( + str_value.to_string(), + ))) + }, + "Address" => { + let str_value = input + .get_value() + .as_str() + .ok_or_else(|| Box::new(DecodeError("expected string value")))?; + + let address = bech32::try_decode(str_value) + .map_err(|_| Box::new(DecodeError("failed to parse address")))?; + + Ok(AnyValue::SingleValue(SingleValue::Bytes( + address.into_boxed_bytes().into_box(), + ))) + }, + "bool" => { + let bool_value = input + .get_value() + .as_bool() + .ok_or_else(|| Box::new(DecodeError("expected bool value")))?; + + Ok(AnyValue::SingleValue(SingleValue::Bool(bool_value))) + }, + _ => Err(Box::new(DecodeError("unknown type"))), + } +} + +pub fn decode_struct( + input: &HumanReadableValue, + fields: &[StructFieldDescription], + contract_abi: &ContractAbi, +) -> Result> { + let mut field_values: Vec = vec![]; + + for field in fields.iter() { + let value = input + .child(&field.name) + .ok_or_else(|| Box::new(DecodeError("missing field")))?; + let value = decode_human_readable_value(&value, &field.field_type.abi, contract_abi)?; + field_values.push(StructField { + name: field.name.clone(), + value, + }); + } + + Ok(AnyValue::Struct(StructValue(field_values))) +} + +pub fn decode_enum( + input: &HumanReadableValue, + variants: &[EnumVariantDescription], + contract_abi: &ContractAbi, +) -> Result> { + if input.get_value().is_string() { + let discriminant_name = input.get_value().as_str().unwrap(); + let variant = variants + .iter() + .find(|el| el.name == discriminant_name) + .ok_or_else(|| Box::new(DecodeError("enum variant not found")))?; + + if !variant.is_empty_variant() { + return Err(Box::new(DecodeError("enum variant is not a tuple variant"))); + } + + return Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value: AnyValue::None, + }))); + } + + if !input.get_value().is_object() { + return Err(Box::new(DecodeError( + "expected object or string value for enum", + ))); + } + + let obj_value = input.get_value().as_object().unwrap(); + if obj_value.keys().len() != 1 { + return Err(Box::new(DecodeError( + "expected object with single key for enum", + ))); + } + + let discriminant_name = obj_value.keys().next().unwrap().as_str(); + let variant = variants + .iter() + .find(|el| el.name == discriminant_name) + .ok_or_else(|| Box::new(DecodeError("enum variant not found")))?; + + // handle tuple with only one field as a special case (we don't need a wrapper array) + if variant.is_tuple_variant() && variant.fields.len() == 1 { + let value = input.child(variant.name.as_str()).unwrap(); + let value = + decode_human_readable_value(&value, &variant.fields[0].field_type.abi, contract_abi)?; + return Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value, + }))); + } else if variant.is_tuple_variant() { + let value = input.child(variant.name.as_str()).unwrap(); + let value = value + .get_value() + .as_array() + .ok_or_else(|| Box::new(DecodeError("expected array for enum tuple variant")))?; + + if value.len() != variant.fields.len() { + return Err(Box::new(DecodeError( + "expected array with the same length as the tuple variant fields", + ))); + } + + let mut field_values: Vec = vec![]; + for (i, field) in variant.fields.iter().enumerate() { + let value = value.get(i).unwrap(); + let value = decode_human_readable_value( + &(value.to_owned().into()), + &field.field_type.abi, + contract_abi, + )?; + field_values.push(StructField { + name: field.name.clone(), + value, + }); + } + + return Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value: AnyValue::Struct(StructValue(field_values)), + }))); + } + + // is not empty and is not a tuple so just try to parse a struct from the fields + let value = input.child(variant.name.as_str()).unwrap(); + let value = decode_struct(&value, &variant.fields, contract_abi)?; + + Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value, + }))) +} + +#[derive(Debug)] +pub struct DecodeError(&'static str); + +impl Display for DecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Error for DecodeError {} diff --git a/data/human-readable/src/defaults.rs b/data/human-readable/src/defaults.rs new file mode 100644 index 0000000000..a48aedf4de --- /dev/null +++ b/data/human-readable/src/defaults.rs @@ -0,0 +1,126 @@ +use std::{error::Error, fmt::Display}; + +use multiversx_sc_scenario::{ + multiversx_sc::abi::{ + ContractAbi, EnumVariantDescription, StructFieldDescription, TypeContents, TypeDescription, + }, + num_bigint::{BigInt, BigUint}, +}; + +use crate::{AnyValue, SingleValue, StructField, StructValue}; + +pub fn default_value_for_abi_type( + type_name: &str, + contract_abi: &ContractAbi, +) -> Result> { + let type_description = contract_abi.type_descriptions.find_or_default(type_name); + default_value_for_any_value(&type_description, contract_abi) +} + +pub fn default_value_for_any_value( + type_description: &TypeDescription, + contract_abi: &ContractAbi, +) -> Result> { + match &type_description.contents { + TypeContents::NotSpecified => { + default_value_for_single_value(type_description.names.abi.as_str()) + }, + TypeContents::Enum(variants) => default_value_for_enum(variants, contract_abi), + TypeContents::Struct(fields) => default_value_for_struct(fields, contract_abi), + TypeContents::ExplicitEnum(_) => panic!("not supported"), + } +} + +pub fn default_value_for_single_value(type_name: &str) -> Result> { + match type_name { + "BigUint" | "u64" | "u32" | "u16" | "usize" | "u8" => Ok(AnyValue::SingleValue( + SingleValue::UnsignedNumber(BigUint::default()), + )), + "BigInt" | "i64" | "i32" | "i16" | "isize" | "i8" => Ok(AnyValue::SingleValue( + SingleValue::SignedNumber(BigInt::default()), + )), + "ManagedBuffer" => Ok(AnyValue::SingleValue(SingleValue::Bytes(Vec::new().into()))), + "string" | "utf-8 string" => Ok(AnyValue::SingleValue(SingleValue::String("".to_owned()))), + "Address" => Ok(AnyValue::SingleValue(SingleValue::Bytes( + vec![0u8; 32].into(), + ))), + "bool" => Ok(AnyValue::SingleValue(SingleValue::Bool(false))), + _ => Err(Box::new(DefaultValueError("unknown type"))), + } +} + +pub fn default_value_for_struct( + fields: &[StructFieldDescription], + contract_abi: &ContractAbi, +) -> Result> { + let mut field_values: Vec = vec![]; + + for field in fields.iter() { + let value = default_value_for_abi_type(&field.field_type.abi, contract_abi)?; + field_values.push(StructField { + name: field.name.clone(), + value, + }); + } + + Ok(AnyValue::Struct(StructValue(field_values))) +} + +pub fn default_value_for_enum( + variants: &[EnumVariantDescription], + contract_abi: &ContractAbi, +) -> Result> { + let variant = variants + .iter() + .find(|el| el.discriminant == 0) + .ok_or_else(|| Box::new(DefaultValueError("enum variant not found")))?; + + if variant.is_empty_variant() { + return Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value: AnyValue::None, + }))); + } + + // handle tuple with only one field as a special case (we don't need a wrapper array) + if variant.is_tuple_variant() && variant.fields.len() == 1 { + let value = default_value_for_abi_type(&variant.fields[0].field_type.abi, contract_abi)?; + return Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value, + }))); + } else if variant.is_tuple_variant() { + let mut field_values: Vec = vec![]; + for field in variant.fields.iter() { + let value = default_value_for_abi_type(&field.field_type.abi, contract_abi)?; + field_values.push(StructField { + name: field.name.clone(), + value, + }); + } + + return Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value: AnyValue::Struct(StructValue(field_values)), + }))); + } + + // is not empty and is not a tuple so just try to parse a struct from the fields + let value = default_value_for_struct(&variant.fields, contract_abi)?; + + Ok(AnyValue::Enum(Box::new(crate::EnumVariant { + discriminant: variant.discriminant, + value, + }))) +} + +#[derive(Debug)] +pub struct DefaultValueError(&'static str); + +impl Display for DefaultValueError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Error for DefaultValueError {} diff --git a/data/human-readable/src/encode.rs b/data/human-readable/src/encode.rs new file mode 100644 index 0000000000..b7da4e3875 --- /dev/null +++ b/data/human-readable/src/encode.rs @@ -0,0 +1,232 @@ +use std::{error::Error, fmt::Display}; + +use multiversx_sc_scenario::{ + bech32, + imports::Address, + multiversx_sc::abi::{ + ContractAbi, EnumVariantDescription, StructFieldDescription, TypeContents, TypeDescription, + }, +}; +use serde_json::{Map, Value as JsonValue}; + +use crate::{format::HumanReadableValue, AnyValue, SingleValue}; + +pub fn encode_human_readable_value( + input: &AnyValue, + type_name: &str, + contract_abi: &ContractAbi, +) -> Result> { + let type_description = contract_abi.type_descriptions.find_or_default(type_name); + encode_any_value(input, &type_description, contract_abi) +} + +pub fn encode_any_value( + input: &AnyValue, + type_description: &TypeDescription, + contract_abi: &ContractAbi, +) -> Result> { + match &type_description.contents { + TypeContents::NotSpecified => { + encode_single_value(input, type_description.names.abi.as_str()) + }, + TypeContents::Enum(variants) => encode_enum(input, variants, contract_abi), + TypeContents::Struct(fields) => encode_struct(input, fields, contract_abi), + TypeContents::ExplicitEnum(_) => panic!("not supported"), + } +} + +fn encode_single_value( + input: &AnyValue, + type_name: &str, +) -> Result> { + match type_name { + "BigUint" | "u64" | "u32" | "u16" | "usize" | "u8" => { + let AnyValue::SingleValue(value) = input else { + return Err(Box::new(EncodeError("expected single value"))); + }; + let SingleValue::UnsignedNumber(value) = value else { + return Err(Box::new(EncodeError("expected unsigned number value"))); + }; + + // could be biguint, so we convert to string first + let json_value: JsonValue = serde_json::from_str(&value.to_string()) + .map_err(|_| Box::new(EncodeError("expected number value")))?; + + Ok(json_value.into()) + }, + "BigInt" | "i64" | "i32" | "i16" | "isize" | "i8" => { + let AnyValue::SingleValue(value) = input else { + return Err(Box::new(EncodeError("expected single value"))); + }; + let SingleValue::SignedNumber(value) = value else { + return Err(Box::new(EncodeError("expected signed number value"))); + }; + + // could be bigint, so we convert to string first + let json_value: JsonValue = serde_json::from_str(&value.to_string()) + .map_err(|_| Box::new(EncodeError("expected number value")))?; + + Ok(json_value.into()) + }, + "ManagedBuffer" => { + let AnyValue::SingleValue(value) = input else { + return Err(Box::new(EncodeError("expected single value"))); + }; + let SingleValue::Bytes(value) = value else { + return Err(Box::new(EncodeError("expected bytes value"))); + }; + + Ok(JsonValue::Array( + value + .iter() + .map(|b| JsonValue::Number(b.to_owned().into())) + .collect(), + ) + .into()) + }, + "string" | "utf-8 string" => { + let AnyValue::SingleValue(value) = input else { + return Err(Box::new(EncodeError("expected single value"))); + }; + let SingleValue::String(value) = value else { + return Err(Box::new(EncodeError("expected string value"))); + }; + + Ok(JsonValue::String(value.to_owned()).into()) + }, + "Address" => { + let AnyValue::SingleValue(value) = input else { + return Err(Box::new(EncodeError("expected single value"))); + }; + let SingleValue::Bytes(value) = value else { + return Err(Box::new(EncodeError("expected bytes value"))); + }; + + let bech32_addres_string = bech32::encode(&Address::from_slice(value)); + Ok(JsonValue::String(bech32_addres_string).into()) + }, + "bool" => { + let AnyValue::SingleValue(value) = input else { + return Err(Box::new(EncodeError("expected single value"))); + }; + let SingleValue::Bool(value) = value else { + return Err(Box::new(EncodeError("expected bool value"))); + }; + + Ok(JsonValue::Bool(value.to_owned()).into()) + }, + _ => { + println!("unknown type: {}", type_name); + Err(Box::new(EncodeError("unknown type"))) + }, + } +} + +pub fn encode_struct( + input: &AnyValue, + fields: &[StructFieldDescription], + contract_abi: &ContractAbi, +) -> Result> { + let AnyValue::Struct(struct_value) = input else { + return Err(Box::new(EncodeError("expected struct value"))); + }; + let mut struct_fields = struct_value.0.iter(); + + let mut field_values: Map = Map::new(); + + for field in fields.iter() { + let value = struct_fields + .find(|f| f.name == field.name) + .ok_or_else(|| Box::new(EncodeError("missing field")))?; + + let value = encode_human_readable_value(&value.value, &field.field_type.abi, contract_abi)?; + field_values.insert(field.name.to_owned(), value.get_value().to_owned()); + } + + Ok(JsonValue::Object(field_values).into()) +} + +pub fn encode_enum( + input: &AnyValue, + variants: &[EnumVariantDescription], + contract_abi: &ContractAbi, +) -> Result> { + let AnyValue::Enum(enum_value) = input else { + return Err(Box::new(EncodeError("expected enum value"))); + }; + let variant = variants + .iter() + .find(|v| v.discriminant == enum_value.discriminant) + .ok_or_else(|| Box::new(EncodeError("missing variant")))?; + + if variant.is_empty_variant() { + return Ok(JsonValue::String(variant.name.to_owned()).into()); + } + + if variant.is_tuple_variant() && variant.fields.len() == 1 { + let value = encode_human_readable_value( + &enum_value.value, + &variant.fields[0].field_type.abi, + contract_abi, + )?; + return Ok(JsonValue::Object( + vec![(variant.name.to_owned(), value.get_value().to_owned())] + .into_iter() + .collect(), + ) + .into()); + } + + if variant.is_tuple_variant() { + let AnyValue::Struct(variant_fields) = &enum_value.value else { + return Err(Box::new(EncodeError("expected struct value"))); + }; + + let mut field_values: Vec = vec![]; + + for (field, field_type) in variant.fields.iter().zip(variant_fields.0.iter()) { + let value = encode_human_readable_value( + &field_type.value, + &field.field_type.abi, + contract_abi, + )?; + field_values.push(value.get_value().to_owned()); + } + + return Ok(JsonValue::Object( + vec![(variant.name.to_owned(), JsonValue::Array(field_values))] + .into_iter() + .collect(), + ) + .into()); + } + + let AnyValue::Struct(variant_fields) = &enum_value.value else { + return Err(Box::new(EncodeError("expected struct value"))); + }; + + let mut field_values: Map = Map::new(); + for (field, field_type) in variant.fields.iter().zip(variant_fields.0.iter()) { + let value = + encode_human_readable_value(&field_type.value, &field.field_type.abi, contract_abi)?; + field_values.insert(field.name.to_owned(), value.get_value().to_owned()); + } + + Ok(JsonValue::Object( + vec![(variant.name.to_owned(), JsonValue::Object(field_values))] + .into_iter() + .collect(), + ) + .into()) +} + +#[derive(Debug)] +pub struct EncodeError(&'static str); + +impl Display for EncodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Error for EncodeError {} diff --git a/data/human-readable/src/format.rs b/data/human-readable/src/format.rs new file mode 100644 index 0000000000..69b244cdf5 --- /dev/null +++ b/data/human-readable/src/format.rs @@ -0,0 +1,39 @@ +pub use serde_json::Value as JsonValue; +use std::{fmt::Display, str::FromStr}; + +pub struct HumanReadableValue { + value: JsonValue, +} + +impl FromStr for HumanReadableValue { + type Err = String; + + fn from_str(s: &str) -> Result { + let value = serde_json::from_str(s).map_err(|e| e.to_string())?; + Ok(HumanReadableValue { value }) + } +} + +impl From for HumanReadableValue { + fn from(value: JsonValue) -> Self { + HumanReadableValue { value } + } +} + +impl HumanReadableValue { + pub fn get_value(&self) -> &JsonValue { + &self.value + } + + pub fn child(&self, key: &str) -> Option { + self.value.get(key).map(|value| HumanReadableValue { + value: value.clone(), + }) + } +} + +impl Display for HumanReadableValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} diff --git a/data/human-readable/src/lib.rs b/data/human-readable/src/lib.rs new file mode 100644 index 0000000000..6a810391bf --- /dev/null +++ b/data/human-readable/src/lib.rs @@ -0,0 +1,11 @@ +mod decode; +mod defaults; +mod encode; +pub mod format; +mod value; + +pub use decode::*; +pub use defaults::*; +pub use encode::*; +use multiversx_sc_scenario::multiversx_sc; +pub use value::*; diff --git a/data/human-readable/src/value/any_value.rs b/data/human-readable/src/value/any_value.rs new file mode 100644 index 0000000000..165143ba85 --- /dev/null +++ b/data/human-readable/src/value/any_value.rs @@ -0,0 +1,42 @@ +use multiversx_sc_scenario::multiversx_sc::codec::{ + EncodeErrorHandler, NestedEncode, NestedEncodeOutput, TopEncode, TopEncodeOutput, +}; + +use crate::{EnumVariant, SingleValue, StructValue}; + +pub enum AnyValue { + None, + SingleValue(SingleValue), + Struct(StructValue), + Enum(Box), +} + +impl NestedEncode for AnyValue { + fn dep_encode_or_handle_err(&self, dest: &mut O, h: H) -> Result<(), H::HandledErr> + where + O: NestedEncodeOutput, + H: EncodeErrorHandler, + { + match self { + AnyValue::None => Ok(()), + AnyValue::SingleValue(sv) => sv.dep_encode_or_handle_err(dest, h), + AnyValue::Struct(s) => s.dep_encode_or_handle_err(dest, h), + AnyValue::Enum(e) => e.dep_encode_or_handle_err(dest, h), + } + } +} + +impl TopEncode for AnyValue { + fn top_encode_or_handle_err(&self, output: O, h: H) -> Result<(), H::HandledErr> + where + O: TopEncodeOutput, + H: EncodeErrorHandler, + { + match self { + AnyValue::None => Ok(()), + AnyValue::SingleValue(sv) => sv.top_encode_or_handle_err(output, h), + AnyValue::Struct(s) => s.top_encode_or_handle_err(output, h), + AnyValue::Enum(e) => e.top_encode_or_handle_err(output, h), + } + } +} diff --git a/data/human-readable/src/value/enum_value.rs b/data/human-readable/src/value/enum_value.rs new file mode 100644 index 0000000000..0a85d668e1 --- /dev/null +++ b/data/human-readable/src/value/enum_value.rs @@ -0,0 +1,35 @@ +use multiversx_sc_scenario::multiversx_sc::codec::{ + EncodeErrorHandler, NestedEncode, NestedEncodeOutput, TopEncode, TopEncodeOutput, +}; + +use crate::AnyValue; + +pub struct EnumVariant { + pub discriminant: usize, + pub value: AnyValue, +} + +impl NestedEncode for EnumVariant { + fn dep_encode_or_handle_err(&self, dest: &mut O, h: H) -> Result<(), H::HandledErr> + where + O: NestedEncodeOutput, + H: EncodeErrorHandler, + { + (self.discriminant as u8).dep_encode_or_handle_err(dest, h)?; + self.value.dep_encode_or_handle_err(dest, h)?; + Ok(()) + } +} + +impl TopEncode for EnumVariant { + fn top_encode_or_handle_err(&self, output: O, h: H) -> Result<(), H::HandledErr> + where + O: TopEncodeOutput, + H: EncodeErrorHandler, + { + let mut buffer = output.start_nested_encode(); + self.dep_encode_or_handle_err(&mut buffer, h)?; + output.finalize_nested_encode(buffer); + Ok(()) + } +} diff --git a/data/human-readable/src/value/mod.rs b/data/human-readable/src/value/mod.rs new file mode 100644 index 0000000000..5c87ded058 --- /dev/null +++ b/data/human-readable/src/value/mod.rs @@ -0,0 +1,9 @@ +mod any_value; +mod enum_value; +mod single_value; +mod struct_value; + +pub use any_value::*; +pub use enum_value::*; +pub use single_value::*; +pub use struct_value::*; diff --git a/data/human-readable/src/value/single_value.rs b/data/human-readable/src/value/single_value.rs new file mode 100644 index 0000000000..a46e811bbd --- /dev/null +++ b/data/human-readable/src/value/single_value.rs @@ -0,0 +1,44 @@ +use multiversx_sc_scenario::multiversx_sc::codec::{ + num_bigint::{BigInt, BigUint}, + EncodeErrorHandler, NestedEncode, NestedEncodeOutput, TopEncode, TopEncodeOutput, +}; + +pub enum SingleValue { + UnsignedNumber(BigUint), + SignedNumber(BigInt), + Bytes(Box<[u8]>), + String(String), + Bool(bool), +} + +impl NestedEncode for SingleValue { + fn dep_encode_or_handle_err(&self, dest: &mut O, h: H) -> Result<(), H::HandledErr> + where + O: NestedEncodeOutput, + H: EncodeErrorHandler, + { + match self { + SingleValue::UnsignedNumber(bu) => bu.dep_encode_or_handle_err(dest, h), + SingleValue::SignedNumber(bi) => bi.dep_encode_or_handle_err(dest, h), + SingleValue::Bytes(bytes) => bytes.dep_encode_or_handle_err(dest, h), + SingleValue::String(s) => s.as_bytes().dep_encode_or_handle_err(dest, h), + SingleValue::Bool(b) => b.dep_encode_or_handle_err(dest, h), + } + } +} + +impl TopEncode for SingleValue { + fn top_encode_or_handle_err(&self, output: O, h: H) -> Result<(), H::HandledErr> + where + O: TopEncodeOutput, + H: EncodeErrorHandler, + { + match self { + SingleValue::UnsignedNumber(bu) => bu.top_encode_or_handle_err(output, h), + SingleValue::SignedNumber(bi) => bi.top_encode_or_handle_err(output, h), + SingleValue::Bytes(bytes) => bytes.top_encode_or_handle_err(output, h), + SingleValue::String(s) => s.as_bytes().top_encode_or_handle_err(output, h), + SingleValue::Bool(b) => b.top_encode_or_handle_err(output, h), + } + } +} diff --git a/data/human-readable/src/value/struct_value.rs b/data/human-readable/src/value/struct_value.rs new file mode 100644 index 0000000000..3f1c6603c9 --- /dev/null +++ b/data/human-readable/src/value/struct_value.rs @@ -0,0 +1,38 @@ +use multiversx_sc_scenario::multiversx_sc::codec::{ + EncodeErrorHandler, NestedEncode, NestedEncodeOutput, TopEncode, TopEncodeOutput, +}; + +use crate::AnyValue; + +pub struct StructValue(pub Vec); + +pub struct StructField { + pub name: String, + pub value: AnyValue, +} + +impl NestedEncode for StructValue { + fn dep_encode_or_handle_err(&self, dest: &mut O, h: H) -> Result<(), H::HandledErr> + where + O: NestedEncodeOutput, + H: EncodeErrorHandler, + { + for field in &self.0 { + field.value.dep_encode_or_handle_err(dest, h)?; + } + Ok(()) + } +} + +impl TopEncode for StructValue { + fn top_encode_or_handle_err(&self, output: O, h: H) -> Result<(), H::HandledErr> + where + O: TopEncodeOutput, + H: EncodeErrorHandler, + { + let mut buffer = output.start_nested_encode(); + self.dep_encode_or_handle_err(&mut buffer, h)?; + output.finalize_nested_encode(buffer); + Ok(()) + } +} diff --git a/data/human-readable/tests/enum_test.rs b/data/human-readable/tests/enum_test.rs new file mode 100644 index 0000000000..88b6840262 --- /dev/null +++ b/data/human-readable/tests/enum_test.rs @@ -0,0 +1,473 @@ +use multiversx_sc_codec_human_readable::{ + decode_human_readable_value, default_value_for_abi_type, encode_human_readable_value, + format::HumanReadableValue, AnyValue, EnumVariant, SingleValue, StructField, StructValue, +}; +use multiversx_sc_scenario::{ + meta::abi_json::deserialize_abi_from_json, + multiversx_sc::{abi::ContractAbi, codec::top_encode_to_vec_u8}, +}; + +const ABI_JSON: &str = r#"{ + "name": "Test", + "endpoints": [], + "events": [], + "esdtAttributes": [], + "hasCallback": false, + "types": { + "TwoU8s": { + "type": "struct", + "fields": [ + { + "name": "first", + "type": "u8" + }, + { + "name": "second", + "type": "u8" + } + ] + }, + "SimpleEnum": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0 + }, + { + "name": "Second", + "discriminant": 1 + } + ] + }, + "EnumWithStruct": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0 + }, + { + "name": "Second", + "discriminant": 1, + "fields": [ + { + "name": "first", + "type": "u8" + }, + { + "name": "second", + "type": "u8" + } + ] + } + ] + }, + "EnumWithTupleStruct": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0 + }, + { + "name": "Second", + "discriminant": 1, + "fields": [ + { + "name": "0", + "type": "TwoU8s" + } + ] + } + ] + }, + "EnumWithTupleValues": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0 + }, + { + "name": "Second", + "discriminant": 1, + "fields": [ + { + "name": "0", + "type": "u8" + }, + { + "name": "1", + "type": "u8" + } + ] + } + ] + }, + "EnumWithTupleValuesAndStruct": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0 + }, + { + "name": "Second", + "discriminant": 1, + "fields": [ + { + "name": "0", + "type": "u8" + }, + { + "name": "1", + "type": "u8" + }, + { + "name": "2", + "type": "TwoU8s" + } + ] + } + ] + }, + "EnumWithDiscriminantOnlyDefault": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0 + }, + { + "name": "Second", + "discriminant": 1 + } + ] + }, + "EnumWithStructDefault": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0, + "fields": [ + { + "name": "first", + "type": "u8" + }, + { + "name": "second", + "type": "u8" + } + ] + }, + { + "name": "Second", + "discriminant": 1 + } + ] + }, + "EnumWithTupleValuesDefault": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0, + "fields": [ + { + "name": "0", + "type": "u8" + }, + { + "name": "1", + "type": "u8" + } + ] + }, + { + "name": "Second", + "discriminant": 1 + } + ] + } + } +}"#; + +#[test] +fn serialize_enum_only_discriminant() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = r#""Second""#.parse::().unwrap(); + + let result = decode_human_readable_value(&value, "SimpleEnum", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, vec![1]); +} + +#[test] +fn deserialize_enum_only_discriminant() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Enum(Box::new(EnumVariant { + discriminant: 1, + value: AnyValue::None, + })); + + let result = encode_human_readable_value(&value, "SimpleEnum", &abi).unwrap(); + assert_eq!(result.to_string(), "\"Second\""); +} + +#[test] +fn serialize_enum_with_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = + r#"{ "Second": { "first": 1, "second": 2 } }"#.parse::().unwrap(); + + let result = decode_human_readable_value(&value, "EnumWithStruct", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!( + serialized, + vec![ + 1, // discriminant + 0, 0, 0, 1, 1, // first + 0, 0, 0, 1, 2 // second + ] + ); +} + +#[test] +fn deserialize_enum_with_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Enum(Box::new(EnumVariant { + discriminant: 1, + value: AnyValue::Struct(StructValue(vec![ + StructField { + name: "first".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "second".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + ])), + })); + + let result = encode_human_readable_value(&value, "EnumWithStruct", &abi).unwrap(); + assert_eq!(result.to_string(), r#"{"Second":{"first":1,"second":2}}"#); +} + +#[test] +fn serialize_enum_tuple_with_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = + r#"{ "Second": { "first": 1, "second": 2 } }"#.parse::().unwrap(); + + let result = decode_human_readable_value(&value, "EnumWithTupleStruct", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!( + serialized, + vec![ + 1, // discriminant + 0, 0, 0, 1, 1, // first + 0, 0, 0, 1, 2 // second + ] + ); +} + +#[test] +fn deserialize_enum_tuple_with_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Enum(Box::new(EnumVariant { + discriminant: 1, + value: AnyValue::Struct(StructValue(vec![ + StructField { + name: "first".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "second".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + ])), + })); + + let result = encode_human_readable_value(&value, "EnumWithTupleStruct", &abi).unwrap(); + assert_eq!(result.to_string(), r#"{"Second":{"first":1,"second":2}}"#); +} + +#[test] +fn serialize_enum_tuple_with_values() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = r#"{ "Second": [1, 2] }"#.parse::().unwrap(); + + let result = decode_human_readable_value(&value, "EnumWithTupleValues", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!( + serialized, + vec![ + 1, // discriminant + 0, 0, 0, 1, 1, // 0 + 0, 0, 0, 1, 2 // 1 + ] + ); +} + +#[test] +fn deserialize_enum_tuple_with_values() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Enum(Box::new(EnumVariant { + discriminant: 1, + value: AnyValue::Struct(StructValue(vec![ + StructField { + name: "0".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "1".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + ])), + })); + + let result = encode_human_readable_value(&value, "EnumWithTupleValues", &abi).unwrap(); + assert_eq!(result.to_string(), r#"{"Second":[1,2]}"#); +} + +#[test] +fn serialize_enum_tuple_with_values_and_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = r#"{ "Second": [1, 2, { "first": 1, "second": 2 }] }"# + .parse::() + .unwrap(); + + let result = decode_human_readable_value(&value, "EnumWithTupleValuesAndStruct", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!( + serialized, + vec![ + 1, // discriminant + 0, 0, 0, 1, 1, // 0 + 0, 0, 0, 1, 2, // 1 + 0, 0, 0, 1, 1, // 2.first + 0, 0, 0, 1, 2 // 2.second + ] + ); +} + +#[test] +fn deserialize_enum_tuple_with_values_and_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Enum(Box::new(EnumVariant { + discriminant: 1, + value: AnyValue::Struct(StructValue(vec![ + StructField { + name: "0".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "1".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + StructField { + name: "2".to_owned(), + value: AnyValue::Struct(StructValue(vec![ + StructField { + name: "first".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "second".to_owned(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + ])), + }, + ])), + })); + + let result = encode_human_readable_value(&value, "EnumWithTupleValuesAndStruct", &abi).unwrap(); + assert_eq!( + result.to_string(), + r#"{"Second":[1,2,{"first":1,"second":2}]}"# + ); +} + +#[test] +fn default_enum_discriminant_only() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = default_value_for_abi_type("EnumWithDiscriminantOnlyDefault", &abi).unwrap(); + + let AnyValue::Enum(variant) = value else { + panic!("Expected enum variant"); + }; + assert_eq!(variant.discriminant, 0); + match variant.value { + AnyValue::None => {}, + _ => panic!("Expected value none"), + }; +} + +#[test] +fn default_enum_with_struct() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = default_value_for_abi_type("EnumWithStructDefault", &abi).unwrap(); + + let AnyValue::Enum(variant) = value else { + panic!("Expected enum variant"); + }; + assert_eq!(variant.discriminant, 0); + let AnyValue::Struct(StructValue(fields)) = variant.value else { + panic!("Expected struct value"); + }; + assert_eq!(fields.len(), 2); + + assert_eq!(fields[0].name, "first"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(num)) = &fields[0].value else { + panic!("Expected unsigned number"); + }; + assert_eq!(*num, 0u8.into()); + + assert_eq!(fields[1].name, "second"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(num)) = &fields[1].value else { + panic!("Expected unsigned number"); + }; + assert_eq!(*num, 0u8.into()); +} + +#[test] +fn default_enum_with_tuple_values() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = default_value_for_abi_type("EnumWithTupleValuesDefault", &abi).unwrap(); + + let AnyValue::Enum(variant) = value else { + panic!("Expected enum variant"); + }; + assert_eq!(variant.discriminant, 0); + let AnyValue::Struct(StructValue(fields)) = variant.value else { + panic!("Expected struct value"); + }; + assert_eq!(fields.len(), 2); + + assert_eq!(fields[0].name, "0"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(num)) = &fields[0].value else { + panic!("Expected unsigned number"); + }; + assert_eq!(*num, 0u8.into()); + + assert_eq!(fields[1].name, "1"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(num)) = &fields[1].value else { + panic!("Expected unsigned number"); + }; + assert_eq!(*num, 0u8.into()); +} diff --git a/data/human-readable/tests/integrated_test.rs b/data/human-readable/tests/integrated_test.rs new file mode 100644 index 0000000000..49621b0033 --- /dev/null +++ b/data/human-readable/tests/integrated_test.rs @@ -0,0 +1,96 @@ +use multiversx_sc_codec_human_readable::{ + decode_human_readable_value, default_value_for_abi_type, encode_human_readable_value, +}; +use multiversx_sc_scenario::{ + meta::abi_json::deserialize_abi_from_json, + multiversx_sc::{abi::ContractAbi, codec::top_encode_to_vec_u8}, +}; + +const ABI_JSON: &str = r#"{ + "name": "Test", + "endpoints": [], + "events": [], + "esdtAttributes": [], + "hasCallback": false, + "types": { + "TwoU8s": { + "type": "struct", + "fields": [ + { + "name": "first", + "type": "u8" + }, + { + "name": "second", + "type": "u8" + } + ] + }, + "EnumWithTupleValuesAndStruct": { + "type": "enum", + "variants": [ + { + "name": "First", + "discriminant": 0, + "fields": [ + { + "name": "first", + "type": "utf-8 string" + }, + { + "name": "second", + "type": "TwoU8s" + } + ] + }, + { + "name": "Second", + "discriminant": 1, + "fields": [ + { + "name": "0", + "type": "u8" + }, + { + "name": "1", + "type": "u8" + }, + { + "name": "2", + "type": "TwoU8s" + } + ] + } + ] + }, + "Integrated": { + "type": "struct", + "fields": [ + { + "name": "first", + "type": "EnumWithTupleValuesAndStruct" + }, + { + "name": "second", + "type": "EnumWithTupleValuesAndStruct" + } + ] + } + } +}"#; + +#[test] +fn integrated_test() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let default_value = default_value_for_abi_type("Integrated", &abi).unwrap(); + let default_value_human = + encode_human_readable_value(&default_value, "Integrated", &abi).unwrap(); + let default_value_decoded = + decode_human_readable_value(&default_value_human, "Integrated", &abi).unwrap(); + + let default_bytes = top_encode_to_vec_u8(&default_value).unwrap(); + let processed_bytes = top_encode_to_vec_u8(&default_value_decoded).unwrap(); + + assert_eq!(default_bytes, processed_bytes); +} diff --git a/data/human-readable/tests/single_value_tests.rs b/data/human-readable/tests/single_value_tests.rs new file mode 100644 index 0000000000..1d4535d97e --- /dev/null +++ b/data/human-readable/tests/single_value_tests.rs @@ -0,0 +1,202 @@ +use multiversx_sc_codec_human_readable::{ + decode_human_readable_value, default_value_for_abi_type, encode_human_readable_value, + format::HumanReadableValue, AnyValue, SingleValue, +}; +use multiversx_sc_scenario::{ + bech32, + meta::abi_json::deserialize_abi_from_json, + multiversx_sc::{abi::ContractAbi, codec::top_encode_to_vec_u8}, +}; + +const EMPTY_ABI_JSON: &str = r#"{ + "name": "Test", + "endpoints": [], + "events": [], + "esdtAttributes": [], + "hasCallback": false, + "types": {} +}"#; + +const TEST_ADDRESS: &str = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"; + +#[test] +fn serialize_single_value_unsigned() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = "1234".parse::().unwrap(); + + let result = decode_human_readable_value(&value, "u32", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, 1234u16.to_be_bytes().to_vec()); // should take only 2 bytes (top encoded) +} + +#[test] +fn deserialize_single_value_unsigned() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = AnyValue::SingleValue(SingleValue::UnsignedNumber(1234u16.into())); + let result = encode_human_readable_value(&value, "u32", &abi).unwrap(); + + assert_eq!(result.to_string(), "1234"); +} + +#[test] +fn serialize_single_value_signed() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = "-1234".parse::().unwrap(); + + let result = decode_human_readable_value(&value, "i32", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, (-1234i16).to_be_bytes().to_vec()); // should take only 2 bytes (top encoded) +} + +#[test] +fn deserialize_single_value_signed() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = AnyValue::SingleValue(SingleValue::SignedNumber((-1234i16).into())); + let result = encode_human_readable_value(&value, "i32", &abi).unwrap(); + + assert_eq!(result.to_string(), "-1234"); +} + +#[test] +fn serialize_single_value_managed_buffer() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = "[12, 34]".parse::().unwrap(); + + let result = decode_human_readable_value(&value, "ManagedBuffer", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, vec![12, 34]); +} + +#[test] +fn deserialize_single_value_managed_buffer() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = AnyValue::SingleValue(SingleValue::Bytes(vec![0x1, 0x2, 0x3].into())); + let result = encode_human_readable_value(&value, "ManagedBuffer", &abi).unwrap(); + + assert_eq!(result.to_string(), "[1,2,3]"); +} + +#[test] +fn serialize_single_value_string() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = r#""hello""#.parse::().unwrap(); + + let result = decode_human_readable_value(&value, "utf-8 string", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, "hello".as_bytes().to_vec()); +} + +#[test] +fn deserialize_single_value_string() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = AnyValue::SingleValue(SingleValue::String("hello".to_owned())); + let result = encode_human_readable_value(&value, "utf-8 string", &abi).unwrap(); + + assert_eq!(result.to_string(), "\"hello\""); +} + +#[test] +fn serialize_single_value_bool() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = "true".parse::().unwrap(); + + let result = decode_human_readable_value(&value, "bool", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, vec![1]); +} + +#[test] +fn deserialize_single_value_bool() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = AnyValue::SingleValue(SingleValue::Bool(true)); + let result = encode_human_readable_value(&value, "bool", &abi).unwrap(); + + assert_eq!(result.to_string(), "true"); +} + +#[test] +fn serialize_single_value_address() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let value = format!("\"{}\"", TEST_ADDRESS) + .parse::() + .unwrap(); + + let result = decode_human_readable_value(&value, "Address", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + + let address = bech32::decode(TEST_ADDRESS); + + assert_eq!(serialized, address.into_boxed_bytes().into_vec()); +} + +#[test] +fn deserialize_single_value_address() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + let address = bech32::decode(TEST_ADDRESS); + + let value = AnyValue::SingleValue(SingleValue::Bytes(address.into_boxed_bytes().into_box())); + let result = encode_human_readable_value(&value, "Address", &abi).unwrap(); + + assert_eq!(result.to_string(), format!("\"{}\"", TEST_ADDRESS)); +} + +#[test] +fn default_single_values() { + let abi: ContractAbi = deserialize_abi_from_json(EMPTY_ABI_JSON).unwrap().into(); + + let AnyValue::SingleValue(SingleValue::UnsignedNumber(default_u32)) = + default_value_for_abi_type("u32", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(default_u32, 0u32.into()); + + let AnyValue::SingleValue(SingleValue::SignedNumber(default_i32)) = + default_value_for_abi_type("i32", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::SignedNumber") + }; + assert_eq!(default_i32, 0u32.into()); + + let AnyValue::SingleValue(SingleValue::Bytes(default_buffer)) = + default_value_for_abi_type("ManagedBuffer", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::Bytes") + }; + assert_eq!(default_buffer.len(), 0); + + let AnyValue::SingleValue(SingleValue::String(default_string)) = + default_value_for_abi_type("utf-8 string", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::String") + }; + assert_eq!(default_string, "".to_string()); + + let AnyValue::SingleValue(SingleValue::Bytes(default_address)) = + default_value_for_abi_type("Address", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::Bytes") + }; + assert_eq!(default_address.len(), 32); + for byte in default_address.iter() { + assert_eq!(*byte, 0); + } + + let AnyValue::SingleValue(SingleValue::Bool(default_bool)) = + default_value_for_abi_type("bool", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::Bool") + }; + assert!(!default_bool); +} diff --git a/data/human-readable/tests/struct_test.rs b/data/human-readable/tests/struct_test.rs new file mode 100644 index 0000000000..b4a6e8b1f3 --- /dev/null +++ b/data/human-readable/tests/struct_test.rs @@ -0,0 +1,204 @@ +use multiversx_sc_codec_human_readable::{ + decode_human_readable_value, default_value_for_abi_type, encode_human_readable_value, + format::HumanReadableValue, AnyValue, SingleValue, StructField, StructValue, +}; +use multiversx_sc_scenario::{ + meta::abi_json::deserialize_abi_from_json, + multiversx_sc::{abi::ContractAbi, codec::top_encode_to_vec_u8}, +}; + +const ABI_JSON: &str = r#"{ + "name": "Test", + "endpoints": [], + "events": [], + "esdtAttributes": [], + "hasCallback": false, + "types": { + "TwoU8s": { + "type": "struct", + "fields": [ + { + "name": "first", + "type": "u8" + }, + { + "name": "second", + "type": "u8" + } + ] + }, + "NestedStruct": { + "type": "struct", + "fields": [ + { + "name": "first", + "type": "u8" + }, + { + "name": "second", + "type": "TwoU8s" + } + ] + } + } +}"#; + +#[test] +fn serialize_struct_two_u8s() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = r#"{ "first": 1, "second": 2 }"#.parse::().unwrap(); + + let result = decode_human_readable_value(&value, "TwoU8s", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!( + serialized, + vec![ + 0, 0, 0, 1, 1, // first + 0, 0, 0, 1, 2 // second + ] + ); +} + +#[test] +fn deserialize_struct_two_u8s() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Struct(StructValue(vec![ + StructField { + name: "first".to_string(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "second".to_string(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + ])); + + let result = encode_human_readable_value(&value, "TwoU8s", &abi).unwrap(); + assert_eq!(result.to_string(), r#"{"first":1,"second":2}"#.to_string()); +} + +#[test] +fn default_struct_simple() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let AnyValue::Struct(struct_value) = default_value_for_abi_type("TwoU8s", &abi).unwrap() else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(struct_value.0.len(), 2); + + let first_field = struct_value.0.first().unwrap(); + assert_eq!(first_field.name, "first"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(first_value)) = &first_field.value else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(*first_value, 0u8.into()); + + let second_field = struct_value.0.get(1).unwrap(); + assert_eq!(second_field.name, "second"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(second_value)) = &second_field.value + else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(*second_value, 0u8.into()); +} + +#[test] +fn serialize_struct_nested() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = r#"{ + "first": 1, + "second": { + "first": 1, + "second": 2 + } + }"# + .parse::() + .unwrap(); + + let result = decode_human_readable_value(&value, "NestedStruct", &abi).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!( + serialized, + vec![ + 0, 0, 0, 1, 1, // first + 0, 0, 0, 1, 1, // second.first + 0, 0, 0, 1, 2 // second.second + ] + ); +} + +#[test] +fn deserialize_struct_nested() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let value = AnyValue::Struct(StructValue(vec![ + StructField { + name: "first".to_string(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "second".to_string(), + value: AnyValue::Struct(StructValue(vec![ + StructField { + name: "first".to_string(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(1u8.into())), + }, + StructField { + name: "second".to_string(), + value: AnyValue::SingleValue(SingleValue::UnsignedNumber(2u8.into())), + }, + ])), + }, + ])); + + let result = encode_human_readable_value(&value, "NestedStruct", &abi).unwrap(); + assert_eq!( + result.to_string(), + r#"{"first":1,"second":{"first":1,"second":2}}"#.to_string() + ); +} + +#[test] +fn default_struct_nested() { + let abi: ContractAbi = deserialize_abi_from_json(ABI_JSON).unwrap().into(); + + let AnyValue::Struct(struct_value) = default_value_for_abi_type("NestedStruct", &abi).unwrap() + else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(struct_value.0.len(), 2); + + let first_field = struct_value.0.first().unwrap(); + assert_eq!(first_field.name, "first"); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(first_value)) = &first_field.value else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(*first_value, 0u8.into()); + + let second_field = struct_value.0.get(1).unwrap(); + assert_eq!(second_field.name, "second"); + let AnyValue::Struct(nested_struct_value) = &second_field.value else { + panic!("Expected default value to be a SingleValue::Struct") + }; + + assert_eq!(nested_struct_value.0.len(), 2); + + let first_nested_field = nested_struct_value.0.first().unwrap(); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(first_nested_value)) = + &first_nested_field.value + else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(*first_nested_value, 0u8.into()); + + let second_nested_field = nested_struct_value.0.get(1).unwrap(); + let AnyValue::SingleValue(SingleValue::UnsignedNumber(second_nested_value)) = + &second_nested_field.value + else { + panic!("Expected default value to be a SingleValue::UnsignedNumber") + }; + assert_eq!(*second_nested_value, 0u8.into()); +} diff --git a/framework/base/src/abi.rs b/framework/base/src/abi.rs index c031a62166..8479e1921f 100644 --- a/framework/base/src/abi.rs +++ b/framework/base/src/abi.rs @@ -38,4 +38,11 @@ impl TypeNames { rust: alloc::string::String::new(), } } + + pub const fn from_abi(abi_name: alloc::string::String) -> Self { + TypeNames { + abi: abi_name, + rust: alloc::string::String::new(), + } + } } diff --git a/framework/base/src/abi/type_description.rs b/framework/base/src/abi/type_description.rs index f7e78ccef7..85ae3f1054 100644 --- a/framework/base/src/abi/type_description.rs +++ b/framework/base/src/abi/type_description.rs @@ -85,6 +85,21 @@ impl EnumVariantDescription { fields, } } + + pub fn is_empty_variant(&self) -> bool { + self.fields.is_empty() + } + + pub fn is_tuple_variant(&self) -> bool { + // all fields are numbers + for field in self.fields.iter() { + if field.name.parse::().is_err() { + return false; + } + } + + true + } } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/framework/base/src/abi/type_description_container.rs b/framework/base/src/abi/type_description_container.rs index 16dda2c8b9..6b178ca5f4 100644 --- a/framework/base/src/abi/type_description_container.rs +++ b/framework/base/src/abi/type_description_container.rs @@ -1,4 +1,5 @@ use super::*; +use alloc::borrow::ToOwned; use multiversx_sc_codec::Vec; pub trait TypeDescriptionContainer { @@ -20,6 +21,28 @@ pub trait TypeDescriptionContainer { #[derive(Clone, Default, Debug)] pub struct TypeDescriptionContainerImpl(pub Vec<(TypeNames, TypeDescription)>); +impl TypeDescriptionContainerImpl { + pub fn find(&self, abi_type_name: &str) -> Option<&TypeDescription> { + self.0 + .iter() + .find(|(existing_type_name, _)| existing_type_name.abi == abi_type_name) + .map(|(_, description)| description) + } + + pub fn find_or_default(&self, abi_type_name: &str) -> TypeDescription { + if let Some(type_description) = self.find(abi_type_name) { + type_description.clone() + } else { + TypeDescription { + docs: Vec::new(), + names: TypeNames::from_abi(abi_type_name.to_owned()), + contents: TypeContents::NotSpecified, + macro_attributes: Vec::new(), + } + } + } +} + impl TypeDescriptionContainer for TypeDescriptionContainerImpl { fn new() -> Self { TypeDescriptionContainerImpl(Vec::new()) diff --git a/framework/meta-lib/src/abi_json/build_info_abi_json.rs b/framework/meta-lib/src/abi_json/build_info_abi_json.rs index fb79f1cbb6..2b852b56a8 100644 --- a/framework/meta-lib/src/abi_json/build_info_abi_json.rs +++ b/framework/meta-lib/src/abi_json/build_info_abi_json.rs @@ -19,6 +19,21 @@ impl From<&BuildInfoAbi> for BuildInfoAbiJson { } } +impl From<&BuildInfoAbiJson> for BuildInfoAbi { + fn from(abi_json: &BuildInfoAbiJson) -> Self { + BuildInfoAbi { + contract_crate: ContractCrateBuildAbi::from(&abi_json.contract_crate), + framework: FrameworkBuildAbi::from(&abi_json.framework), + } + } +} + +impl From for BuildInfoAbi { + fn from(abi_json: BuildInfoAbiJson) -> Self { + BuildInfoAbi::from(&abi_json) + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RustcAbiJson { @@ -62,6 +77,27 @@ impl From<&ContractCrateBuildAbi> for ContractCrateBuildAbiJson { } } +impl From<&ContractCrateBuildAbiJson> for ContractCrateBuildAbi { + fn from(_abi: &ContractCrateBuildAbiJson) -> Self { + // TODO: @Laur the abi struct should probably just own the strings + let name: &'static str = ""; + let version: &'static str = ""; + let git_version: &'static str = ""; + + ContractCrateBuildAbi { + name, + version, + git_version, + } + } +} + +impl From for ContractCrateBuildAbi { + fn from(abi: ContractCrateBuildAbiJson) -> Self { + ContractCrateBuildAbi::from(&abi) + } +} + #[derive(Serialize, Deserialize)] pub struct FrameworkBuildAbiJson { pub name: String, @@ -76,3 +112,19 @@ impl From<&FrameworkBuildAbi> for FrameworkBuildAbiJson { } } } + +impl From<&FrameworkBuildAbiJson> for FrameworkBuildAbi { + fn from(_abi: &FrameworkBuildAbiJson) -> Self { + // TODO: @Laur the abi struct should probably just own the strings + let name: &'static str = ""; + let version: &'static str = ""; + + FrameworkBuildAbi { name, version } + } +} + +impl From for FrameworkBuildAbi { + fn from(abi: FrameworkBuildAbiJson) -> Self { + FrameworkBuildAbi::from(&abi) + } +} diff --git a/framework/meta-lib/src/abi_json/contract_abi_json.rs b/framework/meta-lib/src/abi_json/contract_abi_json.rs index 58c0865eee..80a6f1f7e8 100644 --- a/framework/meta-lib/src/abi_json/contract_abi_json.rs +++ b/framework/meta-lib/src/abi_json/contract_abi_json.rs @@ -45,6 +45,48 @@ pub struct ContractAbiJson { pub types: BTreeMap, } +impl From for ContractAbi { + fn from(abi_json: ContractAbiJson) -> Self { + ContractAbi { + build_info: abi_json + .build_info + .map(BuildInfoAbi::from) + .unwrap_or_default(), + docs: abi_json.docs, + name: abi_json.name, + constructors: abi_json + .constructor + .map(|c| vec![EndpointAbi::from(&c)]) + .unwrap_or_default(), + upgrade_constructors: abi_json + .upgrade_constructor + .map(|c| vec![EndpointAbi::from(&c)]) + .unwrap_or_default(), + endpoints: abi_json + .endpoints + .into_iter() + .map(EndpointAbi::from) + .collect(), + promise_callbacks: abi_json + .promises_callback_names + .into_iter() + .map(|name| EndpointAbi { + name, + ..Default::default() + }) + .collect(), + events: abi_json.events.into_iter().map(EventAbi::from).collect(), + esdt_attributes: abi_json + .esdt_attributes + .into_iter() + .map(EsdtAttributeAbi::from) + .collect(), + has_callback: abi_json.has_callback, + type_descriptions: convert_json_to_type_descriptions(abi_json.types), + } + } +} + impl From<&ContractAbi> for ContractAbiJson { fn from(abi: &ContractAbi) -> Self { ContractAbiJson { @@ -89,6 +131,23 @@ pub fn convert_type_descriptions_to_json( types } +pub fn convert_json_to_type_descriptions( + types: BTreeMap, +) -> TypeDescriptionContainerImpl { + let mut type_descriptions = TypeDescriptionContainerImpl::new(); + for (type_name, type_description) in types.into_iter() { + type_descriptions.insert( + TypeNames::from_abi(type_name), + TypeDescription::from(&type_description), + ); + } + type_descriptions +} + +pub fn empty_type_description_container() -> TypeDescriptionContainerImpl { + TypeDescriptionContainerImpl::new() +} + pub fn serialize_abi_to_json(abi_json: &ContractAbiJson) -> String { let buf = Vec::new(); let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); diff --git a/framework/meta-lib/src/abi_json/endpoint_abi_json.rs b/framework/meta-lib/src/abi_json/endpoint_abi_json.rs index b6bd041713..c5d2427fdc 100644 --- a/framework/meta-lib/src/abi_json/endpoint_abi_json.rs +++ b/framework/meta-lib/src/abi_json/endpoint_abi_json.rs @@ -25,6 +25,22 @@ impl From<&InputAbi> for InputAbiJson { } } +impl From<&InputAbiJson> for InputAbi { + fn from(abi: &InputAbiJson) -> Self { + InputAbi { + arg_name: abi.arg_name.to_string(), + type_names: TypeNames::from_abi(abi.type_name.clone()), + multi_arg: abi.multi_arg.unwrap_or(false), + } + } +} + +impl From for InputAbi { + fn from(abi: InputAbiJson) -> Self { + InputAbi::from(&abi) + } +} + #[derive(Serialize, Deserialize)] pub struct OutputAbiJson { #[serde(rename = "name")] @@ -49,6 +65,22 @@ impl From<&OutputAbi> for OutputAbiJson { } } +impl From<&OutputAbiJson> for OutputAbi { + fn from(abi: &OutputAbiJson) -> Self { + OutputAbi { + output_name: abi.output_name.clone(), + type_names: TypeNames::from_abi(abi.type_name.clone()), + multi_result: abi.multi_result.unwrap_or(false), + } + } +} + +impl From for OutputAbi { + fn from(abi: OutputAbiJson) -> Self { + OutputAbi::from(&abi) + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum EndpointMutabilityAbiJson { @@ -151,3 +183,65 @@ impl From<&EndpointAbi> for EndpointAbiJson { } } } + +impl From<&EndpointAbiJson> for EndpointAbi { + fn from(abi: &EndpointAbiJson) -> Self { + EndpointAbi { + docs: abi.docs.iter().map(|d| d.to_string()).collect(), + name: abi.name.to_string(), + only_owner: abi.only_owner.unwrap_or(false), + only_admin: abi.only_admin.unwrap_or(false), + mutability: match abi.mutability { + EndpointMutabilityAbiJson::Mutable => EndpointMutabilityAbi::Mutable, + EndpointMutabilityAbiJson::Readonly => EndpointMutabilityAbi::Readonly, + EndpointMutabilityAbiJson::Pure => EndpointMutabilityAbi::Pure, + }, + payable_in_tokens: abi + .payable_in_tokens + .iter() + .map(|d| d.to_string()) + .collect(), + inputs: abi.inputs.iter().map(InputAbi::from).collect(), + outputs: abi.outputs.iter().map(OutputAbi::from).collect(), + labels: abi.labels.clone(), + allow_multiple_var_args: abi.allow_multiple_var_args.unwrap_or(false), + rust_method_name: abi.name.clone(), + endpoint_type: EndpointTypeAbi::Endpoint, + } + } +} + +impl From for EndpointAbi { + fn from(abi: EndpointAbiJson) -> Self { + EndpointAbi::from(&abi) + } +} + +impl From<&ConstructorAbiJson> for EndpointAbi { + fn from(abi: &ConstructorAbiJson) -> Self { + EndpointAbi { + docs: abi.docs.iter().map(|d| d.to_string()).collect(), + name: "".to_string(), + only_owner: false, + only_admin: false, + mutability: EndpointMutabilityAbi::Mutable, + payable_in_tokens: abi + .payable_in_tokens + .iter() + .map(|d| d.to_string()) + .collect(), + inputs: abi.inputs.iter().map(InputAbi::from).collect(), + outputs: abi.outputs.iter().map(OutputAbi::from).collect(), + labels: vec![], + allow_multiple_var_args: false, + rust_method_name: "".to_string(), + endpoint_type: EndpointTypeAbi::Init, + } + } +} + +impl From for EndpointAbi { + fn from(abi: ConstructorAbiJson) -> Self { + EndpointAbi::from(&abi) + } +} diff --git a/framework/meta-lib/src/abi_json/esdt_attribute_abi_json.rs b/framework/meta-lib/src/abi_json/esdt_attribute_abi_json.rs index b6d3e1b278..2afcd85774 100644 --- a/framework/meta-lib/src/abi_json/esdt_attribute_abi_json.rs +++ b/framework/meta-lib/src/abi_json/esdt_attribute_abi_json.rs @@ -3,7 +3,10 @@ use std::collections::BTreeMap; use multiversx_sc::abi::EsdtAttributeAbi; use serde::{Deserialize, Serialize}; -use super::{convert_type_descriptions_to_json, EsdtAttributeJson, TypeDescriptionJson}; +use super::{ + convert_type_descriptions_to_json, empty_type_description_container, EsdtAttributeJson, + TypeDescriptionJson, +}; /// Represents an entire ESDT attribute ABI file. The type descriptions only show up here. #[derive(Serialize, Deserialize)] @@ -24,3 +27,13 @@ impl EsdtAttributeAbiJson { } } } + +impl From<&EsdtAttributeAbiJson> for EsdtAttributeAbi { + fn from(abi_json: &EsdtAttributeAbiJson) -> Self { + EsdtAttributeAbi { + ticker: abi_json.esdt_attribute.ticker.clone(), + ty: abi_json.esdt_attribute.ty.clone(), + type_descriptions: empty_type_description_container(), // TODO: @Laur should recursively call convert_json_to_type_descriptions + } + } +} diff --git a/framework/meta-lib/src/abi_json/esdt_attribute_json.rs b/framework/meta-lib/src/abi_json/esdt_attribute_json.rs index a908203643..5600d891b0 100644 --- a/framework/meta-lib/src/abi_json/esdt_attribute_json.rs +++ b/framework/meta-lib/src/abi_json/esdt_attribute_json.rs @@ -16,3 +16,19 @@ impl From<&EsdtAttributeAbi> for EsdtAttributeJson { } } } + +impl From<&EsdtAttributeJson> for EsdtAttributeAbi { + fn from(attr: &EsdtAttributeJson) -> Self { + EsdtAttributeAbi { + ticker: attr.ticker.to_owned(), + ty: attr.ty.clone(), + type_descriptions: Default::default(), + } + } +} + +impl From for EsdtAttributeAbi { + fn from(attr: EsdtAttributeJson) -> Self { + EsdtAttributeAbi::from(&attr) + } +} diff --git a/framework/meta-lib/src/abi_json/event_abi_json.rs b/framework/meta-lib/src/abi_json/event_abi_json.rs index ddde69b8ab..9fa6b96cd3 100644 --- a/framework/meta-lib/src/abi_json/event_abi_json.rs +++ b/framework/meta-lib/src/abi_json/event_abi_json.rs @@ -25,6 +25,22 @@ impl From<&EventInputAbi> for EventInputAbiJson { } } +impl From<&EventInputAbiJson> for EventInputAbi { + fn from(abi: &EventInputAbiJson) -> Self { + EventInputAbi { + arg_name: abi.arg_name.to_string(), + type_name: abi.type_name.clone(), + indexed: abi.indexed.unwrap_or(false), + } + } +} + +impl From for EventInputAbi { + fn from(abi: EventInputAbiJson) -> Self { + EventInputAbi::from(&abi) + } +} + #[derive(Serialize, Deserialize)] pub struct EventAbiJson { #[serde(default)] @@ -43,3 +59,19 @@ impl From<&EventAbi> for EventAbiJson { } } } + +impl From<&EventAbiJson> for EventAbi { + fn from(abi: &EventAbiJson) -> Self { + EventAbi { + docs: abi.docs.iter().map(|d| d.to_string()).collect(), + identifier: abi.identifier.to_string(), + inputs: abi.inputs.iter().map(EventInputAbi::from).collect(), + } + } +} + +impl From for EventAbi { + fn from(abi: EventAbiJson) -> Self { + EventAbi::from(&abi) + } +} diff --git a/framework/meta-lib/src/abi_json/type_abi_json.rs b/framework/meta-lib/src/abi_json/type_abi_json.rs index f26cc7086d..0bff582bbd 100644 --- a/framework/meta-lib/src/abi_json/type_abi_json.rs +++ b/framework/meta-lib/src/abi_json/type_abi_json.rs @@ -24,6 +24,34 @@ pub struct TypeDescriptionJson { pub fields: Vec, } +impl From<&TypeDescriptionJson> for TypeDescription { + fn from(abi: &TypeDescriptionJson) -> Self { + let content_type = match abi.content_type.as_str() { + TYPE_DESCRIPTION_JSON_TYPE_NOT_SPECIFIED => TypeContents::NotSpecified, + TYPE_DESCRIPTION_JSON_TYPE_ENUM => TypeContents::Enum( + abi.variants + .iter() + .map(EnumVariantDescriptionJson::to_enum_variant_description) + .collect(), + ), + TYPE_DESCRIPTION_JSON_TYPE_EXPLICIT_ENUM => TypeContents::ExplicitEnum(vec![]), // TODO: @Laur implement explicit enum + TYPE_DESCRIPTION_JSON_TYPE_STRUCT => TypeContents::Struct( + abi.fields + .iter() + .map(StructFieldDescriptionJson::to_struct_field_description) + .collect(), + ), + _ => TypeContents::NotSpecified, + }; + TypeDescription { + docs: abi.docs.iter().map(|line| line.to_string()).collect(), + names: TypeNames::new(), + contents: content_type, + macro_attributes: Vec::new(), + } + } +} + impl From<&TypeDescription> for TypeDescriptionJson { fn from(abi: &TypeDescription) -> Self { let content_type = match &abi.contents { diff --git a/framework/scenario/src/bech32.rs b/framework/scenario/src/bech32.rs index ef934cf225..a744fc5d35 100644 --- a/framework/scenario/src/bech32.rs +++ b/framework/scenario/src/bech32.rs @@ -1,14 +1,30 @@ +use std::{error::Error, fmt::Display}; + use bech32::{Bech32, Hrp}; use multiversx_sc::types::heap::Address; -pub fn decode(bech32_address: &str) -> Address { - let (_hrp, dest_address_bytes) = bech32::decode(bech32_address) - .unwrap_or_else(|err| panic!("bech32 decode error for {bech32_address}: {err}")); +#[derive(Debug)] +pub struct InvalidAddressLengthError; + +impl Display for InvalidAddressLengthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + "Invalid address length after decoding".fmt(f) + } +} + +impl Error for InvalidAddressLengthError {} + +pub fn try_decode(bech32_address: &str) -> Result> { + let (_hrp, dest_address_bytes) = bech32::decode(bech32_address)?; if dest_address_bytes.len() != 32 { - panic!("Invalid address length after decoding") + return Err(Box::new(InvalidAddressLengthError)); } - Address::from_slice(&dest_address_bytes) + Ok(Address::from_slice(&dest_address_bytes)) +} + +pub fn decode(bech32_address: &str) -> Address { + try_decode(bech32_address).expect("bech32 Address decode failed") } pub fn encode(address: &Address) -> String { diff --git a/framework/snippets/Cargo.toml b/framework/snippets/Cargo.toml index 43ca798a69..45cf5f39fc 100644 --- a/framework/snippets/Cargo.toml +++ b/framework/snippets/Cargo.toml @@ -14,7 +14,6 @@ keywords = ["multiversx", "blockchain", "contract", "snippets"] categories = ["cryptography::cryptocurrencies"] [dependencies] -tokio = { version = "1.24", features = ["full"] } hex = "0.4" base64 = "0.22" log = "0.4.17" diff --git a/framework/snippets/src/imports.rs b/framework/snippets/src/imports.rs index a22c969887..e7fbdf2436 100644 --- a/framework/snippets/src/imports.rs +++ b/framework/snippets/src/imports.rs @@ -7,4 +7,4 @@ pub use crate::{ pub use multiversx_sdk::{data::keystore::InsertPassword, wallet::Wallet}; pub use env_logger; -pub use tokio; +pub use multiversx_sdk::tokio; diff --git a/framework/snippets/src/interactor.rs b/framework/snippets/src/interactor.rs index bb38873afe..76d2b37a35 100644 --- a/framework/snippets/src/interactor.rs +++ b/framework/snippets/src/interactor.rs @@ -61,7 +61,7 @@ impl Interactor { pub async fn sleep(&mut self, duration: Duration) { self.waiting_time_ms += duration.as_millis() as u64; - tokio::time::sleep(duration).await; + crate::tokio::time::sleep(duration).await; } pub async fn with_tracer>(mut self, path: P) -> Self { diff --git a/framework/snippets/src/lib.rs b/framework/snippets/src/lib.rs index 7d3b52c696..61d4ad485e 100644 --- a/framework/snippets/src/lib.rs +++ b/framework/snippets/src/lib.rs @@ -19,7 +19,7 @@ pub use multi::*; pub use multiversx_sc_scenario::{self, multiversx_sc}; pub use multiversx_sdk as erdrs; // TODO: remove pub use multiversx_sdk as sdk; -pub use tokio; +pub use multiversx_sdk::tokio; /// Imports normally needed in interactors, grouped together. pub mod imports; diff --git a/sdk/core/src/lib.rs b/sdk/core/src/lib.rs index 3a1db99fd7..c0e0379587 100644 --- a/sdk/core/src/lib.rs +++ b/sdk/core/src/lib.rs @@ -3,3 +3,6 @@ pub mod data; pub mod gateway; pub mod utils; pub mod wallet; + +/// Re-exported for convenience. +pub use tokio;