diff --git a/Cargo.toml b/Cargo.toml index d40d05c8f8..e80dfdf4ed 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 2826c7d3d2..68d86667a1 100644 --- a/data/codec/src/single/top_en.rs +++ b/data/codec/src/single/top_en.rs @@ -4,7 +4,7 @@ use crate::{ }; use alloc::vec::Vec; -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..4e66f93925 --- /dev/null +++ b/data/human-readable/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "multiversx-sc-codec-human-readable" +version = "0.1.0" +edition = "2018" +publish = false + +authors = ["Andrei Marinica ", "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] + +[dependencies.multiversx-sc-scenario] +version = "=0.41.0" +path = "../../framework/scenario" + +[dependencies.multiversx-sc-meta] +version = "=0.41.0" +path = "../../framework/meta" diff --git a/data/human-readable/src/interpret.rs b/data/human-readable/src/interpret.rs new file mode 100644 index 0000000000..c89d0abf6e --- /dev/null +++ b/data/human-readable/src/interpret.rs @@ -0,0 +1,56 @@ +use std::{error::Error, fmt::Display}; + +use crate::multiversx_sc::abi::{TypeContents, TypeDescription}; +use multiversx_sc_meta::abi_json::ContractAbiJson; +use multiversx_sc_scenario::num_bigint::BigUint; + +use crate::{AnyValue, SingleValue::UnsignedNumber}; + +pub fn interpret_value_according_to_abi( + input: &str, + type_name: &str, + contract_abi: &ContractAbiJson, // TODO: will need to convert to high-level ContractAbi first, this is just a prototype +) -> Result> { + let type_description = if let Some(type_description_json) = contract_abi.types.get(type_name) { + type_description_json.to_type_description(type_name) + } else { + TypeDescription { + docs: &[], + name: type_name.to_string(), + contents: TypeContents::NotSpecified, + } + }; + interpret_any_value(input, &type_description) +} + +pub fn interpret_any_value( + input: &str, + type_description: &TypeDescription, +) -> Result> { + match &type_description.contents { + TypeContents::NotSpecified => interpret_single_value(input, type_description.name.as_str()), + TypeContents::Enum(_) => todo!(), + TypeContents::Struct(_) => todo!(), + } +} + +fn interpret_single_value(input: &str, type_name: &str) -> Result> { + match type_name { + "BigUint" | "u64" | "u32" | "u16" | "usize" | "u8" => { + let value = input.parse::()?; + Ok(AnyValue::SingleValue(UnsignedNumber(value))) + }, + _ => Err(Box::new(InterpretError("unknown type"))), + } +} + +#[derive(Debug)] +pub struct InterpretError(&'static str); + +impl Display for InterpretError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Error for InterpretError {} diff --git a/data/human-readable/src/lib.rs b/data/human-readable/src/lib.rs new file mode 100644 index 0000000000..613e656b7e --- /dev/null +++ b/data/human-readable/src/lib.rs @@ -0,0 +1,6 @@ +mod interpret; +mod value; + +use multiversx_sc_scenario::multiversx_sc; +pub use interpret::*; +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..97bf8ec17e --- /dev/null +++ b/data/human-readable/src/value/any_value.rs @@ -0,0 +1,39 @@ +use multiversx_sc_scenario::multiversx_sc::codec::{ + EncodeErrorHandler, NestedEncode, NestedEncodeOutput, TopEncode, TopEncodeOutput, +}; + +use crate::{EnumVariant, SingleValue, StructValue}; + +pub enum AnyValue { + 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::SingleValue(sv) => sv.dep_encode_or_handle_err(dest, h), + AnyValue::Struct(s) => s.dep_encode_or_handle_err(dest, h), + AnyValue::Enum(_) => todo!(), + } + } +} + +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::SingleValue(sv) => sv.top_encode_or_handle_err(output, h), + AnyValue::Struct(s) => s.top_encode_or_handle_err(output, h), + AnyValue::Enum(_) => todo!(), + } + } +} 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..864b889e8b --- /dev/null +++ b/data/human-readable/src/value/enum_value.rs @@ -0,0 +1,6 @@ +use crate::AnyValue; + +pub struct EnumVariant { + pub discriminant: usize, + pub value: AnyValue, +} 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..fbb972daa8 --- /dev/null +++ b/data/human-readable/src/value/single_value.rs @@ -0,0 +1,41 @@ +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]>), + 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::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::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..d90d5a1b85 --- /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(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/single_value_basic_test.rs b/data/human-readable/tests/single_value_basic_test.rs new file mode 100644 index 0000000000..8f1f85ee84 --- /dev/null +++ b/data/human-readable/tests/single_value_basic_test.rs @@ -0,0 +1,36 @@ +use multiversx_sc_codec_human_readable::interpret_value_according_to_abi; +use multiversx_sc_meta::abi_json::{deserialize_abi_from_json, ContractAbiJson}; +use multiversx_sc_scenario::multiversx_sc::codec::top_encode_to_vec_u8; + +const TEST_ABI_JSON: &str = r#"{ + "buildInfo": { + "rustc": { + "version": "1.62.0-nightly", + "commitHash": "306ba8357fb36212b7d30efb9eb9e41659ac1445", + "commitDate": "2022-04-05", + "channel": "Nightly", + "short": "rustc 1.62.0-nightly (306ba8357 2022-04-05)" + }, + "contractCrate": { + "name": "adder", + "version": "0.0.0" + }, + "framework": { + "name": "elrond-wasm", + "version": "0.30.0" + } + }, + "name": "Test", + "endpoints": [], + "hasCallback": false, + "types": {} +}"#; + +#[test] +fn test_display_unsigned() { + let abi_json: ContractAbiJson = deserialize_abi_from_json(TEST_ABI_JSON).unwrap(); + + let result = interpret_value_according_to_abi("123", "BigUint", &abi_json).unwrap(); + let serialized = top_encode_to_vec_u8(&result).unwrap(); + assert_eq!(serialized, vec![123]); +} diff --git a/framework/base/src/abi/type_description.rs b/framework/base/src/abi/type_description.rs index 79b170f649..615c0ec928 100644 --- a/framework/base/src/abi/type_description.rs +++ b/framework/base/src/abi/type_description.rs @@ -1,4 +1,7 @@ -use alloc::{string::String, vec::Vec}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; #[derive(Clone, Debug)] pub struct TypeDescription { @@ -43,6 +46,16 @@ pub struct EnumVariantDescription { #[derive(Clone, Debug)] pub struct StructFieldDescription { pub docs: &'static [&'static str], - pub name: &'static str, + pub name: String, pub field_type: String, } + +impl StructFieldDescription { + pub fn new(docs: &'static [&'static str], name: &str, field_type: String) -> Self { + Self { + docs, + name: name.to_string(), + field_type, + } + } +} diff --git a/framework/derive/src/type_abi_derive.rs b/framework/derive/src/type_abi_derive.rs index a7a17d4c1f..f927001525 100644 --- a/framework/derive/src/type_abi_derive.rs +++ b/framework/derive/src/type_abi_derive.rs @@ -11,11 +11,11 @@ fn field_snippet(index: usize, field: &syn::Field) -> proc_macro2::TokenStream { }; let field_ty = &field.ty; quote! { - field_descriptions.push(multiversx_sc::abi::StructFieldDescription { - docs: &[ #(#field_docs),* ], - name: #field_name_str, - field_type: <#field_ty>::type_name(), - }); + field_descriptions.push(multiversx_sc::abi::StructFieldDescription::new( + &[ #(#field_docs),* ], + #field_name_str, + <#field_ty>::type_name(), + )); <#field_ty>::provide_type_descriptions(accumulator); } } diff --git a/framework/meta/src/abi_json/build_info_abi_json.rs b/framework/meta/src/abi_json/build_info_abi_json.rs index 92643713bb..fb79f1cbb6 100644 --- a/framework/meta/src/abi_json/build_info_abi_json.rs +++ b/framework/meta/src/abi_json/build_info_abi_json.rs @@ -47,6 +47,7 @@ impl RustcAbiJson { pub struct ContractCrateBuildAbiJson { pub name: String, pub version: String, + #[serde(default)] #[serde(skip_serializing_if = "String::is_empty")] pub git_version: String, } diff --git a/framework/meta/src/abi_json/contract_abi_json.rs b/framework/meta/src/abi_json/contract_abi_json.rs index 88b813cf78..5d17015422 100644 --- a/framework/meta/src/abi_json/contract_abi_json.rs +++ b/framework/meta/src/abi_json/contract_abi_json.rs @@ -9,12 +9,16 @@ pub struct ContractAbiJson { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub build_info: Option, + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, pub name: String, + #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub constructor: Option, pub endpoints: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] pub events: Vec, pub has_callback: bool, pub types: BTreeMap, @@ -53,3 +57,7 @@ pub fn serialize_abi_to_json(abi_json: &ContractAbiJson) -> String { serialized.push('\n'); serialized } + +pub fn deserialize_abi_from_json(input: &str) -> Result { + serde_json::from_str(input).map_err(|err| err.to_string()) +} diff --git a/framework/meta/src/abi_json/endpoint_abi_json.rs b/framework/meta/src/abi_json/endpoint_abi_json.rs index 73a72e7954..dbb8cd9edd 100644 --- a/framework/meta/src/abi_json/endpoint_abi_json.rs +++ b/framework/meta/src/abi_json/endpoint_abi_json.rs @@ -8,6 +8,7 @@ pub struct InputAbiJson { #[serde(rename = "type")] pub type_name: String, /// Bool that is only serialized when true + #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub multi_arg: Option, } @@ -25,11 +26,13 @@ impl From<&InputAbi> for InputAbiJson { #[derive(Serialize, Deserialize)] pub struct OutputAbiJson { #[serde(rename = "name")] + #[serde(default)] #[serde(skip_serializing_if = "String::is_empty")] pub output_name: String, #[serde(rename = "type")] pub type_name: String, /// Bool that is only serialized when true + #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub multi_result: Option, } @@ -55,9 +58,11 @@ pub enum EndpointMutabilityAbiJson { /// Same as EndpointAbiJson but ignores the name #[derive(Serialize, Deserialize)] pub struct ConstructorAbiJson { + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, #[serde(rename = "payableInTokens")] + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub payable_in_tokens: Vec, pub inputs: Vec, @@ -81,17 +86,21 @@ impl From<&EndpointAbi> for ConstructorAbiJson { #[derive(Serialize, Deserialize)] pub struct EndpointAbiJson { + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, pub name: String, #[serde(rename = "onlyOwner")] + #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub only_owner: Option, #[serde(rename = "onlyAdmin")] + #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub only_admin: Option, pub mutability: EndpointMutabilityAbiJson, #[serde(rename = "payableInTokens")] + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub payable_in_tokens: Vec, pub inputs: Vec, diff --git a/framework/meta/src/abi_json/event_abi_json.rs b/framework/meta/src/abi_json/event_abi_json.rs index 562c337320..0b9dd6051d 100644 --- a/framework/meta/src/abi_json/event_abi_json.rs +++ b/framework/meta/src/abi_json/event_abi_json.rs @@ -8,6 +8,7 @@ pub struct EventInputAbiJson { #[serde(rename = "type")] pub type_name: String, /// Bool that is only serialized when true + #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub indexed: Option, } @@ -24,6 +25,7 @@ impl From<&EventInputAbi> for EventInputAbiJson { #[derive(Serialize, Deserialize)] pub struct EventAbiJson { + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, pub identifier: String, diff --git a/framework/meta/src/abi_json/type_abi_json.rs b/framework/meta/src/abi_json/type_abi_json.rs index a07343c214..ddb1383ae6 100644 --- a/framework/meta/src/abi_json/type_abi_json.rs +++ b/framework/meta/src/abi_json/type_abi_json.rs @@ -1,17 +1,24 @@ use multiversx_sc::abi::*; use serde::{Deserialize, Serialize}; +pub const TYPE_DESCRIPTION_JSON_TYPE_NOT_SPECIFIED: &str = "not-specified"; +pub const TYPE_DESCRIPTION_JSON_TYPE_ENUM: &str = "enum"; +pub const TYPE_DESCRIPTION_JSON_TYPE_STRUCT: &str = "struct"; + #[derive(Serialize, Deserialize)] pub struct TypeDescriptionJson { #[serde(rename = "type")] pub content_type: String, + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub variants: Vec, + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub fields: Vec, } @@ -19,9 +26,9 @@ pub struct TypeDescriptionJson { impl From<&TypeDescription> for TypeDescriptionJson { fn from(abi: &TypeDescription) -> Self { let content_type = match &abi.contents { - TypeContents::NotSpecified => "not_specified", - TypeContents::Enum(_) => "enum", - TypeContents::Struct(_) => "struct", + TypeContents::NotSpecified => TYPE_DESCRIPTION_JSON_TYPE_NOT_SPECIFIED, + TypeContents::Enum(_) => TYPE_DESCRIPTION_JSON_TYPE_ENUM, + TypeContents::Struct(_) => TYPE_DESCRIPTION_JSON_TYPE_STRUCT, }; let mut type_desc_json = TypeDescriptionJson { content_type: content_type.to_string(), @@ -51,8 +58,33 @@ impl From<&TypeDescription> for TypeDescriptionJson { } } +impl TypeDescriptionJson { + pub fn to_type_description(&self, name: &str) -> TypeDescription { + TypeDescription { + docs: &[], + name: name.to_string(), + contents: match self.content_type.as_str() { + TYPE_DESCRIPTION_JSON_TYPE_STRUCT => TypeContents::Struct( + self.fields + .iter() + .map(StructFieldDescriptionJson::to_struct_field_description) + .collect(), + ), + TYPE_DESCRIPTION_JSON_TYPE_ENUM => TypeContents::Enum( + self.variants + .iter() + .map(EnumVariantDescriptionJson::to_enum_variant_description) + .collect(), + ), + _ => TypeContents::NotSpecified, + }, + } + } +} + #[derive(Serialize, Deserialize)] pub struct StructFieldDescriptionJson { + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, pub name: String, @@ -70,8 +102,19 @@ impl From<&StructFieldDescription> for StructFieldDescriptionJson { } } +impl StructFieldDescriptionJson { + pub fn to_struct_field_description(&self) -> StructFieldDescription { + StructFieldDescription { + docs: &[], + name: self.name.clone(), + field_type: self.field_type.clone(), + } + } +} + #[derive(Serialize, Deserialize)] pub struct EnumVariantDescriptionJson { + #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub docs: Vec, pub name: String, @@ -94,3 +137,18 @@ impl From<&EnumVariantDescription> for EnumVariantDescriptionJson { } } } + +impl EnumVariantDescriptionJson { + pub fn to_enum_variant_description(&self) -> EnumVariantDescription { + EnumVariantDescription { + docs: &[], + discriminant: self.discriminant, + name: "", + fields: self + .fields + .iter() + .map(StructFieldDescriptionJson::to_struct_field_description) + .collect(), + } + } +}