diff --git a/framework/base/Cargo.toml b/framework/base/Cargo.toml index 39e79ac759..2e81f12402 100644 --- a/framework/base/Cargo.toml +++ b/framework/base/Cargo.toml @@ -4,7 +4,10 @@ version = "0.58.0" edition = "2021" rust-version = "1.83" -authors = ["Andrei Marinica ", "MultiversX "] +authors = [ + "Andrei Marinica ", + "MultiversX ", +] license = "GPL-3.0-only" readme = "README.md" repository = "https://github.com/multiversx/mx-sdk-rs" @@ -12,7 +15,12 @@ homepage = "https://multiversx.com/" documentation = "https://docs.multiversx.com/" description = "MultiversX smart contract API" keywords = ["multiversx", "wasm", "webassembly", "blockchain", "contract"] -categories = ["no-std", "wasm", "cryptography::cryptocurrencies", "development-tools"] +categories = [ + "no-std", + "wasm", + "cryptography::cryptocurrencies", + "development-tools", +] [package.metadata.docs.rs] all-features = true diff --git a/framework/base/src/abi/build_info_abi.rs b/framework/base/src/abi/build_info_abi.rs index 5004c8e673..5373a800f1 100644 --- a/framework/base/src/abi/build_info_abi.rs +++ b/framework/base/src/abi/build_info_abi.rs @@ -1,13 +1,13 @@ /// Designed to hold metadata of the contract crate. /// Must be instanced inside the smart contract crate to work, /// that is why a `create` associated method would not make sense here. -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct BuildInfoAbi { pub contract_crate: ContractCrateBuildAbi, pub framework: FrameworkBuildAbi, } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct ContractCrateBuildAbi { pub name: &'static str, pub version: &'static str, @@ -16,7 +16,7 @@ pub struct ContractCrateBuildAbi { /// Gives the multiversx-sc metadata. /// Should be instanced via the `create` associated function. -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct FrameworkBuildAbi { pub name: &'static str, pub version: &'static str, diff --git a/framework/base/src/abi/endpoint_abi.rs b/framework/base/src/abi/endpoint_abi.rs index 8e96554d84..5b84bd1bc4 100644 --- a/framework/base/src/abi/endpoint_abi.rs +++ b/framework/base/src/abi/endpoint_abi.rs @@ -5,14 +5,14 @@ use alloc::{ vec::Vec, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct InputAbi { pub arg_name: String, pub type_names: TypeNames, pub multi_arg: bool, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct OutputAbi { pub output_name: String, pub type_names: TypeNames, @@ -21,7 +21,7 @@ pub struct OutputAbi { pub type OutputAbis = Vec; -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub enum EndpointMutabilityAbi { #[default] Mutable, @@ -29,7 +29,7 @@ pub enum EndpointMutabilityAbi { Pure, } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub enum EndpointTypeAbi { #[default] Init, @@ -38,7 +38,7 @@ pub enum EndpointTypeAbi { PromisesCallback, } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct EndpointAbi { pub docs: Vec, pub name: String, diff --git a/framework/base/src/abi/esdt_attribute_abi.rs b/framework/base/src/abi/esdt_attribute_abi.rs index ee37466064..66c8ecd8da 100644 --- a/framework/base/src/abi/esdt_attribute_abi.rs +++ b/framework/base/src/abi/esdt_attribute_abi.rs @@ -2,7 +2,7 @@ use alloc::string::{String, ToString}; use super::{TypeAbi, TypeDescriptionContainerImpl, TypeName}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct EsdtAttributeAbi { pub ticker: String, pub ty: TypeName, diff --git a/framework/base/src/abi/event_abi.rs b/framework/base/src/abi/event_abi.rs index 416e967b75..273457266b 100644 --- a/framework/base/src/abi/event_abi.rs +++ b/framework/base/src/abi/event_abi.rs @@ -4,14 +4,14 @@ use alloc::{ vec::Vec, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct EventInputAbi { pub arg_name: String, pub type_name: TypeName, pub indexed: bool, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct EventAbi { pub docs: Vec, pub identifier: String, diff --git a/framework/base/src/abi/type_description.rs b/framework/base/src/abi/type_description.rs index f7e78ccef7..65d9922506 100644 --- a/framework/base/src/abi/type_description.rs +++ b/framework/base/src/abi/type_description.rs @@ -5,7 +5,7 @@ use alloc::{ use super::TypeNames; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct TypeDescription { pub docs: Vec, pub names: TypeNames, @@ -46,7 +46,7 @@ impl TypeDescription { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum TypeContents { NotSpecified, Enum(Vec), @@ -110,7 +110,7 @@ impl StructFieldDescription { /// This makes it easier for humans to read readable in the transaction output. /// /// It cannot have data fields, only simple enums allowed. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ExplicitEnumVariantDescription { pub docs: Vec, pub name: String, diff --git a/framework/base/src/abi/type_description_container.rs b/framework/base/src/abi/type_description_container.rs index 627bf4c437..de482334ec 100644 --- a/framework/base/src/abi/type_description_container.rs +++ b/framework/base/src/abi/type_description_container.rs @@ -17,7 +17,7 @@ pub trait TypeDescriptionContainer { fn insert_all(&mut self, other: &Self); } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct TypeDescriptionContainerImpl(pub Vec<(TypeNames, TypeDescription)>); impl TypeDescriptionContainer for TypeDescriptionContainerImpl { diff --git a/framework/meta-lib/src/contract/generate_snippets.rs b/framework/meta-lib/src/contract/generate_snippets.rs index 9d7fc1ebeb..90262d31cf 100644 --- a/framework/meta-lib/src/contract/generate_snippets.rs +++ b/framework/meta-lib/src/contract/generate_snippets.rs @@ -1,3 +1,4 @@ +pub mod snippet_abi_check; pub mod snippet_crate_gen; pub mod snippet_gen_common; pub mod snippet_gen_main; diff --git a/framework/meta-lib/src/contract/generate_snippets/snippet_abi_check.rs b/framework/meta-lib/src/contract/generate_snippets/snippet_abi_check.rs new file mode 100644 index 0000000000..f0bdb92071 --- /dev/null +++ b/framework/meta-lib/src/contract/generate_snippets/snippet_abi_check.rs @@ -0,0 +1,515 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use multiversx_sc::abi::{ContractAbi, EndpointAbi, InputAbi, OutputAbi}; +use serde::de::DeserializeOwned; +use serde::{de, Deserialize, Deserializer}; + +use crate::abi_json::{serialize_abi_to_json, ContractAbiJson}; + +use super::snippet_crate_gen::LIB_SOURCE_FILE_NAME; +use super::snippet_type_map::map_abi_type_to_rust_type; +use crate::contract::generate_snippets::snippet_sc_functions_gen::DEFAULT_GAS; + +const PREV_ABI_NAME: &str = "prev-abi.json"; + +#[derive(PartialEq, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ShortContractAbi { + #[serde(default)] + pub name: String, + #[serde(default, deserialize_with = "deserialize_single_or_vec")] + pub constructor: Vec, + #[serde( + default, + rename = "upgradeConstructor", + deserialize_with = "deserialize_single_or_vec" + )] + pub upgrade_constructor: Vec, + #[serde(default)] + pub endpoints: Vec, + #[serde(skip_deserializing)] + pub deleted_endpoints: Vec, +} + +#[derive(PartialEq, Deserialize, Clone, Debug)] +pub(crate) struct ShortEndpointAbi { + #[serde(default)] + pub name: String, + #[serde(default)] + pub mutability: String, + #[serde(default, skip_deserializing)] + pub rust_method_name: String, + #[serde(default)] + pub payable_in_tokens: Vec, + #[serde(default)] + pub inputs: Vec, + #[serde(default)] + pub outputs: Vec, + #[serde(default)] + pub allow_multiple_var_args: bool, +} + +#[derive(PartialEq, Deserialize, Clone, Debug)] +pub(crate) struct ShortInputAbi { + #[serde(default)] + pub name: String, + #[serde(rename = "type")] + pub type_name: String, + #[serde(default)] + pub multi_arg: bool, +} + +#[derive(PartialEq, Deserialize, Clone, Debug)] +pub(crate) struct ShortOutputAbi { + #[serde(rename = "type")] + pub type_name: String, + #[serde(default)] + pub multi_result: bool, +} + +fn deserialize_single_or_vec<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: DeserializeOwned, +{ + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::Array(vec) => { + serde_json::from_value(serde_json::Value::Array(vec)).map_err(de::Error::custom) + }, + _ => Ok(serde_json::from_value(value.clone()) + .map(|single| vec![single]) + .unwrap_or_else(|_| panic!("error at {value:?}"))), + } +} + +impl From for ShortEndpointAbi { + fn from(value: EndpointAbi) -> Self { + Self { + name: value.name, + mutability: format!("{:?}", value.mutability).to_lowercase(), + rust_method_name: value.rust_method_name, + payable_in_tokens: value.payable_in_tokens, + inputs: value.inputs.into_iter().map(ShortInputAbi::from).collect(), + outputs: value + .outputs + .into_iter() + .map(ShortOutputAbi::from) + .collect(), + allow_multiple_var_args: value.allow_multiple_var_args, + } + } +} + +impl From for ShortInputAbi { + fn from(value: InputAbi) -> Self { + Self { + name: value.arg_name, + type_name: value.type_names.abi, + multi_arg: value.multi_arg, + } + } +} + +impl From for ShortOutputAbi { + fn from(value: OutputAbi) -> Self { + Self { + type_name: value.type_names.abi, + multi_result: value.multi_result, + } + } +} + +impl From for ShortContractAbi { + fn from(value: ContractAbi) -> Self { + Self { + name: value.name, + constructor: value + .constructors + .into_iter() + .map(ShortEndpointAbi::from) + .collect(), + upgrade_constructor: value + .upgrade_constructors + .into_iter() + .map(ShortEndpointAbi::from) + .collect(), + endpoints: value + .endpoints + .into_iter() + .map(ShortEndpointAbi::from) + .collect(), + deleted_endpoints: vec![], + } + } +} + +pub(crate) fn check_abi_differences( + current_contract_abi: &ShortContractAbi, + snippets_dir: &Path, + overwrite: bool, +) -> ShortContractAbi { + if !overwrite { + let prev_abi_path = snippets_dir.join(PREV_ABI_NAME); + + if let Ok(prev_abi_content) = std::fs::read_to_string(&prev_abi_path) { + if let Ok(mut prev_abi) = serde_json::from_str::(&prev_abi_content) { + if prev_abi.constructor[0].name.is_empty() { + prev_abi.constructor[0].name = "init".to_string(); + prev_abi.constructor[0].mutability = "mutable".to_string(); + prev_abi.constructor[0].rust_method_name = "init".to_string(); + } + + if prev_abi.upgrade_constructor[0].name.is_empty() { + prev_abi.upgrade_constructor[0].name = "upgrade".to_string(); + prev_abi.upgrade_constructor[0].mutability = "mutable".to_string(); + prev_abi.upgrade_constructor[0].rust_method_name = "upgrade".to_string(); + } + + println!("prev ABI constructor {:?}", prev_abi.constructor); + println!( + "prev ABI upgrade constructor {:?}", + prev_abi.upgrade_constructor + ); + println!("prev ABI endpdoints {:?}", prev_abi.endpoints); + println!("**********"); + println!( + "CURRENT ABI constructor {:?}", + current_contract_abi.constructor + ); + println!( + "CURRENT ABI upgrade constructor {:?}", + current_contract_abi.upgrade_constructor + ); + println!("CURRENT ABI endpoints {:?}", current_contract_abi.endpoints); + + let mut diff_abi = ShortContractAbi { + name: current_contract_abi.name.clone(), + constructor: vec![], + upgrade_constructor: vec![], + endpoints: vec![], + deleted_endpoints: vec![], + }; + + // changed and new constructors + for constructor in ¤t_contract_abi.constructor { + if !prev_abi + .constructor + .iter() + .any(|e| e.name == constructor.name) + { + diff_abi.constructor.push(constructor.clone()); + } + } + + // changed and new upgrade constructors + // PartialEq doesn't work + for upgrade_constructor in ¤t_contract_abi.upgrade_constructor { + if !prev_abi + .upgrade_constructor + .iter() + .any(|e| e.name == upgrade_constructor.name) + { + diff_abi + .upgrade_constructor + .push(upgrade_constructor.clone()); + } + } + + // changed and new endpoints + // RUST_METHOD_NAME not inherited from EndpointAbi + for endpoint in ¤t_contract_abi.endpoints { + if !prev_abi.endpoints.iter().any(|e| e.name == endpoint.name) { + diff_abi.endpoints.push(endpoint.clone()); + } + } + + // deleted endpoints + for endpoint in &prev_abi.endpoints { + if !current_contract_abi + .endpoints + .iter() + .any(|e| e.name == endpoint.name) + { + diff_abi.deleted_endpoints.push(endpoint.clone()); + } + } + + // deleted endpoints arrive in struct without a rust name + println!("diff_abi endpoints {:?}", diff_abi.endpoints); + println!( + "diff_abi deleted endpoints {:?}", + diff_abi.deleted_endpoints + ); + + return diff_abi; + } + } + } + current_contract_abi.clone() +} + +pub(crate) fn create_prev_abi_file(snippets_dir: &Path, contract_abi: &ContractAbi) { + let abi_json = ContractAbiJson::from(contract_abi); + let abi_string = serialize_abi_to_json(&abi_json); + let abi_file_path = snippets_dir.join(PREV_ABI_NAME); + + let mut abi_file = File::create(abi_file_path).unwrap(); + write!(abi_file, "{abi_string}").unwrap(); +} + +pub(crate) fn add_new_endpoints_to_file(snippets_dir: &Path, diff_abi: &ShortContractAbi) { + let interact_lib_path = snippets_dir.join("src").join(LIB_SOURCE_FILE_NAME); + let file_content = std::fs::read_to_string(&interact_lib_path).unwrap(); + let mut updated_content = file_content.clone(); + + for deleted_endpoint in &diff_abi.deleted_endpoints { + remove_function2(snippets_dir, deleted_endpoint); + } + + for endpoint_abi in &diff_abi.endpoints { + updated_content = insert_function(&updated_content, endpoint_abi, &diff_abi.name); + } + + for constructor in &diff_abi.constructor { + updated_content = insert_function(&updated_content, constructor, &diff_abi.name); + } + + for upgrade_constructor in &diff_abi.upgrade_constructor { + updated_content = insert_function(&updated_content, upgrade_constructor, &diff_abi.name); + } + + std::fs::write(interact_lib_path, updated_content).unwrap(); +} + +fn insert_function( + file_content: &str, + endpoint_abi: &ShortEndpointAbi, + contract_name: &String, +) -> String { + let mut updated_content = file_content.to_string(); + + println!("Inserting endpoint with name {:?}", endpoint_abi.name); + + let new_function = { + let mut function_buffer = String::new(); + write_endpoint_impl_to_string(&mut function_buffer, endpoint_abi, contract_name); + function_buffer + }; + + updated_content.push_str(&new_function); + updated_content.push('\n'); + + updated_content +} + +pub(crate) fn remove_function2(snippets_dir: &Path, endpoint: &ShortEndpointAbi) { + let interact_lib_path = snippets_dir.join("src").join(LIB_SOURCE_FILE_NAME); + + let file_content = std::fs::read_to_string(&interact_lib_path).unwrap(); + + // find and remove the function + let updated_content = find_and_remove_function(file_content, &endpoint.name); + + // delete the initial file + std::fs::remove_file(&interact_lib_path).unwrap(); + + // create a new file at the same path with the updated content + std::fs::write(&interact_lib_path, &updated_content).unwrap(); +} + +pub(crate) fn find_and_remove_function(file_content: String, endpoint_name: &str) -> String { + let lines: Vec<&str> = file_content.lines().collect(); + + // find the start of the function + if let Some(start_index) = lines.iter().position(|line| { + !line.starts_with("//") + && !line.starts_with("/*") + && !line.ends_with("*/") + && line.contains(&format!("pub async fn {}", endpoint_name)) + }) { + // find the end of the function by tracking brace balance + let mut balance = 0; + let mut end_index = start_index; + + for (i, line) in lines.iter().enumerate().skip(start_index) { + // count opening and closing braces + balance += line.chars().filter(|&c| c == '{').count() as i32; + balance -= line.chars().filter(|&c| c == '}').count() as i32; + + if balance == 0 { + end_index = i; + break; + } + } + + // remove the function block including whitespace + let updated_lines: Vec<&str> = lines + .iter() + .enumerate() + .filter(|&(i, _)| i < start_index || i > end_index) + .map(|(_, &line)| line) + .collect(); + + // join the lines back together + let updated_content = updated_lines.join("\n"); + + println!("********"); + + println!("updated content after deletion {updated_content:?}"); + + updated_content + } else { + println!( + "Cannot delete function {:?} from the file. Not found", + endpoint_name + ); + + file_content + } +} + +pub(crate) fn write_endpoint_impl_to_string( + buffer: &mut String, + endpoint_abi: &ShortEndpointAbi, + name: &String, +) { + println!( + "Writing endpoint impl for function with name {:?}", + endpoint_abi.rust_method_name + ); + write_method_declaration_to_string(buffer, &endpoint_abi.rust_method_name); + write_payments_declaration_to_string(buffer, &endpoint_abi.payable_in_tokens); + write_endpoint_args_declaration_to_string(buffer, &endpoint_abi.inputs); + + if endpoint_abi.mutability == *"readonly" { + write_contract_query_to_string(buffer, endpoint_abi, name); + } else { + write_contract_call_to_string(buffer, endpoint_abi, name); + } + + buffer.push_str(" }\n"); + buffer.push('\n'); +} + +pub(crate) fn write_method_declaration_to_string(buffer: &mut String, endpoint_name: &str) { + buffer.push_str(&format!(" pub async fn {endpoint_name}(&mut self) {{\n")); +} + +pub(crate) fn write_payments_declaration_to_string( + buffer: &mut String, + accepted_tokens: &[String], +) { + if accepted_tokens.is_empty() { + return; + } + + let biguint_default = map_abi_type_to_rust_type("BigUint".to_string()); + let first_accepted = &accepted_tokens[0]; + + if first_accepted == "EGLD" { + buffer.push_str(&format!( + " let egld_amount = {};\n", + biguint_default.get_default_value_expr() + )); + } else { + buffer.push_str( + " let token_id = String::new(); + let token_nonce = 0u64; + let token_amount = ", + ); + buffer.push_str(biguint_default.get_default_value_expr()); + buffer.push_str(";\n"); + } + + buffer.push('\n'); +} + +fn write_endpoint_args_declaration_to_string(buffer: &mut String, inputs: &[ShortInputAbi]) { + if inputs.is_empty() { + return; + } + + for input in inputs { + let rust_type = map_abi_type_to_rust_type(input.type_name.clone()); + buffer.push_str(&format!( + " let {} = {};\n", + input.name, + rust_type.get_default_value_expr() + )); + } + + buffer.push('\n'); +} + +fn write_contract_call_to_string( + buffer: &mut String, + endpoint_abi: &ShortEndpointAbi, + name: &String, +) { + let payment_snippet = if endpoint_abi.payable_in_tokens.is_empty() { + "".to_string() + } else if endpoint_abi.payable_in_tokens[0] == "EGLD" { + "\n .egld(egld_amount)".to_string() + } else { + "\n .payment((TokenIdentifier::from(token_id.as_str()), token_nonce, token_amount))".to_string() + }; + + buffer.push_str(&format!( + r#" let response = self + .interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_address()) + .gas({DEFAULT_GAS}) + .typed(proxy::{}Proxy) + .{}({}){} + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {{response:?}}"); +"#, + name, + endpoint_abi.rust_method_name, + endpoint_args_when_called_short(endpoint_abi.inputs.as_slice()), + payment_snippet, + )); +} + +fn write_contract_query_to_string( + buffer: &mut String, + endpoint_abi: &ShortEndpointAbi, + name: &String, +) { + buffer.push_str(&format!( + r#" let result_value = self + .interactor + .query() + .to(self.state.current_address()) + .typed(proxy::{}Proxy) + .{}({}) + .returns(ReturnsResultUnmanaged) + .run() + .await; + + println!("Result: {{result_value:?}}"); +"#, + name, + endpoint_abi.rust_method_name, + endpoint_args_when_called_short(endpoint_abi.inputs.as_slice()), + )); +} + +pub(crate) fn endpoint_args_when_called_short(inputs: &[ShortInputAbi]) -> String { + let mut result = String::new(); + for input in inputs { + if !result.is_empty() { + result.push_str(", "); + } + result.push_str(&input.name); + } + result +} diff --git a/framework/meta-lib/src/contract/generate_snippets/snippet_crate_gen.rs b/framework/meta-lib/src/contract/generate_snippets/snippet_crate_gen.rs index a293ace797..43525ed3db 100644 --- a/framework/meta-lib/src/contract/generate_snippets/snippet_crate_gen.rs +++ b/framework/meta-lib/src/contract/generate_snippets/snippet_crate_gen.rs @@ -8,7 +8,7 @@ use std::{ use crate::version_history; static SNIPPETS_SOURCE_FILE_NAME: &str = "interactor_main.rs"; -static LIB_SOURCE_FILE_NAME: &str = "interact.rs"; +pub(crate) static LIB_SOURCE_FILE_NAME: &str = "interact.rs"; static SC_CONFIG_FILE_NAME: &str = "sc-config.toml"; static CONFIG_TOML_PATH: &str = "config.toml"; static CONFIG_SOURCE_FILE_NAME: &str = "config.rs"; diff --git a/framework/meta-lib/src/contract/generate_snippets/snippet_gen_main.rs b/framework/meta-lib/src/contract/generate_snippets/snippet_gen_main.rs index 1535e3c147..3f91aeade2 100644 --- a/framework/meta-lib/src/contract/generate_snippets/snippet_gen_main.rs +++ b/framework/meta-lib/src/contract/generate_snippets/snippet_gen_main.rs @@ -3,12 +3,13 @@ use std::{ path::{Path, PathBuf}, }; -use multiversx_sc::abi::ContractAbi; - use crate::cli::GenerateSnippetsArgs; use super::{ super::meta_config::MetaConfig, + snippet_abi_check::{ + add_new_endpoints_to_file, check_abi_differences, create_prev_abi_file, ShortContractAbi, + }, snippet_crate_gen::{ create_and_get_lib_file, create_config_rust_file, create_config_toml_file, create_main_file, create_sc_config_file, create_snippets_cargo_toml, @@ -33,18 +34,31 @@ impl MetaConfig { .contract_crate .name .replace("-", "_"); - let mut file = - create_snippets_crate_and_get_lib_file(&self.snippets_dir, crate_name, args.overwrite); - write_snippets_to_file(&mut file, &self.original_contract_abi, crate_name); - let mut config_file = create_config_and_get_file(&self.snippets_dir); - write_config_to_file(&mut config_file); - let (mut interactor_test_file, mut chain_sim_test_file) = - create_test_folder_and_get_files(&self.snippets_dir); - write_tests_to_files( - &mut interactor_test_file, - &mut chain_sim_test_file, - crate_name, - ); + let original_contract_abi = ShortContractAbi::from(self.original_contract_abi.clone()); + let diff_abi = + check_abi_differences(&original_contract_abi, &self.snippets_dir, args.overwrite); + if diff_abi == original_contract_abi { + let mut file = create_snippets_crate_and_get_lib_file( + &self.snippets_dir, + crate_name, + args.overwrite, + ); + write_snippets_to_file(&mut file, &original_contract_abi, crate_name); + let mut config_file = create_config_and_get_file(&self.snippets_dir); + write_config_to_file(&mut config_file); + let (mut interactor_test_file, mut chain_sim_test_file) = + create_test_folder_and_get_files(&self.snippets_dir); + write_tests_to_files( + &mut interactor_test_file, + &mut chain_sim_test_file, + crate_name, + ); + } else { + add_new_endpoints_to_file(&self.snippets_dir, &diff_abi); + } + + // create prev-abi.json file + create_prev_abi_file(&self.snippets_dir, &self.original_contract_abi); } } @@ -69,7 +83,7 @@ fn create_config_and_get_file(snippets_folder_path: &Path) -> File { create_config_rust_file(snippets_folder_path) } -fn write_snippets_to_file(file: &mut File, abi: &ContractAbi, crate_name: &str) { +fn write_snippets_to_file(file: &mut File, abi: &ShortContractAbi, crate_name: &str) { write_snippet_imports(file); write_snippet_constants(file); write_snippet_main_function(file, abi, crate_name); diff --git a/framework/meta-lib/src/contract/generate_snippets/snippet_sc_functions_gen.rs b/framework/meta-lib/src/contract/generate_snippets/snippet_sc_functions_gen.rs index 8a1187d482..b2a9d46a21 100644 --- a/framework/meta-lib/src/contract/generate_snippets/snippet_sc_functions_gen.rs +++ b/framework/meta-lib/src/contract/generate_snippets/snippet_sc_functions_gen.rs @@ -1,12 +1,14 @@ use std::{fs::File, io::Write, path::Path}; -use multiversx_sc::abi::{ContractAbi, EndpointAbi, EndpointMutabilityAbi, InputAbi}; +use super::snippet_abi_check::{write_endpoint_impl_to_string, ShortContractAbi}; -use super::{snippet_gen_common::write_newline, snippet_type_map::map_abi_type_to_rust_type}; +pub(crate) const DEFAULT_GAS: &str = "30_000_000u64"; -const DEFAULT_GAS: &str = "30_000_000u64"; - -pub(crate) fn write_interact_struct_impl(file: &mut File, abi: &ContractAbi, crate_name: &str) { +pub(crate) fn write_interact_struct_impl( + file: &mut File, + abi: &ShortContractAbi, + crate_name: &str, +) { let crate_path = crate_name.replace("_", "-"); let mxsc_file_name = format!("{crate_path}.mxsc.json"); let wasm_output_file_path = Path::new("..").join("output").join(mxsc_file_name); @@ -45,210 +47,20 @@ pub(crate) fn write_interact_struct_impl(file: &mut File, abi: &ContractAbi, cra crate_path, wasm_output_file_path_expr, ) .unwrap(); - write_deploy_method_impl(file, &abi.constructors[0], &abi.name); - for upgrade_abi in &abi.upgrade_constructors { - write_upgrade_endpoint_impl(file, upgrade_abi, &abi.name); + let mut buffer = String::new(); + write_endpoint_impl_to_string(&mut buffer, &abi.constructor[0], &abi.name); + + for upgrade_abi in &abi.upgrade_constructor { + write_endpoint_impl_to_string(&mut buffer, upgrade_abi, &abi.name); } for endpoint_abi in &abi.endpoints { - write_endpoint_impl(file, endpoint_abi, &abi.name); + write_endpoint_impl_to_string(&mut buffer, endpoint_abi, &abi.name); } + write!(file, "{}", buffer).unwrap(); + // close impl block brackets writeln!(file, "}}").unwrap(); } - -fn write_deploy_method_impl(file: &mut File, init_abi: &EndpointAbi, name: &String) { - write_method_declaration(file, "deploy"); - write_endpoint_args_declaration(file, &init_abi.inputs); - let proxy_name = format!("{}Proxy", name); - - writeln!( - file, - r#" let new_address = self - .interactor - .tx() - .from(&self.wallet_address) - .gas({DEFAULT_GAS}) - .typed(proxy::{}) - .init({}) - .code(&self.contract_code) - .returns(ReturnsNewAddress) - .run() - .await; - let new_address_bech32 = new_address.to_bech32_default(); - println!("new address: {{new_address_bech32}}"); - self.state.set_address(new_address_bech32);"#, - proxy_name, - endpoint_args_when_called(init_abi.inputs.as_slice()), - ) - .unwrap(); - - // close method block brackets - writeln!(file, " }}").unwrap(); - write_newline(file); -} - -fn write_upgrade_endpoint_impl(file: &mut File, upgrade_abi: &EndpointAbi, name: &String) { - write_method_declaration(file, "upgrade"); - write_endpoint_args_declaration(file, &upgrade_abi.inputs); - let proxy_name = format!("{}Proxy", name); - - writeln!( - file, - r#" let response = self - .interactor - .tx() - .to(self.state.current_address()) - .from(&self.wallet_address) - .gas({DEFAULT_GAS}) - .typed(proxy::{}) - .upgrade({}) - .code(&self.contract_code) - .code_metadata(CodeMetadata::UPGRADEABLE) - .returns(ReturnsResultUnmanaged) - .run() - .await; - - println!("Result: {{response:?}}");"#, - proxy_name, - endpoint_args_when_called(upgrade_abi.inputs.as_slice()), - ) - .unwrap(); - - // close method block brackets - writeln!(file, " }}").unwrap(); - write_newline(file); -} - -fn write_endpoint_impl(file: &mut File, endpoint_abi: &EndpointAbi, name: &String) { - write_method_declaration(file, &endpoint_abi.rust_method_name); - write_payments_declaration(file, &endpoint_abi.payable_in_tokens); - write_endpoint_args_declaration(file, &endpoint_abi.inputs); - if matches!(endpoint_abi.mutability, EndpointMutabilityAbi::Readonly) { - write_contract_query(file, endpoint_abi, name); - } else { - write_contract_call(file, endpoint_abi, name); - } - - // close method block brackets - writeln!(file, " }}").unwrap(); - write_newline(file); -} - -fn write_method_declaration(file: &mut File, endpoint_name: &str) { - writeln!(file, " pub async fn {endpoint_name}(&mut self) {{").unwrap(); -} - -fn write_payments_declaration(file: &mut File, accepted_tokens: &[String]) { - if accepted_tokens.is_empty() { - return; - } - - // only handle EGLD and "any" case, as they're the most common - let biguint_default = map_abi_type_to_rust_type("BigUint".to_string()); - let first_accepted = &accepted_tokens[0]; - if first_accepted == "EGLD" { - writeln!( - file, - " let egld_amount = {};", - biguint_default.get_default_value_expr() - ) - .unwrap(); - } else { - writeln!( - file, - " let token_id = String::new(); - let token_nonce = 0u64; - let token_amount = {};", - biguint_default.get_default_value_expr() - ) - .unwrap(); - } - - write_newline(file); -} - -fn write_endpoint_args_declaration(file: &mut File, inputs: &[InputAbi]) { - if inputs.is_empty() { - return; - } - - for input in inputs { - let rust_type = map_abi_type_to_rust_type(input.type_names.abi.clone()); - writeln!( - file, - " let {} = {};", - input.arg_name, - rust_type.get_default_value_expr() - ) - .unwrap(); - } - - write_newline(file); -} - -fn endpoint_args_when_called(inputs: &[InputAbi]) -> String { - let mut result = String::new(); - for input in inputs { - if !result.is_empty() { - result.push_str(", "); - } - result.push_str(&input.arg_name); - } - result -} - -fn write_contract_call(file: &mut File, endpoint_abi: &EndpointAbi, name: &String) { - let payment_snippet = if endpoint_abi.payable_in_tokens.is_empty() { - "" - } else if endpoint_abi.payable_in_tokens[0] == "EGLD" { - "\n .egld(egld_amount)" - } else { - "\n .payment((TokenIdentifier::from(token_id.as_str()), token_nonce, token_amount))" - }; - - writeln!( - file, - r#" let response = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas({DEFAULT_GAS}) - .typed(proxy::{}Proxy) - .{}({}){} - .returns(ReturnsResultUnmanaged) - .run() - .await; - - println!("Result: {{response:?}}");"#, - name, - endpoint_abi.rust_method_name, - endpoint_args_when_called(endpoint_abi.inputs.as_slice()), - payment_snippet, - ) - .unwrap(); -} - -fn write_contract_query(file: &mut File, endpoint_abi: &EndpointAbi, name: &String) { - writeln!( - file, - r#" let result_value = self - .interactor - .query() - .to(self.state.current_address()) - .typed(proxy::{}Proxy) - .{}({}) - .returns(ReturnsResultUnmanaged) - .run() - .await; - - println!("Result: {{result_value:?}}");"#, - name, - endpoint_abi.rust_method_name, - endpoint_args_when_called(endpoint_abi.inputs.as_slice()), - ) - .unwrap(); -} diff --git a/framework/meta-lib/src/contract/generate_snippets/snippet_template_gen.rs b/framework/meta-lib/src/contract/generate_snippets/snippet_template_gen.rs index 75dfe258a5..2aa7b839b2 100644 --- a/framework/meta-lib/src/contract/generate_snippets/snippet_template_gen.rs +++ b/framework/meta-lib/src/contract/generate_snippets/snippet_template_gen.rs @@ -1,8 +1,6 @@ use std::{fs::File, io::Write}; -use multiversx_sc::abi::ContractAbi; - -use super::snippet_gen_common::write_newline; +use super::{snippet_abi_check::ShortContractAbi, snippet_gen_common::write_newline}; pub(crate) fn write_snippet_imports(file: &mut File) { writeln!( @@ -29,7 +27,11 @@ pub(crate) fn write_snippet_constants(file: &mut File) { writeln!(file, "const STATE_FILE: &str = \"state.toml\";").unwrap(); } -pub(crate) fn write_snippet_main_function(file: &mut File, abi: &ContractAbi, crate_name: &str) { +pub(crate) fn write_snippet_main_function( + file: &mut File, + abi: &ShortContractAbi, + crate_name: &str, +) { writeln!( file, " @@ -48,7 +50,7 @@ pub async fn {crate_name}_cli() {{ // all contracts have a deploy snippet writeln!(file, r#" "deploy" => interact.deploy().await,"#).unwrap(); - for upgrade_endpoint in &abi.upgrade_constructors { + for upgrade_endpoint in &abi.upgrade_constructor { writeln!( file, r#" "{}" => interact.{}().await,"#,