From e92f48e48506c10ce2545044f397c3c0293969e3 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 14:32:15 +0300 Subject: [PATCH 001/195] Bech32Address HRP type, refactor --- chain/core/src/std.rs | 2 +- chain/core/src/std/bech32_address.rs | 48 ++++-------- .../bech32_address/bech32_address_error.rs | 26 +++++++ .../core/src/std/bech32_address/bech32_hrp.rs | 74 +++++++++++++++++++ chain/core/src/std/new_address.rs | 2 +- 5 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 chain/core/src/std/bech32_address/bech32_address_error.rs create mode 100644 chain/core/src/std/bech32_address/bech32_hrp.rs diff --git a/chain/core/src/std.rs b/chain/core/src/std.rs index d5863e7703..7916ea25ef 100644 --- a/chain/core/src/std.rs +++ b/chain/core/src/std.rs @@ -3,5 +3,5 @@ mod code_hash; pub mod crypto; pub mod new_address; -pub use bech32_address::Bech32Address; +pub use bech32_address::{Bech32Address, Bech32Hrp}; pub use code_hash::{CODE_HASH_LEN, code_hash}; diff --git a/chain/core/src/std/bech32_address.rs b/chain/core/src/std/bech32_address.rs index 51f853f0b1..41578fc6a0 100644 --- a/chain/core/src/std/bech32_address.rs +++ b/chain/core/src/std/bech32_address.rs @@ -5,39 +5,18 @@ use crate::{codec::*, types::Address}; use bech32::{Bech32, Hrp}; use serde::{Deserialize, Serialize}; +mod bech32_address_error; +pub use bech32_address_error::Bech32AddressError; + +mod bech32_hrp; +pub use bech32_hrp::Bech32Hrp; + const BECH32_PREFIX: &str = "bech32:"; const ERR_ADDRESS_EMPTY: &str = "address string is empty"; const DEFAULT_HRP: &str = "erd"; -/// Error type returned by [`Bech32Address::try_from_bech32_string`]. -#[derive(Debug)] -pub enum Bech32AddressError { - /// The string could not be decoded as a valid bech32 value. - /// Contains a message with the offending input and the underlying error. - DecodeError(String), - /// The decoded payload is not exactly 32 bytes. - /// Contains the actual decoded length. - InvalidLength(usize), -} - -impl core::fmt::Display for Bech32AddressError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Bech32AddressError::DecodeError(msg) => write!(f, "bech32 decode error: {msg}"), - Bech32AddressError::InvalidLength(len) => { - write!( - f, - "invalid address length after decoding: expected 32, got {len}" - ) - } - } - } -} - -impl std::error::Error for Bech32AddressError {} - /// Wraps and address, and presents it as a bech32 expression wherever possible. /// /// In order to avoid repeated conversions, it redundantly keeps the bech32 representation inside. @@ -48,7 +27,7 @@ impl std::error::Error for Bech32AddressError {} #[derive(Clone, PartialEq, Eq)] pub struct Bech32Address { pub address: Address, - pub hrp: String, + pub hrp: Bech32Hrp, pub bech32: String, } @@ -71,7 +50,7 @@ impl Bech32Address { Ok(Bech32Address { address: Address::from_slice(&dest_address_bytes), - hrp: hrp.to_string(), + hrp: Bech32Hrp::from_string(hrp.to_string()), bech32: bech32_string, }) } @@ -102,7 +81,7 @@ impl Bech32Address { Bech32Address { address, - hrp: hrp.to_owned(), + hrp: Bech32Hrp::from_string(hrp.to_owned()), bech32: bech32_string, } } @@ -164,12 +143,12 @@ impl Bech32Address { /// Returns the human-readable part (HRP) as a borrowed `&str`. pub fn as_hrp(&self) -> &str { - &self.hrp + self.hrp.as_str() } /// Clones and returns the human-readable part (HRP) as an owned [`String`]. pub fn to_hrp(&self) -> String { - self.hrp.clone() + self.hrp.as_str().to_string() } /// Consumes `self` and returns the underlying [`Address`]. @@ -322,7 +301,7 @@ impl FromStr for Bech32Address { } Ok(Bech32Address { address: Address::from_slice(&dest_address_bytes), - hrp: hrp.to_string(), + hrp: Bech32Hrp::from_string(hrp.to_string()), bech32: s.to_string(), }) } @@ -348,7 +327,8 @@ mod tests { #[test] fn test_try_from_bech32_string_roundtrip() { let original = Bech32Address::try_from_bech32_string(VALID_BECH32.to_string()).unwrap(); - let re_encoded = Bech32Address::encode_address(&original.hrp, original.address.clone()); + let re_encoded = + Bech32Address::encode_address(original.hrp.as_str(), original.address.clone()); assert_eq!(re_encoded.bech32, VALID_BECH32); } diff --git a/chain/core/src/std/bech32_address/bech32_address_error.rs b/chain/core/src/std/bech32_address/bech32_address_error.rs new file mode 100644 index 0000000000..52d39cedd7 --- /dev/null +++ b/chain/core/src/std/bech32_address/bech32_address_error.rs @@ -0,0 +1,26 @@ +/// Error type returned by [`super::Bech32Address::try_from_bech32_string`]. +#[derive(Debug)] +pub enum Bech32AddressError { + /// The string could not be decoded as a valid bech32 value. + /// Contains a message with the offending input and the underlying error. + DecodeError(String), + /// The decoded payload is not exactly 32 bytes. + /// Contains the actual decoded length. + InvalidLength(usize), +} + +impl core::fmt::Display for Bech32AddressError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Bech32AddressError::DecodeError(msg) => write!(f, "bech32 decode error: {msg}"), + Bech32AddressError::InvalidLength(len) => { + write!( + f, + "invalid address length after decoding: expected 32, got {len}" + ) + } + } + } +} + +impl std::error::Error for Bech32AddressError {} diff --git a/chain/core/src/std/bech32_address/bech32_hrp.rs b/chain/core/src/std/bech32_address/bech32_hrp.rs new file mode 100644 index 0000000000..e6c438d2f3 --- /dev/null +++ b/chain/core/src/std/bech32_address/bech32_hrp.rs @@ -0,0 +1,74 @@ +use std::borrow::Cow; + +/// Newtype wrapping the human-readable part (HRP) of a bech32 address. +/// +/// Uses [`Cow<'static, str>`] so that constants can be created with borrowed string literals: +/// +/// ```rust +/// # use multiversx_chain_core::std::Bech32Hrp; +/// use std::borrow::Cow; +/// const ERD_HRP: Bech32Hrp = Bech32Hrp(Cow::Borrowed("erd")); +/// ``` +/// +/// Dynamic HRPs built at runtime use the [`Cow::Owned`] variant via [`Bech32Hrp::from_string`]. +#[derive(Clone, PartialEq, Eq)] +pub struct Bech32Hrp(pub Cow<'static, str>); + +impl Bech32Hrp { + /// Creates a `Bech32Hrp` from a `&'static str`. + /// + /// This is a `const fn`, making it usable in constant and static contexts: + /// ```rust + /// # use multiversx_chain_core::std::Bech32Hrp; + /// const ERD: Bech32Hrp = Bech32Hrp::from_static("erd"); + /// ``` + pub const fn from_static(hrp: &'static str) -> Self { + Bech32Hrp(Cow::Borrowed(hrp)) + } + + /// Creates a `Bech32Hrp` from an owned [`String`]. + pub fn from_string(hrp: String) -> Self { + Bech32Hrp(Cow::Owned(hrp)) + } + + /// Returns the HRP as a borrowed `&str`. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for Bech32Hrp { + fn from(s: String) -> Self { + Bech32Hrp::from_string(s) + } +} + +impl From<&'static str> for Bech32Hrp { + fn from(s: &'static str) -> Self { + Bech32Hrp::from_static(s) + } +} + +impl PartialEq for Bech32Hrp { + fn eq(&self, other: &str) -> bool { + self.0.as_ref() == other + } +} + +impl PartialEq<&str> for Bech32Hrp { + fn eq(&self, other: &&str) -> bool { + self.0.as_ref() == *other + } +} + +impl core::fmt::Display for Bech32Hrp { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.0) + } +} + +impl core::fmt::Debug for Bech32Hrp { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Bech32Hrp").field(&self.0).finish() + } +} diff --git a/chain/core/src/std/new_address.rs b/chain/core/src/std/new_address.rs index 7b1ff3a50f..0e7eebe5a5 100644 --- a/chain/core/src/std/new_address.rs +++ b/chain/core/src/std/new_address.rs @@ -58,7 +58,7 @@ pub fn compute_new_address_bech32( deployment_nonce: u64, ) -> Bech32Address { let address = compute_new_address(&deployer.address, deployment_nonce); - Bech32Address::encode_address(&deployer.hrp, address) + Bech32Address::encode_address(deployer.hrp.as_str(), address) } /// Generates a mock smart contract address deterministically from a creator address and nonce. From 67850a20c3f474077f71a8acdafbf1bfc263826e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 15:55:44 +0300 Subject: [PATCH 002/195] Bech32Hrp refactor, appropriate usage --- chain/core/src/std/bech32_address.rs | 32 +++---- .../core/src/std/bech32_address/bech32_hrp.rs | 88 ++++++++++++++----- chain/core/src/std/new_address.rs | 2 +- chain/core/src/types/address.rs | 2 +- framework/meta/src/cmd/wallet.rs | 13 ++- .../src/interactor/interactor_base.rs | 11 ++- .../interactor_sc_deploy.rs | 6 +- .../interactor_vm_query.rs | 4 +- .../src/interactor/interactor_sender.rs | 3 +- sdk/core/src/data/network_config.rs | 7 +- sdk/core/src/wallet.rs | 4 +- sdk/http/src/gateway_http_proxy/http_tx.rs | 4 +- 12 files changed, 113 insertions(+), 63 deletions(-) diff --git a/chain/core/src/std/bech32_address.rs b/chain/core/src/std/bech32_address.rs index 41578fc6a0..6cd9e68edf 100644 --- a/chain/core/src/std/bech32_address.rs +++ b/chain/core/src/std/bech32_address.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use std::str::FromStr; use crate::{codec::*, types::Address}; -use bech32::{Bech32, Hrp}; +use bech32::Bech32; use serde::{Deserialize, Serialize}; mod bech32_address_error; @@ -15,8 +15,6 @@ const BECH32_PREFIX: &str = "bech32:"; const ERR_ADDRESS_EMPTY: &str = "address string is empty"; -const DEFAULT_HRP: &str = "erd"; - /// Wraps and address, and presents it as a bech32 expression wherever possible. /// /// In order to avoid repeated conversions, it redundantly keeps the bech32 representation inside. @@ -50,7 +48,7 @@ impl Bech32Address { Ok(Bech32Address { address: Address::from_slice(&dest_address_bytes), - hrp: Bech32Hrp::from_string(hrp.to_string()), + hrp: Bech32Hrp::from(hrp), bech32: bech32_string, }) } @@ -74,25 +72,24 @@ impl Bech32Address { } /// Encodes an [`Address`] as bech32 using the given human-readable part (HRP). - pub fn encode_address(hrp: &str, address: Address) -> Self { - let hrp_obj = Hrp::parse(hrp).expect("invalid hrp"); - let bech32_string = - bech32::encode::(hrp_obj, address.as_bytes()).expect("bech32 encode error"); + pub fn encode_address(hrp: Bech32Hrp, address: Address) -> Self { + let bech32_string = bech32::encode::(hrp.to_inner(), address.as_bytes()) + .expect("bech32 encode error"); Bech32Address { address, - hrp: Bech32Hrp::from_string(hrp.to_owned()), + hrp, bech32: bech32_string, } } /// Encodes an [`Address`] as bech32 using the default HRP (`"erd"`). pub fn encode_address_default_hrp(address: Address) -> Self { - Self::encode_address(DEFAULT_HRP, address) + Self::encode_address(Bech32Hrp::default(), address) } /// Returns the zero address encoded with the given HRP. - pub fn zero(hrp: &str) -> Self { + pub fn zero(hrp: Bech32Hrp) -> Self { Bech32Address::encode_address(hrp, Address::zero()) } @@ -146,9 +143,9 @@ impl Bech32Address { self.hrp.as_str() } - /// Clones and returns the human-readable part (HRP) as an owned [`String`]. - pub fn to_hrp(&self) -> String { - self.hrp.as_str().to_string() + /// Returns the human-readable part (HRP) as [`Bech32Hrp`]. + pub fn to_hrp(&self) -> Bech32Hrp { + self.hrp } /// Consumes `self` and returns the underlying [`Address`]. @@ -301,7 +298,7 @@ impl FromStr for Bech32Address { } Ok(Bech32Address { address: Address::from_slice(&dest_address_bytes), - hrp: Bech32Hrp::from_string(hrp.to_string()), + hrp: Bech32Hrp::from(hrp), bech32: s.to_string(), }) } @@ -327,8 +324,7 @@ mod tests { #[test] fn test_try_from_bech32_string_roundtrip() { let original = Bech32Address::try_from_bech32_string(VALID_BECH32.to_string()).unwrap(); - let re_encoded = - Bech32Address::encode_address(original.hrp.as_str(), original.address.clone()); + let re_encoded = Bech32Address::encode_address(original.hrp, original.address.clone()); assert_eq!(re_encoded.bech32, VALID_BECH32); } @@ -349,7 +345,7 @@ mod tests { #[test] fn test_try_from_bech32_string_wrong_length() { // Encode a payload that is not 32 bytes to trigger the length check. - let hrp = bech32::Hrp::parse(DEFAULT_HRP).unwrap(); + let hrp = Bech32Hrp::default().to_inner(); let short_payload = [0u8; 10]; let bad_bech32 = bech32::encode::(hrp, &short_payload).unwrap(); let result = Bech32Address::try_from_bech32_string(bad_bech32); diff --git a/chain/core/src/std/bech32_address/bech32_hrp.rs b/chain/core/src/std/bech32_address/bech32_hrp.rs index e6c438d2f3..2e967ced93 100644 --- a/chain/core/src/std/bech32_address/bech32_hrp.rs +++ b/chain/core/src/std/bech32_address/bech32_hrp.rs @@ -1,39 +1,68 @@ -use std::borrow::Cow; +use bech32::Hrp; +use serde::{Deserialize, Serialize}; + +/// The default human-readable part (HRP) for MultiversX mainnet addresses: `"erd"`. +/// +/// Used as the [`Default`] value for [`Bech32Hrp`] and as the default HRP when +/// encoding addresses without an explicit HRP. +pub const ERD_HRP: Bech32Hrp = Bech32Hrp::parse_unchecked("erd"); /// Newtype wrapping the human-readable part (HRP) of a bech32 address. /// -/// Uses [`Cow<'static, str>`] so that constants can be created with borrowed string literals: +/// Wraps [`bech32::Hrp`], which validates and stores the HRP in a fixed-size buffer. +/// Because [`Hrp`] is [`Copy`], so is `Bech32Hrp`. +/// +/// Constants can be created with [`Bech32Hrp::parse_unchecked`], which is `const`: /// /// ```rust /// # use multiversx_chain_core::std::Bech32Hrp; -/// use std::borrow::Cow; -/// const ERD_HRP: Bech32Hrp = Bech32Hrp(Cow::Borrowed("erd")); +/// const ERD_HRP: Bech32Hrp = Bech32Hrp::parse_unchecked("erd"); /// ``` -/// -/// Dynamic HRPs built at runtime use the [`Cow::Owned`] variant via [`Bech32Hrp::from_string`]. -#[derive(Clone, PartialEq, Eq)] -pub struct Bech32Hrp(pub Cow<'static, str>); +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Bech32Hrp(Hrp); impl Bech32Hrp { - /// Creates a `Bech32Hrp` from a `&'static str`. + /// Creates a `Bech32Hrp` from a string slice without validation. /// /// This is a `const fn`, making it usable in constant and static contexts: /// ```rust /// # use multiversx_chain_core::std::Bech32Hrp; - /// const ERD: Bech32Hrp = Bech32Hrp::from_static("erd"); + /// const ERD: Bech32Hrp = Bech32Hrp::parse_unchecked("erd"); /// ``` - pub const fn from_static(hrp: &'static str) -> Self { - Bech32Hrp(Cow::Borrowed(hrp)) + /// + /// For runtime strings with validation use [`Bech32Hrp::from_string`]. + pub const fn parse_unchecked(hrp: &str) -> Self { + Bech32Hrp(Hrp::parse_unchecked(hrp)) } - /// Creates a `Bech32Hrp` from an owned [`String`]. + /// Parses and creates a `Bech32Hrp` from an owned [`String`]. + /// + /// # Panics + /// + /// Panics if `hrp` is not a valid bech32 human-readable part. pub fn from_string(hrp: String) -> Self { - Bech32Hrp(Cow::Owned(hrp)) + Bech32Hrp(Hrp::parse(&hrp).unwrap_or_else(|err| panic!("invalid HRP '{hrp}': {err}"))) } /// Returns the HRP as a borrowed `&str`. pub fn as_str(&self) -> &str { - &self.0 + self.0.as_str() + } + + pub fn to_inner(&self) -> Hrp { + self.0 + } +} + +impl Default for Bech32Hrp { + fn default() -> Self { + ERD_HRP + } +} + +impl From for Bech32Hrp { + fn from(hrp: Hrp) -> Self { + Bech32Hrp(hrp) } } @@ -43,32 +72,47 @@ impl From for Bech32Hrp { } } -impl From<&'static str> for Bech32Hrp { - fn from(s: &'static str) -> Self { - Bech32Hrp::from_static(s) +impl From<&str> for Bech32Hrp { + fn from(s: &str) -> Self { + Bech32Hrp::parse_unchecked(s) } } impl PartialEq for Bech32Hrp { fn eq(&self, other: &str) -> bool { - self.0.as_ref() == other + self.0.as_str() == other } } impl PartialEq<&str> for Bech32Hrp { fn eq(&self, other: &&str) -> bool { - self.0.as_ref() == *other + self.0.as_str() == *other } } impl core::fmt::Display for Bech32Hrp { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(&self.0) + self.0.fmt(f) } } impl core::fmt::Debug for Bech32Hrp { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("Bech32Hrp").field(&self.0).finish() + f.debug_tuple("Bech32Hrp").field(&self.0.as_str()).finish() + } +} + +impl Serialize for Bech32Hrp { + fn serialize(&self, serializer: S) -> Result { + self.as_str().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Bech32Hrp { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Hrp::parse(&s) + .map(Bech32Hrp) + .map_err(|e| serde::de::Error::custom(format!("invalid HRP: {e}"))) } } diff --git a/chain/core/src/std/new_address.rs b/chain/core/src/std/new_address.rs index 0e7eebe5a5..bdbd49482b 100644 --- a/chain/core/src/std/new_address.rs +++ b/chain/core/src/std/new_address.rs @@ -58,7 +58,7 @@ pub fn compute_new_address_bech32( deployment_nonce: u64, ) -> Bech32Address { let address = compute_new_address(&deployer.address, deployment_nonce); - Bech32Address::encode_address(deployer.hrp.as_str(), address) + Bech32Address::encode_address(deployer.hrp, address) } /// Generates a mock smart contract address deterministically from a creator address and nonce. diff --git a/chain/core/src/types/address.rs b/chain/core/src/types/address.rs index 515a1d757a..407c0c3ab7 100644 --- a/chain/core/src/types/address.rs +++ b/chain/core/src/types/address.rs @@ -48,7 +48,7 @@ impl Address { /// Encodes the address as a bech32 string using the given human-readable part (HRP). #[cfg(feature = "std")] - pub fn to_bech32(&self, hrp: &str) -> crate::std::Bech32Address { + pub fn to_bech32(&self, hrp: crate::std::Bech32Hrp) -> crate::std::Bech32Address { crate::std::Bech32Address::encode_address(hrp, self.clone()) } diff --git a/framework/meta/src/cmd/wallet.rs b/framework/meta/src/cmd/wallet.rs index 60125cb6c9..10157c4bc1 100644 --- a/framework/meta/src/cmd/wallet.rs +++ b/framework/meta/src/cmd/wallet.rs @@ -6,6 +6,7 @@ use crate::cli::{ }; use bip39::{Language, Mnemonic}; use multiversx_sc::types::{self, Address}; +use multiversx_sc_snippets::sdk::chain_core::std::Bech32Hrp; use multiversx_sc_snippets::sdk::{crypto::public_key::PublicKey, wallet::Wallet}; use multiversx_sc_snippets::{hex, imports::Bech32Address}; use std::{ @@ -192,7 +193,11 @@ impl WalletInfo { fn new(new_args: &WalletNewArgs) { let format = new_args.wallet_format.as_deref(); let outfile = new_args.outfile.as_ref(); - let hrp = new_args.hrp.clone().unwrap_or_else(|| "erd".to_string()); + let hrp = new_args + .hrp + .as_deref() + .map(Bech32Hrp::from) + .unwrap_or_default(); let wallet = if let Some(shard) = new_args.shard { WalletInfo::generate_for_shard(shard) @@ -210,13 +215,13 @@ fn new(new_args: &WalletNewArgs) { println!("Mnemonic: {}", mnemonic); println!("Wallet address:"); - println!(" - bech32: {}", address.to_bech32(&hrp)); + println!(" - bech32: {}", address.to_bech32(hrp)); println!(" - hex: 0x{}", address.to_hex()); match format { Some("pem") => { write_resulted_pem( - &hrp, + hrp.as_str(), public_key_str.as_str(), private_key_str.as_str(), outfile, @@ -230,7 +235,7 @@ fn new(new_args: &WalletNewArgs) { let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); let json_result = Wallet::encrypt_keystore( hex_decoded_keys.as_slice(), - &hrp, + hrp.as_str(), &address, &public_key_str, &Wallet::get_keystore_password(), diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 2cfeb24f90..fa4acf5495 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -5,7 +5,10 @@ use multiversx_sc_scenario::{ meta::tools::find_current_workspace, multiversx_sc::types::Address, }; -use multiversx_sdk::gateway::{GatewayAsyncService, NetworkConfigRequest, SetStateAccount}; +use multiversx_sdk::{ + chain_core::std::Bech32Hrp, + gateway::{GatewayAsyncService, NetworkConfigRequest, SetStateAccount}, +}; use super::ExplorerUrl; use std::{ @@ -82,7 +85,7 @@ where address.clone(), Sender { address: address.clone(), - hrp: self.network_config.address_hrp.clone(), + hrp: self.network_config.address_hrp, wallet, current_nonce: None, }, @@ -115,8 +118,8 @@ where self.current_dir.join(INTERACTOR_SET_STATE_PATH) } - pub fn get_hrp(&self) -> &str { - &self.network_config.address_hrp + pub fn get_hrp(&self) -> Bech32Hrp { + self.network_config.address_hrp } pub fn is_registered_wallet(&self, address: &Address) -> bool { diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index de93bcaf01..c5060acaa7 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -20,13 +20,13 @@ where GatewayProxy: GatewayAsyncService, { pub(crate) fn sc_deploy_to_blockchain_tx(&self, sc_deploy_step: &ScDeployStep) -> Transaction { - let hrp = self.network_config.address_hrp.clone(); + let hrp = self.network_config.address_hrp; Transaction { nonce: 0, value: sc_deploy_step.tx.egld_value.value.to_string(), - sender: sc_deploy_step.tx.from.to_address().to_bech32(&hrp), - receiver: Bech32Address::zero(&hrp), + sender: sc_deploy_step.tx.from.to_address().to_bech32(hrp), + receiver: Bech32Address::zero(hrp), gas_price: self.gas_price, gas_limit: sc_deploy_step.tx.gas_limit.value, data: Some(base64_encode(sc_deploy_step.tx.to_tx_data())), diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs index 83684de909..1e5cb2d48f 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs @@ -17,9 +17,9 @@ where GatewayProxy: GatewayAsyncService, { pub async fn perform_sc_query(&mut self, step: &mut ScQueryStep) { - let hrp = self.network_config.address_hrp.clone(); + let hrp = self.network_config.address_hrp; - let sc_address = step.tx.to.to_address().to_bech32(&hrp); + let sc_address = step.tx.to.to_address().to_bech32(hrp); let req = VMQueryInput { sc_address, func_name: step.tx.function.clone(), diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 7f6265a0e8..d90f2248ed 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use crate::sdk::{data::transaction::Transaction, wallet::Wallet}; use multiversx_sc_scenario::{imports::Bech32Address, multiversx_sc::types::Address}; +use multiversx_sdk::chain_core::std::Bech32Hrp; use multiversx_sdk::data::account::Account; use multiversx_sdk::data::esdt::EsdtBalance; use multiversx_sdk::gateway::{ @@ -13,7 +14,7 @@ use crate::InteractorBase; /// A user account that can sign transactions (a pem is present). pub struct Sender { pub address: Address, - pub hrp: String, + pub hrp: Bech32Hrp, pub wallet: Wallet, pub current_nonce: Option, } diff --git a/sdk/core/src/data/network_config.rs b/sdk/core/src/data/network_config.rs index e0158022e3..eafe9d8c8a 100644 --- a/sdk/core/src/data/network_config.rs +++ b/sdk/core/src/data/network_config.rs @@ -1,3 +1,4 @@ +use multiversx_chain_core::std::Bech32Hrp; use serde::{Deserialize, Serialize}; // NetworkConfig holds the network configuration parameters @@ -6,7 +7,7 @@ pub struct NetworkConfig { #[serde(rename = "erd_chain_id")] pub chain_id: String, #[serde(rename = "erd_address_hrp", default = "default_hrp")] - pub address_hrp: String, + pub address_hrp: Bech32Hrp, #[serde(rename = "erd_denomination")] pub denomination: i32, #[serde(rename = "erd_gas_per_data_byte")] @@ -48,6 +49,6 @@ pub struct NetworkConfigResponse { pub data: Option, } -fn default_hrp() -> String { - "erd".to_string() +fn default_hrp() -> Bech32Hrp { + Bech32Hrp::default() } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 153efe11d2..b7fd417c6a 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -355,7 +355,7 @@ impl Wallet { version: KEYSTORE_VERSION, kind: "secretKey".to_string(), address: public_key.to_string(), - bech32: address.to_bech32(hrp).bech32, + bech32: address.to_bech32(hrp.into()).bech32, }; let mut keystore_json: String = serde_json::to_string_pretty(&keystore).unwrap(); @@ -380,7 +380,7 @@ impl Wallet { .collect::>() .join("\n"); - let address_bech32 = Bech32Address::encode_address(hrp, address.clone()); + let address_bech32 = Bech32Address::encode_address(hrp.into(), address.clone()); let pem_content = format!( "-----BEGIN PRIVATE KEY for {address_bech32}-----\n{formatted_key}\n-----END PRIVATE KEY for {address_bech32}-----\n" ); diff --git a/sdk/http/src/gateway_http_proxy/http_tx.rs b/sdk/http/src/gateway_http_proxy/http_tx.rs index 4f556944f4..cba874cf72 100644 --- a/sdk/http/src/gateway_http_proxy/http_tx.rs +++ b/sdk/http/src/gateway_http_proxy/http_tx.rs @@ -52,10 +52,10 @@ impl GatewayHttpProxy { network_configs: &NetworkConfig, ) -> Result { let account = self - .get_account(&address.to_bech32(&network_configs.address_hrp)) + .get_account(&address.to_bech32(network_configs.address_hrp)) .await?; - let address_bech32 = address.to_bech32(&network_configs.address_hrp); + let address_bech32 = address.to_bech32(network_configs.address_hrp); Ok(ArgCreateTransaction { nonce: account.nonce, From 44b41ce908d1596cec35a26c6e9ff71b3008b2e5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 16:01:24 +0300 Subject: [PATCH 003/195] build fix --- sdk/http/examples/sign_tx.rs | 4 ++-- sdk/http/examples/sign_txs.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index b194680798..1008c6fe69 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -19,8 +19,8 @@ async fn main() { let mut unsign_tx = Transaction { nonce: arg.nonce, value: "0".to_string(), - receiver: addr.to_bech32(&network_config.address_hrp), - sender: addr.to_bech32(&network_config.address_hrp), + receiver: addr.to_bech32(network_config.address_hrp), + sender: addr.to_bech32(network_config.address_hrp), gas_price: arg.gas_price, gas_limit: arg.gas_limit, data: arg.data, diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index 13bf767cef..451e0dbca0 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -19,8 +19,8 @@ async fn main() { let mut unsign_tx = Transaction { nonce: arg.nonce, value: "1000000000000000000".to_string(), - receiver: addr.to_bech32(&network_config.address_hrp), - sender: addr.to_bech32(&network_config.address_hrp), + receiver: addr.to_bech32(network_config.address_hrp), + sender: addr.to_bech32(network_config.address_hrp), gas_price: arg.gas_price, gas_limit: arg.gas_limit, data: arg.data, From 5abbeb149f698d955a87d74533ba914d96a99ec3 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 16:25:35 +0300 Subject: [PATCH 004/195] upgrade Rust 1.95 --- .github/workflows/actions.yml | 18 +++++++++--------- .github/workflows/gas-test.yml | 4 ++-- .github/workflows/interactor-tests.yml | 2 +- .github/workflows/sc-meta-ci.yml | 8 ++++---- rust-toolchain.toml | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 7106951ea9..a2bc06dc63 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -20,17 +20,17 @@ jobs: runs-on: ubuntu-latest rust-tests-max: - name: Rust Tests (rustc 1.92) + name: Rust Tests (rustc 1.95) uses: multiversx/mx-sc-actions/.github/workflows/rust-tests.yml@v6.0.1 with: - rust-toolchain: 1.92 + rust-toolchain: 1.95 runs-on: ubuntu-latest setup: name: Setup Environment uses: multiversx/mx-sc-actions/.github/workflows/setup.yml@v6.0.1 with: - rust-toolchain: 1.87 + rust-toolchain: 1.95 runs-on: ubuntu-latest rust-target: wasm32v1-none path-to-sc-meta: framework/meta @@ -42,7 +42,7 @@ jobs: needs: setup uses: multiversx/mx-sc-actions/.github/workflows/build-contracts.yml@v6.0.1 with: - rust-toolchain: 1.87 + rust-toolchain: 1.95 runs-on: ubuntu-latest rust-target: wasm32v1-none @@ -51,7 +51,7 @@ jobs: needs: build-contracts uses: multiversx/mx-sc-actions/.github/workflows/wasm-tests.yml@v6.0.1 with: - rust-toolchain: 1.87 + rust-toolchain: 1.95 runs-on: ubuntu-latest generate-report: @@ -59,7 +59,7 @@ jobs: needs: build-contracts uses: multiversx/mx-sc-actions/.github/workflows/report.yml@v6.0.1 with: - rust-toolchain: 1.87 + rust-toolchain: 1.95 runs-on: ubuntu-latest test-coverage: @@ -68,7 +68,7 @@ jobs: needs: setup uses: multiversx/mx-sc-actions/.github/workflows/coverage.yml@v6.0.1 with: - rust-toolchain: 1.87 + rust-toolchain: 1.95 runs-on: ubuntu-latest coverage-args: "--ignore-filename-regex='meta/src' --ignore-filename-regex='wasm-adapter' --ignore-filename-regex='benchmarks/' --ignore-filename-regex='tests/' --output ./coverage.md" @@ -77,7 +77,7 @@ jobs: needs: setup uses: multiversx/mx-sc-actions/.github/workflows/proxy-compare.yml@v6.0.1 with: - rust-toolchain: 1.87 + rust-toolchain: 1.95 runs-on: ubuntu-latest clippy-check: @@ -87,7 +87,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: 1.87 + toolchain: 1.95 components: clippy - uses: giraffate/clippy-action@v1 env: diff --git a/.github/workflows/gas-test.yml b/.github/workflows/gas-test.yml index a736399405..68d421d615 100644 --- a/.github/workflows/gas-test.yml +++ b/.github/workflows/gas-test.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toolchain: ["1.92", "1.85"] + toolchain: ["1.95", "1.85"] steps: - name: Checkout code uses: actions/checkout@v5 @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toolchain: ["1.92", "1.85"] + toolchain: ["5", "1.85"] steps: - name: Checkout code uses: actions/checkout@v5 diff --git a/.github/workflows/interactor-tests.yml b/.github/workflows/interactor-tests.yml index 2f5c078608..3467e8d538 100644 --- a/.github/workflows/interactor-tests.yml +++ b/.github/workflows/interactor-tests.yml @@ -19,7 +19,7 @@ jobs: - name: Install rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: 1.87 + toolchain: 1.95 - name: Install sc-meta run: cargo install --path framework/meta --locked diff --git a/.github/workflows/sc-meta-ci.yml b/.github/workflows/sc-meta-ci.yml index af43f12ee8..8816b3ff5e 100644 --- a/.github/workflows/sc-meta-ci.yml +++ b/.github/workflows/sc-meta-ci.yml @@ -22,7 +22,7 @@ jobs: - name: Install rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: 1.87 + toolchain: 1.95 target: wasm32v1-none - name: Install prerequisites @@ -77,7 +77,7 @@ jobs: - name: Install rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: 1.87 + toolchain: 1.95 target: wasm32v1-none - name: Install prerequisites @@ -99,7 +99,7 @@ jobs: - name: Install rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: 1.87 + toolchain: 1.95 target: wasm32v1-none - name: Install prerequisites @@ -120,7 +120,7 @@ jobs: - name: Install rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: 1.87 + toolchain: 1.95 - name: Run install debugger test run: cargo test -p multiversx-sc-meta --features install-debugger-test --test install_debugger_test -- test_install_debugger --exact --nocapture diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 79d2099018..4933b3ba17 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.93" +channel = "1.95" From 8b5f11a96083af11f01e358193924554841bff1a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 16:28:51 +0300 Subject: [PATCH 005/195] upgrade Rust 1.95 fix --- .github/workflows/gas-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gas-test.yml b/.github/workflows/gas-test.yml index 68d421d615..b2ab11afde 100644 --- a/.github/workflows/gas-test.yml +++ b/.github/workflows/gas-test.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - toolchain: ["5", "1.85"] + toolchain: ["1.95", "1.85"] steps: - name: Checkout code uses: actions/checkout@v5 @@ -46,7 +46,7 @@ jobs: with: toolchain: ${{ matrix.toolchain }} - - name: Run gas tests (prod) + - name: Run gas tests (experimental) run: | cd contracts/feature-tests/gas-tests cargo test --features wasmer-experimental From 62c512671592722f7f1db94a1370221133b6b847 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 16:56:57 +0300 Subject: [PATCH 006/195] Bech32Hrp - try_from, unit tests --- Cargo.lock | 1 + chain/core/Cargo.toml | 3 + .../core/src/std/bech32_address/bech32_hrp.rs | 91 +++++++++++++++++-- framework/meta/src/cmd/wallet.rs | 2 +- sdk/core/src/wallet.rs | 7 +- 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cd2a6e728..090221d09c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3214,6 +3214,7 @@ dependencies = [ "hex", "multiversx-sc-codec", "serde", + "serde_json", "sha2 0.11.0", "sha3", ] diff --git a/chain/core/Cargo.toml b/chain/core/Cargo.toml index 5db122ede0..5ed695deac 100644 --- a/chain/core/Cargo.toml +++ b/chain/core/Cargo.toml @@ -32,3 +32,6 @@ sha3 = { version = "0.12", optional = true } version = "=0.25.0" path = "../../data/codec" features = ["derive"] + +[dev-dependencies] +serde_json = "1.0" diff --git a/chain/core/src/std/bech32_address/bech32_hrp.rs b/chain/core/src/std/bech32_address/bech32_hrp.rs index 2e967ced93..6d89794d1a 100644 --- a/chain/core/src/std/bech32_address/bech32_hrp.rs +++ b/chain/core/src/std/bech32_address/bech32_hrp.rs @@ -66,15 +66,19 @@ impl From for Bech32Hrp { } } -impl From for Bech32Hrp { - fn from(s: String) -> Self { - Bech32Hrp::from_string(s) +impl TryFrom for Bech32Hrp { + type Error = bech32::primitives::hrp::Error; + + fn try_from(s: String) -> Result { + Hrp::parse(&s).map(Bech32Hrp) } } -impl From<&str> for Bech32Hrp { - fn from(s: &str) -> Self { - Bech32Hrp::parse_unchecked(s) +impl TryFrom<&str> for Bech32Hrp { + type Error = bech32::primitives::hrp::Error; + + fn try_from(s: &str) -> Result { + Hrp::parse(s).map(Bech32Hrp) } } @@ -116,3 +120,78 @@ impl<'de> Deserialize<'de> for Bech32Hrp { .map_err(|e| serde::de::Error::custom(format!("invalid HRP: {e}"))) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_from_str_valid() { + let hrp = Bech32Hrp::try_from("erd").unwrap(); + assert_eq!(hrp.as_str(), "erd"); + + let hrp = Bech32Hrp::try_from("test").unwrap(); + assert_eq!(hrp.as_str(), "test"); + } + + #[test] + fn test_try_from_string_valid() { + let hrp = Bech32Hrp::try_from(String::from("erd")).unwrap(); + assert_eq!(hrp.as_str(), "erd"); + } + + #[test] + fn test_try_from_str_invalid() { + // HRP cannot be empty + assert!(Bech32Hrp::try_from("").is_err()); + // HRP cannot contain non-ASCII characters + assert!(Bech32Hrp::try_from("\u{e9}rd").is_err()); + } + + #[test] + fn test_try_from_string_invalid() { + assert!(Bech32Hrp::try_from(String::from("")).is_err()); + assert!(Bech32Hrp::try_from(String::from("\u{e9}rd")).is_err()); + } + + #[test] + fn test_parse_unchecked() { + let hrp = Bech32Hrp::parse_unchecked("erd"); + assert_eq!(hrp.as_str(), "erd"); + } + + #[test] + fn test_default_is_erd() { + assert_eq!(Bech32Hrp::default().as_str(), "erd"); + } + + #[test] + fn test_partial_eq_str() { + let hrp = Bech32Hrp::try_from("erd").unwrap(); + assert!(hrp == *"erd"); + assert!(hrp == "erd"); + assert!(hrp != *"test"); + assert!(hrp != "test"); + } + + #[test] + fn test_display() { + let hrp = Bech32Hrp::try_from("erd").unwrap(); + assert_eq!(hrp.to_string(), "erd"); + } + + #[test] + fn test_serde_roundtrip() { + let hrp = Bech32Hrp::try_from("erd").unwrap(); + let json = serde_json::to_string(&hrp).unwrap(); + assert_eq!(json, "\"erd\""); + let decoded: Bech32Hrp = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded, hrp); + } + + #[test] + fn test_serde_deserialize_invalid() { + // Non-ASCII characters are not valid in an HRP + assert!(serde_json::from_str::("\"\u{e9}rd\"").is_err()); + } +} diff --git a/framework/meta/src/cmd/wallet.rs b/framework/meta/src/cmd/wallet.rs index 10157c4bc1..00137c7a16 100644 --- a/framework/meta/src/cmd/wallet.rs +++ b/framework/meta/src/cmd/wallet.rs @@ -196,7 +196,7 @@ fn new(new_args: &WalletNewArgs) { let hrp = new_args .hrp .as_deref() - .map(Bech32Hrp::from) + .map(|hrp| Bech32Hrp::try_from(hrp).expect("invalid HRP")) .unwrap_or_default(); let wallet = if let Some(shard) = new_args.shard { diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index b7fd417c6a..0f339b8f0b 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -355,7 +355,9 @@ impl Wallet { version: KEYSTORE_VERSION, kind: "secretKey".to_string(), address: public_key.to_string(), - bech32: address.to_bech32(hrp.into()).bech32, + bech32: address + .to_bech32(hrp.try_into().expect("invalid HRP")) + .bech32, }; let mut keystore_json: String = serde_json::to_string_pretty(&keystore).unwrap(); @@ -380,7 +382,8 @@ impl Wallet { .collect::>() .join("\n"); - let address_bech32 = Bech32Address::encode_address(hrp.into(), address.clone()); + let address_bech32 = + Bech32Address::encode_address(hrp.try_into().expect("invalid HRP"), address.clone()); let pem_content = format!( "-----BEGIN PRIVATE KEY for {address_bech32}-----\n{formatted_key}\n-----END PRIVATE KEY for {address_bech32}-----\n" ); From f6ab9ea886e91854d81546c939876daecb080ce2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 25 May 2026 17:06:18 +0300 Subject: [PATCH 007/195] wallet_cmd file rename --- framework/meta/src/cli/cli_standalone_main.rs | 2 +- framework/meta/src/cmd.rs | 2 +- framework/meta/src/cmd/{wallet.rs => wallet_cmd.rs} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename framework/meta/src/cmd/{wallet.rs => wallet_cmd.rs} (100%) diff --git a/framework/meta/src/cli/cli_standalone_main.rs b/framework/meta/src/cli/cli_standalone_main.rs index 0218e02a7d..acc611aed9 100644 --- a/framework/meta/src/cli/cli_standalone_main.rs +++ b/framework/meta/src/cli/cli_standalone_main.rs @@ -4,7 +4,7 @@ use crate::cmd::data::data_cli; use crate::cmd::retrieve_address::retrieve_address; use crate::cmd::scen_blackbox::scen_blackbox_tool; use crate::cmd::tx::tx_cli; -use crate::cmd::wallet::wallet; +use crate::cmd::wallet_cmd::wallet; use clap::Parser; use crate::cmd::all::call_all_meta; diff --git a/framework/meta/src/cmd.rs b/framework/meta/src/cmd.rs index e01fa4d5d5..ca2c2d2071 100644 --- a/framework/meta/src/cmd.rs +++ b/framework/meta/src/cmd.rs @@ -15,4 +15,4 @@ pub mod test; pub mod test_coverage; pub mod tx; pub mod upgrade; -pub mod wallet; +pub mod wallet_cmd; diff --git a/framework/meta/src/cmd/wallet.rs b/framework/meta/src/cmd/wallet_cmd.rs similarity index 100% rename from framework/meta/src/cmd/wallet.rs rename to framework/meta/src/cmd/wallet_cmd.rs From 2db286e508c87b509892029ec896843f79e144fc Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 26 May 2026 17:02:54 +0300 Subject: [PATCH 008/195] sc-meta wallet convert integration test --- framework/meta/.gitignore | 1 + .../meta/tests/wallet_convert_cli_test.rs | 218 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 framework/meta/tests/wallet_convert_cli_test.rs diff --git a/framework/meta/.gitignore b/framework/meta/.gitignore index 9b12a82495..70153de369 100644 --- a/framework/meta/.gitignore +++ b/framework/meta/.gitignore @@ -1,2 +1,3 @@ tests/cs_tx_cli_test/outfiles tests/cs_tx_cli_test/alice.pem +tests/temp/ diff --git a/framework/meta/tests/wallet_convert_cli_test.rs b/framework/meta/tests/wallet_convert_cli_test.rs new file mode 100644 index 0000000000..4539ee7f60 --- /dev/null +++ b/framework/meta/tests/wallet_convert_cli_test.rs @@ -0,0 +1,218 @@ +use multiversx_sc_meta::cli::{WalletAction, WalletArgs, WalletConvertArgs}; +use multiversx_sc_meta::cmd::wallet_cmd::wallet; +use multiversx_sdk::wallet::Wallet; +use std::{ + fs, + io::Write, + path::PathBuf, + process::{Command, Stdio}, +}; + +/// A well-known mnemonic with deterministic key material, taken from the wallet SDK tests. +const KNOWN_MNEMONIC: &str = "acid twice post genre topic observe valid viable gesture fortune funny dawn around blood enemy page update reduce decline van bundle zebra rookie real"; +/// Expected private key derived from `KNOWN_MNEMONIC` at account 0, index 0. +const KNOWN_PRIVATE_KEY: &str = "0b7966138e80b8f3bb64046f56aea4250fd7bacad6ed214165cea6767fd0bc2c"; +/// Expected public key derived from `KNOWN_MNEMONIC` at account 0, index 0. +const KNOWN_PUBLIC_KEY: &str = "dfefe0453840e5903f2bd519de9b0ed6e9621e57e28ba0b4c1b15115091dd72f"; +/// Expected bech32 address (default "erd" HRP) for the above key pair. +const KNOWN_BECH32_ERD: &str = "erd1mlh7q3fcgrjeq0et65vaaxcw6m5ky8jhu296pdxpk9g32zga6uhsemxx2a"; + +/// Base directory for temporary test artefacts (ignored by git). +const TEMP_DIR: &str = "tests/temp"; + +/// Returns a path inside `tests/temp/` using the given filename. +/// The directory is created on first use. +fn temp_path(filename: &str) -> PathBuf { + let dir = PathBuf::from(TEMP_DIR); + fs::create_dir_all(&dir).expect("failed to create tests/temp"); + dir.join(filename) +} + +/// Silently removes a file if it exists, ignoring errors. +fn remove_if_exists(path: &PathBuf) { + let _ = fs::remove_file(path); +} + +// ── mnemonic → pem ─────────────────────────────────────────────────────────── + +/// Happy path: mnemonic from a file, PEM written to a file. +/// Verifies that the resulting PEM contains exactly the expected private key, +/// public key, and bech32 address. +#[test] +fn test_convert_mnemonic_to_pem_file_in_file_out() { + let mnemonic_file = temp_path("mnemonic_file_in_file_out.txt"); + let output_pem = temp_path("mnemonic_file_in_file_out.pem"); + remove_if_exists(&mnemonic_file); + remove_if_exists(&output_pem); + + fs::write(&mnemonic_file, KNOWN_MNEMONIC).unwrap(); + + wallet(&WalletArgs { + command: WalletAction::Convert(WalletConvertArgs { + from: "mnemonic".to_string(), + to: "pem".to_string(), + infile: Some(mnemonic_file.to_str().unwrap().to_string()), + outfile: Some(output_pem.to_str().unwrap().to_string()), + ..Default::default() + }), + }); + + let (private_key, public_key) = Wallet::get_wallet_keys_pem(&output_pem); + assert_eq!(private_key, KNOWN_PRIVATE_KEY); + assert_eq!(public_key, KNOWN_PUBLIC_KEY); + + let pem_content = fs::read_to_string(&output_pem).unwrap(); + assert!( + pem_content.contains(KNOWN_BECH32_ERD), + "PEM header should contain the bech32 address" + ); + + remove_if_exists(&mnemonic_file); + remove_if_exists(&output_pem); +} + +/// Mnemonic piped via stdin, PEM returned on stdout. +/// Exercises the interactive (no --infile) code path with a controlled stdin. +/// This is the only test that must remain a subprocess invocation, because the +/// code path reads directly from `io::stdin()`. +#[test] +fn test_convert_mnemonic_to_pem_stdin_in_stdout() { + let sc_meta_bin = env!("CARGO_BIN_EXE_sc-meta"); + + let mut child = Command::new(sc_meta_bin) + .args([ + "wallet", + "convert", + "--in-format", + "mnemonic", + "--out-format", + "pem", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn sc-meta"); + + // Write the mnemonic to the child's stdin, then drop to signal EOF. + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(KNOWN_MNEMONIC.as_bytes()) + .expect("failed to write to stdin"); + } + + let output = child.wait_with_output().expect("failed to wait for child"); + assert!(output.status.success()); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("-----BEGIN PRIVATE KEY for")); + assert!(stdout.contains(KNOWN_BECH32_ERD)); + assert!(stdout.contains("-----END PRIVATE KEY for")); +} + +/// Custom --hrp flag changes the bech32 prefix in the PEM header while keeping +/// the underlying key material identical. +#[test] +fn test_convert_mnemonic_to_pem_custom_hrp() { + let custom_hrp = "test"; + let mnemonic_file = temp_path("mnemonic_custom_hrp.txt"); + let output_pem = temp_path("mnemonic_custom_hrp.pem"); + remove_if_exists(&mnemonic_file); + remove_if_exists(&output_pem); + + fs::write(&mnemonic_file, KNOWN_MNEMONIC).unwrap(); + + wallet(&WalletArgs { + command: WalletAction::Convert(WalletConvertArgs { + from: "mnemonic".to_string(), + to: "pem".to_string(), + infile: Some(mnemonic_file.to_str().unwrap().to_string()), + outfile: Some(output_pem.to_str().unwrap().to_string()), + hrp: Some(custom_hrp.to_string()), + ..Default::default() + }), + }); + + let pem_content = fs::read_to_string(&output_pem).unwrap(); + // bech32 separator between HRP and data is always "1" + assert!( + pem_content.contains(&format!("-----BEGIN PRIVATE KEY for {custom_hrp}1")), + "PEM header should use the custom HRP; got:\n{pem_content}" + ); + + // Key material must be the same regardless of the HRP + let (private_key, public_key) = Wallet::get_wallet_keys_pem(&output_pem); + assert_eq!(private_key, KNOWN_PRIVATE_KEY); + assert_eq!(public_key, KNOWN_PUBLIC_KEY); + + remove_if_exists(&mnemonic_file); + remove_if_exists(&output_pem); +} + +// ── unsupported format pair ─────────────────────────────────────────────────── + +/// Any conversion path that is not explicitly handled should return without +/// panicking (it prints "Unsupported conversion" to stdout). +#[test] +fn test_convert_unsupported_format_does_not_panic() { + // mnemonic → keystore-secret is not a supported path; should be a no-op. + wallet(&WalletArgs { + command: WalletAction::Convert(WalletConvertArgs { + from: "mnemonic".to_string(), + to: "keystore-secret".to_string(), + ..Default::default() + }), + }); +} + +// ── missing --infile errors ─────────────────────────────────────────────────── + +/// pem → keystore-secret requires --infile; omitting it must panic with an +/// informative message. +#[test] +fn test_convert_pem_to_keystore_panics_without_infile() { + let result = std::panic::catch_unwind(|| { + wallet(&WalletArgs { + command: WalletAction::Convert(WalletConvertArgs { + from: "pem".to_string(), + to: "keystore-secret".to_string(), + ..Default::default() + }), + }); + }); + let err = result.expect_err("expected a panic when --infile is missing"); + let msg = err + .downcast_ref::<&str>() + .copied() + .or_else(|| err.downcast_ref::().map(String::as_str)) + .unwrap_or(""); + assert!( + msg.contains("Input file is required for pem format"), + "unexpected panic message: {msg}" + ); +} + +/// keystore-secret → pem requires --infile; omitting it must panic with an +/// informative message. +#[test] +fn test_convert_keystore_to_pem_panics_without_infile() { + let result = std::panic::catch_unwind(|| { + wallet(&WalletArgs { + command: WalletAction::Convert(WalletConvertArgs { + from: "keystore-secret".to_string(), + to: "pem".to_string(), + ..Default::default() + }), + }); + }); + let err = result.expect_err("expected a panic when --infile is missing"); + let msg = err + .downcast_ref::<&str>() + .copied() + .or_else(|| err.downcast_ref::().map(String::as_str)) + .unwrap_or(""); + assert!( + msg.contains("Input file is required for keystore-secret format"), + "unexpected panic message: {msg}" + ); +} From 8f1a7a88ca181f2007cfae5fd3607f9df2c1f1ad Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 26 May 2026 17:09:36 +0300 Subject: [PATCH 009/195] clippy fix --- .../derive/src/preprocessing/substitution_map.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/framework/derive/src/preprocessing/substitution_map.rs b/framework/derive/src/preprocessing/substitution_map.rs index 37e5bf8816..bba1bbed57 100644 --- a/framework/derive/src/preprocessing/substitution_map.rs +++ b/framework/derive/src/preprocessing/substitution_map.rs @@ -21,11 +21,19 @@ impl SubstitutionsMap { self.trie.insert(key.into(), value.into()); } + /// Checks whether the beginning of `tt_iter` matches any key in the substitution map. + /// + /// Consumes tokens from `tt_iter` one at a time, building up a candidate key and probing + /// the trie on each step. Stops as soon as there are no more trie descendants for the + /// current prefix (i.e. no longer key can possibly match). + /// + /// Returns the **longest** match found as `Some((length, substitution))`, where `length` + /// is the number of tokens consumed that form the matching key and `substitution` is the + /// corresponding replacement `TokenStream`. Returns `None` if no key matches. pub fn check_subsequence(&self, tt_iter: IntoIter) -> Option<(usize, &TokenStream)> { - let mut current_length: usize = 1; let mut result: Option<(usize, &TokenStream)> = None; let mut current_key = TokenStream::new(); - for tt in tt_iter { + for (current_length, tt) in (1..).zip(tt_iter) { current_key.extend(once(tt)); let trie_key = TrieTokenStream::new(current_key.clone()); if let Some(sub) = self.trie.get(&trie_key) { @@ -34,7 +42,6 @@ impl SubstitutionsMap { if self.trie.get_raw_descendant(&trie_key).is_none() { break; } - current_length += 1; } result } From 8af84ddcd1d8508753079ea229a3099b83f3903e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 26 May 2026 17:29:31 +0300 Subject: [PATCH 010/195] clippy fix --- framework/meta/tests/wallet_convert_cli_test.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/framework/meta/tests/wallet_convert_cli_test.rs b/framework/meta/tests/wallet_convert_cli_test.rs index 4539ee7f60..71beb4e005 100644 --- a/framework/meta/tests/wallet_convert_cli_test.rs +++ b/framework/meta/tests/wallet_convert_cli_test.rs @@ -4,7 +4,7 @@ use multiversx_sdk::wallet::Wallet; use std::{ fs, io::Write, - path::PathBuf, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -29,7 +29,7 @@ fn temp_path(filename: &str) -> PathBuf { } /// Silently removes a file if it exists, ignoring errors. -fn remove_if_exists(path: &PathBuf) { +fn remove_if_exists(path: &Path) { let _ = fs::remove_file(path); } @@ -129,7 +129,6 @@ fn test_convert_mnemonic_to_pem_custom_hrp() { infile: Some(mnemonic_file.to_str().unwrap().to_string()), outfile: Some(output_pem.to_str().unwrap().to_string()), hrp: Some(custom_hrp.to_string()), - ..Default::default() }), }); From ac36e2fe144504a16dc931fa8f44ece7fbe10660 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 10:12:15 +0300 Subject: [PATCH 011/195] Wallet refactor, WalletPem --- .../interact/src/multisig_interact.rs | 4 +- framework/meta/src/cmd/tx/tx_cli_call.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_new.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 2 +- framework/meta/src/cmd/wallet_cmd.rs | 61 +++---- .../meta/tests/wallet_convert_cli_test.rs | 12 +- .../tests/wallet_encrypt_keystore_test.rs | 27 +-- sdk/core/src/crypto/private_key.rs | 63 +++++++ sdk/core/src/wallet.rs | 171 +++++------------- sdk/core/src/wallet/wallet_pem.rs | 85 +++++++++ sdk/core/tests/wallet_test.rs | 9 +- sdk/http/examples/sign_tx.rs | 2 +- sdk/http/examples/sign_txs.rs | 2 +- 14 files changed, 251 insertions(+), 193 deletions(-) create mode 100644 sdk/core/src/wallet/wallet_pem.rs diff --git a/contracts/examples/multisig/interact/src/multisig_interact.rs b/contracts/examples/multisig/interact/src/multisig_interact.rs index 59574edd76..49e806259a 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact.rs @@ -113,8 +113,8 @@ impl MultisigInteract { let dan = test_wallets::dan(); let eve = test_wallets::eve(); - for wallet in &[carol, dan, eve] { - self.interactor.register_wallet(*wallet).await; + for wallet in [carol, dan, eve] { + self.interactor.register_wallet(wallet).await; } } diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 95c9a4f588..0785c6aa89 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -17,7 +17,7 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { // Create the interactor – fetches network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet).await; + let sender_address = interactor.register_wallet(wallet.clone()).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); // Determine nonce. diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index f6c09f3450..d36c8a27f1 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -21,7 +21,7 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet).await; + let sender_address = interactor.register_wallet(wallet.clone()).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); // Determine nonce. diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index ff6dce3f9e..4e218d15bc 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -29,7 +29,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet).await; + let sender_address = interactor.register_wallet(wallet.clone()).await; let sender = sender_address.to_bech32(interactor.get_hrp()); // Determine nonce (explicit override or recalled from network). diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 6f3714fe4b..71bceb7430 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -22,7 +22,7 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { // Create the interactor – this fetches the network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet).await; + let sender_address = interactor.register_wallet(wallet.clone()).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); // Determine nonce. diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 00137c7a16..a571af2516 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -7,7 +7,7 @@ use crate::cli::{ use bip39::{Language, Mnemonic}; use multiversx_sc::types::{self, Address}; use multiversx_sc_snippets::sdk::chain_core::std::Bech32Hrp; -use multiversx_sc_snippets::sdk::{crypto::public_key::PublicKey, wallet::Wallet}; +use multiversx_sc_snippets::sdk::wallet::Wallet; use multiversx_sc_snippets::{hex, imports::Bech32Address}; use std::{ fs::{self, File}, @@ -31,27 +31,26 @@ fn convert(convert_args: &WalletConvertArgs) { let out_format = &convert_args.to; let hrp = convert_args .hrp - .clone() - .unwrap_or_else(|| "erd".to_string()); + .as_deref() + .map(|hrp| Bech32Hrp::try_from(hrp).expect("invalid HRP")) + .unwrap_or_default(); let mut mnemonic_str = String::new(); - let private_key_str: String; - let public_key_str: String; match (in_format.as_str(), out_format.as_str()) { ("mnemonic", "pem") => match infile { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); - (private_key_str, public_key_str) = Wallet::get_wallet_keys_mnemonic(mnemonic_str); - write_resulted_pem(&hrp, &public_key_str, &private_key_str, outfile); + let wallet = Wallet::from_mnemonic_string(mnemonic_str); + write_resulted_pem(hrp, &wallet.private_key_hex(), outfile); } None => { println!( "Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); - (private_key_str, public_key_str) = Wallet::get_wallet_keys_mnemonic(mnemonic_str); - write_resulted_pem(&hrp, &public_key_str, &private_key_str, outfile); + let wallet = Wallet::from_mnemonic_string(mnemonic_str); + write_resulted_pem(hrp, &wallet.private_key_hex(), outfile); } }, ("keystore-secret", "pem") => match infile { @@ -61,10 +60,7 @@ fn convert(convert_args: &WalletConvertArgs) { &Wallet::get_keystore_password(), ) .unwrap(); - private_key_str = private_key.to_string(); - let public_key = PublicKey::from(&private_key); - public_key_str = public_key.to_string(); - write_resulted_pem(&hrp, &public_key_str, &private_key_str, outfile); + write_resulted_pem(hrp, &private_key.to_string(), outfile); } None => { panic!("Input file is required for keystore-secret format"); @@ -72,15 +68,16 @@ fn convert(convert_args: &WalletConvertArgs) { }, ("pem", "keystore-secret") => match infile { Some(file) => { - let pem_decoded_keys = Wallet::get_pem_decoded_content(file); - (private_key_str, public_key_str) = Wallet::get_wallet_keys_pem(file); - - let address = get_wallet_address(&private_key_str); - let hex_decoded_keys = hex::decode(pem_decoded_keys).unwrap(); + let wallet = Wallet::from_pem_file(file).expect("error reading PEM file"); + let private_key_str = wallet.private_key_hex(); + let public_key_str = wallet.public_key_hex(); + let address = wallet.address; + let hex_decoded_keys = + hex::decode(format!("{}{}", private_key_str, public_key_str)).unwrap(); let json_result = Wallet::encrypt_keystore( hex_decoded_keys.as_slice(), - &hrp, + hrp.as_str(), &address, &public_key_str, &Wallet::get_keystore_password(), @@ -97,16 +94,15 @@ fn convert(convert_args: &WalletConvertArgs) { } } -fn write_resulted_pem(hrp: &str, public_key: &str, private_key: &str, outfile: Option<&String>) { - let address = get_wallet_address(private_key); +fn write_resulted_pem(hrp: Bech32Hrp, private_key: &str, outfile: Option<&String>) { + let wallet = Wallet::from_private_key_hex(private_key).unwrap(); + let pem_content = wallet.to_pem(hrp).to_pem_str(); match outfile { Some(outfile) => { - let pem_content = Wallet::generate_pem_content(hrp, &address, private_key, public_key); let mut file = File::create(outfile).unwrap(); file.write_all(pem_content.as_bytes()).unwrap(); } None => { - let pem_content = Wallet::generate_pem_content(hrp, &address, private_key, public_key); print!("{}", pem_content); } } @@ -149,11 +145,6 @@ fn bech32_conversion(bech32_args: &WalletBech32Args) { } } -fn get_wallet_address(private_key: &str) -> Address { - let wallet = Wallet::from_private_key(private_key).unwrap(); - wallet.to_address() -} - pub fn generate_mnemonic() -> Mnemonic { Mnemonic::generate_in(Language::English, 24).unwrap() } @@ -168,9 +159,10 @@ struct WalletInfo { impl WalletInfo { fn generate() -> Self { let mnemonic = generate_mnemonic(); - let (private_key_str, public_key_str) = - Wallet::get_wallet_keys_mnemonic(mnemonic.to_string()); - let address = get_wallet_address(private_key_str.as_str()); + let wallet = Wallet::from_mnemonic_string(mnemonic.to_string()); + let private_key_str = wallet.private_key_hex(); + let public_key_str = wallet.public_key_hex(); + let address = wallet.address; WalletInfo { mnemonic, private_key_str, @@ -220,12 +212,7 @@ fn new(new_args: &WalletNewArgs) { match format { Some("pem") => { - write_resulted_pem( - hrp.as_str(), - public_key_str.as_str(), - private_key_str.as_str(), - outfile, - ); + write_resulted_pem(hrp, private_key_str.as_str(), outfile); if let Some(outfile) = outfile { println!("Wallet saved to '{outfile}'"); } diff --git a/framework/meta/tests/wallet_convert_cli_test.rs b/framework/meta/tests/wallet_convert_cli_test.rs index 71beb4e005..db63f2862f 100644 --- a/framework/meta/tests/wallet_convert_cli_test.rs +++ b/framework/meta/tests/wallet_convert_cli_test.rs @@ -57,9 +57,9 @@ fn test_convert_mnemonic_to_pem_file_in_file_out() { }), }); - let (private_key, public_key) = Wallet::get_wallet_keys_pem(&output_pem); - assert_eq!(private_key, KNOWN_PRIVATE_KEY); - assert_eq!(public_key, KNOWN_PUBLIC_KEY); + let wallet = Wallet::from_pem_file(&output_pem).unwrap(); + assert_eq!(wallet.private_key_hex(), KNOWN_PRIVATE_KEY); + assert_eq!(wallet.public_key_hex(), KNOWN_PUBLIC_KEY); let pem_content = fs::read_to_string(&output_pem).unwrap(); assert!( @@ -140,9 +140,9 @@ fn test_convert_mnemonic_to_pem_custom_hrp() { ); // Key material must be the same regardless of the HRP - let (private_key, public_key) = Wallet::get_wallet_keys_pem(&output_pem); - assert_eq!(private_key, KNOWN_PRIVATE_KEY); - assert_eq!(public_key, KNOWN_PUBLIC_KEY); + let wallet = Wallet::from_pem_file(&output_pem).unwrap(); + assert_eq!(wallet.private_key_hex(), KNOWN_PRIVATE_KEY); + assert_eq!(wallet.public_key_hex(), KNOWN_PUBLIC_KEY); remove_if_exists(&mnemonic_file); remove_if_exists(&output_pem); diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 29d98fd718..559b3acc86 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -1,5 +1,6 @@ +use multiversx_chain_core::std::Bech32Hrp; use multiversx_sc::types::Address; -use multiversx_sdk::{crypto::public_key::PublicKey, wallet::Wallet}; +use multiversx_sdk::wallet::Wallet; use std::fs::{self, File}; use std::io::Write; @@ -12,7 +13,7 @@ const ALICE_PUBLIC_KEY: &str = "0139472eff6886771a982f3083da5d421f24c29181e63888 const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { - let wallet = Wallet::from_private_key(ALICE_PRIVATE_KEY).unwrap(); + let wallet = Wallet::from_private_key_hex(ALICE_PRIVATE_KEY).unwrap(); let address = wallet.to_address(); let concatenated_keys = format!("{}{}", ALICE_PRIVATE_KEY, ALICE_PUBLIC_KEY); @@ -31,33 +32,33 @@ fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { #[test] fn test_wallet_convert_pem_to_keystore() { let _ = create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_1); - let (private_key_pem, _public_key_pem) = Wallet::get_wallet_keys_pem(ALICE_PEM_PATH); + let wallet_pem = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); assert_eq!( Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST_1, KEYSTORE_PASSWORD) .unwrap() .to_string(), - private_key_pem + wallet_pem.private_key_hex() ); fs::remove_file(ALICE_KEYSTORE_PATH_TEST_1).unwrap(); } #[test] fn test_wallet_convert_keystore_to_pem() { - let address = create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_2); + create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_2); let private_key = Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST_2, KEYSTORE_PASSWORD) .unwrap(); - let private_key_str = private_key.to_string(); - let public_key = PublicKey::from(&private_key); - let public_key_str = public_key.to_string(); - - let pem_content = - Wallet::generate_pem_content("erd", &address, &private_key_str, &public_key_str); + let pem_content = Wallet::from_private_key_hex(&private_key.to_string()) + .unwrap() + .to_pem(Bech32Hrp::default()) + .to_pem_str(); write_to_file(&pem_content, ALICE_PEM_PATH_TEST); assert_eq!( - private_key_str, - Wallet::get_wallet_keys_pem(ALICE_PEM_PATH_TEST).0 + private_key.to_string(), + Wallet::from_pem_file(ALICE_PEM_PATH_TEST) + .unwrap() + .private_key_hex() ); fs::remove_file(ALICE_PEM_PATH_TEST).unwrap(); diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 77b00c0eef..6a316ba263 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -3,16 +3,23 @@ use std::fmt::Display; use super::edwards25519::{sc_mul_add, sc_reduce}; use crate::crypto::edwards25519::extended_group_element::ExtendedGroupElement; use anyhow::{Result, anyhow}; +use bip39::Mnemonic; +use hmac::{Hmac, KeyInit, Mac}; +use pbkdf2::pbkdf2; use serde::{ de::{Deserialize, Deserializer}, ser::{Serialize, Serializer}, }; use sha2::{Digest, Sha512}; +use zeroize::Zeroize; pub const PRIVATE_KEY_LENGTH: usize = 64; pub const SIGNATURE_LENGTH: usize = 64; pub const SEED_LENGTH: usize = 32; +const EGLD_COIN_TYPE: u32 = 508; +const HARDENED: u32 = 0x80000000; + #[derive(Copy, Clone, Debug)] pub struct PrivateKey(pub [u8; PRIVATE_KEY_LENGTH]); @@ -61,6 +68,62 @@ impl PrivateKey { PrivateKey::from_bytes(bytes.as_slice()) } + fn seed_from_mnemonic(mnemonic: Mnemonic, password: &str) -> [u8; 64] { + let mut salt = String::with_capacity(8 + password.len()); + salt.push_str("mnemonic"); + salt.push_str(password); + + let mut seed = [0u8; 64]; + + let _ = pbkdf2::>( + mnemonic.to_string().as_bytes(), + salt.as_bytes(), + 2048, + &mut seed, + ); + + salt.zeroize(); + + seed + } + + pub fn from_mnemonic(mnemonic: Mnemonic, account: u32, address_index: u32) -> PrivateKey { + let seed = Self::seed_from_mnemonic(mnemonic, ""); + + let serialized_key_len = 32; + let hardened_child_padding: u8 = 0; + + let mut digest = + Hmac::::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); + digest.update(&seed); + let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); + let mut key = intermediary[..serialized_key_len].to_vec(); + let mut chain_code = intermediary[serialized_key_len..].to_vec(); + + for child_idx in [ + 44 | HARDENED, + EGLD_COIN_TYPE | HARDENED, + account | HARDENED, + HARDENED, + address_index | HARDENED, + ] { + let mut buff = [vec![hardened_child_padding], key.clone()].concat(); + buff.push((child_idx >> 24) as u8); + buff.push((child_idx >> 16) as u8); + buff.push((child_idx >> 8) as u8); + buff.push(child_idx as u8); + + digest = + Hmac::::new_from_slice(&chain_code).expect("HMAC can take key of any size"); + digest.update(&buff); + let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); + key = intermediary[..serialized_key_len].to_vec(); + chain_code = intermediary[serialized_key_len..].to_vec(); + } + + PrivateKey::from_bytes(key.as_slice()).unwrap() + } + /// Currently not in use. /// /// Guarded by feature "wallet-full", to avoid unnecessarily importing `rand`. diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 0f339b8f0b..f601b6e35b 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,3 +1,7 @@ +mod wallet_pem; + +pub use wallet_pem::WalletPem; + use core::str; use std::{ fs::{self}, @@ -10,111 +14,57 @@ use anyhow::Result; use bip39::Mnemonic; use ctr::{Ctr128BE, cipher::StreamCipher}; use hmac::{Hmac, KeyInit, Mac}; -use multiversx_chain_core::{std::Bech32Address, types::Address}; -use pbkdf2::pbkdf2; +use multiversx_chain_core::{ + std::{Bech32Address, Bech32Hrp}, + types::Address, +}; use scrypt::{Params, scrypt}; use serde_json::json; -use sha2::{Digest, Sha256, Sha512}; +use sha2::{Digest, Sha256}; use sha3::Keccak256; -use zeroize::Zeroize; use crate::{ - crypto::{ - private_key::{PRIVATE_KEY_LENGTH, PrivateKey}, - public_key::PublicKey, - }, + crypto::{private_key::PrivateKey, public_key::PublicKey}, data::{keystore::*, transaction::Transaction}, - utils::*, }; -const EGLD_COIN_TYPE: u32 = 508; -const HARDENED: u32 = 0x80000000; const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr"; const KDF_SCRYPT: &str = "scrypt"; -type HmacSha512 = Hmac; type HmacSha256 = Hmac; -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct Wallet { priv_key: PrivateKey, + pub address: Address, + pub hrp: Option, } -impl Wallet { - fn seed_from_mnemonic(mnemonic: Mnemonic, password: &str) -> [u8; 64] { - let mut salt = String::with_capacity(8 + password.len()); - salt.push_str("mnemonic"); - salt.push_str(password); - - let mut seed = [0u8; 64]; - - let _ = pbkdf2::>( - mnemonic.to_string().as_bytes(), - salt.as_bytes(), - 2048, - &mut seed, - ); - - salt.zeroize(); - - seed +impl From for Wallet { + fn from(wallet_pem: WalletPem) -> Self { + Self::from_private_key(wallet_pem.priv_key, Some(wallet_pem.address.hrp)) } +} - pub fn get_private_key_from_mnemonic( - mnemonic: Mnemonic, - account: u32, - address_index: u32, - ) -> PrivateKey { - let seed = Self::seed_from_mnemonic(mnemonic, ""); - - let serialized_key_len = 32; - let hardened_child_padding: u8 = 0; - - let mut digest = - HmacSha512::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); - digest.update(&seed); - let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); - let mut key = intermediary[..serialized_key_len].to_vec(); - let mut chain_code = intermediary[serialized_key_len..].to_vec(); - - for child_idx in [ - 44 | HARDENED, - EGLD_COIN_TYPE | HARDENED, - account | HARDENED, // account - HARDENED, - address_index | HARDENED, // addressIndex - ] { - let mut buff = [vec![hardened_child_padding], key.clone()].concat(); - buff.push((child_idx >> 24) as u8); - buff.push((child_idx >> 16) as u8); - buff.push((child_idx >> 8) as u8); - buff.push(child_idx as u8); - - digest = - HmacSha512::new_from_slice(&chain_code).expect("HMAC can take key of any size"); - digest.update(&buff); - let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); - key = intermediary[..serialized_key_len].to_vec(); - chain_code = intermediary[serialized_key_len..].to_vec(); +impl Wallet { + fn from_private_key(priv_key: PrivateKey, hrp: Option) -> Self { + let address = PublicKey::from(&priv_key).to_address(); + Wallet { + priv_key, + address, + hrp, } - - PrivateKey::from_bytes(key.as_slice()).unwrap() } - pub fn get_wallet_keys_mnemonic(mnemonic_str: String) -> (String, String) { + pub fn from_mnemonic_string(mnemonic_str: String) -> Wallet { let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', "")).unwrap(); - let private_key = Self::get_private_key_from_mnemonic(mnemonic, 0u32, 0u32); - let public_key = PublicKey::from(&private_key); - - let public_key_str: &str = &public_key.to_string(); - let private_key_str: &str = &private_key.to_string(); - - (private_key_str.to_string(), public_key_str.to_string()) + let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32); + Self::from_private_key(private_key, None) } - pub fn from_private_key(priv_key: &str) -> Result { + pub fn from_private_key_hex(priv_key: &str) -> Result { let priv_key = PrivateKey::from_hex_str(priv_key)?; - Ok(Self { priv_key }) + Ok(Self::from_private_key(priv_key, None)) } pub fn from_pem_file

(file_path: P) -> Result @@ -126,11 +76,7 @@ impl Wallet { } pub fn from_pem_file_contents(contents: String) -> Result { - let x = pem::parse(contents)?; - let x = x.contents()[..PRIVATE_KEY_LENGTH].to_vec(); - let priv_key_str = std::str::from_utf8(x.as_slice())?; - let pri_key = PrivateKey::from_hex_str(priv_key_str)?; - Ok(Self { priv_key: pri_key }) + Ok(WalletPem::from_pem_str(&contents)?.into()) } pub fn get_shard(&self) -> u8 { @@ -139,22 +85,6 @@ impl Wallet { address_bytes[address_bytes.len() - 1] % 3 } - pub fn get_pem_decoded_content>(file: P) -> Vec { - let pem_content = fs::read_to_string(file).unwrap(); - let lines: Vec<&str> = pem_content.split('\n').collect(); - let pem_encoded_keys = format!("{}{}{}", lines[1], lines[2], lines[3]); - base64_decode(pem_encoded_keys) - } - - pub fn get_wallet_keys_pem>(file: P) -> (String, String) { - let pem_decoded_keys = Self::get_pem_decoded_content(file); - let (private_key, public_key) = pem_decoded_keys.split_at(pem_decoded_keys.len() / 2); - let private_key_str = String::from_utf8(private_key.to_vec()).unwrap(); - let public_key_str = String::from_utf8(public_key.to_vec()).unwrap(); - - (private_key_str, public_key_str) - } - pub fn from_keystore_secret>( file_path: P, insert_password: InsertPassword, @@ -177,7 +107,7 @@ impl Wallet { let priv_key = PrivateKey::from_hex_str( hex::encode(Self::decrypt_secret_key(decryption_params)).as_str(), )?; - Ok(Self { priv_key }) + Ok(Self::from_private_key(priv_key, None)) } pub fn get_private_key_from_keystore_secret>( @@ -203,7 +133,15 @@ impl Wallet { } pub fn to_address(&self) -> Address { - PublicKey::from(&self.priv_key).to_address() + self.address.clone() + } + + pub fn private_key_hex(&self) -> String { + self.priv_key.to_string() + } + + pub fn public_key_hex(&self) -> String { + PublicKey::from(&self.priv_key).to_string() } pub fn sign_tx(&self, unsign_tx: &Transaction) -> [u8; 64] { @@ -365,29 +303,10 @@ impl Wallet { keystore_json } - pub fn generate_pem_content( - hrp: &str, - address: &Address, - private_key: &str, - public_key: &str, - ) -> String { - let concat_keys = format!("{}{}", private_key, public_key); - let concat_keys_b64 = base64_encode(concat_keys); - - // Split the base64 string into 64-character lines - let formatted_key = concat_keys_b64 - .as_bytes() - .chunks(64) - .map(|chunk| std::str::from_utf8(chunk).unwrap()) - .collect::>() - .join("\n"); - - let address_bech32 = - Bech32Address::encode_address(hrp.try_into().expect("invalid HRP"), address.clone()); - let pem_content = format!( - "-----BEGIN PRIVATE KEY for {address_bech32}-----\n{formatted_key}\n-----END PRIVATE KEY for {address_bech32}-----\n" - ); - - pem_content + pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { + WalletPem { + priv_key: self.priv_key, + address: Bech32Address::encode_address(hrp, self.address.clone()), + } } } diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs new file mode 100644 index 0000000000..964b4f3a73 --- /dev/null +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -0,0 +1,85 @@ +use anyhow::{Result, anyhow}; +use multiversx_chain_core::std::Bech32Address; + +use crate::{ + crypto::{private_key::PrivateKey, public_key::PublicKey}, + utils::{base64_decode, base64_encode}, +}; + +const PEM_BEGIN_PREFIX: &str = "-----BEGIN PRIVATE KEY for "; +const PEM_END_PREFIX: &str = "-----END PRIVATE KEY for "; +const PEM_MARKER_SUFFIX: &str = "-----"; + +/// A wallet loaded from or destined for the PEM format. +/// +/// The address field carries both the 32-byte address and the HRP, so a +/// round-trip through [`WalletPem::from_pem_str`] / [`WalletPem::to_pem_str`] +/// preserves the original human-readable part (e.g. `"erd"` or a custom one). +pub struct WalletPem { + pub priv_key: PrivateKey, + pub address: Bech32Address, +} + +impl WalletPem { + /// Parses a PEM string into a `WalletPem`. + /// + /// The first line must be `-----BEGIN PRIVATE KEY for {bech32}-----`, which + /// supplies the address together with its HRP. The base64-encoded body + /// contains the concatenated private-key hex and public-key hex. + pub fn from_pem_str(pem_str: &str) -> Result { + let mut lines = pem_str.lines(); + + let header = lines.next().ok_or_else(|| anyhow!("PEM string is empty"))?; + let bech32_str = header + .strip_prefix(PEM_BEGIN_PREFIX) + .and_then(|s| s.strip_suffix(PEM_MARKER_SUFFIX)) + .ok_or_else(|| anyhow!("invalid PEM header: {header}"))?; + let address = Bech32Address::from_bech32_str(bech32_str); + + let b64_body: String = lines + .take_while(|line| !line.starts_with(PEM_END_PREFIX)) + .collect(); + + let decoded = base64_decode(b64_body); + let private_key_bytes = &decoded[..decoded.len() / 2]; + let private_key_str = std::str::from_utf8(private_key_bytes) + .map_err(|e| anyhow!("invalid UTF-8 in private key: {e}"))?; + let priv_key = PrivateKey::from_hex_str(private_key_str)?; + + Ok(WalletPem { priv_key, address }) + } + + /// Produces a PEM string from this `WalletPem`. + /// + /// Output format: + /// ```text + /// -----BEGIN PRIVATE KEY for {bech32}----- + /// {base64(private_key_hex + public_key_hex)} + /// -----END PRIVATE KEY for {bech32}----- + /// ``` + pub fn to_pem_str(&self) -> String { + let private_key = self.private_key_hex(); + let public_key = self.public_key_hex(); + let b64 = base64_encode(format!("{private_key}{public_key}")); + + let formatted_key = b64 + .as_bytes() + .chunks(64) + .map(|chunk| std::str::from_utf8(chunk).unwrap()) + .collect::>() + .join("\n"); + + let bech32 = self.address.to_bech32_str(); + format!( + "{PEM_BEGIN_PREFIX}{bech32}{PEM_MARKER_SUFFIX}\n{formatted_key}\n{PEM_END_PREFIX}{bech32}{PEM_MARKER_SUFFIX}\n" + ) + } + + pub fn private_key_hex(&self) -> String { + self.priv_key.to_string() + } + + pub fn public_key_hex(&self) -> String { + PublicKey::from(&self.priv_key).to_string() + } +} diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 693cc2dce6..69a1f92128 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -1,13 +1,16 @@ use bip39::Mnemonic; use multiversx_sdk::test_wallets; -use multiversx_sdk::{crypto::public_key::PublicKey, wallet::Wallet}; +use multiversx_sdk::{ + crypto::{private_key::PrivateKey, public_key::PublicKey}, + wallet::Wallet, +}; #[test] fn test_private_key_from_mnemonic() { let mnemonic: Mnemonic = Mnemonic::parse_normalized("acid twice post genre topic observe valid viable gesture fortune funny dawn around blood enemy page update reduce decline van bundle zebra rookie real").unwrap(); - let private_key = Wallet::get_private_key_from_mnemonic(mnemonic.clone(), 0, 0); + let private_key = PrivateKey::from_mnemonic(mnemonic.clone(), 0, 0); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( @@ -23,7 +26,7 @@ fn test_private_key_from_mnemonic() { address.to_bech32_default().bech32 ); - let private_key = Wallet::get_private_key_from_mnemonic(mnemonic, 0, 1); + let private_key = PrivateKey::from_mnemonic(mnemonic, 0, 1); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index 1008c6fe69..7299fac4a3 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -3,7 +3,7 @@ use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] async fn main() { - let wl = Wallet::from_private_key( + let wl = Wallet::from_private_key_hex( "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", ) .unwrap(); diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index 451e0dbca0..dc359d1de6 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -3,7 +3,7 @@ use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] async fn main() { - let wl = Wallet::from_private_key( + let wl = Wallet::from_private_key_hex( "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", ) .unwrap(); From d7cf9336dbbc4f37fac0770d9fd35c0d7763c561 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 11:46:48 +0300 Subject: [PATCH 012/195] Wallet - keystore refactor --- framework/meta/src/cli/cli_args_sender.rs | 2 +- framework/meta/src/cmd/wallet_cmd.rs | 23 +-- .../tests/wallet_encrypt_keystore_test.rs | 13 +- framework/snippets/src/imports.rs | 4 +- sdk/core/src/data.rs | 1 - sdk/core/src/wallet.rs | 187 ++---------------- sdk/core/src/wallet/keystore.rs | 180 +++++++++++++++++ .../keystore.rs => wallet/keystore_json.rs} | 4 +- 8 files changed, 219 insertions(+), 195 deletions(-) create mode 100644 sdk/core/src/wallet/keystore.rs rename sdk/core/src/{data/keystore.rs => wallet/keystore_json.rs} (95%) diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index 15518b1338..e9f855e2f2 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, anyhow}; use clap::Args; -use multiversx_sc_snippets::sdk::{data::keystore::InsertPassword, wallet::Wallet}; +use multiversx_sc_snippets::sdk::{wallet::InsertPassword, wallet::Wallet}; use std::path::PathBuf; /// Wallet / sender arguments shared by commands that sign transactions. diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index a571af2516..e2f7615dca 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -7,6 +7,7 @@ use crate::cli::{ use bip39::{Language, Mnemonic}; use multiversx_sc::types::{self, Address}; use multiversx_sc_snippets::sdk::chain_core::std::Bech32Hrp; +use multiversx_sc_snippets::sdk::wallet::Keystore; use multiversx_sc_snippets::sdk::wallet::Wallet; use multiversx_sc_snippets::{hex, imports::Bech32Address}; use std::{ @@ -55,11 +56,9 @@ fn convert(convert_args: &WalletConvertArgs) { }, ("keystore-secret", "pem") => match infile { Some(file) => { - let private_key = Wallet::get_private_key_from_keystore_secret( - file, - &Wallet::get_keystore_password(), - ) - .unwrap(); + let private_key = + Keystore::get_private_key_from_file(file, &Wallet::get_keystore_password()) + .unwrap(); write_resulted_pem(hrp, &private_key.to_string(), outfile); } None => { @@ -75,13 +74,14 @@ fn convert(convert_args: &WalletConvertArgs) { let hex_decoded_keys = hex::decode(format!("{}{}", private_key_str, public_key_str)).unwrap(); - let json_result = Wallet::encrypt_keystore( + let json_result = Keystore::encrypt( hex_decoded_keys.as_slice(), - hrp.as_str(), + hrp, &address, &public_key_str, &Wallet::get_keystore_password(), - ); + ) + .to_json_string(); write_resulted_keystore(json_result, outfile); } None => { @@ -220,13 +220,14 @@ fn new(new_args: &WalletNewArgs) { Some("keystore-secret") => { let concatenated_keys = format!("{}{}", private_key_str, public_key_str); let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); - let json_result = Wallet::encrypt_keystore( + let json_result = Keystore::encrypt( hex_decoded_keys.as_slice(), - hrp.as_str(), + hrp, &address, &public_key_str, &Wallet::get_keystore_password(), - ); + ) + .to_json_string(); write_resulted_keystore(json_result, outfile); if let Some(outfile) = outfile { println!("Wallet saved to '{outfile}'"); diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 559b3acc86..5bb43ffe0a 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -1,5 +1,6 @@ use multiversx_chain_core::std::Bech32Hrp; use multiversx_sc::types::Address; +use multiversx_sdk::wallet::Keystore; use multiversx_sdk::wallet::Wallet; use std::fs::{self, File}; use std::io::Write; @@ -18,13 +19,14 @@ fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let concatenated_keys = format!("{}{}", ALICE_PRIVATE_KEY, ALICE_PUBLIC_KEY); let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); - let json_result = Wallet::encrypt_keystore( + let json_result = Keystore::encrypt( hex_decoded_keys.as_slice(), - hrp, + hrp.try_into().expect("invalid HRP"), &address, ALICE_PUBLIC_KEY, KEYSTORE_PASSWORD, - ); + ) + .to_json_string(); write_to_file(&json_result, file); address } @@ -34,7 +36,7 @@ fn test_wallet_convert_pem_to_keystore() { let _ = create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_1); let wallet_pem = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); assert_eq!( - Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST_1, KEYSTORE_PASSWORD) + Keystore::get_private_key_from_file(ALICE_KEYSTORE_PATH_TEST_1, KEYSTORE_PASSWORD) .unwrap() .to_string(), wallet_pem.private_key_hex() @@ -47,8 +49,7 @@ fn test_wallet_convert_keystore_to_pem() { create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_2); let private_key = - Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST_2, KEYSTORE_PASSWORD) - .unwrap(); + Keystore::get_private_key_from_file(ALICE_KEYSTORE_PATH_TEST_2, KEYSTORE_PASSWORD).unwrap(); let pem_content = Wallet::from_private_key_hex(&private_key.to_string()) .unwrap() .to_pem(Bech32Hrp::default()) diff --git a/framework/snippets/src/imports.rs b/framework/snippets/src/imports.rs index 2e531f793f..6623acf63d 100644 --- a/framework/snippets/src/imports.rs +++ b/framework/snippets/src/imports.rs @@ -5,9 +5,7 @@ pub use crate::{ InteractorSimulateGasAsync, SimulateGas, StepBuffer, dns_address_for_name, }; -pub use crate::sdk::{ - data::keystore::InsertPassword, test_wallets, validator::Validator, wallet::Wallet, -}; +pub use crate::sdk::{test_wallets, validator::Validator, wallet::InsertPassword, wallet::Wallet}; pub use env_logger; diff --git a/sdk/core/src/data.rs b/sdk/core/src/data.rs index 16e6830e57..5d9c96f416 100644 --- a/sdk/core/src/data.rs +++ b/sdk/core/src/data.rs @@ -2,7 +2,6 @@ pub mod account; pub mod account_storage; pub mod esdt; pub mod hyperblock; -pub mod keystore; pub mod network_config; pub mod network_economics; pub mod network_status; diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index f601b6e35b..ea54881171 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,38 +1,32 @@ +mod keystore; +mod keystore_json; mod wallet_pem; +pub use keystore::Keystore; +pub use keystore_json::*; pub use wallet_pem::WalletPem; use core::str; use std::{ - fs::{self}, io::{self, Write}, path::Path, }; -use aes::{Aes128, cipher::KeyIvInit}; use anyhow::Result; use bip39::Mnemonic; -use ctr::{Ctr128BE, cipher::StreamCipher}; -use hmac::{Hmac, KeyInit, Mac}; use multiversx_chain_core::{ std::{Bech32Address, Bech32Hrp}, types::Address, }; -use scrypt::{Params, scrypt}; use serde_json::json; -use sha2::{Digest, Sha256}; +use sha2::Digest; use sha3::Keccak256; use crate::{ crypto::{private_key::PrivateKey, public_key::PublicKey}, - data::{keystore::*, transaction::Transaction}, + data::transaction::Transaction, }; -const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr"; -const KDF_SCRYPT: &str = "scrypt"; - -type HmacSha256 = Hmac; - #[derive(Clone, Debug)] pub struct Wallet { priv_key: PrivateKey, @@ -89,41 +83,25 @@ impl Wallet { file_path: P, insert_password: InsertPassword, ) -> Result { + let keystore = Keystore::from_file(&file_path); let decryption_params = match insert_password { InsertPassword::Plaintext(password) => { - Self::validate_keystore_password(&file_path, password.to_string()).unwrap_or_else( - |e| { - panic!("Error: {:?}", e); - }, - ) - } - InsertPassword::StandardInput => { - Self::validate_keystore_password(&file_path, Self::get_keystore_password()) - .unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }) + keystore.validate_password(&password).unwrap_or_else(|e| { + panic!("Error: {:?}", e); + }) } + InsertPassword::StandardInput => keystore + .validate_password(&Self::get_keystore_password()) + .unwrap_or_else(|e| { + panic!("Error: {:?}", e); + }), }; let priv_key = PrivateKey::from_hex_str( - hex::encode(Self::decrypt_secret_key(decryption_params)).as_str(), + hex::encode(Keystore::decrypt_secret_key(decryption_params)).as_str(), )?; Ok(Self::from_private_key(priv_key, None)) } - pub fn get_private_key_from_keystore_secret>( - file_path: P, - password: &str, - ) -> Result { - let decyption_params = Self::validate_keystore_password(file_path, password.to_string()) - .unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }); - let priv_key = PrivateKey::from_hex_str( - hex::encode(Self::decrypt_secret_key(decyption_params)).as_str(), - )?; - Ok(priv_key) - } - #[deprecated( since = "0.54.0", note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address" @@ -170,139 +148,6 @@ impl Wallet { rpassword::read_password().unwrap() } - pub fn validate_keystore_password>( - path: P, - password: String, - ) -> Result { - let json_body = fs::read_to_string(path).unwrap(); - let keystore: Keystore = serde_json::from_str(&json_body).unwrap(); - let ciphertext = hex::decode(&keystore.crypto.ciphertext).unwrap(); - - let cipher = &keystore.crypto.cipher; - if cipher != CIPHER_ALGORITHM_AES_128_CTR { - return Err(WalletError::InvalidCipher); - } - - let iv = hex::decode(&keystore.crypto.cipherparams.iv).unwrap(); - let salt = hex::decode(&keystore.crypto.kdfparams.salt).unwrap(); - let json_mac = hex::decode(&keystore.crypto.mac).unwrap(); - - let kdf = &keystore.crypto.kdf; - if kdf != KDF_SCRYPT { - return Err(WalletError::InvalidKdf); - } - let n = keystore.crypto.kdfparams.n as f64; - let r = keystore.crypto.kdfparams.r as u64; - let p = keystore.crypto.kdfparams.p as u64; - let _dklen = keystore.crypto.kdfparams.dklen as usize; - - let params = Params::new(n.log2() as u8, r as u32, p as u32).unwrap(); - - let mut derived_key = vec![0u8; 32]; - scrypt(password.as_bytes(), &salt, ¶ms, &mut derived_key).unwrap(); - - let derived_key_first_half = derived_key[0..16].to_vec(); - let derived_key_second_half = derived_key[16..32].to_vec(); - - let mut input_mac = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); - input_mac.update(&ciphertext); - let computed_mac = input_mac.finalize().into_bytes(); - - if computed_mac.to_vec() == json_mac { - println!("Password is correct"); - Ok(DecryptionParams { - derived_key_first_half, - iv, - data: ciphertext, - }) - } else { - println!("Password is incorrect"); - Err(WalletError::InvalidPassword) - } - } - - pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> Vec { - let key: &[u8; 16] = decryption_params - .derived_key_first_half - .as_slice() - .try_into() - .unwrap(); - let iv: &[u8; 16] = decryption_params.iv.as_slice().try_into().unwrap(); - let mut cipher = Ctr128BE::::new(key.into(), iv.into()); - let mut decrypted = decryption_params.data.to_vec(); - cipher.apply_keystream(&mut decrypted); - - decrypted - } - - /// Not available in dapps, since it uses randomness to generate the keystore. - /// - /// Only available in the sc-meta standalone CLI. - #[cfg(feature = "wallet-full")] - pub fn encrypt_keystore( - data: &[u8], - hrp: &str, - address: &Address, - public_key: &str, - password: &str, - ) -> String { - use rand::Rng; - - let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); - let mut rand_salt: [u8; 32] = [0u8; 32]; - rand::rng().fill_bytes(&mut rand_salt); - let salt_hex = hex::encode(rand_salt); - - let mut rand_iv: [u8; 16] = [0u8; 16]; - rand::rng().fill_bytes(&mut rand_iv); - let iv_hex = hex::encode(rand_iv); - - let mut derived_key = vec![0u8; 32]; - scrypt(password.as_bytes(), &rand_salt, ¶ms, &mut derived_key).unwrap(); - - let derived_key_first_half = derived_key[0..16].to_vec(); - let derived_key_second_half = derived_key[16..32].to_vec(); - - let decryption_params = DecryptionParams { - derived_key_first_half, - iv: rand_iv.to_vec(), - data: data.to_vec(), - }; - - let ciphertext = Self::decrypt_secret_key(decryption_params); - - let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); - h.update(&ciphertext); - let mac = h.finalize().into_bytes(); - let keystore = Keystore { - crypto: Crypto { - cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), - cipherparams: CryptoParams { iv: iv_hex }, - ciphertext: hex::encode(&ciphertext), - kdf: KDF_SCRYPT.to_string(), - kdfparams: KdfParams { - salt: salt_hex, - n: KDF_N, - r: KDF_R, - p: KDF_P, - dklen: KDF_DKLEN as u32, - }, - mac: hex::encode(mac), - }, - id: uuid::Uuid::new_v4().to_string(), - version: KEYSTORE_VERSION, - kind: "secretKey".to_string(), - address: public_key.to_string(), - bech32: address - .to_bech32(hrp.try_into().expect("invalid HRP")) - .bech32, - }; - - let mut keystore_json: String = serde_json::to_string_pretty(&keystore).unwrap(); - keystore_json.push('\n'); - keystore_json - } - pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { WalletPem { priv_key: self.priv_key, diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs new file mode 100644 index 0000000000..92d61f4fad --- /dev/null +++ b/sdk/core/src/wallet/keystore.rs @@ -0,0 +1,180 @@ +use std::fs; +use std::path::Path; + +use aes::{Aes128, cipher::KeyIvInit}; +use anyhow::Result; +use ctr::{Ctr128BE, cipher::StreamCipher}; +use hmac::{Hmac, KeyInit, Mac}; +use multiversx_chain_core::{std::Bech32Hrp, types::Address}; +use scrypt::{Params, scrypt}; +use sha2::Sha256; + +use crate::crypto::private_key::PrivateKey; + +use super::{ + Crypto, CryptoParams, DecryptionParams, KDF_DKLEN, KDF_N, KDF_P, KDF_R, KEYSTORE_VERSION, + KdfParams, KeystoreError, KeystoreJson, +}; + +const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr"; +const KDF_SCRYPT: &str = "scrypt"; + +type HmacSha256 = Hmac; + +pub struct Keystore { + pub json: KeystoreJson, +} + +impl Keystore { + pub fn from_file>(path: P) -> Self { + let json_body = fs::read_to_string(path).unwrap(); + Keystore { + json: serde_json::from_str(&json_body).unwrap(), + } + } + + pub fn to_json_string(&self) -> String { + let mut s = serde_json::to_string_pretty(&self.json).unwrap(); + s.push('\n'); + s + } + + pub fn validate_password(&self, password: &str) -> Result { + let ciphertext = hex::decode(&self.json.crypto.ciphertext).unwrap(); + + let cipher = &self.json.crypto.cipher; + if cipher != CIPHER_ALGORITHM_AES_128_CTR { + return Err(KeystoreError::InvalidCipher); + } + + let iv = hex::decode(&self.json.crypto.cipherparams.iv).unwrap(); + let salt = hex::decode(&self.json.crypto.kdfparams.salt).unwrap(); + let json_mac = hex::decode(&self.json.crypto.mac).unwrap(); + + let kdf = &self.json.crypto.kdf; + if kdf != KDF_SCRYPT { + return Err(KeystoreError::InvalidKdf); + } + let n = self.json.crypto.kdfparams.n as f64; + let r = self.json.crypto.kdfparams.r as u64; + let p = self.json.crypto.kdfparams.p as u64; + let _dklen = self.json.crypto.kdfparams.dklen as usize; + + let params = Params::new(n.log2() as u8, r as u32, p as u32).unwrap(); + + let mut derived_key = vec![0u8; 32]; + scrypt(password.as_bytes(), &salt, ¶ms, &mut derived_key).unwrap(); + + let derived_key_first_half = derived_key[0..16].to_vec(); + let derived_key_second_half = derived_key[16..32].to_vec(); + + let mut input_mac = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); + input_mac.update(&ciphertext); + let computed_mac = input_mac.finalize().into_bytes(); + + if computed_mac.to_vec() == json_mac { + println!("Password is correct"); + Ok(DecryptionParams { + derived_key_first_half, + iv, + data: ciphertext, + }) + } else { + println!("Password is incorrect"); + Err(KeystoreError::InvalidPassword) + } + } + + pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> Vec { + let key: &[u8; 16] = decryption_params + .derived_key_first_half + .as_slice() + .try_into() + .unwrap(); + let iv: &[u8; 16] = decryption_params.iv.as_slice().try_into().unwrap(); + let mut cipher = Ctr128BE::::new(key.into(), iv.into()); + let mut decrypted = decryption_params.data.to_vec(); + cipher.apply_keystream(&mut decrypted); + + decrypted + } + + pub fn get_private_key_from_file>( + file_path: P, + password: &str, + ) -> Result { + let decryption_params = Self::from_file(file_path) + .validate_password(password) + .unwrap_or_else(|e| { + panic!("Error: {:?}", e); + }); + let priv_key = PrivateKey::from_hex_str( + hex::encode(Self::decrypt_secret_key(decryption_params)).as_str(), + )?; + Ok(priv_key) + } + + /// Not available in dapps, since it uses randomness to generate the keystore. + /// + /// Only available in the sc-meta standalone CLI. + #[cfg(feature = "wallet-full")] + pub fn encrypt( + data: &[u8], + hrp: Bech32Hrp, + address: &Address, + public_key: &str, + password: &str, + ) -> Self { + use rand::Rng; + + let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); + let mut rand_salt: [u8; 32] = [0u8; 32]; + rand::rng().fill_bytes(&mut rand_salt); + let salt_hex = hex::encode(rand_salt); + + let mut rand_iv: [u8; 16] = [0u8; 16]; + rand::rng().fill_bytes(&mut rand_iv); + let iv_hex = hex::encode(rand_iv); + + let mut derived_key = vec![0u8; 32]; + scrypt(password.as_bytes(), &rand_salt, ¶ms, &mut derived_key).unwrap(); + + let derived_key_first_half = derived_key[0..16].to_vec(); + let derived_key_second_half = derived_key[16..32].to_vec(); + + let decryption_params = DecryptionParams { + derived_key_first_half, + iv: rand_iv.to_vec(), + data: data.to_vec(), + }; + + let ciphertext = Self::decrypt_secret_key(decryption_params); + + let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); + h.update(&ciphertext); + let mac = h.finalize().into_bytes(); + Keystore { + json: KeystoreJson { + crypto: Crypto { + cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), + cipherparams: CryptoParams { iv: iv_hex }, + ciphertext: hex::encode(&ciphertext), + kdf: KDF_SCRYPT.to_string(), + kdfparams: KdfParams { + salt: salt_hex, + n: KDF_N, + r: KDF_R, + p: KDF_P, + dklen: KDF_DKLEN as u32, + }, + mac: hex::encode(mac), + }, + id: uuid::Uuid::new_v4().to_string(), + version: KEYSTORE_VERSION, + kind: "secretKey".to_string(), + address: public_key.to_string(), + bech32: address.to_bech32(hrp).bech32, + }, + } + } +} diff --git a/sdk/core/src/data/keystore.rs b/sdk/core/src/wallet/keystore_json.rs similarity index 95% rename from sdk/core/src/data/keystore.rs rename to sdk/core/src/wallet/keystore_json.rs index 31e6b6538a..9aa04ac861 100644 --- a/sdk/core/src/data/keystore.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -7,7 +7,7 @@ pub const KDF_DKLEN: usize = 32; pub const KEYSTORE_VERSION: u32 = 4; #[derive(Debug)] -pub enum WalletError { +pub enum KeystoreError { InvalidPassword, InvalidKdf, InvalidCipher, @@ -44,7 +44,7 @@ pub struct Crypto { } #[derive(Debug, Serialize, Deserialize)] -pub struct Keystore { +pub struct KeystoreJson { pub version: u32, pub kind: String, pub id: String, From 1f102feba5bf4f3401a71321d2bcb1eb2f52fb74 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 12:25:10 +0300 Subject: [PATCH 013/195] Wallet - keystore encrypt refactor, removed feature wallet-full --- Cargo.lock | 4 +- framework/meta/Cargo.toml | 3 +- framework/meta/src/cmd/wallet_cmd.rs | 39 ++++++++++++++-- .../tests/wallet_encrypt_keystore_test.rs | 5 ++- sdk/core/Cargo.toml | 7 --- sdk/core/src/crypto/private_key.rs | 15 ------- sdk/core/src/wallet.rs | 1 + sdk/core/src/wallet/keystore.rs | 45 ++++++++++++------- 8 files changed, 71 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 090221d09c..e65db48041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3374,6 +3374,7 @@ dependencies = [ "multiversx-sc-snippets", "multiversx-sdk", "pathdiff", + "rand 0.10.1", "regex", "reqwest", "ruplacer", @@ -3383,6 +3384,7 @@ dependencies = [ "serial_test", "tokio", "toml", + "uuid", "zip", ] @@ -3480,7 +3482,6 @@ dependencies = [ "multiversx-chain-core", "pbkdf2", "pem", - "rand 0.10.1", "rpassword", "scrypt", "serde", @@ -3488,7 +3489,6 @@ dependencies = [ "serde_repr", "sha2 0.11.0", "sha3", - "uuid", "zeroize", ] diff --git a/framework/meta/Cargo.toml b/framework/meta/Cargo.toml index 184babfadc..931bcba07c 100644 --- a/framework/meta/Cargo.toml +++ b/framework/meta/Cargo.toml @@ -43,6 +43,8 @@ copy_dir = "0.1.2" pathdiff = "0.2.1" common-path = "1.0.0" bip39 = { version = "2.0", features = ["rand"] } +rand = { version = "0.10", default-features = false } +uuid = { version = "1.10", features = ["v4"] } anyhow = "1.0" hex = "0.4.3" base64 = "0.22" @@ -75,7 +77,6 @@ features = ["std"] [dependencies.multiversx-sdk] version = "=0.16.0" path = "../../sdk/core" -features = ["wallet-full"] [dev-dependencies] serial_test = "3.2" diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index e2f7615dca..17436a87ba 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -1,5 +1,8 @@ use core::str; +use multiversx_sdk::crypto::private_key::PrivateKey; +use rand::Rng; + use crate::cli::{ WalletAction, WalletArgs, WalletBech32Args, WalletConvertArgs, WalletNewArgs, WalletTestWalletArgs, @@ -8,6 +11,7 @@ use bip39::{Language, Mnemonic}; use multiversx_sc::types::{self, Address}; use multiversx_sc_snippets::sdk::chain_core::std::Bech32Hrp; use multiversx_sc_snippets::sdk::wallet::Keystore; +use multiversx_sc_snippets::sdk::wallet::KeystoreRandomness; use multiversx_sc_snippets::sdk::wallet::Wallet; use multiversx_sc_snippets::{hex, imports::Bech32Address}; use std::{ @@ -74,12 +78,13 @@ fn convert(convert_args: &WalletConvertArgs) { let hex_decoded_keys = hex::decode(format!("{}{}", private_key_str, public_key_str)).unwrap(); + let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( hex_decoded_keys.as_slice(), - hrp, - &address, + address.to_bech32(hrp), &public_key_str, &Wallet::get_keystore_password(), + randomness, ) .to_json_string(); write_resulted_keystore(json_result, outfile); @@ -120,6 +125,18 @@ fn write_resulted_keystore(json_result: String, outfile: Option<&String>) { } } +pub fn new_keystore_randomness() -> KeystoreRandomness { + let mut salt = [0u8; 32]; + let mut iv = [0u8; 16]; + rand::rng().fill_bytes(&mut salt); + rand::rng().fill_bytes(&mut iv); + KeystoreRandomness { + salt, + iv, + id: uuid::Uuid::new_v4().to_string(), + } +} + fn bech32_conversion(bech32_args: &WalletBech32Args) { let encode_address = bech32_args.hex_address.as_ref(); let decode_address = bech32_args.bech32_address.as_ref(); @@ -220,12 +237,13 @@ fn new(new_args: &WalletNewArgs) { Some("keystore-secret") => { let concatenated_keys = format!("{}{}", private_key_str, public_key_str); let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); + let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( hex_decoded_keys.as_slice(), - hrp, - &address, + address.to_bech32(hrp), &public_key_str, &Wallet::get_keystore_password(), + randomness, ) .to_json_string(); write_resulted_keystore(json_result, outfile); @@ -255,3 +273,16 @@ fn test_wallet_cmd(args: &WalletTestWalletArgs) { file.write_all(pem.as_bytes()).unwrap(); println!("Saved test wallet '{name}' to '{path}'"); } + +/// Currently not in use. +#[allow(unused)] +pub fn generate_random_private_key(r: &mut T) -> PrivateKey +where + T: rand::CryptoRng + rand::Rng, +{ + let mut secret_key = PrivateKey([0u8; 64]); + + r.fill_bytes(&mut secret_key.0); + + secret_key +} diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 5bb43ffe0a..63c0d82a6a 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -1,5 +1,6 @@ use multiversx_chain_core::std::Bech32Hrp; use multiversx_sc::types::Address; +use multiversx_sc_meta::cmd::wallet_cmd::new_keystore_randomness; use multiversx_sdk::wallet::Keystore; use multiversx_sdk::wallet::Wallet; use std::fs::{self, File}; @@ -21,10 +22,10 @@ fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); let json_result = Keystore::encrypt( hex_decoded_keys.as_slice(), - hrp.try_into().expect("invalid HRP"), - &address, + address.to_bech32(hrp.try_into().expect("invalid HRP")), ALICE_PUBLIC_KEY, KEYSTORE_PASSWORD, + new_keystore_randomness(), ) .to_json_string(); write_to_file(&json_result, file); diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index bb3222e55e..4246b61d9b 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -15,9 +15,6 @@ description = "SDK for interacting with the MultiversX blockchain" categories = ["cryptography::cryptocurrencies", "api-bindings"] keywords = ["multiversx", "blockchain", "sdk", "api"] -[features] -wallet-full = ["rand", "uuid"] - [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } @@ -39,10 +36,6 @@ aes = "0.9" ctr = "0.10" rpassword = "~7.3" # rpassword >=7.4 requires Rust 1.88 (let_chains), pin to 7.3.x for Rust 1.85 compat -# Optional dependencies, for wallet-full -rand = { version = "0.10", optional = true, default-features = false } -uuid = { version = "1.10", features = ["v4"], optional = true } - [dependencies.multiversx-chain-core] version = "=0.23.0" features = ["std"] diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 6a316ba263..40f0a85888 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -124,21 +124,6 @@ impl PrivateKey { PrivateKey::from_bytes(key.as_slice()).unwrap() } - /// Currently not in use. - /// - /// Guarded by feature "wallet-full", to avoid unnecessarily importing `rand`. - #[cfg(feature = "wallet-full")] - pub fn generate(r: &mut T) -> PrivateKey - where - T: rand::CryptoRng + rand::Rng, - { - let mut secret_key = PrivateKey([0u8; 64]); - - r.fill_bytes(&mut secret_key.0); - - secret_key - } - pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_LENGTH] { self.0 } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index ea54881171..eb2279f7dd 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -3,6 +3,7 @@ mod keystore_json; mod wallet_pem; pub use keystore::Keystore; +pub use keystore::KeystoreRandomness; pub use keystore_json::*; pub use wallet_pem::WalletPem; diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 92d61f4fad..b8b241273a 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -5,7 +5,7 @@ use aes::{Aes128, cipher::KeyIvInit}; use anyhow::Result; use ctr::{Ctr128BE, cipher::StreamCipher}; use hmac::{Hmac, KeyInit, Mac}; -use multiversx_chain_core::{std::Bech32Hrp, types::Address}; +use multiversx_chain_core::std::Bech32Address; use scrypt::{Params, scrypt}; use sha2::Sha256; @@ -21,6 +21,19 @@ const KDF_SCRYPT: &str = "scrypt"; type HmacSha256 = Hmac; +/// Groups all randomness inputs required to encrypt a keystore. +/// +/// Keeping these separate from [`Keystore::encrypt`] makes the function +/// deterministic and easy to test with fixed values. +pub struct KeystoreRandomness { + pub salt: [u8; 32], + pub iv: [u8; 16], + pub id: String, +} + +/// Wraps a keystore JSON payload and provides encryption/decryption helpers. +/// +/// The `json` field holds the raw [`KeystoreJson`] that is serialised to disk. pub struct Keystore { pub json: KeystoreJson, } @@ -117,34 +130,32 @@ impl Keystore { /// Not available in dapps, since it uses randomness to generate the keystore. /// /// Only available in the sc-meta standalone CLI. - #[cfg(feature = "wallet-full")] pub fn encrypt( data: &[u8], - hrp: Bech32Hrp, - address: &Address, + bech32_address: Bech32Address, public_key: &str, password: &str, + randomness: KeystoreRandomness, ) -> Self { - use rand::Rng; - let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); - let mut rand_salt: [u8; 32] = [0u8; 32]; - rand::rng().fill_bytes(&mut rand_salt); - let salt_hex = hex::encode(rand_salt); - - let mut rand_iv: [u8; 16] = [0u8; 16]; - rand::rng().fill_bytes(&mut rand_iv); - let iv_hex = hex::encode(rand_iv); + let salt_hex = hex::encode(randomness.salt); + let iv_hex = hex::encode(randomness.iv); let mut derived_key = vec![0u8; 32]; - scrypt(password.as_bytes(), &rand_salt, ¶ms, &mut derived_key).unwrap(); + scrypt( + password.as_bytes(), + &randomness.salt, + ¶ms, + &mut derived_key, + ) + .unwrap(); let derived_key_first_half = derived_key[0..16].to_vec(); let derived_key_second_half = derived_key[16..32].to_vec(); let decryption_params = DecryptionParams { derived_key_first_half, - iv: rand_iv.to_vec(), + iv: randomness.iv.to_vec(), data: data.to_vec(), }; @@ -169,11 +180,11 @@ impl Keystore { }, mac: hex::encode(mac), }, - id: uuid::Uuid::new_v4().to_string(), + id: randomness.id, version: KEYSTORE_VERSION, kind: "secretKey".to_string(), address: public_key.to_string(), - bech32: address.to_bech32(hrp).bech32, + bech32: bech32_address.bech32, }, } } From 42e232a4b4be09474074f70da7c4e880065d72d6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 13:51:50 +0300 Subject: [PATCH 014/195] Wallet - keystore password cleanup --- Cargo.lock | 2 +- framework/meta/Cargo.toml | 2 ++ framework/meta/src/cli/cli_args_sender.rs | 20 +++++++++---- framework/meta/src/cmd/wallet_cmd.rs | 8 +++--- framework/snippets/src/imports.rs | 2 +- sdk/core/Cargo.toml | 1 - sdk/core/src/wallet.rs | 34 +---------------------- sdk/core/src/wallet/keystore_json.rs | 6 ---- 8 files changed, 23 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e65db48041..6ef2622305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3377,6 +3377,7 @@ dependencies = [ "rand 0.10.1", "regex", "reqwest", + "rpassword", "ruplacer", "semver", "serde", @@ -3482,7 +3483,6 @@ dependencies = [ "multiversx-chain-core", "pbkdf2", "pem", - "rpassword", "scrypt", "serde", "serde_json", diff --git a/framework/meta/Cargo.toml b/framework/meta/Cargo.toml index 931bcba07c..611829d74a 100644 --- a/framework/meta/Cargo.toml +++ b/framework/meta/Cargo.toml @@ -51,6 +51,8 @@ base64 = "0.22" indexmap = { version = "2", features = ["serde"] } # warning: newer versions require Rust 1.88, hence we are not upgrading yet zip = { version = "7.2", features = ["deflate"], default-features = false } +# rpassword >=7.4 requires Rust 1.88 (let_chains), pin to 7.3.x for Rust 1.85 compat +rpassword = "~7.3" [dependencies.multiversx-sc-meta-lib] version = "=0.66.0" diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index e9f855e2f2..45c72dbab7 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result, anyhow}; use clap::Args; -use multiversx_sc_snippets::sdk::{wallet::InsertPassword, wallet::Wallet}; -use std::path::PathBuf; +use multiversx_sc_snippets::sdk::{wallet::Keystore, wallet::Wallet}; +use std::{io::Write, path::PathBuf}; /// Wallet / sender arguments shared by commands that sign transactions. #[derive(Clone, PartialEq, Eq, Debug, Args)] @@ -24,13 +24,21 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { if let Some(pem) = &sender.pem { Wallet::from_pem_file(pem).context("failed to load PEM wallet") } else if let Some(keyfile) = &sender.keyfile { - let insert_password = match &sender.keystore_password { - Some(pw) => InsertPassword::Plaintext(pw.clone()), - None => InsertPassword::StandardInput, + let password = match &sender.keystore_password { + Some(pw) => pw.clone(), + None => get_keystore_password(), }; - Wallet::from_keystore_secret(keyfile, insert_password) + let priv_key = Keystore::get_private_key_from_file(keyfile, &password) + .context("failed to load keystore wallet")?; + Wallet::from_private_key_hex(&priv_key.to_string()) .context("failed to load keystore wallet") } else { Err(anyhow!("a wallet is required: use --pem or --keyfile")) } } + +pub fn get_keystore_password() -> String { + print!("Insert password: "); + std::io::stdout().flush().unwrap(); + rpassword::read_password().unwrap() +} diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 17436a87ba..9f4953f98c 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -3,6 +3,7 @@ use core::str; use multiversx_sdk::crypto::private_key::PrivateKey; use rand::Rng; +use crate::cli::cli_args_sender::get_keystore_password; use crate::cli::{ WalletAction, WalletArgs, WalletBech32Args, WalletConvertArgs, WalletNewArgs, WalletTestWalletArgs, @@ -61,8 +62,7 @@ fn convert(convert_args: &WalletConvertArgs) { ("keystore-secret", "pem") => match infile { Some(file) => { let private_key = - Keystore::get_private_key_from_file(file, &Wallet::get_keystore_password()) - .unwrap(); + Keystore::get_private_key_from_file(file, &get_keystore_password()).unwrap(); write_resulted_pem(hrp, &private_key.to_string(), outfile); } None => { @@ -83,7 +83,7 @@ fn convert(convert_args: &WalletConvertArgs) { hex_decoded_keys.as_slice(), address.to_bech32(hrp), &public_key_str, - &Wallet::get_keystore_password(), + &get_keystore_password(), randomness, ) .to_json_string(); @@ -242,7 +242,7 @@ fn new(new_args: &WalletNewArgs) { hex_decoded_keys.as_slice(), address.to_bech32(hrp), &public_key_str, - &Wallet::get_keystore_password(), + &get_keystore_password(), randomness, ) .to_json_string(); diff --git a/framework/snippets/src/imports.rs b/framework/snippets/src/imports.rs index 6623acf63d..fcfcf82f4f 100644 --- a/framework/snippets/src/imports.rs +++ b/framework/snippets/src/imports.rs @@ -5,7 +5,7 @@ pub use crate::{ InteractorSimulateGasAsync, SimulateGas, StepBuffer, dns_address_for_name, }; -pub use crate::sdk::{test_wallets, validator::Validator, wallet::InsertPassword, wallet::Wallet}; +pub use crate::sdk::{test_wallets, validator::Validator, wallet::Wallet}; pub use env_logger; diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 4246b61d9b..14b7b8367b 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -34,7 +34,6 @@ log = "0.4" scrypt = { version = "0.12", default-features = false } aes = "0.9" ctr = "0.10" -rpassword = "~7.3" # rpassword >=7.4 requires Rust 1.88 (let_chains), pin to 7.3.x for Rust 1.85 compat [dependencies.multiversx-chain-core] version = "=0.23.0" diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index eb2279f7dd..b0ed58c883 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -8,10 +8,7 @@ pub use keystore_json::*; pub use wallet_pem::WalletPem; use core::str; -use std::{ - io::{self, Write}, - path::Path, -}; +use std::path::Path; use anyhow::Result; use bip39::Mnemonic; @@ -80,29 +77,6 @@ impl Wallet { address_bytes[address_bytes.len() - 1] % 3 } - pub fn from_keystore_secret>( - file_path: P, - insert_password: InsertPassword, - ) -> Result { - let keystore = Keystore::from_file(&file_path); - let decryption_params = match insert_password { - InsertPassword::Plaintext(password) => { - keystore.validate_password(&password).unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }) - } - InsertPassword::StandardInput => keystore - .validate_password(&Self::get_keystore_password()) - .unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }), - }; - let priv_key = PrivateKey::from_hex_str( - hex::encode(Keystore::decrypt_secret_key(decryption_params)).as_str(), - )?; - Ok(Self::from_private_key(priv_key, None)) - } - #[deprecated( since = "0.54.0", note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address" @@ -143,12 +117,6 @@ impl Wallet { self.priv_key.sign(data) } - pub fn get_keystore_password() -> String { - print!("Insert password: "); - io::stdout().flush().unwrap(); - rpassword::read_password().unwrap() - } - pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { WalletPem { priv_key: self.priv_key, diff --git a/sdk/core/src/wallet/keystore_json.rs b/sdk/core/src/wallet/keystore_json.rs index 9aa04ac861..13f13b0266 100644 --- a/sdk/core/src/wallet/keystore_json.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -13,12 +13,6 @@ pub enum KeystoreError { InvalidCipher, } -#[derive(Debug)] -pub enum InsertPassword { - Plaintext(String), - StandardInput, -} - #[derive(Debug, Serialize, Deserialize)] pub struct CryptoParams { pub iv: String, From 785b2e5489784f9265650071ca4eb48008264a38 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 14:59:10 +0300 Subject: [PATCH 015/195] Wallet keystore encrypt/decrypt cleanup --- framework/meta/src/cli/cli_args_sender.rs | 3 +- framework/meta/src/cmd/wallet_cmd.rs | 5 +- .../tests/wallet_encrypt_keystore_test.rs | 8 +- sdk/core/src/wallet.rs | 2 + sdk/core/src/wallet/keystore.rs | 102 +++++++----------- sdk/core/src/wallet/keystore_error.rs | 34 ++++++ sdk/core/src/wallet/keystore_json.rs | 20 ---- 7 files changed, 87 insertions(+), 87 deletions(-) create mode 100644 sdk/core/src/wallet/keystore_error.rs diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index 45c72dbab7..d3cc674b35 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -28,7 +28,8 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { Some(pw) => pw.clone(), None => get_keystore_password(), }; - let priv_key = Keystore::get_private_key_from_file(keyfile, &password) + let priv_key = Keystore::from_file(keyfile) + .extract_private_key(&password) .context("failed to load keystore wallet")?; Wallet::from_private_key_hex(&priv_key.to_string()) .context("failed to load keystore wallet") diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 9f4953f98c..4a67f62242 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -61,8 +61,9 @@ fn convert(convert_args: &WalletConvertArgs) { }, ("keystore-secret", "pem") => match infile { Some(file) => { - let private_key = - Keystore::get_private_key_from_file(file, &get_keystore_password()).unwrap(); + let private_key = Keystore::from_file(file) + .extract_private_key(&get_keystore_password()) + .unwrap(); write_resulted_pem(hrp, &private_key.to_string(), outfile); } None => { diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 63c0d82a6a..606e34b47d 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -37,7 +37,8 @@ fn test_wallet_convert_pem_to_keystore() { let _ = create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_1); let wallet_pem = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); assert_eq!( - Keystore::get_private_key_from_file(ALICE_KEYSTORE_PATH_TEST_1, KEYSTORE_PASSWORD) + Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_1) + .extract_private_key(KEYSTORE_PASSWORD) .unwrap() .to_string(), wallet_pem.private_key_hex() @@ -49,8 +50,9 @@ fn test_wallet_convert_pem_to_keystore() { fn test_wallet_convert_keystore_to_pem() { create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_2); - let private_key = - Keystore::get_private_key_from_file(ALICE_KEYSTORE_PATH_TEST_2, KEYSTORE_PASSWORD).unwrap(); + let private_key = Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_2) + .extract_private_key(KEYSTORE_PASSWORD) + .unwrap(); let pem_content = Wallet::from_private_key_hex(&private_key.to_string()) .unwrap() .to_pem(Bech32Hrp::default()) diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index b0ed58c883..3ee2513eaf 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,9 +1,11 @@ mod keystore; +mod keystore_error; mod keystore_json; mod wallet_pem; pub use keystore::Keystore; pub use keystore::KeystoreRandomness; +pub use keystore_error::KeystoreError; pub use keystore_json::*; pub use wallet_pem::WalletPem; diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index b8b241273a..1e9d050ae9 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -2,7 +2,6 @@ use std::fs; use std::path::Path; use aes::{Aes128, cipher::KeyIvInit}; -use anyhow::Result; use ctr::{Ctr128BE, cipher::StreamCipher}; use hmac::{Hmac, KeyInit, Mac}; use multiversx_chain_core::std::Bech32Address; @@ -11,13 +10,16 @@ use sha2::Sha256; use crate::crypto::private_key::PrivateKey; -use super::{ - Crypto, CryptoParams, DecryptionParams, KDF_DKLEN, KDF_N, KDF_P, KDF_R, KEYSTORE_VERSION, - KdfParams, KeystoreError, KeystoreJson, -}; +use super::{Crypto, CryptoParams, KdfParams, KeystoreError, KeystoreJson}; +const KDF_N: u32 = 4096; +const KDF_R: u32 = 8; +const KDF_P: u32 = 1; +const KDF_DKLEN: usize = 32; +const KEYSTORE_VERSION: u32 = 4; const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr"; const KDF_SCRYPT: &str = "scrypt"; +const KIND_SECRET_KEY: &str = "secretKey"; type HmacSha256 = Hmac; @@ -52,79 +54,53 @@ impl Keystore { s } - pub fn validate_password(&self, password: &str) -> Result { - let ciphertext = hex::decode(&self.json.crypto.ciphertext).unwrap(); + pub fn extract_private_key(&self, password: &str) -> Result { + let ciphertext = hex::decode(&self.json.crypto.ciphertext)?; let cipher = &self.json.crypto.cipher; if cipher != CIPHER_ALGORITHM_AES_128_CTR { return Err(KeystoreError::InvalidCipher); } - let iv = hex::decode(&self.json.crypto.cipherparams.iv).unwrap(); - let salt = hex::decode(&self.json.crypto.kdfparams.salt).unwrap(); - let json_mac = hex::decode(&self.json.crypto.mac).unwrap(); - let kdf = &self.json.crypto.kdf; if kdf != KDF_SCRYPT { return Err(KeystoreError::InvalidKdf); } + + let iv_bytes = hex::decode(&self.json.crypto.cipherparams.iv)?; + let iv: [u8; 16] = iv_bytes + .as_slice() + .try_into() + .map_err(|e: std::array::TryFromSliceError| KeystoreError::Other(e.into()))?; + let salt = hex::decode(&self.json.crypto.kdfparams.salt)?; + let json_mac = hex::decode(&self.json.crypto.mac)?; + let n = self.json.crypto.kdfparams.n as f64; let r = self.json.crypto.kdfparams.r as u64; let p = self.json.crypto.kdfparams.p as u64; let _dklen = self.json.crypto.kdfparams.dklen as usize; - let params = Params::new(n.log2() as u8, r as u32, p as u32).unwrap(); + let params = Params::new(n.log2() as u8, r as u32, p as u32) + .map_err(|e| KeystoreError::Other(e.into()))?; let mut derived_key = vec![0u8; 32]; scrypt(password.as_bytes(), &salt, ¶ms, &mut derived_key).unwrap(); - let derived_key_first_half = derived_key[0..16].to_vec(); - let derived_key_second_half = derived_key[16..32].to_vec(); + let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); + let derived_key_second_half = &derived_key[16..32]; - let mut input_mac = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); + let mut input_mac = HmacSha256::new_from_slice(derived_key_second_half).unwrap(); input_mac.update(&ciphertext); let computed_mac = input_mac.finalize().into_bytes(); - if computed_mac.to_vec() == json_mac { - println!("Password is correct"); - Ok(DecryptionParams { - derived_key_first_half, - iv, - data: ciphertext, - }) - } else { + if computed_mac.to_vec() != json_mac { println!("Password is incorrect"); - Err(KeystoreError::InvalidPassword) + return Err(KeystoreError::InvalidPassword); } - } - pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> Vec { - let key: &[u8; 16] = decryption_params - .derived_key_first_half - .as_slice() - .try_into() - .unwrap(); - let iv: &[u8; 16] = decryption_params.iv.as_slice().try_into().unwrap(); - let mut cipher = Ctr128BE::::new(key.into(), iv.into()); - let mut decrypted = decryption_params.data.to_vec(); - cipher.apply_keystream(&mut decrypted); - - decrypted - } - - pub fn get_private_key_from_file>( - file_path: P, - password: &str, - ) -> Result { - let decryption_params = Self::from_file(file_path) - .validate_password(password) - .unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }); - let priv_key = PrivateKey::from_hex_str( - hex::encode(Self::decrypt_secret_key(decryption_params)).as_str(), - )?; - Ok(priv_key) + println!("Password is correct"); + let private_key_bytes = run_cipher(derived_key_first_half, iv, ciphertext); + PrivateKey::from_bytes(&private_key_bytes).map_err(Into::into) } /// Not available in dapps, since it uses randomness to generate the keystore. @@ -150,16 +126,10 @@ impl Keystore { ) .unwrap(); - let derived_key_first_half = derived_key[0..16].to_vec(); + let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); let derived_key_second_half = derived_key[16..32].to_vec(); - let decryption_params = DecryptionParams { - derived_key_first_half, - iv: randomness.iv.to_vec(), - data: data.to_vec(), - }; - - let ciphertext = Self::decrypt_secret_key(decryption_params); + let ciphertext = run_cipher(derived_key_first_half, randomness.iv, data.to_vec()); let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); h.update(&ciphertext); @@ -182,10 +152,20 @@ impl Keystore { }, id: randomness.id, version: KEYSTORE_VERSION, - kind: "secretKey".to_string(), + kind: KIND_SECRET_KEY.to_string(), address: public_key.to_string(), bech32: bech32_address.bech32, }, } } } + +/// Applies AES-128-CTR to `data` in place and returns the result. +/// +/// AES-128-CTR is a symmetric stream cipher, so the same operation both +/// encrypts plaintext and decrypts ciphertext. +fn run_cipher(key: [u8; 16], iv: [u8; 16], mut data: Vec) -> Vec { + let mut cipher = Ctr128BE::::new((&key).into(), (&iv).into()); + cipher.apply_keystream(&mut data); + data +} diff --git a/sdk/core/src/wallet/keystore_error.rs b/sdk/core/src/wallet/keystore_error.rs new file mode 100644 index 0000000000..1477361cf5 --- /dev/null +++ b/sdk/core/src/wallet/keystore_error.rs @@ -0,0 +1,34 @@ +#[derive(Debug)] +pub enum KeystoreError { + InvalidPassword, + InvalidKdf, + InvalidCipher, + HexDecodeError(hex::FromHexError), + Other(anyhow::Error), +} + +impl std::fmt::Display for KeystoreError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeystoreError::InvalidPassword => write!(f, "invalid password"), + KeystoreError::InvalidKdf => write!(f, "invalid KDF"), + KeystoreError::InvalidCipher => write!(f, "invalid cipher"), + KeystoreError::HexDecodeError(e) => write!(f, "hex decode error: {e}"), + KeystoreError::Other(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for KeystoreError {} + +impl From for KeystoreError { + fn from(e: anyhow::Error) -> Self { + KeystoreError::Other(e) + } +} + +impl From for KeystoreError { + fn from(e: hex::FromHexError) -> Self { + KeystoreError::HexDecodeError(e) + } +} diff --git a/sdk/core/src/wallet/keystore_json.rs b/sdk/core/src/wallet/keystore_json.rs index 13f13b0266..6d7de08c9e 100644 --- a/sdk/core/src/wallet/keystore_json.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -1,18 +1,5 @@ use serde::{Deserialize, Serialize}; -pub const KDF_N: u32 = 4096; -pub const KDF_R: u32 = 8; -pub const KDF_P: u32 = 1; -pub const KDF_DKLEN: usize = 32; -pub const KEYSTORE_VERSION: u32 = 4; - -#[derive(Debug)] -pub enum KeystoreError { - InvalidPassword, - InvalidKdf, - InvalidCipher, -} - #[derive(Debug, Serialize, Deserialize)] pub struct CryptoParams { pub iv: String, @@ -46,10 +33,3 @@ pub struct KeystoreJson { pub bech32: String, pub crypto: Crypto, } - -#[derive(Clone, Debug)] -pub struct DecryptionParams { - pub derived_key_first_half: Vec, - pub iv: Vec, - pub data: Vec, -} From d02870dfaac19133909585f9bd70b8dd864a4681 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 15:20:19 +0300 Subject: [PATCH 016/195] Wallet keystore contains decoded data, json refactor, decode when parsing --- framework/meta/src/cli/cli_args_sender.rs | 2 +- framework/meta/src/cmd/wallet_cmd.rs | 1 + .../tests/wallet_encrypt_keystore_test.rs | 2 + sdk/core/src/wallet/keystore.rs | 108 ++++++++---------- sdk/core/src/wallet/keystore_error.rs | 8 -- sdk/core/src/wallet/keystore_json.rs | 58 ++++++++++ 6 files changed, 110 insertions(+), 69 deletions(-) diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index d3cc674b35..d62e95feb9 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -28,7 +28,7 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { Some(pw) => pw.clone(), None => get_keystore_password(), }; - let priv_key = Keystore::from_file(keyfile) + let priv_key = Keystore::from_file(keyfile)? .extract_private_key(&password) .context("failed to load keystore wallet")?; Wallet::from_private_key_hex(&priv_key.to_string()) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 4a67f62242..77d4c4a639 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -62,6 +62,7 @@ fn convert(convert_args: &WalletConvertArgs) { ("keystore-secret", "pem") => match infile { Some(file) => { let private_key = Keystore::from_file(file) + .unwrap() .extract_private_key(&get_keystore_password()) .unwrap(); write_resulted_pem(hrp, &private_key.to_string(), outfile); diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 606e34b47d..f88e3a5a69 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -38,6 +38,7 @@ fn test_wallet_convert_pem_to_keystore() { let wallet_pem = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); assert_eq!( Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_1) + .unwrap() .extract_private_key(KEYSTORE_PASSWORD) .unwrap() .to_string(), @@ -51,6 +52,7 @@ fn test_wallet_convert_keystore_to_pem() { create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_2); let private_key = Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_2) + .unwrap() .extract_private_key(KEYSTORE_PASSWORD) .unwrap(); let pem_content = Wallet::from_private_key_hex(&private_key.to_string()) diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 1e9d050ae9..24bf77e0b5 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -10,7 +10,7 @@ use sha2::Sha256; use crate::crypto::private_key::PrivateKey; -use super::{Crypto, CryptoParams, KdfParams, KeystoreError, KeystoreJson}; +use super::{KeystoreError, KeystoreJson}; const KDF_N: u32 = 4096; const KDF_R: u32 = 8; @@ -33,73 +33,68 @@ pub struct KeystoreRandomness { pub id: String, } -/// Wraps a keystore JSON payload and provides encryption/decryption helpers. -/// -/// The `json` field holds the raw [`KeystoreJson`] that is serialised to disk. +/// Keystore with all fields decoded from their hex-string representation. pub struct Keystore { - pub json: KeystoreJson, + pub version: u32, + pub kind: String, + pub id: String, + pub address: String, + pub bech32: String, + pub cipher: String, + pub iv: [u8; 16], + pub ciphertext: Vec, + pub kdf: String, + pub salt: Vec, + pub n: u32, + pub r: u32, + pub p: u32, + pub dklen: u32, + pub mac: Vec, } impl Keystore { - pub fn from_file>(path: P) -> Self { - let json_body = fs::read_to_string(path).unwrap(); - Keystore { - json: serde_json::from_str(&json_body).unwrap(), - } + pub fn from_file>(path: P) -> anyhow::Result { + let json_str = fs::read_to_string(path)?; + let json: KeystoreJson = serde_json::from_str(&json_str)?; + Self::from_json(json) } pub fn to_json_string(&self) -> String { - let mut s = serde_json::to_string_pretty(&self.json).unwrap(); + let mut s = serde_json::to_string_pretty(&self.to_json()).unwrap(); s.push('\n'); s } pub fn extract_private_key(&self, password: &str) -> Result { - let ciphertext = hex::decode(&self.json.crypto.ciphertext)?; - - let cipher = &self.json.crypto.cipher; - if cipher != CIPHER_ALGORITHM_AES_128_CTR { + if self.cipher != CIPHER_ALGORITHM_AES_128_CTR { return Err(KeystoreError::InvalidCipher); } - - let kdf = &self.json.crypto.kdf; - if kdf != KDF_SCRYPT { + if self.kdf != KDF_SCRYPT { return Err(KeystoreError::InvalidKdf); } - let iv_bytes = hex::decode(&self.json.crypto.cipherparams.iv)?; - let iv: [u8; 16] = iv_bytes - .as_slice() - .try_into() - .map_err(|e: std::array::TryFromSliceError| KeystoreError::Other(e.into()))?; - let salt = hex::decode(&self.json.crypto.kdfparams.salt)?; - let json_mac = hex::decode(&self.json.crypto.mac)?; - - let n = self.json.crypto.kdfparams.n as f64; - let r = self.json.crypto.kdfparams.r as u64; - let p = self.json.crypto.kdfparams.p as u64; - let _dklen = self.json.crypto.kdfparams.dklen as usize; - - let params = Params::new(n.log2() as u8, r as u32, p as u32) + let n = self.n as f64; + let params = Params::new(n.log2() as u8, self.r, self.p) .map_err(|e| KeystoreError::Other(e.into()))?; let mut derived_key = vec![0u8; 32]; - scrypt(password.as_bytes(), &salt, ¶ms, &mut derived_key).unwrap(); + scrypt(password.as_bytes(), &self.salt, ¶ms, &mut derived_key).unwrap(); let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); let derived_key_second_half = &derived_key[16..32]; let mut input_mac = HmacSha256::new_from_slice(derived_key_second_half).unwrap(); - input_mac.update(&ciphertext); + input_mac.update(&self.ciphertext); let computed_mac = input_mac.finalize().into_bytes(); - if computed_mac.to_vec() != json_mac { + if computed_mac.to_vec() != self.mac { println!("Password is incorrect"); return Err(KeystoreError::InvalidPassword); } println!("Password is correct"); - let private_key_bytes = run_cipher(derived_key_first_half, iv, ciphertext); + let private_key_bytes = + run_cipher(derived_key_first_half, self.iv, self.ciphertext.clone()); PrivateKey::from_bytes(&private_key_bytes).map_err(Into::into) } @@ -114,8 +109,6 @@ impl Keystore { randomness: KeystoreRandomness, ) -> Self { let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); - let salt_hex = hex::encode(randomness.salt); - let iv_hex = hex::encode(randomness.iv); let mut derived_key = vec![0u8; 32]; scrypt( @@ -133,29 +126,24 @@ impl Keystore { let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); h.update(&ciphertext); - let mac = h.finalize().into_bytes(); + let mac = h.finalize().into_bytes().to_vec(); + Keystore { - json: KeystoreJson { - crypto: Crypto { - cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), - cipherparams: CryptoParams { iv: iv_hex }, - ciphertext: hex::encode(&ciphertext), - kdf: KDF_SCRYPT.to_string(), - kdfparams: KdfParams { - salt: salt_hex, - n: KDF_N, - r: KDF_R, - p: KDF_P, - dklen: KDF_DKLEN as u32, - }, - mac: hex::encode(mac), - }, - id: randomness.id, - version: KEYSTORE_VERSION, - kind: KIND_SECRET_KEY.to_string(), - address: public_key.to_string(), - bech32: bech32_address.bech32, - }, + version: KEYSTORE_VERSION, + kind: KIND_SECRET_KEY.to_string(), + id: randomness.id, + address: public_key.to_string(), + bech32: bech32_address.bech32, + cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), + iv: randomness.iv, + ciphertext, + kdf: KDF_SCRYPT.to_string(), + salt: randomness.salt.to_vec(), + n: KDF_N, + r: KDF_R, + p: KDF_P, + dklen: KDF_DKLEN as u32, + mac, } } } diff --git a/sdk/core/src/wallet/keystore_error.rs b/sdk/core/src/wallet/keystore_error.rs index 1477361cf5..4af6470f75 100644 --- a/sdk/core/src/wallet/keystore_error.rs +++ b/sdk/core/src/wallet/keystore_error.rs @@ -3,7 +3,6 @@ pub enum KeystoreError { InvalidPassword, InvalidKdf, InvalidCipher, - HexDecodeError(hex::FromHexError), Other(anyhow::Error), } @@ -13,7 +12,6 @@ impl std::fmt::Display for KeystoreError { KeystoreError::InvalidPassword => write!(f, "invalid password"), KeystoreError::InvalidKdf => write!(f, "invalid KDF"), KeystoreError::InvalidCipher => write!(f, "invalid cipher"), - KeystoreError::HexDecodeError(e) => write!(f, "hex decode error: {e}"), KeystoreError::Other(e) => write!(f, "{e}"), } } @@ -26,9 +24,3 @@ impl From for KeystoreError { KeystoreError::Other(e) } } - -impl From for KeystoreError { - fn from(e: hex::FromHexError) -> Self { - KeystoreError::HexDecodeError(e) - } -} diff --git a/sdk/core/src/wallet/keystore_json.rs b/sdk/core/src/wallet/keystore_json.rs index 6d7de08c9e..9cce70daa3 100644 --- a/sdk/core/src/wallet/keystore_json.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use super::Keystore; + #[derive(Debug, Serialize, Deserialize)] pub struct CryptoParams { pub iv: String, @@ -33,3 +35,59 @@ pub struct KeystoreJson { pub bech32: String, pub crypto: Crypto, } + +impl Keystore { + pub fn from_json(json: KeystoreJson) -> anyhow::Result { + let ciphertext = hex::decode(&json.crypto.ciphertext)?; + let iv_bytes = hex::decode(&json.crypto.cipherparams.iv)?; + let iv: [u8; 16] = iv_bytes + .as_slice() + .try_into() + .map_err(|_: std::array::TryFromSliceError| anyhow::anyhow!("iv must be 16 bytes"))?; + let salt = hex::decode(&json.crypto.kdfparams.salt)?; + let mac = hex::decode(&json.crypto.mac)?; + Ok(Keystore { + version: json.version, + kind: json.kind, + id: json.id, + address: json.address, + bech32: json.bech32, + cipher: json.crypto.cipher, + iv, + ciphertext, + kdf: json.crypto.kdf, + salt, + n: json.crypto.kdfparams.n, + r: json.crypto.kdfparams.r, + p: json.crypto.kdfparams.p, + dklen: json.crypto.kdfparams.dklen, + mac, + }) + } + + pub fn to_json(&self) -> KeystoreJson { + KeystoreJson { + version: self.version, + kind: self.kind.clone(), + id: self.id.clone(), + address: self.address.clone(), + bech32: self.bech32.clone(), + crypto: Crypto { + cipher: self.cipher.clone(), + cipherparams: CryptoParams { + iv: hex::encode(self.iv), + }, + ciphertext: hex::encode(&self.ciphertext), + kdf: self.kdf.clone(), + kdfparams: KdfParams { + salt: hex::encode(&self.salt), + n: self.n, + r: self.r, + p: self.p, + dklen: self.dklen, + }, + mac: hex::encode(&self.mac), + }, + } + } +} From 5edd23cda0af40b9a80b9d58db07ea1d1a548f29 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 15:33:34 +0300 Subject: [PATCH 017/195] Wallet keystore - embed randomness in keystore struct --- sdk/core/src/wallet/keystore.rs | 29 ++++++++++++++++------------ sdk/core/src/wallet/keystore_json.rs | 22 +++++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 24bf77e0b5..d67725a751 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -37,19 +37,17 @@ pub struct KeystoreRandomness { pub struct Keystore { pub version: u32, pub kind: String, - pub id: String, pub address: String, pub bech32: String, pub cipher: String, - pub iv: [u8; 16], pub ciphertext: Vec, pub kdf: String, - pub salt: Vec, pub n: u32, pub r: u32, pub p: u32, pub dklen: u32, pub mac: Vec, + pub randomness: KeystoreRandomness, } impl Keystore { @@ -77,8 +75,14 @@ impl Keystore { let params = Params::new(n.log2() as u8, self.r, self.p) .map_err(|e| KeystoreError::Other(e.into()))?; - let mut derived_key = vec![0u8; 32]; - scrypt(password.as_bytes(), &self.salt, ¶ms, &mut derived_key).unwrap(); + let mut derived_key = [0u8; 32]; + scrypt( + password.as_bytes(), + &self.randomness.salt, + ¶ms, + &mut derived_key, + ) + .unwrap(); let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); let derived_key_second_half = &derived_key[16..32]; @@ -93,8 +97,11 @@ impl Keystore { } println!("Password is correct"); - let private_key_bytes = - run_cipher(derived_key_first_half, self.iv, self.ciphertext.clone()); + let private_key_bytes = run_cipher( + derived_key_first_half, + self.randomness.iv, + self.ciphertext.clone(), + ); PrivateKey::from_bytes(&private_key_bytes).map_err(Into::into) } @@ -110,7 +117,7 @@ impl Keystore { ) -> Self { let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); - let mut derived_key = vec![0u8; 32]; + let mut derived_key = [0u8; 32]; scrypt( password.as_bytes(), &randomness.salt, @@ -120,7 +127,7 @@ impl Keystore { .unwrap(); let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); - let derived_key_second_half = derived_key[16..32].to_vec(); + let derived_key_second_half = &derived_key[16..32]; let ciphertext = run_cipher(derived_key_first_half, randomness.iv, data.to_vec()); @@ -131,19 +138,17 @@ impl Keystore { Keystore { version: KEYSTORE_VERSION, kind: KIND_SECRET_KEY.to_string(), - id: randomness.id, address: public_key.to_string(), bech32: bech32_address.bech32, cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), - iv: randomness.iv, ciphertext, kdf: KDF_SCRYPT.to_string(), - salt: randomness.salt.to_vec(), n: KDF_N, r: KDF_R, p: KDF_P, dklen: KDF_DKLEN as u32, mac, + randomness, } } } diff --git a/sdk/core/src/wallet/keystore_json.rs b/sdk/core/src/wallet/keystore_json.rs index 9cce70daa3..33c62fe441 100644 --- a/sdk/core/src/wallet/keystore_json.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::Keystore; +use super::{Keystore, KeystoreRandomness}; #[derive(Debug, Serialize, Deserialize)] pub struct CryptoParams { @@ -44,24 +44,30 @@ impl Keystore { .as_slice() .try_into() .map_err(|_: std::array::TryFromSliceError| anyhow::anyhow!("iv must be 16 bytes"))?; - let salt = hex::decode(&json.crypto.kdfparams.salt)?; + let salt_bytes = hex::decode(&json.crypto.kdfparams.salt)?; + let salt: [u8; 32] = salt_bytes + .as_slice() + .try_into() + .map_err(|_: std::array::TryFromSliceError| anyhow::anyhow!("salt must be 32 bytes"))?; let mac = hex::decode(&json.crypto.mac)?; Ok(Keystore { version: json.version, kind: json.kind, - id: json.id, address: json.address, bech32: json.bech32, cipher: json.crypto.cipher, - iv, ciphertext, kdf: json.crypto.kdf, - salt, n: json.crypto.kdfparams.n, r: json.crypto.kdfparams.r, p: json.crypto.kdfparams.p, dklen: json.crypto.kdfparams.dklen, mac, + randomness: KeystoreRandomness { + salt, + iv, + id: json.id, + }, }) } @@ -69,18 +75,18 @@ impl Keystore { KeystoreJson { version: self.version, kind: self.kind.clone(), - id: self.id.clone(), + id: self.randomness.id.clone(), address: self.address.clone(), bech32: self.bech32.clone(), crypto: Crypto { cipher: self.cipher.clone(), cipherparams: CryptoParams { - iv: hex::encode(self.iv), + iv: hex::encode(self.randomness.iv), }, ciphertext: hex::encode(&self.ciphertext), kdf: self.kdf.clone(), kdfparams: KdfParams { - salt: hex::encode(&self.salt), + salt: hex::encode(self.randomness.salt), n: self.n, r: self.r, p: self.p, From 43dc92a7983aab04d37ce80398bcb0612675bd04 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 16:12:48 +0300 Subject: [PATCH 018/195] Wallet keystore encrypt simplified, new wallet refactor --- framework/meta/src/cmd/wallet_cmd.rs | 91 +++++++------------ .../tests/wallet_encrypt_keystore_test.rs | 14 +-- sdk/core/src/wallet.rs | 2 +- sdk/core/src/wallet/keystore.rs | 18 ++-- 4 files changed, 52 insertions(+), 73 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 77d4c4a639..8a6d149f06 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -1,23 +1,24 @@ use core::str; -use multiversx_sdk::crypto::private_key::PrivateKey; +use bip39::{Language, Mnemonic}; use rand::Rng; - -use crate::cli::cli_args_sender::get_keystore_password; -use crate::cli::{ - WalletAction, WalletArgs, WalletBech32Args, WalletConvertArgs, WalletNewArgs, - WalletTestWalletArgs, +use std::{ + fs::{self, File}, + io::{self, Read, Write}, }; -use bip39::{Language, Mnemonic}; -use multiversx_sc::types::{self, Address}; + +use multiversx_sc::types::Address; use multiversx_sc_snippets::sdk::chain_core::std::Bech32Hrp; use multiversx_sc_snippets::sdk::wallet::Keystore; use multiversx_sc_snippets::sdk::wallet::KeystoreRandomness; use multiversx_sc_snippets::sdk::wallet::Wallet; use multiversx_sc_snippets::{hex, imports::Bech32Address}; -use std::{ - fs::{self, File}, - io::{self, Read, Write}, +use multiversx_sdk::crypto::private_key::PrivateKey; + +use crate::cli::cli_args_sender::get_keystore_password; +use crate::cli::{ + WalletAction, WalletArgs, WalletBech32Args, WalletConvertArgs, WalletNewArgs, + WalletTestWalletArgs, }; pub fn wallet(args: &WalletArgs) { @@ -74,17 +75,10 @@ fn convert(convert_args: &WalletConvertArgs) { ("pem", "keystore-secret") => match infile { Some(file) => { let wallet = Wallet::from_pem_file(file).expect("error reading PEM file"); - let private_key_str = wallet.private_key_hex(); - let public_key_str = wallet.public_key_hex(); - let address = wallet.address; - let hex_decoded_keys = - hex::decode(format!("{}{}", private_key_str, public_key_str)).unwrap(); - let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - hex_decoded_keys.as_slice(), - address.to_bech32(hrp), - &public_key_str, + wallet.priv_key, + wallet.address.to_bech32(hrp), &get_keystore_password(), randomness, ) @@ -148,7 +142,7 @@ fn bech32_conversion(bech32_args: &WalletBech32Args) { let bytes_from_hex = hex::decode(hex).unwrap(); let bytes_arr: [u8; 32] = bytes_from_hex.try_into().unwrap(); - let addr = types::Address::from(&bytes_arr); + let addr = Address::from(&bytes_arr); let bech32_addr = Bech32Address::from(addr).to_bech32_str().to_string(); println!("{}", bech32_addr); } @@ -168,34 +162,24 @@ pub fn generate_mnemonic() -> Mnemonic { Mnemonic::generate_in(Language::English, 24).unwrap() } -struct WalletInfo { +struct NewWalletInfo { mnemonic: Mnemonic, - private_key_str: String, - public_key_str: String, - address: Address, + wallet: Wallet, } -impl WalletInfo { +impl NewWalletInfo { fn generate() -> Self { let mnemonic = generate_mnemonic(); let wallet = Wallet::from_mnemonic_string(mnemonic.to_string()); - let private_key_str = wallet.private_key_hex(); - let public_key_str = wallet.public_key_hex(); - let address = wallet.address; - WalletInfo { - mnemonic, - private_key_str, - public_key_str, - address, - } + NewWalletInfo { mnemonic, wallet } } fn generate_for_shard(shard: u8) -> Self { assert!(shard < 3, "Shard must be between 0 and 2"); loop { - let wallet = Self::generate(); - if wallet.address.shard_of_3().as_u32() == shard as u32 { - return wallet; + let wallet_info = Self::generate(); + if wallet_info.wallet.address.shard_of_3().as_u32() == shard as u32 { + return wallet_info; } } } @@ -210,40 +194,33 @@ fn new(new_args: &WalletNewArgs) { .map(|hrp| Bech32Hrp::try_from(hrp).expect("invalid HRP")) .unwrap_or_default(); - let wallet = if let Some(shard) = new_args.shard { - WalletInfo::generate_for_shard(shard) + let new_wallet_info = if let Some(shard) = new_args.shard { + NewWalletInfo::generate_for_shard(shard) } else { - WalletInfo::generate() + NewWalletInfo::generate() }; - let WalletInfo { - mnemonic, - private_key_str, - public_key_str, - address, - } = wallet; - - println!("Mnemonic: {}", mnemonic); + println!("Mnemonic: {}", new_wallet_info.mnemonic); println!("Wallet address:"); - println!(" - bech32: {}", address.to_bech32(hrp)); - println!(" - hex: 0x{}", address.to_hex()); + println!( + " - bech32: {}", + new_wallet_info.wallet.address.to_bech32(hrp) + ); + println!(" - hex: 0x{}", new_wallet_info.wallet.address.to_hex()); match format { Some("pem") => { - write_resulted_pem(hrp, private_key_str.as_str(), outfile); + write_resulted_pem(hrp, &new_wallet_info.wallet.private_key_hex(), outfile); if let Some(outfile) = outfile { println!("Wallet saved to '{outfile}'"); } } Some("keystore-secret") => { - let concatenated_keys = format!("{}{}", private_key_str, public_key_str); - let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - hex_decoded_keys.as_slice(), - address.to_bech32(hrp), - &public_key_str, + new_wallet_info.wallet.priv_key, + new_wallet_info.wallet.address.to_bech32(hrp), &get_keystore_password(), randomness, ) diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index f88e3a5a69..9280fe6617 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -11,25 +11,21 @@ const ALICE_KEYSTORE_PATH_TEST_1: &str = "tests/alice1.json"; const ALICE_KEYSTORE_PATH_TEST_2: &str = "tests/alice2.json"; const ALICE_PEM_PATH_TEST: &str = "tests/alice_test.pem"; const KEYSTORE_PASSWORD: &str = "abcd"; -const ALICE_PUBLIC_KEY: &str = "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"; const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let wallet = Wallet::from_private_key_hex(ALICE_PRIVATE_KEY).unwrap(); - let address = wallet.to_address(); - - let concatenated_keys = format!("{}{}", ALICE_PRIVATE_KEY, ALICE_PUBLIC_KEY); - let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); let json_result = Keystore::encrypt( - hex_decoded_keys.as_slice(), - address.to_bech32(hrp.try_into().expect("invalid HRP")), - ALICE_PUBLIC_KEY, + wallet.priv_key, + wallet + .address + .to_bech32(hrp.try_into().expect("invalid HRP")), KEYSTORE_PASSWORD, new_keystore_randomness(), ) .to_json_string(); write_to_file(&json_result, file); - address + wallet.address } #[test] diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 3ee2513eaf..eae77a5102 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -29,7 +29,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct Wallet { - priv_key: PrivateKey, + pub priv_key: PrivateKey, pub address: Address, pub hrp: Option, } diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index d67725a751..e4a876ca9a 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -8,7 +8,7 @@ use multiversx_chain_core::std::Bech32Address; use scrypt::{Params, scrypt}; use sha2::Sha256; -use crate::crypto::private_key::PrivateKey; +use crate::crypto::{private_key::PrivateKey, public_key::PublicKey}; use super::{KeystoreError, KeystoreJson}; @@ -109,12 +109,14 @@ impl Keystore { /// /// Only available in the sc-meta standalone CLI. pub fn encrypt( - data: &[u8], + private_key: PrivateKey, bech32_address: Bech32Address, - public_key: &str, password: &str, randomness: KeystoreRandomness, ) -> Self { + let private_key_bytes = private_key.to_bytes(); + let public_key_hex = PublicKey::from(&private_key).to_string(); + let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); let mut derived_key = [0u8; 32]; @@ -129,16 +131,20 @@ impl Keystore { let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); let derived_key_second_half = &derived_key[16..32]; - let ciphertext = run_cipher(derived_key_first_half, randomness.iv, data.to_vec()); + let ciphertext = run_cipher( + derived_key_first_half, + randomness.iv, + private_key_bytes.to_vec(), + ); - let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); + let mut h = HmacSha256::new_from_slice(derived_key_second_half).unwrap(); h.update(&ciphertext); let mac = h.finalize().into_bytes().to_vec(); Keystore { version: KEYSTORE_VERSION, kind: KIND_SECRET_KEY.to_string(), - address: public_key.to_string(), + address: public_key_hex, bech32: bech32_address.bech32, cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), ciphertext, From 7a361b4b4a9c0ba7e248e8bd63a6938be47111cc Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 16:20:11 +0300 Subject: [PATCH 019/195] Wallet keystore split derived key refactor --- sdk/core/src/wallet/keystore.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index e4a876ca9a..9f607d575e 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -84,10 +84,9 @@ impl Keystore { ) .unwrap(); - let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); - let derived_key_second_half = &derived_key[16..32]; + let (derived_key_first_half, derived_key_second_half) = split_derived_key(&derived_key); - let mut input_mac = HmacSha256::new_from_slice(derived_key_second_half).unwrap(); + let mut input_mac = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); input_mac.update(&self.ciphertext); let computed_mac = input_mac.finalize().into_bytes(); @@ -128,8 +127,7 @@ impl Keystore { ) .unwrap(); - let derived_key_first_half: [u8; 16] = derived_key[0..16].try_into().unwrap(); - let derived_key_second_half = &derived_key[16..32]; + let (derived_key_first_half, derived_key_second_half) = split_derived_key(&derived_key); let ciphertext = run_cipher( derived_key_first_half, @@ -137,7 +135,7 @@ impl Keystore { private_key_bytes.to_vec(), ); - let mut h = HmacSha256::new_from_slice(derived_key_second_half).unwrap(); + let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); h.update(&ciphertext); let mac = h.finalize().into_bytes().to_vec(); @@ -168,3 +166,15 @@ fn run_cipher(key: [u8; 16], iv: [u8; 16], mut data: Vec) -> Vec { cipher.apply_keystream(&mut data); data } + +/// Splits a 32-byte scrypt derived key into two 16-byte halves. +/// +/// The first half is used as the AES-128 cipher key; the second half is +/// used as the HMAC-SHA256 key for MAC verification. +fn split_derived_key(derived_key: &[u8; 32]) -> ([u8; 16], [u8; 16]) { + let mut first_half = [0u8; 16]; + let mut second_half = [0u8; 16]; + first_half.copy_from_slice(&derived_key[..16]); + second_half.copy_from_slice(&derived_key[16..]); + (first_half, second_half) +} From 4a7bedf7e2a43acf2dbae1b419aac7887e5063d4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 16:37:56 +0300 Subject: [PATCH 020/195] Wallet keystore validate password cleanup --- framework/meta/src/cli/cli_args_sender.rs | 3 ++- framework/meta/src/cmd/wallet_cmd.rs | 20 ++++++++++++++------ sdk/core/src/wallet/keystore.rs | 2 -- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index d62e95feb9..c625478e94 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -28,7 +28,8 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { Some(pw) => pw.clone(), None => get_keystore_password(), }; - let priv_key = Keystore::from_file(keyfile)? + let keystore = Keystore::from_file(keyfile)?; + let priv_key = keystore .extract_private_key(&password) .context("failed to load keystore wallet")?; Wallet::from_private_key_hex(&priv_key.to_string()) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 8a6d149f06..db139d0e91 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -13,7 +13,7 @@ use multiversx_sc_snippets::sdk::wallet::Keystore; use multiversx_sc_snippets::sdk::wallet::KeystoreRandomness; use multiversx_sc_snippets::sdk::wallet::Wallet; use multiversx_sc_snippets::{hex, imports::Bech32Address}; -use multiversx_sdk::crypto::private_key::PrivateKey; +use multiversx_sdk::{crypto::private_key::PrivateKey, wallet::KeystoreError}; use crate::cli::cli_args_sender::get_keystore_password; use crate::cli::{ @@ -62,11 +62,19 @@ fn convert(convert_args: &WalletConvertArgs) { }, ("keystore-secret", "pem") => match infile { Some(file) => { - let private_key = Keystore::from_file(file) - .unwrap() - .extract_private_key(&get_keystore_password()) - .unwrap(); - write_resulted_pem(hrp, &private_key.to_string(), outfile); + let keystore = Keystore::from_file(file).expect("keystore parsing error"); + match keystore.extract_private_key(&get_keystore_password()) { + Ok(priv_key) => { + println!("Password is correct"); + write_resulted_pem(hrp, &priv_key.to_string(), outfile); + } + Err(KeystoreError::InvalidPassword) => { + panic!("Password is incorrect"); + } + Err(e) => { + panic!("Failed to extract private key from keystore: {e}"); + } + } } None => { panic!("Input file is required for keystore-secret format"); diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 9f607d575e..8d589cd61d 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -91,11 +91,9 @@ impl Keystore { let computed_mac = input_mac.finalize().into_bytes(); if computed_mac.to_vec() != self.mac { - println!("Password is incorrect"); return Err(KeystoreError::InvalidPassword); } - println!("Password is correct"); let private_key_bytes = run_cipher( derived_key_first_half, self.randomness.iv, From 6aae57ef0b967e29f91a23aeae9271d8afdf76d1 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 17:12:24 +0300 Subject: [PATCH 021/195] Wallet - remove wrong shard id impl --- sdk/core/src/wallet.rs | 6 ------ sdk/core/tests/wallet_test.rs | 10 +++++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index eae77a5102..11caa738d5 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -73,12 +73,6 @@ impl Wallet { Ok(WalletPem::from_pem_str(&contents)?.into()) } - pub fn get_shard(&self) -> u8 { - let address = self.to_address(); - let address_bytes = address.as_bytes(); - address_bytes[address_bytes.len() - 1] % 3 - } - #[deprecated( since = "0.54.0", note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address" diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 69a1f92128..e3f4b5d3e7 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -55,17 +55,17 @@ fn test_load_from_pem() { #[test] fn test_get_shard() { let alice = test_wallets::alice(); // [1, 57, 71, 46, 255, 104, 134, 119, 26, 152, 47, 48, 131, 218, 93, 66, 31, 36, 194, 145, 129, 230, 56, 136, 34, 141, 200, 28, 166, 13, 105, 225] - assert_eq!(0, alice.get_shard()); + assert_eq!(0, alice.address.shard_of_3().as_u32()); let bob = test_wallets::bob(); // [128, 73, 214, 57, 229, 166, 152, 13, 28, 210, 57, 42, 188, 206, 65, 2, 156, 218, 116, 161, 86, 53, 35, 162, 2, 240, 150, 65, 204, 38, 24, 248] - assert_eq!(2, bob.get_shard()); + assert_eq!(2, bob.address.shard_of_3().as_u32()); let carol = test_wallets::carol(); // [178, 161, 21, 85, 206, 82, 30, 73, 68, 224, 154, 177, 117, 73, 216, 91, 72, 125, 205, 38, 200, 75, 80, 23, 163, 158, 49, 163, 103, 8, 137, 186] - assert_eq!(0, carol.get_shard()); + assert_eq!(0, carol.address.shard_of_3().as_u32()); let heidi = test_wallets::heidi(); // [110, 34, 65, 24, 217, 6, 138, 230, 38, 135, 138, 28, 251, 235, 203, 106, 149, 164, 113, 93, 184, 109, 27, 81, 224, 106, 4, 34, 108, 243, 15, 214] - assert_eq!(1, heidi.get_shard()); + assert_eq!(1, heidi.address.shard_of_3().as_u32()); let mike = test_wallets::mike(); // [227, 42, 254, 220, 144, 79, 225, 147, 151, 70, 173, 151, 59, 235, 56, 53, 99, 207, 99, 100, 43, 166, 105, 179, 4, 15, 155, 148, 40, 165, 237, 96] - assert_eq!(0, mike.get_shard()); + assert_eq!(0, mike.address.shard_of_3().as_u32()); } From fbade3eabc07eee0bf7725eaf30250e2268b29e6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 18:09:16 +0300 Subject: [PATCH 022/195] Wallet keystore encrypt only takes HRP --- framework/meta/src/cmd/wallet_cmd.rs | 13 ++++++++----- .../meta/tests/wallet_encrypt_keystore_test.rs | 4 +--- sdk/core/src/wallet/keystore.rs | 13 ++++++------- sdk/core/src/wallet/keystore_json.rs | 9 +++++---- sdk/core/src/wallet/wallet_pem.rs | 11 +++++++++++ 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index db139d0e91..da90b402be 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -13,7 +13,10 @@ use multiversx_sc_snippets::sdk::wallet::Keystore; use multiversx_sc_snippets::sdk::wallet::KeystoreRandomness; use multiversx_sc_snippets::sdk::wallet::Wallet; use multiversx_sc_snippets::{hex, imports::Bech32Address}; -use multiversx_sdk::{crypto::private_key::PrivateKey, wallet::KeystoreError}; +use multiversx_sdk::{ + crypto::private_key::PrivateKey, + wallet::{KeystoreError, WalletPem}, +}; use crate::cli::cli_args_sender::get_keystore_password; use crate::cli::{ @@ -82,11 +85,11 @@ fn convert(convert_args: &WalletConvertArgs) { }, ("pem", "keystore-secret") => match infile { Some(file) => { - let wallet = Wallet::from_pem_file(file).expect("error reading PEM file"); + let wallet_pem = WalletPem::from_pem_file(file).expect("error reading PEM file"); let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - wallet.priv_key, - wallet.address.to_bech32(hrp), + wallet_pem.priv_key, + hrp, &get_keystore_password(), randomness, ) @@ -228,7 +231,7 @@ fn new(new_args: &WalletNewArgs) { let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( new_wallet_info.wallet.priv_key, - new_wallet_info.wallet.address.to_bech32(hrp), + hrp, &get_keystore_password(), randomness, ) diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 9280fe6617..33a7c8e7b1 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -17,9 +17,7 @@ fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let wallet = Wallet::from_private_key_hex(ALICE_PRIVATE_KEY).unwrap(); let json_result = Keystore::encrypt( wallet.priv_key, - wallet - .address - .to_bech32(hrp.try_into().expect("invalid HRP")), + hrp.try_into().expect("invalid HRP"), KEYSTORE_PASSWORD, new_keystore_randomness(), ) diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 8d589cd61d..71c6f915ea 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -4,7 +4,7 @@ use std::path::Path; use aes::{Aes128, cipher::KeyIvInit}; use ctr::{Ctr128BE, cipher::StreamCipher}; use hmac::{Hmac, KeyInit, Mac}; -use multiversx_chain_core::std::Bech32Address; +use multiversx_chain_core::std::{Bech32Address, Bech32Hrp}; use scrypt::{Params, scrypt}; use sha2::Sha256; @@ -37,8 +37,7 @@ pub struct KeystoreRandomness { pub struct Keystore { pub version: u32, pub kind: String, - pub address: String, - pub bech32: String, + pub bech32_address: Bech32Address, pub cipher: String, pub ciphertext: Vec, pub kdf: String, @@ -107,12 +106,13 @@ impl Keystore { /// Only available in the sc-meta standalone CLI. pub fn encrypt( private_key: PrivateKey, - bech32_address: Bech32Address, + hrp: Bech32Hrp, password: &str, randomness: KeystoreRandomness, ) -> Self { + let public_key = PublicKey::from(&private_key); + let bech32_address = public_key.to_address().to_bech32(hrp); let private_key_bytes = private_key.to_bytes(); - let public_key_hex = PublicKey::from(&private_key).to_string(); let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); @@ -140,8 +140,7 @@ impl Keystore { Keystore { version: KEYSTORE_VERSION, kind: KIND_SECRET_KEY.to_string(), - address: public_key_hex, - bech32: bech32_address.bech32, + bech32_address, cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), ciphertext, kdf: KDF_SCRYPT.to_string(), diff --git a/sdk/core/src/wallet/keystore_json.rs b/sdk/core/src/wallet/keystore_json.rs index 33c62fe441..ef099297b0 100644 --- a/sdk/core/src/wallet/keystore_json.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use multiversx_chain_core::std::Bech32Address; + use super::{Keystore, KeystoreRandomness}; #[derive(Debug, Serialize, Deserialize)] @@ -53,8 +55,7 @@ impl Keystore { Ok(Keystore { version: json.version, kind: json.kind, - address: json.address, - bech32: json.bech32, + bech32_address: Bech32Address::from_bech32_str(&json.bech32), cipher: json.crypto.cipher, ciphertext, kdf: json.crypto.kdf, @@ -76,8 +77,8 @@ impl Keystore { version: self.version, kind: self.kind.clone(), id: self.randomness.id.clone(), - address: self.address.clone(), - bech32: self.bech32.clone(), + address: self.bech32_address.address.to_hex(), + bech32: self.bech32_address.bech32.clone(), crypto: Crypto { cipher: self.cipher.clone(), cipherparams: CryptoParams { diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs index 964b4f3a73..9f73f9c745 100644 --- a/sdk/core/src/wallet/wallet_pem.rs +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use anyhow::{Result, anyhow}; use multiversx_chain_core::std::Bech32Address; @@ -49,6 +51,15 @@ impl WalletPem { Ok(WalletPem { priv_key, address }) } + /// Reads a PEM file from disk and parses it with [`WalletPem::from_pem_str`]. + pub fn from_pem_file

(file_path: P) -> Result + where + P: AsRef, + { + let contents = std::fs::read_to_string(file_path)?; + Self::from_pem_str(&contents) + } + /// Produces a PEM string from this `WalletPem`. /// /// Output format: From d74eb607af2c56493b62bb4e82949775893c82b7 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 21:24:25 +0300 Subject: [PATCH 023/195] test fix --- sdk/core/tests/wallet_test.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index e3f4b5d3e7..e585fb87bb 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -55,16 +55,16 @@ fn test_load_from_pem() { #[test] fn test_get_shard() { let alice = test_wallets::alice(); // [1, 57, 71, 46, 255, 104, 134, 119, 26, 152, 47, 48, 131, 218, 93, 66, 31, 36, 194, 145, 129, 230, 56, 136, 34, 141, 200, 28, 166, 13, 105, 225] - assert_eq!(0, alice.address.shard_of_3().as_u32()); + assert_eq!(1, alice.address.shard_of_3().as_u32()); let bob = test_wallets::bob(); // [128, 73, 214, 57, 229, 166, 152, 13, 28, 210, 57, 42, 188, 206, 65, 2, 156, 218, 116, 161, 86, 53, 35, 162, 2, 240, 150, 65, 204, 38, 24, 248] - assert_eq!(2, bob.address.shard_of_3().as_u32()); + assert_eq!(0, bob.address.shard_of_3().as_u32()); let carol = test_wallets::carol(); // [178, 161, 21, 85, 206, 82, 30, 73, 68, 224, 154, 177, 117, 73, 216, 91, 72, 125, 205, 38, 200, 75, 80, 23, 163, 158, 49, 163, 103, 8, 137, 186] - assert_eq!(0, carol.address.shard_of_3().as_u32()); + assert_eq!(2, carol.address.shard_of_3().as_u32()); let heidi = test_wallets::heidi(); // [110, 34, 65, 24, 217, 6, 138, 230, 38, 135, 138, 28, 251, 235, 203, 106, 149, 164, 113, 93, 184, 109, 27, 81, 224, 106, 4, 34, 108, 243, 15, 214] - assert_eq!(1, heidi.address.shard_of_3().as_u32()); + assert_eq!(2, heidi.address.shard_of_3().as_u32()); let mike = test_wallets::mike(); // [227, 42, 254, 220, 144, 79, 225, 147, 151, 70, 173, 151, 59, 235, 56, 53, 99, 207, 99, 100, 43, 166, 105, 179, 4, 15, 155, 148, 40, 165, 237, 96] assert_eq!(0, mike.address.shard_of_3().as_u32()); From 8ac954c49f9ae0c63091bab3600717bee472b0bf Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 21:54:10 +0300 Subject: [PATCH 024/195] Wallet source, test wallet metadata --- sdk/core/src/test_wallets.rs | 34 +++++++++++------------- sdk/core/src/wallet.rs | 51 +++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/sdk/core/src/test_wallets.rs b/sdk/core/src/test_wallets.rs index 4121b73f55..b0a8c25f4b 100644 --- a/sdk/core/src/test_wallets.rs +++ b/sdk/core/src/test_wallets.rs @@ -39,68 +39,64 @@ const WALLETS: &[(&str, &str)] = &[ ("s2onja", SZONJA_PEM), ]; -fn test_wallet(pem_file_contents: &str) -> Wallet { - Wallet::from_pem_file_contents(pem_file_contents.to_string()).unwrap() -} - /// Test wallet. Do not use on mainnet. pub fn alice() -> Wallet { - test_wallet(ALICE_PEM) + Wallet::new_test_wallet("alice", ALICE_PEM) } /// Test wallet. Do not use on mainnet. pub fn bob() -> Wallet { - test_wallet(BOB_PEM) + Wallet::new_test_wallet("bob", BOB_PEM) } /// Test wallet. Do not use on mainnet. pub fn carol() -> Wallet { - test_wallet(CAROL_PEM) + Wallet::new_test_wallet("carol", CAROL_PEM) } /// Test wallet. Do not use on mainnet. pub fn dan() -> Wallet { - test_wallet(DAN_PEM) + Wallet::new_test_wallet("dan", DAN_PEM) } /// Test wallet. Do not use on mainnet. pub fn eve() -> Wallet { - test_wallet(EVE_PEM) + Wallet::new_test_wallet("eve", EVE_PEM) } /// Test wallet. Do not use on mainnet. pub fn frank() -> Wallet { - test_wallet(FRANK_PEM) + Wallet::new_test_wallet("frank", FRANK_PEM) } /// Test wallet. Do not use on mainnet. pub fn grace() -> Wallet { - test_wallet(GRACE_PEM) + Wallet::new_test_wallet("grace", GRACE_PEM) } /// Test wallet. Do not use on mainnet. pub fn heidi() -> Wallet { - test_wallet(HEIDI_PEM) + Wallet::new_test_wallet("heidi", HEIDI_PEM) } /// Test wallet. Do not use on mainnet. pub fn ivan() -> Wallet { - test_wallet(IVAN_PEM) + Wallet::new_test_wallet("ivan", IVAN_PEM) } /// Test wallet. Do not use on mainnet. pub fn judy() -> Wallet { - test_wallet(JUDY_PEM) + Wallet::new_test_wallet("judy", JUDY_PEM) } /// Test wallet. Do not use on mainnet. pub fn mallory() -> Wallet { - test_wallet(MALLORY_PEM) + Wallet::new_test_wallet("mallory", MALLORY_PEM) } /// Test wallet. Do not use on mainnet. pub fn mike() -> Wallet { - test_wallet(MIKE_PEM) + Wallet::new_test_wallet("mike", MIKE_PEM) } /// Test wallet. Do not use on mainnet. @@ -109,7 +105,7 @@ pub fn mike() -> Wallet { /// /// Address: 0x14af28ce7d79117f689228b1af89d16e8b0c16a3d36062a2b6eeb8fbab6c0000 pub fn sophie() -> Wallet { - test_wallet(SOPHIE_PEM) + Wallet::new_test_wallet("sophie", SOPHIE_PEM) } /// Test wallet. Do not use on mainnet. @@ -118,7 +114,7 @@ pub fn sophie() -> Wallet { /// /// Address: 0x4b9ab2524a7d15416fb78d4d88249dc30272bd6ee1b8a07d4342c33a40a00001 pub fn simon() -> Wallet { - test_wallet(SIMON_PEM) + Wallet::new_test_wallet("simon", SIMON_PEM) } /// Test wallet. Do not use on mainnet. @@ -127,7 +123,7 @@ pub fn simon() -> Wallet { /// /// Address: 0x5ea3f378aaaa9f51cef76093b62e1041c90b415016dfa49760d7a846a8d90002 pub fn szonja() -> Wallet { - test_wallet(SZONJA_PEM) + Wallet::new_test_wallet("szonja", SZONJA_PEM) } /// Test wallets. Do not use on mainnet. diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 11caa738d5..01f876c667 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -31,48 +31,73 @@ use crate::{ pub struct Wallet { pub priv_key: PrivateKey, pub address: Address, - pub hrp: Option, + pub source: WalletSource, } -impl From for Wallet { - fn from(wallet_pem: WalletPem) -> Self { - Self::from_private_key(wallet_pem.priv_key, Some(wallet_pem.address.hrp)) - } +/// Optional structure that indicates how thew [`Wallet`] was created, with additional metadata. +#[derive(Clone, Debug)] +pub enum WalletSource { + Mnemonic, + PrivateKey, + PemFile(Bech32Hrp), + TestWallet(&'static str), + Keystore(Bech32Hrp), } impl Wallet { - fn from_private_key(priv_key: PrivateKey, hrp: Option) -> Self { - let address = PublicKey::from(&priv_key).to_address(); + pub fn new(private_key: PrivateKey, source: WalletSource) -> Self { + let address = PublicKey::from(&private_key).to_address(); Wallet { - priv_key, + priv_key: private_key, address, - hrp, + source, } } +} + +impl From for Wallet { + fn from(wallet_pem: WalletPem) -> Self { + Self::new( + wallet_pem.priv_key, + WalletSource::PemFile(wallet_pem.address.hrp), + ) + } +} + +impl From for Wallet { + fn from(private_key: PrivateKey) -> Self { + Self::new(private_key, WalletSource::PrivateKey) + } +} +impl Wallet { pub fn from_mnemonic_string(mnemonic_str: String) -> Wallet { let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', "")).unwrap(); let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32); - Self::from_private_key(private_key, None) + Self::new(private_key, WalletSource::Mnemonic) } pub fn from_private_key_hex(priv_key: &str) -> Result { let priv_key = PrivateKey::from_hex_str(priv_key)?; - Ok(Self::from_private_key(priv_key, None)) + Ok(Self::new(priv_key, WalletSource::PrivateKey)) } pub fn from_pem_file

(file_path: P) -> Result where P: AsRef, { - let contents = std::fs::read_to_string(file_path)?; - Self::from_pem_file_contents(contents) + Ok(WalletPem::from_pem_file(file_path)?.into()) } pub fn from_pem_file_contents(contents: String) -> Result { Ok(WalletPem::from_pem_str(&contents)?.into()) } + pub fn new_test_wallet(name: &'static str, pem: &str) -> Self { + let wallet_pem = WalletPem::from_pem_str(pem).unwrap(); + Self::new(wallet_pem.priv_key, WalletSource::TestWallet(name)) + } + #[deprecated( since = "0.54.0", note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address" From 589bd5c48eadaa7dd0a6fb7e26d4606cb42b9534 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 22:16:24 +0300 Subject: [PATCH 025/195] Wallet keystore decrypt_wallet further refactor --- framework/meta/src/cli/cli_args_sender.rs | 6 ++-- framework/meta/src/cmd/wallet_cmd.rs | 17 +++++------ .../tests/wallet_encrypt_keystore_test.rs | 21 +++++--------- sdk/core/src/crypto/private_key.rs | 8 ++++- sdk/core/src/wallet.rs | 4 +-- sdk/core/src/wallet/keystore.rs | 29 ++++++++++++++----- sdk/core/src/wallet/keystore_json.rs | 6 ++-- 7 files changed, 50 insertions(+), 41 deletions(-) diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index c625478e94..2dd337c077 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -29,10 +29,8 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { None => get_keystore_password(), }; let keystore = Keystore::from_file(keyfile)?; - let priv_key = keystore - .extract_private_key(&password) - .context("failed to load keystore wallet")?; - Wallet::from_private_key_hex(&priv_key.to_string()) + keystore + .decrypt_wallet(&password) .context("failed to load keystore wallet") } else { Err(anyhow!("a wallet is required: use --pem or --keyfile")) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index da90b402be..77d7aecfdf 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -1,5 +1,3 @@ -use core::str; - use bip39::{Language, Mnemonic}; use rand::Rng; use std::{ @@ -52,7 +50,7 @@ fn convert(convert_args: &WalletConvertArgs) { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); let wallet = Wallet::from_mnemonic_string(mnemonic_str); - write_resulted_pem(hrp, &wallet.private_key_hex(), outfile); + write_resulted_pem(hrp, &wallet, outfile); } None => { println!( @@ -60,16 +58,16 @@ fn convert(convert_args: &WalletConvertArgs) { ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); let wallet = Wallet::from_mnemonic_string(mnemonic_str); - write_resulted_pem(hrp, &wallet.private_key_hex(), outfile); + write_resulted_pem(hrp, &wallet, outfile); } }, ("keystore-secret", "pem") => match infile { Some(file) => { let keystore = Keystore::from_file(file).expect("keystore parsing error"); - match keystore.extract_private_key(&get_keystore_password()) { - Ok(priv_key) => { + match keystore.decrypt_wallet(&get_keystore_password()) { + Ok(wallet) => { println!("Password is correct"); - write_resulted_pem(hrp, &priv_key.to_string(), outfile); + write_resulted_pem(hrp, &wallet, outfile); } Err(KeystoreError::InvalidPassword) => { panic!("Password is incorrect"); @@ -106,8 +104,7 @@ fn convert(convert_args: &WalletConvertArgs) { } } -fn write_resulted_pem(hrp: Bech32Hrp, private_key: &str, outfile: Option<&String>) { - let wallet = Wallet::from_private_key_hex(private_key).unwrap(); +fn write_resulted_pem(hrp: Bech32Hrp, wallet: &Wallet, outfile: Option<&String>) { let pem_content = wallet.to_pem(hrp).to_pem_str(); match outfile { Some(outfile) => { @@ -222,7 +219,7 @@ fn new(new_args: &WalletNewArgs) { match format { Some("pem") => { - write_resulted_pem(hrp, &new_wallet_info.wallet.private_key_hex(), outfile); + write_resulted_pem(hrp, &new_wallet_info.wallet, outfile); if let Some(outfile) = outfile { println!("Wallet saved to '{outfile}'"); } diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 33a7c8e7b1..d553094b3e 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -33,10 +33,10 @@ fn test_wallet_convert_pem_to_keystore() { assert_eq!( Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_1) .unwrap() - .extract_private_key(KEYSTORE_PASSWORD) + .decrypt_wallet(KEYSTORE_PASSWORD) .unwrap() - .to_string(), - wallet_pem.private_key_hex() + .priv_key, + wallet_pem.priv_key ); fs::remove_file(ALICE_KEYSTORE_PATH_TEST_1).unwrap(); } @@ -45,20 +45,15 @@ fn test_wallet_convert_pem_to_keystore() { fn test_wallet_convert_keystore_to_pem() { create_keystore_file_from_scratch("erd", ALICE_KEYSTORE_PATH_TEST_2); - let private_key = Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_2) + let wallet = Keystore::from_file(ALICE_KEYSTORE_PATH_TEST_2) .unwrap() - .extract_private_key(KEYSTORE_PASSWORD) + .decrypt_wallet(KEYSTORE_PASSWORD) .unwrap(); - let pem_content = Wallet::from_private_key_hex(&private_key.to_string()) - .unwrap() - .to_pem(Bech32Hrp::default()) - .to_pem_str(); + let pem_content = wallet.to_pem(Bech32Hrp::default()).to_pem_str(); write_to_file(&pem_content, ALICE_PEM_PATH_TEST); assert_eq!( - private_key.to_string(), - Wallet::from_pem_file(ALICE_PEM_PATH_TEST) - .unwrap() - .private_key_hex() + wallet.priv_key, + Wallet::from_pem_file(ALICE_PEM_PATH_TEST).unwrap().priv_key ); fs::remove_file(ALICE_PEM_PATH_TEST).unwrap(); diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 40f0a85888..ccb4dd9538 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -20,7 +20,7 @@ pub const SEED_LENGTH: usize = 32; const EGLD_COIN_TYPE: u32 = 508; const HARDENED: u32 = 0x80000000; -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, PartialEq, Eq)] pub struct PrivateKey(pub [u8; PRIVATE_KEY_LENGTH]); impl PrivateKey { @@ -185,6 +185,12 @@ impl Display for PrivateKey { } } +impl std::fmt::Debug for PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PrivateKey({})", self) + } +} + impl Serialize for PrivateKey { fn serialize(&self, serializer: S) -> Result where diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 01f876c667..da7909731b 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -27,7 +27,7 @@ use crate::{ data::transaction::Transaction, }; -#[derive(Clone, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct Wallet { pub priv_key: PrivateKey, pub address: Address, @@ -35,7 +35,7 @@ pub struct Wallet { } /// Optional structure that indicates how thew [`Wallet`] was created, with additional metadata. -#[derive(Clone, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum WalletSource { Mnemonic, PrivateKey, diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 71c6f915ea..6e3a97a5c9 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -8,7 +8,10 @@ use multiversx_chain_core::std::{Bech32Address, Bech32Hrp}; use scrypt::{Params, scrypt}; use sha2::Sha256; -use crate::crypto::{private_key::PrivateKey, public_key::PublicKey}; +use crate::{ + crypto::{private_key::PrivateKey, public_key::PublicKey}, + wallet::Wallet, +}; use super::{KeystoreError, KeystoreJson}; @@ -37,7 +40,7 @@ pub struct KeystoreRandomness { pub struct Keystore { pub version: u32, pub kind: String, - pub bech32_address: Bech32Address, + pub address: Bech32Address, pub cipher: String, pub ciphertext: Vec, pub kdf: String, @@ -62,7 +65,11 @@ impl Keystore { s } - pub fn extract_private_key(&self, password: &str) -> Result { + /// Decrypts the keystore with the given password and returns a [`Wallet`]. + /// + /// Returns [`KeystoreError::InvalidPassword`] if the MAC check fails, or + /// other variants for unsupported cipher / KDF parameters. + pub fn decrypt_wallet(&self, password: &str) -> Result { if self.cipher != CIPHER_ALGORITHM_AES_128_CTR { return Err(KeystoreError::InvalidCipher); } @@ -98,12 +105,18 @@ impl Keystore { self.randomness.iv, self.ciphertext.clone(), ); - PrivateKey::from_bytes(&private_key_bytes).map_err(Into::into) + + let private_key = PrivateKey::from_bytes(&private_key_bytes)?; + Ok(Wallet::new( + private_key, + super::WalletSource::Keystore(self.address.hrp), + )) } - /// Not available in dapps, since it uses randomness to generate the keystore. + /// Encrypts a private key into a keystore using scrypt + AES-128-CTR + HMAC-SHA256. /// - /// Only available in the sc-meta standalone CLI. + /// The wallet address stored in the keystore is derived from `private_key` + /// and encoded with the given `hrp`. pub fn encrypt( private_key: PrivateKey, hrp: Bech32Hrp, @@ -111,7 +124,7 @@ impl Keystore { randomness: KeystoreRandomness, ) -> Self { let public_key = PublicKey::from(&private_key); - let bech32_address = public_key.to_address().to_bech32(hrp); + let address = public_key.to_address().to_bech32(hrp); let private_key_bytes = private_key.to_bytes(); let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap(); @@ -140,7 +153,7 @@ impl Keystore { Keystore { version: KEYSTORE_VERSION, kind: KIND_SECRET_KEY.to_string(), - bech32_address, + address, cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), ciphertext, kdf: KDF_SCRYPT.to_string(), diff --git a/sdk/core/src/wallet/keystore_json.rs b/sdk/core/src/wallet/keystore_json.rs index ef099297b0..a3aa807e60 100644 --- a/sdk/core/src/wallet/keystore_json.rs +++ b/sdk/core/src/wallet/keystore_json.rs @@ -55,7 +55,7 @@ impl Keystore { Ok(Keystore { version: json.version, kind: json.kind, - bech32_address: Bech32Address::from_bech32_str(&json.bech32), + address: Bech32Address::from_bech32_str(&json.bech32), cipher: json.crypto.cipher, ciphertext, kdf: json.crypto.kdf, @@ -77,8 +77,8 @@ impl Keystore { version: self.version, kind: self.kind.clone(), id: self.randomness.id.clone(), - address: self.bech32_address.address.to_hex(), - bech32: self.bech32_address.bech32.clone(), + address: self.address.address.to_hex(), + bech32: self.address.bech32.clone(), crypto: Crypto { cipher: self.cipher.clone(), cipherparams: CryptoParams { From c066ea91a8d5353afccb99a6b05ec9d645de6804 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 22:23:00 +0300 Subject: [PATCH 026/195] Wallet rename priv_key -> private_key --- framework/meta/src/cmd/wallet_cmd.rs | 4 ++-- .../tests/wallet_encrypt_keystore_test.rs | 12 +++++----- sdk/core/src/wallet.rs | 22 +++++++++---------- sdk/core/src/wallet/wallet_pem.rs | 13 ++++++----- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 77d7aecfdf..4584653c47 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -86,7 +86,7 @@ fn convert(convert_args: &WalletConvertArgs) { let wallet_pem = WalletPem::from_pem_file(file).expect("error reading PEM file"); let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - wallet_pem.priv_key, + wallet_pem.private_key, hrp, &get_keystore_password(), randomness, @@ -227,7 +227,7 @@ fn new(new_args: &WalletNewArgs) { Some("keystore-secret") => { let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - new_wallet_info.wallet.priv_key, + new_wallet_info.wallet.private_key, hrp, &get_keystore_password(), randomness, diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index d553094b3e..e1595362f8 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -16,7 +16,7 @@ const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e4 fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let wallet = Wallet::from_private_key_hex(ALICE_PRIVATE_KEY).unwrap(); let json_result = Keystore::encrypt( - wallet.priv_key, + wallet.private_key, hrp.try_into().expect("invalid HRP"), KEYSTORE_PASSWORD, new_keystore_randomness(), @@ -35,8 +35,8 @@ fn test_wallet_convert_pem_to_keystore() { .unwrap() .decrypt_wallet(KEYSTORE_PASSWORD) .unwrap() - .priv_key, - wallet_pem.priv_key + .private_key, + wallet_pem.private_key ); fs::remove_file(ALICE_KEYSTORE_PATH_TEST_1).unwrap(); } @@ -52,8 +52,10 @@ fn test_wallet_convert_keystore_to_pem() { let pem_content = wallet.to_pem(Bech32Hrp::default()).to_pem_str(); write_to_file(&pem_content, ALICE_PEM_PATH_TEST); assert_eq!( - wallet.priv_key, - Wallet::from_pem_file(ALICE_PEM_PATH_TEST).unwrap().priv_key + wallet.private_key, + Wallet::from_pem_file(ALICE_PEM_PATH_TEST) + .unwrap() + .private_key ); fs::remove_file(ALICE_PEM_PATH_TEST).unwrap(); diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index da7909731b..52da880c3a 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -29,7 +29,7 @@ use crate::{ #[derive(Clone, PartialEq, Eq, Debug)] pub struct Wallet { - pub priv_key: PrivateKey, + pub private_key: PrivateKey, pub address: Address, pub source: WalletSource, } @@ -48,7 +48,7 @@ impl Wallet { pub fn new(private_key: PrivateKey, source: WalletSource) -> Self { let address = PublicKey::from(&private_key).to_address(); Wallet { - priv_key: private_key, + private_key, address, source, } @@ -58,7 +58,7 @@ impl Wallet { impl From for Wallet { fn from(wallet_pem: WalletPem) -> Self { Self::new( - wallet_pem.priv_key, + wallet_pem.private_key, WalletSource::PemFile(wallet_pem.address.hrp), ) } @@ -78,8 +78,8 @@ impl Wallet { } pub fn from_private_key_hex(priv_key: &str) -> Result { - let priv_key = PrivateKey::from_hex_str(priv_key)?; - Ok(Self::new(priv_key, WalletSource::PrivateKey)) + let private_key = PrivateKey::from_hex_str(priv_key)?; + Ok(Self::new(private_key, WalletSource::PrivateKey)) } pub fn from_pem_file

(file_path: P) -> Result @@ -95,7 +95,7 @@ impl Wallet { pub fn new_test_wallet(name: &'static str, pem: &str) -> Self { let wallet_pem = WalletPem::from_pem_str(pem).unwrap(); - Self::new(wallet_pem.priv_key, WalletSource::TestWallet(name)) + Self::new(wallet_pem.private_key, WalletSource::TestWallet(name)) } #[deprecated( @@ -111,11 +111,11 @@ impl Wallet { } pub fn private_key_hex(&self) -> String { - self.priv_key.to_string() + self.private_key.to_string() } pub fn public_key_hex(&self) -> String { - PublicKey::from(&self.priv_key).to_string() + PublicKey::from(&self.private_key).to_string() } pub fn sign_tx(&self, unsign_tx: &Transaction) -> [u8; 64] { @@ -131,16 +131,16 @@ impl Wallet { tx_bytes = h.finalize().to_vec(); } - self.priv_key.sign(tx_bytes) + self.private_key.sign(tx_bytes) } pub fn sign_bytes(&self, data: Vec) -> [u8; 64] { - self.priv_key.sign(data) + self.private_key.sign(data) } pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { WalletPem { - priv_key: self.priv_key, + private_key: self.private_key, address: Bech32Address::encode_address(hrp, self.address.clone()), } } diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs index 9f73f9c745..62f097c221 100644 --- a/sdk/core/src/wallet/wallet_pem.rs +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -18,7 +18,7 @@ const PEM_MARKER_SUFFIX: &str = "-----"; /// round-trip through [`WalletPem::from_pem_str`] / [`WalletPem::to_pem_str`] /// preserves the original human-readable part (e.g. `"erd"` or a custom one). pub struct WalletPem { - pub priv_key: PrivateKey, + pub private_key: PrivateKey, pub address: Bech32Address, } @@ -46,9 +46,12 @@ impl WalletPem { let private_key_bytes = &decoded[..decoded.len() / 2]; let private_key_str = std::str::from_utf8(private_key_bytes) .map_err(|e| anyhow!("invalid UTF-8 in private key: {e}"))?; - let priv_key = PrivateKey::from_hex_str(private_key_str)?; + let private_key = PrivateKey::from_hex_str(private_key_str)?; - Ok(WalletPem { priv_key, address }) + Ok(WalletPem { + private_key, + address, + }) } /// Reads a PEM file from disk and parses it with [`WalletPem::from_pem_str`]. @@ -87,10 +90,10 @@ impl WalletPem { } pub fn private_key_hex(&self) -> String { - self.priv_key.to_string() + self.private_key.to_string() } pub fn public_key_hex(&self) -> String { - PublicKey::from(&self.priv_key).to_string() + PublicKey::from(&self.private_key).to_string() } } From e10ed3b4f8a7ffa15a4127ccd77b341248fe96e3 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 22:36:36 +0300 Subject: [PATCH 027/195] sdk - MultipleTransactionsResponseData serde fix --- sdk/core/src/data/transaction/multiple_transactions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/src/data/transaction/multiple_transactions.rs b/sdk/core/src/data/transaction/multiple_transactions.rs index c6d849f696..4d79ecc781 100644 --- a/sdk/core/src/data/transaction/multiple_transactions.rs +++ b/sdk/core/src/data/transaction/multiple_transactions.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; /// Corresponds to [`MultipleTransactionsResponseData`](https://github.com/multiversx/mx-chain-proxy-go/blob/master/data/transaction.go) in mx-chain-proxy-go. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MultipleTransactionsResponseData { - #[serde(rename = "txsSent")] + #[serde(rename = "txsSent", default)] pub num_of_sent_txs: u64, #[serde(rename = "txsHashes")] pub txs_hashes: HashMap, From e31071e36ed74c4625015b1ea50ac07e1e72dc0d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 22:44:02 +0300 Subject: [PATCH 028/195] Wallet hex cleanup & refactor --- .../meta/tests/wallet_convert_cli_test.rs | 4 ++-- .../tests/wallet_encrypt_keystore_test.rs | 3 ++- sdk/core/src/crypto/private_key.rs | 8 +++++-- sdk/core/src/crypto/public_key.rs | 16 ++++++++++--- sdk/core/src/wallet.rs | 23 +++++++++++++------ sdk/core/src/wallet/wallet_pem.rs | 4 ++-- sdk/core/tests/wallet_test.rs | 8 +++---- sdk/http/examples/sign_tx.rs | 16 +++++++------ sdk/http/examples/sign_txs.rs | 21 ++++++++++------- 9 files changed, 67 insertions(+), 36 deletions(-) diff --git a/framework/meta/tests/wallet_convert_cli_test.rs b/framework/meta/tests/wallet_convert_cli_test.rs index db63f2862f..07a140e6de 100644 --- a/framework/meta/tests/wallet_convert_cli_test.rs +++ b/framework/meta/tests/wallet_convert_cli_test.rs @@ -58,8 +58,8 @@ fn test_convert_mnemonic_to_pem_file_in_file_out() { }); let wallet = Wallet::from_pem_file(&output_pem).unwrap(); - assert_eq!(wallet.private_key_hex(), KNOWN_PRIVATE_KEY); - assert_eq!(wallet.public_key_hex(), KNOWN_PUBLIC_KEY); + assert_eq!(wallet.private_key.to_hex(), KNOWN_PRIVATE_KEY); + assert_eq!(wallet.public_key().to_hex(), KNOWN_PUBLIC_KEY); let pem_content = fs::read_to_string(&output_pem).unwrap(); assert!( diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index e1595362f8..5008524412 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -2,6 +2,7 @@ use multiversx_chain_core::std::Bech32Hrp; use multiversx_sc::types::Address; use multiversx_sc_meta::cmd::wallet_cmd::new_keystore_randomness; use multiversx_sdk::wallet::Keystore; +use multiversx_sdk::wallet::PrivateKey; use multiversx_sdk::wallet::Wallet; use std::fs::{self, File}; use std::io::Write; @@ -14,7 +15,7 @@ const KEYSTORE_PASSWORD: &str = "abcd"; const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { - let wallet = Wallet::from_private_key_hex(ALICE_PRIVATE_KEY).unwrap(); + let wallet = Wallet::from(PrivateKey::from_hex_str(ALICE_PRIVATE_KEY).unwrap()); let json_result = Keystore::encrypt( wallet.private_key, hrp.try_into().expect("invalid HRP"), diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index ccb4dd9538..6bdb86aa25 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -132,6 +132,10 @@ impl PrivateKey { &self.0 } + pub fn to_hex(&self) -> String { + hex::encode(&self.0[..32]) + } + pub fn sign(&self, message: Vec) -> [u8; 64] { let mut h: Sha512 = Sha512::new(); h.update(&self.0[..32]); @@ -181,7 +185,7 @@ impl PrivateKey { impl Display for PrivateKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - hex::encode(&self.0[..32]).fmt(f) + self.to_hex().fmt(f) } } @@ -196,7 +200,7 @@ impl Serialize for PrivateKey { where S: Serializer, { - serializer.serialize_str(self.to_string().as_str()) + serializer.serialize_str(self.to_hex().as_str()) } } diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index 6ad8604ecb..6b2d699f80 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -10,7 +10,7 @@ use serde::{ pub const PUBLIC_KEY_LENGTH: usize = 32; -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone)] pub struct PublicKey([u8; PUBLIC_KEY_LENGTH]); impl PublicKey { @@ -26,6 +26,10 @@ impl PublicKey { self.0.into() } + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } + pub fn from_hex_str(pk: &str) -> Result { let bytes = hex::decode(pk)?; let mut bits: [u8; 32] = [0u8; 32]; @@ -47,7 +51,13 @@ impl From<&PrivateKey> for PublicKey { impl Display for PublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - hex::encode(self.0).fmt(f) + self.to_hex().fmt(f) + } +} + +impl std::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PublicKey({})", self) } } @@ -56,7 +66,7 @@ impl Serialize for PublicKey { where S: Serializer, { - serializer.serialize_str(self.to_string().as_str()) + serializer.serialize_str(self.to_hex().as_str()) } } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 52da880c3a..566613a26f 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -9,6 +9,10 @@ pub use keystore_error::KeystoreError; pub use keystore_json::*; pub use wallet_pem::WalletPem; +// TODO: move under wallet +pub use crate::crypto::private_key::PrivateKey; +pub use crate::crypto::public_key::PublicKey; + use core::str; use std::path::Path; @@ -22,10 +26,7 @@ use serde_json::json; use sha2::Digest; use sha3::Keccak256; -use crate::{ - crypto::{private_key::PrivateKey, public_key::PublicKey}, - data::transaction::Transaction, -}; +use crate::data::transaction::Transaction; #[derive(Clone, PartialEq, Eq, Debug)] pub struct Wallet { @@ -77,6 +78,10 @@ impl Wallet { Self::new(private_key, WalletSource::Mnemonic) } + #[deprecated( + since = "0.67.0", + note = "Use `Wallet::from(PrivateKey::from_hex_str(hex).unwrap())` instead" + )] pub fn from_private_key_hex(priv_key: &str) -> Result { let private_key = PrivateKey::from_hex_str(priv_key)?; Ok(Self::new(private_key, WalletSource::PrivateKey)) @@ -93,7 +98,7 @@ impl Wallet { Ok(WalletPem::from_pem_str(&contents)?.into()) } - pub fn new_test_wallet(name: &'static str, pem: &str) -> Self { + pub(crate) fn new_test_wallet(name: &'static str, pem: &str) -> Self { let wallet_pem = WalletPem::from_pem_str(pem).unwrap(); Self::new(wallet_pem.private_key, WalletSource::TestWallet(name)) } @@ -111,11 +116,15 @@ impl Wallet { } pub fn private_key_hex(&self) -> String { - self.private_key.to_string() + self.private_key.to_hex() + } + + pub fn public_key(&self) -> PublicKey { + PublicKey::from(&self.private_key) } pub fn public_key_hex(&self) -> String { - PublicKey::from(&self.private_key).to_string() + PublicKey::from(&self.private_key).to_hex() } pub fn sign_tx(&self, unsign_tx: &Transaction) -> [u8; 64] { diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs index 62f097c221..f86973379b 100644 --- a/sdk/core/src/wallet/wallet_pem.rs +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -90,10 +90,10 @@ impl WalletPem { } pub fn private_key_hex(&self) -> String { - self.private_key.to_string() + self.private_key.to_hex() } pub fn public_key_hex(&self) -> String { - PublicKey::from(&self.private_key).to_string() + PublicKey::from(&self.private_key).to_hex() } } diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index e585fb87bb..0d5fa15024 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -15,11 +15,11 @@ fn test_private_key_from_mnemonic() { let address = public_key.to_address(); assert_eq!( "0b7966138e80b8f3bb64046f56aea4250fd7bacad6ed214165cea6767fd0bc2c", - private_key.to_string() + private_key.to_hex() ); assert_eq!( "dfefe0453840e5903f2bd519de9b0ed6e9621e57e28ba0b4c1b15115091dd72f", - public_key.to_string() + public_key.to_hex() ); assert_eq!( "erd1mlh7q3fcgrjeq0et65vaaxcw6m5ky8jhu296pdxpk9g32zga6uhsemxx2a", @@ -31,11 +31,11 @@ fn test_private_key_from_mnemonic() { let address = public_key.to_address(); assert_eq!( "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", - private_key.to_string() + private_key.to_hex() ); assert_eq!( "af8fef070a581873912ccbafb6a78bb9eb4e003085ac43dbbdfa3e20eb93cede", - public_key.to_string() + public_key.to_hex() ); assert_eq!( "erd147877pc2tqv88yfvewhmdfuth845uqpsskky8kaalglzp6unem0qpwh982", diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index 7299fac4a3..eadf5441b0 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -1,13 +1,15 @@ -use multiversx_sdk::{data::transaction::Transaction, wallet::Wallet}; +use multiversx_sdk::{data::transaction::Transaction, wallet::PrivateKey, wallet::Wallet}; use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] async fn main() { - let wl = Wallet::from_private_key_hex( - "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", - ) - .unwrap(); - let addr = wl.to_address(); + let wallet = Wallet::from( + PrivateKey::from_hex_str( + "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", + ) + .unwrap(), + ); + let addr = wallet.to_address(); let blockchain = GatewayHttpProxy::new(DEVNET_GATEWAY.to_string()); let network_config = blockchain.get_network_config().await.unwrap(); @@ -30,7 +32,7 @@ async fn main() { options: arg.options, }; - let signature = wl.sign_tx(&unsign_tx); + let signature = wallet.sign_tx(&unsign_tx); unsign_tx.signature = Some(hex::encode(signature)); let tx_hash = blockchain.send_transaction(&unsign_tx).await.unwrap(); diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index dc359d1de6..afb2cbd541 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -1,13 +1,18 @@ -use multiversx_sdk::{data::transaction::Transaction, wallet::Wallet}; +use multiversx_sdk::{ + data::transaction::Transaction, + wallet::{PrivateKey, Wallet}, +}; use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] async fn main() { - let wl = Wallet::from_private_key_hex( - "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", - ) - .unwrap(); - let addr = wl.to_address(); + let wallet = Wallet::from( + PrivateKey::from_hex_str( + "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", + ) + .unwrap(), + ); + let addr = wallet.to_address(); let blockchain = GatewayHttpProxy::new(DEVNET_GATEWAY.to_string()); let network_config = blockchain.get_network_config().await.unwrap(); @@ -32,7 +37,7 @@ async fn main() { let mut txs: Vec = vec![]; - let signature = wl.sign_tx(&unsign_tx); + let signature = wallet.sign_tx(&unsign_tx); unsign_tx.signature = Some(hex::encode(signature)); txs.push(unsign_tx.clone()); @@ -40,7 +45,7 @@ async fn main() { unsign_tx.options = 1; unsign_tx.nonce += 1; - let signature = wl.sign_tx(&unsign_tx); + let signature = wallet.sign_tx(&unsign_tx); unsign_tx.signature = Some(hex::encode(signature)); txs.push(unsign_tx.clone()); From fca876dad22577d9799c2988386f6b92b66533c3 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 22:53:39 +0300 Subject: [PATCH 029/195] Wallet cmd refactor --- framework/meta/src/cmd/wallet_cmd.rs | 36 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 4584653c47..e676e52bb9 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -1,16 +1,20 @@ -use bip39::{Language, Mnemonic}; -use rand::Rng; use std::{ fs::{self, File}, io::{self, Read, Write}, }; +use bip39::{Language, Mnemonic}; +use rand::Rng; + use multiversx_sc::types::Address; -use multiversx_sc_snippets::sdk::chain_core::std::Bech32Hrp; -use multiversx_sc_snippets::sdk::wallet::Keystore; -use multiversx_sc_snippets::sdk::wallet::KeystoreRandomness; -use multiversx_sc_snippets::sdk::wallet::Wallet; -use multiversx_sc_snippets::{hex, imports::Bech32Address}; +use multiversx_sc_snippets::{ + hex, + imports::Bech32Address, + sdk::{ + chain_core::std::Bech32Hrp, + wallet::{Keystore, KeystoreRandomness, Wallet}, + }, +}; use multiversx_sdk::{ crypto::private_key::PrivateKey, wallet::{KeystoreError, WalletPem}, @@ -50,7 +54,7 @@ fn convert(convert_args: &WalletConvertArgs) { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); let wallet = Wallet::from_mnemonic_string(mnemonic_str); - write_resulted_pem(hrp, &wallet, outfile); + write_resulted_pem(wallet.to_pem(hrp), outfile); } None => { println!( @@ -58,7 +62,7 @@ fn convert(convert_args: &WalletConvertArgs) { ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); let wallet = Wallet::from_mnemonic_string(mnemonic_str); - write_resulted_pem(hrp, &wallet, outfile); + write_resulted_pem(wallet.to_pem(hrp), outfile); } }, ("keystore-secret", "pem") => match infile { @@ -67,7 +71,7 @@ fn convert(convert_args: &WalletConvertArgs) { match keystore.decrypt_wallet(&get_keystore_password()) { Ok(wallet) => { println!("Password is correct"); - write_resulted_pem(hrp, &wallet, outfile); + write_resulted_pem(wallet.to_pem(hrp), outfile); } Err(KeystoreError::InvalidPassword) => { panic!("Password is incorrect"); @@ -104,8 +108,8 @@ fn convert(convert_args: &WalletConvertArgs) { } } -fn write_resulted_pem(hrp: Bech32Hrp, wallet: &Wallet, outfile: Option<&String>) { - let pem_content = wallet.to_pem(hrp).to_pem_str(); +fn write_resulted_pem(wallet_pem: WalletPem, outfile: Option<&String>) { + let pem_content = wallet_pem.to_pem_str(); match outfile { Some(outfile) => { let mut file = File::create(outfile).unwrap(); @@ -219,7 +223,7 @@ fn new(new_args: &WalletNewArgs) { match format { Some("pem") => { - write_resulted_pem(hrp, &new_wallet_info.wallet, outfile); + write_resulted_pem(new_wallet_info.wallet.to_pem(hrp), outfile); if let Some(outfile) = outfile { println!("Wallet saved to '{outfile}'"); } @@ -238,8 +242,10 @@ fn new(new_args: &WalletNewArgs) { println!("Wallet saved to '{outfile}'"); } } - Some(_) => { - println!("Unsupported format"); + Some(format) => { + println!( + "Unsupported format: '{format}'. Supported formats are 'pem' and 'keystore-secret'." + ); } None => {} } From 208a4fee7417f3e7f23f75acbabeb8ff2b977b2c Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 23:01:58 +0300 Subject: [PATCH 030/195] Wallet sign arg refactor --- sdk/core/src/crypto/private_key.rs | 6 +++--- sdk/core/src/wallet.rs | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 6bdb86aa25..6a367c53f3 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -136,7 +136,7 @@ impl PrivateKey { hex::encode(&self.0[..32]) } - pub fn sign(&self, message: Vec) -> [u8; 64] { + pub fn sign(&self, message: impl AsRef<[u8]>) -> [u8; 64] { let mut h: Sha512 = Sha512::new(); h.update(&self.0[..32]); @@ -152,7 +152,7 @@ impl PrivateKey { expanded_secret_key[31] |= 64; h.update(&digest1[32..]); - h.update(&message); + h.update(message.as_ref()); message_digest.copy_from_slice(h.finalize_reset().as_ref()); let message_digest_reduced = sc_reduce(message_digest); @@ -163,7 +163,7 @@ impl PrivateKey { h.update(encoded_r); h.update(&self.0[32..]); - h.update(&message); + h.update(message.as_ref()); hram_digest.copy_from_slice(h.finalize_reset().as_ref()); let hram_digest_reduced = sc_reduce(hram_digest); diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 566613a26f..d37992288d 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -94,10 +94,6 @@ impl Wallet { Ok(WalletPem::from_pem_file(file_path)?.into()) } - pub fn from_pem_file_contents(contents: String) -> Result { - Ok(WalletPem::from_pem_str(&contents)?.into()) - } - pub(crate) fn new_test_wallet(name: &'static str, pem: &str) -> Self { let wallet_pem = WalletPem::from_pem_str(pem).unwrap(); Self::new(wallet_pem.private_key, WalletSource::TestWallet(name)) @@ -143,7 +139,7 @@ impl Wallet { self.private_key.sign(tx_bytes) } - pub fn sign_bytes(&self, data: Vec) -> [u8; 64] { + pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> [u8; 64] { self.private_key.sign(data) } From 318404e188ca126e0d46fe5e19dd8201a9b770fa Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 23:27:05 +0300 Subject: [PATCH 031/195] Wallet, keystore, pem, signature tests --- sdk/core/tests/wallet_keystore_test.rs | 160 +++++++++++++++++++++++++ sdk/core/tests/wallet_pem_test.rs | 91 ++++++++++++++ sdk/core/tests/wallet_sign_test.rs | 44 +++++++ sdk/core/tests/wallet_test.rs | 14 +-- 4 files changed, 296 insertions(+), 13 deletions(-) create mode 100644 sdk/core/tests/wallet_keystore_test.rs create mode 100644 sdk/core/tests/wallet_pem_test.rs create mode 100644 sdk/core/tests/wallet_sign_test.rs diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs new file mode 100644 index 0000000000..4e8b483838 --- /dev/null +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -0,0 +1,160 @@ +use multiversx_chain_core::std::Bech32Hrp; +use multiversx_sdk::{ + crypto::{private_key::PrivateKey, public_key::PublicKey}, + wallet::{Keystore, KeystoreError, KeystoreRandomness, WalletSource}, +}; + +// Well-known Alice private key used across the test suite. +const ALICE_PRIVATE_KEY_HEX: &str = + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; +const KEYSTORE_PASSWORD: &str = "test_password"; + +/// Fully deterministic randomness: all-zeroes salt, all-ones IV, fixed UUID. +fn fixed_randomness() -> KeystoreRandomness { + KeystoreRandomness { + salt: [0u8; 32], + iv: [1u8; 16], + id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890".to_string(), + } +} + +fn alice_private_key() -> PrivateKey { + PrivateKey::from_hex_str(ALICE_PRIVATE_KEY_HEX).unwrap() +} + +fn erd_hrp() -> Bech32Hrp { + Bech32Hrp::try_from("erd").unwrap() +} + +// --------------------------------------------------------------------------- +// Encrypt / decrypt +// --------------------------------------------------------------------------- + +#[test] +fn test_keystore_encrypt_decrypt_roundtrip() { + let private_key = alice_private_key(); + let keystore = Keystore::encrypt( + private_key, + erd_hrp(), + KEYSTORE_PASSWORD, + fixed_randomness(), + ); + + let wallet = keystore.decrypt_wallet(KEYSTORE_PASSWORD).unwrap(); + + assert_eq!(wallet.private_key, private_key); + assert_eq!( + wallet.source, + WalletSource::Keystore(Bech32Hrp::try_from("erd").unwrap()) + ); + assert_eq!(wallet.address, PublicKey::from(&private_key).to_address(),); +} + +#[test] +fn test_keystore_wrong_password_rejected() { + let keystore = Keystore::encrypt( + alice_private_key(), + erd_hrp(), + KEYSTORE_PASSWORD, + fixed_randomness(), + ); + + assert!(matches!( + keystore.decrypt_wallet("wrong_password"), + Err(KeystoreError::InvalidPassword) + )); +} + +// --------------------------------------------------------------------------- +// Serialization / deserialization +// --------------------------------------------------------------------------- + +#[test] +fn test_keystore_serialize_deserialize_roundtrip() { + let private_key = alice_private_key(); + let keystore = Keystore::encrypt( + private_key, + erd_hrp(), + KEYSTORE_PASSWORD, + fixed_randomness(), + ); + + let json = keystore.to_json_string(); + + let keystore2: Keystore = Keystore::from_json(serde_json::from_str(&json).unwrap()).unwrap(); + let wallet = keystore2.decrypt_wallet(KEYSTORE_PASSWORD).unwrap(); + + assert_eq!(wallet.private_key, private_key); +} + +#[test] +fn test_keystore_json_snapshot() { + let keystore = Keystore::encrypt( + alice_private_key(), + erd_hrp(), + KEYSTORE_PASSWORD, + fixed_randomness(), + ); + + let json = keystore.to_json_string(); + + assert_eq!(json, EXPECTED_KEYSTORE_JSON); +} + +#[test] +fn test_full_encrypt_serialize_deserialize_decrypt_sign() { + const MESSAGE: &[u8] = b"hello multiversx"; + + // Encrypt. + let keystore = Keystore::encrypt( + alice_private_key(), + erd_hrp(), + KEYSTORE_PASSWORD, + fixed_randomness(), + ); + + // Serialize to JSON, then deserialize back. + let json = keystore.to_json_string(); + let keystore2 = Keystore::from_json(serde_json::from_str(&json).unwrap()).unwrap(); + + // Decrypt into a Wallet. + let wallet = keystore2.decrypt_wallet(KEYSTORE_PASSWORD).unwrap(); + assert_eq!(wallet.private_key, alice_private_key()); + + // Sign the message. + let sig = wallet.sign_bytes(MESSAGE); + + // Ed25519 is deterministic: same key + same message always gives the same signature. + assert_eq!(sig, wallet.sign_bytes(MESSAGE)); + + // Snapshot: exact signature bytes must match. + assert_eq!(hex::encode(sig), EXPECTED_SIGNATURE_HEX); +} + +// Generated by running the test suite once with fixed randomness. +const EXPECTED_KEYSTORE_JSON: &str = r#"{ + "version": 4, + "kind": "secretKey", + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "address": "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", + "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + "crypto": { + "ciphertext": "66f761de7f386c3cde5d8b1650c37614263880e96314b3070c45a39e44eb7b46573174ddade22c2a5f7dae73385d5a0abc5cff566e34152867c0ff560f1d5d05", + "cipherparams": { + "iv": "01010101010101010101010101010101" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "0000000000000000000000000000000000000000000000000000000000000000", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "4f9db1601de9013e32b2a8ca6cd941b5f5a3feb7a592c0b2586f6b8c1b2a87b9" + } +} +"#; + +const EXPECTED_SIGNATURE_HEX: &str = "c32f811c809a02d3548f190b134d5fa542b028935f4e300556e29b3164f146d54aa7e37ba97080e9f72402af67f6f6f070cbbd1b496c9779e75038a6aee54c07"; diff --git a/sdk/core/tests/wallet_pem_test.rs b/sdk/core/tests/wallet_pem_test.rs new file mode 100644 index 0000000000..0abcfbb8b6 --- /dev/null +++ b/sdk/core/tests/wallet_pem_test.rs @@ -0,0 +1,91 @@ +use multiversx_chain_core::std::Bech32Hrp; +use multiversx_sdk::wallet::{Wallet, WalletPem, WalletSource}; + +const ALICE_PEM_PATH: &str = "tests/alice.pem"; +const ALICE_BECH32: &str = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; +const ALICE_PRIVATE_KEY_HEX: &str = + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; +const ALICE_PUBLIC_KEY_HEX: &str = + "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"; + +// --------------------------------------------------------------------------- +// WalletPem parsing +// --------------------------------------------------------------------------- + +#[test] +fn test_pem_from_file_address() { + let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); + assert_eq!(pem.address.to_bech32_str(), ALICE_BECH32); +} + +#[test] +fn test_pem_from_file_private_key() { + let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); + assert_eq!(pem.private_key_hex(), ALICE_PRIVATE_KEY_HEX); +} + +#[test] +fn test_pem_from_file_public_key() { + let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); + assert_eq!(pem.public_key_hex(), ALICE_PUBLIC_KEY_HEX); +} + +#[test] +fn test_pem_from_str_parses_correctly() { + let pem_str = std::fs::read_to_string(ALICE_PEM_PATH).unwrap(); + let pem = WalletPem::from_pem_str(&pem_str).unwrap(); + assert_eq!(pem.address.to_bech32_str(), ALICE_BECH32); + assert_eq!(pem.private_key_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!(pem.public_key_hex(), ALICE_PUBLIC_KEY_HEX); +} + +// --------------------------------------------------------------------------- +// WalletPem serialization +// --------------------------------------------------------------------------- + +#[test] +fn test_pem_str_roundtrip() { + let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); + let serialized = pem.to_pem_str(); + let reparsed = WalletPem::from_pem_str(&serialized).unwrap(); + assert_eq!(reparsed.private_key_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!(reparsed.address.to_bech32_str(), ALICE_BECH32); +} + +// --------------------------------------------------------------------------- +// WalletPem <-> Wallet conversion +// --------------------------------------------------------------------------- + +#[test] +fn test_wallet_from_pem_fields() { + let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); + let wallet = Wallet::from(pem); + assert_eq!(wallet.private_key.to_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!(wallet.address.to_bech32_default().bech32, ALICE_BECH32); +} + +#[test] +fn test_wallet_from_pem_source() { + let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); + let hrp = pem.address.hrp; + let wallet = Wallet::from(pem); + assert_eq!(wallet.source, WalletSource::PemFile(hrp)); +} + +#[test] +fn test_wallet_to_pem_roundtrip() { + let wallet = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); + let hrp = Bech32Hrp::try_from("erd").unwrap(); + let pem = wallet.to_pem(hrp); + assert_eq!(pem.private_key_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!(pem.address.to_bech32_str(), ALICE_BECH32); +} + +#[test] +fn test_wallet_to_pem_str_roundtrip() { + let wallet = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); + let hrp = Bech32Hrp::try_from("erd").unwrap(); + let pem_str = wallet.to_pem(hrp).to_pem_str(); + let reparsed = Wallet::from(WalletPem::from_pem_str(&pem_str).unwrap()); + assert_eq!(reparsed.private_key.to_hex(), ALICE_PRIVATE_KEY_HEX); +} diff --git a/sdk/core/tests/wallet_sign_test.rs b/sdk/core/tests/wallet_sign_test.rs new file mode 100644 index 0000000000..21969ff7e6 --- /dev/null +++ b/sdk/core/tests/wallet_sign_test.rs @@ -0,0 +1,44 @@ +use multiversx_sdk::test_wallets; + +#[test] +fn test_sign_bytes_snapshot() { + let alice = test_wallets::alice(); + let bob = test_wallets::bob(); + + // Signatures are deterministic: same key + same message always produces the same bytes. + let sig_alice = alice.sign_bytes(b"hello multiversx"); + let sig_bob = bob.sign_bytes(b"hello multiversx"); + + assert_eq!(hex::encode(sig_alice), SIG_ALICE_HELLO); + assert_eq!(hex::encode(sig_bob), SIG_BOB_HELLO); +} + +#[test] +fn test_sign_bytes_deterministic() { + let alice = test_wallets::alice(); + assert_eq!( + alice.sign_bytes(b"deterministic"), + alice.sign_bytes(b"deterministic"), + ); +} + +#[test] +fn test_sign_bytes_different_messages_differ() { + let alice = test_wallets::alice(); + assert_ne!( + alice.sign_bytes(b"message one"), + alice.sign_bytes(b"message two"), + ); +} + +#[test] +fn test_sign_bytes_different_keys_differ() { + assert_ne!( + test_wallets::alice().sign_bytes(b"same message"), + test_wallets::bob().sign_bytes(b"same message"), + ); +} + +// Snapshots captured with fixed test-wallet keys. +const SIG_ALICE_HELLO: &str = "c32f811c809a02d3548f190b134d5fa542b028935f4e300556e29b3164f146d54aa7e37ba97080e9f72402af67f6f6f070cbbd1b496c9779e75038a6aee54c07"; +const SIG_BOB_HELLO: &str = "09e1b11a87e47bb0c1bebc71f653daabb966c583219588d4b80e396a966b91d41767af8f4704bbc50f8e8fd34cc6fb0da210329ea19437a7e07c0fc3f5cdf50e"; diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 0d5fa15024..cf93fffba1 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -1,10 +1,7 @@ use bip39::Mnemonic; +use multiversx_sdk::crypto::{private_key::PrivateKey, public_key::PublicKey}; use multiversx_sdk::test_wallets; -use multiversx_sdk::{ - crypto::{private_key::PrivateKey, public_key::PublicKey}, - wallet::Wallet, -}; #[test] fn test_private_key_from_mnemonic() { @@ -43,15 +40,6 @@ fn test_private_key_from_mnemonic() { ); } -#[test] -fn test_load_from_pem() { - let wallet = Wallet::from_pem_file("tests/alice.pem").unwrap(); - let address = wallet.to_address(); - assert_eq!( - "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - address.to_bech32_default().bech32 - ); -} #[test] fn test_get_shard() { let alice = test_wallets::alice(); // [1, 57, 71, 46, 255, 104, 134, 119, 26, 152, 47, 48, 131, 218, 93, 66, 31, 36, 194, 145, 129, 230, 56, 136, 34, 141, 200, 28, 166, 13, 105, 225] From a781510c1cd343ee36d3bc6ecc268781489b13d4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 23:34:45 +0300 Subject: [PATCH 032/195] typo --- sdk/core/src/wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index d37992288d..f2aed9c60f 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -35,7 +35,7 @@ pub struct Wallet { pub source: WalletSource, } -/// Optional structure that indicates how thew [`Wallet`] was created, with additional metadata. +/// Optional structure that indicates how the [`Wallet`] was created, with additional metadata. #[derive(Clone, PartialEq, Eq, Debug)] pub enum WalletSource { Mnemonic, From 70d018f02a61354f7572620af74381c321d6294f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 23:35:20 +0300 Subject: [PATCH 033/195] dead code --- framework/meta/src/cmd/wallet_cmd.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index e676e52bb9..59717cd810 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -266,16 +266,3 @@ fn test_wallet_cmd(args: &WalletTestWalletArgs) { file.write_all(pem.as_bytes()).unwrap(); println!("Saved test wallet '{name}' to '{path}'"); } - -/// Currently not in use. -#[allow(unused)] -pub fn generate_random_private_key(r: &mut T) -> PrivateKey -where - T: rand::CryptoRng + rand::Rng, -{ - let mut secret_key = PrivateKey([0u8; 64]); - - r.fill_bytes(&mut secret_key.0); - - secret_key -} From 2c43b444c4f6164e9212b7a12aa5a6fc2b93d477 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 23:37:34 +0300 Subject: [PATCH 034/195] doc fix --- sdk/core/src/wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index f2aed9c60f..f1f3d9e467 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -80,7 +80,7 @@ impl Wallet { #[deprecated( since = "0.67.0", - note = "Use `Wallet::from(PrivateKey::from_hex_str(hex).unwrap())` instead" + note = "Use `PrivateKey::from_hex_str(hex).map(Wallet::from)` instead" )] pub fn from_private_key_hex(priv_key: &str) -> Result { let private_key = PrivateKey::from_hex_str(priv_key)?; From 4849b1cae16911b2bc9c7dccce2d8d76a082967f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 27 May 2026 23:38:33 +0300 Subject: [PATCH 035/195] cleanup --- framework/meta/src/cmd/wallet_cmd.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 59717cd810..0903392ffd 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -15,10 +15,7 @@ use multiversx_sc_snippets::{ wallet::{Keystore, KeystoreRandomness, Wallet}, }, }; -use multiversx_sdk::{ - crypto::private_key::PrivateKey, - wallet::{KeystoreError, WalletPem}, -}; +use multiversx_sdk::wallet::{KeystoreError, WalletPem}; use crate::cli::cli_args_sender::get_keystore_password; use crate::cli::{ From 21d465982efe12f603cc37110945b27f498ded4b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 00:16:40 +0300 Subject: [PATCH 036/195] PublicKey verify message, test signing using ed25519-dalek --- Cargo.lock | 1 + chain/vm/Cargo.toml | 2 +- sdk/core/Cargo.toml | 1 + sdk/core/src/crypto/public_key.rs | 9 ++++++++ sdk/core/tests/wallet_sign_test.rs | 33 ++++++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6ef2622305..32cdc32a22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3476,6 +3476,7 @@ dependencies = [ "base64", "bip39", "ctr", + "ed25519-dalek", "hex", "hmac", "itertools 0.14.0", diff --git a/chain/vm/Cargo.toml b/chain/vm/Cargo.toml index f22b318094..6a3033b734 100644 --- a/chain/vm/Cargo.toml +++ b/chain/vm/Cargo.toml @@ -32,7 +32,7 @@ bitflags = "2.9" colored = "3.0" rand = { version = "0.10", optional = true } rand_seeder = "0.5.0" -ed25519-dalek = "2.1.0" +ed25519-dalek = "2.1" serde = { version = "1.0", features = ["derive"] } toml = "1.0" anyhow = "1.0" diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 14b7b8367b..a2b71d0aae 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -23,6 +23,7 @@ anyhow = "1.0" bip39 = "2.0" sha2 = "0.11" sha3 = "0.12" +ed25519-dalek = "2.1" hmac = "0.13" hex = "0.4" base64 = "0.22" diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index 6b2d699f80..47de0a3af0 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use super::private_key::PrivateKey; use anyhow::Result; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use multiversx_chain_core::types::Address; use serde::{ de::{Deserialize, Deserializer}, @@ -36,6 +37,14 @@ impl PublicKey { bits.copy_from_slice(&bytes[32..]); Ok(Self(bits)) } + + pub fn verify(&self, message: &[u8], signature: &[u8; 64]) -> bool { + let Ok(verifying_key) = VerifyingKey::from_bytes(&self.0) else { + return false; + }; + let signature = Signature::from_bytes(signature); + verifying_key.verify(message, &signature).is_ok() + } } impl From<&PrivateKey> for PublicKey { diff --git a/sdk/core/tests/wallet_sign_test.rs b/sdk/core/tests/wallet_sign_test.rs index 21969ff7e6..4d282f07fc 100644 --- a/sdk/core/tests/wallet_sign_test.rs +++ b/sdk/core/tests/wallet_sign_test.rs @@ -39,6 +39,39 @@ fn test_sign_bytes_different_keys_differ() { ); } +#[test] +fn test_verify_valid_signature() { + let alice = test_wallets::alice(); + let message = b"hello multiversx"; + let sig = alice.sign_bytes(message); + assert!(alice.public_key().verify(message, &sig)); +} + +#[test] +fn test_verify_wrong_message_fails() { + let alice = test_wallets::alice(); + let sig = alice.sign_bytes(b"hello multiversx"); + assert!(!alice.public_key().verify(b"different message", &sig)); +} + +#[test] +fn test_verify_wrong_key_fails() { + let alice = test_wallets::alice(); + let bob = test_wallets::bob(); + let sig = alice.sign_bytes(b"hello multiversx"); + assert!(!bob.public_key().verify(b"hello multiversx", &sig)); +} + +#[test] +fn test_verify_snapshot_signatures() { + let alice = test_wallets::alice(); + let bob = test_wallets::bob(); + let alice_sig: [u8; 64] = hex::decode(SIG_ALICE_HELLO).unwrap().try_into().unwrap(); + let bob_sig: [u8; 64] = hex::decode(SIG_BOB_HELLO).unwrap().try_into().unwrap(); + assert!(alice.public_key().verify(b"hello multiversx", &alice_sig)); + assert!(bob.public_key().verify(b"hello multiversx", &bob_sig)); +} + // Snapshots captured with fixed test-wallet keys. const SIG_ALICE_HELLO: &str = "c32f811c809a02d3548f190b134d5fa542b028935f4e300556e29b3164f146d54aa7e37ba97080e9f72402af67f6f6f070cbbd1b496c9779e75038a6aee54c07"; const SIG_BOB_HELLO: &str = "09e1b11a87e47bb0c1bebc71f653daabb966c583219588d4b80e396a966b91d41767af8f4704bbc50f8e8fd34cc6fb0da210329ea19437a7e07c0fc3f5cdf50e"; From b923b40891ea32d0bad1909bbcc2768757777732 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 00:24:30 +0300 Subject: [PATCH 037/195] signing: replaced custom ed25519 implementation with ed25519-dalek --- sdk/core/src/crypto.rs | 1 - sdk/core/src/crypto/edwards25519.rs | 848 ---- .../edwards25519/completed_group_element.rs | 42 - sdk/core/src/crypto/edwards25519/constant.rs | 3654 ----------------- .../edwards25519/extended_group_element.rs | 103 - .../src/crypto/edwards25519/field_element.rs | 637 --- .../pre_computed_group_element.rs | 46 - .../edwards25519/projective_group_element.rs | 24 - sdk/core/src/crypto/private_key.rs | 78 +- 9 files changed, 9 insertions(+), 5424 deletions(-) delete mode 100644 sdk/core/src/crypto/edwards25519.rs delete mode 100644 sdk/core/src/crypto/edwards25519/completed_group_element.rs delete mode 100644 sdk/core/src/crypto/edwards25519/constant.rs delete mode 100644 sdk/core/src/crypto/edwards25519/extended_group_element.rs delete mode 100644 sdk/core/src/crypto/edwards25519/field_element.rs delete mode 100644 sdk/core/src/crypto/edwards25519/pre_computed_group_element.rs delete mode 100644 sdk/core/src/crypto/edwards25519/projective_group_element.rs diff --git a/sdk/core/src/crypto.rs b/sdk/core/src/crypto.rs index 9a2e0e5d63..a37c90a77d 100644 --- a/sdk/core/src/crypto.rs +++ b/sdk/core/src/crypto.rs @@ -1,3 +1,2 @@ -mod edwards25519; pub mod private_key; pub mod public_key; diff --git a/sdk/core/src/crypto/edwards25519.rs b/sdk/core/src/crypto/edwards25519.rs deleted file mode 100644 index 7aa881b235..0000000000 --- a/sdk/core/src/crypto/edwards25519.rs +++ /dev/null @@ -1,848 +0,0 @@ -#![allow(clippy::identity_op)] - -pub mod completed_group_element; -pub mod constant; -pub mod extended_group_element; -pub mod field_element; -pub mod pre_computed_group_element; -pub mod projective_group_element; - -fn load3(input: Vec) -> i64 { - let mut r = input[0] as i64; - r |= (input[1] as i64) << 8; - r |= (input[2] as i64) << 16; - r -} - -fn load4(input: Vec) -> i64 { - let mut r = input[0] as i64; - r |= (input[1] as i64) << 8; - r |= (input[2] as i64) << 16; - r |= (input[3] as i64) << 24; - r -} - -// The scalars are GF(2^252 + 27742317777372353535851937790883648493). - -// Input: -// a[0]+256*a[1]+...+256^31*a[31] = a -// b[0]+256*b[1]+...+256^31*b[31] = b -// c[0]+256*c[1]+...+256^31*c[31] = c -// -// Output: -// s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l -// where l = 2^252 + 27742317777372353535851937790883648493. -pub fn sc_mul_add(a: [u8; 32], b: [u8; 32], c: [u8; 32]) -> [u8; 32] { - let a0 = 2097151 & load3(a[..].to_vec()); - let a1 = 2097151 & (load4(a[2..].to_vec()) >> 5); - let a2 = 2097151 & (load3(a[5..].to_vec()) >> 2); - let a3 = 2097151 & (load4(a[7..].to_vec()) >> 7); - let a4 = 2097151 & (load4(a[10..].to_vec()) >> 4); - let a5 = 2097151 & (load3(a[13..].to_vec()) >> 1); - let a6 = 2097151 & (load4(a[15..].to_vec()) >> 6); - let a7 = 2097151 & (load3(a[18..].to_vec()) >> 3); - let a8 = 2097151 & load3(a[21..].to_vec()); - let a9 = 2097151 & (load4(a[23..].to_vec()) >> 5); - let a10 = 2097151 & (load3(a[26..].to_vec()) >> 2); - let a11 = load4(a[28..].to_vec()) >> 7; - let b0 = 2097151 & load3(b[..].to_vec()); - let b1 = 2097151 & (load4(b[2..].to_vec()) >> 5); - let b2 = 2097151 & (load3(b[5..].to_vec()) >> 2); - let b3 = 2097151 & (load4(b[7..].to_vec()) >> 7); - let b4 = 2097151 & (load4(b[10..].to_vec()) >> 4); - let b5 = 2097151 & (load3(b[13..].to_vec()) >> 1); - let b6 = 2097151 & (load4(b[15..].to_vec()) >> 6); - let b7 = 2097151 & (load3(b[18..].to_vec()) >> 3); - let b8 = 2097151 & load3(b[21..].to_vec()); - let b9 = 2097151 & (load4(b[23..].to_vec()) >> 5); - let b10 = 2097151 & (load3(b[26..].to_vec()) >> 2); - let b11 = load4(b[28..].to_vec()) >> 7; - let c0 = 2097151 & load3(c[..].to_vec()); - let c1 = 2097151 & (load4(c[2..].to_vec()) >> 5); - let c2 = 2097151 & (load3(c[5..].to_vec()) >> 2); - let c3 = 2097151 & (load4(c[7..].to_vec()) >> 7); - let c4 = 2097151 & (load4(c[10..].to_vec()) >> 4); - let c5 = 2097151 & (load3(c[13..].to_vec()) >> 1); - let c6 = 2097151 & (load4(c[15..].to_vec()) >> 6); - let c7 = 2097151 & (load3(c[18..].to_vec()) >> 3); - let c8 = 2097151 & load3(c[21..].to_vec()); - let c9 = 2097151 & (load4(c[23..].to_vec()) >> 5); - let c10 = 2097151 & (load3(c[26..].to_vec()) >> 2); - let c11 = load4(c[28..].to_vec()) >> 7; - let mut carry = [0i64; 23]; - - let mut s0 = c0 + a0 * b0; - let mut s1 = c1 + a0 * b1 + a1 * b0; - let mut s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; - let mut s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; - let mut s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; - let mut s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; - let mut s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; - let mut s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; - let mut s8 = c8 - + a0 * b8 - + a1 * b7 - + a2 * b6 - + a3 * b5 - + a4 * b4 - + a5 * b3 - + a6 * b2 - + a7 * b1 - + a8 * b0; - let mut s9 = c9 - + a0 * b9 - + a1 * b8 - + a2 * b7 - + a3 * b6 - + a4 * b5 - + a5 * b4 - + a6 * b3 - + a7 * b2 - + a8 * b1 - + a9 * b0; - let mut s10 = c10 - + a0 * b10 - + a1 * b9 - + a2 * b8 - + a3 * b7 - + a4 * b6 - + a5 * b5 - + a6 * b4 - + a7 * b3 - + a8 * b2 - + a9 * b1 - + a10 * b0; - let mut s11 = c11 - + a0 * b11 - + a1 * b10 - + a2 * b9 - + a3 * b8 - + a4 * b7 - + a5 * b6 - + a6 * b5 - + a7 * b4 - + a8 * b3 - + a9 * b2 - + a10 * b1 - + a11 * b0; - let mut s12 = a1 * b11 - + a2 * b10 - + a3 * b9 - + a4 * b8 - + a5 * b7 - + a6 * b6 - + a7 * b5 - + a8 * b4 - + a9 * b3 - + a10 * b2 - + a11 * b1; - let mut s13 = a2 * b11 - + a3 * b10 - + a4 * b9 - + a5 * b8 - + a6 * b7 - + a7 * b6 - + a8 * b5 - + a9 * b4 - + a10 * b3 - + a11 * b2; - let mut s14 = - a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; - let mut s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; - let mut s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; - let mut s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; - let mut s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; - let mut s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; - let mut s20 = a9 * b11 + a10 * b10 + a11 * b9; - let mut s21 = a10 * b11 + a11 * b10; - let mut s22 = a11 * b11; - let mut s23 = 0i64; - - carry[0] = (s0 + (1 << 20)) >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[2] = (s2 + (1 << 20)) >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[4] = (s4 + (1 << 20)) >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[6] = (s6 + (1 << 20)) >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[8] = (s8 + (1 << 20)) >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[10] = (s10 + (1 << 20)) >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - carry[12] = (s12 + (1 << 20)) >> 21; - s13 += carry[12]; - s12 -= carry[12] << 21; - carry[14] = (s14 + (1 << 20)) >> 21; - s15 += carry[14]; - s14 -= carry[14] << 21; - carry[16] = (s16 + (1 << 20)) >> 21; - s17 += carry[16]; - s16 -= carry[16] << 21; - carry[18] = (s18 + (1 << 20)) >> 21; - s19 += carry[18]; - s18 -= carry[18] << 21; - carry[20] = (s20 + (1 << 20)) >> 21; - s21 += carry[20]; - s20 -= carry[20] << 21; - carry[22] = (s22 + (1 << 20)) >> 21; - s23 += carry[22]; - s22 -= carry[22] << 21; - - carry[1] = (s1 + (1 << 20)) >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[3] = (s3 + (1 << 20)) >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[5] = (s5 + (1 << 20)) >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[7] = (s7 + (1 << 20)) >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[9] = (s9 + (1 << 20)) >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[11] = (s11 + (1 << 20)) >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - carry[13] = (s13 + (1 << 20)) >> 21; - s14 += carry[13]; - s13 -= carry[13] << 21; - carry[15] = (s15 + (1 << 20)) >> 21; - s16 += carry[15]; - s15 -= carry[15] << 21; - carry[17] = (s17 + (1 << 20)) >> 21; - s18 += carry[17]; - s17 -= carry[17] << 21; - carry[19] = (s19 + (1 << 20)) >> 21; - s20 += carry[19]; - s19 -= carry[19] << 21; - carry[21] = (s21 + (1 << 20)) >> 21; - s22 += carry[21]; - s21 -= carry[21] << 21; - - s11 += s23 * 666643; - s12 += s23 * 470296; - s13 += s23 * 654183; - s14 -= s23 * 997805; - s15 += s23 * 136657; - s16 -= s23 * 683901; - // s23 = 0; - - s10 += s22 * 666643; - s11 += s22 * 470296; - s12 += s22 * 654183; - s13 -= s22 * 997805; - s14 += s22 * 136657; - s15 -= s22 * 683901; - // s22 = 0; - - s9 += s21 * 666643; - s10 += s21 * 470296; - s11 += s21 * 654183; - s12 -= s21 * 997805; - s13 += s21 * 136657; - s14 -= s21 * 683901; - // s21 = 0; - - s8 += s20 * 666643; - s9 += s20 * 470296; - s10 += s20 * 654183; - s11 -= s20 * 997805; - s12 += s20 * 136657; - s13 -= s20 * 683901; - // s20 = 0; - - s7 += s19 * 666643; - s8 += s19 * 470296; - s9 += s19 * 654183; - s10 -= s19 * 997805; - s11 += s19 * 136657; - s12 -= s19 * 683901; - // s19 = 0; - - s6 += s18 * 666643; - s7 += s18 * 470296; - s8 += s18 * 654183; - s9 -= s18 * 997805; - s10 += s18 * 136657; - s11 -= s18 * 683901; - // s18 = 0; - - carry[6] = (s6 + (1 << 20)) >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[8] = (s8 + (1 << 20)) >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[10] = (s10 + (1 << 20)) >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - carry[12] = (s12 + (1 << 20)) >> 21; - s13 += carry[12]; - s12 -= carry[12] << 21; - carry[14] = (s14 + (1 << 20)) >> 21; - s15 += carry[14]; - s14 -= carry[14] << 21; - carry[16] = (s16 + (1 << 20)) >> 21; - s17 += carry[16]; - s16 -= carry[16] << 21; - - carry[7] = (s7 + (1 << 20)) >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[9] = (s9 + (1 << 20)) >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[11] = (s11 + (1 << 20)) >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - carry[13] = (s13 + (1 << 20)) >> 21; - s14 += carry[13]; - s13 -= carry[13] << 21; - carry[15] = (s15 + (1 << 20)) >> 21; - s16 += carry[15]; - s15 -= carry[15] << 21; - - s5 += s17 * 666643; - s6 += s17 * 470296; - s7 += s17 * 654183; - s8 -= s17 * 997805; - s9 += s17 * 136657; - s10 -= s17 * 683901; - // s17 = 0; - - s4 += s16 * 666643; - s5 += s16 * 470296; - s6 += s16 * 654183; - s7 -= s16 * 997805; - s8 += s16 * 136657; - s9 -= s16 * 683901; - // s16 = 0; - - s3 += s15 * 666643; - s4 += s15 * 470296; - s5 += s15 * 654183; - s6 -= s15 * 997805; - s7 += s15 * 136657; - s8 -= s15 * 683901; - // s15 = 0; - - s2 += s14 * 666643; - s3 += s14 * 470296; - s4 += s14 * 654183; - s5 -= s14 * 997805; - s6 += s14 * 136657; - s7 -= s14 * 683901; - // s14 = 0; - - s1 += s13 * 666643; - s2 += s13 * 470296; - s3 += s13 * 654183; - s4 -= s13 * 997805; - s5 += s13 * 136657; - s6 -= s13 * 683901; - // s13 = 0; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry[0] = (s0 + (1 << 20)) >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[2] = (s2 + (1 << 20)) >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[4] = (s4 + (1 << 20)) >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[6] = (s6 + (1 << 20)) >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[8] = (s8 + (1 << 20)) >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[10] = (s10 + (1 << 20)) >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - - carry[1] = (s1 + (1 << 20)) >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[3] = (s3 + (1 << 20)) >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[5] = (s5 + (1 << 20)) >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[7] = (s7 + (1 << 20)) >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[9] = (s9 + (1 << 20)) >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[11] = (s11 + (1 << 20)) >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry[0] = s0 >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[1] = s1 >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[2] = s2 >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[3] = s3 >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[4] = s4 >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[5] = s5 >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[6] = s6 >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[7] = s7 >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[8] = s8 >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[9] = s9 >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[10] = s10 >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - carry[11] = s11 >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - // s12 = 0; - - carry[0] = s0 >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[1] = s1 >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[2] = s2 >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[3] = s3 >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[4] = s4 >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[5] = s5 >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[6] = s6 >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[7] = s7 >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[8] = s8 >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[9] = s9 >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[10] = s10 >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - - [ - (s0 >> 0) as u8, - (s0 >> 8) as u8, - ((s0 >> 16) | (s1 << 5)) as u8, - (s1 >> 3) as u8, - (s1 >> 11) as u8, - ((s1 >> 19) | (s2 << 2)) as u8, - (s2 >> 6) as u8, - ((s2 >> 14) | (s3 << 7)) as u8, - (s3 >> 1) as u8, - (s3 >> 9) as u8, - ((s3 >> 17) | (s4 << 4)) as u8, - (s4 >> 4) as u8, - (s4 >> 12) as u8, - ((s4 >> 20) | (s5 << 1)) as u8, - (s5 >> 7) as u8, - ((s5 >> 15) | (s6 << 6)) as u8, - (s6 >> 2) as u8, - (s6 >> 10) as u8, - ((s6 >> 18) | (s7 << 3)) as u8, - (s7 >> 5) as u8, - (s7 >> 13) as u8, - (s8 >> 0) as u8, - (s8 >> 8) as u8, - ((s8 >> 16) | (s9 << 5)) as u8, - (s9 >> 3) as u8, - (s9 >> 11) as u8, - ((s9 >> 19) | (s10 << 2)) as u8, - (s10 >> 6) as u8, - ((s10 >> 14) | (s11 << 7)) as u8, - (s11 >> 1) as u8, - (s11 >> 9) as u8, - (s11 >> 17) as u8, - ] -} - -// Input: -// s[0]+256*s[1]+...+256^63*s[63] = s -// -// Output: -// s[0]+256*s[1]+...+256^31*s[31] = s mod l -// where l = 2^252 + 27742317777372353535851937790883648493. -pub fn sc_reduce(s: [u8; 64]) -> [u8; 32] { - let mut s0 = 2097151 & load3(s[..].to_vec()); - let mut s1 = 2097151 & (load4(s[2..].to_vec()) >> 5); - let mut s2 = 2097151 & (load3(s[5..].to_vec()) >> 2); - let mut s3 = 2097151 & (load4(s[7..].to_vec()) >> 7); - let mut s4 = 2097151 & (load4(s[10..].to_vec()) >> 4); - let mut s5 = 2097151 & (load3(s[13..].to_vec()) >> 1); - let mut s6 = 2097151 & (load4(s[15..].to_vec()) >> 6); - let mut s7 = 2097151 & (load3(s[18..].to_vec()) >> 3); - let mut s8 = 2097151 & load3(s[21..].to_vec()); - let mut s9 = 2097151 & (load4(s[23..].to_vec()) >> 5); - let mut s10 = 2097151 & (load3(s[26..].to_vec()) >> 2); - let mut s11 = 2097151 & (load4(s[28..].to_vec()) >> 7); - let mut s12 = 2097151 & (load4(s[31..].to_vec()) >> 4); - let mut s13 = 2097151 & (load3(s[34..].to_vec()) >> 1); - let mut s14 = 2097151 & (load4(s[36..].to_vec()) >> 6); - let mut s15 = 2097151 & (load3(s[39..].to_vec()) >> 3); - let mut s16 = 2097151 & load3(s[42..].to_vec()); - let mut s17 = 2097151 & (load4(s[44..].to_vec()) >> 5); - let s18 = 2097151 & (load3(s[47..].to_vec()) >> 2); - let s19 = 2097151 & (load4(s[49..].to_vec()) >> 7); - let s20 = 2097151 & (load4(s[52..].to_vec()) >> 4); - let s21 = 2097151 & (load3(s[55..].to_vec()) >> 1); - let s22 = 2097151 & (load4(s[57..].to_vec()) >> 6); - let s23 = load4(s[60..].to_vec()) >> 3; - - s11 += s23 * 666643; - s12 += s23 * 470296; - s13 += s23 * 654183; - s14 -= s23 * 997805; - s15 += s23 * 136657; - s16 -= s23 * 683901; - // s23 = 0; - - s10 += s22 * 666643; - s11 += s22 * 470296; - s12 += s22 * 654183; - s13 -= s22 * 997805; - s14 += s22 * 136657; - s15 -= s22 * 683901; - // s22 = 0; - - s9 += s21 * 666643; - s10 += s21 * 470296; - s11 += s21 * 654183; - s12 -= s21 * 997805; - s13 += s21 * 136657; - s14 -= s21 * 683901; - // s21 = 0; - - s8 += s20 * 666643; - s9 += s20 * 470296; - s10 += s20 * 654183; - s11 -= s20 * 997805; - s12 += s20 * 136657; - s13 -= s20 * 683901; - // s20 = 0; - - s7 += s19 * 666643; - s8 += s19 * 470296; - s9 += s19 * 654183; - s10 -= s19 * 997805; - s11 += s19 * 136657; - s12 -= s19 * 683901; - // s19 = 0; - - s6 += s18 * 666643; - s7 += s18 * 470296; - s8 += s18 * 654183; - s9 -= s18 * 997805; - s10 += s18 * 136657; - s11 -= s18 * 683901; - // s18 = 0; - - let mut carry = [0i64; 17]; - - carry[6] = (s6 + (1 << 20)) >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[8] = (s8 + (1 << 20)) >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[10] = (s10 + (1 << 20)) >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - carry[12] = (s12 + (1 << 20)) >> 21; - s13 += carry[12]; - s12 -= carry[12] << 21; - carry[14] = (s14 + (1 << 20)) >> 21; - s15 += carry[14]; - s14 -= carry[14] << 21; - carry[16] = (s16 + (1 << 20)) >> 21; - s17 += carry[16]; - s16 -= carry[16] << 21; - - carry[7] = (s7 + (1 << 20)) >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[9] = (s9 + (1 << 20)) >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[11] = (s11 + (1 << 20)) >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - carry[13] = (s13 + (1 << 20)) >> 21; - s14 += carry[13]; - s13 -= carry[13] << 21; - carry[15] = (s15 + (1 << 20)) >> 21; - s16 += carry[15]; - s15 -= carry[15] << 21; - - s5 += s17 * 666643; - s6 += s17 * 470296; - s7 += s17 * 654183; - s8 -= s17 * 997805; - s9 += s17 * 136657; - s10 -= s17 * 683901; - // s17 = 0; - - s4 += s16 * 666643; - s5 += s16 * 470296; - s6 += s16 * 654183; - s7 -= s16 * 997805; - s8 += s16 * 136657; - s9 -= s16 * 683901; - // s16 = 0; - - s3 += s15 * 666643; - s4 += s15 * 470296; - s5 += s15 * 654183; - s6 -= s15 * 997805; - s7 += s15 * 136657; - s8 -= s15 * 683901; - // s15 = 0; - - s2 += s14 * 666643; - s3 += s14 * 470296; - s4 += s14 * 654183; - s5 -= s14 * 997805; - s6 += s14 * 136657; - s7 -= s14 * 683901; - // s14 = 0; - - s1 += s13 * 666643; - s2 += s13 * 470296; - s3 += s13 * 654183; - s4 -= s13 * 997805; - s5 += s13 * 136657; - s6 -= s13 * 683901; - // s13 = 0; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry[0] = (s0 + (1 << 20)) >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[2] = (s2 + (1 << 20)) >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[4] = (s4 + (1 << 20)) >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[6] = (s6 + (1 << 20)) >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[8] = (s8 + (1 << 20)) >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[10] = (s10 + (1 << 20)) >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - - carry[1] = (s1 + (1 << 20)) >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[3] = (s3 + (1 << 20)) >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[5] = (s5 + (1 << 20)) >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[7] = (s7 + (1 << 20)) >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[9] = (s9 + (1 << 20)) >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[11] = (s11 + (1 << 20)) >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry[0] = s0 >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[1] = s1 >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[2] = s2 >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[3] = s3 >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[4] = s4 >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[5] = s5 >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[6] = s6 >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[7] = s7 >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[8] = s8 >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[9] = s9 >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[10] = s10 >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - carry[11] = s11 >> 21; - s12 += carry[11]; - s11 -= carry[11] << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - // s12 = 0; - - carry[0] = s0 >> 21; - s1 += carry[0]; - s0 -= carry[0] << 21; - carry[1] = s1 >> 21; - s2 += carry[1]; - s1 -= carry[1] << 21; - carry[2] = s2 >> 21; - s3 += carry[2]; - s2 -= carry[2] << 21; - carry[3] = s3 >> 21; - s4 += carry[3]; - s3 -= carry[3] << 21; - carry[4] = s4 >> 21; - s5 += carry[4]; - s4 -= carry[4] << 21; - carry[5] = s5 >> 21; - s6 += carry[5]; - s5 -= carry[5] << 21; - carry[6] = s6 >> 21; - s7 += carry[6]; - s6 -= carry[6] << 21; - carry[7] = s7 >> 21; - s8 += carry[7]; - s7 -= carry[7] << 21; - carry[8] = s8 >> 21; - s9 += carry[8]; - s8 -= carry[8] << 21; - carry[9] = s9 >> 21; - s10 += carry[9]; - s9 -= carry[9] << 21; - carry[10] = s10 >> 21; - s11 += carry[10]; - s10 -= carry[10] << 21; - - [ - (s0 >> 0) as u8, - (s0 >> 8) as u8, - ((s0 >> 16) | (s1 << 5)) as u8, - (s1 >> 3) as u8, - (s1 >> 11) as u8, - ((s1 >> 19) | (s2 << 2)) as u8, - (s2 >> 6) as u8, - ((s2 >> 14) | (s3 << 7)) as u8, - (s3 >> 1) as u8, - (s3 >> 9) as u8, - ((s3 >> 17) | (s4 << 4)) as u8, - (s4 >> 4) as u8, - (s4 >> 12) as u8, - ((s4 >> 20) | (s5 << 1)) as u8, - (s5 >> 7) as u8, - ((s5 >> 15) | (s6 << 6)) as u8, - (s6 >> 2) as u8, - (s6 >> 10) as u8, - ((s6 >> 18) | (s7 << 3)) as u8, - (s7 >> 5) as u8, - (s7 >> 13) as u8, - (s8 >> 0) as u8, - (s8 >> 8) as u8, - ((s8 >> 16) | (s9 << 5)) as u8, - (s9 >> 3) as u8, - (s9 >> 11) as u8, - ((s9 >> 19) | (s10 << 2)) as u8, - (s10 >> 6) as u8, - ((s10 >> 14) | (s11 << 7)) as u8, - (s11 >> 1) as u8, - (s11 >> 9) as u8, - (s11 >> 17) as u8, - ] -} diff --git a/sdk/core/src/crypto/edwards25519/completed_group_element.rs b/sdk/core/src/crypto/edwards25519/completed_group_element.rs deleted file mode 100644 index 4341850b98..0000000000 --- a/sdk/core/src/crypto/edwards25519/completed_group_element.rs +++ /dev/null @@ -1,42 +0,0 @@ -use super::{ - extended_group_element::ExtendedGroupElement, field_element::FieldElement, - pre_computed_group_element::PreComputedGroupElement, - projective_group_element::ProjectiveGroupElement, -}; - -#[derive(Default)] -pub struct CompletedGroupElement { - pub x: FieldElement, - pub y: FieldElement, - pub z: FieldElement, - pub t: FieldElement, -} - -impl CompletedGroupElement { - pub fn ge_mixed_add(&mut self, p: &ExtendedGroupElement, q: &PreComputedGroupElement) { - let mut t0 = FieldElement::default(); - self.x.fe_add(&p.y, &p.x); - self.y.fe_sub(&p.y, &p.x); - self.z.fe_mul(&self.x, &q.y_plus_x); - self.y.fe_mul(&self.y.clone(), &q.y_minus_x); - self.t.fe_mul(&q.xy2d, &p.t); - t0.fe_add(&p.z, &p.z); - self.x.fe_sub(&self.z, &self.y); - self.y.fe_add(&self.z, &self.y.clone()); - self.z.fe_add(&t0, &self.t); - self.t.fe_sub(&t0, &self.t.clone()); - } - - pub fn to_extended(&self, r: &mut ExtendedGroupElement) { - r.x.fe_mul(&self.x, &self.t); - r.y.fe_mul(&self.y, &self.z); - r.z.fe_mul(&self.z, &self.t); - r.t.fe_mul(&self.x, &self.y); - } - - pub fn to_projective(&self, r: &mut ProjectiveGroupElement) { - r.x.fe_mul(&self.x, &self.t); - r.y.fe_mul(&self.y, &self.z); - r.z.fe_mul(&self.z, &self.t); - } -} diff --git a/sdk/core/src/crypto/edwards25519/constant.rs b/sdk/core/src/crypto/edwards25519/constant.rs deleted file mode 100644 index cb8d9e36b7..0000000000 --- a/sdk/core/src/crypto/edwards25519/constant.rs +++ /dev/null @@ -1,3654 +0,0 @@ -use super::{field_element::FieldElement, pre_computed_group_element::PreComputedGroupElement}; - -// base contains precomputed multiples of the base-point. See the Ed25519 paper -// for a discussion about how these values are used. -pub static BASE: [[PreComputedGroupElement; 8]; 32] = [ - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, - -6079156, 2047605, - ]), - y_minus_x: FieldElement([ - -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, - 19500929, -15469378, - ]), - xy2d: FieldElement([ - -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, - -24514362, -4438546, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, - -3814571, -358445, -10211303, - ]), - y_minus_x: FieldElement([ - -21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, - -26829678, -5319081, - ]), - xy2d: FieldElement([ - 26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, - -15472047, -4166697, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, - 28944400, -1550024, - ]), - y_minus_x: FieldElement([ - 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, - 7689662, 11199574, - ]), - xy2d: FieldElement([ - 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, - -17749093, -9920357, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, - 3284568, -6276540, - ]), - y_minus_x: FieldElement([ - 23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, - 13059162, 10374397, - ]), - xy2d: FieldElement([ - 7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, - -641708, -101325, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, - -15438304, 10819380, - ]), - y_minus_x: FieldElement([ - 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, - -12668491, 5581306, - ]), - xy2d: FieldElement([ - 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, - -23678021, -15815942, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, - -9818940, -12085777, - ]), - y_minus_x: FieldElement([ - -8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, - 1762328, 14866737, - ]), - xy2d: FieldElement([ - -18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, - 27914454, 4383652, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, - -23952439, -15175766, - ]), - y_minus_x: FieldElement([ - -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, - 30598449, 7715701, - ]), - xy2d: FieldElement([ - 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, - 29794553, -1409300, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, - -33154098, 2381726, - ]), - y_minus_x: FieldElement([ - -7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, - -32300832, 15351955, - ]), - xy2d: FieldElement([ - 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, - 29551813, 10109425, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, - 10092783, -4764171, - ]), - y_minus_x: FieldElement([ - 27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, - -17810127, 12803510, - ]), - xy2d: FieldElement([ - 17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, - 18016357, 4397660, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, - -17477504, 982639, - ]), - y_minus_x: FieldElement([ - 29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, - -21047696, 9934963, - ]), - xy2d: FieldElement([ - 5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, - -30123922, -10897950, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, - -13338309, 5500568, - ]), - y_minus_x: FieldElement([ - 12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, - 10874051, 13524335, - ]), - xy2d: FieldElement([ - 25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, - -22528059, 5376628, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, - 4535768, 1569007, - ]), - y_minus_x: FieldElement([ - -2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, - 31848280, 12543772, - ]), - xy2d: FieldElement([ - -22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, - 7718482, 14474653, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, - 24316168, -5253567, - ]), - y_minus_x: FieldElement([ - 13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, - -20729456, 8321686, - ]), - xy2d: FieldElement([ - 21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, - -9984458, 608372, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, - 27229399, 23887, - ]), - y_minus_x: FieldElement([ - -23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, - -1633405, 16678954, - ]), - xy2d: FieldElement([ - -29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, - -17169265, 4904953, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, - 2128236, -4326833, - ]), - y_minus_x: FieldElement([ - -16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, - -6882542, -2986532, - ]), - xy2d: FieldElement([ - -22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, - 15736322, 4143876, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, - 23527084, -16458268, - ]), - y_minus_x: FieldElement([ - 33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, - -15188911, -14416214, - ]), - xy2d: FieldElement([ - 1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, - -4774191, -16323038, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, - 7390890, 2759800, - ]), - y_minus_x: FieldElement([ - 4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, - 1244380, -12919645, - ]), - xy2d: FieldElement([ - -4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, - 15886429, 16489664, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, - -30606455, -712933, - ]), - y_minus_x: FieldElement([ - -25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, - -30836462, 5113182, - ]), - xy2d: FieldElement([ - -17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, - 13847711, 5387222, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, - -19600662, 10370991, - ]), - y_minus_x: FieldElement([ - 20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, - 32232924, 16763880, - ]), - xy2d: FieldElement([ - 9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, - 3140038, -16510092, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, - -28774454, 366295, - ]), - y_minus_x: FieldElement([ - 19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, - 719605, 11671788, - ]), - xy2d: FieldElement([ - 8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, - 27000813, -10195553, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, - 6750977, -14521026, - ]), - y_minus_x: FieldElement([ - 11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, - 8169720, 16220347, - ]), - xy2d: FieldElement([ - -18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, - -5990708, -14166033, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, - 2655861, 1738395, - ]), - y_minus_x: FieldElement([ - -27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, - -19087449, -11005278, - ]), - xy2d: FieldElement([ - 1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, - 10478196, 8544890, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, - 17789017, -3395995, - ]), - y_minus_x: FieldElement([ - -30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, - 30460520, 1052596, - ]), - xy2d: FieldElement([ - -11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, - 3179268, -9478891, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, - -16411740, 19072640, -9511060, - ]), - y_minus_x: FieldElement([ - 11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, - 473099, 5040608, - ]), - xy2d: FieldElement([ - -20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, - -19558642, -3131606, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, - -27868496, 11538389, - ]), - y_minus_x: FieldElement([ - -19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, - -5676054, 5797016, - ]), - xy2d: FieldElement([ - -11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, - -2369172, -5877341, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, - 15123619, 10811505, - ]), - y_minus_x: FieldElement([ - 14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, - -28886779, -11974553, - ]), - xy2d: FieldElement([ - -28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, - 4714547, -9600655, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, - -11745876, 12340220, - ]), - y_minus_x: FieldElement([ - 12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, - 15370987, 9608631, - ]), - xy2d: FieldElement([ - -4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, - -28210621, -8814099, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, - 20571223, 8420556, - ]), - y_minus_x: FieldElement([ - 14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, - -21574435, -12476749, - ]), - xy2d: FieldElement([ - 236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, - 15069930, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, - -30663140, -2331391, - ]), - y_minus_x: FieldElement([ - 5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, - -33069337, 9234253, - ]), - xy2d: FieldElement([ - 20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, - 18979186, 13396066, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, - 33514650, 7021958, - ]), - y_minus_x: FieldElement([ - -11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, - -21480480, 12868082, - ]), - xy2d: FieldElement([ - -28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, - -12455797, -8089383, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, - -15233648, 5540521, - ]), - y_minus_x: FieldElement([ - -11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, - -16128528, -14962807, - ]), - xy2d: FieldElement([ - 23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, - -11513277, -15205948, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, - -19179614, 5867134, - ]), - y_minus_x: FieldElement([ - -32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, - -29749703, -16108455, - ]), - xy2d: FieldElement([ - 27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, - -19526700, 7734629, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, - 18549497, 15302069, - ]), - y_minus_x: FieldElement([ - -32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, - -8872242, 8424746, - ]), - xy2d: FieldElement([ - 24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, - -11284482, -828919, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, - -5066034, 16498837, - ]), - y_minus_x: FieldElement([ - 8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, - 6536641, 10543906, - ]), - xy2d: FieldElement([ - -28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, - -27235709, 8876771, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, - -604426, -9039817, - ]), - y_minus_x: FieldElement([ - 10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, - 3084098, - ]), - xy2d: FieldElement([ - 10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, - -30233575, 15272409, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, - -25014757, 1950504, - ]), - y_minus_x: FieldElement([ - -26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, - 15341279, 8373727, - ]), - xy2d: FieldElement([ - 28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, - -2878207, 15190420, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, - -26033313, -6875003, - ]), - y_minus_x: FieldElement([ - -1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, - -16247779, -12154742, 6048605, - ]), - xy2d: FieldElement([ - -30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, - 11213262, 9168384, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, - 20470157, -16398701, - ]), - y_minus_x: FieldElement([ - -23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, - 14042978, 5230683, - ]), - xy2d: FieldElement([ - 29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, - -5933891, -12449708, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, - -28539189, 15326563, - ]), - y_minus_x: FieldElement([ - -19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, - -20514118, 9168260, - ]), - xy2d: FieldElement([ - -5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, - 33087103, -9011387, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, - -15000531, -5996870, 15664672, - ]), - y_minus_x: FieldElement([ - 23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, - 18151676, 13417686, - ]), - xy2d: FieldElement([ - -24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, - 15271676, -15452665, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, - -6921301, -13440182, - ]), - y_minus_x: FieldElement([ - -31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, - 12215110, 12028277, - ]), - xy2d: FieldElement([ - 14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, - -16900089, -655628, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, - 23973261, -12684474, - ]), - y_minus_x: FieldElement([ - -26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, - 32013174, -10103539, - ]), - xy2d: FieldElement([ - -25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, - 4425632, -837822, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, - 4725005, 14044970, - ]), - y_minus_x: FieldElement([ - 19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, - -19522291, -16109756, - ]), - xy2d: FieldElement([ - -24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, - 9282262, 10282508, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, - 5542595, -10702683, - ]), - y_minus_x: FieldElement([ - -10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, - -18093885, 10114655, - ]), - xy2d: FieldElement([ - -20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, - 25953725, -106158, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, - -3315279, 12831125, - ]), - y_minus_x: FieldElement([ - -15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, - 7381791, -2421839, - ]), - xy2d: FieldElement([ - -20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, - -15362489, 12339664, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, - 25091167, 14856294, - ]), - y_minus_x: FieldElement([ - -18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, - -22182473, -9012899, - ]), - xy2d: FieldElement([ - -11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, - 30146206, 9142070, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, - -8237858, 4171294, - ]), - y_minus_x: FieldElement([ - -15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, - 345228, -5462949, - ]), - xy2d: FieldElement([ - -21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, - -16253183, -13582083, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, - 28012650, -16405420, - ]), - y_minus_x: FieldElement([ - -5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, - -8867157, 3507940, - ]), - xy2d: FieldElement([ - 29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, - -9164929, 6580396, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, - -2095802, 9304567, - ]), - y_minus_x: FieldElement([ - 20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, - 5289421, -477127, - ]), - xy2d: FieldElement([ - -16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, - 26939669, -3752294, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, - -3698806, 117887, - ]), - y_minus_x: FieldElement([ - 22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, - -23319780, 541964, - ]), - xy2d: FieldElement([ - 16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, - -13337066, -13552195, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, - 690426, 14876244, - ]), - y_minus_x: FieldElement([ - 24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, - -22380984, 6618999, - ]), - xy2d: FieldElement([ - -1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, - 32239829, -5652762, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, - 32779359, 5095274, - ]), - y_minus_x: FieldElement([ - -33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, - 21639561, -2630236, - ]), - xy2d: FieldElement([ - -16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, - 17874574, 558605, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, - 2085892, 5119761, - ]), - y_minus_x: FieldElement([ - -22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, - -31999993, -5759884, - ]), - xy2d: FieldElement([ - -6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, - -22351378, 12961482, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, - -30194652, -5159638, - ]), - y_minus_x: FieldElement([ - -11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, - 7589640, 8945490, - ]), - xy2d: FieldElement([ - -32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, - 24099109, -14456170, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, - -6231896, -10280736, - ]), - y_minus_x: FieldElement([ - 10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, - -3363451, -6441124, - ]), - xy2d: FieldElement([ - -17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, - -13866408, 7421392, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, - 28826358, -4123029, - ]), - y_minus_x: FieldElement([ - 6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, - 13746021, -1742048, - ]), - xy2d: FieldElement([ - 28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, - 4771362, -8419958, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, - 3569627, 11342593, - ]), - y_minus_x: FieldElement([ - 26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, - 7325975, -14801071, - ]), - xy2d: FieldElement([ - -11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, - -17646694, -8186692, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, - 9280358, -3973687, - ]), - y_minus_x: FieldElement([ - -160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, - -11181622, -15545091, 4387441, - ]), - xy2d: FieldElement([ - -20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, - 20617071, -7482001, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, - 24345683, 10325460, - ]), - y_minus_x: FieldElement([ - -19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, - 4766743, 3552007, - ]), - xy2d: FieldElement([ - -21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, - 10988822, -3994762, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, - -8295852, 13296005, - ]), - y_minus_x: FieldElement([ - -9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, - -30000824, 12074674, - ]), - xy2d: FieldElement([ - 4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, - 29832613, -16391035, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, - -27122660, 13101590, - ]), - y_minus_x: FieldElement([ - -2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, - -30356070, -4190957, - ]), - xy2d: FieldElement([ - -30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, - 19568978, 9628812, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, - -6291786, 3437740, - ]), - y_minus_x: FieldElement([ - -18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, - 7463304, 4176122, - ]), - xy2d: FieldElement([ - -27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, - 24216882, 5944158, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, - 11632993, 5847885, - ]), - y_minus_x: FieldElement([ - 26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, - -9727230, 4782140, - ]), - xy2d: FieldElement([ - 19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, - 6358847, -1873857, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, - -13538503, 1387155, - ]), - y_minus_x: FieldElement([ - 19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, - -21866831, 11835260, - ]), - xy2d: FieldElement([ - 19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, - -26560550, 5052483, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, - 12228557, -7003677, - ]), - y_minus_x: FieldElement([ - 32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, - -6114803, 8653815, - ]), - xy2d: FieldElement([ - 22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, - 28483275, 2841751, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, - 19932058, -12739203, - ]), - y_minus_x: FieldElement([ - -11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, - -4777469, -13910208, - ]), - xy2d: FieldElement([ - 1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, - -14013818, 3093230, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, - -24049421, -6691850, - ]), - y_minus_x: FieldElement([ - -21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, - -21652117, -16739389, - ]), - xy2d: FieldElement([ - -9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, - 17361620, 11864968, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, - -25578460, -16240689, - ]), - y_minus_x: FieldElement([ - 14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, - 9145645, -6443880, - ]), - xy2d: FieldElement([ - 5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, - -5652273, -3050304, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, - -27999779, 1803632, - ]), - y_minus_x: FieldElement([ - -3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, - -21405489, 7047412, - ]), - xy2d: FieldElement([ - 20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, - -32343828, -10257566, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, - -21679658, 2288038, - ]), - y_minus_x: FieldElement([ - -26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, - -24819617, 12570232, - ]), - xy2d: FieldElement([ - -1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, - -11974704, 4724943, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, - -29576300, 1903856, - ]), - y_minus_x: FieldElement([ - 23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, - 26955097, 14109738, - ]), - xy2d: FieldElement([ - 15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, - 31960942, 11934971, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, - 10491392, 1379718, - ]), - y_minus_x: FieldElement([ - -13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, - 21432314, 12180697, - ]), - xy2d: FieldElement([ - -11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, - -10301319, -13872883, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, - 26128231, 6032912, - ]), - y_minus_x: FieldElement([ - -26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, - -25875034, -10437358, - ]), - xy2d: FieldElement([ - 3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, - 12376617, 3188849, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, - -16109234, -9852307, - ]), - y_minus_x: FieldElement([ - -14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, - 18640741, -960977, - ]), - xy2d: FieldElement([ - -6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, - 10530747, 1053335, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, - -2948985, 24018831, 15026644, - ]), - y_minus_x: FieldElement([ - -22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, - -2314791, -15145616, - ]), - xy2d: FieldElement([ - -27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, - -23166419, -2531735, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, - -5070217, 726099, - ]), - y_minus_x: FieldElement([ - 29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, - 30007388, -15823341, - ]), - xy2d: FieldElement([ - -936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, - 4555336, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, - -11985298, 12422646, - ]), - y_minus_x: FieldElement([ - 31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, - 29120153, 13924425, - ]), - xy2d: FieldElement([ - -17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, - 7240931, -237388, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, - 3293637, -15551743, - ]), - y_minus_x: FieldElement([ - -16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, - 12796905, -6335822, - ]), - xy2d: FieldElement([ - -8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, - 3222231, -11160462, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, - -7550776, 2484985, - ]), - y_minus_x: FieldElement([ - 9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, - -19802075, -3034702, - ]), - xy2d: FieldElement([ - -22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, - -30097688, -10618797, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, - -7360503, -4109293, - ]), - y_minus_x: FieldElement([ - 27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, - -23659143, -8132100, - ]), - xy2d: FieldElement([ - 19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, - 13243957, 8709688, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, - 5150968, 7274186, - ]), - y_minus_x: FieldElement([ - 2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, - 31021603, -9793610, - ]), - xy2d: FieldElement([ - -2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, - 15067285, -14147707, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, - -19379462, -12403220, - ]), - y_minus_x: FieldElement([ - 915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, - -7220461, 16527025, - ]), - xy2d: FieldElement([ - 32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, - 23834301, 6588044, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, - -20314580, -1305992, - ]), - y_minus_x: FieldElement([ - -4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, - 1976123, -7249027, - ]), - xy2d: FieldElement([ - 21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, - 12331345, -8237197, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, - 10008136, -4667901, - ]), - y_minus_x: FieldElement([ - 31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, - 8684155, -10532952, - ]), - xy2d: FieldElement([ - 19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, - 31316348, 14219878, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, - 29126555, 9207390, - ]), - y_minus_x: FieldElement([ - 32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, - -7957600, -14435730, - ]), - xy2d: FieldElement([ - 2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, - 28346258, 1994730, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, - 22628102, 8115180, - ]), - y_minus_x: FieldElement([ - -30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, - -9943017, 930272, - ]), - xy2d: FieldElement([ - -15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, - -22765376, -10650715, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, - -16554220, -1867018, 8398970, - ]), - y_minus_x: FieldElement([ - -31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, - 18009408, -15772772, - ]), - xy2d: FieldElement([ - -17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, - 19288549, 1325865, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, - 33166107, -8042750, - ]), - y_minus_x: FieldElement([ - 20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, - 2213263, -13878373, - ]), - xy2d: FieldElement([ - 32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, - -5766928, 8371348, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, - -26442943, 10486144, - ]), - y_minus_x: FieldElement([ - -22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, - -23921530, -11455195, - ]), - xy2d: FieldElement([ - 5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, - 19985175, -3436086, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, - 65030, 8370684, - ]), - y_minus_x: FieldElement([ - -8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, - 12743482, -9800518, - ]), - xy2d: FieldElement([ - -2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, - 18800704, 255233, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, - 19258688, -14753793, - ]), - y_minus_x: FieldElement([ - -2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, - -1524053, 2055794, - ]), - xy2d: FieldElement([ - 580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, - -30021019, 7394435, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, - -4821741, 14799921, - ]), - y_minus_x: FieldElement([ - 13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, - -9056018, 7402518, - ]), - xy2d: FieldElement([ - 2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, - 8205061, 1607563, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, - 30019587, -9029278, - ]), - y_minus_x: FieldElement([ - -27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, - 9944378, 8024, - ]), - xy2d: FieldElement([ - 4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, - -8142388, 5640030, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, - 15219798, -14327783, - ]), - y_minus_x: FieldElement([ - 27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, - -18342183, 9742717, - ]), - xy2d: FieldElement([ - 6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, - 1994844, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, - 24319929, -6446333, - ]), - y_minus_x: FieldElement([ - 16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, - -30102368, -4739048, - ]), - xy2d: FieldElement([ - 22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, - 21880021, -12250760, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, - 8840445, 4957760, - ]), - y_minus_x: FieldElement([ - -15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, - -14796503, 5005757, - ]), - xy2d: FieldElement([ - -2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, - -16395286, -2176112, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, - 22208662, 2000468, - ]), - y_minus_x: FieldElement([ - 3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, - -3691214, -7414184, - ]), - xy2d: FieldElement([ - 10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, - -3132688, 16400289, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, - 26894937, 9132066, - ]), - y_minus_x: FieldElement([ - 24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, - -6817084, -2692882, - ]), - xy2d: FieldElement([ - 13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, - -4688006, 2364226, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, - 15445875, -7798101, - ]), - y_minus_x: FieldElement([ - 29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, - -278050, -15759279, - ]), - xy2d: FieldElement([ - -6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, - -29828287, -10815811, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, - 17293797, 960709, - ]), - y_minus_x: FieldElement([ - 20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, - -19384511, 7639714, - ]), - xy2d: FieldElement([ - -2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, - 29994677, -15808121, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, - -31184575, 709464, - ]), - y_minus_x: FieldElement([ - 12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, - 16102007, 13205847, - ]), - xy2d: FieldElement([ - 13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, - 10151379, 10394400, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, - -33302334, 8934414, - ]), - y_minus_x: FieldElement([ - -15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, - -11536886, 11721158, - ]), - xy2d: FieldElement([ - 17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, - -9205489, -1280045, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, - 6108462, -6183415, - ]), - y_minus_x: FieldElement([ - -5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, - -26898490, -7867459, - ]), - xy2d: FieldElement([ - -31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, - 7592688, -14992079, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, - -28256052, 4298412, - ]), - y_minus_x: FieldElement([ - -20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, - 13717174, 10805743, - ]), - xy2d: FieldElement([ - -14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, - -26938930, -5863836, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, - -4381056, 9882022, - ]), - y_minus_x: FieldElement([ - 18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, - -23967430, -3299429, - ]), - xy2d: FieldElement([ - -6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, - -2403099, 5276065, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, - 1449224, 13082861, - ]), - y_minus_x: FieldElement([ - 10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, - -21016438, -8202000, - ]), - xy2d: FieldElement([ - -33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, - -10300080, -11060101, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, - -22664288, 6904815, - ]), - y_minus_x: FieldElement([ - 29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, - -29003639, -6657642, - ]), - xy2d: FieldElement([ - 10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, - -25584551, 5181966, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, - -2322852, -12388574, - ]), - y_minus_x: FieldElement([ - -21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, - 20603771, -6561742, - ]), - xy2d: FieldElement([ - -18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, - 4662781, 7820689, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, - 16136752, 15264020, - ]), - y_minus_x: FieldElement([ - -10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, - 19012087, 3772772, - ]), - xy2d: FieldElement([ - 3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, - 20527771, 12988982, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, - 3299665, -12424953, - ]), - y_minus_x: FieldElement([ - -16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, - -25583147, 8991218, - ]), - xy2d: FieldElement([ - -17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, - -30242287, 1507265, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, - 14606362, -10647073, - ]), - y_minus_x: FieldElement([ - -3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, - 4170404, -2085325, - ]), - xy2d: FieldElement([ - -11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, - -14276837, -8400798, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, - 13240845, 10965870, - ]), - y_minus_x: FieldElement([ - -7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, - 29514390, 4302863, - ]), - xy2d: FieldElement([ - -13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, - 17846988, -13971927, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, - -5222701, 12650267, - ]), - y_minus_x: FieldElement([ - -9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, - -19134326, 10958663, - ]), - xy2d: FieldElement([ - 22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, - -25083830, 4271862, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, - -32526221, 8469673, - ]), - y_minus_x: FieldElement([ - 15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, - 24154791, -9460943, - ]), - xy2d: FieldElement([ - 15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, - 24913809, 9815020, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, - 18735128, 9466238, - ]), - y_minus_x: FieldElement([ - 11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, - -22728655, 16199064, - ]), - xy2d: FieldElement([ - 14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, - 30801119, 2164795, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, - -13492807, 1268052, - ]), - y_minus_x: FieldElement([ - -10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, - -17055369, 3565904, - ]), - xy2d: FieldElement([ - 29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, - -27162222, -14030531, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, - 2244111, -14001979, - ]), - y_minus_x: FieldElement([ - -5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, - 13491506, 4641841, - ]), - xy2d: FieldElement([ - 10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, - 14476989, -12767431, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, - -27912810, 12651324, - ]), - y_minus_x: FieldElement([ - -31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, - 21721536, -3148940, - ]), - xy2d: FieldElement([ - 10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, - 13216206, 14842320, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, - -27434397, 9900184, - ]), - y_minus_x: FieldElement([ - 14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, - -7051866, -8040114, - ]), - xy2d: FieldElement([ - 30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, - 9524356, -7018878, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, - 15663516, 4035784, - ]), - y_minus_x: FieldElement([ - -2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, - -22258478, 4659091, - ]), - xy2d: FieldElement([ - -16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, - -2178256, -13455585, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, - 23440562, -290208, - ]), - y_minus_x: FieldElement([ - 10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, - -16571960, -7442864, - ]), - xy2d: FieldElement([ - 17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, - -3646624, 3898661, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, - 21831162, 1245233, - ]), - y_minus_x: FieldElement([ - 26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, - -30402091, -16716212, - ]), - xy2d: FieldElement([ - -12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, - -22338025, 13987525, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, - -7469781, -2858068, - ]), - y_minus_x: FieldElement([ - 9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, - -14581012, 4091397, - ]), - xy2d: FieldElement([ - -8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, - 29077877, -14741988, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, - -20017847, 2357889, - ]), - y_minus_x: FieldElement([ - 32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, - 5727338, 189038, - ]), - xy2d: FieldElement([ - 14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, - -25888931, -14884697, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, - 13913677, -5137875, - ]), - y_minus_x: FieldElement([ - -25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, - 12878652, 8511905, - ]), - xy2d: FieldElement([ - -25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, - 5568676, -3127656, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, - -14395196, 8070818, - ]), - y_minus_x: FieldElement([ - 27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, - -11552749, -1028714, - ]), - xy2d: FieldElement([ - 10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, - -27493162, 15431203, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, - -15056790, -7935931, - ]), - y_minus_x: FieldElement([ - -30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, - -3356550, 9594024, - ]), - xy2d: FieldElement([ - -23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, - 13352335, -10977084, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, - -13215537, -319204, - ]), - y_minus_x: FieldElement([ - 20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, - -22750759, 14523817, - ]), - xy2d: FieldElement([ - 27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, - -30172742, -4805667, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, - -27727044, 11358504, - ]), - y_minus_x: FieldElement([ - -12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, - -26123651, 4985768, - ]), - xy2d: FieldElement([ - -19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, - -19414307, -15621255, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, - -1691065, -9004790, - ]), - y_minus_x: FieldElement([ - 1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, - -16796596, -5031438, - ]), - xy2d: FieldElement([ - -22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, - -99853, -10237333, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, - -29260363, -5511971, - ]), - y_minus_x: FieldElement([ - 31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, - 4882242, -10626905, - ]), - xy2d: FieldElement([ - 29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, - -5190932, -4162409, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, - 330070, -3659409, - ]), - y_minus_x: FieldElement([ - 6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, - -271295, 12071499, - ]), - xy2d: FieldElement([ - -8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, - -5159697, 3829363, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, - 26333140, 14267664, - ]), - y_minus_x: FieldElement([ - -11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, - -21761224, 8930324, - ]), - xy2d: FieldElement([ - -21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, - -26491501, -16408940, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, - -15943699, 13814990, - ]), - y_minus_x: FieldElement([ - -17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, - -1956526, -1776914, - ]), - xy2d: FieldElement([ - -25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, - -18685978, 4578290, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, - 21964432, 8235257, - ]), - y_minus_x: FieldElement([ - -6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, - 13006806, 2355433, - ]), - xy2d: FieldElement([ - -16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, - 1141648, -12796236, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, - 32674895, 13715045, - ]), - y_minus_x: FieldElement([ - 11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, - -28740881, -15642093, - ]), - xy2d: FieldElement([ - -1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, - -27245943, 4383347, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, - 27193557, 6245191, - ]), - y_minus_x: FieldElement([ - -15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, - 22510453, 8577507, - ]), - xy2d: FieldElement([ - -12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, - -29571492, -3635906, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, - 5611860, 8164018, - ]), - y_minus_x: FieldElement([ - -16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, - 5773085, -8422109, - ]), - xy2d: FieldElement([ - -23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, - 31632953, 190926, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, - -25341555, -3627528, - ]), - y_minus_x: FieldElement([ - 8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, - -1672488, 827625, - ]), - xy2d: FieldElement([ - -32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, - -24878478, 1541286, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, - 20456845, -1885033, - ]), - y_minus_x: FieldElement([ - 27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, - 29439641, 15138866, - ]), - xy2d: FieldElement([ - 21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, - 7779328, 109896, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, - 23177719, -554075, - ]), - y_minus_x: FieldElement([ - 26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, - 4439158, -13279347, - ]), - xy2d: FieldElement([ - -22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, - -17717757, -5461437, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, - 8550082, -15114165, - ]), - y_minus_x: FieldElement([ - -18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, - -23260460, -8428588, - ]), - xy2d: FieldElement([ - -32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, - -21902570, 1494193, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, - 8742704, 12967017, - ]), - y_minus_x: FieldElement([ - -28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, - -29903758, 15553883, - ]), - xy2d: FieldElement([ - 21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, - 19375923, -12647961, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, - 2862653, 9455043, - ]), - y_minus_x: FieldElement([ - 29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, - 15511449, 4789663, - ]), - xy2d: FieldElement([ - -20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, - 23513200, 16652362, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, - -18319198, 10212860, - ]), - y_minus_x: FieldElement([ - 2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, - -9988298, -12506466, - ]), - xy2d: FieldElement([ - -24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, - -2499677, -16710063, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, - 2719757, 4940997, - ]), - y_minus_x: FieldElement([ - -1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, - 26699843, 5276295, - ]), - xy2d: FieldElement([ - -13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, - -15452774, 7159369, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, - 22477218, -8403385, - ]), - y_minus_x: FieldElement([ - 18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, - -22548173, 9334109, - ]), - xy2d: FieldElement([ - 2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, - -22587149, 536906, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, - 10838060, -15420424, - ]), - y_minus_x: FieldElement([ - -19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, - 6378260, 699185, - ]), - xy2d: FieldElement([ - 7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, - -5045064, -13373962, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, - -3918636, -9669325, - ]), - y_minus_x: FieldElement([ - 10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, - -21786507, 5427593, - ]), - xy2d: FieldElement([ - 696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, - 19236243, 12477404, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, - -23169516, 7733644, - ]), - y_minus_x: FieldElement([ - 17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, - 10322027, 15313801, - ]), - xy2d: FieldElement([ - 26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, - -24449242, 10890804, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, - 25180797, -5334884, - ]), - y_minus_x: FieldElement([ - -586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, - 316878, 13820577, - ]), - xy2d: FieldElement([ - -9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, - 30696930, -3712849, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, - -32349517, 7392473, - ]), - y_minus_x: FieldElement([ - -8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, - 25112947, -2926644, - ]), - xy2d: FieldElement([ - -2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, - 5537438, -13914319, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, - 31234590, 6090599, - ]), - y_minus_x: FieldElement([ - -9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, - -6970405, -9034768, - ]), - xy2d: FieldElement([ - -27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, - 20650474, 1804084, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, - 27842616, -202328, - ]), - y_minus_x: FieldElement([ - -15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, - -11375082, 12714369, - ]), - xy2d: FieldElement([ - 20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, - -19733229, 12796920, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, - -12899438, 3480665, -15182815, - ]), - y_minus_x: FieldElement([ - -32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, - -33374054, 2771025, - ]), - xy2d: FieldElement([ - -21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, - -24552282, 15673397, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, - 3727144, -12934448, - ]), - y_minus_x: FieldElement([ - 6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, - 31123697, -10958981, - ]), - xy2d: FieldElement([ - 30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, - -16432438, 9648165, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, - -26008332, -11377501, - ]), - y_minus_x: FieldElement([ - 17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, - 2662509, -16297073, - ]), - xy2d: FieldElement([ - -1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, - 32247248, -14389861, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, - -16027623, -13378845, - ]), - y_minus_x: FieldElement([ - -1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, - 17597934, 2346211, - ]), - xy2d: FieldElement([ - 18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, - 3059833, -11782870, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, - -33502212, 3592096, - ]), - y_minus_x: FieldElement([ - 33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, - -982665, -6837803, - ]), - xy2d: FieldElement([ - -32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, - 26353178, 693168, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, - 31375464, 14369965, - ]), - y_minus_x: FieldElement([ - -14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, - 17901441, 16011505, - ]), - xy2d: FieldElement([ - 18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, - -16046376, 8764035, 12309598, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, - 17544096, -10593782, - ]), - y_minus_x: FieldElement([ - 5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, - -5565381, 12348900, - ]), - xy2d: FieldElement([ - -31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, - -10632534, -585479, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, - 30944593, 1130208, - ]), - y_minus_x: FieldElement([ - 8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, - 23550156, -271232, - ]), - xy2d: FieldElement([ - 17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, - -408818, -137719, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, - 3260525, -7166271, - ]), - y_minus_x: FieldElement([ - -4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, - -12748722, 2701326, - ]), - xy2d: FieldElement([ - -8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, - 11329923, 1862132, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, - -9037963, -940300, - ]), - y_minus_x: FieldElement([ - 8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, - -15438168, 11595570, - ]), - xy2d: FieldElement([ - 15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, - -13489462, -4363670, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, - 977108, 699994, - ]), - y_minus_x: FieldElement([ - -12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, - -30534533, -14337913, - ]), - xy2d: FieldElement([ - 31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, - 12493932, -5409317, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, - 29375955, 6024730, - ]), - y_minus_x: FieldElement([ - 842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, - -9940361, 2854096, - ]), - xy2d: FieldElement([ - -4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, - 12121869, 16648078, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, - 20237806, 2838411, - ]), - y_minus_x: FieldElement([ - -19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, - 1068881, -12499905, - ]), - xy2d: FieldElement([ - -9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, - 12521378, 4845654, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, - -6482306, -12885870, - ]), - y_minus_x: FieldElement([ - -23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, - -3113227, 13974498, - ]), - xy2d: FieldElement([ - 12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, - 18895762, 12629579, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, - 32195181, 3895677, - ]), - y_minus_x: FieldElement([ - 10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, - -8982069, 4429647, - ]), - xy2d: FieldElement([ - -2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, - 18047436, -15281743, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, - -12850837, -17620701, -9408468, - ]), - y_minus_x: FieldElement([ - 21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, - -29958985, 8773375, - ]), - xy2d: FieldElement([ - -25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, - -7874389, -13920155, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, - -1721788, -2776725, - ]), - y_minus_x: FieldElement([ - -12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, - 28408820, 6816612, - ]), - xy2d: FieldElement([ - -10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, - -10339570, 5067943, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, - 5331210, -10105944, - ]), - y_minus_x: FieldElement([ - 30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, - 27679908, -1648928, - ]), - xy2d: FieldElement([ - 9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, - 22611444, -12715406, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, - 4854859, 6622139, - ]), - y_minus_x: FieldElement([ - -8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, - 26404409, 13001963, - ]), - xy2d: FieldElement([ - -31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, - -15405155, 11020693, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, - 28761762, 1406734, - ]), - y_minus_x: FieldElement([ - -448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, - 25577411, -13378680, - ]), - xy2d: FieldElement([ - -24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, - -9368926, 4745410, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, - 10931924, -11931931, - ]), - y_minus_x: FieldElement([ - -16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, - -20470770, 13434654, - ]), - xy2d: FieldElement([ - 22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, - 28445307, -5364710, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, - 9074234, 1167180, - ]), - y_minus_x: FieldElement([ - -26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, - 3843903, 9367684, - ]), - xy2d: FieldElement([ - -10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, - -139197, 4242895, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, - -27417366, 16625501, - ]), - y_minus_x: FieldElement([ - -11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, - -27351616, 14247413, - ]), - xy2d: FieldElement([ - 6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, - 27108877, -1180880, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, - -27148451, 981874, - ]), - y_minus_x: FieldElement([ - 22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, - -10479804, -16197962, - ]), - xy2d: FieldElement([ - -7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, - -15933690, 3797899, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, - 20491983, -8042152, - ]), - y_minus_x: FieldElement([ - 9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, - 30997318, -6703063, - ]), - xy2d: FieldElement([ - 7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, - 17649998, -250080, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, - -2233253, 7662146, - ]), - y_minus_x: FieldElement([ - -17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, - -3174674, 3440183, - ]), - xy2d: FieldElement([ - -19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, - 4862400, 1133, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, - -30594991, -7379421, - ]), - y_minus_x: FieldElement([ - -3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, - 18278453, 15405622, - ]), - xy2d: FieldElement([ - -4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, - -21875062, 13626197, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, - 13340839, 15928663, - ]), - y_minus_x: FieldElement([ - 31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, - -23452306, 5964753, - ]), - xy2d: FieldElement([ - 4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, - 2538215, -7570755, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, - -629256, -15958862, - ]), - y_minus_x: FieldElement([ - -26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, - 11659922, -11115118, - ]), - xy2d: FieldElement([ - 26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, - 33100372, -1306171, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, - -5931810, -14466380, - ]), - y_minus_x: FieldElement([ - 16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, - -10735770, -10039824, - ]), - xy2d: FieldElement([ - 28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, - 18036436, 5803270, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, - -6159431, -14117438, - ]), - y_minus_x: FieldElement([ - -31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, - -20095739, 11763584, - ]), - xy2d: FieldElement([ - -594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, - -19773211, -10713562, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, - -17020157, 992471, - ]), - y_minus_x: FieldElement([ - 18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, - -11574335, -10601610, - ]), - xy2d: FieldElement([ - 19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, - 26224780, 16452269, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, - -20533829, 3698650, - ]), - y_minus_x: FieldElement([ - 14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, - 27394301, 12015369, - ]), - xy2d: FieldElement([ - 19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, - -13259127, -3402461, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, - -8550524, -10393462, - ]), - y_minus_x: FieldElement([ - -5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, - -4976164, 12651793, - ]), - xy2d: FieldElement([ - -2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, - 9768698, -2533218, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, - 4940095, 10678226, - ]), - y_minus_x: FieldElement([ - 18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, - -24372348, 16582019, - ]), - xy2d: FieldElement([ - -15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, - -11003761, 7989037, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, - -19686113, -14800171, - ]), - y_minus_x: FieldElement([ - -17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, - 28550068, -6857132, - ]), - xy2d: FieldElement([ - -28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, - 1155602, 5988841, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, - 24618407, 8283181, - ]), - y_minus_x: FieldElement([ - -33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, - 1466169, 10740210, - ]), - xy2d: FieldElement([ - -1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, - -32164649, -15327040, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, - -3260321, -573935, - ]), - y_minus_x: FieldElement([ - 24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, - -7380369, -6144105, - ]), - xy2d: FieldElement([ - -28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, - -14453128, -1625486, -6494814, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, - -9906200, -621852, - ]), - y_minus_x: FieldElement([ - 5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, - -6171428, -15186581, - ]), - xy2d: FieldElement([ - -4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, - -1558923, -9863646, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, - -30581476, -15757844, - ]), - y_minus_x: FieldElement([ - 10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, - 24003793, 14264025, - ]), - xy2d: FieldElement([ - 27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, - 13958495, -5732453, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, - 7340521, -15410068, - ]), - y_minus_x: FieldElement([ - 4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, - -15066784, -10375192, - ]), - xy2d: FieldElement([ - -17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, - -8843694, 3849921, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, - 5002294, -15550259, - ]), - y_minus_x: FieldElement([ - -12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, - -25845716, 12741426, - ]), - xy2d: FieldElement([ - -5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, - 28834118, -7646072, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, - 20461858, 5491305, - ]), - y_minus_x: FieldElement([ - 13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, - 11293807, -28588204, -9421832, - ]), - xy2d: FieldElement([ - 28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, - 29867745, -8795943, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, - 22853429, -4012011, - ]), - y_minus_x: FieldElement([ - 24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, - 12876623, -2112447, - ]), - xy2d: FieldElement([ - 17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, - 16031844, 3723494, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, - 23896954, -4314245, - ]), - y_minus_x: FieldElement([ - -20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, - -9919646, -8826859, - ]), - xy2d: FieldElement([ - 28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, - -18051778, -2082915, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, - -19715240, 7847707, - ]), - y_minus_x: FieldElement([ - 10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, - -10020567, 3852848, - ]), - xy2d: FieldElement([ - -11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, - 29330899, -15076224, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, - -27548796, 12314391, - ]), - y_minus_x: FieldElement([ - 15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, - 16103996, -3731215, - ]), - xy2d: FieldElement([ - -23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, - -29815713, -9841101, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, - -25205859, 2739713, - ]), - y_minus_x: FieldElement([ - 21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, - 4931255, 11987849, - ]), - xy2d: FieldElement([ - -7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, - -30076310, 10117930, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, - 12890019, 15728940, - ]), - y_minus_x: FieldElement([ - -21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, - 12885167, 8311031, - ]), - xy2d: FieldElement([ - -17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, - 1888765, -5435404, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, - -12707869, -19464434, -3340243, - ]), - y_minus_x: FieldElement([ - -23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, - 17151279, -9854116, - ]), - xy2d: FieldElement([ - -24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, - -16866484, 16176525, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, - -28746253, -10197509, - ]), - y_minus_x: FieldElement([ - -10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, - 32808310, 1099883, - ]), - xy2d: FieldElement([ - 15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, - 2051441, -15225865, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, - -22686354, 16633660, - ]), - y_minus_x: FieldElement([ - 4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, - 8402478, -9864273, - ]), - xy2d: FieldElement([ - -28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, - 17983010, 9967138, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, - 17507396, 3651560, - ]), - y_minus_x: FieldElement([ - -10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, - -18553322, -11357135, - ]), - xy2d: FieldElement([ - 2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, - -5605463, -7621941, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, - 17258084, -7977739, - ]), - y_minus_x: FieldElement([ - 18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, - 26908270, 12150756, - ]), - xy2d: FieldElement([ - -30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, - -32302074, 16215819, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, - -7503072, -8675347, - ]), - y_minus_x: FieldElement([ - -27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, - 21676107, -1943028, - ]), - xy2d: FieldElement([ - 21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, - -3556191, -7913075, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, - -18907032, -9662799, - ]), - y_minus_x: FieldElement([ - -2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, - 25459437, -4564609, - ]), - xy2d: FieldElement([ - -25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, - -6432418, -1644817, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, - 6335692, 7249989, - ]), - y_minus_x: FieldElement([ - -30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, - 25993170, -12478523, - ]), - xy2d: FieldElement([ - 4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, - 31820368, 15075278, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, - -17970238, 12833045, - ]), - y_minus_x: FieldElement([ - 19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, - -9413939, 14905377, - ]), - xy2d: FieldElement([ - 24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, - 27628530, -7555480, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, - -14110875, 15297016, - ]), - y_minus_x: FieldElement([ - 510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, - 8683221, 2921426, - ]), - xy2d: FieldElement([ - 18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, - 4674690, 13890525, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, - 4389687, 288396, - ]), - y_minus_x: FieldElement([ - 9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, - 23388070, 16052080, - ]), - xy2d: FieldElement([ - 12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, - -14754271, -10812892, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, - -28822128, 929275, - ]), - y_minus_x: FieldElement([ - 11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, - -9371869, -21393143, 2465074, - ]), - xy2d: FieldElement([ - 20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, - 2463391, -4622140, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, - 4003896, 12673717, - ]), - y_minus_x: FieldElement([ - -1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, - 7903886, 2348101, - ]), - xy2d: FieldElement([ - 24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, - -28377538, 10048127, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, - -32297756, 15221632, - ]), - y_minus_x: FieldElement([ - -26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, - 29769758, 6593415, - ]), - xy2d: FieldElement([ - -31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, - 30958054, 8292160, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, - 26143136, -3148876, - ]), - y_minus_x: FieldElement([ - 22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, - -3758243, -2304625, - ]), - xy2d: FieldElement([ - -15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, - -16664475, 8194478, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, - 28572286, 3005164, - ]), - y_minus_x: FieldElement([ - 26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, - -26094821, -13079595, - ]), - xy2d: FieldElement([ - -7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, - 32208683, -1198248, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, - -27672585, -11539858, - ]), - y_minus_x: FieldElement([ - 15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, - 15397331, -4130193, - ]), - xy2d: FieldElement([ - 8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, - -27505566, 15087184, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, - 16788528, -5868942, - ]), - y_minus_x: FieldElement([ - -1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, - 31322514, -11615635, - ]), - xy2d: FieldElement([ - 21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, - 13040862, -12112948, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, - 1613711, 4896935, - ]), - y_minus_x: FieldElement([ - -25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, - -23971776, -11267415, - ]), - xy2d: FieldElement([ - -15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, - -23753030, -8436416, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, - 23097949, -566018, - ]), - y_minus_x: FieldElement([ - 4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, - -18512313, 2424778, - ]), - xy2d: FieldElement([ - 366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, - -3642553, 12412659, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, - -3141501, 11179385, - ]), - y_minus_x: FieldElement([ - 18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, - -18684645, -11443503, - ]), - xy2d: FieldElement([ - 476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, - 11052904, 5219329, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, - 9014762, -8579056, - ]), - y_minus_x: FieldElement([ - -13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, - 8473550, -3256838, - ]), - xy2d: FieldElement([ - 24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, - -24568481, 11788948, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, - -24568470, -1458691, - ]), - y_minus_x: FieldElement([ - -15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, - -18513325, 8464118, - ]), - xy2d: FieldElement([ - -23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, - 33313881, -8370517, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, - -8940735, -5818269, - ]), - y_minus_x: FieldElement([ - -6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, - 28358192, -4253904, - ]), - xy2d: FieldElement([ - -23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, - -5351664, 5596589, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, - 1768301, 15373193, - ]), - y_minus_x: FieldElement([ - -7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, - -24660491, 3442910, - ]), - xy2d: FieldElement([ - -30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, - -18585478, 13365930, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, - -9689599, -3031667, - ]), - y_minus_x: FieldElement([ - 25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, - 15723479, -15163481, - ]), - xy2d: FieldElement([ - -9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, - 519526, -1235876, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, - 16648397, 7605640, - ]), - y_minus_x: FieldElement([ - -8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, - -9468848, 4763278, - ]), - xy2d: FieldElement([ - -21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, - -11356529, 728112, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, - -29365436, -5498272, - ]), - y_minus_x: FieldElement([ - 17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, - 10750447, 10014012, - ]), - xy2d: FieldElement([ - -10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, - 3424691, 7540221, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, - -9244265, 15258046, - ]), - y_minus_x: FieldElement([ - 13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, - 1062915, -5136345, - ]), - xy2d: FieldElement([ - -19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, - 32960380, 1459310, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, - 18638003, -11174937, - ]), - y_minus_x: FieldElement([ - 31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, - -6643087, -5442636, - ]), - xy2d: FieldElement([ - -9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, - -32294889, -6456008, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, - -7852844, -8138429, - ]), - y_minus_x: FieldElement([ - -15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, - -9431476, 5203576, - ]), - xy2d: FieldElement([ - 31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, - 31809243, 7347066, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, - 15192876, -2087490, - ]), - y_minus_x: FieldElement([ - -12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, - 33473243, -13382104, - ]), - xy2d: FieldElement([ - 33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, - -6135662, 14480053, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, - 27595050, 8737275, - ]), - y_minus_x: FieldElement([ - -20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, - 26498114, 66511, - ]), - xy2d: FieldElement([ - 22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, - -13327787, -7515095, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, - -17127465, 15115439, - ]), - y_minus_x: FieldElement([ - 23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, - 8236921, 16492939, - ]), - xy2d: FieldElement([ - -23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, - 6708380, -6222424, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, - 29955601, -11678310, - ]), - y_minus_x: FieldElement([ - 3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, - -9917575, 6216608, - ]), - xy2d: FieldElement([ - -32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, - -26767480, 7525079, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, - -19386633, 11994101, - ]), - y_minus_x: FieldElement([ - 21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, - -7477437, 13381418, - ]), - xy2d: FieldElement([ - 18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, - 28535282, 15779576, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, - 9955285, -16303356, - ]), - y_minus_x: FieldElement([ - 9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, - 20194861, 13380996, - ]), - xy2d: FieldElement([ - -26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, - 26342023, 10146099, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - -26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, - 21612326, -545728, - ]), - y_minus_x: FieldElement([ - -13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, - -20856566, 11649658, - ]), - xy2d: FieldElement([ - -10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, - 33514190, 2333242, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, - 24350578, -13450001, - ]), - y_minus_x: FieldElement([ - -4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, - 8317860, 12352766, - ]), - xy2d: FieldElement([ - 18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, - 20712163, 6719373, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, - 26224235, -3297458, - ]), - y_minus_x: FieldElement([ - -17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, - 18620611, -16428628, - ]), - xy2d: FieldElement([ - -13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, - -30701573, -16479657, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, - 13735342, 1934062, - ]), - y_minus_x: FieldElement([ - 25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, - -21277997, 5473616, - ]), - xy2d: FieldElement([ - 31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, - 29111212, -5451014, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, - -10295587, 452383, - ]), - y_minus_x: FieldElement([ - -25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, - 15766062, 8407814, - ]), - xy2d: FieldElement([ - -20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, - -8067909, 2276718, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, - 22740376, -7303417, - ]), - y_minus_x: FieldElement([ - 2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, - -15637599, 13295222, - ]), - xy2d: FieldElement([ - 33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, - -4451357, -14669444, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, - 7770569, 9620597, - ]), - y_minus_x: FieldElement([ - 23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, - -33502340, -14767970, - ]), - xy2d: FieldElement([ - 1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, - 30494170, -11440799, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, - -1684339, -13333647, - ]), - y_minus_x: FieldElement([ - 13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, - 3353509, 4033511, - ]), - xy2d: FieldElement([ - -29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, - 27485041, 7356032, - ]), - }, - ], - [ - PreComputedGroupElement { - y_plus_x: FieldElement([ - 9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, - 28431036, -16771834, - ]), - y_minus_x: FieldElement([ - -23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, - 15970762, 4099461, - ]), - xy2d: FieldElement([ - 29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, - 11465739, 8317062, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, - 20969334, -5157516, - ]), - y_minus_x: FieldElement([ - -20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, - 11177095, 14989547, - ]), - xy2d: FieldElement([ - -24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, - -28487508, 9930240, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, - -13403753, 16291481, -5314038, - ]), - y_minus_x: FieldElement([ - -33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, - 4368891, 9788741, - ]), - xy2d: FieldElement([ - 16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, - 16250552, -11111103, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, - -30190403, 4782747, - ]), - y_minus_x: FieldElement([ - -1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, - 22546403, 437323, - ]), - xy2d: FieldElement([ - 31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, - -30633590, -14097016, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, - -19284671, -6114373, - ]), - y_minus_x: FieldElement([ - 15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, - -29380133, 11754228, - ]), - xy2d: FieldElement([ - -2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, - 22761615, -10134141, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - 16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, - 32851222, 11717399, - ]), - y_minus_x: FieldElement([ - 11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, - -1193175, -4030831, - ]), - xy2d: FieldElement([ - -185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, - -19985279, -3948376, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, - -30772034, -15486313, - ]), - y_minus_x: FieldElement([ - -18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, - 10928917, 3011958, - ]), - xy2d: FieldElement([ - -6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, - 18008031, 10258577, - ]), - }, - PreComputedGroupElement { - y_plus_x: FieldElement([ - -22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, - 25127874, 6671743, - ]), - y_minus_x: FieldElement([ - 29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, - 25838796, 4642684, - ]), - xy2d: FieldElement([ - -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, - 18423289, 4177476, - ]), - }, - ], -]; diff --git a/sdk/core/src/crypto/edwards25519/extended_group_element.rs b/sdk/core/src/crypto/edwards25519/extended_group_element.rs deleted file mode 100644 index 6315fc5627..0000000000 --- a/sdk/core/src/crypto/edwards25519/extended_group_element.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::{ - completed_group_element::CompletedGroupElement, field_element::FieldElement, - pre_computed_group_element::PreComputedGroupElement, - projective_group_element::ProjectiveGroupElement, -}; - -#[derive(Default, Copy, Clone, Debug)] -pub struct ExtendedGroupElement { - pub x: FieldElement, - pub y: FieldElement, - pub z: FieldElement, - pub t: FieldElement, -} - -impl ExtendedGroupElement { - pub fn to_projective(self, r: &mut ProjectiveGroupElement) { - r.x.fe_copy(&self.x); - r.y.fe_copy(&self.y); - r.z.fe_copy(&self.z); - } - - pub fn double(&self, r: &mut CompletedGroupElement) { - let mut q = ProjectiveGroupElement::default(); - - self.to_projective(&mut q); - q.double(r); - } - - pub fn zero(&mut self) { - self.x.fe_zero(); - self.y.fe_one(); - self.z.fe_one(); - self.t.fe_zero(); - } - - // ge_scalar_mult_base computes h = a*B, where - // a = a[0]+256*a[1]+...+256^31 a[31] - // B is the Ed25519 base point (x,4/5) with x positive. - // - // Preconditions: - // a[31] <= 127 - #[allow(clippy::needless_range_loop)] - pub fn ge_scalar_mult_base(&mut self, a: [u8; 32]) { - let mut e = [0i8; 64]; - for (i, v) in a.iter().enumerate() { - e[2 * i] = (v & 15) as i8; - e[2 * i + 1] = ((v >> 4) & 15) as i8; - } - - // each e[i] is between 0 and 15 and e[63] is between 0 and 7. - - let mut carry: i8 = 0; - for i in 0..63 { - e[i] += carry; - carry = (e[i] + 8) >> 4; - e[i] -= carry << 4; - } - - e[63] += carry; - // each e[i] is between -8 and 8. - - self.zero(); - let mut t = PreComputedGroupElement::default(); - let mut r = CompletedGroupElement::default(); - for i in (1..64).step_by(2) { - t.select_point(i / 2, e[i as usize] as i32); - r.ge_mixed_add(self, &t); - r.to_extended(self); - } - - let mut s = ProjectiveGroupElement::default(); - - self.double(&mut r); - r.to_projective(&mut s); - s.double(&mut r); - r.to_projective(&mut s); - s.double(&mut r); - r.to_projective(&mut s); - s.double(&mut r); - r.to_extended(self); - - for i in (0..64).step_by(2) { - t.select_point(i / 2, e[i as usize] as i32); - r.ge_mixed_add(self, &t); - r.to_extended(self); - } - } - - pub fn to_bytes(self) -> [u8; 32] { - let mut recip = FieldElement::default(); - let mut x = FieldElement::default(); - let mut y = FieldElement::default(); - - recip.fe_invert(&self.z); - x.fe_mul(&self.x, &recip); - y.fe_mul(&self.y, &recip); - let mut s = y.to_bytes(); - - s[31] ^= x.fe_is_negative() << 7; - - s - } -} diff --git a/sdk/core/src/crypto/edwards25519/field_element.rs b/sdk/core/src/crypto/edwards25519/field_element.rs deleted file mode 100644 index afa56c4dda..0000000000 --- a/sdk/core/src/crypto/edwards25519/field_element.rs +++ /dev/null @@ -1,637 +0,0 @@ -use std::marker::Copy; - -#[derive(Default, Copy, Clone, Debug)] -pub struct FieldElement(pub [i32; 10]); - -const ZERO: FieldElement = FieldElement([0; 10]); - -impl FieldElement { - pub fn fe_zero(&mut self) { - self.0.copy_from_slice(&ZERO.0); - } - - pub fn fe_one(&mut self) { - self.fe_zero(); - self.0[0] = 1; - } - - pub fn fe_add(&mut self, a: &FieldElement, b: &FieldElement) { - self.0[0] = a.0[0] + b.0[0]; - self.0[1] = a.0[1] + b.0[1]; - self.0[2] = a.0[2] + b.0[2]; - self.0[3] = a.0[3] + b.0[3]; - self.0[4] = a.0[4] + b.0[4]; - self.0[5] = a.0[5] + b.0[5]; - self.0[6] = a.0[6] + b.0[6]; - self.0[7] = a.0[7] + b.0[7]; - self.0[8] = a.0[8] + b.0[8]; - self.0[9] = a.0[9] + b.0[9]; - } - - pub fn fe_sub(&mut self, a: &FieldElement, b: &FieldElement) { - self.0[0] = a.0[0] - b.0[0]; - self.0[1] = a.0[1] - b.0[1]; - self.0[2] = a.0[2] - b.0[2]; - self.0[3] = a.0[3] - b.0[3]; - self.0[4] = a.0[4] - b.0[4]; - self.0[5] = a.0[5] - b.0[5]; - self.0[6] = a.0[6] - b.0[6]; - self.0[7] = a.0[7] - b.0[7]; - self.0[8] = a.0[8] - b.0[8]; - self.0[9] = a.0[9] - b.0[9]; - } - - fn fe_square_internal(&self) -> (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) { - let f0 = self.0[0] as i64; - let f1 = self.0[1] as i64; - let f2 = self.0[2] as i64; - let f3 = self.0[3] as i64; - let f4 = self.0[4] as i64; - let f5 = self.0[5] as i64; - let f6 = self.0[6] as i64; - let f7 = self.0[7] as i64; - let f8 = self.0[8] as i64; - let f9 = self.0[9] as i64; - let f0_2 = (2 * self.0[0]) as i64; - let f1_2 = (2 * self.0[1]) as i64; - let f2_2 = (2 * self.0[2]) as i64; - let f3_2 = (2 * self.0[3]) as i64; - let f4_2 = (2 * self.0[4]) as i64; - let f5_2 = (2 * self.0[5]) as i64; - let f6_2 = (2 * self.0[6]) as i64; - let f7_2 = (2 * self.0[7]) as i64; - let f5_38 = 38 * f5; // 1.31*2^30 - let f6_19 = 19 * f6; // 1.31*2^30 - let f7_38 = 38 * f7; // 1.31*2^30 - let f8_19 = 19 * f8; // 1.31*2^30 - let f9_38 = 38 * f9; // 1.31*2^30 - - let h0 = f0 * f0 + f1_2 * f9_38 + f2_2 * f8_19 + f3_2 * f7_38 + f4_2 * f6_19 + f5 * f5_38; - let h1 = f0_2 * f1 + f2 * f9_38 + f3_2 * f8_19 + f4 * f7_38 + f5_2 * f6_19; - let h2 = f0_2 * f2 + f1_2 * f1 + f3_2 * f9_38 + f4_2 * f8_19 + f5_2 * f7_38 + f6 * f6_19; - let h3 = f0_2 * f3 + f1_2 * f2 + f4 * f9_38 + f5_2 * f8_19 + f6 * f7_38; - let h4 = f0_2 * f4 + f1_2 * f3_2 + f2 * f2 + f5_2 * f9_38 + f6_2 * f8_19 + f7 * f7_38; - let h5 = f0_2 * f5 + f1_2 * f4 + f2_2 * f3 + f6 * f9_38 + f7_2 * f8_19; - let h6 = f0_2 * f6 + f1_2 * f5_2 + f2_2 * f4 + f3_2 * f3 + f7_2 * f9_38 + f8 * f8_19; - let h7 = f0_2 * f7 + f1_2 * f6 + f2_2 * f5 + f3_2 * f4 + f8 * f9_38; - let h8 = f0_2 * f8 + f1_2 * f7_2 + f2_2 * f6 + f3_2 * f5_2 + f4 * f4 + f9 * f9_38; - let h9 = f0_2 * f9 + f1_2 * f8 + f2_2 * f7 + f3_2 * f6 + f4_2 * f5; - - (h0, h1, h2, h3, h4, h5, h6, h7, h8, h9) - } - - // fe_square sets h = 2 * f * f - // - // Can overlap h with f. - // - // Preconditions: - // |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. - // - // Postconditions: - // |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. - // See fe_mul.c for discussion of implementation strategy. - pub fn fe_square(&mut self, f: &FieldElement) { - let (h0, h1, h2, h3, h4, h5, h6, h7, h8, h9) = f.fe_square_internal(); - self.fe_combine(h0, h1, h2, h3, h4, h5, h6, h7, h8, h9); - } - - pub fn fe_square_2(&mut self, f: &FieldElement) { - let (mut h0, mut h1, mut h2, mut h3, mut h4, mut h5, mut h6, mut h7, mut h8, mut h9) = - f.fe_square_internal(); - - h0 += h0; - h1 += h1; - h2 += h2; - h3 += h3; - h4 += h4; - h5 += h5; - h6 += h6; - h7 += h7; - h8 += h8; - h9 += h9; - - self.fe_combine(h0, h1, h2, h3, h4, h5, h6, h7, h8, h9); - } - - pub fn fe_copy(&mut self, src: &FieldElement) { - self.0.copy_from_slice(&src.0); - } - - pub fn fe_cmove(&mut self, g: &FieldElement, b: i32) { - let b = -b; - self.0[0] ^= b & (self.0[0] ^ g.0[0]); - self.0[1] ^= b & (self.0[1] ^ g.0[1]); - self.0[2] ^= b & (self.0[2] ^ g.0[2]); - self.0[3] ^= b & (self.0[3] ^ g.0[3]); - self.0[4] ^= b & (self.0[4] ^ g.0[4]); - self.0[5] ^= b & (self.0[5] ^ g.0[5]); - self.0[6] ^= b & (self.0[6] ^ g.0[6]); - self.0[7] ^= b & (self.0[7] ^ g.0[7]); - self.0[8] ^= b & (self.0[8] ^ g.0[8]); - self.0[9] ^= b & (self.0[9] ^ g.0[9]); - } - - // FeNeg sets h = -f - // - // Preconditions: - // |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. - // - // Postconditions: - // |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. - pub fn fe_neg(&mut self, f: &FieldElement) { - self.0[0] = -f.0[0]; - self.0[1] = -f.0[1]; - self.0[2] = -f.0[2]; - self.0[3] = -f.0[3]; - self.0[4] = -f.0[4]; - self.0[5] = -f.0[5]; - self.0[6] = -f.0[6]; - self.0[7] = -f.0[7]; - self.0[8] = -f.0[8]; - self.0[9] = -f.0[9]; - } - - #[allow(clippy::too_many_arguments)] - pub fn fe_combine( - &mut self, - h0: i64, - h1: i64, - h2: i64, - h3: i64, - h4: i64, - h5: i64, - h6: i64, - h7: i64, - h8: i64, - h9: i64, - ) { - let mut h0 = h0; - let mut h1 = h1; - let mut h2 = h2; - let mut h3 = h3; - let mut h4 = h4; - let mut h5 = h5; - let mut h6 = h6; - let mut h7 = h7; - let mut h8 = h8; - let mut h9 = h9; - - /* - |h0| <= (1.1*1.1*2^52*(1+19+19+19+19)+1.1*1.1*2^50*(38+38+38+38+38)) - i.e. |h0| <= 1.2*2^59; narrower ranges for h2, h4, h6, h8 - |h1| <= (1.1*1.1*2^51*(1+1+19+19+19+19+19+19+19+19)) - i.e. |h1| <= 1.5*2^58; narrower ranges for h3, h5, h7, h9 - */ - - let mut c0 = (h0 + (1 << 25)) >> 26; - h1 += c0; - h0 -= c0 << 26; - let c4 = (h4 + (1 << 25)) >> 26; - h5 += c4; - h4 -= c4 << 26; - /* |h0| <= 2^25 */ - /* |h4| <= 2^25 */ - /* |h1| <= 1.51*2^58 */ - /* |h5| <= 1.51*2^58 */ - - let c1 = (h1 + (1 << 24)) >> 25; - h2 += c1; - h1 -= c1 << 25; - let c5 = (h5 + (1 << 24)) >> 25; - h6 += c5; - h5 -= c5 << 25; - /* |h1| <= 2^24; from now on fits into int32 */ - /* |h5| <= 2^24; from now on fits into int32 */ - /* |h2| <= 1.21*2^59 */ - /* |h6| <= 1.21*2^59 */ - - let c2 = (h2 + (1 << 25)) >> 26; - h3 += c2; - h2 -= c2 << 26; - let c6 = (h6 + (1 << 25)) >> 26; - h7 += c6; - h6 -= c6 << 26; - /* |h2| <= 2^25; from now on fits into int32 unchanged */ - /* |h6| <= 2^25; from now on fits into int32 unchanged */ - /* |h3| <= 1.51*2^58 */ - /* |h7| <= 1.51*2^58 */ - - let c3 = (h3 + (1 << 24)) >> 25; - h4 += c3; - h3 -= c3 << 25; - let c7 = (h7 + (1 << 24)) >> 25; - h8 += c7; - h7 -= c7 << 25; - /* |h3| <= 2^24; from now on fits into int32 unchanged */ - /* |h7| <= 2^24; from now on fits into int32 unchanged */ - /* |h4| <= 1.52*2^33 */ - /* |h8| <= 1.52*2^33 */ - - let c4 = (h4 + (1 << 25)) >> 26; - h5 += c4; - h4 -= c4 << 26; - let c8 = (h8 + (1 << 25)) >> 26; - h9 += c8; - h8 -= c8 << 26; - /* |h4| <= 2^25; from now on fits into int32 unchanged */ - /* |h8| <= 2^25; from now on fits into int32 unchanged */ - /* |h5| <= 1.01*2^24 */ - /* |h9| <= 1.51*2^58 */ - - let c9 = (h9 + (1 << 24)) >> 25; - h0 += c9 * 19; - h9 -= c9 << 25; - /* |h9| <= 2^24; from now on fits into int32 unchanged */ - /* |h0| <= 1.8*2^37 */ - - c0 = (h0 + (1 << 25)) >> 26; - h1 += c0; - h0 -= c0 << 26; - /* |h0| <= 2^25; from now on fits into int32 unchanged */ - /* |h1| <= 1.01*2^24 */ - - self.0[0] = h0 as i32; - self.0[1] = h1 as i32; - self.0[2] = h2 as i32; - self.0[3] = h3 as i32; - self.0[4] = h4 as i32; - self.0[5] = h5 as i32; - self.0[6] = h6 as i32; - self.0[7] = h7 as i32; - self.0[8] = h8 as i32; - self.0[9] = h9 as i32; - } - - // FeMul calculates h = f * g - // Can overlap h with f or g. - // - // Preconditions: - // |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. - // |g| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. - // - // Postconditions: - // |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. - // - // Notes on implementation strategy: - // - // Using schoolbook multiplication. - // Karatsuba would save a little in some cost models. - // - // Most multiplications by 2 and 19 are 32-bit precomputations; - // cheaper than 64-bit postcomputations. - // - // There is one remaining multiplication by 19 in the carry chain; - // one *19 precomputation can be merged into this, - // but the resulting data flow is considerably less clean. - // - // There are 12 carries below. - // 10 of them are 2-way parallelizable and vectorizable. - // Can get away with 11 carries, but then data flow is much deeper. - // - // With tighter constraints on inputs, can squeeze carries into int32. - pub fn fe_mul(&mut self, f: &FieldElement, g: &FieldElement) { - let f0 = f.0[0] as i64; - let f1 = f.0[1] as i64; - let f2 = f.0[2] as i64; - let f3 = f.0[3] as i64; - let f4 = f.0[4] as i64; - let f5 = f.0[5] as i64; - let f6 = f.0[6] as i64; - let f7 = f.0[7] as i64; - let f8 = f.0[8] as i64; - let f9 = f.0[9] as i64; - - let f1_2 = (2 * f.0[1]) as i64; - let f3_2 = (2 * f.0[3]) as i64; - let f5_2 = (2 * f.0[5]) as i64; - let f7_2 = (2 * f.0[7]) as i64; - let f9_2 = (2 * f.0[9]) as i64; - - let g0 = g.0[0] as i64; - let g1 = g.0[1] as i64; - let g2 = g.0[2] as i64; - let g3 = g.0[3] as i64; - let g4 = g.0[4] as i64; - let g5 = g.0[5] as i64; - let g6 = g.0[6] as i64; - let g7 = g.0[7] as i64; - let g8 = g.0[8] as i64; - let g9 = g.0[9] as i64; - - let g1_19 = (19 * g.0[1]) as i64; /* 1.4*2^29 */ - let g2_19 = (19 * g.0[2]) as i64; /* 1.4*2^30; still ok */ - let g3_19 = (19 * g.0[3]) as i64; - let g4_19 = (19 * g.0[4]) as i64; - let g5_19 = (19 * g.0[5]) as i64; - let g6_19 = (19 * g.0[6]) as i64; - let g7_19 = (19 * g.0[7]) as i64; - let g8_19 = (19 * g.0[8]) as i64; - let g9_19 = (19 * g.0[9]) as i64; - - let h0 = f0 * g0 - + f1_2 * g9_19 - + f2 * g8_19 - + f3_2 * g7_19 - + f4 * g6_19 - + f5_2 * g5_19 - + f6 * g4_19 - + f7_2 * g3_19 - + f8 * g2_19 - + f9_2 * g1_19; - let h1 = f0 * g1 - + f1 * g0 - + f2 * g9_19 - + f3 * g8_19 - + f4 * g7_19 - + f5 * g6_19 - + f6 * g5_19 - + f7 * g4_19 - + f8 * g3_19 - + f9 * g2_19; - let h2 = f0 * g2 - + f1_2 * g1 - + f2 * g0 - + f3_2 * g9_19 - + f4 * g8_19 - + f5_2 * g7_19 - + f6 * g6_19 - + f7_2 * g5_19 - + f8 * g4_19 - + f9_2 * g3_19; - let h3 = f0 * g3 - + f1 * g2 - + f2 * g1 - + f3 * g0 - + f4 * g9_19 - + f5 * g8_19 - + f6 * g7_19 - + f7 * g6_19 - + f8 * g5_19 - + f9 * g4_19; - let h4 = f0 * g4 - + f1_2 * g3 - + f2 * g2 - + f3_2 * g1 - + f4 * g0 - + f5_2 * g9_19 - + f6 * g8_19 - + f7_2 * g7_19 - + f8 * g6_19 - + f9_2 * g5_19; - let h5 = f0 * g5 - + f1 * g4 - + f2 * g3 - + f3 * g2 - + f4 * g1 - + f5 * g0 - + f6 * g9_19 - + f7 * g8_19 - + f8 * g7_19 - + f9 * g6_19; - let h6 = f0 * g6 - + f1_2 * g5 - + f2 * g4 - + f3_2 * g3 - + f4 * g2 - + f5_2 * g1 - + f6 * g0 - + f7_2 * g9_19 - + f8 * g8_19 - + f9_2 * g7_19; - let h7 = f0 * g7 - + f1 * g6 - + f2 * g5 - + f3 * g4 - + f4 * g3 - + f5 * g2 - + f6 * g1 - + f7 * g0 - + f8 * g9_19 - + f9 * g8_19; - let h8 = f0 * g8 - + f1_2 * g7 - + f2 * g6 - + f3_2 * g5 - + f4 * g4 - + f5_2 * g3 - + f6 * g2 - + f7_2 * g1 - + f8 * g0 - + f9_2 * g9_19; - let h9 = f0 * g9 - + f1 * g8 - + f2 * g7 - + f3 * g6 - + f4 * g5 - + f5 * g4 - + f6 * g3 - + f7 * g2 - + f8 * g1 - + f9 * g0; - - self.fe_combine(h0, h1, h2, h3, h4, h5, h6, h7, h8, h9); - } - - pub fn fe_invert(&mut self, z: &FieldElement) { - let mut t0 = FieldElement::default(); - let mut t1 = FieldElement::default(); - let mut t2 = FieldElement::default(); - let mut t3 = FieldElement::default(); - - t0.fe_square(z); // 2^1 - t1.fe_square(&t0); // 2^2 - - for _ in 1..2 { - // 2^3 - t1.fe_square(&t1.clone()); - } - - t1.fe_mul(z, &t1.clone()); // 2^3 + 2^0 - t0.fe_mul(&t0.clone(), &t1); // 2^3 + 2^1 + 2^0 - t2.fe_square(&t0); // 2^4 + 2^2 + 2^1 - t1.fe_mul(&t1.clone(), &t2); // 2^4 + 2^3 + 2^2 + 2^1 + 2^0 - t2.fe_square(&t1); // 5,4,3,2,1 - - for _ in 1..5 { - // 9,8,7,6,5 - t2.fe_square(&t2.clone()); - } - - t1.fe_mul(&t2, &t1.clone()); // 9,8,7,6,5,4,3,2,1,0 - t2.fe_square(&t1); // 10..1 - - for _ in 1..10 { - // 19..10 - t2.fe_square(&t2.clone()); - } - - t2.fe_mul(&t2.clone(), &t1); // 19..0 - t3.fe_square(&t2); // 20..1 - - for _ in 1..20 { - // 39..20 - t3.fe_square(&t3.clone()); - } - - t2.fe_mul(&t3, &t2.clone()); // 39..0 - t2.fe_square(&t2.clone()); // 40..1 - - for _ in 1..10 { - // 49..10 - t2.fe_square(&t2.clone()); - } - - t1.fe_mul(&t2, &t1.clone()); // 49..0 - t2.fe_square(&t1); // 50..1 - - for _ in 1..50 { - // 99..50 - t2.fe_square(&t2.clone()); - } - - t2.fe_mul(&t2.clone(), &t1); // 99..0 - t3.fe_square(&t2); // 100..1 - - for _ in 1..100 { - // 199..100 - t3.fe_square(&t3.clone()); - } - - t2.fe_mul(&t3, &t2.clone()); // 199..0 - t2.fe_square(&t2.clone()); // 200..1 - - for _ in 1..50 { - // 249..50 - t2.fe_square(&t2.clone()); - } - - t1.fe_mul(&t2, &t1.clone()); // 249..0 - t1.fe_square(&t1.clone()); // 250..1 - - for _ in 1..5 { - // 254..5 - t1.fe_square(&t1.clone()); - } - - self.fe_mul(&t1, &t0); // 254..5,3,1,0 - } - - // to_bytes marshals h to s. - // Preconditions: - // |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. - // - // Write p=2^255-19; q=floor(h/p). - // Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). - // - // Proof: - // Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. - // Also have |h-2^230 h9|<2^230 so |19 2^(-255)(h-2^230 h9)|<1/4. - // - // Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). - // Then 0 [u8; 32] { - let mut carry = [0i32; 10]; - - let mut q = (19 * self.0[9] + (1 << 24)) >> 25; - q = (self.0[0] + q) >> 26; - q = (self.0[1] + q) >> 25; - q = (self.0[2] + q) >> 26; - q = (self.0[3] + q) >> 25; - q = (self.0[4] + q) >> 26; - q = (self.0[5] + q) >> 25; - q = (self.0[6] + q) >> 26; - q = (self.0[7] + q) >> 25; - q = (self.0[8] + q) >> 26; - q = (self.0[9] + q) >> 25; - - // Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. - self.0[0] += 19 * q; - // Goal: Output h-2^255 q, which is between 0 and 2^255-20. - - carry[0] = self.0[0] >> 26; - self.0[1] += carry[0]; - self.0[0] -= carry[0] << 26; - carry[1] = self.0[1] >> 25; - self.0[2] += carry[1]; - self.0[1] -= carry[1] << 25; - carry[2] = self.0[2] >> 26; - self.0[3] += carry[2]; - self.0[2] -= carry[2] << 26; - carry[3] = self.0[3] >> 25; - self.0[4] += carry[3]; - self.0[3] -= carry[3] << 25; - carry[4] = self.0[4] >> 26; - self.0[5] += carry[4]; - self.0[4] -= carry[4] << 26; - carry[5] = self.0[5] >> 25; - self.0[6] += carry[5]; - self.0[5] -= carry[5] << 25; - carry[6] = self.0[6] >> 26; - self.0[7] += carry[6]; - self.0[6] -= carry[6] << 26; - carry[7] = self.0[7] >> 25; - self.0[8] += carry[7]; - self.0[7] -= carry[7] << 25; - carry[8] = self.0[8] >> 26; - self.0[9] += carry[8]; - self.0[8] -= carry[8] << 26; - carry[9] = self.0[9] >> 25; - self.0[9] -= carry[9] << 25; - // h10 = carry9 - - // Goal: Output h[0]+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. - // Have h[0]+...+2^230 h[9] between 0 and 2^255-1; - // evidently 2^255 h10-2^255 q = 0. - // Goal: Output h[0]+...+2^230 h[9]. - - [ - (self.0[0] >> 0) as u8, - (self.0[0] >> 8) as u8, - (self.0[0] >> 16) as u8, - ((self.0[0] >> 24) | (self.0[1] << 2)) as u8, - (self.0[1] >> 6) as u8, - (self.0[1] >> 14) as u8, - ((self.0[1] >> 22) | (self.0[2] << 3)) as u8, - (self.0[2] >> 5) as u8, - (self.0[2] >> 13) as u8, - ((self.0[2] >> 21) | (self.0[3] << 5)) as u8, - (self.0[3] >> 3) as u8, - (self.0[3] >> 11) as u8, - ((self.0[3] >> 19) | (self.0[4] << 6)) as u8, - (self.0[4] >> 2) as u8, - (self.0[4] >> 10) as u8, - (self.0[4] >> 18) as u8, - (self.0[5] >> 0) as u8, - (self.0[5] >> 8) as u8, - (self.0[5] >> 16) as u8, - ((self.0[5] >> 24) | (self.0[6] << 1)) as u8, - (self.0[6] >> 7) as u8, - (self.0[6] >> 15) as u8, - ((self.0[6] >> 23) | (self.0[7] << 3)) as u8, - (self.0[7] >> 5) as u8, - (self.0[7] >> 13) as u8, - ((self.0[7] >> 21) | (self.0[8] << 4)) as u8, - (self.0[8] >> 4) as u8, - (self.0[8] >> 12) as u8, - ((self.0[8] >> 20) | (self.0[9] << 6)) as u8, - (self.0[9] >> 2) as u8, - (self.0[9] >> 10) as u8, - (self.0[9] >> 18) as u8, - ] - } - - pub fn fe_is_negative(&mut self) -> u8 { - let s = self.to_bytes(); - s[0] & 1 - } -} diff --git a/sdk/core/src/crypto/edwards25519/pre_computed_group_element.rs b/sdk/core/src/crypto/edwards25519/pre_computed_group_element.rs deleted file mode 100644 index 88da9d3f03..0000000000 --- a/sdk/core/src/crypto/edwards25519/pre_computed_group_element.rs +++ /dev/null @@ -1,46 +0,0 @@ -use super::{constant::BASE, field_element::FieldElement}; -use std::marker::Copy; - -#[derive(Default, Copy, Clone, Debug)] -pub struct PreComputedGroupElement { - pub y_plus_x: FieldElement, - pub y_minus_x: FieldElement, - pub xy2d: FieldElement, -} - -impl PreComputedGroupElement { - pub fn zero(&mut self) { - self.y_plus_x.fe_one(); - self.y_minus_x.fe_one(); - self.xy2d.fe_zero(); - } - - pub fn cmove(&mut self, u: &PreComputedGroupElement, b: i32) { - self.y_plus_x.fe_cmove(&u.y_plus_x, b); - self.y_minus_x.fe_cmove(&u.y_minus_x, b); - self.xy2d.fe_cmove(&u.xy2d, b); - } - - pub fn select_point(&mut self, pos: i32, b: i32) { - let mut minus_t = PreComputedGroupElement::default(); - let b_negative = (b >> 31) & 1; - let b_abs = b - (((-b_negative) & b) << 1); - - self.zero(); - - for i in 0..8 { - self.cmove(&BASE[pos as usize][i as usize], equal(b_abs, i + 1)); - } - - minus_t.y_plus_x.fe_copy(&self.y_minus_x); - minus_t.y_minus_x.fe_copy(&self.y_plus_x); - minus_t.xy2d.fe_neg(&self.xy2d); - self.cmove(&minus_t, b_negative); - } -} - -// equal returns 1 if b == c and 0 otherwise, assuming that b and c are -// non-negative. -fn equal(b: i32, c: i32) -> i32 { - if b == c { 1 } else { 0 } -} diff --git a/sdk/core/src/crypto/edwards25519/projective_group_element.rs b/sdk/core/src/crypto/edwards25519/projective_group_element.rs deleted file mode 100644 index 1f2a18f7fc..0000000000 --- a/sdk/core/src/crypto/edwards25519/projective_group_element.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::{completed_group_element::CompletedGroupElement, field_element::FieldElement}; - -#[derive(Default)] -pub struct ProjectiveGroupElement { - pub x: FieldElement, - pub y: FieldElement, - pub z: FieldElement, -} - -impl ProjectiveGroupElement { - pub fn double(&self, r: &mut CompletedGroupElement) { - let mut t0 = FieldElement::default(); - - r.x.fe_square(&self.x); - r.z.fe_square(&self.y); - r.t.fe_square_2(&self.z); - r.y.fe_add(&self.x, &self.y); - t0.fe_square(&r.y); - r.y.fe_add(&r.z, &r.x); - r.z.fe_sub(&r.z.clone(), &r.x); - r.x.fe_sub(&t0, &r.y); - r.t.fe_sub(&r.t.clone(), &r.z.clone()); - } -} diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 6a367c53f3..d92a677a7c 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -1,16 +1,15 @@ use std::fmt::Display; -use super::edwards25519::{sc_mul_add, sc_reduce}; -use crate::crypto::edwards25519::extended_group_element::ExtendedGroupElement; use anyhow::{Result, anyhow}; use bip39::Mnemonic; +use ed25519_dalek::{Signer, SigningKey}; use hmac::{Hmac, KeyInit, Mac}; use pbkdf2::pbkdf2; use serde::{ de::{Deserialize, Deserializer}, ser::{Serialize, Serializer}, }; -use sha2::{Digest, Sha512}; +use sha2::Sha512; use zeroize::Zeroize; pub const PRIVATE_KEY_LENGTH: usize = 64; @@ -27,30 +26,11 @@ impl PrivateKey { pub fn from_bytes(bytes: &[u8]) -> Result { match bytes.len() { SEED_LENGTH => { - let mut h: Sha512 = Sha512::new(); - let mut hash: [u8; 64] = [0u8; 64]; - let mut digest: [u8; 32] = [0u8; 32]; - - h.update(bytes); - hash.copy_from_slice(h.finalize().as_ref()); - - digest.copy_from_slice(&hash[..32]); - - digest[0] &= 248; - digest[31] &= 127; - digest[31] |= 64; - - let mut a = ExtendedGroupElement::default(); - a.ge_scalar_mult_base(digest); - let public_key_bytes = a.to_bytes(); - - let merge: Vec = [bytes.to_vec(), public_key_bytes.to_vec()] - .concat() - .into_iter() - .collect(); + let seed: [u8; 32] = bytes.try_into().unwrap(); + let signing_key = SigningKey::from_bytes(&seed); let mut bits: [u8; 64] = [0u8; 64]; - bits.copy_from_slice(&merge[..64]); - + bits[..32].copy_from_slice(bytes); + bits[32..].copy_from_slice(signing_key.verifying_key().as_bytes()); Ok(PrivateKey(bits)) } PRIVATE_KEY_LENGTH => { @@ -137,49 +117,9 @@ impl PrivateKey { } pub fn sign(&self, message: impl AsRef<[u8]>) -> [u8; 64] { - let mut h: Sha512 = Sha512::new(); - h.update(&self.0[..32]); - - let mut digest1 = [0u8; 64]; - let mut message_digest = [0u8; 64]; - let mut hram_digest = [0u8; 64]; - let mut expanded_secret_key = [0u8; 32]; - - digest1.copy_from_slice(h.finalize_reset().as_ref()); - expanded_secret_key.copy_from_slice(&digest1[..32]); - expanded_secret_key[0] &= 248; - expanded_secret_key[31] &= 63; - expanded_secret_key[31] |= 64; - - h.update(&digest1[32..]); - h.update(message.as_ref()); - message_digest.copy_from_slice(h.finalize_reset().as_ref()); - - let message_digest_reduced = sc_reduce(message_digest); - let mut r = ExtendedGroupElement::default(); - r.ge_scalar_mult_base(message_digest_reduced); - - let encoded_r = r.to_bytes(); - - h.update(encoded_r); - h.update(&self.0[32..]); - h.update(message.as_ref()); - hram_digest.copy_from_slice(h.finalize_reset().as_ref()); - - let hram_digest_reduced = sc_reduce(hram_digest); - - let s = sc_mul_add( - hram_digest_reduced, - expanded_secret_key, - message_digest_reduced, - ); - - let mut signature = [0u8; 64]; - - signature[..32].copy_from_slice(&encoded_r); - signature[32..].copy_from_slice(&s); - - signature + let seed: [u8; 32] = self.0[..32].try_into().unwrap(); + let signing_key = SigningKey::from_bytes(&seed); + signing_key.sign(message.as_ref()).to_bytes() } } From a589c992af506343abc09cfc3311a89fbcad8b5e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 09:46:39 +0300 Subject: [PATCH 038/195] fix after review, public key test --- sdk/core/src/crypto/public_key.rs | 5 ++- sdk/core/tests/public_key_test.rs | 70 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 sdk/core/tests/public_key_test.rs diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index 47de0a3af0..a1aa874293 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -33,8 +33,9 @@ impl PublicKey { pub fn from_hex_str(pk: &str) -> Result { let bytes = hex::decode(pk)?; - let mut bits: [u8; 32] = [0u8; 32]; - bits.copy_from_slice(&bytes[32..]); + let bits: [u8; PUBLIC_KEY_LENGTH] = bytes + .try_into() + .map_err(|_| anyhow::anyhow!("invalid public key length, expected 32 bytes"))?; Ok(Self(bits)) } diff --git a/sdk/core/tests/public_key_test.rs b/sdk/core/tests/public_key_test.rs new file mode 100644 index 0000000000..cb5f940a73 --- /dev/null +++ b/sdk/core/tests/public_key_test.rs @@ -0,0 +1,70 @@ +use multiversx_sdk::crypto::public_key::PublicKey; + +// Alice's known values, consistent with wallet_pem_test.rs constants. +const ALICE_PUBLIC_KEY_HEX: &str = + "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"; +const ALICE_BECH32: &str = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + +// --------------------------------------------------------------------------- +// from_hex_str +// --------------------------------------------------------------------------- + +#[test] +fn test_from_hex_str_valid() { + let pk = PublicKey::from_hex_str(ALICE_PUBLIC_KEY_HEX).unwrap(); + assert_eq!(pk.to_hex(), ALICE_PUBLIC_KEY_HEX); +} + +#[test] +fn test_from_hex_str_invalid_hex() { + assert!(PublicKey::from_hex_str("not_valid_hex!!").is_err()); +} + +#[test] +fn test_from_hex_str_too_short() { + // 8 bytes — not 32 + assert!(PublicKey::from_hex_str("0102030405060708").is_err()); +} + +#[test] +fn test_from_hex_str_too_long() { + // 64 bytes (128 hex chars) — not 32 + let long_hex = "01".repeat(64); + assert!(PublicKey::from_hex_str(&long_hex).is_err()); +} + +// --------------------------------------------------------------------------- +// to_hex +// --------------------------------------------------------------------------- + +#[test] +fn test_to_hex_matches_input() { + let pk = PublicKey::from_hex_str(ALICE_PUBLIC_KEY_HEX).unwrap(); + assert_eq!(pk.to_hex(), ALICE_PUBLIC_KEY_HEX); +} + +#[test] +fn test_to_hex_from_hex_roundtrip() { + let pk = PublicKey::from_hex_str(ALICE_PUBLIC_KEY_HEX).unwrap(); + let pk2 = PublicKey::from_hex_str(&pk.to_hex()).unwrap(); + assert_eq!(pk.to_bytes(), pk2.to_bytes()); +} + +// --------------------------------------------------------------------------- +// to_address +// --------------------------------------------------------------------------- + +#[test] +fn test_to_address_bytes_match_public_key() { + // On MultiversX the address IS the public key bytes. + let pk = PublicKey::from_hex_str(ALICE_PUBLIC_KEY_HEX).unwrap(); + let address = pk.to_address(); + assert_eq!(address.to_hex(), ALICE_PUBLIC_KEY_HEX); +} + +#[test] +fn test_to_address_bech32() { + let pk = PublicKey::from_hex_str(ALICE_PUBLIC_KEY_HEX).unwrap(); + let address = pk.to_address(); + assert_eq!(address.to_bech32_default().to_bech32_str(), ALICE_BECH32); +} From 49ecf71dbc4414999a1ecf63b5d04b2ccdbcb6b3 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 13:03:16 +0300 Subject: [PATCH 039/195] Wallet signature refactor --- .../src/cmd/reproducible_builds/publish.rs | 20 ++--- .../src/cmd/reproducible_builds/unpublish.rs | 6 +- framework/meta/src/cmd/tx/tx_cli_common.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_new.rs | 3 +- framework/meta/src/cmd/tx/tx_cli_sign.rs | 4 +- .../src/interactor/interactor_sender.rs | 2 +- .../src/multi/interactor_multi_sc_process.rs | 2 +- sdk/core/src/crypto.rs | 1 + sdk/core/src/crypto/private_key.rs | 6 +- sdk/core/src/crypto/public_key.rs | 5 +- sdk/core/src/crypto/wallet_signature.rs | 77 +++++++++++++++++++ .../data/transaction/transaction_request.rs | 3 +- sdk/core/src/wallet.rs | 5 +- sdk/core/tests/wallet_keystore_test.rs | 2 +- sdk/core/tests/wallet_sign_test.rs | 59 ++++++++++++-- sdk/http/examples/sign_tx.rs | 2 +- sdk/http/examples/sign_txs.rs | 4 +- 17 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 sdk/core/src/crypto/wallet_signature.rs diff --git a/framework/meta/src/cmd/reproducible_builds/publish.rs b/framework/meta/src/cmd/reproducible_builds/publish.rs index 0eccefdad4..f284ee9b67 100644 --- a/framework/meta/src/cmd/reproducible_builds/publish.rs +++ b/framework/meta/src/cmd/reproducible_builds/publish.rs @@ -4,7 +4,7 @@ use std::time::Duration; use hex::ToHex; use multiversx_chain_core::std::crypto; use multiversx_sc_snippets::imports::Bech32Address; -use multiversx_sc_snippets::sdk::wallet::Wallet; +use multiversx_sc_snippets::sdk::wallet::{Wallet, WalletSignature}; use crate::cli::ReproducibleBuildPublishArgs; use crate::cli::cli_args_sender::load_wallet; @@ -52,12 +52,12 @@ pub async fn publish_contract(args: &ReproducibleBuildPublishArgs) { ); // Sign the payload. - let signature_hex = sign_payload(&wallet, &contract, &payload); + let signature = sign_payload(&wallet, &contract, &payload); let url = format!("{}/verifier", args.verifier_url.trim_end_matches('/')); let (status, body) = post_verification_request( &url, - &signature_hex, + signature, &contract, &args.docker_image, &source_code, @@ -106,17 +106,20 @@ pub(super) fn compute_bytes_for_signing(contract: &Bech32Address, payload: &str) crypto::keccak256(&content).to_vec() } -/// Signs the payload using the MultiversX message signing protocol and returns the hex signature. -pub(super) fn sign_payload(wallet: &Wallet, contract: &Bech32Address, payload: &str) -> String { +/// Signs the payload using the MultiversX message signing protocol and returns the signature. +pub(super) fn sign_payload( + wallet: &Wallet, + contract: &Bech32Address, + payload: &str, +) -> WalletSignature { let bytes_to_sign = compute_bytes_for_signing(contract, payload); - let signature: [u8; 64] = wallet.sign_bytes(bytes_to_sign); - signature.encode_hex() + wallet.sign_bytes(bytes_to_sign) } /// POSTs the verification request and returns the HTTP status code and parsed response body. async fn post_verification_request( url: &str, - signature: &str, + signature: WalletSignature, contract: &Bech32Address, docker_image: &str, source_code: &serde_json::Value, @@ -228,7 +231,6 @@ async fn poll_task(verifier_url: &str, task_id: &str) { #[cfg(test)] mod tests { use super::*; - use hex::ToHex; use multiversx_chain_core::std::crypto::{keccak256, sha256}; /// Independently re-implements the signing bytes computation and checks it matches. diff --git a/framework/meta/src/cmd/reproducible_builds/unpublish.rs b/framework/meta/src/cmd/reproducible_builds/unpublish.rs index 99ba434fea..0f96a19a60 100644 --- a/framework/meta/src/cmd/reproducible_builds/unpublish.rs +++ b/framework/meta/src/cmd/reproducible_builds/unpublish.rs @@ -1,6 +1,5 @@ use std::io::{self, Write}; -use hex::ToHex; use multiversx_sc_snippets::imports::Bech32Address; use crate::cli::ReproducibleBuildUnpublishArgs; @@ -38,11 +37,10 @@ pub async fn unpublish_contract(args: &ReproducibleBuildUnpublishArgs) { // Sign using the same MultiversX message signing protocol as verify. let bytes_to_sign = compute_bytes_for_signing(&contract, &payload); - let signature: [u8; 64] = wallet.sign_bytes(bytes_to_sign); - let signature_hex: String = signature.encode_hex(); + let signature = wallet.sign_bytes(bytes_to_sign); let request_body = serde_json::json!({ - "signature": signature_hex, + "signature": signature, "payload": payload_obj, }); diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 5b8c21edbb..0a64552158 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -164,7 +164,7 @@ pub async fn sign_and_dispatch( .unwrap_or_default(); let sig = wallet.sign_tx(&tx); - tx.signature = Some(hex::encode(sig)); + tx.signature = Some(sig); let output = TxOutputFile { emitted_transaction: tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 4e218d15bc..fbc0379cee 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -2,7 +2,6 @@ use std::fs; use anyhow::{Context, Result}; use multiversx_sc_snippets::{ - hex, imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}, sdk::utils::{base64_decode, base64_encode}, }; @@ -70,7 +69,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { } let sig = wallet.sign_tx(&tx); - tx.signature = Some(hex::encode(sig)); + tx.signature = Some(sig); let output = TxOutputFile { emitted_transaction: tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 02455d6bf4..827d9dd1e3 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -1,5 +1,5 @@ use anyhow::{Result, anyhow}; -use multiversx_sc_snippets::{hex, sdk::utils::base64_decode}; +use multiversx_sc_snippets::sdk::utils::base64_decode; use super::{ output::TxOutputFile, @@ -33,7 +33,7 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { } let sig = wallet.sign_tx(&tx); - tx.signature = Some(hex::encode(sig)); + tx.signature = Some(sig); let decoded_data = tx .data diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index d90f2248ed..72ee215c1d 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -110,6 +110,6 @@ where // sign let signature = sender.wallet.sign_tx(transaction); - transaction.signature = Some(hex::encode(signature)); + transaction.signature = Some(signature); } } diff --git a/framework/snippets/src/multi/interactor_multi_sc_process.rs b/framework/snippets/src/multi/interactor_multi_sc_process.rs index 2cc8acf811..c816b9f533 100644 --- a/framework/snippets/src/multi/interactor_multi_sc_process.rs +++ b/framework/snippets/src/multi/interactor_multi_sc_process.rs @@ -57,5 +57,5 @@ pub(crate) fn update_nonces_and_sign_tx(transaction: &mut Transaction, sender: & sender.current_nonce = Some(sender.current_nonce.unwrap() + 1); let signature = sender.wallet.sign_tx(&*transaction); - transaction.signature = Some(hex::encode(signature)); + transaction.signature = Some(signature); } diff --git a/sdk/core/src/crypto.rs b/sdk/core/src/crypto.rs index a37c90a77d..350909fc85 100644 --- a/sdk/core/src/crypto.rs +++ b/sdk/core/src/crypto.rs @@ -1,2 +1,3 @@ pub mod private_key; pub mod public_key; +pub mod wallet_signature; diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index d92a677a7c..dd2f5f9ff4 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -12,6 +12,8 @@ use serde::{ use sha2::Sha512; use zeroize::Zeroize; +use super::wallet_signature::WalletSignature; + pub const PRIVATE_KEY_LENGTH: usize = 64; pub const SIGNATURE_LENGTH: usize = 64; pub const SEED_LENGTH: usize = 32; @@ -116,10 +118,10 @@ impl PrivateKey { hex::encode(&self.0[..32]) } - pub fn sign(&self, message: impl AsRef<[u8]>) -> [u8; 64] { + pub fn sign(&self, message: impl AsRef<[u8]>) -> WalletSignature { let seed: [u8; 32] = self.0[..32].try_into().unwrap(); let signing_key = SigningKey::from_bytes(&seed); - signing_key.sign(message.as_ref()).to_bytes() + WalletSignature::from_bytes(signing_key.sign(message.as_ref()).to_bytes()) } } diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index a1aa874293..e14b1ef882 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use super::private_key::PrivateKey; +use super::wallet_signature::WalletSignature; use anyhow::Result; use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use multiversx_chain_core::types::Address; @@ -39,11 +40,11 @@ impl PublicKey { Ok(Self(bits)) } - pub fn verify(&self, message: &[u8], signature: &[u8; 64]) -> bool { + pub fn verify(&self, message: &[u8], signature: &WalletSignature) -> bool { let Ok(verifying_key) = VerifyingKey::from_bytes(&self.0) else { return false; }; - let signature = Signature::from_bytes(signature); + let signature = Signature::from_bytes(signature.as_bytes()); verifying_key.verify(message, &signature).is_ok() } } diff --git a/sdk/core/src/crypto/wallet_signature.rs b/sdk/core/src/crypto/wallet_signature.rs new file mode 100644 index 0000000000..efe0eecd39 --- /dev/null +++ b/sdk/core/src/crypto/wallet_signature.rs @@ -0,0 +1,77 @@ +use std::fmt; + +use anyhow::Result; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// A 64-byte Ed25519 signature, serialized as a lowercase hex string. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct WalletSignature([u8; 64]); + +impl WalletSignature { + pub fn from_bytes(bytes: [u8; 64]) -> Self { + Self(bytes) + } + + pub fn to_bytes(&self) -> [u8; 64] { + self.0 + } + + pub fn as_bytes(&self) -> &[u8; 64] { + &self.0 + } + + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } + + pub fn from_hex_str(s: &str) -> Result { + let bytes = hex::decode(s)?; + let bits: [u8; 64] = bytes + .try_into() + .map_err(|_| anyhow::anyhow!("invalid signature length, expected 64 bytes"))?; + Ok(Self(bits)) + } +} + +impl From<[u8; 64]> for WalletSignature { + fn from(bytes: [u8; 64]) -> Self { + Self(bytes) + } +} + +impl AsRef<[u8]> for WalletSignature { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for WalletSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.to_hex().fmt(f) + } +} + +impl fmt::Debug for WalletSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "WalletSignature({})", self) + } +} + +impl Serialize for WalletSignature { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_hex()) + } +} + +impl<'de> Deserialize<'de> for WalletSignature { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_hex_str(&s).map_err(serde::de::Error::custom) + } +} diff --git a/sdk/core/src/data/transaction/transaction_request.rs b/sdk/core/src/data/transaction/transaction_request.rs index 39b5ec5c71..09bf7e06e1 100644 --- a/sdk/core/src/data/transaction/transaction_request.rs +++ b/sdk/core/src/data/transaction/transaction_request.rs @@ -1,3 +1,4 @@ +use crate::crypto::wallet_signature::WalletSignature; use multiversx_chain_core::std::Bech32Address; use serde::{Deserialize, Serialize}; @@ -16,7 +17,7 @@ pub struct Transaction { #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub signature: Option, + pub signature: Option, #[serde(rename = "chainID")] pub chain_id: String, pub version: u32, diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index f1f3d9e467..a4b73d1eff 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -12,6 +12,7 @@ pub use wallet_pem::WalletPem; // TODO: move under wallet pub use crate::crypto::private_key::PrivateKey; pub use crate::crypto::public_key::PublicKey; +pub use crate::crypto::wallet_signature::WalletSignature; use core::str; use std::path::Path; @@ -123,7 +124,7 @@ impl Wallet { PublicKey::from(&self.private_key).to_hex() } - pub fn sign_tx(&self, unsign_tx: &Transaction) -> [u8; 64] { + pub fn sign_tx(&self, unsign_tx: &Transaction) -> WalletSignature { let mut unsign_tx = unsign_tx.clone(); unsign_tx.signature = None; @@ -139,7 +140,7 @@ impl Wallet { self.private_key.sign(tx_bytes) } - pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> [u8; 64] { + pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> WalletSignature { self.private_key.sign(data) } diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs index 4e8b483838..1f492367d6 100644 --- a/sdk/core/tests/wallet_keystore_test.rs +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -128,7 +128,7 @@ fn test_full_encrypt_serialize_deserialize_decrypt_sign() { assert_eq!(sig, wallet.sign_bytes(MESSAGE)); // Snapshot: exact signature bytes must match. - assert_eq!(hex::encode(sig), EXPECTED_SIGNATURE_HEX); + assert_eq!(sig.to_hex(), EXPECTED_SIGNATURE_HEX); } // Generated by running the test suite once with fixed randomness. diff --git a/sdk/core/tests/wallet_sign_test.rs b/sdk/core/tests/wallet_sign_test.rs index 4d282f07fc..1cde364b2c 100644 --- a/sdk/core/tests/wallet_sign_test.rs +++ b/sdk/core/tests/wallet_sign_test.rs @@ -1,4 +1,4 @@ -use multiversx_sdk::test_wallets; +use multiversx_sdk::{test_wallets, wallet::WalletSignature}; #[test] fn test_sign_bytes_snapshot() { @@ -9,8 +9,8 @@ fn test_sign_bytes_snapshot() { let sig_alice = alice.sign_bytes(b"hello multiversx"); let sig_bob = bob.sign_bytes(b"hello multiversx"); - assert_eq!(hex::encode(sig_alice), SIG_ALICE_HELLO); - assert_eq!(hex::encode(sig_bob), SIG_BOB_HELLO); + assert_eq!(sig_alice.to_hex(), SIG_ALICE_HELLO); + assert_eq!(sig_bob.to_hex(), SIG_BOB_HELLO); } #[test] @@ -66,8 +66,8 @@ fn test_verify_wrong_key_fails() { fn test_verify_snapshot_signatures() { let alice = test_wallets::alice(); let bob = test_wallets::bob(); - let alice_sig: [u8; 64] = hex::decode(SIG_ALICE_HELLO).unwrap().try_into().unwrap(); - let bob_sig: [u8; 64] = hex::decode(SIG_BOB_HELLO).unwrap().try_into().unwrap(); + let alice_sig = WalletSignature::from_hex_str(SIG_ALICE_HELLO).unwrap(); + let bob_sig = WalletSignature::from_hex_str(SIG_BOB_HELLO).unwrap(); assert!(alice.public_key().verify(b"hello multiversx", &alice_sig)); assert!(bob.public_key().verify(b"hello multiversx", &bob_sig)); } @@ -75,3 +75,52 @@ fn test_verify_snapshot_signatures() { // Snapshots captured with fixed test-wallet keys. const SIG_ALICE_HELLO: &str = "c32f811c809a02d3548f190b134d5fa542b028935f4e300556e29b3164f146d54aa7e37ba97080e9f72402af67f6f6f070cbbd1b496c9779e75038a6aee54c07"; const SIG_BOB_HELLO: &str = "09e1b11a87e47bb0c1bebc71f653daabb966c583219588d4b80e396a966b91d41767af8f4704bbc50f8e8fd34cc6fb0da210329ea19437a7e07c0fc3f5cdf50e"; + +#[test] +fn test_wallet_signature_to_hex_roundtrip() { + let raw = [0xabu8; 64]; + let sig = WalletSignature::from_bytes(raw); + let hex = sig.to_hex(); + assert_eq!(hex.len(), 128); + let sig2 = WalletSignature::from_hex_str(&hex).unwrap(); + assert_eq!(sig2, sig); +} + +#[test] +fn test_wallet_signature_from_hex_str_invalid_hex() { + assert!(WalletSignature::from_hex_str("not-hex").is_err()); +} + +#[test] +fn test_wallet_signature_from_hex_str_wrong_length() { + // 63 bytes (126 hex chars) — too short + assert!(WalletSignature::from_hex_str(&"ab".repeat(63)).is_err()); + // 65 bytes (130 hex chars) — too long + assert!(WalletSignature::from_hex_str(&"ab".repeat(65)).is_err()); +} + +#[test] +fn test_wallet_signature_json_serialize_as_hex() { + let raw = [0x01u8; 64]; + let sig = WalletSignature::from_bytes(raw); + let json = serde_json::to_string(&sig).unwrap(); + // JSON value should be a quoted hex string of length 128 chars + assert_eq!(json, format!("\"{}\"", sig.to_hex())); +} + +#[test] +fn test_wallet_signature_json_deserialize_from_hex() { + let hex = "ab".repeat(64); + let json = format!("\"{}\"", hex); + let sig: WalletSignature = serde_json::from_str(&json).unwrap(); + assert_eq!(sig.to_hex(), hex); +} + +#[test] +fn test_wallet_signature_json_roundtrip() { + let raw = [0xdeu8; 64]; + let sig = WalletSignature::from_bytes(raw); + let json = serde_json::to_string(&sig).unwrap(); + let sig2: WalletSignature = serde_json::from_str(&json).unwrap(); + assert_eq!(sig, sig2); +} diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index eadf5441b0..d5cedaf1d8 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -33,7 +33,7 @@ async fn main() { }; let signature = wallet.sign_tx(&unsign_tx); - unsign_tx.signature = Some(hex::encode(signature)); + unsign_tx.signature = Some(signature); let tx_hash = blockchain.send_transaction(&unsign_tx).await.unwrap(); assert!(!tx_hash.is_empty()); diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index afb2cbd541..b2f2ed2859 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -38,7 +38,7 @@ async fn main() { let mut txs: Vec = vec![]; let signature = wallet.sign_tx(&unsign_tx); - unsign_tx.signature = Some(hex::encode(signature)); + unsign_tx.signature = Some(signature); txs.push(unsign_tx.clone()); unsign_tx.version = 2; @@ -46,7 +46,7 @@ async fn main() { unsign_tx.nonce += 1; let signature = wallet.sign_tx(&unsign_tx); - unsign_tx.signature = Some(hex::encode(signature)); + unsign_tx.signature = Some(signature); txs.push(unsign_tx.clone()); let tx_hash = blockchain.send_transactions(&txs).await.unwrap(); From 107b6c4c0c060616d712cd0478f91fd3e1bc6df8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 17:15:19 +0300 Subject: [PATCH 040/195] Wallet private key from_bytes test --- sdk/core/tests/private_key_test.rs | 102 +++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 sdk/core/tests/private_key_test.rs diff --git a/sdk/core/tests/private_key_test.rs b/sdk/core/tests/private_key_test.rs new file mode 100644 index 0000000000..fe981fc8be --- /dev/null +++ b/sdk/core/tests/private_key_test.rs @@ -0,0 +1,102 @@ +use multiversx_sdk::crypto::private_key::{PRIVATE_KEY_LENGTH, SEED_LENGTH, PrivateKey}; + +// Alice's well-known key material (consistent with wallet_pem_test.rs). +const ALICE_SEED_HEX: &str = + "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; +const ALICE_PUBLIC_KEY_HEX: &str = + "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"; + +fn alice_seed_bytes() -> Vec { + hex::decode(ALICE_SEED_HEX).unwrap() +} + +fn alice_64byte_hex() -> String { + format!("{}{}", ALICE_SEED_HEX, ALICE_PUBLIC_KEY_HEX) +} + +// --------------------------------------------------------------------------- +// from_bytes — 32-byte (seed) branch +// --------------------------------------------------------------------------- + +#[test] +fn test_from_bytes_seed_length_ok() { + let seed = alice_seed_bytes(); + assert_eq!(seed.len(), SEED_LENGTH); + + let pk = PrivateKey::from_bytes(&seed).unwrap(); + + // to_hex() encodes only the first 32 bytes (the seed). + assert_eq!(pk.to_hex(), ALICE_SEED_HEX); +} + +#[test] +fn test_from_bytes_seed_derives_correct_public_key() { + let seed = alice_seed_bytes(); + let pk = PrivateKey::from_bytes(&seed).unwrap(); + + // The upper 32 bytes of the stored key must be the verifying key. + let stored = pk.to_bytes(); + let derived_pubkey_hex = hex::encode(&stored[32..]); + assert_eq!(derived_pubkey_hex, ALICE_PUBLIC_KEY_HEX); +} + +#[test] +fn test_from_bytes_seed_roundtrip() { + let seed = alice_seed_bytes(); + let pk = PrivateKey::from_bytes(&seed).unwrap(); + + // Re-constructing from the returned 64-byte array must yield an equal key. + let pk2 = PrivateKey::from_bytes(&pk.to_bytes()).unwrap(); + assert_eq!(pk, pk2); +} + +// --------------------------------------------------------------------------- +// from_bytes — 64-byte (full key) branch +// --------------------------------------------------------------------------- + +#[test] +fn test_from_bytes_full_length_ok() { + let full = hex::decode(alice_64byte_hex()).unwrap(); + assert_eq!(full.len(), PRIVATE_KEY_LENGTH); + + let pk = PrivateKey::from_bytes(&full).unwrap(); + + // Seed portion must be preserved verbatim. + assert_eq!(pk.to_hex(), ALICE_SEED_HEX); +} + +#[test] +fn test_from_bytes_full_preserves_all_64_bytes() { + let full = hex::decode(alice_64byte_hex()).unwrap(); + let pk = PrivateKey::from_bytes(&full).unwrap(); + + assert_eq!(pk.to_bytes().as_slice(), full.as_slice()); +} + +#[test] +fn test_from_bytes_full_roundtrip() { + let full = hex::decode(alice_64byte_hex()).unwrap(); + let pk = PrivateKey::from_bytes(&full).unwrap(); + let pk2 = PrivateKey::from_bytes(&pk.to_bytes()).unwrap(); + assert_eq!(pk, pk2); +} + +// --------------------------------------------------------------------------- +// from_bytes — error cases +// --------------------------------------------------------------------------- + +#[test] +fn test_from_bytes_empty_is_err() { + assert!(PrivateKey::from_bytes(&[]).is_err()); +} + +#[test] +fn test_from_bytes_wrong_length_is_err() { + // 16 bytes — neither 32 nor 64. + assert!(PrivateKey::from_bytes(&[0u8; 16]).is_err()); +} + +#[test] +fn test_from_bytes_65_bytes_is_err() { + assert!(PrivateKey::from_bytes(&[0u8; 65]).is_err()); +} From 49326c072160f2d21ad66b3c10d53e4a9fd8aaf6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 15:21:05 +0300 Subject: [PATCH 041/195] chain core crypto reorg --- Cargo.lock | 1 + chain/core/Cargo.toml | 1 + chain/core/src/std/crypto.rs | 21 +++------------ chain/core/src/std/crypto/keccak256.rs | 36 ++++++++++++++++++++++++++ chain/core/src/std/crypto/sha256.rs | 36 ++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 chain/core/src/std/crypto/keccak256.rs create mode 100644 chain/core/src/std/crypto/sha256.rs diff --git a/Cargo.lock b/Cargo.lock index 32cdc32a22..a335773dae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3212,6 +3212,7 @@ dependencies = [ "bitflags 2.11.1", "blake2", "hex", + "hex-literal", "multiversx-sc-codec", "serde", "serde_json", diff --git a/chain/core/Cargo.toml b/chain/core/Cargo.toml index 5ed695deac..f1adf21d3c 100644 --- a/chain/core/Cargo.toml +++ b/chain/core/Cargo.toml @@ -35,3 +35,4 @@ features = ["derive"] [dev-dependencies] serde_json = "1.0" +hex-literal = "1.0" diff --git a/chain/core/src/std/crypto.rs b/chain/core/src/std/crypto.rs index e7f2cab9a7..677e24d762 100644 --- a/chain/core/src/std/crypto.rs +++ b/chain/core/src/std/crypto.rs @@ -2,21 +2,8 @@ //! //! TODO: move here the rest of the functions and the Wallet, with signing. -pub const SHA256_RESULT_LEN: usize = 32; -pub const KECCAK256_RESULT_LEN: usize = 32; +mod keccak256; +mod sha256; -/// Computes the SHA-256 hash of the given data. -/// -/// Returns a 32-byte array containing the hash digest. -pub fn sha256(data: &[u8]) -> [u8; SHA256_RESULT_LEN] { - use sha2::Digest; - sha2::Sha256::digest(data).into() -} - -/// Computes the Keccak-256 hash of the given data. -/// -/// Returns a 32-byte array containing the hash digest. -pub fn keccak256(data: &[u8]) -> [u8; KECCAK256_RESULT_LEN] { - use sha3::Digest; - sha3::Keccak256::digest(data).into() -} +pub use keccak256::{KECCAK256_RESULT_LEN, keccak256}; +pub use sha256::{SHA256_RESULT_LEN, sha256}; diff --git a/chain/core/src/std/crypto/keccak256.rs b/chain/core/src/std/crypto/keccak256.rs new file mode 100644 index 0000000000..619bf4d80c --- /dev/null +++ b/chain/core/src/std/crypto/keccak256.rs @@ -0,0 +1,36 @@ +pub const KECCAK256_RESULT_LEN: usize = 32; + +/// Computes the Keccak-256 hash of the given data. +/// +/// Returns a 32-byte array containing the hash digest. +pub fn keccak256(data: &[u8]) -> [u8; KECCAK256_RESULT_LEN] { + use sha3::Digest; + sha3::Keccak256::digest(data).into() +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn keccak256_empty() { + // Keccak-256("") = + // c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 + let expected = hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + assert_eq!(keccak256(b""), expected); + } + + #[test] + fn keccak256_abc() { + // Keccak-256("abc") = + // 4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45 + let expected = hex!("4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"); + assert_eq!(keccak256(b"abc"), expected); + } + + #[test] + fn keccak256_different_inputs_differ() { + assert_ne!(keccak256(b"foo"), keccak256(b"bar")); + } +} diff --git a/chain/core/src/std/crypto/sha256.rs b/chain/core/src/std/crypto/sha256.rs new file mode 100644 index 0000000000..1d19d963d0 --- /dev/null +++ b/chain/core/src/std/crypto/sha256.rs @@ -0,0 +1,36 @@ +pub const SHA256_RESULT_LEN: usize = 32; + +/// Computes the SHA-256 hash of the given data. +/// +/// Returns a 32-byte array containing the hash digest. +pub fn sha256(data: &[u8]) -> [u8; SHA256_RESULT_LEN] { + use sha2::Digest; + sha2::Sha256::digest(data).into() +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn sha256_empty() { + // NIST FIPS 180-4: SHA-256("") = + // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + let expected = hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + assert_eq!(sha256(b""), expected); + } + + #[test] + fn sha256_abc() { + // NIST FIPS 180-4: SHA-256("abc") = + // ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad + let expected = hex!("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + assert_eq!(sha256(b"abc"), expected); + } + + #[test] + fn sha256_different_inputs_differ() { + assert_ne!(sha256(b"foo"), sha256(b"bar")); + } +} From 12ab00ffec6af3d70477bd0bb2e368f3b2e51699 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 16:51:11 +0300 Subject: [PATCH 042/195] chain core crypto ed25519 --- Cargo.lock | 2 +- chain/core/Cargo.toml | 3 +- chain/core/src/std/crypto.rs | 3 +- chain/core/src/std/crypto/ed25519.rs | 123 ++++++++++++++++++++++++ chain/vm/src/crypto_functions.rs | 45 +++++---- sdk/core/Cargo.toml | 1 - sdk/core/src/crypto/private_key.rs | 32 +++--- sdk/core/src/crypto/public_key.rs | 29 +++--- sdk/core/src/crypto/wallet_signature.rs | 23 ++--- sdk/core/src/wallet.rs | 2 +- sdk/core/tests/wallet_keystore_test.rs | 4 +- 11 files changed, 191 insertions(+), 76 deletions(-) create mode 100644 chain/core/src/std/crypto/ed25519.rs diff --git a/Cargo.lock b/Cargo.lock index a335773dae..7e68470098 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3211,6 +3211,7 @@ dependencies = [ "bech32", "bitflags 2.11.1", "blake2", + "ed25519-dalek", "hex", "hex-literal", "multiversx-sc-codec", @@ -3477,7 +3478,6 @@ dependencies = [ "base64", "bip39", "ctr", - "ed25519-dalek", "hex", "hmac", "itertools 0.14.0", diff --git a/chain/core/Cargo.toml b/chain/core/Cargo.toml index f1adf21d3c..7412c10a92 100644 --- a/chain/core/Cargo.toml +++ b/chain/core/Cargo.toml @@ -17,7 +17,7 @@ keywords = ["multiversx", "blockchain", "vm", "tools"] categories = ["cryptography::cryptocurrencies", "development-tools::debugging"] [features] -std = ["bech32", "serde", "hex", "blake2", "sha2", "sha3"] +std = ["bech32", "serde", "hex", "blake2", "sha2", "sha3", "ed25519-dalek"] [dependencies] bitflags = "2.9" @@ -27,6 +27,7 @@ hex = { version = "0.4", optional = true } blake2 = { version = "0.10", optional = true } sha2 = { version = "0.11", optional = true } sha3 = { version = "0.12", optional = true } +ed25519-dalek = { version = "2.1", optional = true } [dependencies.multiversx-sc-codec] version = "=0.25.0" diff --git a/chain/core/src/std/crypto.rs b/chain/core/src/std/crypto.rs index 677e24d762..23abf5fad6 100644 --- a/chain/core/src/std/crypto.rs +++ b/chain/core/src/std/crypto.rs @@ -1,7 +1,6 @@ //! Standard crypto functions and constants used across many MultiversX crates. -//! -//! TODO: move here the rest of the functions and the Wallet, with signing. +pub mod ed25519; mod keccak256; mod sha256; diff --git a/chain/core/src/std/crypto/ed25519.rs b/chain/core/src/std/crypto/ed25519.rs new file mode 100644 index 0000000000..7d5526e7e3 --- /dev/null +++ b/chain/core/src/std/crypto/ed25519.rs @@ -0,0 +1,123 @@ +use ed25519_dalek::{Signer, Verifier}; + +pub const SIGNATURE_LENGTH: usize = 64; + +pub type Ed25519SigningKey = ed25519_dalek::SigningKey; +pub type Ed25519VerifyingKey = ed25519_dalek::VerifyingKey; +pub type Ed25519Signature = ed25519_dalek::Signature; + +/// Constructs a signing key from a 32-byte seed. +pub fn signing_key_from_seed(seed: &[u8; 32]) -> Ed25519SigningKey { + Ed25519SigningKey::from_bytes(seed) +} + +/// Derives the verifying (public) key from a 32-byte signing seed. +pub fn verifying_key_from_seed(seed: &[u8; 32]) -> Ed25519VerifyingKey { + signing_key_from_seed(seed).verifying_key() +} + +/// Constructs a verifying key from its 32-byte compressed representation. +/// Returns `None` if the bytes do not represent a valid curve point. +pub fn verifying_key_from_bytes(bytes: &[u8; 32]) -> Option { + Ed25519VerifyingKey::from_bytes(bytes).ok() +} + +/// Constructs an Ed25519 signature from its 64-byte representation. +pub fn signature_from_bytes(bytes: &[u8; 64]) -> Ed25519Signature { + Ed25519Signature::from_bytes(bytes) +} + +/// Signs a message with the given signing key. +pub fn sign(signing_key: &Ed25519SigningKey, message: &[u8]) -> Ed25519Signature { + signing_key.sign(message) +} + +/// Verifies an Ed25519 signature. Returns `true` if the signature is valid. +pub fn verify( + verifying_key: &Ed25519VerifyingKey, + message: &[u8], + signature: &Ed25519Signature, +) -> bool { + verifying_key.verify(message, signature).is_ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + // Known-good vectors generated with ed25519-dalek 2.2.0 and verified via + // the roundtrip test below. The seed is arbitrary; the pk and sig are + // the values the library produces for (seed, empty message). + const SEED: [u8; 32] = + hex_literal::hex!("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae3d55"); + const PK_BYTES: [u8; 32] = + hex_literal::hex!("700e2ce7c4b674427eab27ba820bcf6f0faebe68e09fe8564292114e41dc6a41"); + const SIG_BYTES: [u8; 64] = hex_literal::hex!( + "37b4bd5f28b61f55dc9673ae2895baceb863d9cf51780d040f98ad8cdc896cf5\ + be46be655a863525da0959f7f373611585e437e28ec971b7bd206ff9bd26e803" + ); + + #[test] + fn verifying_key_derived_from_seed() { + assert_eq!(verifying_key_from_seed(&SEED).to_bytes(), PK_BYTES); + } + + #[test] + fn signing_key_from_seed_roundtrip() { + let sk = signing_key_from_seed(&SEED); + assert_eq!(sk.verifying_key().to_bytes(), PK_BYTES); + } + + #[test] + fn sign_produces_known_signature() { + let sk = signing_key_from_seed(&SEED); + assert_eq!(sign(&sk, b"").to_bytes(), SIG_BYTES); + } + + #[test] + fn verify_valid_signature() { + let vk = verifying_key_from_seed(&SEED); + let sig = signature_from_bytes(&SIG_BYTES); + assert!(verify(&vk, b"", &sig)); + } + + #[test] + fn verify_rejects_wrong_message() { + let vk = verifying_key_from_seed(&SEED); + let sig = signature_from_bytes(&SIG_BYTES); + assert!(!verify(&vk, b"wrong", &sig)); + } + + #[test] + fn verify_rejects_wrong_signature() { + let vk = verifying_key_from_seed(&SEED); + let mut bad_bytes = SIG_BYTES; + bad_bytes[0] ^= 0xff; + let bad_sig = signature_from_bytes(&bad_bytes); + assert!(!verify(&vk, b"", &bad_sig)); + } + + #[test] + fn verifying_key_from_bytes_roundtrip() { + let vk = verifying_key_from_bytes(&PK_BYTES).unwrap(); + assert_eq!(vk.to_bytes(), PK_BYTES); + } + + #[test] + fn verifying_key_from_bytes_different_seeds_differ() { + let vk1 = verifying_key_from_bytes(&PK_BYTES).unwrap(); + let vk2 = verifying_key_from_seed(&[0x42u8; 32]); + assert_ne!(vk1.to_bytes(), vk2.to_bytes()); + } + + #[test] + fn sign_then_verify_roundtrip() { + let seed: [u8; 32] = [0x42u8; 32]; + let sk = signing_key_from_seed(&seed); + let vk = sk.verifying_key(); + let msg = b"hello multiversx"; + let sig = sign(&sk, msg); + assert!(verify(&vk, msg, &sig)); + assert!(!verify(&vk, b"other message", &sig)); + } +} diff --git a/chain/vm/src/crypto_functions.rs b/chain/vm/src/crypto_functions.rs index e70b10e513..d77e85eb2a 100644 --- a/chain/vm/src/crypto_functions.rs +++ b/chain/vm/src/crypto_functions.rs @@ -1,36 +1,45 @@ +//! Thin wrappers over [`multiversx_chain_core::std::crypto`] for convenient use in the VM. + use multiversx_chain_core::std::crypto; +/// Computes the SHA-256 hash of `data`. +/// Returns a 32-byte digest. +/// +/// Wraps [`multiversx_chain_core::std::crypto::sha256`]. pub fn sha256(data: &[u8]) -> [u8; crypto::SHA256_RESULT_LEN] { crypto::sha256(data) } +/// Computes the Keccak-256 hash of `data`. +/// Returns a 32-byte digest. +/// +/// Wraps [`multiversx_chain_core::std::crypto::keccak256`]. pub fn keccak256(data: &[u8]) -> [u8; crypto::KECCAK256_RESULT_LEN] { crypto::keccak256(data) } +/// Verifies an Ed25519 signature. +/// +/// Wraps [`multiversx_chain_core::std::crypto::ed25519::verify`]. +/// +/// Returns `true` if `signature` is a valid Ed25519 signature of `message` +/// under the public key `key`. +/// +/// Returns `false` if: +/// - `key` is not exactly 32 bytes +/// - `signature` is not exactly 64 bytes +/// - `key` does not represent a valid curve point +/// - the signature does not verify pub fn verify_ed25519(key: &[u8], message: &[u8], signature: &[u8]) -> bool { - use ed25519_dalek::{Signature, Verifier, VerifyingKey}; - - let key_32: [u8; 32] = if let Ok(key_32) = key.try_into() { - key_32 - } else { + let Ok(key_32) = key.try_into() else { return false; }; - let signature_64: [u8; 64] = if let Ok(signature_64) = signature.try_into() { - signature_64 - } else { + let Ok(sig_64) = signature.try_into() else { return false; }; - - let verifying_key_result = VerifyingKey::from_bytes(&key_32); - let verifying_key = if let Ok(verifying_key) = verifying_key_result { - verifying_key - } else { + let Some(verifying_key) = crypto::ed25519::verifying_key_from_bytes(key_32) else { return false; }; - - let sig = Signature::from_bytes(&signature_64); - - let result = verifying_key.verify(message, &sig); - result.is_ok() + let sig = crypto::ed25519::signature_from_bytes(sig_64); + crypto::ed25519::verify(&verifying_key, message, &sig) } diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index a2b71d0aae..14b7b8367b 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -23,7 +23,6 @@ anyhow = "1.0" bip39 = "2.0" sha2 = "0.11" sha3 = "0.12" -ed25519-dalek = "2.1" hmac = "0.13" hex = "0.4" base64 = "0.22" diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index dd2f5f9ff4..f2aea59546 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -2,8 +2,8 @@ use std::fmt::Display; use anyhow::{Result, anyhow}; use bip39::Mnemonic; -use ed25519_dalek::{Signer, SigningKey}; use hmac::{Hmac, KeyInit, Mac}; +use multiversx_chain_core::std::crypto::ed25519; use pbkdf2::pbkdf2; use serde::{ de::{Deserialize, Deserializer}, @@ -21,25 +21,21 @@ pub const SEED_LENGTH: usize = 32; const EGLD_COIN_TYPE: u32 = 508; const HARDENED: u32 = 0x80000000; -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct PrivateKey(pub [u8; PRIVATE_KEY_LENGTH]); +#[derive(Clone, PartialEq, Eq)] +pub struct PrivateKey(pub ed25519::Ed25519SigningKey); impl PrivateKey { pub fn from_bytes(bytes: &[u8]) -> Result { match bytes.len() { SEED_LENGTH => { let seed: [u8; 32] = bytes.try_into().unwrap(); - let signing_key = SigningKey::from_bytes(&seed); - let mut bits: [u8; 64] = [0u8; 64]; - bits[..32].copy_from_slice(bytes); - bits[32..].copy_from_slice(signing_key.verifying_key().as_bytes()); - Ok(PrivateKey(bits)) + Ok(PrivateKey(ed25519::signing_key_from_seed(&seed))) } PRIVATE_KEY_LENGTH => { - let mut bits: [u8; 64] = [0u8; 64]; - bits.copy_from_slice(&bytes[..64]); - - Ok(PrivateKey(bits)) + let keypair: &[u8; 64] = bytes.try_into().unwrap(); + ed25519::Ed25519SigningKey::from_keypair_bytes(keypair) + .map(PrivateKey) + .map_err(|e| anyhow!("Invalid keypair bytes: {e}")) } _ => Err(anyhow!("Invalid secret key length")), } @@ -107,21 +103,15 @@ impl PrivateKey { } pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_LENGTH] { - self.0 - } - - pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_LENGTH] { - &self.0 + self.0.to_keypair_bytes() } pub fn to_hex(&self) -> String { - hex::encode(&self.0[..32]) + hex::encode(self.0.as_bytes()) } pub fn sign(&self, message: impl AsRef<[u8]>) -> WalletSignature { - let seed: [u8; 32] = self.0[..32].try_into().unwrap(); - let signing_key = SigningKey::from_bytes(&seed); - WalletSignature::from_bytes(signing_key.sign(message.as_ref()).to_bytes()) + WalletSignature::from(ed25519::sign(&self.0, message.as_ref())) } } diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index e14b1ef882..2cd67c0ac6 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use super::private_key::PrivateKey; use super::wallet_signature::WalletSignature; use anyhow::Result; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use multiversx_chain_core::std::crypto::ed25519; use multiversx_chain_core::types::Address; use serde::{ de::{Deserialize, Deserializer}, @@ -13,23 +13,23 @@ use serde::{ pub const PUBLIC_KEY_LENGTH: usize = 32; #[derive(Copy, Clone)] -pub struct PublicKey([u8; PUBLIC_KEY_LENGTH]); +pub struct PublicKey(ed25519::Ed25519VerifyingKey); impl PublicKey { pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] { - self.0 + self.0.to_bytes() } pub fn as_bytes(&self) -> &[u8; PUBLIC_KEY_LENGTH] { - &self.0 + self.0.as_bytes() } pub fn to_address(&self) -> Address { - self.0.into() + (*self.0.as_bytes()).into() } pub fn to_hex(&self) -> String { - hex::encode(self.0) + hex::encode(self.0.as_bytes()) } pub fn from_hex_str(pk: &str) -> Result { @@ -37,26 +37,19 @@ impl PublicKey { let bits: [u8; PUBLIC_KEY_LENGTH] = bytes .try_into() .map_err(|_| anyhow::anyhow!("invalid public key length, expected 32 bytes"))?; - Ok(Self(bits)) + ed25519::verifying_key_from_bytes(&bits) + .map(PublicKey) + .ok_or_else(|| anyhow::anyhow!("invalid ed25519 public key")) } pub fn verify(&self, message: &[u8], signature: &WalletSignature) -> bool { - let Ok(verifying_key) = VerifyingKey::from_bytes(&self.0) else { - return false; - }; - let signature = Signature::from_bytes(signature.as_bytes()); - verifying_key.verify(message, &signature).is_ok() + ed25519::verify(&self.0, message, signature.inner()) } } impl From<&PrivateKey> for PublicKey { fn from(private_key: &PrivateKey) -> PublicKey { - let bytes = private_key.to_bytes(); - - let mut bits: [u8; 32] = [0u8; 32]; - bits.copy_from_slice(&bytes[32..]); - - PublicKey(bits) + PublicKey(private_key.0.verifying_key()) } } diff --git a/sdk/core/src/crypto/wallet_signature.rs b/sdk/core/src/crypto/wallet_signature.rs index efe0eecd39..3ded496b4a 100644 --- a/sdk/core/src/crypto/wallet_signature.rs +++ b/sdk/core/src/crypto/wallet_signature.rs @@ -1,27 +1,28 @@ use std::fmt; use anyhow::Result; +use multiversx_chain_core::std::crypto::ed25519; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -/// A 64-byte Ed25519 signature, serialized as a lowercase hex string. +/// An Ed25519 signature, serialized as a lowercase hex string. #[derive(Copy, Clone, PartialEq, Eq)] -pub struct WalletSignature([u8; 64]); +pub struct WalletSignature(ed25519::Ed25519Signature); impl WalletSignature { pub fn from_bytes(bytes: [u8; 64]) -> Self { - Self(bytes) + Self(ed25519::signature_from_bytes(&bytes)) } pub fn to_bytes(&self) -> [u8; 64] { - self.0 + self.0.to_bytes() } - pub fn as_bytes(&self) -> &[u8; 64] { + pub fn inner(&self) -> &ed25519::Ed25519Signature { &self.0 } pub fn to_hex(&self) -> String { - hex::encode(self.0) + hex::encode(self.to_bytes()) } pub fn from_hex_str(s: &str) -> Result { @@ -29,19 +30,19 @@ impl WalletSignature { let bits: [u8; 64] = bytes .try_into() .map_err(|_| anyhow::anyhow!("invalid signature length, expected 64 bytes"))?; - Ok(Self(bits)) + Ok(Self::from_bytes(bits)) } } impl From<[u8; 64]> for WalletSignature { fn from(bytes: [u8; 64]) -> Self { - Self(bytes) + Self::from_bytes(bytes) } } -impl AsRef<[u8]> for WalletSignature { - fn as_ref(&self) -> &[u8] { - &self.0 +impl From for WalletSignature { + fn from(sig: ed25519::Ed25519Signature) -> Self { + Self(sig) } } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index a4b73d1eff..c217946cd2 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -146,7 +146,7 @@ impl Wallet { pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { WalletPem { - private_key: self.private_key, + private_key: self.private_key.clone(), address: Bech32Address::encode_address(hrp, self.address.clone()), } } diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs index 1f492367d6..3267297496 100644 --- a/sdk/core/tests/wallet_keystore_test.rs +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -34,7 +34,7 @@ fn erd_hrp() -> Bech32Hrp { fn test_keystore_encrypt_decrypt_roundtrip() { let private_key = alice_private_key(); let keystore = Keystore::encrypt( - private_key, + private_key.clone(), erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), @@ -73,7 +73,7 @@ fn test_keystore_wrong_password_rejected() { fn test_keystore_serialize_deserialize_roundtrip() { let private_key = alice_private_key(); let keystore = Keystore::encrypt( - private_key, + private_key.clone(), erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), From 94b1418a24e89a1ad3993511eabed44b8c6cbdd5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 16:52:07 +0300 Subject: [PATCH 043/195] Wallet keystore encrypt takes private key ref --- framework/meta/src/cmd/wallet_cmd.rs | 4 ++-- framework/meta/tests/wallet_encrypt_keystore_test.rs | 2 +- sdk/core/src/wallet/keystore.rs | 4 ++-- sdk/core/tests/wallet_keystore_test.rs | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 0903392ffd..50d4b6f493 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -87,7 +87,7 @@ fn convert(convert_args: &WalletConvertArgs) { let wallet_pem = WalletPem::from_pem_file(file).expect("error reading PEM file"); let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - wallet_pem.private_key, + &wallet_pem.private_key, hrp, &get_keystore_password(), randomness, @@ -228,7 +228,7 @@ fn new(new_args: &WalletNewArgs) { Some("keystore-secret") => { let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - new_wallet_info.wallet.private_key, + &new_wallet_info.wallet.private_key, hrp, &get_keystore_password(), randomness, diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 5008524412..fa826d96ed 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -17,7 +17,7 @@ const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e4 fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let wallet = Wallet::from(PrivateKey::from_hex_str(ALICE_PRIVATE_KEY).unwrap()); let json_result = Keystore::encrypt( - wallet.private_key, + &wallet.private_key, hrp.try_into().expect("invalid HRP"), KEYSTORE_PASSWORD, new_keystore_randomness(), diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 6e3a97a5c9..3dbe6b97ee 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -118,12 +118,12 @@ impl Keystore { /// The wallet address stored in the keystore is derived from `private_key` /// and encoded with the given `hrp`. pub fn encrypt( - private_key: PrivateKey, + private_key: &PrivateKey, hrp: Bech32Hrp, password: &str, randomness: KeystoreRandomness, ) -> Self { - let public_key = PublicKey::from(&private_key); + let public_key = PublicKey::from(private_key); let address = public_key.to_address().to_bech32(hrp); let private_key_bytes = private_key.to_bytes(); diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs index 3267297496..26978f5264 100644 --- a/sdk/core/tests/wallet_keystore_test.rs +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -34,7 +34,7 @@ fn erd_hrp() -> Bech32Hrp { fn test_keystore_encrypt_decrypt_roundtrip() { let private_key = alice_private_key(); let keystore = Keystore::encrypt( - private_key.clone(), + &private_key, erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), @@ -53,7 +53,7 @@ fn test_keystore_encrypt_decrypt_roundtrip() { #[test] fn test_keystore_wrong_password_rejected() { let keystore = Keystore::encrypt( - alice_private_key(), + &alice_private_key(), erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), @@ -73,7 +73,7 @@ fn test_keystore_wrong_password_rejected() { fn test_keystore_serialize_deserialize_roundtrip() { let private_key = alice_private_key(); let keystore = Keystore::encrypt( - private_key.clone(), + &private_key, erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), @@ -90,7 +90,7 @@ fn test_keystore_serialize_deserialize_roundtrip() { #[test] fn test_keystore_json_snapshot() { let keystore = Keystore::encrypt( - alice_private_key(), + &alice_private_key(), erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), @@ -107,7 +107,7 @@ fn test_full_encrypt_serialize_deserialize_decrypt_sign() { // Encrypt. let keystore = Keystore::encrypt( - alice_private_key(), + &alice_private_key(), erd_hrp(), KEYSTORE_PASSWORD, fixed_randomness(), From 2c0b04be02469613a05c85e012ba7dc105cf0b4f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 28 May 2026 17:30:59 +0300 Subject: [PATCH 044/195] fmt --- sdk/core/tests/private_key_test.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/core/tests/private_key_test.rs b/sdk/core/tests/private_key_test.rs index fe981fc8be..badd6d43ee 100644 --- a/sdk/core/tests/private_key_test.rs +++ b/sdk/core/tests/private_key_test.rs @@ -1,8 +1,7 @@ -use multiversx_sdk::crypto::private_key::{PRIVATE_KEY_LENGTH, SEED_LENGTH, PrivateKey}; +use multiversx_sdk::crypto::private_key::{PRIVATE_KEY_LENGTH, PrivateKey, SEED_LENGTH}; // Alice's well-known key material (consistent with wallet_pem_test.rs). -const ALICE_SEED_HEX: &str = - "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; +const ALICE_SEED_HEX: &str = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; const ALICE_PUBLIC_KEY_HEX: &str = "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"; From 0511a1869d177c375e2c9382460fadba7d20fcf6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 29 May 2026 01:13:22 +0300 Subject: [PATCH 045/195] chain core crypto ed25519 refactor --- chain/core/src/std/crypto/ed25519.rs | 146 ++++++++++++++++-------- chain/vm/src/crypto_functions.rs | 8 +- sdk/core/src/crypto/private_key.rs | 6 +- sdk/core/src/crypto/public_key.rs | 4 +- sdk/core/src/crypto/wallet_signature.rs | 2 +- 5 files changed, 106 insertions(+), 60 deletions(-) diff --git a/chain/core/src/std/crypto/ed25519.rs b/chain/core/src/std/crypto/ed25519.rs index 7d5526e7e3..e0aea57f3f 100644 --- a/chain/core/src/std/crypto/ed25519.rs +++ b/chain/core/src/std/crypto/ed25519.rs @@ -1,44 +1,85 @@ use ed25519_dalek::{Signer, Verifier}; -pub const SIGNATURE_LENGTH: usize = 64; +/// An Ed25519 signing (private) key, wrapping a 32-byte seed. +#[derive(Clone, PartialEq, Eq)] +pub struct Ed25519SigningKey(ed25519_dalek::SigningKey); + +/// An Ed25519 verifying (public) key. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Ed25519VerifyingKey(ed25519_dalek::VerifyingKey); + +/// An Ed25519 signature. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Ed25519Signature(ed25519_dalek::Signature); + +impl Ed25519SigningKey { + /// Constructs a signing key from a 32-byte seed. + pub fn from_seed(seed: &[u8; 32]) -> Self { + Self(ed25519_dalek::SigningKey::from_bytes(seed)) + } -pub type Ed25519SigningKey = ed25519_dalek::SigningKey; -pub type Ed25519VerifyingKey = ed25519_dalek::VerifyingKey; -pub type Ed25519Signature = ed25519_dalek::Signature; + /// Constructs a signing key from a 64-byte keypair (seed || public key). + /// Returns an error if the public key half does not match the seed half. + pub fn from_keypair_bytes(bytes: &[u8; 64]) -> Result { + ed25519_dalek::SigningKey::from_keypair_bytes(bytes).map(Self) + } -/// Constructs a signing key from a 32-byte seed. -pub fn signing_key_from_seed(seed: &[u8; 32]) -> Ed25519SigningKey { - Ed25519SigningKey::from_bytes(seed) -} + /// Returns the 32-byte seed (secret scalar). + pub fn to_seed_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } -/// Derives the verifying (public) key from a 32-byte signing seed. -pub fn verifying_key_from_seed(seed: &[u8; 32]) -> Ed25519VerifyingKey { - signing_key_from_seed(seed).verifying_key() -} + /// Returns the full 64-byte keypair encoding (seed || public key). + pub fn to_keypair_bytes(&self) -> [u8; 64] { + self.0.to_keypair_bytes() + } -/// Constructs a verifying key from its 32-byte compressed representation. -/// Returns `None` if the bytes do not represent a valid curve point. -pub fn verifying_key_from_bytes(bytes: &[u8; 32]) -> Option { - Ed25519VerifyingKey::from_bytes(bytes).ok() -} + /// Derives the corresponding verifying (public) key. + pub fn verifying_key(&self) -> Ed25519VerifyingKey { + Ed25519VerifyingKey(self.0.verifying_key()) + } -/// Constructs an Ed25519 signature from its 64-byte representation. -pub fn signature_from_bytes(bytes: &[u8; 64]) -> Ed25519Signature { - Ed25519Signature::from_bytes(bytes) + /// Signs a message with this key. + pub fn sign(&self, message: &[u8]) -> Ed25519Signature { + Ed25519Signature(self.0.sign(message)) + } } -/// Signs a message with the given signing key. -pub fn sign(signing_key: &Ed25519SigningKey, message: &[u8]) -> Ed25519Signature { - signing_key.sign(message) +impl Ed25519VerifyingKey { + /// Constructs a verifying key from its 32-byte compressed representation. + /// Returns `None` if the bytes do not represent a valid curve point. + pub fn from_bytes(bytes: &[u8; 32]) -> Option { + ed25519_dalek::VerifyingKey::from_bytes(bytes) + .ok() + .map(Self) + } + + /// Returns the 32-byte compressed encoding of the public key. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Returns a reference to the 32-byte compressed encoding of the public key. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + + /// Verifies an Ed25519 signature. Returns `true` if the signature is valid. + pub fn verify(&self, message: &[u8], signature: &Ed25519Signature) -> bool { + self.0.verify(message, &signature.0).is_ok() + } } -/// Verifies an Ed25519 signature. Returns `true` if the signature is valid. -pub fn verify( - verifying_key: &Ed25519VerifyingKey, - message: &[u8], - signature: &Ed25519Signature, -) -> bool { - verifying_key.verify(message, signature).is_ok() +impl Ed25519Signature { + /// Constructs a signature from its 64-byte representation. + pub fn from_bytes(bytes: &[u8; 64]) -> Self { + Self(ed25519_dalek::Signature::from_bytes(bytes)) + } + + /// Returns the 64-byte encoding of this signature. + pub fn to_bytes(&self) -> [u8; 64] { + self.0.to_bytes() + } } #[cfg(test)] @@ -59,65 +100,70 @@ mod tests { #[test] fn verifying_key_derived_from_seed() { - assert_eq!(verifying_key_from_seed(&SEED).to_bytes(), PK_BYTES); + assert_eq!( + Ed25519SigningKey::from_seed(&SEED) + .verifying_key() + .to_bytes(), + PK_BYTES + ); } #[test] fn signing_key_from_seed_roundtrip() { - let sk = signing_key_from_seed(&SEED); + let sk = Ed25519SigningKey::from_seed(&SEED); assert_eq!(sk.verifying_key().to_bytes(), PK_BYTES); } #[test] fn sign_produces_known_signature() { - let sk = signing_key_from_seed(&SEED); - assert_eq!(sign(&sk, b"").to_bytes(), SIG_BYTES); + let sk = Ed25519SigningKey::from_seed(&SEED); + assert_eq!(sk.sign(b"").to_bytes(), SIG_BYTES); } #[test] fn verify_valid_signature() { - let vk = verifying_key_from_seed(&SEED); - let sig = signature_from_bytes(&SIG_BYTES); - assert!(verify(&vk, b"", &sig)); + let vk = Ed25519SigningKey::from_seed(&SEED).verifying_key(); + let sig = Ed25519Signature::from_bytes(&SIG_BYTES); + assert!(vk.verify(b"", &sig)); } #[test] fn verify_rejects_wrong_message() { - let vk = verifying_key_from_seed(&SEED); - let sig = signature_from_bytes(&SIG_BYTES); - assert!(!verify(&vk, b"wrong", &sig)); + let vk = Ed25519SigningKey::from_seed(&SEED).verifying_key(); + let sig = Ed25519Signature::from_bytes(&SIG_BYTES); + assert!(!vk.verify(b"wrong", &sig)); } #[test] fn verify_rejects_wrong_signature() { - let vk = verifying_key_from_seed(&SEED); + let vk = Ed25519SigningKey::from_seed(&SEED).verifying_key(); let mut bad_bytes = SIG_BYTES; bad_bytes[0] ^= 0xff; - let bad_sig = signature_from_bytes(&bad_bytes); - assert!(!verify(&vk, b"", &bad_sig)); + let bad_sig = Ed25519Signature::from_bytes(&bad_bytes); + assert!(!vk.verify(b"", &bad_sig)); } #[test] fn verifying_key_from_bytes_roundtrip() { - let vk = verifying_key_from_bytes(&PK_BYTES).unwrap(); + let vk = Ed25519VerifyingKey::from_bytes(&PK_BYTES).unwrap(); assert_eq!(vk.to_bytes(), PK_BYTES); } #[test] fn verifying_key_from_bytes_different_seeds_differ() { - let vk1 = verifying_key_from_bytes(&PK_BYTES).unwrap(); - let vk2 = verifying_key_from_seed(&[0x42u8; 32]); + let vk1 = Ed25519VerifyingKey::from_bytes(&PK_BYTES).unwrap(); + let vk2 = Ed25519SigningKey::from_seed(&[0x42u8; 32]).verifying_key(); assert_ne!(vk1.to_bytes(), vk2.to_bytes()); } #[test] fn sign_then_verify_roundtrip() { let seed: [u8; 32] = [0x42u8; 32]; - let sk = signing_key_from_seed(&seed); + let sk = Ed25519SigningKey::from_seed(&seed); let vk = sk.verifying_key(); let msg = b"hello multiversx"; - let sig = sign(&sk, msg); - assert!(verify(&vk, msg, &sig)); - assert!(!verify(&vk, b"other message", &sig)); + let sig = sk.sign(msg); + assert!(vk.verify(msg, &sig)); + assert!(!vk.verify(b"other message", &sig)); } } diff --git a/chain/vm/src/crypto_functions.rs b/chain/vm/src/crypto_functions.rs index d77e85eb2a..e5e63ec969 100644 --- a/chain/vm/src/crypto_functions.rs +++ b/chain/vm/src/crypto_functions.rs @@ -20,7 +20,7 @@ pub fn keccak256(data: &[u8]) -> [u8; crypto::KECCAK256_RESULT_LEN] { /// Verifies an Ed25519 signature. /// -/// Wraps [`multiversx_chain_core::std::crypto::ed25519::verify`]. +/// Wraps [`multiversx_chain_core::std::crypto::ed25519::Ed25519VerifyingKey::verify`]. /// /// Returns `true` if `signature` is a valid Ed25519 signature of `message` /// under the public key `key`. @@ -37,9 +37,9 @@ pub fn verify_ed25519(key: &[u8], message: &[u8], signature: &[u8]) -> bool { let Ok(sig_64) = signature.try_into() else { return false; }; - let Some(verifying_key) = crypto::ed25519::verifying_key_from_bytes(key_32) else { + let Some(verifying_key) = crypto::ed25519::Ed25519VerifyingKey::from_bytes(key_32) else { return false; }; - let sig = crypto::ed25519::signature_from_bytes(sig_64); - crypto::ed25519::verify(&verifying_key, message, &sig) + let sig = crypto::ed25519::Ed25519Signature::from_bytes(sig_64); + verifying_key.verify(message, &sig) } diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index f2aea59546..8d6bfda76b 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -29,7 +29,7 @@ impl PrivateKey { match bytes.len() { SEED_LENGTH => { let seed: [u8; 32] = bytes.try_into().unwrap(); - Ok(PrivateKey(ed25519::signing_key_from_seed(&seed))) + Ok(PrivateKey(ed25519::Ed25519SigningKey::from_seed(&seed))) } PRIVATE_KEY_LENGTH => { let keypair: &[u8; 64] = bytes.try_into().unwrap(); @@ -107,11 +107,11 @@ impl PrivateKey { } pub fn to_hex(&self) -> String { - hex::encode(self.0.as_bytes()) + hex::encode(self.0.to_seed_bytes()) } pub fn sign(&self, message: impl AsRef<[u8]>) -> WalletSignature { - WalletSignature::from(ed25519::sign(&self.0, message.as_ref())) + WalletSignature::from(self.0.sign(message.as_ref())) } } diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index 2cd67c0ac6..869e9bd15b 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -37,13 +37,13 @@ impl PublicKey { let bits: [u8; PUBLIC_KEY_LENGTH] = bytes .try_into() .map_err(|_| anyhow::anyhow!("invalid public key length, expected 32 bytes"))?; - ed25519::verifying_key_from_bytes(&bits) + ed25519::Ed25519VerifyingKey::from_bytes(&bits) .map(PublicKey) .ok_or_else(|| anyhow::anyhow!("invalid ed25519 public key")) } pub fn verify(&self, message: &[u8], signature: &WalletSignature) -> bool { - ed25519::verify(&self.0, message, signature.inner()) + self.0.verify(message, signature.inner()) } } diff --git a/sdk/core/src/crypto/wallet_signature.rs b/sdk/core/src/crypto/wallet_signature.rs index 3ded496b4a..352dc29144 100644 --- a/sdk/core/src/crypto/wallet_signature.rs +++ b/sdk/core/src/crypto/wallet_signature.rs @@ -10,7 +10,7 @@ pub struct WalletSignature(ed25519::Ed25519Signature); impl WalletSignature { pub fn from_bytes(bytes: [u8; 64]) -> Self { - Self(ed25519::signature_from_bytes(&bytes)) + Self(ed25519::Ed25519Signature::from_bytes(&bytes)) } pub fn to_bytes(&self) -> [u8; 64] { From 9311ae9b6996caddca2082f3390a7a20d2555580 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 29 May 2026 01:20:19 +0300 Subject: [PATCH 046/195] dependency cleanup --- Cargo.lock | 1 - chain/vm/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e68470098..d97ca985d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3241,7 +3241,6 @@ dependencies = [ "anyhow", "bitflags 2.11.1", "colored 3.1.1", - "ed25519-dalek", "hex", "itertools 0.14.0", "log", diff --git a/chain/vm/Cargo.toml b/chain/vm/Cargo.toml index 6a3033b734..bf0144dd3d 100644 --- a/chain/vm/Cargo.toml +++ b/chain/vm/Cargo.toml @@ -32,7 +32,6 @@ bitflags = "2.9" colored = "3.0" rand = { version = "0.10", optional = true } rand_seeder = "0.5.0" -ed25519-dalek = "2.1" serde = { version = "1.0", features = ["derive"] } toml = "1.0" anyhow = "1.0" From 71629c6a7c132ee4393e21b29f8dede3e98c6011 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 29 May 2026 01:22:39 +0300 Subject: [PATCH 047/195] Wallet - tx hashing refactor --- sdk/core/src/wallet.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index c217946cd2..f92a74e3a6 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -20,12 +20,10 @@ use std::path::Path; use anyhow::Result; use bip39::Mnemonic; use multiversx_chain_core::{ - std::{Bech32Address, Bech32Hrp}, + std::{Bech32Address, Bech32Hrp, crypto}, types::Address, }; use serde_json::json; -use sha2::Digest; -use sha3::Keccak256; use crate::data::transaction::Transaction; @@ -132,9 +130,7 @@ impl Wallet { let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0; if should_sign_on_tx_hash { - let mut h = Keccak256::new(); - h.update(tx_bytes); - tx_bytes = h.finalize().to_vec(); + tx_bytes = crypto::keccak256(&tx_bytes).to_vec(); } self.private_key.sign(tx_bytes) From dac23b02d2cab4c57dd9c17ef6bb04e0775db540 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 29 May 2026 02:15:25 +0300 Subject: [PATCH 048/195] CI - windows Rust version --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 88b42d83aa..bb81937f95 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -21,7 +21,7 @@ jobs: - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: stable + toolchain: 1.95 target: wasm32-unknown-unknown rustflags: "" From 245cbd1e40bd4bdffb0454f4587558d6cd238efe Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 09:50:02 +0300 Subject: [PATCH 049/195] Wallet PrivateKey from_bytes/from_seed_hex_str split into seed/keypair; mor erefactoring --- framework/meta/src/cmd/wallet_cmd.rs | 6 +- .../meta/tests/wallet_convert_cli_test.rs | 2 +- .../tests/wallet_encrypt_keystore_test.rs | 2 +- sdk/core/src/crypto/private_key.rs | 84 ++++++++++++++++--- sdk/core/src/wallet.rs | 10 +-- sdk/core/src/wallet/keystore.rs | 5 +- sdk/core/src/wallet/wallet_pem.rs | 4 +- sdk/core/tests/private_key_test.rs | 20 ++--- sdk/core/tests/wallet_keystore_test.rs | 2 +- sdk/core/tests/wallet_pem_test.rs | 4 +- sdk/core/tests/wallet_test.rs | 8 +- sdk/http/examples/sign_tx.rs | 19 ++--- sdk/http/examples/sign_txs.rs | 19 ++--- 13 files changed, 122 insertions(+), 63 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 50d4b6f493..9e329dc983 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -50,7 +50,7 @@ fn convert(convert_args: &WalletConvertArgs) { ("mnemonic", "pem") => match infile { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); - let wallet = Wallet::from_mnemonic_string(mnemonic_str); + let wallet = Wallet::from_mnemonic_string(mnemonic_str).unwrap(); write_resulted_pem(wallet.to_pem(hrp), outfile); } None => { @@ -58,7 +58,7 @@ fn convert(convert_args: &WalletConvertArgs) { "Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); - let wallet = Wallet::from_mnemonic_string(mnemonic_str); + let wallet = Wallet::from_mnemonic_string(mnemonic_str).unwrap(); write_resulted_pem(wallet.to_pem(hrp), outfile); } }, @@ -179,7 +179,7 @@ struct NewWalletInfo { impl NewWalletInfo { fn generate() -> Self { let mnemonic = generate_mnemonic(); - let wallet = Wallet::from_mnemonic_string(mnemonic.to_string()); + let wallet = Wallet::from_mnemonic_string(mnemonic.to_string()).unwrap(); NewWalletInfo { mnemonic, wallet } } diff --git a/framework/meta/tests/wallet_convert_cli_test.rs b/framework/meta/tests/wallet_convert_cli_test.rs index 07a140e6de..492533def7 100644 --- a/framework/meta/tests/wallet_convert_cli_test.rs +++ b/framework/meta/tests/wallet_convert_cli_test.rs @@ -58,7 +58,7 @@ fn test_convert_mnemonic_to_pem_file_in_file_out() { }); let wallet = Wallet::from_pem_file(&output_pem).unwrap(); - assert_eq!(wallet.private_key.to_hex(), KNOWN_PRIVATE_KEY); + assert_eq!(wallet.private_key.to_seed_hex(), KNOWN_PRIVATE_KEY); assert_eq!(wallet.public_key().to_hex(), KNOWN_PUBLIC_KEY); let pem_content = fs::read_to_string(&output_pem).unwrap(); diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index fa826d96ed..92c6087393 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -15,7 +15,7 @@ const KEYSTORE_PASSWORD: &str = "abcd"; const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { - let wallet = Wallet::from(PrivateKey::from_hex_str(ALICE_PRIVATE_KEY).unwrap()); + let wallet = Wallet::from(PrivateKey::from_seed_hex_str(ALICE_PRIVATE_KEY).unwrap()); let json_result = Keystore::encrypt( &wallet.private_key, hrp.try_into().expect("invalid HRP"), diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 8d6bfda76b..357a519413 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -25,22 +25,76 @@ const HARDENED: u32 = 0x80000000; pub struct PrivateKey(pub ed25519::Ed25519SigningKey); impl PrivateKey { + /// Constructs a [`PrivateKey`] from a 32-byte ed25519 seed. + /// + /// The corresponding public (verifying) key is derived automatically. + /// This is the canonical representation used throughout the MultiversX SDK. + pub fn from_seed_bytes(bytes: &[u8; 32]) -> PrivateKey { + PrivateKey(ed25519::Ed25519SigningKey::from_seed(bytes)) + } + + /// Constructs a [`PrivateKey`] from a 64-byte keypair `[seed || public_key]`. + /// + /// Returns an error if the embedded public key does not match the seed. + pub fn from_keypair_bytes(bytes: &[u8; 64]) -> Result { + ed25519::Ed25519SigningKey::from_keypair_bytes(bytes) + .map(PrivateKey) + .map_err(|e| anyhow!("Invalid keypair bytes: {e}")) + } + + /// Constructs a [`PrivateKey`] from a slice whose length determines the format: + /// - 32 bytes → treated as a seed (see [`from_seed_bytes`](Self::from_seed_bytes)) + /// - 64 bytes → treated as a keypair (see [`from_keypair_bytes`](Self::from_keypair_bytes)) + /// + /// Returns an error for any other length. pub fn from_bytes(bytes: &[u8]) -> Result { match bytes.len() { SEED_LENGTH => { - let seed: [u8; 32] = bytes.try_into().unwrap(); - Ok(PrivateKey(ed25519::Ed25519SigningKey::from_seed(&seed))) + let seed: &[u8; 32] = bytes + .try_into() + .map_err(|_| anyhow!("Invalid secret key length"))?; + Ok(PrivateKey::from_seed_bytes(seed)) } PRIVATE_KEY_LENGTH => { - let keypair: &[u8; 64] = bytes.try_into().unwrap(); - ed25519::Ed25519SigningKey::from_keypair_bytes(keypair) - .map(PrivateKey) - .map_err(|e| anyhow!("Invalid keypair bytes: {e}")) + let keypair: &[u8; 64] = bytes + .try_into() + .map_err(|_| anyhow!("Invalid secret key length"))?; + PrivateKey::from_keypair_bytes(keypair) } _ => Err(anyhow!("Invalid secret key length")), } } + /// Decodes a hex string into a 32-byte seed and constructs a [`PrivateKey`]. + /// + /// The input must be exactly 64 hex characters (32 bytes). Returns an error + /// if the string is not valid hex or does not decode to exactly 32 bytes. + pub fn from_seed_hex_str(pk: &str) -> Result { + let bytes = hex::decode(pk)?; + let seed: [u8; 32] = bytes + .try_into() + .map_err(|_| anyhow!("Invalid seed key length"))?; + Ok(PrivateKey::from_seed_bytes(&seed)) + } + + /// Decodes a hex string into a 64-byte keypair and constructs a [`PrivateKey`]. + /// + /// The input must be exactly 128 hex characters (64 bytes). Returns an error + /// if the string is not valid hex, does not decode to exactly 64 bytes, or + /// the embedded public key does not match the seed. + pub fn from_keypair_hex_str(pk: &str) -> Result { + let bytes = hex::decode(pk)?; + let keypair: [u8; 64] = bytes + .try_into() + .map_err(|_| anyhow!("Invalid keypair key length"))?; + PrivateKey::from_keypair_bytes(&keypair) + } + + /// Decodes a hex string and constructs a [`PrivateKey`], inferring the format + /// from the decoded length (32 bytes → seed, 64 bytes → keypair). + /// + /// Prefer [`from_seed_hex_str`](Self::from_seed_hex_str) or + /// [`from_keypair_hex_str`](Self::from_keypair_hex_str) when the format is known. pub fn from_hex_str(pk: &str) -> Result { let bytes = hex::decode(pk)?; PrivateKey::from_bytes(bytes.as_slice()) @@ -65,7 +119,11 @@ impl PrivateKey { seed } - pub fn from_mnemonic(mnemonic: Mnemonic, account: u32, address_index: u32) -> PrivateKey { + pub fn from_mnemonic( + mnemonic: Mnemonic, + account: u32, + address_index: u32, + ) -> Result { let seed = Self::seed_from_mnemonic(mnemonic, ""); let serialized_key_len = 32; @@ -99,14 +157,18 @@ impl PrivateKey { chain_code = intermediary[serialized_key_len..].to_vec(); } - PrivateKey::from_bytes(key.as_slice()).unwrap() + let seed: &[u8; 32] = key + .as_slice() + .try_into() + .map_err(|_| anyhow!("BIP32 derived key has unexpected length"))?; + Ok(PrivateKey::from_seed_bytes(seed)) } pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_LENGTH] { self.0.to_keypair_bytes() } - pub fn to_hex(&self) -> String { + pub fn to_seed_hex(&self) -> String { hex::encode(self.0.to_seed_bytes()) } @@ -117,7 +179,7 @@ impl PrivateKey { impl Display for PrivateKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.to_hex().fmt(f) + self.to_seed_hex().fmt(f) } } @@ -132,7 +194,7 @@ impl Serialize for PrivateKey { where S: Serializer, { - serializer.serialize_str(self.to_hex().as_str()) + serializer.serialize_str(self.to_seed_hex().as_str()) } } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index f92a74e3a6..cfa39c5204 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -71,10 +71,10 @@ impl From for Wallet { } impl Wallet { - pub fn from_mnemonic_string(mnemonic_str: String) -> Wallet { - let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', "")).unwrap(); - let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32); - Self::new(private_key, WalletSource::Mnemonic) + pub fn from_mnemonic_string(mnemonic_str: String) -> Result { + let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', ""))?; + let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32)?; + Ok(Self::new(private_key, WalletSource::Mnemonic)) } #[deprecated( @@ -111,7 +111,7 @@ impl Wallet { } pub fn private_key_hex(&self) -> String { - self.private_key.to_hex() + self.private_key.to_seed_hex() } pub fn public_key(&self) -> PublicKey { diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index 3dbe6b97ee..d5918afdb4 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -106,7 +106,10 @@ impl Keystore { self.ciphertext.clone(), ); - let private_key = PrivateKey::from_bytes(&private_key_bytes)?; + let private_key_arr: [u8; 64] = private_key_bytes + .try_into() + .map_err(|_| anyhow::anyhow!("decrypted keystore has wrong key length"))?; + let private_key = PrivateKey::from_keypair_bytes(&private_key_arr)?; Ok(Wallet::new( private_key, super::WalletSource::Keystore(self.address.hrp), diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs index f86973379b..0f1c437ee1 100644 --- a/sdk/core/src/wallet/wallet_pem.rs +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -46,7 +46,7 @@ impl WalletPem { let private_key_bytes = &decoded[..decoded.len() / 2]; let private_key_str = std::str::from_utf8(private_key_bytes) .map_err(|e| anyhow!("invalid UTF-8 in private key: {e}"))?; - let private_key = PrivateKey::from_hex_str(private_key_str)?; + let private_key = PrivateKey::from_seed_hex_str(private_key_str)?; Ok(WalletPem { private_key, @@ -90,7 +90,7 @@ impl WalletPem { } pub fn private_key_hex(&self) -> String { - self.private_key.to_hex() + self.private_key.to_seed_hex() } pub fn public_key_hex(&self) -> String { diff --git a/sdk/core/tests/private_key_test.rs b/sdk/core/tests/private_key_test.rs index badd6d43ee..58b9ca4737 100644 --- a/sdk/core/tests/private_key_test.rs +++ b/sdk/core/tests/private_key_test.rs @@ -22,16 +22,16 @@ fn test_from_bytes_seed_length_ok() { let seed = alice_seed_bytes(); assert_eq!(seed.len(), SEED_LENGTH); - let pk = PrivateKey::from_bytes(&seed).unwrap(); + let pk = PrivateKey::from_seed_bytes(seed.as_slice().try_into().unwrap()); // to_hex() encodes only the first 32 bytes (the seed). - assert_eq!(pk.to_hex(), ALICE_SEED_HEX); + assert_eq!(pk.to_seed_hex(), ALICE_SEED_HEX); } #[test] fn test_from_bytes_seed_derives_correct_public_key() { let seed = alice_seed_bytes(); - let pk = PrivateKey::from_bytes(&seed).unwrap(); + let pk = PrivateKey::from_seed_bytes(seed.as_slice().try_into().unwrap()); // The upper 32 bytes of the stored key must be the verifying key. let stored = pk.to_bytes(); @@ -42,10 +42,10 @@ fn test_from_bytes_seed_derives_correct_public_key() { #[test] fn test_from_bytes_seed_roundtrip() { let seed = alice_seed_bytes(); - let pk = PrivateKey::from_bytes(&seed).unwrap(); + let pk = PrivateKey::from_seed_bytes(seed.as_slice().try_into().unwrap()); // Re-constructing from the returned 64-byte array must yield an equal key. - let pk2 = PrivateKey::from_bytes(&pk.to_bytes()).unwrap(); + let pk2 = PrivateKey::from_keypair_bytes(&pk.to_bytes()).unwrap(); assert_eq!(pk, pk2); } @@ -58,16 +58,16 @@ fn test_from_bytes_full_length_ok() { let full = hex::decode(alice_64byte_hex()).unwrap(); assert_eq!(full.len(), PRIVATE_KEY_LENGTH); - let pk = PrivateKey::from_bytes(&full).unwrap(); + let pk = PrivateKey::from_keypair_bytes(full.as_slice().try_into().unwrap()).unwrap(); // Seed portion must be preserved verbatim. - assert_eq!(pk.to_hex(), ALICE_SEED_HEX); + assert_eq!(pk.to_seed_hex(), ALICE_SEED_HEX); } #[test] fn test_from_bytes_full_preserves_all_64_bytes() { let full = hex::decode(alice_64byte_hex()).unwrap(); - let pk = PrivateKey::from_bytes(&full).unwrap(); + let pk = PrivateKey::from_keypair_bytes(full.as_slice().try_into().unwrap()).unwrap(); assert_eq!(pk.to_bytes().as_slice(), full.as_slice()); } @@ -75,8 +75,8 @@ fn test_from_bytes_full_preserves_all_64_bytes() { #[test] fn test_from_bytes_full_roundtrip() { let full = hex::decode(alice_64byte_hex()).unwrap(); - let pk = PrivateKey::from_bytes(&full).unwrap(); - let pk2 = PrivateKey::from_bytes(&pk.to_bytes()).unwrap(); + let pk = PrivateKey::from_keypair_bytes(full.as_slice().try_into().unwrap()).unwrap(); + let pk2 = PrivateKey::from_keypair_bytes(&pk.to_bytes()).unwrap(); assert_eq!(pk, pk2); } diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs index 26978f5264..b1f22f1051 100644 --- a/sdk/core/tests/wallet_keystore_test.rs +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -19,7 +19,7 @@ fn fixed_randomness() -> KeystoreRandomness { } fn alice_private_key() -> PrivateKey { - PrivateKey::from_hex_str(ALICE_PRIVATE_KEY_HEX).unwrap() + PrivateKey::from_seed_hex_str(ALICE_PRIVATE_KEY_HEX).unwrap() } fn erd_hrp() -> Bech32Hrp { diff --git a/sdk/core/tests/wallet_pem_test.rs b/sdk/core/tests/wallet_pem_test.rs index 0abcfbb8b6..f5d016f7db 100644 --- a/sdk/core/tests/wallet_pem_test.rs +++ b/sdk/core/tests/wallet_pem_test.rs @@ -60,7 +60,7 @@ fn test_pem_str_roundtrip() { fn test_wallet_from_pem_fields() { let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); let wallet = Wallet::from(pem); - assert_eq!(wallet.private_key.to_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!(wallet.private_key.to_seed_hex(), ALICE_PRIVATE_KEY_HEX); assert_eq!(wallet.address.to_bech32_default().bech32, ALICE_BECH32); } @@ -87,5 +87,5 @@ fn test_wallet_to_pem_str_roundtrip() { let hrp = Bech32Hrp::try_from("erd").unwrap(); let pem_str = wallet.to_pem(hrp).to_pem_str(); let reparsed = Wallet::from(WalletPem::from_pem_str(&pem_str).unwrap()); - assert_eq!(reparsed.private_key.to_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!(reparsed.private_key.to_seed_hex(), ALICE_PRIVATE_KEY_HEX); } diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index cf93fffba1..04cad40f40 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -7,12 +7,12 @@ use multiversx_sdk::test_wallets; fn test_private_key_from_mnemonic() { let mnemonic: Mnemonic = Mnemonic::parse_normalized("acid twice post genre topic observe valid viable gesture fortune funny dawn around blood enemy page update reduce decline van bundle zebra rookie real").unwrap(); - let private_key = PrivateKey::from_mnemonic(mnemonic.clone(), 0, 0); + let private_key = PrivateKey::from_mnemonic(mnemonic.clone(), 0, 0).unwrap(); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( "0b7966138e80b8f3bb64046f56aea4250fd7bacad6ed214165cea6767fd0bc2c", - private_key.to_hex() + private_key.to_seed_hex() ); assert_eq!( "dfefe0453840e5903f2bd519de9b0ed6e9621e57e28ba0b4c1b15115091dd72f", @@ -23,12 +23,12 @@ fn test_private_key_from_mnemonic() { address.to_bech32_default().bech32 ); - let private_key = PrivateKey::from_mnemonic(mnemonic, 0, 1); + let private_key = PrivateKey::from_mnemonic(mnemonic, 0, 1).unwrap(); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", - private_key.to_hex() + private_key.to_seed_hex() ); assert_eq!( "af8fef070a581873912ccbafb6a78bb9eb4e003085ac43dbbdfa3e20eb93cede", diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index d5cedaf1d8..d69ae7f78c 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -2,21 +2,17 @@ use multiversx_sdk::{data::transaction::Transaction, wallet::PrivateKey, wallet: use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] -async fn main() { - let wallet = Wallet::from( - PrivateKey::from_hex_str( - "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", - ) - .unwrap(), - ); +async fn main() -> anyhow::Result<()> { + let wallet = Wallet::from(PrivateKey::from_seed_hex_str( + "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", + )?); let addr = wallet.to_address(); let blockchain = GatewayHttpProxy::new(DEVNET_GATEWAY.to_string()); - let network_config = blockchain.get_network_config().await.unwrap(); + let network_config = blockchain.get_network_config().await?; let arg = blockchain .get_default_transaction_arguments(&addr, &network_config) - .await - .unwrap(); + .await?; let mut unsign_tx = Transaction { nonce: arg.nonce, @@ -34,8 +30,9 @@ async fn main() { let signature = wallet.sign_tx(&unsign_tx); unsign_tx.signature = Some(signature); - let tx_hash = blockchain.send_transaction(&unsign_tx).await.unwrap(); + let tx_hash = blockchain.send_transaction(&unsign_tx).await?; assert!(!tx_hash.is_empty()); println!("tx_hash {tx_hash}"); + Ok(()) } diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index b2f2ed2859..fb99c8db48 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -5,21 +5,17 @@ use multiversx_sdk::{ use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] -async fn main() { - let wallet = Wallet::from( - PrivateKey::from_hex_str( - "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", - ) - .unwrap(), - ); +async fn main() -> anyhow::Result<()> { + let wallet = Wallet::from(PrivateKey::from_seed_hex_str( + "1648ad209d6b157a289884933e3bb30f161ec7113221ec16f87c3578b05830b0", + )?); let addr = wallet.to_address(); let blockchain = GatewayHttpProxy::new(DEVNET_GATEWAY.to_string()); - let network_config = blockchain.get_network_config().await.unwrap(); + let network_config = blockchain.get_network_config().await?; let arg = blockchain .get_default_transaction_arguments(&addr, &network_config) - .await - .unwrap(); + .await?; let mut unsign_tx = Transaction { nonce: arg.nonce, @@ -49,8 +45,9 @@ async fn main() { unsign_tx.signature = Some(signature); txs.push(unsign_tx.clone()); - let tx_hash = blockchain.send_transactions(&txs).await.unwrap(); + let tx_hash = blockchain.send_transactions(&txs).await?; assert!(!tx_hash.is_empty()); println!("tx_hashes {tx_hash:?}"); + Ok(()) } From 3c382ab60fd03c5683d2115c1e75ebd995a0a035 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 12:47:03 +0300 Subject: [PATCH 050/195] Wallet key docs --- sdk/core/src/crypto/private_key.rs | 8 ++++++++ sdk/core/src/crypto/public_key.rs | 12 ++++++++++++ sdk/core/tests/private_key_test.rs | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/crypto/private_key.rs index 357a519413..8391a5e005 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/crypto/private_key.rs @@ -119,6 +119,11 @@ impl PrivateKey { seed } + /// Derives a [`PrivateKey`] from a BIP-39 mnemonic using the MultiversX HD path + /// `m/44'/508'/'/0'/'`. + /// + /// Returns an error if the internal BIP-32 key derivation produces a key of + /// unexpected length (should not happen in practice). pub fn from_mnemonic( mnemonic: Mnemonic, account: u32, @@ -164,14 +169,17 @@ impl PrivateKey { Ok(PrivateKey::from_seed_bytes(seed)) } + /// Returns the full 64-byte keypair as `[seed (32 bytes) || public_key (32 bytes)]`. pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_LENGTH] { self.0.to_keypair_bytes() } + /// Returns the 32-byte seed encoded as a lowercase hex string (64 characters). pub fn to_seed_hex(&self) -> String { hex::encode(self.0.to_seed_bytes()) } + /// Signs `message` with this key and returns a [`WalletSignature`]. pub fn sign(&self, message: impl AsRef<[u8]>) -> WalletSignature { WalletSignature::from(self.0.sign(message.as_ref())) } diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/crypto/public_key.rs index 869e9bd15b..2b5ca668cc 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/crypto/public_key.rs @@ -16,22 +16,32 @@ pub const PUBLIC_KEY_LENGTH: usize = 32; pub struct PublicKey(ed25519::Ed25519VerifyingKey); impl PublicKey { + /// Returns the raw 32-byte public key. pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] { self.0.to_bytes() } + /// Returns a reference to the raw 32-byte public key. pub fn as_bytes(&self) -> &[u8; PUBLIC_KEY_LENGTH] { self.0.as_bytes() } + /// Derives the MultiversX [`Address`] from this public key. + /// + /// The address is the raw 32-byte public key interpreted as an address. pub fn to_address(&self) -> Address { (*self.0.as_bytes()).into() } + /// Returns the public key encoded as a lowercase hex string (64 characters). pub fn to_hex(&self) -> String { hex::encode(self.0.as_bytes()) } + /// Decodes a 64-character hex string into a [`PublicKey`]. + /// + /// Returns an error if the string is not valid hex, does not decode to + /// exactly 32 bytes, or the bytes do not represent a valid ed25519 point. pub fn from_hex_str(pk: &str) -> Result { let bytes = hex::decode(pk)?; let bits: [u8; PUBLIC_KEY_LENGTH] = bytes @@ -42,6 +52,8 @@ impl PublicKey { .ok_or_else(|| anyhow::anyhow!("invalid ed25519 public key")) } + /// Verifies that `signature` is a valid ed25519 signature over `message` + /// produced by the private key corresponding to this public key. pub fn verify(&self, message: &[u8], signature: &WalletSignature) -> bool { self.0.verify(message, signature.inner()) } diff --git a/sdk/core/tests/private_key_test.rs b/sdk/core/tests/private_key_test.rs index 58b9ca4737..9a135d0ebe 100644 --- a/sdk/core/tests/private_key_test.rs +++ b/sdk/core/tests/private_key_test.rs @@ -24,7 +24,7 @@ fn test_from_bytes_seed_length_ok() { let pk = PrivateKey::from_seed_bytes(seed.as_slice().try_into().unwrap()); - // to_hex() encodes only the first 32 bytes (the seed). + // to_seed_hex() encodes only the first 32 bytes (the seed). assert_eq!(pk.to_seed_hex(), ALICE_SEED_HEX); } From 64a12538030448feb62f41cd4255e321d81ef4a7 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 14:30:55 +0300 Subject: [PATCH 051/195] Wallet module reorg --- sdk/core/src/crypto.rs | 3 - .../data/transaction/transaction_request.rs | 2 +- sdk/core/src/lib.rs | 1 - sdk/core/src/wallet.rs | 149 ++---------------- sdk/core/src/wallet/keystore.rs | 7 +- .../src/{crypto => wallet}/private_key.rs | 1 - sdk/core/src/{crypto => wallet}/public_key.rs | 2 +- sdk/core/src/wallet/wallet_impl.rs | 126 +++++++++++++++ sdk/core/src/wallet/wallet_pem.rs | 7 +- .../{crypto => wallet}/wallet_signature.rs | 0 sdk/core/src/wallet/wallet_source.rs | 11 ++ sdk/core/tests/private_key_test.rs | 2 +- sdk/core/tests/public_key_test.rs | 2 +- sdk/core/tests/wallet_keystore_test.rs | 2 +- sdk/core/tests/wallet_test.rs | 2 +- 15 files changed, 157 insertions(+), 160 deletions(-) delete mode 100644 sdk/core/src/crypto.rs rename sdk/core/src/{crypto => wallet}/private_key.rs (99%) rename sdk/core/src/{crypto => wallet}/public_key.rs (98%) create mode 100644 sdk/core/src/wallet/wallet_impl.rs rename sdk/core/src/{crypto => wallet}/wallet_signature.rs (100%) create mode 100644 sdk/core/src/wallet/wallet_source.rs diff --git a/sdk/core/src/crypto.rs b/sdk/core/src/crypto.rs deleted file mode 100644 index 350909fc85..0000000000 --- a/sdk/core/src/crypto.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod private_key; -pub mod public_key; -pub mod wallet_signature; diff --git a/sdk/core/src/data/transaction/transaction_request.rs b/sdk/core/src/data/transaction/transaction_request.rs index 09bf7e06e1..a9aed0fb87 100644 --- a/sdk/core/src/data/transaction/transaction_request.rs +++ b/sdk/core/src/data/transaction/transaction_request.rs @@ -1,4 +1,4 @@ -use crate::crypto::wallet_signature::WalletSignature; +use crate::wallet::WalletSignature; use multiversx_chain_core::std::Bech32Address; use serde::{Deserialize, Serialize}; diff --git a/sdk/core/src/lib.rs b/sdk/core/src/lib.rs index c16cef68a6..9ca02c071e 100644 --- a/sdk/core/src/lib.rs +++ b/sdk/core/src/lib.rs @@ -3,7 +3,6 @@ #![allow(clippy::collapsible_if)] #![allow(clippy::manual_is_multiple_of)] -pub mod crypto; pub mod data; pub mod gateway; pub mod retrieve_tx_on_network; diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index cfa39c5204..67ad2d5137 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,149 +1,20 @@ mod keystore; mod keystore_error; mod keystore_json; +mod private_key; +mod public_key; +mod wallet_impl; mod wallet_pem; +mod wallet_signature; +mod wallet_source; pub use keystore::Keystore; pub use keystore::KeystoreRandomness; pub use keystore_error::KeystoreError; pub use keystore_json::*; +pub use private_key::{PRIVATE_KEY_LENGTH, PrivateKey, SEED_LENGTH}; +pub use public_key::{PUBLIC_KEY_LENGTH, PublicKey}; +pub use wallet_impl::Wallet; pub use wallet_pem::WalletPem; - -// TODO: move under wallet -pub use crate::crypto::private_key::PrivateKey; -pub use crate::crypto::public_key::PublicKey; -pub use crate::crypto::wallet_signature::WalletSignature; - -use core::str; -use std::path::Path; - -use anyhow::Result; -use bip39::Mnemonic; -use multiversx_chain_core::{ - std::{Bech32Address, Bech32Hrp, crypto}, - types::Address, -}; -use serde_json::json; - -use crate::data::transaction::Transaction; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Wallet { - pub private_key: PrivateKey, - pub address: Address, - pub source: WalletSource, -} - -/// Optional structure that indicates how the [`Wallet`] was created, with additional metadata. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum WalletSource { - Mnemonic, - PrivateKey, - PemFile(Bech32Hrp), - TestWallet(&'static str), - Keystore(Bech32Hrp), -} - -impl Wallet { - pub fn new(private_key: PrivateKey, source: WalletSource) -> Self { - let address = PublicKey::from(&private_key).to_address(); - Wallet { - private_key, - address, - source, - } - } -} - -impl From for Wallet { - fn from(wallet_pem: WalletPem) -> Self { - Self::new( - wallet_pem.private_key, - WalletSource::PemFile(wallet_pem.address.hrp), - ) - } -} - -impl From for Wallet { - fn from(private_key: PrivateKey) -> Self { - Self::new(private_key, WalletSource::PrivateKey) - } -} - -impl Wallet { - pub fn from_mnemonic_string(mnemonic_str: String) -> Result { - let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', ""))?; - let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32)?; - Ok(Self::new(private_key, WalletSource::Mnemonic)) - } - - #[deprecated( - since = "0.67.0", - note = "Use `PrivateKey::from_hex_str(hex).map(Wallet::from)` instead" - )] - pub fn from_private_key_hex(priv_key: &str) -> Result { - let private_key = PrivateKey::from_hex_str(priv_key)?; - Ok(Self::new(private_key, WalletSource::PrivateKey)) - } - - pub fn from_pem_file

(file_path: P) -> Result - where - P: AsRef, - { - Ok(WalletPem::from_pem_file(file_path)?.into()) - } - - pub(crate) fn new_test_wallet(name: &'static str, pem: &str) -> Self { - let wallet_pem = WalletPem::from_pem_str(pem).unwrap(); - Self::new(wallet_pem.private_key, WalletSource::TestWallet(name)) - } - - #[deprecated( - since = "0.54.0", - note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address" - )] - pub fn address(&self) -> Bech32Address { - self.to_address().to_bech32_default() - } - - pub fn to_address(&self) -> Address { - self.address.clone() - } - - pub fn private_key_hex(&self) -> String { - self.private_key.to_seed_hex() - } - - pub fn public_key(&self) -> PublicKey { - PublicKey::from(&self.private_key) - } - - pub fn public_key_hex(&self) -> String { - PublicKey::from(&self.private_key).to_hex() - } - - pub fn sign_tx(&self, unsign_tx: &Transaction) -> WalletSignature { - let mut unsign_tx = unsign_tx.clone(); - unsign_tx.signature = None; - - let mut tx_bytes = json!(unsign_tx).to_string().as_bytes().to_vec(); - - let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0; - if should_sign_on_tx_hash { - tx_bytes = crypto::keccak256(&tx_bytes).to_vec(); - } - - self.private_key.sign(tx_bytes) - } - - pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> WalletSignature { - self.private_key.sign(data) - } - - pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { - WalletPem { - private_key: self.private_key.clone(), - address: Bech32Address::encode_address(hrp, self.address.clone()), - } - } -} +pub use wallet_signature::WalletSignature; +pub use wallet_source::WalletSource; diff --git a/sdk/core/src/wallet/keystore.rs b/sdk/core/src/wallet/keystore.rs index d5918afdb4..e7b5daaa22 100644 --- a/sdk/core/src/wallet/keystore.rs +++ b/sdk/core/src/wallet/keystore.rs @@ -8,12 +8,7 @@ use multiversx_chain_core::std::{Bech32Address, Bech32Hrp}; use scrypt::{Params, scrypt}; use sha2::Sha256; -use crate::{ - crypto::{private_key::PrivateKey, public_key::PublicKey}, - wallet::Wallet, -}; - -use super::{KeystoreError, KeystoreJson}; +use super::{KeystoreError, KeystoreJson, PrivateKey, PublicKey, Wallet}; const KDF_N: u32 = 4096; const KDF_R: u32 = 8; diff --git a/sdk/core/src/crypto/private_key.rs b/sdk/core/src/wallet/private_key.rs similarity index 99% rename from sdk/core/src/crypto/private_key.rs rename to sdk/core/src/wallet/private_key.rs index 8391a5e005..0d518749c6 100644 --- a/sdk/core/src/crypto/private_key.rs +++ b/sdk/core/src/wallet/private_key.rs @@ -15,7 +15,6 @@ use zeroize::Zeroize; use super::wallet_signature::WalletSignature; pub const PRIVATE_KEY_LENGTH: usize = 64; -pub const SIGNATURE_LENGTH: usize = 64; pub const SEED_LENGTH: usize = 32; const EGLD_COIN_TYPE: u32 = 508; diff --git a/sdk/core/src/crypto/public_key.rs b/sdk/core/src/wallet/public_key.rs similarity index 98% rename from sdk/core/src/crypto/public_key.rs rename to sdk/core/src/wallet/public_key.rs index 2b5ca668cc..c382ae8aba 100644 --- a/sdk/core/src/crypto/public_key.rs +++ b/sdk/core/src/wallet/public_key.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use super::private_key::PrivateKey; +use super::PrivateKey; use super::wallet_signature::WalletSignature; use anyhow::Result; use multiversx_chain_core::std::crypto::ed25519; diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs new file mode 100644 index 0000000000..5722538ede --- /dev/null +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -0,0 +1,126 @@ +use core::str; +use std::path::Path; + +use anyhow::Result; +use bip39::Mnemonic; +use multiversx_chain_core::{ + std::{Bech32Address, Bech32Hrp, crypto}, + types::Address, +}; +use serde_json::json; + +use crate::{ + data::transaction::Transaction, + wallet::{PrivateKey, PublicKey, WalletPem, WalletSignature, WalletSource}, +}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Wallet { + pub private_key: PrivateKey, + pub address: Address, + pub source: WalletSource, +} + +impl Wallet { + pub fn new(private_key: PrivateKey, source: WalletSource) -> Self { + let address = PublicKey::from(&private_key).to_address(); + Wallet { + private_key, + address, + source, + } + } +} + +impl From for Wallet { + fn from(wallet_pem: WalletPem) -> Self { + Self::new( + wallet_pem.private_key, + WalletSource::PemFile(wallet_pem.address.hrp), + ) + } +} + +impl From for Wallet { + fn from(private_key: PrivateKey) -> Self { + Self::new(private_key, WalletSource::PrivateKey) + } +} + +impl Wallet { + pub fn from_mnemonic_string(mnemonic_str: String) -> Result { + let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', ""))?; + let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32)?; + Ok(Self::new(private_key, WalletSource::Mnemonic)) + } + + #[deprecated( + since = "0.67.0", + note = "Use `PrivateKey::from_hex_str(hex).map(Wallet::from)` instead" + )] + pub fn from_private_key_hex(priv_key: &str) -> Result { + let private_key = PrivateKey::from_hex_str(priv_key)?; + Ok(Self::new(private_key, WalletSource::PrivateKey)) + } + + pub fn from_pem_file

(file_path: P) -> Result + where + P: AsRef, + { + Ok(WalletPem::from_pem_file(file_path)?.into()) + } + + pub(crate) fn new_test_wallet(name: &'static str, pem: &str) -> Self { + let wallet_pem = WalletPem::from_pem_str(pem).unwrap(); + Self::new(wallet_pem.private_key, WalletSource::TestWallet(name)) + } + + #[deprecated( + since = "0.54.0", + note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address" + )] + pub fn address(&self) -> Bech32Address { + self.to_address().to_bech32_default() + } + + pub fn to_address(&self) -> Address { + self.address.clone() + } + + pub fn private_key_hex(&self) -> String { + self.private_key.to_seed_hex() + } + + pub fn public_key(&self) -> PublicKey { + PublicKey::from(&self.private_key) + } + + pub fn public_key_hex(&self) -> String { + PublicKey::from(&self.private_key).to_hex() + } + + pub fn sign_tx(&self, unsign_tx: &Transaction) -> WalletSignature { + let mut unsign_tx = unsign_tx.clone(); + unsign_tx.signature = None; + + let mut tx_bytes = json!(unsign_tx).to_string().as_bytes().to_vec(); + + let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0; + if should_sign_on_tx_hash { + tx_bytes = crypto::keccak256(&tx_bytes).to_vec(); + } + + self.private_key.sign(tx_bytes) + } + + pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> WalletSignature { + self.private_key.sign(data) + } + + pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { + WalletPem { + private_key: self.private_key.clone(), + address: Bech32Address::encode_address(hrp, self.address.clone()), + } + } +} diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs index 0f1c437ee1..ea0ae39490 100644 --- a/sdk/core/src/wallet/wallet_pem.rs +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -3,10 +3,9 @@ use std::path::Path; use anyhow::{Result, anyhow}; use multiversx_chain_core::std::Bech32Address; -use crate::{ - crypto::{private_key::PrivateKey, public_key::PublicKey}, - utils::{base64_decode, base64_encode}, -}; +use crate::utils::{base64_decode, base64_encode}; + +use super::{PrivateKey, PublicKey}; const PEM_BEGIN_PREFIX: &str = "-----BEGIN PRIVATE KEY for "; const PEM_END_PREFIX: &str = "-----END PRIVATE KEY for "; diff --git a/sdk/core/src/crypto/wallet_signature.rs b/sdk/core/src/wallet/wallet_signature.rs similarity index 100% rename from sdk/core/src/crypto/wallet_signature.rs rename to sdk/core/src/wallet/wallet_signature.rs diff --git a/sdk/core/src/wallet/wallet_source.rs b/sdk/core/src/wallet/wallet_source.rs new file mode 100644 index 0000000000..8b62d29346 --- /dev/null +++ b/sdk/core/src/wallet/wallet_source.rs @@ -0,0 +1,11 @@ +use multiversx_chain_core::std::Bech32Hrp; + +/// Optional structure that indicates how the [`Wallet`] was created, with additional metadata. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum WalletSource { + Mnemonic, + PrivateKey, + PemFile(Bech32Hrp), + TestWallet(&'static str), + Keystore(Bech32Hrp), +} diff --git a/sdk/core/tests/private_key_test.rs b/sdk/core/tests/private_key_test.rs index 9a135d0ebe..5d594a763e 100644 --- a/sdk/core/tests/private_key_test.rs +++ b/sdk/core/tests/private_key_test.rs @@ -1,4 +1,4 @@ -use multiversx_sdk::crypto::private_key::{PRIVATE_KEY_LENGTH, PrivateKey, SEED_LENGTH}; +use multiversx_sdk::wallet::{PRIVATE_KEY_LENGTH, PrivateKey, SEED_LENGTH}; // Alice's well-known key material (consistent with wallet_pem_test.rs). const ALICE_SEED_HEX: &str = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; diff --git a/sdk/core/tests/public_key_test.rs b/sdk/core/tests/public_key_test.rs index cb5f940a73..0600c4d302 100644 --- a/sdk/core/tests/public_key_test.rs +++ b/sdk/core/tests/public_key_test.rs @@ -1,4 +1,4 @@ -use multiversx_sdk::crypto::public_key::PublicKey; +use multiversx_sdk::wallet::PublicKey; // Alice's known values, consistent with wallet_pem_test.rs constants. const ALICE_PUBLIC_KEY_HEX: &str = diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs index b1f22f1051..93c6b0901b 100644 --- a/sdk/core/tests/wallet_keystore_test.rs +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -1,7 +1,7 @@ use multiversx_chain_core::std::Bech32Hrp; use multiversx_sdk::{ - crypto::{private_key::PrivateKey, public_key::PublicKey}, wallet::{Keystore, KeystoreError, KeystoreRandomness, WalletSource}, + wallet::{PrivateKey, PublicKey}, }; // Well-known Alice private key used across the test suite. diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 04cad40f40..120a72a5ad 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -1,7 +1,7 @@ use bip39::Mnemonic; -use multiversx_sdk::crypto::{private_key::PrivateKey, public_key::PublicKey}; use multiversx_sdk::test_wallets; +use multiversx_sdk::wallet::{PrivateKey, PublicKey}; #[test] fn test_private_key_from_mnemonic() { From 4824049d819f13ef47d452b12ac211987d490be4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 16:19:20 +0300 Subject: [PATCH 052/195] Wallet Mnemonic refactor --- framework/meta/src/cmd/wallet_cmd.rs | 12 ++-- sdk/core/src/wallet.rs | 2 + sdk/core/src/wallet/mnemonic.rs | 90 ++++++++++++++++++++++++++++ sdk/core/src/wallet/private_key.rs | 77 ------------------------ sdk/core/src/wallet/wallet_impl.rs | 15 ++--- sdk/core/tests/wallet_test.rs | 11 ++-- 6 files changed, 111 insertions(+), 96 deletions(-) create mode 100644 sdk/core/src/wallet/mnemonic.rs diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 9e329dc983..835a6763c4 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -3,7 +3,7 @@ use std::{ io::{self, Read, Write}, }; -use bip39::{Language, Mnemonic}; +use bip39::Language; use rand::Rng; use multiversx_sc::types::Address; @@ -15,7 +15,7 @@ use multiversx_sc_snippets::{ wallet::{Keystore, KeystoreRandomness, Wallet}, }, }; -use multiversx_sdk::wallet::{KeystoreError, WalletPem}; +use multiversx_sdk::wallet::{KeystoreError, Mnemonic, WalletPem}; use crate::cli::cli_args_sender::get_keystore_password; use crate::cli::{ @@ -50,7 +50,7 @@ fn convert(convert_args: &WalletConvertArgs) { ("mnemonic", "pem") => match infile { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); - let wallet = Wallet::from_mnemonic_string(mnemonic_str).unwrap(); + let wallet = Wallet::from(Mnemonic::parse(&mnemonic_str).unwrap()); write_resulted_pem(wallet.to_pem(hrp), outfile); } None => { @@ -58,7 +58,7 @@ fn convert(convert_args: &WalletConvertArgs) { "Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); - let wallet = Wallet::from_mnemonic_string(mnemonic_str).unwrap(); + let wallet = Wallet::from(Mnemonic::parse(&mnemonic_str).unwrap()); write_resulted_pem(wallet.to_pem(hrp), outfile); } }, @@ -168,7 +168,7 @@ fn bech32_conversion(bech32_args: &WalletBech32Args) { } pub fn generate_mnemonic() -> Mnemonic { - Mnemonic::generate_in(Language::English, 24).unwrap() + Mnemonic::from(bip39::Mnemonic::generate_in(Language::English, 24).unwrap()) } struct NewWalletInfo { @@ -179,7 +179,7 @@ struct NewWalletInfo { impl NewWalletInfo { fn generate() -> Self { let mnemonic = generate_mnemonic(); - let wallet = Wallet::from_mnemonic_string(mnemonic.to_string()).unwrap(); + let wallet = Wallet::from(mnemonic.clone()); NewWalletInfo { mnemonic, wallet } } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 67ad2d5137..cc3a0fd97b 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,6 +1,7 @@ mod keystore; mod keystore_error; mod keystore_json; +mod mnemonic; mod private_key; mod public_key; mod wallet_impl; @@ -12,6 +13,7 @@ pub use keystore::Keystore; pub use keystore::KeystoreRandomness; pub use keystore_error::KeystoreError; pub use keystore_json::*; +pub use mnemonic::Mnemonic; pub use private_key::{PRIVATE_KEY_LENGTH, PrivateKey, SEED_LENGTH}; pub use public_key::{PUBLIC_KEY_LENGTH, PublicKey}; pub use wallet_impl::Wallet; diff --git a/sdk/core/src/wallet/mnemonic.rs b/sdk/core/src/wallet/mnemonic.rs new file mode 100644 index 0000000000..acedb1eb93 --- /dev/null +++ b/sdk/core/src/wallet/mnemonic.rs @@ -0,0 +1,90 @@ +use std::fmt; + +use anyhow::Result; +use hmac::{Hmac, KeyInit, Mac}; +use sha2::Sha512; + +use super::private_key::PrivateKey; + +const EGLD_COIN_TYPE: u32 = 508; +const HARDENED: u32 = 0x80000000; + +/// A BIP-39 mnemonic used to derive [`PrivateKey`]s via the MultiversX HD path. +/// +/// Wraps [`bip39::Mnemonic`] and provides MultiversX-specific key derivation. +#[derive(Clone)] +pub struct Mnemonic(bip39::Mnemonic); + +impl Mnemonic { + /// Parses a mnemonic phrase. + /// + /// Embedded newlines are stripped before parsing, so file contents can be + /// passed directly without pre-processing. + pub fn parse(s: &str) -> Result { + Ok(Mnemonic(bip39::Mnemonic::parse( + s.replace('\n', "").trim(), + )?)) + } + + /// Derives a [`PrivateKey`] using the MultiversX HD path + /// `m/44'/508'/'/0'/'`. + pub fn to_private_key(&self, account: u32, address_index: u32) -> PrivateKey { + let seed = self.to_bip39_seed(); + self.bip32_derive(&seed, account, address_index) + } + + fn to_bip39_seed(&self) -> [u8; 64] { + self.0.to_seed_normalized("") + } + + fn bip32_derive(&self, seed: &[u8; 64], account: u32, address_index: u32) -> PrivateKey { + let serialized_key_len = 32; + let hardened_child_padding: u8 = 0; + + let mut digest = + Hmac::::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); + digest.update(seed); + let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); + let mut key = intermediary[..serialized_key_len].to_vec(); + let mut chain_code = intermediary[serialized_key_len..].to_vec(); + + for child_idx in [ + 44 | HARDENED, + EGLD_COIN_TYPE | HARDENED, + account | HARDENED, + HARDENED, + address_index | HARDENED, + ] { + let mut buff = [vec![hardened_child_padding], key.clone()].concat(); + buff.push((child_idx >> 24) as u8); + buff.push((child_idx >> 16) as u8); + buff.push((child_idx >> 8) as u8); + buff.push(child_idx as u8); + + digest = + Hmac::::new_from_slice(&chain_code).expect("HMAC can take key of any size"); + digest.update(&buff); + let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); + key = intermediary[..serialized_key_len].to_vec(); + chain_code = intermediary[serialized_key_len..].to_vec(); + } + + let seed_bytes: &[u8; 32] = key + .as_slice() + .try_into() + .expect("BIP32 derived key has unexpected length"); + PrivateKey::from_seed_bytes(seed_bytes) + } +} + +impl From for Mnemonic { + fn from(m: bip39::Mnemonic) -> Self { + Mnemonic(m) + } +} + +impl fmt::Display for Mnemonic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/sdk/core/src/wallet/private_key.rs b/sdk/core/src/wallet/private_key.rs index 0d518749c6..c972cf2605 100644 --- a/sdk/core/src/wallet/private_key.rs +++ b/sdk/core/src/wallet/private_key.rs @@ -1,25 +1,17 @@ use std::fmt::Display; use anyhow::{Result, anyhow}; -use bip39::Mnemonic; -use hmac::{Hmac, KeyInit, Mac}; use multiversx_chain_core::std::crypto::ed25519; -use pbkdf2::pbkdf2; use serde::{ de::{Deserialize, Deserializer}, ser::{Serialize, Serializer}, }; -use sha2::Sha512; -use zeroize::Zeroize; use super::wallet_signature::WalletSignature; pub const PRIVATE_KEY_LENGTH: usize = 64; pub const SEED_LENGTH: usize = 32; -const EGLD_COIN_TYPE: u32 = 508; -const HARDENED: u32 = 0x80000000; - #[derive(Clone, PartialEq, Eq)] pub struct PrivateKey(pub ed25519::Ed25519SigningKey); @@ -99,75 +91,6 @@ impl PrivateKey { PrivateKey::from_bytes(bytes.as_slice()) } - fn seed_from_mnemonic(mnemonic: Mnemonic, password: &str) -> [u8; 64] { - let mut salt = String::with_capacity(8 + password.len()); - salt.push_str("mnemonic"); - salt.push_str(password); - - let mut seed = [0u8; 64]; - - let _ = pbkdf2::>( - mnemonic.to_string().as_bytes(), - salt.as_bytes(), - 2048, - &mut seed, - ); - - salt.zeroize(); - - seed - } - - /// Derives a [`PrivateKey`] from a BIP-39 mnemonic using the MultiversX HD path - /// `m/44'/508'/'/0'/'`. - /// - /// Returns an error if the internal BIP-32 key derivation produces a key of - /// unexpected length (should not happen in practice). - pub fn from_mnemonic( - mnemonic: Mnemonic, - account: u32, - address_index: u32, - ) -> Result { - let seed = Self::seed_from_mnemonic(mnemonic, ""); - - let serialized_key_len = 32; - let hardened_child_padding: u8 = 0; - - let mut digest = - Hmac::::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); - digest.update(&seed); - let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); - let mut key = intermediary[..serialized_key_len].to_vec(); - let mut chain_code = intermediary[serialized_key_len..].to_vec(); - - for child_idx in [ - 44 | HARDENED, - EGLD_COIN_TYPE | HARDENED, - account | HARDENED, - HARDENED, - address_index | HARDENED, - ] { - let mut buff = [vec![hardened_child_padding], key.clone()].concat(); - buff.push((child_idx >> 24) as u8); - buff.push((child_idx >> 16) as u8); - buff.push((child_idx >> 8) as u8); - buff.push(child_idx as u8); - - digest = - Hmac::::new_from_slice(&chain_code).expect("HMAC can take key of any size"); - digest.update(&buff); - let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); - key = intermediary[..serialized_key_len].to_vec(); - chain_code = intermediary[serialized_key_len..].to_vec(); - } - - let seed: &[u8; 32] = key - .as_slice() - .try_into() - .map_err(|_| anyhow!("BIP32 derived key has unexpected length"))?; - Ok(PrivateKey::from_seed_bytes(seed)) - } - /// Returns the full 64-byte keypair as `[seed (32 bytes) || public_key (32 bytes)]`. pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_LENGTH] { self.0.to_keypair_bytes() diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index 5722538ede..e59585e43d 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -2,7 +2,6 @@ use core::str; use std::path::Path; use anyhow::Result; -use bip39::Mnemonic; use multiversx_chain_core::{ std::{Bech32Address, Bech32Hrp, crypto}, types::Address, @@ -11,7 +10,7 @@ use serde_json::json; use crate::{ data::transaction::Transaction, - wallet::{PrivateKey, PublicKey, WalletPem, WalletSignature, WalletSource}, + wallet::{Mnemonic, PrivateKey, PublicKey, WalletPem, WalletSignature, WalletSource}, }; #[derive(Clone, PartialEq, Eq, Debug)] @@ -47,13 +46,15 @@ impl From for Wallet { } } -impl Wallet { - pub fn from_mnemonic_string(mnemonic_str: String) -> Result { - let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', ""))?; - let private_key = PrivateKey::from_mnemonic(mnemonic, 0u32, 0u32)?; - Ok(Self::new(private_key, WalletSource::Mnemonic)) +impl From for Wallet { + /// Derives the wallet at account 0, address index 0 from the mnemonic. + fn from(mnemonic: Mnemonic) -> Self { + let private_key = mnemonic.to_private_key(0, 0); + Self::new(private_key, WalletSource::Mnemonic) } +} +impl Wallet { #[deprecated( since = "0.67.0", note = "Use `PrivateKey::from_hex_str(hex).map(Wallet::from)` instead" diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 120a72a5ad..9291758907 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -1,13 +1,12 @@ -use bip39::Mnemonic; - use multiversx_sdk::test_wallets; -use multiversx_sdk::wallet::{PrivateKey, PublicKey}; +use multiversx_sdk::wallet::Mnemonic; +use multiversx_sdk::wallet::PublicKey; #[test] fn test_private_key_from_mnemonic() { - let mnemonic: Mnemonic = Mnemonic::parse_normalized("acid twice post genre topic observe valid viable gesture fortune funny dawn around blood enemy page update reduce decline van bundle zebra rookie real").unwrap(); + let mnemonic = Mnemonic::parse("acid twice post genre topic observe valid viable gesture fortune funny dawn around blood enemy page update reduce decline van bundle zebra rookie real").unwrap(); - let private_key = PrivateKey::from_mnemonic(mnemonic.clone(), 0, 0).unwrap(); + let private_key = mnemonic.to_private_key(0, 0); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( @@ -23,7 +22,7 @@ fn test_private_key_from_mnemonic() { address.to_bech32_default().bech32 ); - let private_key = PrivateKey::from_mnemonic(mnemonic, 0, 1).unwrap(); + let private_key = mnemonic.to_private_key(0, 1); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( From e7dbb1139fac7390f22b4f8631957a6677fd989d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 16:20:49 +0300 Subject: [PATCH 053/195] cleanup --- sdk/core/src/wallet/private_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/src/wallet/private_key.rs b/sdk/core/src/wallet/private_key.rs index c972cf2605..ff1b40e528 100644 --- a/sdk/core/src/wallet/private_key.rs +++ b/sdk/core/src/wallet/private_key.rs @@ -13,7 +13,7 @@ pub const PRIVATE_KEY_LENGTH: usize = 64; pub const SEED_LENGTH: usize = 32; #[derive(Clone, PartialEq, Eq)] -pub struct PrivateKey(pub ed25519::Ed25519SigningKey); +pub struct PrivateKey(pub(crate) ed25519::Ed25519SigningKey); impl PrivateKey { /// Constructs a [`PrivateKey`] from a 32-byte ed25519 seed. From f18b382f1a6a40de3bca1f08b824a3247362a579 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 17:13:19 +0300 Subject: [PATCH 054/195] Mnemonic parse fix --- sdk/core/src/wallet/mnemonic.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/src/wallet/mnemonic.rs b/sdk/core/src/wallet/mnemonic.rs index acedb1eb93..216db5ee4a 100644 --- a/sdk/core/src/wallet/mnemonic.rs +++ b/sdk/core/src/wallet/mnemonic.rs @@ -18,12 +18,12 @@ pub struct Mnemonic(bip39::Mnemonic); impl Mnemonic { /// Parses a mnemonic phrase. /// - /// Embedded newlines are stripped before parsing, so file contents can be + /// All whitespace sequences (spaces, tabs, `\n`, `\r\n`, etc.) are + /// collapsed to single spaces before parsing, so file contents can be /// passed directly without pre-processing. pub fn parse(s: &str) -> Result { - Ok(Mnemonic(bip39::Mnemonic::parse( - s.replace('\n', "").trim(), - )?)) + let normalized = s.split_whitespace().collect::>().join(" "); + Ok(Mnemonic(bip39::Mnemonic::parse(&normalized)?)) } /// Derives a [`PrivateKey`] using the MultiversX HD path From 66c49704d2d6c16a57c3f78f9dcd215390a8bfd2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 2 Jun 2026 17:29:10 +0300 Subject: [PATCH 055/195] Mnemonic - better error handling --- framework/meta/src/cmd/wallet_cmd.rs | 6 +++--- sdk/core/src/wallet/mnemonic.rs | 27 +++++++++++++++++---------- sdk/core/src/wallet/wallet_impl.rs | 10 ++++++---- sdk/core/tests/wallet_test.rs | 4 ++-- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 835a6763c4..1b2827c7c9 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -50,7 +50,7 @@ fn convert(convert_args: &WalletConvertArgs) { ("mnemonic", "pem") => match infile { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); - let wallet = Wallet::from(Mnemonic::parse(&mnemonic_str).unwrap()); + let wallet = Wallet::try_from(Mnemonic::parse(&mnemonic_str).unwrap()).unwrap(); write_resulted_pem(wallet.to_pem(hrp), outfile); } None => { @@ -58,7 +58,7 @@ fn convert(convert_args: &WalletConvertArgs) { "Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); - let wallet = Wallet::from(Mnemonic::parse(&mnemonic_str).unwrap()); + let wallet = Wallet::try_from(Mnemonic::parse(&mnemonic_str).unwrap()).unwrap(); write_resulted_pem(wallet.to_pem(hrp), outfile); } }, @@ -179,7 +179,7 @@ struct NewWalletInfo { impl NewWalletInfo { fn generate() -> Self { let mnemonic = generate_mnemonic(); - let wallet = Wallet::from(mnemonic.clone()); + let wallet = Wallet::try_from(mnemonic.clone()).unwrap(); NewWalletInfo { mnemonic, wallet } } diff --git a/sdk/core/src/wallet/mnemonic.rs b/sdk/core/src/wallet/mnemonic.rs index 216db5ee4a..580b9d1498 100644 --- a/sdk/core/src/wallet/mnemonic.rs +++ b/sdk/core/src/wallet/mnemonic.rs @@ -1,6 +1,6 @@ use std::fmt; -use anyhow::Result; +use anyhow::{Context, Result}; use hmac::{Hmac, KeyInit, Mac}; use sha2::Sha512; @@ -23,12 +23,14 @@ impl Mnemonic { /// passed directly without pre-processing. pub fn parse(s: &str) -> Result { let normalized = s.split_whitespace().collect::>().join(" "); - Ok(Mnemonic(bip39::Mnemonic::parse(&normalized)?)) + Ok(Mnemonic( + bip39::Mnemonic::parse(&normalized).context("invalid mnemonic phrase")?, + )) } /// Derives a [`PrivateKey`] using the MultiversX HD path /// `m/44'/508'/'/0'/'`. - pub fn to_private_key(&self, account: u32, address_index: u32) -> PrivateKey { + pub fn to_private_key(&self, account: u32, address_index: u32) -> Result { let seed = self.to_bip39_seed(); self.bip32_derive(&seed, account, address_index) } @@ -37,12 +39,17 @@ impl Mnemonic { self.0.to_seed_normalized("") } - fn bip32_derive(&self, seed: &[u8; 64], account: u32, address_index: u32) -> PrivateKey { + fn bip32_derive( + &self, + seed: &[u8; 64], + account: u32, + address_index: u32, + ) -> Result { let serialized_key_len = 32; let hardened_child_padding: u8 = 0; - let mut digest = - Hmac::::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); + let mut digest = Hmac::::new_from_slice(b"ed25519 seed") + .context("failed to initialise root HMAC-SHA512 digest")?; digest.update(seed); let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); let mut key = intermediary[..serialized_key_len].to_vec(); @@ -61,8 +68,8 @@ impl Mnemonic { buff.push((child_idx >> 8) as u8); buff.push(child_idx as u8); - digest = - Hmac::::new_from_slice(&chain_code).expect("HMAC can take key of any size"); + digest = Hmac::::new_from_slice(&chain_code) + .context("failed to initialise child HMAC-SHA512 digest")?; digest.update(&buff); let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); key = intermediary[..serialized_key_len].to_vec(); @@ -72,8 +79,8 @@ impl Mnemonic { let seed_bytes: &[u8; 32] = key .as_slice() .try_into() - .expect("BIP32 derived key has unexpected length"); - PrivateKey::from_seed_bytes(seed_bytes) + .context("BIP32 derived key has unexpected length")?; + Ok(PrivateKey::from_seed_bytes(seed_bytes)) } } diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index e59585e43d..96f5695972 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -46,11 +46,13 @@ impl From for Wallet { } } -impl From for Wallet { +impl TryFrom for Wallet { + type Error = anyhow::Error; + /// Derives the wallet at account 0, address index 0 from the mnemonic. - fn from(mnemonic: Mnemonic) -> Self { - let private_key = mnemonic.to_private_key(0, 0); - Self::new(private_key, WalletSource::Mnemonic) + fn try_from(mnemonic: Mnemonic) -> Result { + let private_key = mnemonic.to_private_key(0, 0)?; + Ok(Self::new(private_key, WalletSource::Mnemonic)) } } diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 9291758907..e51f5f4bad 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -6,7 +6,7 @@ use multiversx_sdk::wallet::PublicKey; fn test_private_key_from_mnemonic() { let mnemonic = Mnemonic::parse("acid twice post genre topic observe valid viable gesture fortune funny dawn around blood enemy page update reduce decline van bundle zebra rookie real").unwrap(); - let private_key = mnemonic.to_private_key(0, 0); + let private_key = mnemonic.to_private_key(0, 0).unwrap(); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( @@ -22,7 +22,7 @@ fn test_private_key_from_mnemonic() { address.to_bech32_default().bech32 ); - let private_key = mnemonic.to_private_key(0, 1); + let private_key = mnemonic.to_private_key(0, 1).unwrap(); let public_key = PublicKey::from(&private_key); let address = public_key.to_address(); assert_eq!( From 2560517b894b4757466df2d076b28dfd241b3aa2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 00:38:22 +0300 Subject: [PATCH 056/195] base64 functions moved to chain core --- Cargo.lock | 3 +-- chain/core/Cargo.toml | 3 ++- chain/core/src/std.rs | 2 ++ chain/core/src/std/base64.rs | 20 +++++++++++++++++++ framework/meta/Cargo.toml | 1 - .../cmd/reproducible_builds/source_pack.rs | 4 ++-- .../cmd/reproducible_builds/source_unpack.rs | 5 ++--- framework/meta/src/cmd/tx/tx_cli_common.rs | 14 +++++++------ framework/meta/src/cmd/tx/tx_cli_new.rs | 18 ++++++++--------- framework/meta/src/cmd/tx/tx_cli_send.rs | 14 +++++++------ framework/meta/src/cmd/tx/tx_cli_sign.rs | 14 +++++++------ .../interactor_scenario/interactor_sc_call.rs | 5 ++--- .../interactor_sc_deploy.rs | 5 ++--- framework/snippets/src/network_response.rs | 15 ++++++++------ sdk/core/Cargo.toml | 1 - sdk/core/src/data/vm.rs | 7 +++++-- .../gateway_chain_simulator_add_keys.rs | 3 ++- sdk/core/src/lib.rs | 1 - sdk/core/src/retrieve_tx_on_network.rs | 6 ++++-- sdk/core/src/utils.rs | 16 +-------------- sdk/core/src/wallet/wallet_pem.rs | 5 +++-- sdk/core/tests/retrieve_tx_on_network_test.rs | 5 ++--- sdk/dapp/src/lib.rs | 2 +- sdk/http/examples/tx_cost.rs | 3 ++- .../src/delegation_sc_interact.rs | 4 ++-- .../src/governance_sc_interact.rs | 2 +- .../src/system_sc_interact.rs | 4 ++-- 27 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 chain/core/src/std/base64.rs diff --git a/Cargo.lock b/Cargo.lock index edbd02b42c..3727384d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3115,6 +3115,7 @@ checksum = "049721bdb4abb82202cf8d9b7981ef0c080615b7cb4ef2de345694a5329f1216" name = "multiversx-chain-core" version = "0.23.0" dependencies = [ + "base64", "bech32", "bitflags 2.11.1", "blake2", @@ -3266,7 +3267,6 @@ name = "multiversx-sc-meta" version = "0.66.1" dependencies = [ "anyhow", - "base64", "bip39", "clap", "colored 3.1.1", @@ -3381,7 +3381,6 @@ version = "0.16.0" dependencies = [ "aes", "anyhow", - "base64", "bip39", "ctr", "hex", diff --git a/chain/core/Cargo.toml b/chain/core/Cargo.toml index 7412c10a92..0ecde3a2c6 100644 --- a/chain/core/Cargo.toml +++ b/chain/core/Cargo.toml @@ -17,10 +17,11 @@ keywords = ["multiversx", "blockchain", "vm", "tools"] categories = ["cryptography::cryptocurrencies", "development-tools::debugging"] [features] -std = ["bech32", "serde", "hex", "blake2", "sha2", "sha3", "ed25519-dalek"] +std = ["base64", "bech32", "serde", "hex", "blake2", "sha2", "sha3", "ed25519-dalek"] [dependencies] bitflags = "2.9" +base64 = { version = "0.22", optional = true } bech32 = { version = "0.11", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } hex = { version = "0.4", optional = true } diff --git a/chain/core/src/std.rs b/chain/core/src/std.rs index 7916ea25ef..a0fd170436 100644 --- a/chain/core/src/std.rs +++ b/chain/core/src/std.rs @@ -1,7 +1,9 @@ +mod base64; mod bech32_address; mod code_hash; pub mod crypto; pub mod new_address; +pub use base64::{base64_decode, base64_encode}; pub use bech32_address::{Bech32Address, Bech32Hrp}; pub use code_hash::{CODE_HASH_LEN, code_hash}; diff --git a/chain/core/src/std/base64.rs b/chain/core/src/std/base64.rs new file mode 100644 index 0000000000..b76b694b26 --- /dev/null +++ b/chain/core/src/std/base64.rs @@ -0,0 +1,20 @@ +use base64::{Engine as _, engine::general_purpose}; + +/// Decodes a base64-encoded byte slice using the standard alphabet. +/// +/// Returns an error if the input contains characters outside the base64 alphabet +/// or has invalid padding. +pub fn base64_decode(to_decode: T) -> Result, base64::DecodeError> +where + T: AsRef<[u8]>, +{ + general_purpose::STANDARD.decode(to_decode) +} + +/// Encodes a byte slice as a base64 string using the standard alphabet with padding. +pub fn base64_encode(to_encode: T) -> String +where + T: AsRef<[u8]>, +{ + general_purpose::STANDARD.encode(to_encode) +} diff --git a/framework/meta/Cargo.toml b/framework/meta/Cargo.toml index b85c410499..18a9343bdf 100644 --- a/framework/meta/Cargo.toml +++ b/framework/meta/Cargo.toml @@ -47,7 +47,6 @@ rand = { version = "0.10", default-features = false } uuid = { version = "1.10", features = ["v4"] } anyhow = "1.0" hex = "0.4.3" -base64 = "0.22" indexmap = { version = "2", features = ["serde"] } # warning: newer versions require Rust 1.88, hence we are not upgrading yet zip = { version = "7.2", features = ["deflate"], default-features = false } diff --git a/framework/meta/src/cmd/reproducible_builds/source_pack.rs b/framework/meta/src/cmd/reproducible_builds/source_pack.rs index 2650d5ff0f..8fc5c2f77d 100644 --- a/framework/meta/src/cmd/reproducible_builds/source_pack.rs +++ b/framework/meta/src/cmd/reproducible_builds/source_pack.rs @@ -1,4 +1,4 @@ -use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use multiversx_chain_core::std::base64_encode; use serde::Serialize; use std::{ collections::HashSet, @@ -165,7 +165,7 @@ fn make_entry( ) -> SourceFileEntry { let rel = pathdiff::diff_paths(file, project_folder).unwrap(); let path_str = rel.to_string_lossy().replace('\\', "/"); - let content = BASE64.encode(fs::read(file).unwrap()); + let content = base64_encode(fs::read(file).unwrap()); SourceFileEntry { path: path_str.clone(), content, diff --git a/framework/meta/src/cmd/reproducible_builds/source_unpack.rs b/framework/meta/src/cmd/reproducible_builds/source_unpack.rs index eb56248355..3a3bad2ed9 100644 --- a/framework/meta/src/cmd/reproducible_builds/source_unpack.rs +++ b/framework/meta/src/cmd/reproducible_builds/source_unpack.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result, bail}; -use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use multiversx_chain_core::std::base64_decode; use std::{ fs, path::{Component, Path, PathBuf}, @@ -67,8 +67,7 @@ pub fn unpack_packed_source( fs::create_dir_all(parent) .with_context(|| format!("Failed to create {}", parent.display()))?; } - let content = BASE64 - .decode(&entry.content) + let content = base64_decode(&entry.content) .with_context(|| format!("Failed to decode entry '{}'", entry.path))?; fs::write(&file_path, content) .with_context(|| format!("Failed to write {}", file_path.display()))?; diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 0a64552158..adf8c9405a 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -1,6 +1,7 @@ use std::fs; use anyhow::{Context, Result, anyhow}; +use multiversx_chain_core::std::base64_decode; use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::{ hex, @@ -10,7 +11,6 @@ use multiversx_sc_snippets::{ }, sdk::{ data::transaction::{ApiTransactionResult, Transaction}, - utils::base64_decode, wallet::Wallet, }, }; @@ -157,11 +157,13 @@ pub async fn sign_and_dispatch( tx.chain_id = chain_id.clone(); } - let decoded_data = tx - .data - .as_ref() - .map(|d| String::from_utf8_lossy(&base64_decode(d)).into_owned()) - .unwrap_or_default(); + let decoded_data = match &tx.data { + None => String::new(), + Some(d) => { + let bytes = base64_decode(d)?; + String::from_utf8_lossy(&bytes).into_owned() + } + }; let sig = wallet.sign_tx(&tx); tx.signature = Some(sig); diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index fbc0379cee..966d756893 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -1,10 +1,8 @@ use std::fs; use anyhow::{Context, Result}; -use multiversx_sc_snippets::{ - imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}, - sdk::utils::{base64_decode, base64_encode}, -}; +use multiversx_chain_core::std::{base64_decode, base64_encode}; +use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; use crate::cmd::tx::tx_cli_common::load_wallet; @@ -55,11 +53,13 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { } // Decode the data field for the human-readable output. - let decoded_data = tx - .data - .as_ref() - .map(|d| String::from_utf8_lossy(&base64_decode(d)).into_owned()) - .unwrap_or_default(); + let decoded_data = match &tx.data { + None => String::new(), + Some(d) => { + let bytes = base64_decode(d)?; + String::from_utf8_lossy(&bytes).into_owned() + } + }; tx.nonce = nonce; if let Some(gas_price) = args.tx.gas_price { tx.gas_price = gas_price; diff --git a/framework/meta/src/cmd/tx/tx_cli_send.rs b/framework/meta/src/cmd/tx/tx_cli_send.rs index 432b75de87..d21226c870 100644 --- a/framework/meta/src/cmd/tx/tx_cli_send.rs +++ b/framework/meta/src/cmd/tx/tx_cli_send.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use multiversx_sc_snippets::sdk::utils::base64_decode; +use multiversx_chain_core::std::base64_decode; use super::{ output::TxOutputFile, @@ -17,11 +17,13 @@ pub async fn tx_send(args: &SendArgs) { async fn tx_send_inner(args: &SendArgs) -> Result<()> { let tx = load_transaction_from_file(&args.infile)?; - let decoded_data = tx - .data - .as_ref() - .map(|d| String::from_utf8_lossy(&base64_decode(d)).into_owned()) - .unwrap_or_default(); + let decoded_data = match &tx.data { + None => String::new(), + Some(d) => { + let bytes = base64_decode(d)?; + String::from_utf8_lossy(&bytes).into_owned() + } + }; let output = TxOutputFile { emitted_transaction: tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 827d9dd1e3..13cff560ee 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -1,5 +1,5 @@ use anyhow::{Result, anyhow}; -use multiversx_sc_snippets::sdk::utils::base64_decode; +use multiversx_chain_core::std::base64_decode; use super::{ output::TxOutputFile, @@ -35,11 +35,13 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { let sig = wallet.sign_tx(&tx); tx.signature = Some(sig); - let decoded_data = tx - .data - .as_ref() - .map(|d| String::from_utf8_lossy(&base64_decode(d)).into_owned()) - .unwrap_or_default(); + let decoded_data = match &tx.data { + None => String::new(), + Some(d) => { + let bytes = base64_decode(d)?; + String::from_utf8_lossy(&bytes).into_owned() + } + }; let output = TxOutputFile { emitted_transaction: tx, diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index 4b6d2758b2..d3aea037e8 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -7,9 +7,8 @@ use multiversx_sc_scenario::{ scenario::ScenarioRunner, scenario_model::{ScCallStep, SetStateStep, TxCall}, }; -use multiversx_sdk::{ - data::transaction::Transaction, gateway::SimulateTxRequest, utils::base64_encode, -}; +use multiversx_sdk::chain_core::std::base64_encode; +use multiversx_sdk::{data::transaction::Transaction, gateway::SimulateTxRequest}; use multiversx_sdk::{ gateway::{GatewayAsyncService, SendTxRequest}, retrieve_tx_on_network, diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index c5060acaa7..ba2ea4f838 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -7,9 +7,8 @@ use multiversx_sc_scenario::{ mandos_system::ScenarioRunner, scenario_model::{ScDeployStep, SetStateStep}, }; -use multiversx_sdk::{ - data::transaction::Transaction, gateway::SimulateTxRequest, utils::base64_encode, -}; +use multiversx_sdk::chain_core::std::base64_encode; +use multiversx_sdk::{data::transaction::Transaction, gateway::SimulateTxRequest}; use multiversx_sdk::{ gateway::{GatewayAsyncService, SendTxRequest}, retrieve_tx_on_network, diff --git a/framework/snippets/src/network_response.rs b/framework/snippets/src/network_response.rs index 773caa5ca8..ed20b79ffb 100644 --- a/framework/snippets/src/network_response.rs +++ b/framework/snippets/src/network_response.rs @@ -1,6 +1,6 @@ use crate::sdk::{ + chain_core::std::base64_decode, data::transaction::{ApiLogs, ApiSmartContractResult, ApiTransactionResult, Events}, - utils::base64_decode, }; use multiversx_sc_scenario::{ imports::{Address, ESDTSystemSCAddress, ReturnCode}, @@ -29,9 +29,11 @@ pub fn parse_tx_response(tx: ApiTransactionResult, return_code: ReturnCode) -> T fn process_signal_error(tx: &ApiTransactionResult, return_code: ReturnCode) -> TxResponseStatus { if let Some(event) = find_log(tx, LOG_IDENTIFIER_SIGNAL_ERROR) { if event.topics.len() >= 2 { - let error_message = String::from_utf8(base64_decode(&event.topics[1])).expect( - "Failed to decode base64-encoded error message from transaction event topic", - ); + let error_message = String::from_utf8( + base64_decode(&event.topics[1]) + .expect("Failed to base64-decode error message topic"), + ) + .expect("Failed to decode base64-encoded error message from transaction event topic"); return TxResponseStatus::new(return_code, &error_message); } } @@ -140,7 +142,8 @@ fn process_new_issued_token_identifier(tx: &ApiTransactionResult) -> Option) -> Option Vec> { let mut out = Vec::new(); event.data.for_each(|data_member| { - let decoded_data = String::from_utf8(base64_decode(data_member)).unwrap(); + let decoded_data = String::from_utf8(base64_decode(data_member).unwrap()).unwrap(); if decoded_data.starts_with('@') { let out_content = decode_scr_data_or_panic(decoded_data.as_str()); diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 14b7b8367b..43ca05dcd7 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -25,7 +25,6 @@ sha2 = "0.11" sha3 = "0.12" hmac = "0.13" hex = "0.4" -base64 = "0.22" pbkdf2 = { version = "0.13", default-features = false } zeroize = "1.4" itertools = "0.14" diff --git a/sdk/core/src/data/vm.rs b/sdk/core/src/data/vm.rs index 74a6943747..ce0f17e6b1 100644 --- a/sdk/core/src/data/vm.rs +++ b/sdk/core/src/data/vm.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::collections::HashMap; -use crate::utils::base64_decode; +use multiversx_chain_core::std::base64_decode; #[derive(Serialize_repr, Deserialize_repr, Debug, Clone)] #[repr(u8)] @@ -107,7 +107,10 @@ impl VMOutputApi { } pub fn return_data_base64_decode(&self) -> Vec> { - self.return_data().iter().map(base64_decode).collect() + self.return_data() + .iter() + .map(|d| base64_decode(d).expect("invalid base64 in return data")) + .collect() } pub fn is_ok(&self) -> bool { diff --git a/sdk/core/src/gateway/gateway_chain_simulator_add_keys.rs b/sdk/core/src/gateway/gateway_chain_simulator_add_keys.rs index 480c9a3f20..f45ab7fa73 100644 --- a/sdk/core/src/gateway/gateway_chain_simulator_add_keys.rs +++ b/sdk/core/src/gateway/gateway_chain_simulator_add_keys.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use anyhow::anyhow; -use crate::{gateway::ADD_KEYS, utils::base64_encode}; +use crate::gateway::ADD_KEYS; +use multiversx_chain_core::std::base64_encode; use super::{ GatewayRequest, GatewayRequestType, gateway_chain_simulator_blocks::GenerateBlocksResponse, diff --git a/sdk/core/src/lib.rs b/sdk/core/src/lib.rs index 9ca02c071e..40402370bd 100644 --- a/sdk/core/src/lib.rs +++ b/sdk/core/src/lib.rs @@ -7,7 +7,6 @@ pub mod data; pub mod gateway; pub mod retrieve_tx_on_network; pub mod test_wallets; -pub mod utils; pub mod validator; pub mod wallet; diff --git a/sdk/core/src/retrieve_tx_on_network.rs b/sdk/core/src/retrieve_tx_on_network.rs index 25bd2e6abb..66c754930d 100644 --- a/sdk/core/src/retrieve_tx_on_network.rs +++ b/sdk/core/src/retrieve_tx_on_network.rs @@ -1,11 +1,13 @@ use crate::{ data::transaction::{ApiLogs, ApiTransactionResult, Events, LogData}, gateway::{GetTxInfo, GetTxProcessStatus}, - utils::base64_encode, }; use anyhow::{Context, Result}; use log::info; -use multiversx_chain_core::{std::Bech32Address, types::ReturnCode}; +use multiversx_chain_core::{ + std::{Bech32Address, base64_encode}, + types::ReturnCode, +}; use crate::gateway::GatewayAsyncService; diff --git a/sdk/core/src/utils.rs b/sdk/core/src/utils.rs index d3393c8210..b757df150b 100644 --- a/sdk/core/src/utils.rs +++ b/sdk/core/src/utils.rs @@ -1,15 +1 @@ -use base64::{Engine as _, engine::general_purpose}; - -pub fn base64_decode(to_decode: T) -> Vec -where - T: AsRef<[u8]>, -{ - general_purpose::STANDARD.decode(to_decode).unwrap() -} - -pub fn base64_encode(to_encode: T) -> String -where - T: AsRef<[u8]>, -{ - general_purpose::STANDARD.encode(to_encode) -} +pub use multiversx_chain_core::std::{base64_decode, base64_encode}; diff --git a/sdk/core/src/wallet/wallet_pem.rs b/sdk/core/src/wallet/wallet_pem.rs index ea0ae39490..9d1ad1bddf 100644 --- a/sdk/core/src/wallet/wallet_pem.rs +++ b/sdk/core/src/wallet/wallet_pem.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::{Result, anyhow}; use multiversx_chain_core::std::Bech32Address; -use crate::utils::{base64_decode, base64_encode}; +use multiversx_chain_core::std::{base64_decode, base64_encode}; use super::{PrivateKey, PublicKey}; @@ -41,7 +41,8 @@ impl WalletPem { .take_while(|line| !line.starts_with(PEM_END_PREFIX)) .collect(); - let decoded = base64_decode(b64_body); + let decoded = + base64_decode(b64_body).map_err(|e| anyhow!("invalid base64 in PEM body: {e}"))?; let private_key_bytes = &decoded[..decoded.len() / 2]; let private_key_str = std::str::from_utf8(private_key_bytes) .map_err(|e| anyhow!("invalid UTF-8 in private key: {e}"))?; diff --git a/sdk/core/tests/retrieve_tx_on_network_test.rs b/sdk/core/tests/retrieve_tx_on_network_test.rs index bac74f6d5d..ee9c45e975 100644 --- a/sdk/core/tests/retrieve_tx_on_network_test.rs +++ b/sdk/core/tests/retrieve_tx_on_network_test.rs @@ -1,11 +1,10 @@ -use multiversx_chain_core::types::ReturnCode; +use multiversx_chain_core::{std::base64_decode, types::ReturnCode}; use multiversx_sdk::{ data::transaction::{ApiTransactionResult, GetTransactionResponse}, retrieve_tx_on_network::{ extract_message_from_string_reason, find_code_and_message, parse_reason, replace_with_error_message, }, - utils::base64_decode, }; #[test] @@ -869,7 +868,7 @@ fn replace_logs_reason_sc_panic_test() { serde_json::from_str::(tx_str).unwrap(); let expected_tx: ApiTransactionResult = serde_json::from_str::(expected_tx_str).unwrap(); - replace_with_error_message(&mut tx, &String::from_utf8(base64_decode("c3RvcmFnZSBkZWNvZGUgZXJyb3IgKGtleTogcG9vbENvbnRyYWN0AAAAAAAAAe+/vSk6IGlucHV0IHRvbyBzaG9ydA==")).unwrap()); + replace_with_error_message(&mut tx, &String::from_utf8(base64_decode("c3RvcmFnZSBkZWNvZGUgZXJyb3IgKGtleTogcG9vbENvbnRyYWN0AAAAAAAAAe+/vSk6IGlucHV0IHRvbyBzaG9ydA==").unwrap()).unwrap()); assert_eq!( expected_tx.logs.unwrap().events[0].topics, tx.logs.unwrap().events[0].topics diff --git a/sdk/dapp/src/lib.rs b/sdk/dapp/src/lib.rs index 4ca9648fd7..de17f4742e 100644 --- a/sdk/dapp/src/lib.rs +++ b/sdk/dapp/src/lib.rs @@ -3,4 +3,4 @@ pub use gateway_dapp_proxy::GatewayDappProxy; pub use multiversx_sdk as core; -pub use multiversx_sdk::{data, utils, wallet}; +pub use multiversx_sdk::{data, wallet}; diff --git a/sdk/http/examples/tx_cost.rs b/sdk/http/examples/tx_cost.rs index dfdd81b19d..5501650f07 100644 --- a/sdk/http/examples/tx_cost.rs +++ b/sdk/http/examples/tx_cost.rs @@ -1,5 +1,6 @@ use multiversx_sdk::{ - chain_core::std::Bech32Address, data::transaction::Transaction, utils::base64_encode, + chain_core::std::{Bech32Address, base64_encode}, + data::transaction::Transaction, }; use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; diff --git a/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs b/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs index a810801c15..32d0feee21 100644 --- a/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs +++ b/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs @@ -10,7 +10,7 @@ use delegation_sc_interact_state::State; use multiversx_sc_snippets::{ imports::*, - sdk::{gateway::SetStateAccount, utils::base64_decode}, + sdk::{chain_core::std::base64_decode, gateway::SetStateAccount}, }; pub async fn delegation_sc_interact_cli() { @@ -197,7 +197,7 @@ impl DelegateCallsInteract { .find(|log| log.endpoint == "SCDeploy") .expect("No SCDeploy log found"); - let decode_deploy_address = base64_decode(deploy_log.topics[0].clone()); + let decode_deploy_address = base64_decode(deploy_log.topics[0].clone()).unwrap(); let deploy_address = Bech32Address::from(Address::from_slice(&decode_deploy_address)); self.state.set_delegation_address(deploy_address); diff --git a/tools/interactor-governance-func-calls/src/governance_sc_interact.rs b/tools/interactor-governance-func-calls/src/governance_sc_interact.rs index 57e81a5a0a..df33685b8e 100644 --- a/tools/interactor-governance-func-calls/src/governance_sc_interact.rs +++ b/tools/interactor-governance-func-calls/src/governance_sc_interact.rs @@ -8,7 +8,7 @@ use governance_sc_interact_state::State; use multiversx_sc_snippets::{ imports::*, - sdk::{gateway::SetStateAccount, utils::base64_decode}, + sdk::{chain_core::std::base64_decode, gateway::SetStateAccount}, }; pub async fn governance_sc_interact_cli() { diff --git a/tools/interactor-system-func-calls/src/system_sc_interact.rs b/tools/interactor-system-func-calls/src/system_sc_interact.rs index e81f1796b2..0ade6a184e 100644 --- a/tools/interactor-system-func-calls/src/system_sc_interact.rs +++ b/tools/interactor-system-func-calls/src/system_sc_interact.rs @@ -11,7 +11,7 @@ use system_sc_interact_state::State; use multiversx_sc_snippets::{ imports::*, - sdk::{data::esdt::EsdtBalance, utils::base64_decode}, + sdk::{chain_core::std::base64_decode, data::esdt::EsdtBalance}, }; pub async fn system_sc_interact_cli() { @@ -1029,7 +1029,7 @@ impl SysFuncCallsInteract { assert_eq!(expected_uris.len(), uris.len()); for (index, uri) in uris.iter().enumerate() { - let uri_string = String::from_utf8(base64_decode(uri)).unwrap(); + let uri_string = String::from_utf8(base64_decode(uri).unwrap()).unwrap(); assert_eq!(expected_uris[index], uri_string); } } From 2c22fac753d58311b6d39435671b8fa45c9b8524 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 01:01:41 +0300 Subject: [PATCH 057/195] decode fix --- framework/snippets/src/network_response.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/snippets/src/network_response.rs b/framework/snippets/src/network_response.rs index ed20b79ffb..739a1d58f1 100644 --- a/framework/snippets/src/network_response.rs +++ b/framework/snippets/src/network_response.rs @@ -143,7 +143,8 @@ fn process_new_issued_token_identifier(tx: &ApiTransactionResult) -> Option Date: Wed, 3 Jun 2026 02:29:22 +0300 Subject: [PATCH 058/195] interactor - standard connection & wallet configs --- Cargo.lock | 1 + .../examples/adder/interactor/config.toml | 18 ++++ .../adder/interactor/src/basic_interactor.rs | 26 +++--- .../interactor/src/basic_interactor_config.rs | 31 ++----- .../tests/basic_interactor_cs_test.rs | 4 +- framework/snippets/Cargo.toml | 1 + framework/snippets/src/config.rs | 5 ++ .../snippets/src/config/connection_config.rs | 33 +++++++ .../snippets/src/config/wallet_config.rs | 86 +++++++++++++++++++ framework/snippets/src/lib.rs | 3 + sdk/core/src/test_wallets.rs | 10 +++ sdk/core/src/wallet/wallet_impl.rs | 10 +++ 12 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 framework/snippets/src/config.rs create mode 100644 framework/snippets/src/config/connection_config.rs create mode 100644 framework/snippets/src/config/wallet_config.rs diff --git a/Cargo.lock b/Cargo.lock index 3727384d80..58ae62999d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3364,6 +3364,7 @@ dependencies = [ "multiversx-sdk", "multiversx-sdk-dapp", "multiversx-sdk-http", + "serde", "serde_json", "tokio", ] diff --git a/contracts/examples/adder/interactor/config.toml b/contracts/examples/adder/interactor/config.toml index 97acd5a5c6..42bc79559f 100644 --- a/contracts/examples/adder/interactor/config.toml +++ b/contracts/examples/adder/interactor/config.toml @@ -1,7 +1,25 @@ +[connection] # chain_type = 'simulator' # gateway_uri = 'http://localhost:8085' chain_type = 'real' gateway_uri = 'https://devnet-gateway.multiversx.com' + +# Owner wallet - used for deploying and upgrading the contract. +# Provide exactly one of: test_wallet, pem, or keyfile. +[owner] +test_wallet = 'mike' +# pem = 'path/to/owner.pem' +# keyfile = 'path/to/owner.json' +# keystore_password = 'password' + +# User wallet - used for calling contract endpoints. +# Provide exactly one of: test_wallet, pem, or keyfile. +# Can be the same as the owner wallet if desired. +[wallet] +test_wallet = 'ivan' +# pem = 'path/to/wallet.pem' +# keyfile = 'path/to/wallet.json' +# keystore_password = 'password' diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index e587e18bce..e12b917198 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -24,7 +24,7 @@ pub async fn adder_cli() { basic_interact.deploy().await; } Some(basic_interactor_cli::InteractCliCommand::Upgrade(args)) => { - let owner_address = basic_interact.adder_owner_address.clone(); + let owner_address = basic_interact.config.owner.address(); basic_interact .upgrade(args.value, &owner_address, None) .await @@ -42,27 +42,29 @@ pub async fn adder_cli() { pub struct BasicInteractor { pub interactor: Interactor, - pub adder_owner_address: Bech32Address, - pub wallet_address: Bech32Address, + pub config: Config, pub state: State, } impl BasicInteractor { pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) + let mut interactor = Interactor::new(config.connection.gateway_uri()) .await - .use_chain_simulator(config.use_chain_simulator()); + .use_chain_simulator(config.connection.use_chain_simulator()); interactor.set_current_dir_from_workspace("contracts/examples/adder/interactor"); - let adder_owner_address = interactor.register_wallet(test_wallets::mike()).await; - let wallet_address = interactor.register_wallet(test_wallets::ivan()).await; + interactor + .register_wallet(config.owner.wallet().clone()) + .await; + interactor + .register_wallet(config.wallet.wallet().clone()) + .await; interactor.generate_blocks(30u64).await.unwrap(); BasicInteractor { interactor, - adder_owner_address: adder_owner_address.into(), - wallet_address: wallet_address.into(), + config, state: State::load_state(), } } @@ -75,11 +77,12 @@ impl BasicInteractor { } pub async fn deploy(&mut self) { + let owner_address = self.config.owner.address(); let new_address = self .interactor .tx() .id("interactor deploy") - .from(&self.adder_owner_address.clone()) + .from(&owner_address) .gas(100_000_000) .typed(adder_proxy::AdderProxy) .init(0u64) @@ -119,10 +122,11 @@ impl BasicInteractor { } pub async fn add(&mut self, value: u32) { + let wallet_address = self.config.wallet.address(); self.interactor .tx() .id("interactor add") - .from(&self.wallet_address) + .from(&wallet_address) .to(self.state.current_adder_address()) .gas(6_000_000u64) .typed(adder_proxy::AdderProxy) diff --git a/contracts/examples/adder/interactor/src/basic_interactor_config.rs b/contracts/examples/adder/interactor/src/basic_interactor_config.rs index bd19da629d..57d408b306 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor_config.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor_config.rs @@ -1,21 +1,16 @@ +use multiversx_sc_snippets::{ConnectionConfig, WalletConfig}; use serde::Deserialize; use std::io::Read; /// Config file const CONFIG_FILE: &str = "config.toml"; -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - /// Adder Interact configuration #[derive(Debug, Deserialize)] pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, + pub connection: ConnectionConfig, + pub owner: WalletConfig, + pub wallet: WalletConfig, } impl Config { @@ -29,21 +24,9 @@ impl Config { pub fn chain_simulator_config() -> Self { Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri - } - - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, + connection: ConnectionConfig::chain_simulator(), + owner: WalletConfig::from_test_wallet("mike"), + wallet: WalletConfig::from_test_wallet("ivan"), } } } diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index f7b7053107..ad78a95b76 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -23,7 +23,7 @@ async fn simulator_upgrade_test() { assert_eq!(sum, 1u32.into()); basic_interact - .upgrade(7u32, &basic_interact.adder_owner_address.clone(), None) + .upgrade(7u32, &basic_interact.config.owner.address(), None) .await; basic_interact.generate_blocks(2).await; @@ -36,7 +36,7 @@ async fn simulator_upgrade_test() { basic_interact .upgrade( 10u32, - &basic_interact.wallet_address.clone(), + &basic_interact.config.wallet.address(), Some("upgrade is allowed only for owner"), ) .await; diff --git a/framework/snippets/Cargo.toml b/framework/snippets/Cargo.toml index 07686f7af3..d9601985fd 100644 --- a/framework/snippets/Cargo.toml +++ b/framework/snippets/Cargo.toml @@ -24,6 +24,7 @@ log = "0.4" env_logger = "0.11" futures = "0.3" anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" colored = "3.0" diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs new file mode 100644 index 0000000000..8edd130d08 --- /dev/null +++ b/framework/snippets/src/config.rs @@ -0,0 +1,5 @@ +pub mod connection_config; +pub mod wallet_config; + +pub use connection_config::*; +pub use wallet_config::*; diff --git a/framework/snippets/src/config/connection_config.rs b/framework/snippets/src/config/connection_config.rs new file mode 100644 index 0000000000..135dfcdac3 --- /dev/null +++ b/framework/snippets/src/config/connection_config.rs @@ -0,0 +1,33 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChainType { + Real, + Simulator, +} + +/// Configuration for connecting to the MultiversX network. +#[derive(Debug, Deserialize)] +pub struct ConnectionConfig { + pub gateway_uri: String, + pub chain_type: ChainType, +} + +impl ConnectionConfig { + /// Creates a default chain-simulator connection config pointing to localhost. + pub fn chain_simulator() -> Self { + ConnectionConfig { + gateway_uri: "http://localhost:8085".to_owned(), + chain_type: ChainType::Simulator, + } + } + + pub fn gateway_uri(&self) -> &str { + &self.gateway_uri + } + + pub fn use_chain_simulator(&self) -> bool { + matches!(self.chain_type, ChainType::Simulator) + } +} diff --git a/framework/snippets/src/config/wallet_config.rs b/framework/snippets/src/config/wallet_config.rs new file mode 100644 index 0000000000..f065b5205f --- /dev/null +++ b/framework/snippets/src/config/wallet_config.rs @@ -0,0 +1,86 @@ +use multiversx_sc_scenario::imports::Bech32Address; +use serde::Deserialize; +use std::{path::PathBuf, sync::OnceLock}; + +use crate::sdk::{ + test_wallets, + wallet::{Keystore, Wallet}, +}; + +/// Wallet configuration embeddable in a TOML/JSON config file. +/// Mirrors `SenderArgs` from the CLI but uses `serde` instead of `clap`. +#[derive(Debug, Deserialize)] +pub struct WalletConfig { + /// Name of a built-in test wallet (e.g. "alice", "bob", "mike"). + /// See `multiversx_sdk::test_wallets::valid_names()` for the full list. + pub test_wallet: Option, + + /// Path to a PEM wallet file. + pub pem: Option, + + /// Path to a JSON keystore wallet file. + pub keyfile: Option, + + /// Keystore password (plain text). Required when `keyfile` is set. + pub keystore_password: Option, + + #[serde(skip)] + cache: OnceLock, +} + +impl WalletConfig { + /// Creates a `WalletConfig` from a built-in test wallet name. + pub fn from_test_wallet(name: impl Into) -> Self { + WalletConfig { + test_wallet: Some(name.into()), + pem: None, + keyfile: None, + keystore_password: None, + cache: OnceLock::new(), + } + } + + /// Creates a `WalletConfig` from a PEM file path. + pub fn from_pem(path: impl Into) -> Self { + WalletConfig { + test_wallet: None, + pem: Some(path.into()), + keyfile: None, + keystore_password: None, + cache: OnceLock::new(), + } + } + + /// Returns the wallet, loading and caching it on first call. + /// + /// Priority: `test_wallet` > `pem` > `keyfile`. + /// Panics if none of the sources are set, or if loading fails. + pub fn wallet(&self) -> &Wallet { + self.cache.get_or_init(|| self.load_wallet()) + } + + /// Returns the on-chain address derived from this wallet config. + pub fn address(&self) -> Bech32Address { + self.wallet().to_bech32() + } + + fn load_wallet(&self) -> Wallet { + if let Some(name) = &self.test_wallet { + test_wallets::by_name(name.as_str()) + .unwrap_or_else(|| panic!("unknown test wallet name: '{name}'")) + } else if let Some(pem) = &self.pem { + Wallet::from_pem_file(pem).expect("failed to load PEM wallet") + } else if let Some(keyfile) = &self.keyfile { + let password = self + .keystore_password + .as_deref() + .expect("keystore_password is required when using keyfile"); + let keystore = Keystore::from_file(keyfile).expect("failed to load keystore file"); + keystore + .decrypt_wallet(password) + .expect("failed to decrypt wallet") + } else { + panic!("WalletConfig requires one of: `test_wallet`, `pem`, or `keyfile`") + } + } +} diff --git a/framework/snippets/src/lib.rs b/framework/snippets/src/lib.rs index 91e4f6e64c..cd0a343cdd 100644 --- a/framework/snippets/src/lib.rs +++ b/framework/snippets/src/lib.rs @@ -4,10 +4,13 @@ #![allow(clippy::manual_is_multiple_of)] pub mod account_tool; +pub mod config; mod interactor; mod multi; pub mod network_response; +pub use config::*; + pub use env_logger; pub use hex; pub use interactor::*; diff --git a/sdk/core/src/test_wallets.rs b/sdk/core/src/test_wallets.rs index b0a8c25f4b..be3baa20fa 100644 --- a/sdk/core/src/test_wallets.rs +++ b/sdk/core/src/test_wallets.rs @@ -139,6 +139,16 @@ pub fn for_shard(shard_id: ShardId) -> Wallet { } } +/// Returns the wallet for the named test wallet, or `None` if the name is unknown. +/// +/// For the list of valid names see [`valid_names()`]. +pub fn by_name(name: &str) -> Option { + WALLETS + .iter() + .find(|(n, _)| *n == name) + .map(|(static_name, pem)| Wallet::new_test_wallet(static_name, pem)) +} + pub fn valid_names() -> Vec<&'static str> { WALLETS.iter().map(|(n, _)| *n).collect() } diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index 96f5695972..b65ffceb0c 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -90,6 +90,16 @@ impl Wallet { self.address.clone() } + /// Returns the address as a [`Bech32Address`], using the HRP from the wallet source + /// (`PemFile` or `Keystore`) when available, and the default HRP (`"erd"`) otherwise. + pub fn to_bech32(&self) -> Bech32Address { + let hrp = match &self.source { + WalletSource::PemFile(hrp) | WalletSource::Keystore(hrp) => *hrp, + _ => Bech32Hrp::default(), + }; + Bech32Address::encode_address(hrp, self.address.clone()) + } + pub fn private_key_hex(&self) -> String { self.private_key.to_seed_hex() } From 8071dcfaec99c795b9f495581f2ca7b6a55c43e1 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 02:44:51 +0300 Subject: [PATCH 059/195] adder interactor - path to contract in config --- contracts/examples/adder/interactor/config.toml | 1 + contracts/examples/adder/interactor/src/basic_interactor.rs | 6 ++---- .../adder/interactor/src/basic_interactor_config.rs | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/examples/adder/interactor/config.toml b/contracts/examples/adder/interactor/config.toml index 42bc79559f..e9658c3e91 100644 --- a/contracts/examples/adder/interactor/config.toml +++ b/contracts/examples/adder/interactor/config.toml @@ -1,3 +1,4 @@ +contract_path = '../output/adder.mxsc.json' [connection] # chain_type = 'simulator' diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index e12b917198..36d7f1417a 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -9,8 +9,6 @@ use clap::Parser; use multiversx_sc_snippets::imports::*; -const ADDER_CODE_PATH: MxscPath = MxscPath::new("../output/adder.mxsc.json"); - pub async fn adder_cli() { env_logger::init(); @@ -86,7 +84,7 @@ impl BasicInteractor { .gas(100_000_000) .typed(adder_proxy::AdderProxy) .init(0u64) - .code(ADDER_CODE_PATH) + .code(MxscPath::new(&self.config.contract_path)) .returns(ReturnsNewBech32Address) .run() .await; @@ -104,7 +102,7 @@ impl BasicInteractor { .gas(6_000_000) .typed(adder_proxy::AdderProxy) .upgrade(new_value) - .code(ADDER_CODE_PATH) + .code(MxscPath::new(&self.config.contract_path)) .code_metadata(CodeMetadata::UPGRADEABLE) .returns(ReturnsHandledOrError::new()) .run() diff --git a/contracts/examples/adder/interactor/src/basic_interactor_config.rs b/contracts/examples/adder/interactor/src/basic_interactor_config.rs index 57d408b306..21619293bb 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor_config.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor_config.rs @@ -8,6 +8,7 @@ const CONFIG_FILE: &str = "config.toml"; /// Adder Interact configuration #[derive(Debug, Deserialize)] pub struct Config { + pub contract_path: String, pub connection: ConnectionConfig, pub owner: WalletConfig, pub wallet: WalletConfig, @@ -24,6 +25,7 @@ impl Config { pub fn chain_simulator_config() -> Self { Config { + contract_path: "../output/adder.mxsc.json".to_owned(), connection: ConnectionConfig::chain_simulator(), owner: WalletConfig::from_test_wallet("mike"), wallet: WalletConfig::from_test_wallet("ivan"), From 442e545d43404b99319eed0c0cbb48d2ad2c1f84 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 04:54:55 +0300 Subject: [PATCH 060/195] interactor - compact builder --- Cargo.lock | 1 + .../adder/interactor/src/basic_interactor.rs | 29 ++-- .../interactor/src/basic_interactor_config.rs | 27 ++-- .../interactor/src/basic_interactor_state.rs | 35 +---- .../tests/basic_interactor_cs_test.rs | 38 ++++-- framework/snippets/Cargo.toml | 1 + framework/snippets/src/config.rs | 8 +- framework/snippets/src/config/auto_save.rs | 81 ++++++++++++ .../snippets/src/config/interactor_config.rs | 21 +++ .../snippets/src/http_interactor_builder.rs | 125 ++++++++++++++++++ framework/snippets/src/imports.rs | 3 +- .../src/interactor/interactor_base.rs | 21 +++ framework/snippets/src/lib.rs | 6 + 13 files changed, 312 insertions(+), 84 deletions(-) create mode 100644 framework/snippets/src/config/auto_save.rs create mode 100644 framework/snippets/src/config/interactor_config.rs create mode 100644 framework/snippets/src/http_interactor_builder.rs diff --git a/Cargo.lock b/Cargo.lock index 58ae62999d..0d9857a78b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3367,6 +3367,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "toml", ] [[package]] diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index 36d7f1417a..8e77ac70fe 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -12,9 +12,7 @@ use multiversx_sc_snippets::imports::*; pub async fn adder_cli() { env_logger::init(); - let config = Config::load_config(); - - let mut basic_interact = BasicInteractor::new(config).await; + let mut basic_interact = BasicInteractor::new().await; let cli = basic_interactor_cli::InteractCli::parse(); match &cli.command { @@ -41,29 +39,20 @@ pub async fn adder_cli() { pub struct BasicInteractor { pub interactor: Interactor, pub config: Config, - pub state: State, + pub state: AutoSave, } impl BasicInteractor { - pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.connection.gateway_uri()) - .await - .use_chain_simulator(config.connection.use_chain_simulator()); - interactor.set_current_dir_from_workspace("contracts/examples/adder/interactor"); - - interactor - .register_wallet(config.owner.wallet().clone()) + pub async fn new() -> Self { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() .await; - interactor - .register_wallet(config.wallet.wallet().clone()) - .await; - - interactor.generate_blocks(30u64).await.unwrap(); - + let state = interactor.load_state::(); BasicInteractor { interactor, config, - state: State::load_state(), + state, } } @@ -90,7 +79,7 @@ impl BasicInteractor { .await; println!("new address: {new_address}"); - self.state.set_adder_address(new_address); + self.state.adder_address = Some(new_address); } pub async fn upgrade(&mut self, new_value: u32, sender: &Bech32Address, err: Option<&str>) { diff --git a/contracts/examples/adder/interactor/src/basic_interactor_config.rs b/contracts/examples/adder/interactor/src/basic_interactor_config.rs index 21619293bb..fddc48fc1a 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor_config.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor_config.rs @@ -1,9 +1,5 @@ -use multiversx_sc_snippets::{ConnectionConfig, WalletConfig}; +use multiversx_sc_snippets::{ConnectionConfig, InteractorConfig, WalletConfig, imports::Wallet}; use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; /// Adder Interact configuration #[derive(Debug, Deserialize)] @@ -14,21 +10,14 @@ pub struct Config { pub wallet: WalletConfig, } -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection } - pub fn chain_simulator_config() -> Self { - Config { - contract_path: "../output/adder.mxsc.json".to_owned(), - connection: ConnectionConfig::chain_simulator(), - owner: WalletConfig::from_test_wallet("mike"), - wallet: WalletConfig::from_test_wallet("ivan"), - } + fn register_wallets(&self) -> Vec { + vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] } } + +impl Config {} diff --git a/contracts/examples/adder/interactor/src/basic_interactor_state.rs b/contracts/examples/adder/interactor/src/basic_interactor_state.rs index bcab774134..523d0f5e07 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor_state.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor_state.rs @@ -1,37 +1,13 @@ use multiversx_sc_snippets::imports::*; use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; - -/// State file -const STATE_FILE: &str = "state.toml"; /// Adder Interact state #[derive(Debug, Default, Serialize, Deserialize)] pub struct State { - adder_address: Option, + pub adder_address: Option, } impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - - /// Sets the adder address - pub fn set_adder_address(&mut self, address: Bech32Address) { - self.adder_address = Some(address); - } - /// Returns the adder contract pub fn current_adder_address(&self) -> &Bech32Address { self.adder_address @@ -39,12 +15,3 @@ impl State { .expect("no known adder contract, deploy first") } } - -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index ad78a95b76..bb1ba31e8b 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -1,12 +1,34 @@ use basic_interactor::{BasicInteractor, Config}; -use multiversx_sc_snippets::{imports::Bech32Address, sdk::gateway::SetStateAccount, test_wallets}; +use multiversx_sc_snippets::{imports::*, sdk::gateway::SetStateAccount}; use serial_test::serial; +fn chain_simulator_config() -> Config { + Config { + contract_path: "../output/adder.mxsc.json".to_owned(), + connection: ConnectionConfig::chain_simulator(), + owner: WalletConfig::from_test_wallet("mike"), + wallet: WalletConfig::from_test_wallet("ivan"), + } +} + +async fn test_interactor() -> BasicInteractor { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(chain_simulator_config()) + .build() + .await; + BasicInteractor { + interactor, + config, + state: multiversx_sc_snippets::AutoSave::no_save_default(), + } +} + #[tokio::test] #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn simulator_upgrade_test() { - let mut basic_interact = BasicInteractor::new(Config::chain_simulator_config()).await; + let mut basic_interact = test_interactor().await; basic_interact.generate_blocks(2).await; @@ -54,8 +76,8 @@ async fn simulator_upgrade_test() { async fn set_state_cs_test() { let account_address = test_wallets::mike(); - let real_chain_interact = BasicInteractor::new(Config::load_config()).await; - let simulator_interact = BasicInteractor::new(Config::chain_simulator_config()).await; + let real_chain_interact = BasicInteractor::new().await; + let simulator_interact = test_interactor().await; let account = real_chain_interact .interactor @@ -96,8 +118,8 @@ async fn set_state_from_file_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = BasicInteractor::new(Config::load_config()).await; - let simulator_interact = BasicInteractor::new(Config::chain_simulator_config()).await; + let mut real_chain_interact = BasicInteractor::new().await; + let simulator_interact = test_interactor().await; // now we should have current mike account in the set state file real_chain_interact @@ -140,8 +162,8 @@ async fn set_state_overwrite_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = BasicInteractor::new(Config::load_config()).await; - let simulator_interact = BasicInteractor::new(Config::chain_simulator_config()).await; + let mut real_chain_interact = BasicInteractor::new().await; + let simulator_interact = test_interactor().await; // now we should have current mike and ivan accounts in the set state file real_chain_interact diff --git a/framework/snippets/Cargo.toml b/framework/snippets/Cargo.toml index d9601985fd..54635d55b3 100644 --- a/framework/snippets/Cargo.toml +++ b/framework/snippets/Cargo.toml @@ -26,6 +26,7 @@ futures = "0.3" anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +toml = "1.0" colored = "3.0" tokio = { version = "1.24", features = ["full"], optional = true } diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs index 8edd130d08..9de8c5fed2 100644 --- a/framework/snippets/src/config.rs +++ b/framework/snippets/src/config.rs @@ -1,5 +1,9 @@ -pub mod connection_config; -pub mod wallet_config; +mod auto_save; +mod connection_config; +mod interactor_config; +mod wallet_config; +pub use auto_save::*; pub use connection_config::*; +pub use interactor_config::*; pub use wallet_config::*; diff --git a/framework/snippets/src/config/auto_save.rs b/framework/snippets/src/config/auto_save.rs new file mode 100644 index 0000000000..a609852429 --- /dev/null +++ b/framework/snippets/src/config/auto_save.rs @@ -0,0 +1,81 @@ +use std::{ + io::Write, + ops::{Deref, DerefMut}, + path::PathBuf, +}; + +use serde::Serialize; + +/// RAII wrapper that serialises its value to a TOML file on drop. +/// +/// Saving can be disabled by constructing with [`AutoSave::no_save`] or by +/// calling [`AutoSave::disable`], which is useful in tests where you don't want +/// side-effects on disk. +pub struct AutoSave { + value: T, + /// `None` means saving is disabled. + path: Option, +} + +impl AutoSave { + /// Wraps `value` and saves to `path` on drop. + pub fn new(value: T, path: impl Into) -> Self { + AutoSave { + value, + path: Some(path.into()), + } + } + + /// Wraps `value` without saving on drop. + pub fn no_save(value: T) -> Self { + AutoSave { value, path: None } + } + + /// Wraps the default value without saving on drop. + pub fn no_save_default() -> Self + where + T: Default, + { + AutoSave::no_save(T::default()) + } + + /// Disables saving for this instance. + pub fn disable(&mut self) { + self.path = None; + } + + /// Serialises the value to the configured path immediately. + /// + /// Returns `Ok(())` if saving is disabled (no-op) or if the write + /// succeeds. Returns `Err` on serialisation or I/O failure. + pub fn save(&self) -> Result<(), Box> { + if let Some(path) = &self.path { + let content = toml::to_string(&self.value)?; + let mut file = std::fs::File::create(path)?; + file.write_all(content.as_bytes())?; + } + Ok(()) + } +} + +impl Deref for AutoSave { + type Target = T; + + fn deref(&self) -> &T { + &self.value + } +} + +impl DerefMut for AutoSave { + fn deref_mut(&mut self) -> &mut T { + &mut self.value + } +} + +impl Drop for AutoSave { + fn drop(&mut self) { + if let Err(e) = self.save() { + eprintln!("AutoSave: failed to write state: {e}"); + } + } +} diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs new file mode 100644 index 0000000000..3c8053c277 --- /dev/null +++ b/framework/snippets/src/config/interactor_config.rs @@ -0,0 +1,21 @@ +use crate::sdk::wallet::Wallet; + +use super::ConnectionConfig; + +/// Trait implemented by a contract-specific `Config` struct to tell the +/// [`InteractorBuilder`](crate::InteractorBuilder) how to initialise the interactor. +/// +/// The builder calls `connection()` to open the gateway and calls `wallets()` to +/// register every wallet that will sign transactions. +pub trait InteractorConfig { + /// Returns the connection settings (gateway URI, chain type). + fn connection(&self) -> &ConnectionConfig; + + /// Returns the wallets to be registered with the interactor during setup. + /// + /// Optional — defaults to no wallets. Additional wallets can always be + /// registered manually on the interactor after construction. + fn register_wallets(&self) -> Vec { + Vec::new() + } +} diff --git a/framework/snippets/src/http_interactor_builder.rs b/framework/snippets/src/http_interactor_builder.rs new file mode 100644 index 0000000000..381a35ad21 --- /dev/null +++ b/framework/snippets/src/http_interactor_builder.rs @@ -0,0 +1,125 @@ +use std::{ + io::Read, + path::{Path, PathBuf}, +}; + +use serde::de::DeserializeOwned; + +use crate::{InteractorBase, config::InteractorConfig}; + +const DEFAULT_CONFIG_FILE: &str = "config.toml"; + +/// Fluent builder that constructs an [`InteractorBase`] together with a typed `Config`. +/// +/// # Typical usage +/// +/// ```rust,ignore +/// let (interactor, config) = InteractorBuilder::::new() +/// .crate_dir(env!("CARGO_MANIFEST_DIR")) +/// .build() +/// .await; +/// let state = interactor.load_autosave::(); +/// ``` +pub struct HttpInteractorBuilder { + /// Directory that contains `config.toml` and `state.toml`. + crate_dir: PathBuf, + + /// Name of the config file relative to `crate_dir`. Default: `"config.toml"`. + config_file: String, + + /// Optional config override. If set, skips file loading. + config: Option, +} + +impl Default for HttpInteractorBuilder { + fn default() -> Self { + HttpInteractorBuilder { + crate_dir: PathBuf::from("."), + config_file: DEFAULT_CONFIG_FILE.to_owned(), + config: None, + } + } +} + +impl HttpInteractorBuilder +where + Config: DeserializeOwned + InteractorConfig, +{ + /// Creates a new builder. + /// + /// Use `.crate_dir(env!("CARGO_MANIFEST_DIR"))` to set the directory for + /// config and state file resolution. Defaults to `"."` (current directory). + /// The config type is inferred from context. + pub fn new() -> Self { + HttpInteractorBuilder::default() + } + + /// Override the directory used for config / state file resolution. + pub fn crate_dir(mut self, path: impl Into) -> Self { + self.crate_dir = path.into(); + self + } + + /// Override the config file name (relative to `crate_dir`). Default: `"config.toml"`. + pub fn config_file(mut self, name: impl Into) -> Self { + self.config_file = name.into(); + self + } + + /// Override the config with a pre-built value, skipping file loading. + pub fn with_config(mut self, config: Config) -> Self { + self.config = Some(config); + self + } + + fn resolve_config(self) -> (Config, PathBuf) { + let config = match self.config { + Some(c) => c, + None => { + let path = self.crate_dir.join(&self.config_file); + load_toml(&path) + } + }; + (config, self.crate_dir) + } +} + +impl HttpInteractorBuilder +where + Config: DeserializeOwned + InteractorConfig, +{ + /// Builds the pair `(Interactor, Config)`. + /// + /// 1. Resolves `Config` (from file or pre-built). + /// 2. Connects to the gateway described by `Config::connection()`. + /// 3. Registers all wallets returned by `Config::register_wallets()`. + /// + /// Use [`InteractorBase::load_autosave`] afterwards to load state. + pub async fn build(self) -> (crate::Interactor, Config) { + let (config, crate_dir) = self.resolve_config(); + + let conn = config.connection(); + let mut interactor = + InteractorBase::::new(conn.gateway_uri()) + .await + .use_chain_simulator(conn.use_chain_simulator()); + + interactor.current_dir = crate_dir; + + for wallet in config.register_wallets() { + interactor.register_wallet(wallet).await; + } + + (interactor, config) + } +} + +// ── helpers ────────────────────────────────────────────────────────────────── + +fn load_toml(path: &Path) -> T { + let mut file = + std::fs::File::open(path).unwrap_or_else(|e| panic!("cannot open {}: {e}", path.display())); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + toml::from_str(&content).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display())) +} diff --git a/framework/snippets/src/imports.rs b/framework/snippets/src/imports.rs index fcfcf82f4f..b8a83cb18a 100644 --- a/framework/snippets/src/imports.rs +++ b/framework/snippets/src/imports.rs @@ -5,12 +5,13 @@ pub use crate::{ InteractorSimulateGasAsync, SimulateGas, StepBuffer, dns_address_for_name, }; +pub use crate::config::*; pub use crate::sdk::{test_wallets, validator::Validator, wallet::Wallet}; pub use env_logger; #[cfg(feature = "http")] -pub use crate::{HttpInteractor, Interactor}; +pub use crate::{HttpInteractor, HttpInteractorBuilder, Interactor}; #[cfg(feature = "http")] pub use multiversx_sdk_http::GatewayHttpProxy; diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index fa4acf5495..cde3dc3816 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -150,4 +150,25 @@ where self.current_dir = path; self } + + /// Loads `State` from `state.toml` in `current_dir` (or returns the default) + /// and wraps it in an [`AutoSave`] that persists changes on drop. + /// + /// Call [`AutoSave::disable`] or use [`AutoSave::no_save`] directly when you + /// do not want side-effects (e.g. in tests). + pub fn load_state(&self) -> crate::AutoSave + where + State: serde::Serialize + serde::de::DeserializeOwned + Default, + { + let path = self.current_dir.join("state.toml"); + let value = if path.exists() { + let content = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); + toml::from_str(&content) + .unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display())) + } else { + State::default() + }; + crate::AutoSave::new(value, path) + } } diff --git a/framework/snippets/src/lib.rs b/framework/snippets/src/lib.rs index cd0a343cdd..3087302eab 100644 --- a/framework/snippets/src/lib.rs +++ b/framework/snippets/src/lib.rs @@ -33,5 +33,11 @@ pub type HttpInteractor = crate::InteractorBase; From 43b2e45eb8a1d56327e08a8b3607d27446e3b9ba Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 04:59:25 +0300 Subject: [PATCH 061/195] adder interactor - config and state to main file --- .../adder/interactor/src/basic_interactor.rs | 40 ++++++++++++++++--- .../interactor/src/basic_interactor_config.rs | 23 ----------- .../interactor/src/basic_interactor_state.rs | 17 -------- 3 files changed, 35 insertions(+), 45 deletions(-) delete mode 100644 contracts/examples/adder/interactor/src/basic_interactor_config.rs delete mode 100644 contracts/examples/adder/interactor/src/basic_interactor_state.rs diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index 8e77ac70fe..d7891715c0 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -1,13 +1,43 @@ mod basic_interactor_cli; -mod basic_interactor_config; -mod basic_interactor_state; use adder::adder_proxy; -pub use basic_interactor_config::Config; -use basic_interactor_state::State; use clap::Parser; - use multiversx_sc_snippets::imports::*; +use serde::{Deserialize, Serialize}; + +/// Adder Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub contract_path: String, + pub connection: ConnectionConfig, + pub owner: WalletConfig, + pub wallet: WalletConfig, +} + +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection + } + + fn register_wallets(&self) -> Vec { + vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] + } +} + +/// Adder Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + pub adder_address: Option, +} + +impl State { + /// Returns the adder contract + pub fn current_adder_address(&self) -> &Bech32Address { + self.adder_address + .as_ref() + .expect("no known adder contract, deploy first") + } +} pub async fn adder_cli() { env_logger::init(); diff --git a/contracts/examples/adder/interactor/src/basic_interactor_config.rs b/contracts/examples/adder/interactor/src/basic_interactor_config.rs deleted file mode 100644 index fddc48fc1a..0000000000 --- a/contracts/examples/adder/interactor/src/basic_interactor_config.rs +++ /dev/null @@ -1,23 +0,0 @@ -use multiversx_sc_snippets::{ConnectionConfig, InteractorConfig, WalletConfig, imports::Wallet}; -use serde::Deserialize; - -/// Adder Interact configuration -#[derive(Debug, Deserialize)] -pub struct Config { - pub contract_path: String, - pub connection: ConnectionConfig, - pub owner: WalletConfig, - pub wallet: WalletConfig, -} - -impl InteractorConfig for Config { - fn connection(&self) -> &ConnectionConfig { - &self.connection - } - - fn register_wallets(&self) -> Vec { - vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] - } -} - -impl Config {} diff --git a/contracts/examples/adder/interactor/src/basic_interactor_state.rs b/contracts/examples/adder/interactor/src/basic_interactor_state.rs deleted file mode 100644 index 523d0f5e07..0000000000 --- a/contracts/examples/adder/interactor/src/basic_interactor_state.rs +++ /dev/null @@ -1,17 +0,0 @@ -use multiversx_sc_snippets::imports::*; -use serde::{Deserialize, Serialize}; - -/// Adder Interact state -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct State { - pub adder_address: Option, -} - -impl State { - /// Returns the adder contract - pub fn current_adder_address(&self) -> &Bech32Address { - self.adder_address - .as_ref() - .expect("no known adder contract, deploy first") - } -} From 0306cb92ba2f63a7ca83f2c750f8b1bbdfbc7111 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 05:07:42 +0300 Subject: [PATCH 062/195] interactor builder - generate blocks --- framework/snippets/src/http_interactor_builder.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/framework/snippets/src/http_interactor_builder.rs b/framework/snippets/src/http_interactor_builder.rs index 381a35ad21..17861bcdd8 100644 --- a/framework/snippets/src/http_interactor_builder.rs +++ b/framework/snippets/src/http_interactor_builder.rs @@ -93,6 +93,8 @@ where /// 1. Resolves `Config` (from file or pre-built). /// 2. Connects to the gateway described by `Config::connection()`. /// 3. Registers all wallets returned by `Config::register_wallets()`. + /// 4. Generates 30 initial blocks when running against the chain simulator + /// (no-op on a real network). /// /// Use [`InteractorBase::load_autosave`] afterwards to load state. pub async fn build(self) -> (crate::Interactor, Config) { @@ -110,6 +112,8 @@ where interactor.register_wallet(wallet).await; } + interactor.generate_blocks(30).await.unwrap(); + (interactor, config) } } From 3098d2e7dc1d32ed9189df9eb3cc193681c17d95 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 3 Jun 2026 09:38:53 +0300 Subject: [PATCH 063/195] doc fixes --- framework/snippets/src/config/interactor_config.rs | 4 ++-- framework/snippets/src/http_interactor_builder.rs | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs index 3c8053c277..a396f5a5fe 100644 --- a/framework/snippets/src/config/interactor_config.rs +++ b/framework/snippets/src/config/interactor_config.rs @@ -3,9 +3,9 @@ use crate::sdk::wallet::Wallet; use super::ConnectionConfig; /// Trait implemented by a contract-specific `Config` struct to tell the -/// [`InteractorBuilder`](crate::InteractorBuilder) how to initialise the interactor. +/// [`HttpInteractorBuilder`](crate::HttpInteractorBuilder) how to initialise the interactor. /// -/// The builder calls `connection()` to open the gateway and calls `wallets()` to +/// The builder calls `connection()` to open the gateway and calls `register_wallets()` to /// register every wallet that will sign transactions. pub trait InteractorConfig { /// Returns the connection settings (gateway URI, chain type). diff --git a/framework/snippets/src/http_interactor_builder.rs b/framework/snippets/src/http_interactor_builder.rs index 17861bcdd8..8e2baeda7b 100644 --- a/framework/snippets/src/http_interactor_builder.rs +++ b/framework/snippets/src/http_interactor_builder.rs @@ -14,11 +14,11 @@ const DEFAULT_CONFIG_FILE: &str = "config.toml"; /// # Typical usage /// /// ```rust,ignore -/// let (interactor, config) = InteractorBuilder::::new() +/// let (interactor, config) = HttpInteractorBuilder::::new() /// .crate_dir(env!("CARGO_MANIFEST_DIR")) /// .build() /// .await; -/// let state = interactor.load_autosave::(); +/// let state = interactor.load_state::(); /// ``` pub struct HttpInteractorBuilder { /// Directory that contains `config.toml` and `state.toml`. @@ -96,7 +96,7 @@ where /// 4. Generates 30 initial blocks when running against the chain simulator /// (no-op on a real network). /// - /// Use [`InteractorBase::load_autosave`] afterwards to load state. + /// Use [`InteractorBase::load_state`] afterwards to load state. pub async fn build(self) -> (crate::Interactor, Config) { let (config, crate_dir) = self.resolve_config(); @@ -124,6 +124,7 @@ fn load_toml(path: &Path) -> T { let mut file = std::fs::File::open(path).unwrap_or_else(|e| panic!("cannot open {}: {e}", path.display())); let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); + file.read_to_string(&mut content) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); toml::from_str(&content).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display())) } From f135b7834960856a63efad906bfd3caf6f582bb0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 14:12:07 +0300 Subject: [PATCH 064/195] interactor cleanup - very large storage benchmark --- .../very-large-storage/interactor/config.toml | 4 + .../interactor/src/vls_interactor.rs | 75 ++++++++++++------- .../interactor/src/vls_interactor_config.rs | 48 ------------ .../interactor/src/vls_interactor_state.rs | 48 ------------ 4 files changed, 53 insertions(+), 122 deletions(-) delete mode 100644 contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_config.rs delete mode 100644 contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_state.rs diff --git a/contracts/benchmarks/very-large-storage/interactor/config.toml b/contracts/benchmarks/very-large-storage/interactor/config.toml index 64846243cc..7be0aaaee5 100644 --- a/contracts/benchmarks/very-large-storage/interactor/config.toml +++ b/contracts/benchmarks/very-large-storage/interactor/config.toml @@ -1,2 +1,6 @@ +[connection] chain_type = 'simulator' gateway_uri = 'http://localhost:8085' + +[owner] +test_wallet = 'mike' diff --git a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs b/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs index 3658f06d54..191bb43733 100644 --- a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs +++ b/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs @@ -1,22 +1,48 @@ mod vls_interactor_cli; -mod vls_interactor_config; -mod vls_interactor_state; use clap::Parser; +use multiversx_sc_snippets::imports::*; +use serde::{Deserialize, Serialize}; use very_large_storage::very_large_storage_proxy; -pub use vls_interactor_config::Config; -use vls_interactor_state::State; -use multiversx_sc_snippets::imports::*; +/// Very Large Storage Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub connection: ConnectionConfig, + pub owner: WalletConfig, +} + +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection + } + + fn register_wallets(&self) -> Vec { + vec![self.owner.wallet().clone()] + } +} + +/// Very Large Storage Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + pub contract_address: Option, +} + +impl State { + /// Returns the contract address + pub fn current_contract_address(&self) -> &Bech32Address { + self.contract_address + .as_ref() + .expect("no known contract, deploy first") + } +} const CODE_PATH: MxscPath = MxscPath::new("../output/very-large-storage.mxsc.json"); pub async fn very_large_storage_cli() { env_logger::init(); - let config = Config::load_config(); - - let mut basic_interact = VeryLargeStorageInteractor::new(config).await; + let mut basic_interact = VeryLargeStorageInteractor::new().await; let cli = vls_interactor_cli::InteractCli::parse(); match &cli.command { @@ -32,34 +58,30 @@ pub async fn very_large_storage_cli() { pub struct VeryLargeStorageInteractor { pub interactor: Interactor, - pub owner_address: Bech32Address, - pub state: State, + pub config: Config, + pub state: AutoSave, } impl VeryLargeStorageInteractor { - pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .use_chain_simulator(config.use_chain_simulator()); - interactor - .set_current_dir_from_workspace("contracts/benchmarks/very-large-storage/interactor"); - - let owner_address = interactor.register_wallet(test_wallets::mike()).await; - - interactor.generate_blocks(30u64).await.unwrap(); - + pub async fn new() -> Self { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() + .await; + let state = interactor.load_state::(); VeryLargeStorageInteractor { interactor, - owner_address: owner_address.into(), - state: State::load_state(), + config, + state, } } pub async fn deploy(&mut self) { + let owner_address = self.config.owner.address(); let new_address = self .interactor .tx() - .from(&self.owner_address) + .from(&owner_address) .gas(10_000_000u64) .typed(very_large_storage_proxy::VeryLargeStorageProxy) .init() @@ -69,14 +91,15 @@ impl VeryLargeStorageInteractor { .await; println!("new address: {new_address}"); - self.state.set_contract_address(new_address); + self.state.contract_address = Some(new_address); } pub async fn append(&mut self, num_bytes: u64) { + let owner_address = self.config.owner.address(); let gas_used = self .interactor .tx() - .from(&self.owner_address) + .from(&owner_address) .to(self.state.current_contract_address()) .gas(SimulateGas) .typed(very_large_storage_proxy::VeryLargeStorageProxy) diff --git a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_config.rs b/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_config.rs deleted file mode 100644 index d9802f6436..0000000000 --- a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_config.rs +++ /dev/null @@ -1,48 +0,0 @@ -use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -#[derive(Debug, Deserialize)] -pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, -} - -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - pub fn chain_simulator_config() -> Self { - Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri - } - - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } - } -} diff --git a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_state.rs b/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_state.rs deleted file mode 100644 index e606fb445a..0000000000 --- a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor_state.rs +++ /dev/null @@ -1,48 +0,0 @@ -use multiversx_sc_snippets::imports::*; -use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; - -/// State file -const STATE_FILE: &str = "state.toml"; - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct State { - contract_address: Option, -} - -impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - - pub fn set_contract_address(&mut self, address: Bech32Address) { - self.contract_address = Some(address); - } - - /// Returns the contract address - pub fn current_contract_address(&self) -> &Bech32Address { - self.contract_address - .as_ref() - .expect("no known contract, deploy first") - } -} - -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} From c3ec0fd2adaed557bd58d8411e6e9868e0fb0d56 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 14:30:48 +0300 Subject: [PATCH 065/195] Bech32Address - serde deserialization error handling fix --- chain/core/src/std/bech32_address.rs | 38 +++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/chain/core/src/std/bech32_address.rs b/chain/core/src/std/bech32_address.rs index 6cd9e68edf..23ac41d036 100644 --- a/chain/core/src/std/bech32_address.rs +++ b/chain/core/src/std/bech32_address.rs @@ -223,7 +223,7 @@ impl<'de> Deserialize<'de> for Bech32Address { if let Some(stripped) = bech32.strip_prefix("bech32:") { bech32 = stripped.to_string(); } - Ok(Bech32Address::from_bech32_string(bech32)) + Bech32Address::try_from_bech32_string(bech32).map_err(serde::de::Error::custom) } } @@ -351,4 +351,40 @@ mod tests { let result = Bech32Address::try_from_bech32_string(bad_bech32); assert!(matches!(result, Err(Bech32AddressError::InvalidLength(10)))); } + + // --- serde Deserialize tests --- + + #[test] + fn test_deserialize_valid() { + let json = format!("\"{VALID_BECH32}\""); + let addr: Bech32Address = serde_json::from_str(&json).unwrap(); + assert_eq!(addr.bech32, VALID_BECH32); + } + + #[test] + fn test_deserialize_bech32_prefix_stripped() { + // Old interactor format: "bech32:

" + let json = format!("\"bech32:{VALID_BECH32}\""); + let addr: Bech32Address = serde_json::from_str(&json).unwrap(); + assert_eq!(addr.bech32, VALID_BECH32); + } + + #[test] + fn test_deserialize_empty_string_returns_error() { + let result: Result = serde_json::from_str("\"\""); + assert!(result.is_err(), "expected error for empty string, got Ok"); + } + + #[test] + fn test_deserialize_invalid_bech32_returns_error() { + let result: Result = serde_json::from_str("\"not_valid!!!\""); + assert!(result.is_err(), "expected error for invalid bech32, got Ok"); + } + + #[test] + fn test_deserialize_bech32_prefix_only_returns_error() { + // "bech32:" strips to "" which should fail, not panic + let result: Result = serde_json::from_str("\"bech32:\""); + assert!(result.is_err(), "expected error for bare prefix, got Ok"); + } } From 250866d6266b0daf1782de4b70604566ff228561 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 14:31:29 +0300 Subject: [PATCH 066/195] Bech32Address - more tests --- chain/core/src/std/bech32_address.rs | 59 +++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/chain/core/src/std/bech32_address.rs b/chain/core/src/std/bech32_address.rs index 23ac41d036..dcaa6a4459 100644 --- a/chain/core/src/std/bech32_address.rs +++ b/chain/core/src/std/bech32_address.rs @@ -308,24 +308,37 @@ impl FromStr for Bech32Address { mod tests { use super::*; - // A known valid bech32 address on the MultiversX network (32-byte payload). - const VALID_BECH32: &str = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"; + // Alice's address, used as a representative non-trivial test address. + const ALICE_BECH32: &str = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + + // The zero address (all-zero 32-byte payload), kept for tests that specifically + // exercise that edge case. + const ZERO_BECH32: &str = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"; #[test] fn test_try_from_bech32_string_valid() { - let result = Bech32Address::try_from_bech32_string(VALID_BECH32.to_string()); + let result = Bech32Address::try_from_bech32_string(ALICE_BECH32.to_string()); assert!(result.is_ok()); let addr = result.unwrap(); - assert_eq!(addr.bech32, VALID_BECH32); + assert_eq!(addr.bech32, ALICE_BECH32); assert_eq!(addr.hrp, "erd"); assert_eq!(addr.address.as_bytes().len(), 32); } + #[test] + fn test_try_from_bech32_string_zero_address() { + let result = Bech32Address::try_from_bech32_string(ZERO_BECH32.to_string()); + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.bech32, ZERO_BECH32); + assert!(addr.address.as_bytes().iter().all(|&b| b == 0)); + } + #[test] fn test_try_from_bech32_string_roundtrip() { - let original = Bech32Address::try_from_bech32_string(VALID_BECH32.to_string()).unwrap(); + let original = Bech32Address::try_from_bech32_string(ALICE_BECH32.to_string()).unwrap(); let re_encoded = Bech32Address::encode_address(original.hrp, original.address.clone()); - assert_eq!(re_encoded.bech32, VALID_BECH32); + assert_eq!(re_encoded.bech32, ALICE_BECH32); } #[test] @@ -356,17 +369,25 @@ mod tests { #[test] fn test_deserialize_valid() { - let json = format!("\"{VALID_BECH32}\""); + let json = format!("\"{ALICE_BECH32}\""); let addr: Bech32Address = serde_json::from_str(&json).unwrap(); - assert_eq!(addr.bech32, VALID_BECH32); + assert_eq!(addr.bech32, ALICE_BECH32); + } + + #[test] + fn test_deserialize_zero_address() { + let json = format!("\"{ZERO_BECH32}\""); + let addr: Bech32Address = serde_json::from_str(&json).unwrap(); + assert_eq!(addr.bech32, ZERO_BECH32); + assert!(addr.address.as_bytes().iter().all(|&b| b == 0)); } #[test] fn test_deserialize_bech32_prefix_stripped() { // Old interactor format: "bech32:
" - let json = format!("\"bech32:{VALID_BECH32}\""); + let json = format!("\"bech32:{ALICE_BECH32}\""); let addr: Bech32Address = serde_json::from_str(&json).unwrap(); - assert_eq!(addr.bech32, VALID_BECH32); + assert_eq!(addr.bech32, ALICE_BECH32); } #[test] @@ -387,4 +408,22 @@ mod tests { let result: Result = serde_json::from_str("\"bech32:\""); assert!(result.is_err(), "expected error for bare prefix, got Ok"); } + + // --- serde Serialize tests --- + + #[test] + fn test_serialize_valid() { + let addr = Bech32Address::try_from_bech32_string(ALICE_BECH32.to_string()).unwrap(); + let json = serde_json::to_string(&addr).unwrap(); + assert_eq!(json, format!("\"{ALICE_BECH32}\"")); + } + + #[test] + fn test_serde_roundtrip() { + let original = Bech32Address::try_from_bech32_string(ALICE_BECH32.to_string()).unwrap(); + let json = serde_json::to_string(&original).unwrap(); + let decoded: Bech32Address = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.bech32, original.bech32); + assert_eq!(decoded.address, original.address); + } } From 224e11cea6993daa6346911583fbc8f60b52890e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 14:55:16 +0300 Subject: [PATCH 067/195] Bech32Address - refactor --- chain/core/src/std/bech32_address.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain/core/src/std/bech32_address.rs b/chain/core/src/std/bech32_address.rs index dcaa6a4459..28a4fe31bc 100644 --- a/chain/core/src/std/bech32_address.rs +++ b/chain/core/src/std/bech32_address.rs @@ -220,7 +220,7 @@ impl<'de> Deserialize<'de> for Bech32Address { { // some old interactors have it serialized like this let mut bech32 = String::deserialize(deserializer)?; - if let Some(stripped) = bech32.strip_prefix("bech32:") { + if let Some(stripped) = bech32.strip_prefix(BECH32_PREFIX) { bech32 = stripped.to_string(); } Bech32Address::try_from_bech32_string(bech32).map_err(serde::de::Error::custom) From b00b830f95e813b5dd790963dc8e27866d1f61ed Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 20:06:11 +0300 Subject: [PATCH 068/195] interactor cleanup - ping pong --- .../ping-pong-egld/interactor/config.toml | 8 ++ .../ping-pong-egld/interactor/src/interact.rs | 98 +++++++++++-------- .../interactor/src/interact_config.rs | 49 ---------- .../interactor/src/interact_state.rs | 50 ---------- .../tests/ping_pong_interact_cs_test.rs | 32 +++++- 5 files changed, 95 insertions(+), 142 deletions(-) delete mode 100644 contracts/examples/ping-pong-egld/interactor/src/interact_config.rs delete mode 100644 contracts/examples/ping-pong-egld/interactor/src/interact_state.rs diff --git a/contracts/examples/ping-pong-egld/interactor/config.toml b/contracts/examples/ping-pong-egld/interactor/config.toml index aca748706a..984e2a13db 100644 --- a/contracts/examples/ping-pong-egld/interactor/config.toml +++ b/contracts/examples/ping-pong-egld/interactor/config.toml @@ -1,5 +1,13 @@ +[connection] chain_type = 'simulator' gateway_uri = 'http://localhost:8085' +# [connection] # chain_type = 'real' # gateway_uri = 'https://devnet-gateway.multiversx.com' + +[owner] +test_wallet = 'eve' + +[wallet] +test_wallet = 'mallory' diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact.rs b/contracts/examples/ping-pong-egld/interactor/src/interact.rs index 1f7e5c6ab5..1e7cced3bc 100644 --- a/contracts/examples/ping-pong-egld/interactor/src/interact.rs +++ b/contracts/examples/ping-pong-egld/interactor/src/interact.rs @@ -1,24 +1,51 @@ mod interact_cli; -mod interact_config; -mod interact_state; -use crate::interact_state::State; use clap::Parser; -pub use interact_config::Config; +use multiversx_sc_snippets::imports::*; use ping_pong_egld::proxy::{self, ContractState, UserStatus}; +use serde::{Deserialize, Serialize}; + +/// Ping Pong Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub connection: ConnectionConfig, + pub owner: WalletConfig, + pub wallet: WalletConfig, +} -use multiversx_sc_snippets::imports::*; +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection + } + + fn register_wallets(&self) -> Vec { + vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] + } +} -const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; +/// Ping Pong Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + pub ping_pong_egld_address: Option, +} + +impl State { + /// Returns the ping pong contract + pub fn current_ping_pong_egld_address(&self) -> &Bech32Address { + self.ping_pong_egld_address + .as_ref() + .expect("no known ping pong contract, deploy first") + } +} + +pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; const PING_PONG_CODE: MxscPath = MxscPath::new("../output/ping-pong-egld.mxsc.json"); pub async fn ping_pong_egld_cli() { env_logger::init(); - let config = Config::load_config(); - - let mut interact = PingPongEgldInteract::init(config).await; + let mut interact = PingPongEgldInteract::new().await; let cli = interact_cli::InteractCli::parse(); match &cli.command { @@ -43,17 +70,17 @@ pub async fn ping_pong_egld_cli() { .await } Some(interact_cli::InteractCliCommand::Ping(args)) => { - let sender = interact.ping_pong_owner_address.clone(); + let sender = interact.config.owner.address(); interact .ping(args.cost.unwrap_or_default(), None, &sender) .await } Some(interact_cli::InteractCliCommand::Pong) => { - let sender = interact.ping_pong_owner_address.clone(); + let sender = interact.config.owner.address(); interact.pong(None, &sender).await; } Some(interact_cli::InteractCliCommand::PongAll) => { - let sender = interact.ping_pong_owner_address.clone(); + let sender = interact.config.owner.address(); interact.pong_all(None, &sender).await; } Some(interact_cli::InteractCliCommand::GetUserAddresses) => { @@ -111,40 +138,31 @@ pub async fn ping_pong_egld_cli() { pub struct PingPongEgldInteract { pub interactor: Interactor, - pub ping_pong_owner_address: Bech32Address, - pub wallet_address: Bech32Address, - pub state: State, + pub config: Config, + pub state: AutoSave, } impl PingPongEgldInteract { - pub async fn init(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .use_chain_simulator(config.use_chain_simulator()) - .with_tracer(INTERACTOR_SCENARIO_TRACE_PATH) + pub async fn new() -> Self { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() .await; - - interactor.set_current_dir_from_workspace("contracts/examples/ping-pong-egld/interactor"); - let ping_pong_owner_address = interactor.register_wallet(test_wallets::eve()).await; - let wallet_address = interactor.register_wallet(test_wallets::mallory()).await; - - // generate blocks until ESDTSystemSCAddress is enabled - interactor.generate_blocks_until_all_activations().await; - + let interactor = interactor.with_tracer(INTERACTOR_SCENARIO_TRACE_PATH).await; + let state = interactor.load_state::(); Self { interactor, - ping_pong_owner_address: ping_pong_owner_address.into(), - wallet_address: wallet_address.into(), - state: State::load_state(), + config, + state, } } pub async fn set_state(&mut self) { - println!("wallet address: {}", self.wallet_address); - self.interactor - .retrieve_account(&self.ping_pong_owner_address) - .await; - self.interactor.retrieve_account(&self.wallet_address).await; + let owner_address = self.config.owner.address(); + let wallet_address = self.config.wallet.address(); + println!("wallet address: {}", wallet_address); + self.interactor.retrieve_account(&owner_address).await; + self.interactor.retrieve_account(&wallet_address).await; } pub async fn deploy( @@ -156,10 +174,11 @@ impl PingPongEgldInteract { ) -> (u64, String) { self.set_state().await; + let owner_address = self.config.owner.address(); let (new_address, status, message) = self .interactor .tx() - .from(&self.ping_pong_owner_address) + .from(&owner_address) .gas(30_000_000u64) .typed(proxy::PingPongEgldProxy) .init(ping_amount, duration, opt_activation_timestamp, max_funds) @@ -171,7 +190,7 @@ impl PingPongEgldInteract { .await; println!("new address: {new_address}"); - self.state.set_ping_pong_egld_address(new_address); + self.state.ping_pong_egld_address = Some(new_address); (status, message) } @@ -183,11 +202,12 @@ impl PingPongEgldInteract { opt_activation_timestamp: Option, max_funds: OptionalValue, ) { + let wallet_address = self.config.wallet.address(); let response = self .interactor .tx() .to(self.state.current_ping_pong_egld_address()) - .from(&self.wallet_address) + .from(&wallet_address) .gas(30_000_000u64) .typed(proxy::PingPongEgldProxy) .upgrade(ping_amount, duration, opt_activation_timestamp, max_funds) diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact_config.rs b/contracts/examples/ping-pong-egld/interactor/src/interact_config.rs deleted file mode 100644 index 013e16714f..0000000000 --- a/contracts/examples/ping-pong-egld/interactor/src/interact_config.rs +++ /dev/null @@ -1,49 +0,0 @@ -use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -/// Ping Pong Interact configuration -#[derive(Debug, Deserialize)] -pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, -} - -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - pub fn chain_simulator_config() -> Self { - Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri - } - - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } - } -} diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact_state.rs b/contracts/examples/ping-pong-egld/interactor/src/interact_state.rs deleted file mode 100644 index 38fb3d320c..0000000000 --- a/contracts/examples/ping-pong-egld/interactor/src/interact_state.rs +++ /dev/null @@ -1,50 +0,0 @@ -use multiversx_sc_snippets::imports::*; -use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; - -/// State file -const STATE_FILE: &str = "state.toml"; - -/// Multisig Interact state -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct State { - ping_pong_egld_address: Option, -} - -impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - - /// Sets the ping pong address - pub fn set_ping_pong_egld_address(&mut self, address: Bech32Address) { - self.ping_pong_egld_address = Some(address); - } - - /// Returns the ping pong contract - pub fn current_ping_pong_egld_address(&self) -> &Bech32Address { - self.ping_pong_egld_address - .as_ref() - .expect("no known ping pong contract, deploy first") - } -} - -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} diff --git a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs index 1ef518c115..697fb2f0b9 100644 --- a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs +++ b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs @@ -1,14 +1,38 @@ -use multiversx_sc_snippets::imports::{DurationMillis, RustBigUint, TimestampMillis}; +use multiversx_sc_snippets::imports::{DurationMillis, RustBigUint, TimestampMillis, *}; use ping_pong_egld_interact::{Config, PingPongEgldInteract}; use serial_test::serial; +fn chain_simulator_config() -> Config { + Config { + connection: ConnectionConfig::chain_simulator(), + owner: WalletConfig::from_test_wallet("mike"), + wallet: WalletConfig::from_test_wallet("ivan"), + } +} + +async fn cs_interactor() -> PingPongEgldInteract { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(chain_simulator_config()) + .build() + .await; + let interactor = interactor + .with_tracer(ping_pong_egld_interact::INTERACTOR_SCENARIO_TRACE_PATH) + .await; + PingPongEgldInteract { + interactor, + config, + state: multiversx_sc_snippets::AutoSave::no_save_default(), + } +} + #[tokio::test] #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn test_ping_pong_egld() { - let mut interact = PingPongEgldInteract::init(Config::chain_simulator_config()).await; - let wallet_address = interact.wallet_address.clone(); - let ping_pong_owner_address = interact.ping_pong_owner_address.clone(); + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); + let ping_pong_owner_address = interact.config.owner.address(); let ping_amount = 1u64; From c1155b10ff9d7d6c583b3d0268ba0f45c09b6704 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 20:28:46 +0300 Subject: [PATCH 069/195] interactor cleanup - basic features --- .../basic-features/interact/config.toml | 7 ++- .../interact/src/bf_interact.rs | 29 +++++------ .../interact/src/bf_interact_config.rs | 48 ++++--------------- .../interact/src/bf_interact_state.rs | 28 ----------- .../interact/tests/bf_interact_cs_test.rs | 35 ++++++++++++-- 5 files changed, 57 insertions(+), 90 deletions(-) diff --git a/contracts/feature-tests/basic-features/interact/config.toml b/contracts/feature-tests/basic-features/interact/config.toml index 3a71d98ce1..366fe2b4a6 100644 --- a/contracts/feature-tests/basic-features/interact/config.toml +++ b/contracts/feature-tests/basic-features/interact/config.toml @@ -1,5 +1,10 @@ +[connection] chain_type = 'real' -gateway_uri = 'https://testnet-gateway.multiversx.com' +gateway_uri = 'https://devnet-gateway.multiversx.com' +[wallet] +test_wallet = 'mike' + +# [connection] # chain_type = 'simulator' # gateway_uri = 'http://localhost:8085' diff --git a/contracts/feature-tests/basic-features/interact/src/bf_interact.rs b/contracts/feature-tests/basic-features/interact/src/bf_interact.rs index 9bc513a895..9eeec27bf8 100644 --- a/contracts/feature-tests/basic-features/interact/src/bf_interact.rs +++ b/contracts/feature-tests/basic-features/interact/src/bf_interact.rs @@ -10,7 +10,7 @@ use clap::Parser; use multiversx_sc_snippets::imports::*; -const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; +pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; const CODE_EXPR_STORAGE_BYTES: MxscPath = MxscPath::new("../output/basic-features-storage-bytes.mxsc.json"); @@ -20,9 +20,7 @@ const CODE_EXPR: MxscPath = MxscPath::new("../output/basic-features.mxsc.json"); pub async fn basic_features_cli() { env_logger::init(); - let config = Config::load_config(); - - let mut bf_interact = BasicFeaturesInteract::init(config).await; + let mut bf_interact = BasicFeaturesInteract::init().await; let cli = bf_interact_cli::InteractCli::parse(); match cli.command { @@ -72,27 +70,24 @@ pub async fn basic_features_cli() { pub struct BasicFeaturesInteract { pub interactor: Interactor, pub wallet_address: Bech32Address, - pub state: State, + pub state: AutoSave, pub large_storage_payload: Vec, } impl BasicFeaturesInteract { - pub async fn init(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .use_chain_simulator(config.use_chain_simulator()) - .with_tracer(INTERACTOR_SCENARIO_TRACE_PATH) + pub async fn init() -> Self { + let (interactor, config) = HttpInteractorBuilder::::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() .await; - interactor - .set_current_dir_from_workspace("contracts/feature-tests/basic-features/interact"); - let wallet_address = interactor.register_wallet(test_wallets::mike()).await; - + let interactor = interactor.with_tracer(INTERACTOR_SCENARIO_TRACE_PATH).await; interactor.generate_blocks_until_all_activations().await; - + let wallet_address = config.wallet.address(); + let state = interactor.load_state::(); Self { interactor, - wallet_address: wallet_address.into(), - state: State::load_state(), + wallet_address, + state, large_storage_payload: Vec::new(), } } diff --git a/contracts/feature-tests/basic-features/interact/src/bf_interact_config.rs b/contracts/feature-tests/basic-features/interact/src/bf_interact_config.rs index bd19da629d..98faa57cb1 100644 --- a/contracts/feature-tests/basic-features/interact/src/bf_interact_config.rs +++ b/contracts/feature-tests/basic-features/interact/src/bf_interact_config.rs @@ -1,49 +1,19 @@ +use multiversx_sc_snippets::imports::*; use serde::Deserialize; -use std::io::Read; -/// Config file -const CONFIG_FILE: &str = "config.toml"; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -/// Adder Interact configuration +/// Basic Features Interact configuration #[derive(Debug, Deserialize)] pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, + pub connection: ConnectionConfig, + pub wallet: WalletConfig, } -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - pub fn chain_simulator_config() -> Self { - Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection } - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } + fn register_wallets(&self) -> Vec { + vec![self.wallet.wallet().clone()] } } diff --git a/contracts/feature-tests/basic-features/interact/src/bf_interact_state.rs b/contracts/feature-tests/basic-features/interact/src/bf_interact_state.rs index 68ac67a960..517b94ce32 100644 --- a/contracts/feature-tests/basic-features/interact/src/bf_interact_state.rs +++ b/contracts/feature-tests/basic-features/interact/src/bf_interact_state.rs @@ -1,12 +1,5 @@ use multiversx_sc_snippets::imports::Bech32Address; use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; - -/// State file -const STATE_FILE: &str = "state.toml"; /// Basic Features Interact state #[derive(Debug, Default, Serialize, Deserialize)] @@ -17,18 +10,6 @@ pub struct State { } impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - /// Sets the contract address for basic-features-storage-bytes pub fn set_bf_address_storage_bytes(&mut self, address: Bech32Address) { self.bf_address_storage_bytes = Some(address); @@ -53,12 +34,3 @@ impl State { .expect("basic-features contract not yet deployed") } } - -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} diff --git a/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs b/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs index 2b76ebc214..66ec88127d 100644 --- a/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs +++ b/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs @@ -7,11 +7,37 @@ use system_sc_interact::SysFuncCallsInteract; const ISSUE_COST: u64 = 50000000000000000u64; +fn chain_simulator_config() -> Config { + Config { + connection: ConnectionConfig::chain_simulator(), + wallet: WalletConfig::from_test_wallet("mike"), + } +} + +async fn cs_interactor() -> BasicFeaturesInteract { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(chain_simulator_config()) + .build() + .await; + let interactor = interactor + .with_tracer(basic_features_interact::INTERACTOR_SCENARIO_TRACE_PATH) + .await; + interactor.generate_blocks_until_all_activations().await; + let wallet_address = config.wallet.address(); + BasicFeaturesInteract { + interactor, + wallet_address, + state: multiversx_sc_snippets::AutoSave::no_save_default(), + large_storage_payload: Vec::new(), + } +} + #[tokio::test] #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn simulator_basic_features_test() { - let mut bf_interact = BasicFeaturesInteract::init(Config::chain_simulator_config()).await; + let mut bf_interact = cs_interactor().await; bf_interact.add_validator_key().await; bf_interact.deploy_storage_bytes(false).await; @@ -50,8 +76,7 @@ async fn send_esdt_to_non_existent_address_test() { let registered_wallet_address = test_wallets::mike().to_address(); let not_registered_wallet_address = test_wallets::alice().to_address(); - let mut chain_simulator_interact = - BasicFeaturesInteract::init(Config::chain_simulator_config()).await; + let mut chain_simulator_interact = cs_interactor().await; let token = chain_simulator_interact .interactor @@ -96,7 +121,7 @@ async fn send_esdt_to_non_existent_address_test() { #[serial] #[ignore = "signature verification is currently unavailable"] async fn simulator_crypto_test() { - let mut bf_interact = BasicFeaturesInteract::init(Config::chain_simulator_config()).await; + let mut bf_interact = cs_interactor().await; bf_interact.deploy().await; @@ -258,7 +283,7 @@ async fn verify_bls_aggregated_signature(interact: &mut BasicFeaturesInteract) { #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn chain_simulator_bf_get_special_roles_test() { - let mut bf_interact = BasicFeaturesInteract::init(Config::chain_simulator_config()).await; + let mut bf_interact = cs_interactor().await; let mut system_interact = SysFuncCallsInteract::init(system_sc_interact::Config::chain_simulator_config()).await; From 6772e93b47e8d889d99ca1174ac1264e3e541f92 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 20:35:47 +0300 Subject: [PATCH 070/195] interactor builder - generate blocks --- framework/snippets/src/http_interactor_builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/snippets/src/http_interactor_builder.rs b/framework/snippets/src/http_interactor_builder.rs index 8e2baeda7b..76ca01b9f3 100644 --- a/framework/snippets/src/http_interactor_builder.rs +++ b/framework/snippets/src/http_interactor_builder.rs @@ -93,7 +93,7 @@ where /// 1. Resolves `Config` (from file or pre-built). /// 2. Connects to the gateway described by `Config::connection()`. /// 3. Registers all wallets returned by `Config::register_wallets()`. - /// 4. Generates 30 initial blocks when running against the chain simulator + /// 4. Generates 2 initial blocks when running against the chain simulator /// (no-op on a real network). /// /// Use [`InteractorBase::load_state`] afterwards to load state. @@ -112,7 +112,7 @@ where interactor.register_wallet(wallet).await; } - interactor.generate_blocks(30).await.unwrap(); + interactor.generate_blocks_until_all_activations().await; (interactor, config) } From 7c24aec0833e9d08970170e548a2537e3f1423c5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 4 Jun 2026 20:36:08 +0300 Subject: [PATCH 071/195] interactor cleanup - tiny --- .../feature-tests/tiny/interactor/config.toml | 6 ++ .../tiny/interactor/src/tiny_interactor.rs | 77 ++++++++++++------- .../interactor/src/tiny_interactor_config.rs | 48 ------------ .../interactor/src/tiny_interactor_state.rs | 48 ------------ 4 files changed, 56 insertions(+), 123 deletions(-) delete mode 100644 contracts/feature-tests/tiny/interactor/src/tiny_interactor_config.rs delete mode 100644 contracts/feature-tests/tiny/interactor/src/tiny_interactor_state.rs diff --git a/contracts/feature-tests/tiny/interactor/config.toml b/contracts/feature-tests/tiny/interactor/config.toml index 1e629db942..feb8a005b6 100644 --- a/contracts/feature-tests/tiny/interactor/config.toml +++ b/contracts/feature-tests/tiny/interactor/config.toml @@ -1,3 +1,9 @@ +[connection] chain_type = 'real' gateway_uri = 'https://testnet-gateway.multiversx.com' +[owner] +test_wallet = 'alice' + +[wallet] +test_wallet = 'alice' diff --git a/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs b/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs index 94c060164e..fed7bfb3f9 100644 --- a/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs +++ b/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs @@ -1,21 +1,48 @@ mod tiny_interactor_cli; -mod tiny_interactor_config; -mod tiny_interactor_state; use clap::Parser; -pub use tiny_interactor_config::Config; -use tiny_interactor_state::State; - use multiversx_sc_snippets::imports::*; +use serde::{Deserialize, Serialize}; + +/// Tiny Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub connection: ConnectionConfig, + pub owner: WalletConfig, + pub wallet: WalletConfig, +} + +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection + } + + fn register_wallets(&self) -> Vec { + vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] + } +} + +/// Tiny Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + pub sc_address: Option, +} + +impl State { + /// Returns the contract address + pub fn current_contract_address(&self) -> &Bech32Address { + self.sc_address + .as_ref() + .expect("no known contract, deploy first") + } +} const CODE_PATH: FilePath = FilePath("../tiny.wasm"); pub async fn tiny_interactor_cli() { env_logger::init(); - let config = Config::load_config(); - - let mut basic_interact = TinyInteractor::new(config).await; + let mut basic_interact = TinyInteractor::new().await; let cli = tiny_interactor_cli::InteractCli::parse(); match &cli.command { @@ -31,27 +58,21 @@ pub async fn tiny_interactor_cli() { pub struct TinyInteractor { pub interactor: Interactor, - pub owner_address: Bech32Address, - pub wallet_address: Bech32Address, - pub state: State, + pub config: Config, + pub state: AutoSave, } impl TinyInteractor { - pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .use_chain_simulator(config.use_chain_simulator()); - - let owner_address = interactor.register_wallet(test_wallets::mike()).await; - let wallet_address = interactor.register_wallet(test_wallets::ivan()).await; - - interactor.generate_blocks(30u64).await.unwrap(); - + pub async fn new() -> Self { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() + .await; + let state = interactor.load_state::(); TinyInteractor { interactor, - owner_address: owner_address.into(), - wallet_address: wallet_address.into(), - state: State::load_state(), + config, + state, } } @@ -63,10 +84,11 @@ impl TinyInteractor { } pub async fn deploy(&mut self) { + let owner_address = self.config.owner.address(); let new_address = self .interactor .tx() - .from(&self.owner_address.clone()) + .from(&owner_address.clone()) .gas(600_000) .raw_deploy() .code(CODE_PATH) @@ -75,13 +97,14 @@ impl TinyInteractor { .await; println!("new address: {new_address}"); - self.state.set_sc_address(new_address); + self.state.sc_address = Some(new_address); } pub async fn call_x(&mut self) { + let wallet_address = self.config.wallet.address(); self.interactor .tx() - .from(&self.wallet_address) + .from(&wallet_address) .to(self.state.current_contract_address()) .gas(1_100_000) .raw_call("x") diff --git a/contracts/feature-tests/tiny/interactor/src/tiny_interactor_config.rs b/contracts/feature-tests/tiny/interactor/src/tiny_interactor_config.rs deleted file mode 100644 index d9802f6436..0000000000 --- a/contracts/feature-tests/tiny/interactor/src/tiny_interactor_config.rs +++ /dev/null @@ -1,48 +0,0 @@ -use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -#[derive(Debug, Deserialize)] -pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, -} - -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - pub fn chain_simulator_config() -> Self { - Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri - } - - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } - } -} diff --git a/contracts/feature-tests/tiny/interactor/src/tiny_interactor_state.rs b/contracts/feature-tests/tiny/interactor/src/tiny_interactor_state.rs deleted file mode 100644 index ef2161bf7f..0000000000 --- a/contracts/feature-tests/tiny/interactor/src/tiny_interactor_state.rs +++ /dev/null @@ -1,48 +0,0 @@ -use multiversx_sc_snippets::imports::*; -use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; - -/// State file -const STATE_FILE: &str = "state.toml"; - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct State { - sc_address: Option, -} - -impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - - pub fn set_sc_address(&mut self, address: Bech32Address) { - self.sc_address = Some(address); - } - - /// Returns the contract address - pub fn current_contract_address(&self) -> &Bech32Address { - self.sc_address - .as_ref() - .expect("no known contract, deploy first") - } -} - -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} From b0caa749d190f58a319ce41ae75946702e2ee763 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 11:34:33 +0300 Subject: [PATCH 072/195] CodeMetadata - stricter constructor/convertors --- chain/core/src/types/flags.rs | 2 +- chain/core/src/types/flags/code_metadata.rs | 127 ++++++++++++++---- .../general/upgrade_contract.rs | 2 +- .../tx_managed_buffer.rs | 3 +- .../wrappers/blockchain_wrapper.rs | 2 +- .../scenario/src/scenario/run_vm/sc_deploy.rs | 2 +- .../scenario/src/scenario/run_vm/set_state.rs | 4 +- 7 files changed, 112 insertions(+), 30 deletions(-) diff --git a/chain/core/src/types/flags.rs b/chain/core/src/types/flags.rs index 6b2522c386..9d2db6dc65 100644 --- a/chain/core/src/types/flags.rs +++ b/chain/core/src/types/flags.rs @@ -5,7 +5,7 @@ mod esdt_token_type; mod return_code; mod token_type; -pub use code_metadata::CodeMetadata; +pub use code_metadata::{CodeMetadata, CodeMetadataError}; pub use esdt_local_role::EsdtLocalRole; pub use esdt_local_role_flags::EsdtLocalRoleFlags; pub use esdt_token_type::EsdtTokenType; diff --git a/chain/core/src/types/flags/code_metadata.rs b/chain/core/src/types/flags/code_metadata.rs index ee55b3c734..133b0d1cea 100644 --- a/chain/core/src/types/flags/code_metadata.rs +++ b/chain/core/src/types/flags/code_metadata.rs @@ -98,32 +98,68 @@ impl CodeMetadata { f(DEFAULT_STRING); } } + + /// Parses code metadata from a 2-byte slice, mirroring the MultiversX protocol implementation. + /// + /// If the slice is not exactly 2 bytes, returns [`CodeMetadata::DEFAULT`] (all flags cleared). + /// Unknown or reserved bits are silently ignored; only the bits corresponding to known flags + /// are extracted. + /// + /// This intentionally lenient behaviour matches the on-chain Go implementation + /// (`CodeMetadataFromBytes`). Prefer [`TryFrom<&[u8]>`] in tooling and off-chain code + /// where strict validation is desirable. + pub fn from_bytes_or_default(bytes: &[u8]) -> Self { + if bytes.len() != 2 { + return CodeMetadata::DEFAULT; + } + let value = u16::from_be_bytes([bytes[0], bytes[1]]); + CodeMetadata::from_bits_truncate(value) + } +} + +/// Error type returned when converting raw bytes or integers into [`CodeMetadata`] fails. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum CodeMetadataError { + /// The input slice was not exactly 2 bytes long. + InvalidLength, + /// The bit pattern contains bits that do not correspond to any known flag. + InvalidBits(u16), } -impl From<[u8; 2]> for CodeMetadata { +impl TryFrom for CodeMetadata { + type Error = CodeMetadataError; + #[inline] - fn from(arr: [u8; 2]) -> Self { - CodeMetadata::from(u16::from_be_bytes(arr)) + fn try_from(value: u16) -> Result { + CodeMetadata::from_bits(value).ok_or(CodeMetadataError::InvalidBits(value)) } } -impl From for CodeMetadata { +impl TryFrom<[u8; 2]> for CodeMetadata { + type Error = CodeMetadataError; + #[inline] - fn from(value: u16) -> Self { - CodeMetadata::from_bits_truncate(value) + fn try_from(arr: [u8; 2]) -> Result { + CodeMetadata::try_from(u16::from_be_bytes(arr)) } } -impl From<&[u8]> for CodeMetadata { - fn from(slice: &[u8]) -> Self { - let arr: [u8; 2] = slice.try_into().unwrap_or_default(); - CodeMetadata::from(arr) +impl TryFrom<&[u8]> for CodeMetadata { + type Error = CodeMetadataError; + + fn try_from(slice: &[u8]) -> Result { + let arr: [u8; 2] = slice + .try_into() + .map_err(|_| CodeMetadataError::InvalidLength)?; + CodeMetadata::try_from(arr) } } -impl From<&Vec> for CodeMetadata { - fn from(v: &Vec) -> Self { - CodeMetadata::from(v.as_slice()) +impl TryFrom<&Vec> for CodeMetadata { + type Error = CodeMetadataError; + + fn try_from(v: &Vec) -> Result { + CodeMetadata::try_from(v.as_slice()) } } @@ -155,7 +191,8 @@ impl NestedDecode for CodeMetadata { I: NestedDecodeInput, H: DecodeErrorHandler, { - Ok(CodeMetadata::from(u16::dep_decode_or_handle_err(input, h)?)) + let value = u16::dep_decode_or_handle_err(input, h)?; + CodeMetadata::try_from(value).map_err(|_| h.handle_error(DecodeError::INVALID_VALUE)) } } @@ -219,16 +256,60 @@ mod tests { assert!(CodeMetadata::READABLE.is_readable()); } + #[test] + fn test_try_from_slice_exact_length() { + assert!( + CodeMetadata::try_from(&[1u8, 0u8][..]) + .unwrap() + .is_upgradeable() + ); + assert!( + CodeMetadata::try_from(&[0u8, 2u8][..]) + .unwrap() + .is_payable() + ); + assert!( + !CodeMetadata::try_from(&[0u8, 0u8][..]) + .unwrap() + .is_upgradeable() + ); + } + + #[test] + fn test_try_from_slice_wrong_length() { + assert_eq!( + CodeMetadata::try_from(&[1u8][..]), + Err(CodeMetadataError::InvalidLength) + ); + assert_eq!( + CodeMetadata::try_from(&[][..]), + Err(CodeMetadataError::InvalidLength) + ); + assert_eq!( + CodeMetadata::try_from(&[1u8, 0u8, 0u8][..]), + Err(CodeMetadataError::InvalidLength) + ); + } + + #[test] + fn test_try_from_invalid_bits() { + // 0x0001 has no defined flag in the second byte at bit 0 + assert_eq!( + CodeMetadata::try_from(0x0001u16), + Err(CodeMetadataError::InvalidBits(0x0001)) + ); + } + /// Translated from vm-wasm. #[test] - fn test_from_array() { - assert!(CodeMetadata::from([1, 0]).is_upgradeable()); - assert!(!CodeMetadata::from([1, 0]).is_readable()); - assert!(CodeMetadata::from([0, 2]).is_payable()); - assert!(CodeMetadata::from([4, 0]).is_readable()); - assert!(!CodeMetadata::from([4, 0]).is_upgradeable()); - assert!(!CodeMetadata::from([0, 0]).is_upgradeable()); - assert!(!CodeMetadata::from([0, 0]).is_payable()); - assert!(!CodeMetadata::from([0, 0]).is_readable()); + fn test_try_from_array() { + assert!(CodeMetadata::try_from([1u8, 0u8]).unwrap().is_upgradeable()); + assert!(!CodeMetadata::try_from([1u8, 0u8]).unwrap().is_readable()); + assert!(CodeMetadata::try_from([0u8, 2u8]).unwrap().is_payable()); + assert!(CodeMetadata::try_from([4u8, 0u8]).unwrap().is_readable()); + assert!(!CodeMetadata::try_from([4u8, 0u8]).unwrap().is_upgradeable()); + assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_upgradeable()); + assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_payable()); + assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_readable()); } } diff --git a/chain/vm/src/builtin_functions/general/upgrade_contract.rs b/chain/vm/src/builtin_functions/general/upgrade_contract.rs index 1bbb1563c1..5b332d6256 100644 --- a/chain/vm/src/builtin_functions/general/upgrade_contract.rs +++ b/chain/vm/src/builtin_functions/general/upgrade_contract.rs @@ -33,7 +33,7 @@ impl BuiltinFunction for UpgradeContract { } let new_code = tx_input.args[0].clone(); - let code_metadata = VMCodeMetadata::from(&tx_input.args[1]); + let code_metadata = VMCodeMetadata::from_bytes_or_default(&tx_input.args[1]); let args = if tx_input.args.len() > 2 { tx_input.args[2..].to_vec() diff --git a/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs b/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs index ef1aca4dc2..a18acce255 100644 --- a/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs +++ b/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs @@ -35,8 +35,7 @@ impl ManagedTypeContainer { } pub fn mb_to_code_metadata(&self, handle: RawHandle) -> VMCodeMetadata { - let bytes: [u8; 2] = self.mb_get(handle).try_into().unwrap(); - VMCodeMetadata::from(bytes) + VMCodeMetadata::from_bytes_or_default(self.mb_get(handle)) } pub fn mb_get_slice( diff --git a/framework/base/src/contract_base/wrappers/blockchain_wrapper.rs b/framework/base/src/contract_base/wrappers/blockchain_wrapper.rs index 0d16849fbb..ba27d63504 100644 --- a/framework/base/src/contract_base/wrappers/blockchain_wrapper.rs +++ b/framework/base/src/contract_base/wrappers/blockchain_wrapper.rs @@ -165,7 +165,7 @@ where ManagedRefMut::<'static, A, ManagedBuffer>::wrap_handle(mbuf_temp_1) .load_to_byte_array(&mut buffer); } - CodeMetadata::from(buffer) + CodeMetadata::from_bytes_or_default(&buffer) } pub fn get_code_hash(&self, address: &ManagedAddress) -> ManagedBuffer { diff --git a/framework/scenario/src/scenario/run_vm/sc_deploy.rs b/framework/scenario/src/scenario/run_vm/sc_deploy.rs index f6f1c4eebd..e23ecf8185 100644 --- a/framework/scenario/src/scenario/run_vm/sc_deploy.rs +++ b/framework/scenario/src/scenario/run_vm/sc_deploy.rs @@ -42,7 +42,7 @@ impl ScenarioVMRunner { let (new_address, tx_result) = execution::commit_deploy( tx_input, contract_code, - VMCodeMetadata::from(sc_deploy_step.tx.code_metadata.bits()), + sc_deploy_step.tx.code_metadata, &mut self.blockchain_mock.state, &runtime, f, diff --git a/framework/scenario/src/scenario/run_vm/set_state.rs b/framework/scenario/src/scenario/run_vm/set_state.rs index 6bcb366870..656d1df477 100644 --- a/framework/scenario/src/scenario/run_vm/set_state.rs +++ b/framework/scenario/src/scenario/run_vm/set_state.rs @@ -61,7 +61,9 @@ fn execute(state: &mut BlockchainState, set_state_step: &SetStateStep) { code_metadata: account .code_metadata .as_ref() - .map(|bytes_value| VMCodeMetadata::from(&bytes_value.value)) + .map(|bytes_value| { + VMCodeMetadata::try_from(&bytes_value.value).expect("invalid CodeMetadata") + }) .unwrap_or(DEFAULT_CODE_METADATA), contract_owner: account .owner From 8dddf0124aff01ba4e82b625a5bdc0e37e300961 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 11:38:08 +0300 Subject: [PATCH 073/195] CodeMetadata - removed VMCodeMetadata alias --- chain/vm/src/blockchain/state/account_data.rs | 6 +++--- .../src/builtin_functions/general/upgrade_contract.rs | 4 ++-- .../managed_type_container/tx_managed_buffer.rs | 6 +++--- chain/vm/src/host/context/tx_context.rs | 6 +++--- chain/vm/src/host/execution/exec_call.rs | 4 ++-- chain/vm/src/host/execution/exec_create.rs | 6 +++--- chain/vm/src/host/vm_hooks/vh_context.rs | 4 ++-- chain/vm/src/host/vm_hooks/vh_handler/vh_send.rs | 4 ++-- chain/vm/src/host/vm_hooks/vh_tx_context.rs | 4 ++-- chain/vm/src/types.rs | 2 +- .../scenario/src/api/impl_vh/vh_single_tx_api.rs | 4 ++-- framework/scenario/src/api/impl_vh/vh_static_api.rs | 4 ++-- framework/scenario/src/scenario/run_vm/sc_deploy.rs | 11 ++++------- framework/scenario/src/scenario/run_vm/set_state.rs | 6 +++--- 14 files changed, 34 insertions(+), 37 deletions(-) diff --git a/chain/vm/src/blockchain/state/account_data.rs b/chain/vm/src/blockchain/state/account_data.rs index a4e1906e07..0c890e55d3 100644 --- a/chain/vm/src/blockchain/state/account_data.rs +++ b/chain/vm/src/blockchain/state/account_data.rs @@ -4,7 +4,7 @@ use num_traits::Zero; use super::AccountEsdt; use crate::{ display_util::key_hex, - types::{Address, VMCodeMetadata}, + types::{Address, CodeMetadata}, }; use std::{collections::HashMap, fmt, fmt::Write}; @@ -19,7 +19,7 @@ pub struct AccountData { pub storage: AccountStorage, pub username: Vec, pub contract_path: Option>, - pub code_metadata: VMCodeMetadata, + pub code_metadata: CodeMetadata, pub contract_owner: Option
, pub developer_rewards: BigUint, } @@ -34,7 +34,7 @@ impl AccountData { storage: AccountStorage::default(), username: vec![], contract_path: None, - code_metadata: VMCodeMetadata::empty(), + code_metadata: CodeMetadata::empty(), contract_owner: None, developer_rewards: BigUint::zero(), } diff --git a/chain/vm/src/builtin_functions/general/upgrade_contract.rs b/chain/vm/src/builtin_functions/general/upgrade_contract.rs index 5b332d6256..fb672d1e06 100644 --- a/chain/vm/src/builtin_functions/general/upgrade_contract.rs +++ b/chain/vm/src/builtin_functions/general/upgrade_contract.rs @@ -3,7 +3,7 @@ use crate::{ host::context::{BlockchainUpdate, CallType, TxCache, TxFunctionName, TxInput, TxResult}, host::execution, host::runtime::{RuntimeInstanceCallLambda, RuntimeRef}, - types::VMCodeMetadata, + types::CodeMetadata, }; use super::super::builtin_func_trait::BuiltinFunction; @@ -33,7 +33,7 @@ impl BuiltinFunction for UpgradeContract { } let new_code = tx_input.args[0].clone(); - let code_metadata = VMCodeMetadata::from_bytes_or_default(&tx_input.args[1]); + let code_metadata = CodeMetadata::from_bytes_or_default(&tx_input.args[1]); let args = if tx_input.args.len() > 2 { tx_input.args[2..].to_vec() diff --git a/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs b/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs index a18acce255..13e2f5c313 100644 --- a/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs +++ b/chain/vm/src/host/context/managed_type_container/tx_managed_buffer.rs @@ -1,6 +1,6 @@ use crate::{ host::context::{TxFunctionName, TxTokenTransfer}, - types::{Address, RawHandle, VMCodeMetadata}, + types::{Address, CodeMetadata, RawHandle}, }; use super::ManagedTypeContainer; @@ -34,8 +34,8 @@ impl ManagedTypeContainer { TxFunctionName::from(self.mb_get(handle)) } - pub fn mb_to_code_metadata(&self, handle: RawHandle) -> VMCodeMetadata { - VMCodeMetadata::from_bytes_or_default(self.mb_get(handle)) + pub fn mb_to_code_metadata(&self, handle: RawHandle) -> CodeMetadata { + CodeMetadata::from_bytes_or_default(self.mb_get(handle)) } pub fn mb_get_slice( diff --git a/chain/vm/src/host/context/tx_context.rs b/chain/vm/src/host/context/tx_context.rs index ef26b188e5..1479fbf65a 100644 --- a/chain/vm/src/host/context/tx_context.rs +++ b/chain/vm/src/host/context/tx_context.rs @@ -4,7 +4,7 @@ use crate::{ state::{AccountData, AccountEsdt, BlockchainState}, }, host::runtime::RuntimeRef, - types::{Address, VMCodeMetadata}, + types::{Address, CodeMetadata}, }; use num_bigint::BigUint; use num_traits::Zero; @@ -53,7 +53,7 @@ impl TxContext { esdt: AccountEsdt::default(), username: Vec::new(), contract_path: None, - code_metadata: VMCodeMetadata::empty(), + code_metadata: CodeMetadata::empty(), contract_owner: None, developer_rewards: BigUint::zero(), }); @@ -154,7 +154,7 @@ impl TxContext { &self, new_address: &Address, contract_path: Vec, - code_metadata: VMCodeMetadata, + code_metadata: CodeMetadata, contract_owner: Address, ) { assert!( diff --git a/chain/vm/src/host/execution/exec_call.rs b/chain/vm/src/host/execution/exec_call.rs index b17d0916af..c970a43290 100644 --- a/chain/vm/src/host/execution/exec_call.rs +++ b/chain/vm/src/host/execution/exec_call.rs @@ -11,7 +11,7 @@ use crate::{ }, runtime::{RuntimeInstanceCallLambda, RuntimeInstanceCallLambdaDefault, RuntimeRef}, }, - types::VMCodeMetadata, + types::CodeMetadata, }; use num_bigint::BigUint; use num_traits::Zero; @@ -210,7 +210,7 @@ fn insert_ghost_account( username: Vec::new(), storage: HashMap::new(), contract_path: None, - code_metadata: VMCodeMetadata::empty(), + code_metadata: CodeMetadata::empty(), contract_owner: None, developer_rewards: BigUint::zero(), }); diff --git a/chain/vm/src/host/execution/exec_create.rs b/chain/vm/src/host/execution/exec_create.rs index ccc370ce2e..ec49c3c06b 100644 --- a/chain/vm/src/host/execution/exec_create.rs +++ b/chain/vm/src/host/execution/exec_create.rs @@ -2,14 +2,14 @@ use crate::{ blockchain::state::BlockchainStateRef, host::context::{BlockchainUpdate, TxCache, TxContext, TxInput, TxResult}, host::runtime::{RuntimeInstanceCallLambda, RuntimeRef}, - types::{Address, VMCodeMetadata}, + types::{Address, CodeMetadata}, }; /// Executes deploy transaction and commits changes back to the underlying blockchain state. pub fn commit_deploy( tx_input: TxInput, contract_path: &[u8], - code_metadata: VMCodeMetadata, + code_metadata: CodeMetadata, state: &mut BlockchainStateRef, runtime: &RuntimeRef, f: F, @@ -47,7 +47,7 @@ where pub fn execute_deploy( mut tx_input: TxInput, contract_path: Vec, - code_metadata: VMCodeMetadata, + code_metadata: CodeMetadata, tx_cache: TxCache, runtime: &RuntimeRef, f: F, diff --git a/chain/vm/src/host/vm_hooks/vh_context.rs b/chain/vm/src/host/vm_hooks/vh_context.rs index bb750ae4ff..0d567de474 100644 --- a/chain/vm/src/host/vm_hooks/vh_context.rs +++ b/chain/vm/src/host/vm_hooks/vh_context.rs @@ -8,7 +8,7 @@ use crate::{ BackTransfers, ManagedTypeContainer, TxErrorTrace, TxFunctionName, TxInput, TxLog, TxResult, }, schedule::GasSchedule, - types::{Address, H256, VMCodeMetadata}, + types::{Address, CodeMetadata, H256}, }; /// Abstracts away the borrowing of a managed types structure. @@ -119,7 +119,7 @@ pub trait VMHooksContext: Debug { &mut self, egld_value: num_bigint::BigUint, contract_code: Vec, - code_metadata: VMCodeMetadata, + code_metadata: CodeMetadata, args: Vec>, ) -> Result<(Address, Vec>), VMHooksEarlyExit>; diff --git a/chain/vm/src/host/vm_hooks/vh_handler/vh_send.rs b/chain/vm/src/host/vm_hooks/vh_handler/vh_send.rs index eb80f6731f..2404004bf8 100644 --- a/chain/vm/src/host/vm_hooks/vh_handler/vh_send.rs +++ b/chain/vm/src/host/vm_hooks/vh_handler/vh_send.rs @@ -7,7 +7,7 @@ use crate::{ context::{AsyncCallTxData, Promise, TxFunctionName, TxTokenTransfer}, vm_hooks::{VMHooksContext, vh_early_exit::early_exit_vm_error}, }, - types::{Address, RawHandle, VMCodeMetadata, top_encode_big_uint, top_encode_u64}, + types::{Address, CodeMetadata, RawHandle, top_encode_big_uint, top_encode_u64}, vm_err_msg, }; use multiversx_chain_core::types::ReturnCode; @@ -114,7 +114,7 @@ impl VMHooksHandler { to: Address, egld_value: num_bigint::BigUint, contract_code: Vec, - code_metadata: VMCodeMetadata, + code_metadata: CodeMetadata, args: Vec>, ) -> Result<(), VMHooksEarlyExit> { let mut arguments = vec![contract_code, code_metadata.to_vec()]; diff --git a/chain/vm/src/host/vm_hooks/vh_tx_context.rs b/chain/vm/src/host/vm_hooks/vh_tx_context.rs index f97960283c..f2d5a2b660 100644 --- a/chain/vm/src/host/vm_hooks/vh_tx_context.rs +++ b/chain/vm/src/host/vm_hooks/vh_tx_context.rs @@ -16,7 +16,7 @@ use crate::{ TxContextRef, TxFunctionName, TxInput, TxResult, async_call_tx_input, }, host::execution, - types::{Address, VMCodeMetadata}, + types::{Address, CodeMetadata}, vm_err_msg, }; @@ -209,7 +209,7 @@ impl VMHooksContext for TxVMHooksContext { &mut self, egld_value: num_bigint::BigUint, contract_code: Vec, - code_metadata: VMCodeMetadata, + code_metadata: CodeMetadata, args: Vec>, ) -> Result<(Address, Vec>), VMHooksEarlyExit> { let contract_address = self.current_address(); diff --git a/chain/vm/src/types.rs b/chain/vm/src/types.rs index e584909d0e..573563ed25 100644 --- a/chain/vm/src/types.rs +++ b/chain/vm/src/types.rs @@ -1,5 +1,5 @@ pub use crate::chain_core::types::Address; -pub use crate::chain_core::types::CodeMetadata as VMCodeMetadata; +pub use crate::chain_core::types::CodeMetadata; pub use crate::chain_core::types::EsdtLocalRole; pub use crate::chain_core::types::EsdtLocalRoleFlags; pub use crate::chain_core::types::H256; diff --git a/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs b/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs index b07f3063c6..573c5c3978 100644 --- a/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs +++ b/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs @@ -12,7 +12,7 @@ use multiversx_chain_vm::{ vm_hooks::VMHooksContext, }, schedule::GasSchedule, - types::{Address, VMCodeMetadata}, + types::{Address, CodeMetadata}, }; use crate::executor::debug::ContractDebugInstanceState; @@ -153,7 +153,7 @@ impl VMHooksContext for SingleTxApiVMHooksContext { &mut self, _egld_value: num_bigint::BigUint, _contract_code: Vec, - _code_metadata: VMCodeMetadata, + _code_metadata: CodeMetadata, _args: Vec>, ) -> Result<(Address, Vec>), VMHooksEarlyExit> { panic!("cannot launch contract calls in the SingleTxApi") diff --git a/framework/scenario/src/api/impl_vh/vh_static_api.rs b/framework/scenario/src/api/impl_vh/vh_static_api.rs index a5efd55b1d..bb3a670c67 100644 --- a/framework/scenario/src/api/impl_vh/vh_static_api.rs +++ b/framework/scenario/src/api/impl_vh/vh_static_api.rs @@ -9,7 +9,7 @@ use multiversx_chain_vm::{ vm_hooks::VMHooksContext, }, schedule::GasSchedule, - types::{Address, VMCodeMetadata}, + types::{Address, CodeMetadata}, }; use crate::executor::debug::ContractDebugInstanceState; @@ -132,7 +132,7 @@ impl VMHooksContext for StaticApiVMHooksContext { &mut self, _egld_value: num_bigint::BigUint, _contract_code: Vec, - _code_metadata: VMCodeMetadata, + _code_metadata: CodeMetadata, _args: Vec>, ) -> Result<(Address, Vec>), VMHooksEarlyExit> { panic!("cannot launch contract calls in the StaticApi") diff --git a/framework/scenario/src/scenario/run_vm/sc_deploy.rs b/framework/scenario/src/scenario/run_vm/sc_deploy.rs index e23ecf8185..fbfbb38720 100644 --- a/framework/scenario/src/scenario/run_vm/sc_deploy.rs +++ b/framework/scenario/src/scenario/run_vm/sc_deploy.rs @@ -2,13 +2,10 @@ use crate::{ multiversx_sc::types::heap::Address, scenario::model::ScDeployStep, scenario_model::TxResponse, }; -use multiversx_chain_vm::{ - host::{ - context::{TxFunctionName, TxInput, TxResult}, - execution, - runtime::{RuntimeInstanceCallLambda, RuntimeInstanceCallLambdaDefault}, - }, - types::VMCodeMetadata, +use multiversx_chain_vm::host::{ + context::{TxFunctionName, TxInput, TxResult}, + execution, + runtime::{RuntimeInstanceCallLambda, RuntimeInstanceCallLambdaDefault}, }; use super::{ScenarioVMRunner, check_tx_output, tx_input_util::generate_tx_hash}; diff --git a/framework/scenario/src/scenario/run_vm/set_state.rs b/framework/scenario/src/scenario/run_vm/set_state.rs index 656d1df477..39071dc709 100644 --- a/framework/scenario/src/scenario/run_vm/set_state.rs +++ b/framework/scenario/src/scenario/run_vm/set_state.rs @@ -5,14 +5,14 @@ use multiversx_chain_vm::{ AccountData, AccountEsdt, BlockInfo as CrateBlockInfo, BlockchainState, EsdtData, EsdtInstance, EsdtInstanceMetadata, EsdtInstances, EsdtRoles, }, - types::VMCodeMetadata, + types::CodeMetadata, }; use multiversx_sc::types::{TimestampMillis, TimestampSeconds}; use super::ScenarioVMRunner; /// Refers to the default of the "setState" scenario step. -pub const DEFAULT_CODE_METADATA: VMCodeMetadata = VMCodeMetadata::all(); +pub const DEFAULT_CODE_METADATA: CodeMetadata = CodeMetadata::all(); impl ScenarioVMRunner { pub fn perform_set_state(&mut self, set_state_step: &SetStateStep) { @@ -62,7 +62,7 @@ fn execute(state: &mut BlockchainState, set_state_step: &SetStateStep) { .code_metadata .as_ref() .map(|bytes_value| { - VMCodeMetadata::try_from(&bytes_value.value).expect("invalid CodeMetadata") + CodeMetadata::try_from(&bytes_value.value).expect("invalid CodeMetadata") }) .unwrap_or(DEFAULT_CODE_METADATA), contract_owner: account From dfe5d47ad830c3f97dcc5b06dde33545ba84f917 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 11:48:30 +0300 Subject: [PATCH 074/195] CodeMetadata - guarded flag --- chain/core/src/types/flags/code_metadata.rs | 67 ++++++++++++++------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/chain/core/src/types/flags/code_metadata.rs b/chain/core/src/types/flags/code_metadata.rs index 133b0d1cea..72b1b4410d 100644 --- a/chain/core/src/types/flags/code_metadata.rs +++ b/chain/core/src/types/flags/code_metadata.rs @@ -5,6 +5,7 @@ use bitflags::bitflags; const UPGRADEABLE_STRING: &str = "Upgradeable"; const READABLE_STRING: &str = "Readable"; +const GUARDED_STRING: &str = "Guarded"; const PAYABLE_STRING: &str = "Payable"; const PAYABLE_BY_SC_STRING: &str = "PayableBySC"; const DEFAULT_STRING: &str = "Default"; @@ -22,6 +23,10 @@ bitflags! { /// The contract's storage can be read by other contracts. const READABLE = 0b0000_0100_0000_0000; // 3rd LSB of first byte + /// The contract is guarded: transactions must carry a second signature + /// from a guardian account in order to be executed. + const GUARDED = 0b0000_1000_0000_0000; // 4th LSB of first byte + /// The contract can receive funds without having any endpoint called, /// just like user accounts. /// @@ -57,6 +62,11 @@ impl CodeMetadata { *self & CodeMetadata::READABLE != CodeMetadata::DEFAULT } + /// Returns `true` if the contract is guarded. + pub fn is_guarded(&self) -> bool { + *self & CodeMetadata::GUARDED != CodeMetadata::DEFAULT + } + #[inline] pub fn to_byte_array(&self) -> [u8; 2] { self.bits().to_be_bytes() @@ -79,6 +89,13 @@ impl CodeMetadata { f(READABLE_STRING); nothing_printed = false; } + if self.is_guarded() { + if !nothing_printed { + f("|"); + } + f(GUARDED_STRING); + nothing_printed = false; + } if self.is_payable() { if !nothing_printed { f("|"); @@ -222,38 +239,40 @@ mod tests { let all = CodeMetadata::UPGRADEABLE | CodeMetadata::PAYABLE | CodeMetadata::PAYABLE_BY_SC - | CodeMetadata::READABLE; + | CodeMetadata::READABLE + | CodeMetadata::GUARDED; assert!(all.is_upgradeable()); assert!(all.is_payable()); assert!(all.is_payable_by_sc()); assert!(all.is_readable()); + assert!(all.is_guarded()); - assert_eq!(all.bits(), 0x0506); + assert_eq!(all.bits(), 0x0d06); assert_eq!(CodeMetadata::from_bits_truncate(0xffff), all); } #[test] fn test_each() { - assert!(CodeMetadata::UPGRADEABLE.is_upgradeable()); - assert!(!CodeMetadata::PAYABLE.is_upgradeable()); - assert!(!CodeMetadata::PAYABLE_BY_SC.is_upgradeable()); - assert!(!CodeMetadata::READABLE.is_upgradeable()); - - assert!(!CodeMetadata::UPGRADEABLE.is_payable()); - assert!(CodeMetadata::PAYABLE.is_payable()); - assert!(!CodeMetadata::PAYABLE_BY_SC.is_payable()); - assert!(!CodeMetadata::READABLE.is_payable()); - - assert!(!CodeMetadata::UPGRADEABLE.is_payable_by_sc()); - assert!(!CodeMetadata::PAYABLE.is_payable_by_sc()); - assert!(CodeMetadata::PAYABLE_BY_SC.is_payable_by_sc()); - assert!(!CodeMetadata::READABLE.is_payable_by_sc()); - - assert!(!CodeMetadata::UPGRADEABLE.is_readable()); - assert!(!CodeMetadata::PAYABLE.is_readable()); - assert!(!CodeMetadata::PAYABLE_BY_SC.is_readable()); - assert!(CodeMetadata::READABLE.is_readable()); + type CodeMetadataChecker = fn(&CodeMetadata) -> bool; + let flags_and_checkers: &[(CodeMetadata, CodeMetadataChecker)] = &[ + (CodeMetadata::UPGRADEABLE, CodeMetadata::is_upgradeable), + (CodeMetadata::READABLE, CodeMetadata::is_readable), + (CodeMetadata::GUARDED, CodeMetadata::is_guarded), + (CodeMetadata::PAYABLE, CodeMetadata::is_payable), + (CodeMetadata::PAYABLE_BY_SC, CodeMetadata::is_payable_by_sc), + ]; + + // checking that checkers match, and are orthogonal (each checker only returns true for its own flag) + for (flag, checker) in flags_and_checkers { + for (other_flag, _) in flags_and_checkers { + assert_eq!( + checker(other_flag), + flag == other_flag, + "{checker:?} returned unexpected result for flag {other_flag:?}", + ); + } + } } #[test] @@ -305,11 +324,17 @@ mod tests { fn test_try_from_array() { assert!(CodeMetadata::try_from([1u8, 0u8]).unwrap().is_upgradeable()); assert!(!CodeMetadata::try_from([1u8, 0u8]).unwrap().is_readable()); + assert!(!CodeMetadata::try_from([1u8, 0u8]).unwrap().is_guarded()); assert!(CodeMetadata::try_from([0u8, 2u8]).unwrap().is_payable()); assert!(CodeMetadata::try_from([4u8, 0u8]).unwrap().is_readable()); assert!(!CodeMetadata::try_from([4u8, 0u8]).unwrap().is_upgradeable()); + assert!(!CodeMetadata::try_from([4u8, 0u8]).unwrap().is_guarded()); + assert!(CodeMetadata::try_from([8u8, 0u8]).unwrap().is_guarded()); + assert!(!CodeMetadata::try_from([8u8, 0u8]).unwrap().is_upgradeable()); + assert!(!CodeMetadata::try_from([8u8, 0u8]).unwrap().is_readable()); assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_upgradeable()); assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_payable()); assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_readable()); + assert!(!CodeMetadata::try_from([0u8, 0u8]).unwrap().is_guarded()); } } From 8bca926f29d944ac1fa571504364493ec7a775a8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 12:11:20 +0300 Subject: [PATCH 075/195] CodeMetadata - empty vs. default clarification, docs, refactor --- chain/core/src/types/flags/code_metadata.rs | 72 +++++++++++++++---- .../multisig/tests/multisig_blackbox_test.rs | 4 +- .../multisig/tests/multisig_whitebox_test.rs | 4 +- .../examples/order-book/factory/src/lib.rs | 2 +- .../forwarder-legacy/src/fwd_deploy_legacy.rs | 4 +- .../composability/forwarder/src/fwd_deploy.rs | 2 +- .../mesh-interactor/src/call_tree_deploy.rs | 2 +- .../types/interaction/tx_data/upgrade_call.rs | 2 +- framework/base/tests/formatter_test.rs | 2 +- .../meta/src/cmd/tx/parse_code_metadata.rs | 2 +- .../scenario/model/transaction/tx_deploy.rs | 2 +- .../scenario/src/scenario/run_vm/set_state.rs | 5 +- 12 files changed, 71 insertions(+), 32 deletions(-) diff --git a/chain/core/src/types/flags/code_metadata.rs b/chain/core/src/types/flags/code_metadata.rs index 72b1b4410d..8c005c156b 100644 --- a/chain/core/src/types/flags/code_metadata.rs +++ b/chain/core/src/types/flags/code_metadata.rs @@ -11,11 +11,42 @@ const PAYABLE_BY_SC_STRING: &str = "PayableBySC"; const DEFAULT_STRING: &str = "Default"; bitflags! { - /// Flags representing the smart contract's allowed actions after deploy. - #[derive(Default, PartialEq, Debug, Clone, Copy)] + /// Bit-flags that govern a smart contract's permissions after deployment. + /// + /// Each flag corresponds to a protocol-level capability: + /// - [`UPGRADEABLE`](CodeMetadata::UPGRADEABLE) — the contract may be upgraded by its owner. + /// - [`READABLE`](CodeMetadata::READABLE) — other contracts may read this contract's storage. + /// - [`GUARDED`](CodeMetadata::GUARDED) — transactions require a second guardian signature. + /// - [`PAYABLE`](CodeMetadata::PAYABLE) — the contract accepts direct EGLD/ESDT transfers. + /// - [`PAYABLE_BY_SC`](CodeMetadata::PAYABLE_BY_SC) — the contract accepts transfers from other contracts only. + /// + /// # Wire format + /// + /// Metadata is serialised as exactly **2 big-endian bytes**. The first byte carries + /// `UPGRADEABLE` (bit 0), `READABLE` (bit 2), and `GUARDED` (bit 3); the second byte carries + /// `PAYABLE` (bit 1) and `PAYABLE_BY_SC` (bit 2). + /// + /// # Parsing + /// + /// - Use [`TryFrom<&[u8]>`] / [`TryFrom`] in **tooling and off-chain code** — these + /// return [`CodeMetadataError`] on wrong-length input or unknown bits, enabling fail-closed + /// validation. + /// - Use [`from_bytes_or_default`](CodeMetadata::from_bytes_or_default) in the **Rust VM** — + /// this mirrors the lenient Go protocol implementation: wrong-length input yields + /// [`EMPTY`](CodeMetadata::EMPTY) and unknown bits are silently truncated. + /// + /// # Default value + /// + /// [`Default::default()`] returns `UPGRADEABLE | READABLE | PAYABLE | PAYABLE_BY_SC`, + /// which is the permissive set used as a fallback in **scenario tests** when a deployed + /// contract's metadata field is left unset. This is intentionally *not* the zero value; + /// use [`EMPTY`](CodeMetadata::EMPTY) explicitly when you need all flags cleared. + #[derive(PartialEq, Debug, Clone, Copy)] pub struct CodeMetadata: u16 { - /// No flags set. The contract is not upgradeable, not readable, and not payable. - const DEFAULT = 0; + /// No flags set. The contract is not upgradeable, not readable, and not payable, has no guardian. + /// + /// Warning! This is not the default value, see CodeMetadata::default() for that. + const EMPTY = 0; /// The contract can be upgraded in the future. const UPGRADEABLE = 0b0000_0001_0000_0000; // LSB of first byte @@ -40,31 +71,40 @@ bitflags! { } } +impl Default for CodeMetadata { + fn default() -> Self { + CodeMetadata::UPGRADEABLE + | CodeMetadata::READABLE + | CodeMetadata::PAYABLE + | CodeMetadata::PAYABLE_BY_SC + } +} + impl CodeMetadata { /// Returns `true` if the contract is allowed to be upgraded. pub fn is_upgradeable(&self) -> bool { - *self & CodeMetadata::UPGRADEABLE != CodeMetadata::DEFAULT + *self & CodeMetadata::UPGRADEABLE != CodeMetadata::EMPTY } /// Returns `true` if the contract can receive funds without an endpoint call. pub fn is_payable(&self) -> bool { - *self & CodeMetadata::PAYABLE != CodeMetadata::DEFAULT + *self & CodeMetadata::PAYABLE != CodeMetadata::EMPTY } /// Returns `true` if the contract can receive funds from other smart contracts /// without an endpoint call. Direct user transfers are rejected. pub fn is_payable_by_sc(&self) -> bool { - *self & CodeMetadata::PAYABLE_BY_SC != CodeMetadata::DEFAULT + *self & CodeMetadata::PAYABLE_BY_SC != CodeMetadata::EMPTY } /// Returns `true` if the contract's storage can be read by other contracts. pub fn is_readable(&self) -> bool { - *self & CodeMetadata::READABLE != CodeMetadata::DEFAULT + *self & CodeMetadata::READABLE != CodeMetadata::EMPTY } /// Returns `true` if the contract is guarded. pub fn is_guarded(&self) -> bool { - *self & CodeMetadata::GUARDED != CodeMetadata::DEFAULT + *self & CodeMetadata::GUARDED != CodeMetadata::EMPTY } #[inline] @@ -118,7 +158,7 @@ impl CodeMetadata { /// Parses code metadata from a 2-byte slice, mirroring the MultiversX protocol implementation. /// - /// If the slice is not exactly 2 bytes, returns [`CodeMetadata::DEFAULT`] (all flags cleared). + /// If the slice is not exactly 2 bytes, returns [`CodeMetadata::EMPTY`] (all flags cleared). /// Unknown or reserved bits are silently ignored; only the bits corresponding to known flags /// are extracted. /// @@ -127,7 +167,7 @@ impl CodeMetadata { /// where strict validation is desirable. pub fn from_bytes_or_default(bytes: &[u8]) -> Self { if bytes.len() != 2 { - return CodeMetadata::DEFAULT; + return CodeMetadata::EMPTY; } let value = u16::from_be_bytes([bytes[0], bytes[1]]); CodeMetadata::from_bits_truncate(value) @@ -228,10 +268,12 @@ mod tests { use super::*; #[test] - fn test_default() { - assert!(!CodeMetadata::DEFAULT.is_upgradeable()); - assert!(!CodeMetadata::DEFAULT.is_payable()); - assert!(!CodeMetadata::DEFAULT.is_readable()); + fn test_empty() { + assert!(!CodeMetadata::EMPTY.is_upgradeable()); + assert!(!CodeMetadata::EMPTY.is_payable()); + assert!(!CodeMetadata::EMPTY.is_payable_by_sc()); + assert!(!CodeMetadata::EMPTY.is_readable()); + assert!(!CodeMetadata::EMPTY.is_guarded()); } #[test] diff --git a/contracts/examples/multisig/tests/multisig_blackbox_test.rs b/contracts/examples/multisig/tests/multisig_blackbox_test.rs index bc2579ba4f..060cff6d7e 100644 --- a/contracts/examples/multisig/tests/multisig_blackbox_test.rs +++ b/contracts/examples/multisig/tests/multisig_blackbox_test.rs @@ -530,7 +530,7 @@ fn blackbox_deploy_and_upgrade_from_source() { let action_id = state.propose_sc_deploy_from_source( 0u64, ADDER_ADDRESS, - CodeMetadata::all(), + CodeMetadata::default(), MultiValueVec::from([top_encode_to_vec_u8_or_panic(&5u64)]), ); state.sign(action_id); @@ -576,7 +576,7 @@ fn blackbox_deploy_and_upgrade_from_source() { ADDER_ADDRESS, 0u64, factorial_address, - CodeMetadata::all(), + CodeMetadata::default(), MultiValueVec::new(), ); state.sign(action_id); diff --git a/contracts/examples/multisig/tests/multisig_whitebox_test.rs b/contracts/examples/multisig/tests/multisig_whitebox_test.rs index f602753aa6..f57781b767 100644 --- a/contracts/examples/multisig/tests/multisig_whitebox_test.rs +++ b/contracts/examples/multisig/tests/multisig_whitebox_test.rs @@ -626,7 +626,7 @@ fn whitebox_deploy_and_upgrade_from_source() { ActionRaw::SCDeployFromSource { amount: 0u64.into(), source: ADDER_ADDRESS.to_address(), - code_metadata: CodeMetadata::all(), + code_metadata: CodeMetadata::default(), arguments: vec![BoxedBytes::from(&[5u8][..])], }, None, @@ -683,7 +683,7 @@ fn whitebox_deploy_and_upgrade_from_source() { ActionRaw::SCUpgradeFromSource { source: FACTORIAL_ADDRESS.to_address(), amount: 0u64.into(), - code_metadata: CodeMetadata::all(), + code_metadata: CodeMetadata::default(), arguments: Vec::new(), sc_address: ADDER_ADDRESS.to_address(), }, diff --git a/contracts/examples/order-book/factory/src/lib.rs b/contracts/examples/order-book/factory/src/lib.rs index d6ca7e9642..1df8c7115a 100644 --- a/contracts/examples/order-book/factory/src/lib.rs +++ b/contracts/examples/order-book/factory/src/lib.rs @@ -33,7 +33,7 @@ pub trait Factory { .raw_deploy() .arguments_raw(arguments) .from_source(source) - .code_metadata(CodeMetadata::DEFAULT) + .code_metadata(CodeMetadata::EMPTY) .returns(ReturnsNewManagedAddress) .sync_call(); diff --git a/contracts/feature-tests/composability/forwarder-legacy/src/fwd_deploy_legacy.rs b/contracts/feature-tests/composability/forwarder-legacy/src/fwd_deploy_legacy.rs index d573b45ec0..64a58ae0da 100644 --- a/contracts/feature-tests/composability/forwarder-legacy/src/fwd_deploy_legacy.rs +++ b/contracts/feature-tests/composability/forwarder-legacy/src/fwd_deploy_legacy.rs @@ -38,7 +38,7 @@ pub trait DeployContractModule { ) -> (ManagedAddress, OptionalValue) { self.vault_proxy() .init(opt_arg) - .deploy_contract(code, CodeMetadata::DEFAULT) + .deploy_contract(code, CodeMetadata::EMPTY) } #[endpoint] @@ -49,7 +49,7 @@ pub trait DeployContractModule { ) -> MultiValue2> { self.vault_proxy() .init(opt_arg) - .deploy_from_source(&source_address, CodeMetadata::DEFAULT) + .deploy_from_source(&source_address, CodeMetadata::EMPTY) .into() } } diff --git a/contracts/feature-tests/composability/forwarder/src/fwd_deploy.rs b/contracts/feature-tests/composability/forwarder/src/fwd_deploy.rs index 7145f2555a..bc8db840bd 100644 --- a/contracts/feature-tests/composability/forwarder/src/fwd_deploy.rs +++ b/contracts/feature-tests/composability/forwarder/src/fwd_deploy.rs @@ -53,7 +53,7 @@ pub trait DeployContractModule { self.tx() .typed(vault_proxy::VaultProxy) .init(opt_arg) - .code_metadata(CodeMetadata::DEFAULT) + .code_metadata(CodeMetadata::EMPTY) .from_source(source_address) .returns(ReturnsNewManagedAddress) .returns(ReturnsResult) diff --git a/contracts/feature-tests/composability/mesh-interactor/src/call_tree_deploy.rs b/contracts/feature-tests/composability/mesh-interactor/src/call_tree_deploy.rs index 1843afc9f7..640d11cf21 100644 --- a/contracts/feature-tests/composability/mesh-interactor/src/call_tree_deploy.rs +++ b/contracts/feature-tests/composability/mesh-interactor/src/call_tree_deploy.rs @@ -44,7 +44,7 @@ impl ComposabilityInteract { let code_metadata = if contract.payable.unwrap_or(false) { CodeMetadata::PAYABLE } else { - CodeMetadata::DEFAULT + CodeMetadata::EMPTY }; buffer.push_tx(|tx| { tx.from(wallet) diff --git a/framework/base/src/types/interaction/tx_data/upgrade_call.rs b/framework/base/src/types/interaction/tx_data/upgrade_call.rs index 591e8accb5..185ef43396 100644 --- a/framework/base/src/types/interaction/tx_data/upgrade_call.rs +++ b/framework/base/src/types/interaction/tx_data/upgrade_call.rs @@ -23,7 +23,7 @@ where fn default() -> UpgradeCall { UpgradeCall { code_source: (), - code_metadata: CodeMetadata::DEFAULT, + code_metadata: CodeMetadata::EMPTY, arg_buffer: ManagedArgBuffer::new(), } } diff --git a/framework/base/tests/formatter_test.rs b/framework/base/tests/formatter_test.rs index 1519374672..04038b3c95 100644 --- a/framework/base/tests/formatter_test.rs +++ b/framework/base/tests/formatter_test.rs @@ -140,7 +140,7 @@ fn test_display_code_metadata() { | CodeMetadata::PAYABLE_BY_SC, "Upgradeable|Readable|Payable|PayableBySC", ); - check_code_metadata_display(CodeMetadata::DEFAULT, "Default"); + check_code_metadata_display(CodeMetadata::EMPTY, "Default"); } fn check_lower_hex_eq(item: T, expected: &str) { diff --git a/framework/meta/src/cmd/tx/parse_code_metadata.rs b/framework/meta/src/cmd/tx/parse_code_metadata.rs index 1b971bb0db..d9e2e9b530 100644 --- a/framework/meta/src/cmd/tx/parse_code_metadata.rs +++ b/framework/meta/src/cmd/tx/parse_code_metadata.rs @@ -5,7 +5,7 @@ use crate::cli::cli_args_tx::MetadataArgs; /// Build a [`CodeMetadata`] bitfield from the CLI [`MetadataArgs`] flags. /// Defaults match standard contract conventions: upgradeable + readable, not payable. pub fn parse_code_metadata(meta: &MetadataArgs) -> CodeMetadata { - let mut flags = CodeMetadata::DEFAULT; + let mut flags = CodeMetadata::EMPTY; if !meta.metadata_not_upgradeable { flags |= CodeMetadata::UPGRADEABLE; } diff --git a/framework/scenario/src/scenario/model/transaction/tx_deploy.rs b/framework/scenario/src/scenario/model/transaction/tx_deploy.rs index b497265eb0..52617151ed 100644 --- a/framework/scenario/src/scenario/model/transaction/tx_deploy.rs +++ b/framework/scenario/src/scenario/model/transaction/tx_deploy.rs @@ -25,7 +25,7 @@ impl Default for TxDeploy { Self { from: Default::default(), egld_value: Default::default(), - code_metadata: CodeMetadata::all(), + code_metadata: CodeMetadata::default(), contract_code: Default::default(), arguments: Default::default(), gas_limit: U64Value::from(DEFAULT_GAS_EXPR), diff --git a/framework/scenario/src/scenario/run_vm/set_state.rs b/framework/scenario/src/scenario/run_vm/set_state.rs index 39071dc709..6ff84d8b68 100644 --- a/framework/scenario/src/scenario/run_vm/set_state.rs +++ b/framework/scenario/src/scenario/run_vm/set_state.rs @@ -11,9 +11,6 @@ use multiversx_sc::types::{TimestampMillis, TimestampSeconds}; use super::ScenarioVMRunner; -/// Refers to the default of the "setState" scenario step. -pub const DEFAULT_CODE_METADATA: CodeMetadata = CodeMetadata::all(); - impl ScenarioVMRunner { pub fn perform_set_state(&mut self, set_state_step: &SetStateStep) { execute(&mut self.blockchain_mock.state, set_state_step); @@ -64,7 +61,7 @@ fn execute(state: &mut BlockchainState, set_state_step: &SetStateStep) { .map(|bytes_value| { CodeMetadata::try_from(&bytes_value.value).expect("invalid CodeMetadata") }) - .unwrap_or(DEFAULT_CODE_METADATA), + .unwrap_or_default(), contract_owner: account .owner .as_ref() From 61c6e2fe0223d6e34f3597c37ef51971ffc48666 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 12:19:55 +0300 Subject: [PATCH 076/195] CodeMetadata - write_display_tokens rename, more docs --- chain/core/src/types/flags/code_metadata.rs | 37 +++++++++++++------ .../src/formatter/formatter_impl_vm_core.rs | 2 +- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/chain/core/src/types/flags/code_metadata.rs b/chain/core/src/types/flags/code_metadata.rs index 8c005c156b..e190c8d33b 100644 --- a/chain/core/src/types/flags/code_metadata.rs +++ b/chain/core/src/types/flags/code_metadata.rs @@ -107,52 +107,67 @@ impl CodeMetadata { *self & CodeMetadata::GUARDED != CodeMetadata::EMPTY } + /// Serialises the metadata to its 2-byte big-endian wire representation. + /// + /// The returned array is the canonical on-chain encoding: first byte carries + /// `UPGRADEABLE`, `READABLE`, and `GUARDED`; second byte carries `PAYABLE` and `PAYABLE_BY_SC`. #[inline] pub fn to_byte_array(&self) -> [u8; 2] { self.bits().to_be_bytes() } + /// Serialises the metadata to a 2-byte big-endian `Vec`. + /// + /// Convenience wrapper around [`to_byte_array`](Self::to_byte_array) for call sites + /// that need an owned buffer. pub fn to_vec(&self) -> Vec { self.to_byte_array().to_vec() } - pub fn for_each_string_token(&self, mut f: F) { + /// Calls `f` once for each token that makes up the human-readable display of these flags. + /// + /// Tokens are the flag name strings (`"Upgradeable"`, `"Readable"`, `"Guarded"`, + /// `"Payable"`, `"PayableBySC"`) and the `"|"` separator between them. + /// If no flags are set, `f` is called once with `"Default"`. + /// + /// The flag order matches the canonical display order used in scenario JSON files. + pub fn write_display_tokens(&self, mut write: F) { let mut nothing_printed: bool = true; if self.is_upgradeable() { - f(UPGRADEABLE_STRING); + write(UPGRADEABLE_STRING); nothing_printed = false; } if self.is_readable() { if !nothing_printed { - f("|"); + write("|"); } - f(READABLE_STRING); + write(READABLE_STRING); nothing_printed = false; } if self.is_guarded() { if !nothing_printed { - f("|"); + write("|"); } - f(GUARDED_STRING); + write(GUARDED_STRING); nothing_printed = false; } if self.is_payable() { if !nothing_printed { - f("|"); + write("|"); } - f(PAYABLE_STRING); + write(PAYABLE_STRING); nothing_printed = false; } if self.is_payable_by_sc() { if !nothing_printed { - f("|"); + write("|"); } - f(PAYABLE_BY_SC_STRING); + write(PAYABLE_BY_SC_STRING); nothing_printed = false; } if nothing_printed { - f(DEFAULT_STRING); + write(DEFAULT_STRING); } } diff --git a/framework/base/src/formatter/formatter_impl_vm_core.rs b/framework/base/src/formatter/formatter_impl_vm_core.rs index 26d12294c2..1653a04b54 100644 --- a/framework/base/src/formatter/formatter_impl_vm_core.rs +++ b/framework/base/src/formatter/formatter_impl_vm_core.rs @@ -4,7 +4,7 @@ use super::{FormatByteReceiver, SCBinary, SCDisplay, SCLowerHex, hex_util}; impl SCDisplay for CodeMetadata { fn fmt(&self, f: &mut F) { - self.for_each_string_token(|token| f.append_bytes(token.as_bytes())) + self.write_display_tokens(|token| f.append_bytes(token.as_bytes())) } } From 75db2b80f57dc4c22d9f99e83b1a86e42dd5cca0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 16:35:18 +0300 Subject: [PATCH 077/195] token identifier - add missing hex check when validating; tests --- chain/core/src/token_identifier_util.rs | 8 +++---- framework/scenario/tests/token_id_test.rs | 29 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/chain/core/src/token_identifier_util.rs b/chain/core/src/token_identifier_util.rs index 55c424fbcd..2d7541e171 100644 --- a/chain/core/src/token_identifier_util.rs +++ b/chain/core/src/token_identifier_util.rs @@ -19,7 +19,6 @@ pub fn validate_token_identifier(token_id_slice: &[u8]) -> bool { return false; } - let lowercase_letter_range = b'a'..=b'z'; let uppercase_letter_range = b'A'..=b'Z'; let number_range = b'0'..=b'9'; @@ -40,13 +39,14 @@ pub fn validate_token_identifier(token_id_slice: &[u8]) -> bool { return false; } - // random chars are alphanumeric lowercase + // random chars are lowercase hex digits (the suffix is hex-encoded, so only [0-9a-f] is valid) + let hex_letter_range = b'a'..=b'f'; let random_chars = &token_id_slice[(length - ADDITIONAL_RANDOM_CHARS_LENGTH)..]; for rand_char in random_chars { - let is_lowercase_letter = lowercase_letter_range.contains(rand_char); + let is_hex_letter = hex_letter_range.contains(rand_char); let is_number = number_range.contains(rand_char); - if !is_lowercase_letter && !is_number { + if !is_hex_letter && !is_number { return false; } } diff --git a/framework/scenario/tests/token_id_test.rs b/framework/scenario/tests/token_id_test.rs index 700057ff73..1524e1efad 100644 --- a/framework/scenario/tests/token_id_test.rs +++ b/framework/scenario/tests/token_id_test.rs @@ -79,6 +79,12 @@ fn test_is_valid_esdt_identifier() { // valid ticker only numbers assert!(TokenId::::from("12345-6258d2").is_valid_esdt_identifier()); + // valid identifier with max-length ticker (10 chars) + assert!(TokenId::::from("EGLDRIDEFL-08d8ef").is_valid_esdt_identifier()); + + // valid identifier with number at end of max-length ticker + assert!(TokenId::::from("EGLDRIDEF2-08d8ef").is_valid_esdt_identifier()); + // missing dash assert!(!TokenId::::from("ALC6258d2").is_valid_esdt_identifier()); @@ -88,12 +94,35 @@ fn test_is_valid_esdt_identifier() { // lowercase ticker assert!(!TokenId::::from("alc-6258d2").is_valid_esdt_identifier()); + // lowercase char in ticker + assert!(!TokenId::::from("EGLDRIDEFl-08d8ef").is_valid_esdt_identifier()); + + // special char in ticker + assert!(!TokenId::::from("EGLDRIDEF*-08d8ef").is_valid_esdt_identifier()); + // uppercase random chars assert!(!TokenId::::from("ALC-6258D2").is_valid_esdt_identifier()); + // uppercase char in random part + assert!(!TokenId::::from("EGLDRIDEFL-08d8eF").is_valid_esdt_identifier()); + + // special char in random part + assert!(!TokenId::::from("EGLDRIDEFL-08d*ef").is_valid_esdt_identifier()); + + // non-hex lowercase letter in random part (g-z are not valid hex digits) + assert!(!TokenId::::from("ABC-ghijkl").is_valid_esdt_identifier()); + assert!(!TokenId::::from("ALC-6258g2").is_valid_esdt_identifier()); + assert!(!TokenId::::from("ALC-zzzzzz").is_valid_esdt_identifier()); + // too many random chars assert!(!TokenId::::from("ALC-6258d2ff").is_valid_esdt_identifier()); + // too many random chars (7) + assert!(!TokenId::::from("EGLDRIDEFL-08d8eff").is_valid_esdt_identifier()); + + // too few random chars (5) + assert!(!TokenId::::from("EGLDRIDEFL-08d8e").is_valid_esdt_identifier()); + // ticker too short assert!(!TokenId::::from("AL-6258d2").is_valid_esdt_identifier()); From 89bf92e932cbf2aeef10e4573c3b3cbc4f34d692 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 17:51:00 +0300 Subject: [PATCH 078/195] token identifier validation - refactor & tests --- chain/core/src/token_identifier_util.rs | 177 +++++++++++++++--- .../host/vm_hooks/vh_handler/vh_blockchain.rs | 5 +- 2 files changed, 158 insertions(+), 24 deletions(-) diff --git a/chain/core/src/token_identifier_util.rs b/chain/core/src/token_identifier_util.rs index 2d7541e171..5c9d412a6c 100644 --- a/chain/core/src/token_identifier_util.rs +++ b/chain/core/src/token_identifier_util.rs @@ -1,3 +1,16 @@ +/// Error type returned by token identifier validation functions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenIdValidationError { + /// Token identifier length is outside the allowed range. + InvalidLength, + /// Ticker contains a character that is not uppercase alphanumeric. + InvalidTickerChar, + /// The dash separator is missing or not at the expected position. + MissingOrMisplacedDash, + /// The random suffix contains a character outside `[0-9a-f]`. + InvalidRandomChar, +} + const TICKER_MIN_LENGTH: usize = 3; const TICKER_MAX_LENGTH: usize = 10; const ADDITIONAL_RANDOM_CHARS_LENGTH: usize = 6; @@ -8,52 +21,170 @@ pub const IDENTIFIER_MAX_LENGTH: usize = TICKER_MAX_LENGTH + ADDITIONAL_RANDOM_C const DASH_CHARACTER: u8 = b'-'; -/// There is a VM implementation of this very function. -/// This implementation is used for debugging mode and backwards compatibility. -/// Using the VM implementation instead of this one saves ~0.6 kB of contract code. -pub fn validate_token_identifier(token_id_slice: &[u8]) -> bool { - let length = token_id_slice.len(); - - #[allow(clippy::manual_range_contains)] - if length < IDENTIFIER_MIN_LENGTH || length > IDENTIFIER_MAX_LENGTH { - return false; - } - +/// Validates the ticker portion of a token identifier (uppercase alphanumeric). +/// +/// Expects `ticker` to be the slice before the dash. +fn validate_ticker(ticker: &[u8]) -> Result<(), TokenIdValidationError> { let uppercase_letter_range = b'A'..=b'Z'; let number_range = b'0'..=b'9'; - // ticker must be all uppercase alphanumeric - let ticker_len = get_token_ticker_len(length); - let ticker = &token_id_slice[..ticker_len]; for ticker_char in ticker { let is_uppercase_letter = uppercase_letter_range.contains(ticker_char); let is_number = number_range.contains(ticker_char); if !is_uppercase_letter && !is_number { - return false; + return Err(TokenIdValidationError::InvalidTickerChar); } } - let dash_position = ticker_len; - if token_id_slice[dash_position] != DASH_CHARACTER { - return false; - } + Ok(()) +} - // random chars are lowercase hex digits (the suffix is hex-encoded, so only [0-9a-f] is valid) +/// Validates the random suffix of a token identifier (lowercase hex digits `[0-9a-f]`). +/// +/// The suffix is hex-encoded from 3 bytes, so only `[0-9a-f]{6}` is valid. +fn validate_random_chars(random_chars: &[u8]) -> Result<(), TokenIdValidationError> { let hex_letter_range = b'a'..=b'f'; - let random_chars = &token_id_slice[(length - ADDITIONAL_RANDOM_CHARS_LENGTH)..]; + let number_range = b'0'..=b'9'; + for rand_char in random_chars { let is_hex_letter = hex_letter_range.contains(rand_char); let is_number = number_range.contains(rand_char); if !is_hex_letter && !is_number { - return false; + return Err(TokenIdValidationError::InvalidRandomChar); } } - true + Ok(()) +} + +/// There is a VM implementation of this very function. +/// This implementation is used for debugging mode and backwards compatibility. +/// Using the VM implementation instead of this one saves ~0.6 kB of contract code. +pub fn validate_token_identifier(token_id_slice: &[u8]) -> Result<(), TokenIdValidationError> { + let length = token_id_slice.len(); + + #[allow(clippy::manual_range_contains)] + if length < IDENTIFIER_MIN_LENGTH || length > IDENTIFIER_MAX_LENGTH { + return Err(TokenIdValidationError::InvalidLength); + } + + // The dash must be at the fixed position separating the ticker from the 6-char suffix. + let dash_pos = get_token_ticker_len(length); + if token_id_slice[dash_pos] != DASH_CHARACTER { + return Err(TokenIdValidationError::MissingOrMisplacedDash); + } + + validate_ticker(&token_id_slice[..dash_pos])?; + + validate_random_chars(&token_id_slice[(dash_pos + 1)..]) } pub fn get_token_ticker_len(token_id_len: usize) -> usize { token_id_len - ADDITIONAL_RANDOM_CHARS_LENGTH - 1 } + +#[cfg(test)] +mod tests { + use super::{TokenIdValidationError, validate_token_identifier}; + + fn valid(token_id: &str) { + assert_eq!(validate_token_identifier(token_id.as_bytes()), Ok(())); + } + + fn invalid(token_id: &str, expected_err: TokenIdValidationError) { + assert_eq!( + validate_token_identifier(token_id.as_bytes()), + Err(expected_err) + ); + } + + #[test] + fn test_valid_identifiers() { + valid("ALC-6258d2"); + valid("ALC123-6258d2"); + valid("12345-6258d2"); + // max-length ticker (10 chars) + valid("EGLDRIDEFL-08d8ef"); + // number at end of max-length ticker + valid("EGLDRIDEF2-08d8ef"); + } + + #[test] + fn test_invalid_length() { + // total too short: ticker 0 chars + invalid("-6258d2", TokenIdValidationError::InvalidLength); + // total too short: ticker 2 chars + invalid("AL-6258d2", TokenIdValidationError::InvalidLength); + // total too short: no dash, 9 chars + invalid("ALC6258d2", TokenIdValidationError::InvalidLength); + // total too long: ticker 11 chars + invalid("ALCCCCCCCCC-6258d2", TokenIdValidationError::InvalidLength); + // total too long: suffix 7 chars + invalid("EGLDRIDEFL-08d8eff", TokenIdValidationError::InvalidLength); + } + + #[test] + fn test_missing_or_misplaced_dash() { + // no dash at expected position (valid length, but no dash at pos 3) + invalid("ABCX6258d2", TokenIdValidationError::MissingOrMisplacedDash); + // dash too early (valid length 10, dash at pos 2, expected at pos 3) + invalid("AL-C6258d2", TokenIdValidationError::MissingOrMisplacedDash); + // dash too early (valid length 12, dash at pos 3, expected at pos 5) + invalid( + "ALC-6258d2ff", + TokenIdValidationError::MissingOrMisplacedDash, + ); + // dash too late (valid length 16, dash at pos 10, expected at pos 9) + invalid( + "EGLDRIDEFL-08d8e", + TokenIdValidationError::MissingOrMisplacedDash, + ); + } + + #[test] + fn test_invalid_ticker_char() { + // all lowercase ticker + invalid("alc-6258d2", TokenIdValidationError::InvalidTickerChar); + // lowercase char in ticker + invalid( + "EGLDRIDEFl-08d8ef", + TokenIdValidationError::InvalidTickerChar, + ); + // special char in ticker + invalid( + "EGLDRIDEF*-08d8ef", + TokenIdValidationError::InvalidTickerChar, + ); + // extra dash inside the ticker (dash at expected pos, but ticker contains a dash) + // "AL-C-6258d2" (11 chars): dash_pos=4, ticker="AL-C" → InvalidTickerChar + invalid("AL-C-6258d2", TokenIdValidationError::InvalidTickerChar); + // "ALC--6258d2" (11 chars): dash_pos=4, ticker="ALC-" → InvalidTickerChar + invalid("ALC--6258d2", TokenIdValidationError::InvalidTickerChar); + // identifier starting with a dash: dash_pos=3, ticker="-LC" → InvalidTickerChar + invalid("-LC-6258d2", TokenIdValidationError::InvalidTickerChar); + } + + #[test] + fn test_invalid_random_char() { + // uppercase char in random part + invalid("ALC-6258D2", TokenIdValidationError::InvalidRandomChar); + invalid( + "EGLDRIDEFL-08d8eF", + TokenIdValidationError::InvalidRandomChar, + ); + // special char in random part + invalid( + "EGLDRIDEFL-08d*ef", + TokenIdValidationError::InvalidRandomChar, + ); + // non-hex lowercase letters (g-z) in random part + invalid("ABC-ghijkl", TokenIdValidationError::InvalidRandomChar); + invalid("ALC-6258g2", TokenIdValidationError::InvalidRandomChar); + invalid("ALC-zzzzzz", TokenIdValidationError::InvalidRandomChar); + // dash in the random part (dash at expected pos, suffix contains a dash) + // "ALC-625-d2" (10 chars): dash_pos=3, ticker="ALC" ✓, suffix="625-d2" → InvalidRandomChar + invalid("ALC-625-d2", TokenIdValidationError::InvalidRandomChar); + } +} diff --git a/chain/vm/src/host/vm_hooks/vh_handler/vh_blockchain.rs b/chain/vm/src/host/vm_hooks/vh_handler/vh_blockchain.rs index 333b2e71a0..e8903bcec7 100644 --- a/chain/vm/src/host/vm_hooks/vh_handler/vh_blockchain.rs +++ b/chain/vm/src/host/vm_hooks/vh_handler/vh_blockchain.rs @@ -554,7 +554,10 @@ impl VMHooksHandler { let m_types = self.context.m_types_lock(); let token_id = m_types.mb_get(token_id_handle); - Ok(multiversx_chain_core::token_identifier_util::validate_token_identifier(token_id)) + Ok( + multiversx_chain_core::token_identifier_util::validate_token_identifier(token_id) + .is_ok(), + ) } #[allow(clippy::too_many_arguments)] From fd88449ca7c09cf309c7bc7531287a3391afcf2a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 5 Jun 2026 18:34:58 +0300 Subject: [PATCH 079/195] EsdtLocalRole flag fix, docs, tests --- chain/core/src/types/flags/esdt_local_role.rs | 46 +++++- .../src/types/flags/esdt_local_role_flags.rs | 28 +++- chain/core/tests/esdt_local_role_test.rs | 148 ++++++++++++++++++ 3 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 chain/core/tests/esdt_local_role_test.rs diff --git a/chain/core/src/types/flags/esdt_local_role.rs b/chain/core/src/types/flags/esdt_local_role.rs index 4feedd654e..73a1c3d34b 100644 --- a/chain/core/src/types/flags/esdt_local_role.rs +++ b/chain/core/src/types/flags/esdt_local_role.rs @@ -18,6 +18,19 @@ const ESDT_ROLE_MODIFY_CREATOR: &str = "ESDTRoleModifyCreator"; const ESDT_ROLE_NFT_RECREATE: &str = "ESDTRoleNFTRecreate"; const ESDT_ROLE_TRANSFER: &str = "ESDTTransferRole"; +/// An ESDT local role that can be granted to or revoked from an account for a specific token. +/// +/// Each role has two canonical representations: +/// - A **byte-string name** (e.g. `"ESDTRoleLocalMint"`) used in on-chain role +/// assignment, decoded via `From<&[u8]>`. +/// - A **numeric ID** (`u16`) used for `ManagedVecItem` payload encoding, +/// accessed via [`as_u16`][EsdtLocalRole::as_u16] and decoded via `From`. +/// +/// The numeric IDs and bit-flag positions correspond to the `Role*` iota constants +/// in `vmhost/vmhooks/eei_helpers.go` of `mx-chain-vm-go`. +/// +/// The declaration order matches the Go VM `iota` ordering so that the codec +/// discriminant (declaration index) aligns with [`as_u16`][EsdtLocalRole::as_u16]. #[derive(TopDecode, TopEncode, NestedDecode, NestedEncode, Clone, PartialEq, Eq, Debug, Copy)] pub enum EsdtLocalRole { None, @@ -29,13 +42,18 @@ pub enum EsdtLocalRole { NftUpdateAttributes, NftAddUri, NftRecreate, - ModifyRoyalties, ModifyCreator, + ModifyRoyalties, SetNewUri, Transfer, } impl EsdtLocalRole { + /// Returns the numeric role ID used for `ManagedVecItem` encoding and VM-hook role flags. + /// + /// Values mirror the Go VM's `Role*` iota constants (`RoleMint = 1 << 0` … + /// `RoleSetNewURI = 1 << 10`). `Transfer` (12) has no counterpart in the + /// current Go VM. This is the inverse of `From`. pub fn as_u16(&self) -> u16 { match self { Self::None => 0, @@ -54,10 +72,17 @@ impl EsdtLocalRole { } } + /// Returns the canonical byte-string name of this role (e.g. `b"ESDTRoleLocalMint"`). + /// + /// This is the format used in on-chain ESDT role grant/revoke operations. pub fn as_role_name(&self) -> &'static [u8] { self.name().as_bytes() } + /// Returns the canonical string name of this role (e.g. `"ESDTRoleLocalMint"`). + /// + /// This is the format used in on-chain ESDT role grant/revoke operations and + /// corresponds to the role-name constants in the Go VM. pub fn name(&self) -> &'static str { match self { Self::None => ESDT_ROLE_NONE, @@ -76,6 +101,9 @@ impl EsdtLocalRole { } } + /// Converts this role to its corresponding bit-flag in [`EsdtLocalRoleFlags`]. + /// + /// The bit position matches the Go VM's `Role*` iota constant for the same role. pub fn to_flag(&self) -> EsdtLocalRoleFlags { match self { Self::None => EsdtLocalRoleFlags::NONE, @@ -106,19 +134,24 @@ const ALL_ROLES: [EsdtLocalRole; 12] = [ EsdtLocalRole::NftUpdateAttributes, EsdtLocalRole::NftAddUri, EsdtLocalRole::NftRecreate, - EsdtLocalRole::ModifyRoyalties, EsdtLocalRole::ModifyCreator, + EsdtLocalRole::ModifyRoyalties, EsdtLocalRole::SetNewUri, EsdtLocalRole::Transfer, ]; impl EsdtLocalRole { + /// Iterates over all non-`None` roles in canonical numeric-ID order. pub fn iter_all() -> core::slice::Iter<'static, EsdtLocalRole> { ALL_ROLES.iter() } } impl From for EsdtLocalRole { + /// Decodes a numeric role ID into an [`EsdtLocalRole`]. + /// + /// This is the inverse of [`EsdtLocalRole::as_u16`]. Unknown values map to + /// [`EsdtLocalRole::None`]. #[inline] fn from(value: u16) -> Self { match value { @@ -130,8 +163,8 @@ impl From for EsdtLocalRole { 6 => Self::NftUpdateAttributes, 7 => Self::NftAddUri, 8 => Self::NftRecreate, - 9 => Self::ModifyRoyalties, - 10 => Self::ModifyCreator, + 9 => Self::ModifyCreator, + 10 => Self::ModifyRoyalties, 11 => Self::SetNewUri, 12 => Self::Transfer, _ => Self::None, @@ -140,6 +173,11 @@ impl From for EsdtLocalRole { } impl<'a> From<&'a [u8]> for EsdtLocalRole { + /// Decodes an ESDT role from its canonical byte-string name + /// (e.g. `b"ESDTRoleLocalMint"`). + /// + /// This is the primary decoding path for on-chain ESDT role data. Unknown + /// byte strings map to [`EsdtLocalRole::None`]. #[inline] fn from(byte_slice: &'a [u8]) -> Self { if byte_slice == ESDT_ROLE_LOCAL_MINT.as_bytes() { diff --git a/chain/core/src/types/flags/esdt_local_role_flags.rs b/chain/core/src/types/flags/esdt_local_role_flags.rs index 11f8d5fcba..da1ef103aa 100644 --- a/chain/core/src/types/flags/esdt_local_role_flags.rs +++ b/chain/core/src/types/flags/esdt_local_role_flags.rs @@ -2,7 +2,29 @@ use super::EsdtLocalRole; use bitflags::bitflags; bitflags! { - #[derive(PartialEq, Clone, Copy)] + /// Bit-flag representation of ESDT local roles. + /// + /// Each flag corresponds to a `Role*` constant defined in the Go VM at + /// `vmhost/vmhooks/eei_helpers.go` (`mx-chain-vm-go`), where the same + /// values are produced via `1 << iota` starting from `RoleMint = 1`. + /// + /// Correspondence table (Rust flag → Go constant → bit): + /// + /// | Rust flag | Go constant | Bit | + /// |------------------------|--------------------------|----| + /// | `MINT` | `RoleMint` | 0 | + /// | `BURN` | `RoleBurn` | 1 | + /// | `NFT_CREATE` | `RoleNFTCreate` | 2 | + /// | `NFT_ADD_QUANTITY` | `RoleNFTAddQuantity` | 3 | + /// | `NFT_BURN` | `RoleNFTBurn` | 4 | + /// | `NFT_UPDATE_ATTRIBUTES`| `RoleNFTUpdateAttributes`| 5 | + /// | `NFT_ADD_URI` | `RoleNFTAddURI` | 6 | + /// | `NFT_RECREATE` | `RoleNFTRecreate` | 7 | + /// | `MODIFY_CREATOR` | `RoleModifyCreator` | 8 | + /// | `MODIFY_ROYALTIES` | `RoleModifyRoyalties` | 9 | + /// | `SET_NEW_URI` | `RoleSetNewURI` | 10 | + /// | `TRANSFER` | *(not yet in Go VM)* | 11 | + #[derive(PartialEq, Clone, Copy, Debug)] pub struct EsdtLocalRoleFlags: u64 { const NONE = 0b00000000_00000000; const MINT = 0b00000000_00000001; @@ -22,10 +44,14 @@ bitflags! { } impl EsdtLocalRoleFlags { + /// Returns `true` if this flag set contains the bit corresponding to `role`. pub fn has_role(&self, role: &EsdtLocalRole) -> bool { *self & role.to_flag() != EsdtLocalRoleFlags::NONE } + /// Iterates over all [`EsdtLocalRole`] variants whose bit is set in this flag set. + /// + /// Roles are yielded in canonical numeric-ID order (see [`EsdtLocalRole::iter_all`]). pub fn iter_roles(&self) -> impl Iterator { EsdtLocalRole::iter_all().filter(move |role| self.has_role(role)) } diff --git a/chain/core/tests/esdt_local_role_test.rs b/chain/core/tests/esdt_local_role_test.rs new file mode 100644 index 0000000000..9dd401193c --- /dev/null +++ b/chain/core/tests/esdt_local_role_test.rs @@ -0,0 +1,148 @@ +use multiversx_chain_core::types::{EsdtLocalRole, EsdtLocalRoleFlags}; + +/// Every non-None role paired with its expected numeric ID, canonical name, +/// and expected flag bit. This is the single source of truth for all +/// round-trip checks below. +#[rustfmt::skip] +const ROLE_TABLE: &[(EsdtLocalRole, u16, &str, EsdtLocalRoleFlags)] = &[ + (EsdtLocalRole::Mint, 1, "ESDTRoleLocalMint", EsdtLocalRoleFlags::MINT), + (EsdtLocalRole::Burn, 2, "ESDTRoleLocalBurn", EsdtLocalRoleFlags::BURN), + (EsdtLocalRole::NftCreate, 3, "ESDTRoleNFTCreate", EsdtLocalRoleFlags::NFT_CREATE), + (EsdtLocalRole::NftAddQuantity, 4, "ESDTRoleNFTAddQuantity", EsdtLocalRoleFlags::NFT_ADD_QUANTITY), + (EsdtLocalRole::NftBurn, 5, "ESDTRoleNFTBurn", EsdtLocalRoleFlags::NFT_BURN), + (EsdtLocalRole::NftUpdateAttributes, 6, "ESDTRoleNFTUpdateAttributes",EsdtLocalRoleFlags::NFT_UPDATE_ATTRIBUTES), + (EsdtLocalRole::NftAddUri, 7, "ESDTRoleNFTAddURI", EsdtLocalRoleFlags::NFT_ADD_URI), + (EsdtLocalRole::NftRecreate, 8, "ESDTRoleNFTRecreate", EsdtLocalRoleFlags::NFT_RECREATE), + (EsdtLocalRole::ModifyCreator, 9, "ESDTRoleModifyCreator", EsdtLocalRoleFlags::MODIFY_CREATOR), + (EsdtLocalRole::ModifyRoyalties, 10, "ESDTRoleModifyRoyalties", EsdtLocalRoleFlags::MODIFY_ROYALTIES), + (EsdtLocalRole::SetNewUri, 11, "ESDTRoleSetNewURI", EsdtLocalRoleFlags::SET_NEW_URI), + (EsdtLocalRole::Transfer, 12, "ESDTTransferRole", EsdtLocalRoleFlags::TRANSFER), +]; + +#[test] +fn test_none_role() { + assert_eq!(EsdtLocalRole::None.as_u16(), 0); + assert_eq!(EsdtLocalRole::None.name(), ""); + assert_eq!(EsdtLocalRole::None.as_role_name(), b""); + assert_eq!(EsdtLocalRole::None.to_flag(), EsdtLocalRoleFlags::NONE); +} + +#[test] +fn test_as_u16() { + for (role, id, _, _) in ROLE_TABLE { + assert_eq!(role.as_u16(), *id, "{role:?}.as_u16()"); + } +} + +#[test] +fn test_from_u16_round_trip() { + for (role, id, _, _) in ROLE_TABLE { + assert_eq!(EsdtLocalRole::from(*id), *role, "from({})", id); + } +} + +#[test] +fn test_from_u16_unknown_returns_none() { + assert_eq!(EsdtLocalRole::from(0u16), EsdtLocalRole::None); + assert_eq!(EsdtLocalRole::from(13u16), EsdtLocalRole::None); + assert_eq!(EsdtLocalRole::from(u16::MAX), EsdtLocalRole::None); +} + +/// `as_u16` and `From` must be exact inverses for every defined role. +#[test] +fn test_u16_round_trip_both_directions() { + for (role, _, _, _) in ROLE_TABLE { + let id = role.as_u16(); + assert_eq!(&EsdtLocalRole::from(id), role, "round-trip for {role:?}"); + } +} + +#[test] +fn test_name_and_as_role_name() { + for (role, _, name, _) in ROLE_TABLE { + assert_eq!(role.name(), *name, "{role:?}.name()"); + assert_eq!( + role.as_role_name(), + name.as_bytes(), + "{role:?}.as_role_name()" + ); + } +} + +#[test] +fn test_from_byte_slice_round_trip() { + for (role, _, name, _) in ROLE_TABLE { + let decoded = EsdtLocalRole::from(name.as_bytes()); + assert_eq!(decoded, *role, "from(b\"{name}\")"); + } +} + +#[test] +fn test_from_byte_slice_unknown_returns_none() { + assert_eq!(EsdtLocalRole::from(b"".as_ref()), EsdtLocalRole::None); + assert_eq!( + EsdtLocalRole::from(b"unknown".as_ref()), + EsdtLocalRole::None + ); + // Ensure a near-match (extra char) does not decode as a valid role. + assert_eq!( + EsdtLocalRole::from(b"ESDTRoleLocalMintX".as_ref()), + EsdtLocalRole::None, + ); +} + +/// `name()` and `From<&[u8]>` must be exact inverses for every defined role. +#[test] +fn test_byte_slice_round_trip_both_directions() { + for (role, _, _, _) in ROLE_TABLE { + let name = role.name(); + assert_eq!( + &EsdtLocalRole::from(name.as_bytes()), + role, + "round-trip for {role:?}", + ); + } +} + +#[test] +fn test_to_flag() { + for (role, _, _, flag) in ROLE_TABLE { + assert_eq!(role.to_flag(), *flag, "{role:?}.to_flag()"); + } + assert_eq!(EsdtLocalRole::None.to_flag(), EsdtLocalRoleFlags::NONE); +} + +/// `ModifyCreator` (9) and `ModifyRoyalties` (10) are the historically +/// swapped pair — verify both directions explicitly. +#[test] +fn test_modify_creator_and_modify_royalties_not_swapped() { + assert_eq!(EsdtLocalRole::ModifyCreator.as_u16(), 9); + assert_eq!(EsdtLocalRole::ModifyRoyalties.as_u16(), 10); + assert_eq!(EsdtLocalRole::from(9u16), EsdtLocalRole::ModifyCreator); + assert_eq!(EsdtLocalRole::from(10u16), EsdtLocalRole::ModifyRoyalties); + assert_eq!( + EsdtLocalRole::from(b"ESDTRoleModifyCreator".as_ref()), + EsdtLocalRole::ModifyCreator, + ); + assert_eq!( + EsdtLocalRole::from(b"ESDTRoleModifyRoyalties".as_ref()), + EsdtLocalRole::ModifyRoyalties, + ); + assert_eq!( + EsdtLocalRole::ModifyCreator.to_flag(), + EsdtLocalRoleFlags::MODIFY_CREATOR, + ); + assert_eq!( + EsdtLocalRole::ModifyRoyalties.to_flag(), + EsdtLocalRoleFlags::MODIFY_ROYALTIES, + ); +} + +#[test] +fn test_iter_all_order_and_completeness() { + let roles: Vec = EsdtLocalRole::iter_all().copied().collect(); + assert_eq!(roles.len(), ROLE_TABLE.len()); + for (idx, (expected_role, _, _, _)) in ROLE_TABLE.iter().enumerate() { + assert_eq!(roles[idx], *expected_role, "iter_all position {idx}"); + } +} From caeb116a0c7c842bf9e4df05f7f96338a26590a2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 14 Jun 2026 21:02:05 +0300 Subject: [PATCH 080/195] doc fixes --- chain/core/src/types/flags/esdt_local_role.rs | 10 ++++++---- chain/core/tests/esdt_local_role_test.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/chain/core/src/types/flags/esdt_local_role.rs b/chain/core/src/types/flags/esdt_local_role.rs index 73a1c3d34b..3a6e0cf819 100644 --- a/chain/core/src/types/flags/esdt_local_role.rs +++ b/chain/core/src/types/flags/esdt_local_role.rs @@ -49,11 +49,13 @@ pub enum EsdtLocalRole { } impl EsdtLocalRole { - /// Returns the numeric role ID used for `ManagedVecItem` encoding and VM-hook role flags. + /// Returns the 1-based ordinal role ID used for `ManagedVecItem` payload encoding. /// - /// Values mirror the Go VM's `Role*` iota constants (`RoleMint = 1 << 0` … - /// `RoleSetNewURI = 1 << 10`). `Transfer` (12) has no counterpart in the - /// current Go VM. This is the inverse of `From`. + /// This is a sequential index (1..=12), **not** a bitflag value. The + /// corresponding bitflag in [`EsdtLocalRoleFlags`] is `1 << (id - 1)`, which + /// aligns with the Go VM's `Role*` iota ordering in + /// `vmhost/vmhooks/eei_helpers.go`. `Transfer` (12) has no counterpart in + /// the current Go VM iota. This is the inverse of `From`. pub fn as_u16(&self) -> u16 { match self { Self::None => 0, diff --git a/chain/core/tests/esdt_local_role_test.rs b/chain/core/tests/esdt_local_role_test.rs index 9dd401193c..2b11489212 100644 --- a/chain/core/tests/esdt_local_role_test.rs +++ b/chain/core/tests/esdt_local_role_test.rs @@ -1,7 +1,7 @@ use multiversx_chain_core::types::{EsdtLocalRole, EsdtLocalRoleFlags}; /// Every non-None role paired with its expected numeric ID, canonical name, -/// and expected flag bit. This is the single source of truth for all +/// and expected flag bit. This is the single source of truth for all /// round-trip checks below. #[rustfmt::skip] const ROLE_TABLE: &[(EsdtLocalRole, u16, &str, EsdtLocalRoleFlags)] = &[ From 04c27696f3c1e3b2d95709e2abd9564e28d7e735 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 15 Jun 2026 16:03:40 +0300 Subject: [PATCH 081/195] codec - trait default implementation cleanup --- data/codec/src/single/nested_de.rs | 8 +------- data/codec/src/single/nested_en.rs | 8 +------- data/codec/src/single/top_de.rs | 8 +------- data/codec/src/single/top_en.rs | 8 +------- .../scenario/src/scenario/model/value/bytes_value.rs | 7 ++++--- 5 files changed, 8 insertions(+), 31 deletions(-) diff --git a/data/codec/src/single/nested_de.rs b/data/codec/src/single/nested_de.rs index ec62a31b86..e0870c338a 100644 --- a/data/codec/src/single/nested_de.rs +++ b/data/codec/src/single/nested_de.rs @@ -17,13 +17,7 @@ pub trait NestedDecode: Sized { fn dep_decode_or_handle_err(input: &mut I, h: H) -> Result where I: NestedDecodeInput, - H: DecodeErrorHandler, - { - match Self::dep_decode(input) { - Ok(v) => Ok(v), - Err(e) => Err(h.handle_error(e)), - } - } + H: DecodeErrorHandler; /// Allows the framework to do monomorphisation of special cases where the data is of type `u8`. /// diff --git a/data/codec/src/single/nested_en.rs b/data/codec/src/single/nested_en.rs index cb193b8975..b9cc9c72b5 100644 --- a/data/codec/src/single/nested_en.rs +++ b/data/codec/src/single/nested_en.rs @@ -18,13 +18,7 @@ pub trait NestedEncode: Sized { fn dep_encode_or_handle_err(&self, dest: &mut O, h: H) -> Result<(), H::HandledErr> where O: NestedEncodeOutput, - H: EncodeErrorHandler, - { - match self.dep_encode(dest) { - Ok(()) => Ok(()), - Err(e) => Err(h.handle_error(e)), - } - } + H: EncodeErrorHandler; /// Allows the framework to do monomorphisation of special cases where the data is of type `u8`. /// diff --git a/data/codec/src/single/top_de.rs b/data/codec/src/single/top_de.rs index 921d9c50bb..01a1447f96 100644 --- a/data/codec/src/single/top_de.rs +++ b/data/codec/src/single/top_de.rs @@ -33,13 +33,7 @@ pub trait TopDecode: Sized { fn top_decode_or_handle_err(input: I, h: H) -> Result where I: TopDecodeInput, - H: DecodeErrorHandler, - { - match Self::top_decode(input) { - Ok(v) => Ok(v), - Err(e) => Err(h.handle_error(e)), - } - } + H: DecodeErrorHandler; /// Allows types to provide optimized implementations for their boxed version. /// Especially designed for byte arrays that can be transmuted directly from the input sometimes. diff --git a/data/codec/src/single/top_en.rs b/data/codec/src/single/top_en.rs index 7871cf55f9..e79a5b690b 100644 --- a/data/codec/src/single/top_en.rs +++ b/data/codec/src/single/top_en.rs @@ -20,13 +20,7 @@ pub trait TopEncode: Sized { fn top_encode_or_handle_err(&self, output: O, h: H) -> Result<(), H::HandledErr> where O: TopEncodeOutput, - H: EncodeErrorHandler, - { - match self.top_encode(output) { - Ok(()) => Ok(()), - Err(e) => Err(h.handle_error(e)), - } - } + H: EncodeErrorHandler; } pub fn top_encode_from_nested(obj: &T, output: O, h: H) -> Result<(), H::HandledErr> diff --git a/framework/scenario/src/scenario/model/value/bytes_value.rs b/framework/scenario/src/scenario/model/value/bytes_value.rs index 752df8f41c..1a8d8566c7 100644 --- a/framework/scenario/src/scenario/model/value/bytes_value.rs +++ b/framework/scenario/src/scenario/model/value/bytes_value.rs @@ -1,5 +1,5 @@ use multiversx_sc::{ - codec::{EncodeError, TopEncode, TopEncodeOutput}, + codec::{EncodeErrorHandler, TopEncode, TopEncodeOutput}, types::{AnnotatedValue, ManagedBuffer, TxCodeValue, TxEnv}, }; @@ -163,11 +163,12 @@ impl fmt::Display for BytesValue { } impl TopEncode for BytesValue { - fn top_encode(&self, output: O) -> Result<(), EncodeError> + fn top_encode_or_handle_err(&self, output: O, h: H) -> Result<(), H::HandledErr> where O: TopEncodeOutput, + H: EncodeErrorHandler, { - self.value.top_encode(output) + self.value.top_encode_or_handle_err(output, h) } } From 239b4de3d2aaa8ce9e40e3da44d047071df6a73d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 16 Jun 2026 01:16:32 +0300 Subject: [PATCH 082/195] interator config loader as trait --- .../adder/interactor/src/basic_interactor.rs | 12 +-- .../tests/basic_interactor_cs_test.rs | 30 +++--- .../forwarder-interactor/config.toml | 10 +- .../forwarder-interactor/src/config.rs | 57 ----------- .../forwarder-interactor/src/interact.rs | 96 +++++++++++-------- .../tests/interact_cs_tests.rs | 13 ++- .../tests/interact_tests.rs | 14 +-- .../payable-features/interactor/config.toml | 8 +- .../interactor/src/payable_interactor.rs | 83 ++++++++++------ .../src/payable_interactor_config.rs | 49 ---------- .../src/payable_interactor_state.rs | 50 ---------- .../tests/payable_interactor_cs_test.rs | 24 ++++- framework/snippets/src/config.rs | 4 + .../snippets/src/config/config_loader.rs | 71 ++++++++++++++ .../snippets/src/config/interactor_config.rs | 8 ++ .../src/config/interactor_config_loader.rs | 36 +++++++ .../src/interactor/interactor_base.rs | 30 +++++- 17 files changed, 330 insertions(+), 265 deletions(-) delete mode 100644 contracts/feature-tests/composability/forwarder-interactor/src/config.rs delete mode 100644 contracts/feature-tests/payable-features/interactor/src/payable_interactor_config.rs delete mode 100644 contracts/feature-tests/payable-features/interactor/src/payable_interactor_state.rs create mode 100644 framework/snippets/src/config/config_loader.rs create mode 100644 framework/snippets/src/config/interactor_config_loader.rs diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index d7891715c0..5db345096e 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -42,7 +42,7 @@ impl State { pub async fn adder_cli() { env_logger::init(); - let mut basic_interact = BasicInteractor::new().await; + let mut basic_interact = BasicInteractor::new(ConfigLoader::::new(env!("CARGO_MANIFEST_DIR"))).await; let cli = basic_interactor_cli::InteractCli::parse(); match &cli.command { @@ -73,11 +73,11 @@ pub struct BasicInteractor { } impl BasicInteractor { - pub async fn new() -> Self { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; + pub async fn new>(config_loader: L) -> Self { + let (interactor, config) = + Interactor::new_with_config(config_loader) + .await; + interactor.generate_blocks_until_all_activations().await; let state = interactor.load_state::(); BasicInteractor { interactor, diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index bb1ba31e8b..070be2ae0f 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -11,18 +11,18 @@ fn chain_simulator_config() -> Config { } } -async fn test_interactor() -> BasicInteractor { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .with_config(chain_simulator_config()) - .build() - .await; - BasicInteractor { - interactor, - config, - state: multiversx_sc_snippets::AutoSave::no_save_default(), - } -} +// async fn test_interactor() -> BasicInteractor { +// let (interactor, config) = HttpInteractorBuilder::new() +// .crate_dir(env!("CARGO_MANIFEST_DIR")) +// .with_config(chain_simulator_config()) +// .build() +// .await; +// BasicInteractor { +// interactor, +// config, +// state: multiversx_sc_snippets::AutoSave::no_save_default(), +// } +// } #[tokio::test] #[serial] @@ -76,7 +76,7 @@ async fn simulator_upgrade_test() { async fn set_state_cs_test() { let account_address = test_wallets::mike(); - let real_chain_interact = BasicInteractor::new().await; + let real_chain_interact = BasicInteractor::new(chain_simulator_config()).await; let simulator_interact = test_interactor().await; let account = real_chain_interact @@ -118,7 +118,7 @@ async fn set_state_from_file_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = BasicInteractor::new().await; + let mut real_chain_interact = BasicInteractor::new(chain_simulator_config()).await; let simulator_interact = test_interactor().await; // now we should have current mike account in the set state file @@ -162,7 +162,7 @@ async fn set_state_overwrite_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = BasicInteractor::new().await; + let mut real_chain_interact = BasicInteractor::new(chain_simulator_config()).await; let simulator_interact = test_interactor().await; // now we should have current mike and ivan accounts in the set state file diff --git a/contracts/feature-tests/composability/forwarder-interactor/config.toml b/contracts/feature-tests/composability/forwarder-interactor/config.toml index eb77c86462..0fbb6abd39 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/config.toml +++ b/contracts/feature-tests/composability/forwarder-interactor/config.toml @@ -1,7 +1,11 @@ +[connection] +chain_type = 'real' +gateway_uri = 'https://testnet-gateway.multiversx.com' + +[wallet] +test_wallet = 'alice' +# [connection] # chain_type = 'simulator' # gateway_uri = 'http://localhost:8085' -chain_type = 'real' -gateway_uri = 'https://testnet-gateway.multiversx.com' - diff --git a/contracts/feature-tests/composability/forwarder-interactor/src/config.rs b/contracts/feature-tests/composability/forwarder-interactor/src/config.rs deleted file mode 100644 index 4c4f034bb4..0000000000 --- a/contracts/feature-tests/composability/forwarder-interactor/src/config.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![allow(unused)] - -use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -/// Contract Interact configuration -#[derive(Debug, Deserialize)] -pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, -} - -impl Default for Config { - fn default() -> Self { - Self::new() - } -} - -impl Config { - // Deserializes config from file - pub fn new() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - pub fn chain_simulator_config() -> Self { - Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri - } - - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } - } -} diff --git a/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs b/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs index 95c73fd126..46ce479985 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs +++ b/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs @@ -1,18 +1,10 @@ #![allow(non_snake_case)] -mod config; mod proxy; -pub use config::Config; use multiversx_sc_snippets::imports::*; pub use proxy::Color; -use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; -const STATE_FILE: &str = "state.toml"; pub const FORWARDER_DEPLOY_INTERACTOR_TRACE_PATH: &str = "scenarios/forwarder_deploy_scenario.scen.json"; pub const FORWARDER_BUILTIN_INTERACTOR_TRACE_PATH: &str = @@ -30,7 +22,7 @@ pub async fn forwarder_cli() { let mut args = std::env::args(); let _ = args.next(); let cmd = args.next().expect("at least one argument required"); - let mut interact = ContractInteract::new(Config::new(), None).await; + let mut interact = ContractInteract::new(None).await; match cmd.as_str() { "deploy" => interact.deploy().await, "send_egld" => interact.send_egld().await, @@ -129,24 +121,29 @@ pub async fn forwarder_cli() { } } -#[derive(Debug, Default, Serialize, Deserialize)] +/// Contract Interact configuration +#[derive(Debug, serde::Deserialize)] +pub struct Config { + pub connection: ConnectionConfig, + pub wallet: WalletConfig, +} + +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection + } + + fn register_wallets(&self) -> Vec { + vec![self.wallet.wallet().clone()] + } +} + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct State { contract_address: Option, } impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - /// Sets the contract address pub fn set_address(&mut self, address: Bech32Address) { self.contract_address = Some(address); @@ -160,49 +157,64 @@ impl State { } } -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} - pub struct ContractInteract { interactor: Interactor, - pub wallet_address: Address, + pub wallet_address: Bech32Address, contract_code: BytesValue, - pub state: State, + pub state: AutoSave, } impl ContractInteract { - pub async fn new(config: Config, trace_path: Option<&str>) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .use_chain_simulator(config.use_chain_simulator()); + pub async fn new(trace_path: Option<&str>) -> Self { + let (mut interactor, config) = HttpInteractorBuilder::::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() + .await; if let Some(path) = trace_path { interactor = interactor.with_tracer(path).await; } - interactor.set_current_dir_from_workspace("forwarder-interactor"); - let wallet_address = interactor.register_wallet(test_wallets::alice()).await; + let wallet_address = config.wallet.address(); + let contract_code = BytesValue::interpret_from( + "mxsc:../forwarder/output/forwarder.mxsc.json", + &InterpreterContext::default(), + ); + let state = interactor.load_state::(); + + ContractInteract { + interactor, + wallet_address, + contract_code, + state, + } + } + + pub async fn new_with_config(config: Config, trace_path: Option<&str>) -> Self { + let (mut interactor, config) = HttpInteractorBuilder::::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(config) + .build() + .await; + + if let Some(path) = trace_path { + interactor = interactor.with_tracer(path).await; + } - // Useful in the chain simulator setting - // generate blocks until ESDTSystemSCAddress is enabled interactor.generate_blocks_until_all_activations().await; + let wallet_address = config.wallet.address(); let contract_code = BytesValue::interpret_from( "mxsc:../forwarder/output/forwarder.mxsc.json", &InterpreterContext::default(), ); + let state = AutoSave::no_save_default(); ContractInteract { interactor, wallet_address, contract_code, - state: State::load_state(), + state, } } diff --git a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs index 5e1fa29ee6..9af8c1effa 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs +++ b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs @@ -2,6 +2,17 @@ use forwarder_interact::{Config, ContractInteract}; use multiversx_sc_snippets::imports::*; use serial_test::serial; +fn chain_simulator_config() -> Config { + Config { + connection: ConnectionConfig::chain_simulator(), + wallet: WalletConfig::from_test_wallet("alice"), + } +} + +async fn cs_interactor() -> ContractInteract { + ContractInteract::new_with_config(chain_simulator_config(), None).await +} + // Simple deploy test that runs using the chain simulator configuration. // In order for this test to work, make sure that the `config.toml` file contains the chain simulator config (or choose it manually) // The chain simulator should already be installed and running before attempting to run this test. @@ -11,7 +22,7 @@ use serial_test::serial; #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn deploy_test_forwarder_cs() { - let mut interactor = ContractInteract::new(Config::chain_simulator_config(), None).await; + let mut interactor = cs_interactor().await; interactor.deploy().await; } diff --git a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_tests.rs b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_tests.rs index 74c4cdb0aa..2886f147a5 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_tests.rs +++ b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_tests.rs @@ -1,4 +1,4 @@ -use forwarder_interact::{Color, Config, ContractInteract}; +use forwarder_interact::{Color, ContractInteract}; use multiversx_sc_snippets::imports::*; const ISSUE_COST: u64 = 50000000000000000u64; @@ -9,7 +9,7 @@ const ISSUE_COST: u64 = 50000000000000000u64; #[tokio::test] #[ignore = "run on demand, relies on real blockchain state"] async fn deploy_test_forwarder() { - let mut interactor = ContractInteract::new(Config::new(), None).await; + let mut interactor = ContractInteract::new(None).await; interactor.deploy().await; } @@ -17,7 +17,7 @@ async fn deploy_test_forwarder() { #[tokio::test] #[ignore = "run on demand"] async fn builtin_func_tokens_test() { - let mut interact = ContractInteract::new(Config::new(), None).await; + let mut interact = ContractInteract::new(None).await; // deploy forwarder interact.deploy().await; @@ -172,7 +172,7 @@ async fn builtin_func_tokens_test() { #[tokio::test] #[ignore = "run on demand"] async fn change_to_dynamic_test() { - let mut interact = ContractInteract::new(Config::new(), None).await; + let mut interact = ContractInteract::new(None).await; // deploy forwarder interact.deploy().await; @@ -226,7 +226,7 @@ async fn change_to_dynamic_test() { #[tokio::test] #[ignore = "run on demand"] async fn update_token_test() { - let mut interact = ContractInteract::new(Config::new(), None).await; + let mut interact = ContractInteract::new(None).await; // deploy forwarder interact.deploy().await; @@ -252,12 +252,12 @@ async fn update_token_test() { #[tokio::test] #[ignore = "run on demand"] async fn modify_creator() { - let mut interact = ContractInteract::new(Config::new(), None).await; + let mut interact = ContractInteract::new(None).await; // deploy forwarder interact.deploy().await; - let wallet_address = interact.wallet_address.clone(); + let wallet_address = interact.wallet_address.to_address(); let sc_address = interact.state.current_address().clone(); // issue dynamic NFT diff --git a/contracts/feature-tests/payable-features/interactor/config.toml b/contracts/feature-tests/payable-features/interactor/config.toml index 4a108d868b..04f61527c3 100644 --- a/contracts/feature-tests/payable-features/interactor/config.toml +++ b/contracts/feature-tests/payable-features/interactor/config.toml @@ -1,6 +1,12 @@ - +[connection] # chain_type = 'simulator' # gateway_uri = 'http://localhost:8085' chain_type = 'real' gateway_uri = 'https://testnet-gateway.multiversx.com' + +[owner] +test_wallet = 'heidi' + +[wallet] +test_wallet = 'ivan' diff --git a/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs b/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs index 673cd847ef..bfbd939be9 100644 --- a/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs +++ b/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs @@ -1,22 +1,49 @@ mod payable_interactor_cli; -mod payable_interactor_config; -mod payable_interactor_state; use clap::Parser; +use multiversx_sc_snippets::imports::*; use payable_features::payable_features_proxy; -pub use payable_interactor_config::Config; -use payable_interactor_state::State; +use serde::{Deserialize, Serialize}; + +/// Payable Features Interact configuration +#[derive(Debug, Deserialize)] +pub struct Config { + pub connection: ConnectionConfig, + pub owner: WalletConfig, + pub wallet: WalletConfig, +} -use multiversx_sc_snippets::imports::*; +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection + } + + fn register_wallets(&self) -> Vec { + vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] + } +} + +/// Payable Features Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + pub payable_features_address: Option, +} + +impl State { + /// Returns the payable features contract + pub fn current_payable_features_address(&self) -> &Bech32Address { + self.payable_features_address + .as_ref() + .expect("no known payable features contract, deploy first") + } +} const CODE_PATH: MxscPath = MxscPath::new("../output/payable-features.mxsc.json"); pub async fn payable_features_cli() { env_logger::init(); - let config = Config::load_config(); - - let mut payable_interact = PayableInteract::new(config).await; + let mut payable_interact = PayableInteract::new().await; let cli = payable_interactor_cli::InteractCli::parse(); match &cli.command { @@ -37,38 +64,30 @@ pub async fn payable_features_cli() { pub struct PayableInteract { pub interactor: Interactor, - pub sc_owner_address: Bech32Address, - pub wallet_address: Bech32Address, - pub state: State, + pub config: Config, + pub state: AutoSave, } impl PayableInteract { - pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .use_chain_simulator(config.use_chain_simulator()); - - interactor - .set_current_dir_from_workspace("contracts/feature-tests/payable-features/interactor"); - - let sc_owner_address = interactor.register_wallet(test_wallets::heidi()).await; - let wallet_address = interactor.register_wallet(test_wallets::ivan()).await; - - interactor.generate_blocks(30u64).await.unwrap(); - + pub async fn new() -> Self { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .build() + .await; + let state = interactor.load_state::(); PayableInteract { interactor, - sc_owner_address: sc_owner_address.into(), - wallet_address: wallet_address.into(), - state: State::load_state(), + config, + state, } } pub async fn deploy(&mut self) { + let owner_address = self.config.owner.address(); let new_address = self .interactor .tx() - .from(&self.sc_owner_address.clone()) + .from(&owner_address.clone()) .gas(30_000_000) .typed(payable_features_proxy::PayableFeaturesProxy) .init() @@ -79,17 +98,18 @@ impl PayableInteract { .await; println!("new address: {new_address}"); - self.state.set_payable_features_address(new_address); + self.state.payable_features_address = Some(new_address); } pub async fn check_multi_transfer_only_egld_transfer(&mut self) { + let wallet_address = self.config.wallet.address(); let mut payment = MultiEgldOrEsdtPayment::new(); payment.push(EgldOrEsdtTokenPayment::egld_payment(1_0000u64.into())); let result = self .interactor .tx() - .from(&self.wallet_address) + .from(&wallet_address) .to(self.state.current_payable_features_address()) .gas(6_000_000u64) .typed(payable_features_proxy::PayableFeaturesProxy) @@ -103,6 +123,7 @@ impl PayableInteract { } pub async fn check_all_transfers(&mut self) { + let wallet_address = self.config.wallet.address(); let mut payment = MultiEgldOrEsdtPayment::new(); payment.push(EgldOrEsdtTokenPayment::egld_payment(1_0000u64.into())); payment.push(EgldOrEsdtTokenPayment::egld_payment(2_0000u64.into())); @@ -110,7 +131,7 @@ impl PayableInteract { let result = self .interactor .tx() - .from(&self.wallet_address) + .from(&wallet_address) .to(self.state.current_payable_features_address()) .gas(6_000_000u64) .typed(payable_features_proxy::PayableFeaturesProxy) diff --git a/contracts/feature-tests/payable-features/interactor/src/payable_interactor_config.rs b/contracts/feature-tests/payable-features/interactor/src/payable_interactor_config.rs deleted file mode 100644 index bd19da629d..0000000000 --- a/contracts/feature-tests/payable-features/interactor/src/payable_interactor_config.rs +++ /dev/null @@ -1,49 +0,0 @@ -use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -/// Adder Interact configuration -#[derive(Debug, Deserialize)] -pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, -} - -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - pub fn chain_simulator_config() -> Self { - Config { - gateway_uri: "http://localhost:8085".to_owned(), - chain_type: ChainType::Simulator, - } - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri - } - - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } - } -} diff --git a/contracts/feature-tests/payable-features/interactor/src/payable_interactor_state.rs b/contracts/feature-tests/payable-features/interactor/src/payable_interactor_state.rs deleted file mode 100644 index c5ed95f47d..0000000000 --- a/contracts/feature-tests/payable-features/interactor/src/payable_interactor_state.rs +++ /dev/null @@ -1,50 +0,0 @@ -use multiversx_sc_snippets::imports::*; -use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; - -/// State file -const STATE_FILE: &str = "state.toml"; - -/// Payable Features Interact state -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct State { - payable_features_address: Option, -} - -impl State { - // Deserializes state from file - pub fn load_state() -> Self { - if Path::new(STATE_FILE).exists() { - let mut file = std::fs::File::open(STATE_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } else { - Self::default() - } - } - - /// Sets the payable features address - pub fn set_payable_features_address(&mut self, address: Bech32Address) { - self.payable_features_address = Some(address); - } - - /// Returns the payable features contract - pub fn current_payable_features_address(&self) -> &Bech32Address { - self.payable_features_address - .as_ref() - .expect("no known payable features contract, deploy first") - } -} - -impl Drop for State { - // Serializes state to file - fn drop(&mut self) { - let mut file = std::fs::File::create(STATE_FILE).unwrap(); - file.write_all(toml::to_string(self).unwrap().as_bytes()) - .unwrap(); - } -} diff --git a/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs b/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs index 87a02a511b..9140d2f004 100644 --- a/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs +++ b/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs @@ -1,11 +1,33 @@ +use multiversx_sc_snippets::imports::*; use payable_interactor::{Config, PayableInteract}; use serial_test::serial; +fn chain_simulator_config() -> Config { + Config { + connection: ConnectionConfig::chain_simulator(), + owner: WalletConfig::from_test_wallet("mike"), + wallet: WalletConfig::from_test_wallet("ivan"), + } +} + +async fn cs_interactor() -> PayableInteract { + let (interactor, config) = HttpInteractorBuilder::new() + .crate_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(chain_simulator_config()) + .build() + .await; + PayableInteract { + interactor, + config, + state: multiversx_sc_snippets::AutoSave::no_save_default(), + } +} + #[tokio::test] #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn payable_interactor_test() { - let mut payable_interact = PayableInteract::new(Config::chain_simulator_config()).await; + let mut payable_interact = cs_interactor().await; payable_interact.deploy().await; diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs index 9de8c5fed2..85ef49245b 100644 --- a/framework/snippets/src/config.rs +++ b/framework/snippets/src/config.rs @@ -1,9 +1,13 @@ mod auto_save; +mod config_loader; mod connection_config; mod interactor_config; +mod interactor_config_loader; mod wallet_config; pub use auto_save::*; +pub use config_loader::*; pub use connection_config::*; pub use interactor_config::*; +pub use interactor_config_loader::*; pub use wallet_config::*; diff --git a/framework/snippets/src/config/config_loader.rs b/framework/snippets/src/config/config_loader.rs new file mode 100644 index 0000000000..a1370fdc72 --- /dev/null +++ b/framework/snippets/src/config/config_loader.rs @@ -0,0 +1,71 @@ +use std::{io::Read, path::PathBuf}; + +use serde::de::DeserializeOwned; + +use super::{InteractorConfig, InteractorConfigLoader}; + +const DEFAULT_CONFIG_FILE: &str = "config.toml"; + +/// An [`InteractorConfigLoader`] that lazily loads a typed `C` config +/// from a TOML file on first use. +/// +/// # Example +/// +/// ```rust,ignore +/// let config = ConfigLoader::::new(env!("CARGO_MANIFEST_DIR")); +/// let interactor = InteractorBase::new_with_config(config).await; +/// ``` +pub struct ConfigLoader { + /// Directory that contains the config file (and where `state.toml` will be written). + crate_dir: PathBuf, + + /// Config file name relative to `crate_dir`. Default: `"config.toml"`. + config_file: String, + + _phantom: std::marker::PhantomData, +} + +impl ConfigLoader { + /// Creates a new loader pointing at `crate_dir`, using the default file name + /// `"config.toml"`. + pub fn new(crate_dir: impl Into) -> Self { + ConfigLoader { + crate_dir: crate_dir.into(), + config_file: DEFAULT_CONFIG_FILE.to_owned(), + _phantom: std::marker::PhantomData, + } + } + + /// Overrides the config file name (relative to `crate_dir`). + pub fn config_file(mut self, name: impl Into) -> Self { + self.config_file = name.into(); + self + } +} + +impl ConfigLoader { + /// Returns a reference to the loaded config, loading from disk on the first call. + fn get(&self) -> C { + let path = self.crate_dir.join(&self.config_file); + let mut file = std::fs::File::open(&path) + .unwrap_or_else(|e| panic!("cannot open {}: {e}", path.display())); + let mut content = String::new(); + file.read_to_string(&mut content) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); + toml::from_str(&content).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display())) + } +} + +/// `ConfigLoader` overrides `current_dir` and `resolve_config` to return the directory +/// and loaded config from construction, rather than the defaults from the blanket impl. +impl InteractorConfigLoader for ConfigLoader { + type LoadedConfig = C; + + fn resolve_config(self) -> C { + self.get() + } + + fn current_dir(&self) -> PathBuf { + self.crate_dir.clone() + } +} diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs index a396f5a5fe..5293bb529a 100644 --- a/framework/snippets/src/config/interactor_config.rs +++ b/framework/snippets/src/config/interactor_config.rs @@ -19,3 +19,11 @@ pub trait InteractorConfig { Vec::new() } } + +/// A bare [`ConnectionConfig`] can be used directly as an [`InteractorConfig`] +/// when no wallets or custom directory are needed. +impl InteractorConfig for ConnectionConfig { + fn connection(&self) -> &ConnectionConfig { + self + } +} diff --git a/framework/snippets/src/config/interactor_config_loader.rs b/framework/snippets/src/config/interactor_config_loader.rs new file mode 100644 index 0000000000..55bcf7a922 --- /dev/null +++ b/framework/snippets/src/config/interactor_config_loader.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use super::InteractorConfig; + +/// Extension of [`InteractorConfig`] that also knows where to find config and state files. +/// +/// Implement this when you need to control the directory used for `config.toml` / `state.toml` +/// resolution (e.g. by returning `env!("CARGO_MANIFEST_DIR")`). +/// +/// A blanket implementation is provided for every `T: InteractorConfig`, so all existing +/// configs work as a loader with the default `"."` directory. +pub trait InteractorConfigLoader { + /// The concrete [`InteractorConfig`] that this loader resolves to. + type LoadedConfig: InteractorConfig; + + /// Resolves and returns a reference to the loaded config. + fn resolve_config(self) -> Self::LoadedConfig; + + /// Returns the directory used for config / state file resolution. + /// + /// Defaults to `"."` (current working directory). + /// Override this to point at the crate's manifest directory, e.g. + /// `PathBuf::from(env!("CARGO_MANIFEST_DIR"))`. + fn current_dir(&self) -> PathBuf { + PathBuf::from(".") + } +} + +/// Every `InteractorConfig` is trivially its own loader (resolved config = Self). +impl InteractorConfigLoader for T { + type LoadedConfig = Self; + + fn resolve_config(self) -> Self { + self + } +} diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index cde3dc3816..0e2d50225e 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -1,4 +1,4 @@ -use crate::sdk::{data::network_config::NetworkConfig, wallet::Wallet}; +use crate::{InteractorConfigLoader, sdk::{data::network_config::NetworkConfig, wallet::Wallet}}; use multiversx_sc_scenario::{ imports::{Bech32Address, ScenarioRunner}, mandos_system::{run_list::ScenarioRunnerList, run_trace::ScenarioTraceFile}, @@ -19,7 +19,7 @@ use std::{ time::Duration, }; -use crate::{Sender, account_tool::retrieve_account_as_scenario_set_state}; +use crate::{Sender, account_tool::retrieve_account_as_scenario_set_state, config::InteractorConfig}; pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; pub const INTERACTOR_SET_STATE_PATH: &str = "set_state.json"; @@ -69,6 +69,32 @@ where } } + /// Constructs an interactor from a typed config. + /// + /// 1. Connects to the gateway described by [`InteractorConfig::connection`]. + /// 2. Sets [`InteractorBase::current_dir`] from [`InteractorConfig::current_dir`]. + /// 3. Registers all wallets returned by [`InteractorConfig::register_wallets`]. + /// + /// Block generation (for the chain simulator) is **not** performed here; call + /// [`crate::InteractorBase::generate_blocks`] or + /// [`crate::InteractorBase::generate_blocks_until_all_activations`] afterwards if needed. + pub async fn new_with_config(config: L) -> (Self, L::LoadedConfig) { + let current_dir = config.current_dir(); + let loaded = config.resolve_config(); + let conn = loaded.connection(); + let gateway_uri = conn.gateway_uri().to_owned(); + let use_chain_simulator = conn.use_chain_simulator(); + let wallets = loaded.register_wallets(); + let mut interactor = Self::new(&gateway_uri) + .await + .use_chain_simulator(use_chain_simulator); + interactor.current_dir = current_dir; + for wallet in wallets { + interactor.register_wallet(wallet).await; + } + (interactor, loaded) + } + pub fn use_chain_simulator(mut self, use_chain_simulator: bool) -> Self { self.use_chain_simulator = use_chain_simulator; self From 181b5198cb9359f66069f316840b57e5245cf654 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 22 Jun 2026 16:06:09 +0300 Subject: [PATCH 083/195] interactor config loader as enum --- .../adder/interactor/src/basic_interactor.rs | 11 +- .../tests/basic_interactor_cs_test.rs | 31 ++--- framework/snippets/src/config.rs | 2 - .../snippets/src/config/config_loader.rs | 71 ------------ .../src/config/interactor_config_loader.rs | 109 ++++++++++++++---- .../src/interactor/interactor_base.rs | 45 ++++++-- 6 files changed, 143 insertions(+), 126 deletions(-) delete mode 100644 framework/snippets/src/config/config_loader.rs diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index 5db345096e..9c072debf8 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -4,6 +4,7 @@ use adder::adder_proxy; use clap::Parser; use multiversx_sc_snippets::imports::*; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; /// Adder Interact configuration #[derive(Debug, Deserialize)] @@ -42,7 +43,9 @@ impl State { pub async fn adder_cli() { env_logger::init(); - let mut basic_interact = BasicInteractor::new(ConfigLoader::::new(env!("CARGO_MANIFEST_DIR"))).await; + let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml"); + let mut basic_interact = + BasicInteractor::new(InteractorConfigLoader::FromFile(config_path)).await; let cli = basic_interactor_cli::InteractCli::parse(); match &cli.command { @@ -73,10 +76,8 @@ pub struct BasicInteractor { } impl BasicInteractor { - pub async fn new>(config_loader: L) -> Self { - let (interactor, config) = - Interactor::new_with_config(config_loader) - .await; + pub async fn new(config_loader: InteractorConfigLoader) -> Self { + let (interactor, config) = Interactor::new_with_config(config_loader).await; interactor.generate_blocks_until_all_activations().await; let state = interactor.load_state::(); BasicInteractor { diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index 070be2ae0f..f1f6fe3f99 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -11,18 +11,16 @@ fn chain_simulator_config() -> Config { } } -// async fn test_interactor() -> BasicInteractor { -// let (interactor, config) = HttpInteractorBuilder::new() -// .crate_dir(env!("CARGO_MANIFEST_DIR")) -// .with_config(chain_simulator_config()) -// .build() -// .await; -// BasicInteractor { -// interactor, -// config, -// state: multiversx_sc_snippets::AutoSave::no_save_default(), -// } -// } +async fn test_interactor() -> BasicInteractor { + let (interactor, config) = + Interactor::new_with_config(InteractorConfigLoader::Direct(chain_simulator_config())).await; + interactor.generate_blocks_until_all_activations().await; + BasicInteractor { + interactor, + config, + state: multiversx_sc_snippets::AutoSave::no_save_default(), + } +} #[tokio::test] #[serial] @@ -76,7 +74,8 @@ async fn simulator_upgrade_test() { async fn set_state_cs_test() { let account_address = test_wallets::mike(); - let real_chain_interact = BasicInteractor::new(chain_simulator_config()).await; + let real_chain_interact = + BasicInteractor::new(InteractorConfigLoader::Direct(chain_simulator_config())).await; let simulator_interact = test_interactor().await; let account = real_chain_interact @@ -118,7 +117,8 @@ async fn set_state_from_file_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = BasicInteractor::new(chain_simulator_config()).await; + let mut real_chain_interact = + BasicInteractor::new(InteractorConfigLoader::Direct(chain_simulator_config())).await; let simulator_interact = test_interactor().await; // now we should have current mike account in the set state file @@ -162,7 +162,8 @@ async fn set_state_overwrite_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = BasicInteractor::new(chain_simulator_config()).await; + let mut real_chain_interact = + BasicInteractor::new(InteractorConfigLoader::Direct(chain_simulator_config())).await; let simulator_interact = test_interactor().await; // now we should have current mike and ivan accounts in the set state file diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs index 85ef49245b..423d3cb85b 100644 --- a/framework/snippets/src/config.rs +++ b/framework/snippets/src/config.rs @@ -1,12 +1,10 @@ mod auto_save; -mod config_loader; mod connection_config; mod interactor_config; mod interactor_config_loader; mod wallet_config; pub use auto_save::*; -pub use config_loader::*; pub use connection_config::*; pub use interactor_config::*; pub use interactor_config_loader::*; diff --git a/framework/snippets/src/config/config_loader.rs b/framework/snippets/src/config/config_loader.rs deleted file mode 100644 index a1370fdc72..0000000000 --- a/framework/snippets/src/config/config_loader.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::{io::Read, path::PathBuf}; - -use serde::de::DeserializeOwned; - -use super::{InteractorConfig, InteractorConfigLoader}; - -const DEFAULT_CONFIG_FILE: &str = "config.toml"; - -/// An [`InteractorConfigLoader`] that lazily loads a typed `C` config -/// from a TOML file on first use. -/// -/// # Example -/// -/// ```rust,ignore -/// let config = ConfigLoader::::new(env!("CARGO_MANIFEST_DIR")); -/// let interactor = InteractorBase::new_with_config(config).await; -/// ``` -pub struct ConfigLoader { - /// Directory that contains the config file (and where `state.toml` will be written). - crate_dir: PathBuf, - - /// Config file name relative to `crate_dir`. Default: `"config.toml"`. - config_file: String, - - _phantom: std::marker::PhantomData, -} - -impl ConfigLoader { - /// Creates a new loader pointing at `crate_dir`, using the default file name - /// `"config.toml"`. - pub fn new(crate_dir: impl Into) -> Self { - ConfigLoader { - crate_dir: crate_dir.into(), - config_file: DEFAULT_CONFIG_FILE.to_owned(), - _phantom: std::marker::PhantomData, - } - } - - /// Overrides the config file name (relative to `crate_dir`). - pub fn config_file(mut self, name: impl Into) -> Self { - self.config_file = name.into(); - self - } -} - -impl ConfigLoader { - /// Returns a reference to the loaded config, loading from disk on the first call. - fn get(&self) -> C { - let path = self.crate_dir.join(&self.config_file); - let mut file = std::fs::File::open(&path) - .unwrap_or_else(|e| panic!("cannot open {}: {e}", path.display())); - let mut content = String::new(); - file.read_to_string(&mut content) - .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); - toml::from_str(&content).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display())) - } -} - -/// `ConfigLoader` overrides `current_dir` and `resolve_config` to return the directory -/// and loaded config from construction, rather than the defaults from the blanket impl. -impl InteractorConfigLoader for ConfigLoader { - type LoadedConfig = C; - - fn resolve_config(self) -> C { - self.get() - } - - fn current_dir(&self) -> PathBuf { - self.crate_dir.clone() - } -} diff --git a/framework/snippets/src/config/interactor_config_loader.rs b/framework/snippets/src/config/interactor_config_loader.rs index 55bcf7a922..84b707b68b 100644 --- a/framework/snippets/src/config/interactor_config_loader.rs +++ b/framework/snippets/src/config/interactor_config_loader.rs @@ -1,36 +1,97 @@ use std::path::PathBuf; +use serde::de::DeserializeOwned; + use super::InteractorConfig; -/// Extension of [`InteractorConfig`] that also knows where to find config and state files. +const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; + +/// Runtime config source used by [`crate::InteractorBase::new_with_config`]. /// -/// Implement this when you need to control the directory used for `config.toml` / `state.toml` -/// resolution (e.g. by returning `env!("CARGO_MANIFEST_DIR")`). +/// This enum is intended for call sites that want a lightweight API: +/// either pass a fully constructed config (`Direct`) or pass a path to a +/// TOML file to be loaded on demand (`FromFile`), or pass a directory and use +/// the conventional `config.toml` name (`FromDir`). /// -/// A blanket implementation is provided for every `T: InteractorConfig`, so all existing -/// configs work as a loader with the default `"."` directory. -pub trait InteractorConfigLoader { - /// The concrete [`InteractorConfig`] that this loader resolves to. - type LoadedConfig: InteractorConfig; - - /// Resolves and returns a reference to the loaded config. - fn resolve_config(self) -> Self::LoadedConfig; - - /// Returns the directory used for config / state file resolution. +/// # Typical usage +/// +/// ```rust,ignore +/// // Runtime file path selection. +/// let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml"); +/// let (interactor, cfg) = Interactor::new_with_config( +/// InteractorConfigLoader::::FromFile(path), +/// ) +/// .await; +/// +/// // Or use an already built config value. +/// let cfg = MyConfig { /* ... */ }; +/// let (interactor, cfg) = Interactor::new_with_config( +/// InteractorConfigLoader::Direct(cfg), +/// ) +/// .await; +/// ``` +/// +/// # Current directory behavior +/// +/// - `Direct` uses `"."` as current dir. +/// - `FromFile(path)` uses `path.parent()` (or `"."` if there is no parent). +/// - `FromDir(path)` uses `path` as current dir. +pub enum InteractorConfigLoader { + /// Load `C` from a TOML file path at runtime. /// - /// Defaults to `"."` (current working directory). - /// Override this to point at the crate's manifest directory, e.g. - /// `PathBuf::from(env!("CARGO_MANIFEST_DIR"))`. - fn current_dir(&self) -> PathBuf { - PathBuf::from(".") - } + /// The file is deserialized with `toml::from_str` and must match `C`. + FromFile(PathBuf), + /// Load `C` from `config.toml` in the provided directory. + FromDir(PathBuf), + /// Use an already constructed config value. + /// + /// Useful for tests and programmatic overrides. + Direct(C), } -/// Every `InteractorConfig` is trivially its own loader (resolved config = Self). -impl InteractorConfigLoader for T { - type LoadedConfig = Self; +impl InteractorConfigLoader +where + C: InteractorConfig + DeserializeOwned, +{ + fn load_toml_config(config_path: &PathBuf) -> C { + let mut file = std::fs::File::open(config_path) + .unwrap_or_else(|e| panic!("cannot open {}: {e}", config_path.display())); + let mut content = String::new(); + use std::io::Read; + file.read_to_string(&mut content) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", config_path.display())); + toml::from_str(&content) + .unwrap_or_else(|e| panic!("cannot parse {}: {e}", config_path.display())) + } - fn resolve_config(self) -> Self { - self + /// Resolves both the config directory and the loaded config in one step. + /// + /// This is the central resolver used by enum-based interactor construction. + /// + /// # Panics + /// + /// Panics when `FromFile` is used and: + /// - the file cannot be opened, + /// - the file cannot be read, + /// - or TOML deserialization into `C` fails. + /// + /// Panic messages include the offending file path for easier diagnostics. + pub fn resolve_with_current_dir(self) -> (PathBuf, C) { + match self { + InteractorConfigLoader::Direct(config) => (PathBuf::from("."), config), + InteractorConfigLoader::FromFile(config_path) => { + let current_dir = config_path + .parent() + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")); + let config = Self::load_toml_config(&config_path); + (current_dir, config) + } + InteractorConfigLoader::FromDir(dir_path) => { + let config_path = dir_path.join(DEFAULT_CONFIG_FILE_NAME); + let config = Self::load_toml_config(&config_path); + (dir_path, config) + } + } } } diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 0e2d50225e..f0c1fde8c6 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -1,4 +1,7 @@ -use crate::{InteractorConfigLoader, sdk::{data::network_config::NetworkConfig, wallet::Wallet}}; +use crate::{ + config::InteractorConfigLoader, + sdk::{data::network_config::NetworkConfig, wallet::Wallet}, +}; use multiversx_sc_scenario::{ imports::{Bech32Address, ScenarioRunner}, mandos_system::{run_list::ScenarioRunnerList, run_trace::ScenarioTraceFile}, @@ -19,7 +22,9 @@ use std::{ time::Duration, }; -use crate::{Sender, account_tool::retrieve_account_as_scenario_set_state, config::InteractorConfig}; +use crate::{ + Sender, account_tool::retrieve_account_as_scenario_set_state, config::InteractorConfig, +}; pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; pub const INTERACTOR_SET_STATE_PATH: &str = "set_state.json"; @@ -69,18 +74,40 @@ where } } - /// Constructs an interactor from a typed config. + /// Constructs an interactor from an enum config source. + /// + /// It accepts [`crate::config::InteractorConfigLoader`] so callers can choose + /// config source at runtime: + /// + /// - [`crate::config::InteractorConfigLoader::Direct`]: use an already-built config. + /// - [`crate::config::InteractorConfigLoader::FromFile`]: load config from a TOML file path. + /// - [`crate::config::InteractorConfigLoader::FromDir`]: load `config.toml` from a directory. + /// + /// Behavior: + /// + /// 1. Resolve `(current_dir, config)` from the enum source. + /// 2. Connect to `config.connection().gateway_uri()`. + /// 3. Apply `use_chain_simulator(config.connection().use_chain_simulator())`. + /// 4. Set [`InteractorBase::current_dir`] to the resolved directory. + /// 5. Register all wallets from `config.register_wallets()`. /// - /// 1. Connects to the gateway described by [`InteractorConfig::connection`]. - /// 2. Sets [`InteractorBase::current_dir`] from [`InteractorConfig::current_dir`]. - /// 3. Registers all wallets returned by [`InteractorConfig::register_wallets`]. + /// Returns both the constructed interactor and the resolved config `(Self, C)`. /// /// Block generation (for the chain simulator) is **not** performed here; call /// [`crate::InteractorBase::generate_blocks`] or /// [`crate::InteractorBase::generate_blocks_until_all_activations`] afterwards if needed. - pub async fn new_with_config(config: L) -> (Self, L::LoadedConfig) { - let current_dir = config.current_dir(); - let loaded = config.resolve_config(); + /// + /// # Panics + /// + /// May panic when loading config from disk (`FromFile` / `FromDir`) if the file + /// cannot be opened/read or parsed as TOML into `C`. Panic messages include file + /// paths for easier diagnostics. + pub async fn new_with_config(config_source: InteractorConfigLoader) -> (Self, C) + where + C: InteractorConfig + serde::de::DeserializeOwned, + { + let (current_dir, loaded) = config_source.resolve_with_current_dir(); + let conn = loaded.connection(); let gateway_uri = conn.gateway_uri().to_owned(); let use_chain_simulator = conn.use_chain_simulator(); From 46190c0cff929d8cf7972d670f973fb1964f5c62 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 23 Jun 2026 04:17:58 +0300 Subject: [PATCH 084/195] interactor can be uninitialized, new init API, removed config loader --- .../adder/interactor/src/basic_interactor.rs | 11 +- .../tests/basic_interactor_cs_test.rs | 27 ++- .../ping-pong-egld/dapp/src/requests/query.rs | 2 +- framework/snippets/src/config.rs | 2 - .../snippets/src/config/interactor_config.rs | 16 ++ .../src/config/interactor_config_loader.rs | 97 -------- .../src/interactor/interactor_base.rs | 212 +++++++++++++----- .../interactor/interactor_chain_simulator.rs | 16 +- .../interactor_scenario/interactor_sc_call.rs | 10 +- .../interactor_sc_deploy.rs | 12 +- .../interactor_transfer.rs | 6 +- .../interactor_vm_query.rs | 4 +- .../src/interactor/interactor_sender.rs | 8 +- .../src/multi/interactor_multi_sc_process.rs | 4 +- 14 files changed, 223 insertions(+), 204 deletions(-) delete mode 100644 framework/snippets/src/config/interactor_config_loader.rs diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index 9c072debf8..458969b245 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -4,7 +4,6 @@ use adder::adder_proxy; use clap::Parser; use multiversx_sc_snippets::imports::*; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; /// Adder Interact configuration #[derive(Debug, Deserialize)] @@ -43,9 +42,7 @@ impl State { pub async fn adder_cli() { env_logger::init(); - let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml"); - let mut basic_interact = - BasicInteractor::new(InteractorConfigLoader::FromFile(config_path)).await; + let mut basic_interact = BasicInteractor::from_config().await; let cli = basic_interactor_cli::InteractCli::parse(); match &cli.command { @@ -76,9 +73,9 @@ pub struct BasicInteractor { } impl BasicInteractor { - pub async fn new(config_loader: InteractorConfigLoader) -> Self { - let (interactor, config) = Interactor::new_with_config(config_loader).await; - interactor.generate_blocks_until_all_activations().await; + pub async fn from_config() -> Self { + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); BasicInteractor { interactor, diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index f1f6fe3f99..6aab6070a8 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -12,9 +12,21 @@ fn chain_simulator_config() -> Config { } async fn test_interactor() -> BasicInteractor { - let (interactor, config) = - Interactor::new_with_config(InteractorConfigLoader::Direct(chain_simulator_config())).await; - interactor.generate_blocks_until_all_activations().await; + let config = chain_simulator_config(); + let interactor = Interactor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&config) + .await; + BasicInteractor { + interactor, + config, + state: multiversx_sc_snippets::AutoSave::no_save_default(), + } +} + +async fn real_chain_reader_interactor() -> BasicInteractor { + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; BasicInteractor { interactor, config, @@ -74,8 +86,7 @@ async fn simulator_upgrade_test() { async fn set_state_cs_test() { let account_address = test_wallets::mike(); - let real_chain_interact = - BasicInteractor::new(InteractorConfigLoader::Direct(chain_simulator_config())).await; + let real_chain_interact = real_chain_reader_interactor().await; let simulator_interact = test_interactor().await; let account = real_chain_interact @@ -117,8 +128,7 @@ async fn set_state_from_file_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = - BasicInteractor::new(InteractorConfigLoader::Direct(chain_simulator_config())).await; + let mut real_chain_interact = real_chain_reader_interactor().await; let simulator_interact = test_interactor().await; // now we should have current mike account in the set state file @@ -162,8 +172,7 @@ async fn set_state_overwrite_cs_test() { let account_address = test_wallets::mike(); let account_address_2 = test_wallets::ivan(); - let mut real_chain_interact = - BasicInteractor::new(InteractorConfigLoader::Direct(chain_simulator_config())).await; + let mut real_chain_interact = real_chain_reader_interactor().await; let simulator_interact = test_interactor().await; // now we should have current mike and ivan accounts in the set state file diff --git a/contracts/examples/ping-pong-egld/dapp/src/requests/query.rs b/contracts/examples/ping-pong-egld/dapp/src/requests/query.rs index 43bf114684..370222fdf5 100644 --- a/contracts/examples/ping-pong-egld/dapp/src/requests/query.rs +++ b/contracts/examples/ping-pong-egld/dapp/src/requests/query.rs @@ -10,7 +10,7 @@ pub async fn get_network_status() -> Result { let response = contract_interact .interactor - .proxy + .proxy() .request(NetworkStatusRequest::new(shard)) .await; diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs index 423d3cb85b..9de8c5fed2 100644 --- a/framework/snippets/src/config.rs +++ b/framework/snippets/src/config.rs @@ -1,11 +1,9 @@ mod auto_save; mod connection_config; mod interactor_config; -mod interactor_config_loader; mod wallet_config; pub use auto_save::*; pub use connection_config::*; pub use interactor_config::*; -pub use interactor_config_loader::*; pub use wallet_config::*; diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs index 5293bb529a..021e209a91 100644 --- a/framework/snippets/src/config/interactor_config.rs +++ b/framework/snippets/src/config/interactor_config.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use crate::sdk::wallet::Wallet; use super::ConnectionConfig; @@ -27,3 +29,17 @@ impl InteractorConfig for ConnectionConfig { self } } + +pub fn load_toml_config(config_path: &PathBuf) -> C +where + C: InteractorConfig + serde::de::DeserializeOwned, +{ + let mut file = std::fs::File::open(config_path) + .unwrap_or_else(|e| panic!("cannot open {}: {e}", config_path.display())); + let mut content = String::new(); + use std::io::Read; + file.read_to_string(&mut content) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", config_path.display())); + toml::from_str(&content) + .unwrap_or_else(|e| panic!("cannot parse {}: {e}", config_path.display())) +} diff --git a/framework/snippets/src/config/interactor_config_loader.rs b/framework/snippets/src/config/interactor_config_loader.rs deleted file mode 100644 index 84b707b68b..0000000000 --- a/framework/snippets/src/config/interactor_config_loader.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::path::PathBuf; - -use serde::de::DeserializeOwned; - -use super::InteractorConfig; - -const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; - -/// Runtime config source used by [`crate::InteractorBase::new_with_config`]. -/// -/// This enum is intended for call sites that want a lightweight API: -/// either pass a fully constructed config (`Direct`) or pass a path to a -/// TOML file to be loaded on demand (`FromFile`), or pass a directory and use -/// the conventional `config.toml` name (`FromDir`). -/// -/// # Typical usage -/// -/// ```rust,ignore -/// // Runtime file path selection. -/// let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml"); -/// let (interactor, cfg) = Interactor::new_with_config( -/// InteractorConfigLoader::::FromFile(path), -/// ) -/// .await; -/// -/// // Or use an already built config value. -/// let cfg = MyConfig { /* ... */ }; -/// let (interactor, cfg) = Interactor::new_with_config( -/// InteractorConfigLoader::Direct(cfg), -/// ) -/// .await; -/// ``` -/// -/// # Current directory behavior -/// -/// - `Direct` uses `"."` as current dir. -/// - `FromFile(path)` uses `path.parent()` (or `"."` if there is no parent). -/// - `FromDir(path)` uses `path` as current dir. -pub enum InteractorConfigLoader { - /// Load `C` from a TOML file path at runtime. - /// - /// The file is deserialized with `toml::from_str` and must match `C`. - FromFile(PathBuf), - /// Load `C` from `config.toml` in the provided directory. - FromDir(PathBuf), - /// Use an already constructed config value. - /// - /// Useful for tests and programmatic overrides. - Direct(C), -} - -impl InteractorConfigLoader -where - C: InteractorConfig + DeserializeOwned, -{ - fn load_toml_config(config_path: &PathBuf) -> C { - let mut file = std::fs::File::open(config_path) - .unwrap_or_else(|e| panic!("cannot open {}: {e}", config_path.display())); - let mut content = String::new(); - use std::io::Read; - file.read_to_string(&mut content) - .unwrap_or_else(|e| panic!("cannot read {}: {e}", config_path.display())); - toml::from_str(&content) - .unwrap_or_else(|e| panic!("cannot parse {}: {e}", config_path.display())) - } - - /// Resolves both the config directory and the loaded config in one step. - /// - /// This is the central resolver used by enum-based interactor construction. - /// - /// # Panics - /// - /// Panics when `FromFile` is used and: - /// - the file cannot be opened, - /// - the file cannot be read, - /// - or TOML deserialization into `C` fails. - /// - /// Panic messages include the offending file path for easier diagnostics. - pub fn resolve_with_current_dir(self) -> (PathBuf, C) { - match self { - InteractorConfigLoader::Direct(config) => (PathBuf::from("."), config), - InteractorConfigLoader::FromFile(config_path) => { - let current_dir = config_path - .parent() - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(".")); - let config = Self::load_toml_config(&config_path); - (current_dir, config) - } - InteractorConfigLoader::FromDir(dir_path) => { - let config_path = dir_path.join(DEFAULT_CONFIG_FILE_NAME); - let config = Self::load_toml_config(&config_path); - (dir_path, config) - } - } - } -} diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index f0c1fde8c6..05eefd87d0 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -1,7 +1,4 @@ -use crate::{ - config::InteractorConfigLoader, - sdk::{data::network_config::NetworkConfig, wallet::Wallet}, -}; +use crate::sdk::{data::network_config::NetworkConfig, wallet::Wallet}; use multiversx_sc_scenario::{ imports::{Bech32Address, ScenarioRunner}, mandos_system::{run_list::ScenarioRunnerList, run_trace::ScenarioTraceFile}, @@ -23,9 +20,12 @@ use std::{ }; use crate::{ - Sender, account_tool::retrieve_account_as_scenario_set_state, config::InteractorConfig, + Sender, + account_tool::retrieve_account_as_scenario_set_state, + config::{InteractorConfig, load_toml_config}, }; +const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; pub const INTERACTOR_SET_STATE_PATH: &str = "set_state.json"; @@ -33,9 +33,9 @@ pub struct InteractorBase where GatewayProxy: GatewayAsyncService, { - pub proxy: GatewayProxy, + pub proxy: Option, pub use_chain_simulator: bool, - pub network_config: NetworkConfig, + pub network_config: Option, pub sender_map: HashMap, pub gas_price: u64, @@ -51,82 +51,169 @@ impl InteractorBase where GatewayProxy: GatewayAsyncService, { - /// Not yet changed for backwards compatibility. - pub async fn new(gateway_uri: &str) -> Self { - let proxy = GatewayProxy::from_uri(gateway_uri); - let network_config = proxy - .request(NetworkConfigRequest) - .await - .expect("could not get network config"); - let gas_price = network_config.min_gas_price; - let explorer_url = ExplorerUrl::from_chain_id(&network_config.chain_id); + /// Creates an uninitialized interactor instance. + /// + /// Use [`Self::with_connection`] to initialize gateway-dependent fields. + pub fn empty() -> Self { Self { - proxy, + proxy: None, use_chain_simulator: false, - network_config, + network_config: None, sender_map: HashMap::new(), waiting_time_ms: 0, pre_runners: ScenarioRunnerList::empty(), post_runners: ScenarioRunnerList::empty(), current_dir: PathBuf::default(), - gas_price, - explorer_url, + gas_price: 0, + explorer_url: None, } } - /// Constructs an interactor from an enum config source. - /// - /// It accepts [`crate::config::InteractorConfigLoader`] so callers can choose - /// config source at runtime: + /// Backwards-compatible constructor that initializes the interactor connection. + pub async fn new(gateway_uri: &str) -> Self { + Self::empty().with_connection(gateway_uri).await + } + + /// Initializes the interactor current directory. + pub fn with_current_dir>(mut self, current_dir: P) -> Self { + self.current_dir = current_dir.into(); + self + } + + /// Initializes gateway-specific fields from a gateway URI. /// - /// - [`crate::config::InteractorConfigLoader::Direct`]: use an already-built config. - /// - [`crate::config::InteractorConfigLoader::FromFile`]: load config from a TOML file path. - /// - [`crate::config::InteractorConfigLoader::FromDir`]: load `config.toml` from a directory. + /// This sets: + /// - `proxy` + /// - `network_config` + /// - `gas_price` + /// - `explorer_url` + async fn init_connection(&mut self, gateway_uri: &str) { + let proxy = GatewayProxy::from_uri(gateway_uri); + let network_config = proxy + .request(NetworkConfigRequest) + .await + .expect("could not get network config"); + self.gas_price = network_config.min_gas_price; + self.explorer_url = ExplorerUrl::from_chain_id(&network_config.chain_id); + self.network_config = Some(network_config); + self.proxy = Some(proxy); + } + + /// Ensures config-loading initialization runs only once. /// - /// Behavior: + /// # Panics /// - /// 1. Resolve `(current_dir, config)` from the enum source. - /// 2. Connect to `config.connection().gateway_uri()`. - /// 3. Apply `use_chain_simulator(config.connection().use_chain_simulator())`. - /// 4. Set [`InteractorBase::current_dir`] to the resolved directory. - /// 5. Register all wallets from `config.register_wallets()`. + /// Panics if connection fields are already initialized. + fn assert_uninitialized_for_config_loading(&self) { + if self.proxy.is_some() || self.network_config.is_some() { + panic!( + "interactor connection already initialized; config loading must be called only on an uninitialized interactor" + ); + } + } + + /// Applies connection settings from a typed config. /// - /// Returns both the constructed interactor and the resolved config `(Self, C)`. + /// This initializes the gateway connection and chain-simulator toggle. + async fn apply_config_connection(&mut self, config: &C) + where + C: InteractorConfig, + { + let conn = config.connection(); + let gateway_uri = conn.gateway_uri().to_owned(); + self.init_connection(&gateway_uri).await; + self.use_chain_simulator = conn.use_chain_simulator(); + } + + /// Initializes connection-dependent fields on an interactor. + pub async fn with_connection(mut self, gateway_uri: &str) -> Self { + self.init_connection(gateway_uri).await; + self + } + + /// Loads an already-constructed config into this interactor. /// - /// Block generation (for the chain simulator) is **not** performed here; call - /// [`crate::InteractorBase::generate_blocks`] or - /// [`crate::InteractorBase::generate_blocks_until_all_activations`] afterwards if needed. + /// This initializes connection details, registers wallets from + /// [`InteractorConfig::register_wallets`], and advances chain-simulator blocks + /// until activation epoch is reached. /// /// # Panics /// - /// May panic when loading config from disk (`FromFile` / `FromDir`) if the file - /// cannot be opened/read or parsed as TOML into `C`. Panic messages include file - /// paths for easier diagnostics. - pub async fn new_with_config(config_source: InteractorConfigLoader) -> (Self, C) + /// Panics if the interactor is already initialized. + pub async fn load_config(&mut self, config: &C) + where + C: InteractorConfig, + { + self.assert_uninitialized_for_config_loading(); + self.apply_config_connection(config).await; + for wallet in config.register_wallets() { + self.register_wallet(wallet).await; + } + self.generate_blocks_until_all_activations().await; + } + + /// Builder-style variant of [`Self::load_config`]. + pub async fn with_config(mut self, config: &C) -> Self + where + C: InteractorConfig, + { + self.load_config(config).await; + self + } + + /// Loads `config.toml` from [`InteractorBase::current_dir`] and initializes the interactor. + pub async fn load_config_toml(&mut self) -> C where C: InteractorConfig + serde::de::DeserializeOwned, { - let (current_dir, loaded) = config_source.resolve_with_current_dir(); + self.load_config_from_file(DEFAULT_CONFIG_FILE_NAME).await + } - let conn = loaded.connection(); - let gateway_uri = conn.gateway_uri().to_owned(); - let use_chain_simulator = conn.use_chain_simulator(); - let wallets = loaded.register_wallets(); - let mut interactor = Self::new(&gateway_uri) - .await - .use_chain_simulator(use_chain_simulator); - interactor.current_dir = current_dir; - for wallet in wallets { - interactor.register_wallet(wallet).await; - } - (interactor, loaded) + /// Loads a config file relative to [`InteractorBase::current_dir`] and initializes the interactor. + /// + /// Returns the parsed config value so callers can keep their typed config. + pub async fn load_config_from_file(&mut self, config_file: P) -> C + where + C: InteractorConfig + serde::de::DeserializeOwned, + P: AsRef, + { + let config_path = self.current_dir.join(config_file.as_ref()); + let config: C = load_toml_config(&config_path); + self.load_config(&config).await; + config + } + + /// Returns the initialized gateway proxy reference. + /// + /// # Panics + /// + /// Panics if connection has not been initialized. + pub fn proxy(&self) -> &GatewayProxy { + self.proxy.as_ref().expect( + "interactor proxy is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", + ) + } + + /// Returns the initialized network configuration. + /// + /// # Panics + /// + /// Panics if connection has not been initialized. + pub fn network_config(&self) -> &NetworkConfig { + self.network_config.as_ref().expect( + "interactor network_config is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", + ) } + /// Enables or disables chain-simulator mode. pub fn use_chain_simulator(mut self, use_chain_simulator: bool) -> Self { self.use_chain_simulator = use_chain_simulator; self } + /// Registers a wallet as a transaction sender and funds it in simulator mode. + /// + /// Returns the wallet address. pub async fn register_wallet(&mut self, wallet: Wallet) -> Address { let address = wallet.to_address(); @@ -138,7 +225,7 @@ where address.clone(), Sender { address: address.clone(), - hrp: self.network_config.address_hrp, + hrp: self.get_hrp(), wallet, current_nonce: None, }, @@ -146,20 +233,23 @@ where address } + /// Sleeps for `duration` using the configured gateway and accumulates waited time. pub async fn sleep(&mut self, duration: Duration) { let millis = duration.as_millis() as u64; self.waiting_time_ms += millis; - self.proxy.sleep(millis).await; + self.proxy().sleep(millis).await; } + /// Adds a scenario trace output runner to this interactor. pub async fn with_tracer>(mut self, path: P) -> Self { self.post_runners.push(ScenarioTraceFile::new(path)); self } + /// Fetches on-chain account state and appends it to the local set-state file. pub async fn retrieve_account(&mut self, wallet_address: &Bech32Address) { let (set_state_account, set_state_step) = - retrieve_account_as_scenario_set_state(&self.proxy, wallet_address).await; + retrieve_account_as_scenario_set_state(self.proxy(), wallet_address).await; self.pre_runners.run_set_state_step(&set_state_step); self.post_runners.run_set_state_step(&set_state_step); @@ -167,18 +257,24 @@ where set_state_account.add_to_state_file(path.as_path()); } + /// Returns the absolute path to the persisted set-state file. pub fn get_state_file_path(&self) -> PathBuf { self.current_dir.join(INTERACTOR_SET_STATE_PATH) } + /// Returns the configured Bech32 HRP for this network. pub fn get_hrp(&self) -> Bech32Hrp { - self.network_config.address_hrp + self.network_config().address_hrp } + /// Returns `true` if `address` belongs to a registered sender wallet. pub fn is_registered_wallet(&self, address: &Address) -> bool { self.sender_map.contains_key(address) } + /// Reads and parses all saved set-state accounts from disk. + /// + /// Returns an empty list if the file does not exist or cannot be parsed. pub fn get_accounts_from_file(&self) -> Vec { let file_path = self.get_state_file_path(); diff --git a/framework/snippets/src/interactor/interactor_chain_simulator.rs b/framework/snippets/src/interactor/interactor_chain_simulator.rs index 27ebddd164..b5409d6a96 100644 --- a/framework/snippets/src/interactor/interactor_chain_simulator.rs +++ b/framework/snippets/src/interactor/interactor_chain_simulator.rs @@ -19,7 +19,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorSendFundsRequest::to_address(receiver)) .await } @@ -29,7 +29,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorGenerateBlocksRequest::num_blocks(num_blocks)) .await } @@ -39,7 +39,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorAddKeysRequest::with_keys(vec![key])) .await } @@ -49,7 +49,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorGenerateBlocksRequest::until_epoch( epoch_number, )) @@ -69,7 +69,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorGenerateBlocksRequest::until_tx_processed( tx_hash, )) @@ -81,7 +81,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorSetStateRequest::for_accounts(accounts)) .await } @@ -94,7 +94,7 @@ where return Ok(String::from("no-simulator")); } - self.proxy + self.proxy() .request(ChainSimulatorSetStateOverwriteRequest::for_accounts( accounts, )) @@ -107,7 +107,7 @@ where } let accounts = self.get_accounts_from_file(); - self.proxy + self.proxy() .request(ChainSimulatorSetStateRequest::for_accounts(accounts)) .await } diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index d3aea037e8..92b2360195 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -39,7 +39,7 @@ where self.generate_blocks_until_tx_processed(&tx_hash) .await .unwrap(); - let (tx, return_code) = retrieve_tx_on_network(&self.proxy, tx_hash) + let (tx, return_code) = retrieve_tx_on_network(self.proxy(), tx_hash) .await .expect("failed to fetch transaction result"); @@ -73,7 +73,7 @@ where } async fn launch_sc_call(&mut self, transaction: &Transaction) -> String { - let tx_hash = self.proxy.request(SendTxRequest(transaction)).await; + let tx_hash = self.proxy().request(SendTxRequest(transaction)).await; match tx_hash { Ok(tx_hash) => { @@ -95,7 +95,7 @@ where } async fn sc_call_simulate_transaction(&mut self, transaction: &Transaction) -> u64 { - let result = self.proxy.request(SimulateTxRequest(transaction)).await; + let result = self.proxy().request(SimulateTxRequest(transaction)).await; match result { Ok(gas) => { @@ -131,8 +131,8 @@ where gas_limit: normalized.gas_limit.value, data, signature: None, - chain_id: self.network_config.chain_id.clone(), - version: self.network_config.min_transaction_version, + chain_id: self.network_config().chain_id.clone(), + version: self.network_config().min_transaction_version, options: 0, } } diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index ba2ea4f838..9fb0bb6fa7 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -19,7 +19,7 @@ where GatewayProxy: GatewayAsyncService, { pub(crate) fn sc_deploy_to_blockchain_tx(&self, sc_deploy_step: &ScDeployStep) -> Transaction { - let hrp = self.network_config.address_hrp; + let hrp = self.network_config().address_hrp; Transaction { nonce: 0, @@ -30,8 +30,8 @@ where gas_limit: sc_deploy_step.tx.gas_limit.value, data: Some(base64_encode(sc_deploy_step.tx.to_tx_data())), signature: None, - chain_id: self.network_config.chain_id.clone(), - version: self.network_config.min_transaction_version, + chain_id: self.network_config().chain_id.clone(), + version: self.network_config().min_transaction_version, options: 0, } } @@ -50,7 +50,7 @@ where } async fn launch_sc_deploy(&mut self, transaction: &Transaction) -> String { - let tx_hash_result = self.proxy.request(SendTxRequest(transaction)).await; + let tx_hash_result = self.proxy().request(SendTxRequest(transaction)).await; match tx_hash_result { Ok(tx_hash) => { @@ -92,7 +92,7 @@ where self.generate_blocks_until_tx_processed(&tx_hash) .await .unwrap(); - let (tx, return_code) = retrieve_tx_on_network(&self.proxy, tx_hash.clone()) + let (tx, return_code) = retrieve_tx_on_network(self.proxy(), tx_hash.clone()) .await .expect("failed to fetch transaction result"); @@ -121,7 +121,7 @@ where } async fn sc_deploy_simulate_transaction(&mut self, transaction: &Transaction) -> u64 { - let gas_result = self.proxy.request(SimulateTxRequest(transaction)).await; + let gas_result = self.proxy().request(SimulateTxRequest(transaction)).await; match gas_result { Ok(gas) => { diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs index 159362529a..c61dcd8879 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs @@ -17,7 +17,7 @@ where pub async fn transfer(&mut self, transfer_step: TransferStep) -> String { let transaction = self.launch_transfer(&transfer_step).await; - let tx_hash = match self.proxy.request(SendTxRequest(&transaction)).await { + let tx_hash = match self.proxy().request(SendTxRequest(&transaction)).await { Ok(hash) => hash, Err(err) => { transfer_err_message(&err); @@ -31,7 +31,7 @@ where println!("transfer tx hash: {tx_hash}"); info!("transfer tx hash: {}", tx_hash); - retrieve_tx_on_network(&self.proxy, tx_hash.clone()) + retrieve_tx_on_network(self.proxy(), tx_hash.clone()) .await .expect("failed to fetch transfer tx result"); @@ -43,7 +43,7 @@ where pub async fn simulate_gas_transfer(&mut self, transfer_step: TransferStep) -> u64 { let transaction = self.launch_transfer(&transfer_step).await; - match self.proxy.request(SimulateTxRequest(&transaction)).await { + match self.proxy().request(SimulateTxRequest(&transaction)).await { Ok(gas) => { println!("Gas simulation for the SC transfer: {gas} units."); log::info!("Gas simulation for the SC transfer: {gas} units."); diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs index 1e5cb2d48f..b1883f165f 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_vm_query.rs @@ -17,7 +17,7 @@ where GatewayProxy: GatewayAsyncService, { pub async fn perform_sc_query(&mut self, step: &mut ScQueryStep) { - let hrp = self.network_config.address_hrp; + let hrp = self.network_config().address_hrp; let sc_address = step.tx.to.to_address().to_bech32(hrp); let req = VMQueryInput { @@ -30,7 +30,7 @@ where .map(|arg| hex::encode(&arg.value)) .collect(), }; - let result = match self.proxy.request(VMQueryRequest(&req)).await { + let result = match self.proxy().request(VMQueryRequest(&req)).await { Ok(r) => r, Err(err) => { query_err_message(&err); diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 72ee215c1d..8b1bbe9c46 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -25,7 +25,7 @@ where { pub async fn recall_nonce(&self, address: &Address) -> u64 { let account = self - .proxy + .proxy() .request(GetAccountRequest::new(&address.to_bech32(self.get_hrp()))) .await .expect("failed to retrieve account nonce"); @@ -34,14 +34,14 @@ where } pub async fn get_account(&self, address: &Address) -> Account { - self.proxy + self.proxy() .request(GetAccountRequest::new(&address.to_bech32(self.get_hrp()))) .await .expect("failed to retrieve account") } pub async fn get_account_storage(&self, address: &Address) -> HashMap { - self.proxy + self.proxy() .request(GetAccountStorageRequest::new( &address.to_bech32(self.get_hrp()), )) @@ -63,7 +63,7 @@ where } pub async fn get_account_esdt(&self, address: &Address) -> HashMap { - self.proxy + self.proxy() .request(GetAccountEsdtTokensRequest::new( &address.to_bech32(self.get_hrp()), )) diff --git a/framework/snippets/src/multi/interactor_multi_sc_process.rs b/framework/snippets/src/multi/interactor_multi_sc_process.rs index c816b9f533..dc2b43e22e 100644 --- a/framework/snippets/src/multi/interactor_multi_sc_process.rs +++ b/framework/snippets/src/multi/interactor_multi_sc_process.rs @@ -34,13 +34,13 @@ where for tx in &txs { let tx_hash = self - .proxy + .proxy() .request(SendTxRequest(tx)) .await .expect("failed to send transaction"); println!("process tx hash: {tx_hash} with nonce: {}", tx.nonce); - futures.push(retrieve_tx_on_network(&self.proxy, tx_hash.clone())); + futures.push(retrieve_tx_on_network(self.proxy(), tx_hash.clone())); } self.generate_blocks(4).await.unwrap(); From e655e5ad398f82c97e887344ff09178a60ed0ea1 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 23 Jun 2026 04:22:22 +0300 Subject: [PATCH 085/195] interactor connection refactor --- .../src/interactor/interactor_base.rs | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 05eefd87d0..469b335fd3 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -29,13 +29,42 @@ const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; pub const INTERACTOR_SET_STATE_PATH: &str = "set_state.json"; +/// Holds the gateway connection and chain network configuration together. +/// +/// `proxy` and `network_config` are always obtained from the same gateway call, so +/// they are stored as a unit in [`InteractorBase::connection`] and are either both +/// present or both absent. +pub struct InteractorConnection +where + GatewayProxy: GatewayAsyncService, +{ + /// The async gateway used to communicate with the blockchain network. + pub proxy: GatewayProxy, + /// Chain-level parameters (chain ID, gas price, HRP, …) fetched from the gateway. + pub network_config: NetworkConfig, +} + +/// Core interactor state shared across all contract-specific interactors. +/// +/// An `InteractorBase` is created in two phases: +/// +/// 1. **Construction** – call [`Self::empty`] (or [`Self::new`] for the +/// backwards-compatible path) to obtain an instance whose gateway fields are +/// still uninitialised. +/// 2. **Initialisation** – connect to a gateway via [`Self::with_connection`] or +/// load a full typed config via [`Self::load_config`] / +/// [`Self::load_config_toml`] / [`Self::load_config_from_file`]. +/// +/// Once initialised the interactor exposes helpers for sending transactions, +/// querying state, registering wallets, managing chain-simulator blocks, and +/// persisting/restoring account state to/from disk. pub struct InteractorBase where GatewayProxy: GatewayAsyncService, { - pub proxy: Option, + /// Gateway proxy and network config, initialised together by [`Self::with_connection`]. + connection: Option>, pub use_chain_simulator: bool, - pub network_config: Option, pub sender_map: HashMap, pub gas_price: u64, @@ -56,9 +85,8 @@ where /// Use [`Self::with_connection`] to initialize gateway-dependent fields. pub fn empty() -> Self { Self { - proxy: None, + connection: None, use_chain_simulator: false, - network_config: None, sender_map: HashMap::new(), waiting_time_ms: 0, pre_runners: ScenarioRunnerList::empty(), @@ -83,8 +111,8 @@ where /// Initializes gateway-specific fields from a gateway URI. /// /// This sets: - /// - `proxy` - /// - `network_config` + /// - `connection.proxy` + /// - `connection.network_config` /// - `gas_price` /// - `explorer_url` async fn init_connection(&mut self, gateway_uri: &str) { @@ -95,17 +123,19 @@ where .expect("could not get network config"); self.gas_price = network_config.min_gas_price; self.explorer_url = ExplorerUrl::from_chain_id(&network_config.chain_id); - self.network_config = Some(network_config); - self.proxy = Some(proxy); + self.connection = Some(InteractorConnection { + proxy, + network_config, + }); } /// Ensures config-loading initialization runs only once. /// /// # Panics /// - /// Panics if connection fields are already initialized. + /// Panics if connection is already initialized. fn assert_uninitialized_for_config_loading(&self) { - if self.proxy.is_some() || self.network_config.is_some() { + if self.connection.is_some() { panic!( "interactor connection already initialized; config loading must be called only on an uninitialized interactor" ); @@ -189,9 +219,13 @@ where /// /// Panics if connection has not been initialized. pub fn proxy(&self) -> &GatewayProxy { - self.proxy.as_ref().expect( + &self + .connection + .as_ref() + .expect( "interactor proxy is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", - ) + ) + .proxy } /// Returns the initialized network configuration. @@ -200,9 +234,13 @@ where /// /// Panics if connection has not been initialized. pub fn network_config(&self) -> &NetworkConfig { - self.network_config.as_ref().expect( + &self + .connection + .as_ref() + .expect( "interactor network_config is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", - ) + ) + .network_config } /// Enables or disables chain-simulator mode. From ee46708d6ea5b8cbbb93611f8f3b5613384d7dee Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 23 Jun 2026 04:30:53 +0300 Subject: [PATCH 086/195] interactor base field refactor --- .../src/interactor/interactor_base.rs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 469b335fd3..7de0bcf887 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -62,18 +62,43 @@ pub struct InteractorBase where GatewayProxy: GatewayAsyncService, { + /// Working directory used to resolve file paths such as `config.toml`, + /// `state.toml`, and the set-state snapshot file. + /// + /// Set at construction time via [`Self::with_current_dir`] or + /// [`Self::set_current_dir_from_workspace`]. + pub current_dir: PathBuf, + + /// When `true` the interactor is connected to the MultiversX Chain Simulator + /// and will issue simulator-specific requests (fund accounts, generate blocks, + /// set state, …). When `false` those requests are silently skipped. + pub use_chain_simulator: bool, + /// Gateway proxy and network config, initialised together by [`Self::with_connection`]. connection: Option>, - pub use_chain_simulator: bool, - pub sender_map: HashMap, + + /// Gas price used when building transactions, derived from + /// `connection.network_config.min_gas_price` at connection time. pub gas_price: u64, - pub waiting_time_ms: u64, + /// All wallets registered as transaction signers, keyed by their on-chain + /// address. Populated by [`Self::register_wallet`]. + pub sender_map: HashMap, + + /// Scenario runners executed **before** each step (e.g. pre-condition checks + /// or state loading). pub pre_runners: ScenarioRunnerList, + + /// Scenario runners executed **after** each step (e.g. trace file writers). pub post_runners: ScenarioRunnerList, - pub current_dir: PathBuf, + /// Optional block-explorer base URL derived from the network chain ID. Used + /// to pretty-print transaction and address links in log output. pub explorer_url: Option, + + /// Cumulative wall-clock time (in milliseconds) spent waiting inside + /// [`Self::sleep`] calls. + pub waiting_time_ms: u64, } impl InteractorBase From 79befb345babced737d69ef30f9862da7f639355 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 23 Jun 2026 15:48:11 +0300 Subject: [PATCH 087/195] interactor update & fix - ping pong --- .../ping-pong-egld/interactor/src/interact.rs | 6 ++---- .../tests/ping_pong_interact_cs_test.rs | 18 ++++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact.rs b/contracts/examples/ping-pong-egld/interactor/src/interact.rs index 1e7cced3bc..6b828b5d35 100644 --- a/contracts/examples/ping-pong-egld/interactor/src/interact.rs +++ b/contracts/examples/ping-pong-egld/interactor/src/interact.rs @@ -144,10 +144,8 @@ pub struct PingPongEgldInteract { impl PingPongEgldInteract { pub async fn new() -> Self { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; let interactor = interactor.with_tracer(INTERACTOR_SCENARIO_TRACE_PATH).await; let state = interactor.load_state::(); Self { diff --git a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs index 697fb2f0b9..91ac42c389 100644 --- a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs +++ b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs @@ -1,23 +1,21 @@ -use multiversx_sc_snippets::imports::{DurationMillis, RustBigUint, TimestampMillis, *}; +use multiversx_sc_snippets::imports::*; use ping_pong_egld_interact::{Config, PingPongEgldInteract}; use serial_test::serial; fn chain_simulator_config() -> Config { Config { connection: ConnectionConfig::chain_simulator(), - owner: WalletConfig::from_test_wallet("mike"), - wallet: WalletConfig::from_test_wallet("ivan"), + owner: WalletConfig::from_test_wallet("eve"), + wallet: WalletConfig::from_test_wallet("mallory"), } } async fn cs_interactor() -> PingPongEgldInteract { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .with_config(chain_simulator_config()) - .build() - .await; - let interactor = interactor - .with_tracer(ping_pong_egld_interact::INTERACTOR_SCENARIO_TRACE_PATH) + let config = chain_simulator_config(); + let interactor = Interactor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .use_chain_simulator(true) + .with_config(&config) .await; PingPongEgldInteract { interactor, From 8e464076a6ed41217969931e268ff3045607a6db Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 23 Jun 2026 17:02:55 +0300 Subject: [PATCH 088/195] interactor - removed HttpInteractorBuilder --- .../interactor/src/vls_interactor.rs | 8 +- .../ping-pong-egld/interactor/src/interact.rs | 6 +- .../interact/src/bf_interact.rs | 10 +- .../interact/tests/bf_interact_cs_test.rs | 12 +- .../forwarder-interactor/src/interact.rs | 53 ++----- .../tests/interact_cs_tests.rs | 16 ++- .../interactor/src/payable_interactor.rs | 8 +- .../tests/payable_interactor_cs_test.rs | 8 +- .../tiny/interactor/src/tiny_interactor.rs | 8 +- .../snippets/src/config/interactor_config.rs | 8 +- .../snippets/src/http_interactor_builder.rs | 130 ------------------ framework/snippets/src/imports.rs | 2 +- framework/snippets/src/lib.rs | 6 - 13 files changed, 54 insertions(+), 221 deletions(-) delete mode 100644 framework/snippets/src/http_interactor_builder.rs diff --git a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs b/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs index 191bb43733..578e832872 100644 --- a/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs +++ b/contracts/benchmarks/very-large-storage/interactor/src/vls_interactor.rs @@ -64,12 +64,10 @@ pub struct VeryLargeStorageInteractor { impl VeryLargeStorageInteractor { pub async fn new() -> Self { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); - VeryLargeStorageInteractor { + Self { interactor, config, state, diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact.rs b/contracts/examples/ping-pong-egld/interactor/src/interact.rs index 6b828b5d35..a53eb1c51d 100644 --- a/contracts/examples/ping-pong-egld/interactor/src/interact.rs +++ b/contracts/examples/ping-pong-egld/interactor/src/interact.rs @@ -144,9 +144,11 @@ pub struct PingPongEgldInteract { impl PingPongEgldInteract { pub async fn new() -> Self { - let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let mut interactor = Interactor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_tracer(INTERACTOR_SCENARIO_TRACE_PATH) + .await; let config: Config = interactor.load_config_toml().await; - let interactor = interactor.with_tracer(INTERACTOR_SCENARIO_TRACE_PATH).await; let state = interactor.load_state::(); Self { interactor, diff --git a/contracts/feature-tests/basic-features/interact/src/bf_interact.rs b/contracts/feature-tests/basic-features/interact/src/bf_interact.rs index 9eeec27bf8..1a31092019 100644 --- a/contracts/feature-tests/basic-features/interact/src/bf_interact.rs +++ b/contracts/feature-tests/basic-features/interact/src/bf_interact.rs @@ -76,14 +76,10 @@ pub struct BasicFeaturesInteract { impl BasicFeaturesInteract { pub async fn init() -> Self { - let (interactor, config) = HttpInteractorBuilder::::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; - let interactor = interactor.with_tracer(INTERACTOR_SCENARIO_TRACE_PATH).await; - interactor.generate_blocks_until_all_activations().await; - let wallet_address = config.wallet.address(); + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); + let wallet_address = config.wallet.address(); Self { interactor, wallet_address, diff --git a/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs b/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs index 66ec88127d..4c32d8d7a5 100644 --- a/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs +++ b/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs @@ -15,15 +15,11 @@ fn chain_simulator_config() -> Config { } async fn cs_interactor() -> BasicFeaturesInteract { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .with_config(chain_simulator_config()) - .build() + let config = chain_simulator_config(); + let interactor = HttpInteractor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&config) .await; - let interactor = interactor - .with_tracer(basic_features_interact::INTERACTOR_SCENARIO_TRACE_PATH) - .await; - interactor.generate_blocks_until_all_activations().await; let wallet_address = config.wallet.address(); BasicFeaturesInteract { interactor, diff --git a/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs b/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs index 46ce479985..43c144b45f 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs +++ b/contracts/feature-tests/composability/forwarder-interactor/src/interact.rs @@ -158,62 +158,29 @@ impl State { } pub struct ContractInteract { - interactor: Interactor, + pub interactor: Interactor, pub wallet_address: Bech32Address, - contract_code: BytesValue, + pub contract_code: BytesValue, pub state: AutoSave, } impl ContractInteract { pub async fn new(trace_path: Option<&str>) -> Self { - let (mut interactor, config) = HttpInteractorBuilder::::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); if let Some(path) = trace_path { interactor = interactor.with_tracer(path).await; } - let wallet_address = config.wallet.address(); - let contract_code = BytesValue::interpret_from( - "mxsc:../forwarder/output/forwarder.mxsc.json", - &InterpreterContext::default(), - ); + let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); - - ContractInteract { - interactor, - wallet_address, - contract_code, - state, - } - } - - pub async fn new_with_config(config: Config, trace_path: Option<&str>) -> Self { - let (mut interactor, config) = HttpInteractorBuilder::::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .with_config(config) - .build() - .await; - - if let Some(path) = trace_path { - interactor = interactor.with_tracer(path).await; - } - - interactor.generate_blocks_until_all_activations().await; - - let wallet_address = config.wallet.address(); - let contract_code = BytesValue::interpret_from( - "mxsc:../forwarder/output/forwarder.mxsc.json", - &InterpreterContext::default(), - ); - let state = AutoSave::no_save_default(); - - ContractInteract { + Self { interactor, - wallet_address, - contract_code, + wallet_address: config.wallet.address(), + contract_code: BytesValue::interpret_from( + "mxsc:../forwarder/output/forwarder.mxsc.json", + &InterpreterContext::default(), + ), state, } } diff --git a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs index 9af8c1effa..6c01a5beb3 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs +++ b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs @@ -10,7 +10,21 @@ fn chain_simulator_config() -> Config { } async fn cs_interactor() -> ContractInteract { - ContractInteract::new_with_config(chain_simulator_config(), None).await + let config = chain_simulator_config(); + let interactor = HttpInteractor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&config) + .await; + let wallet_address = config.wallet.address(); + ContractInteract { + interactor, + wallet_address, + contract_code: BytesValue::interpret_from( + "mxsc:../forwarder/output/forwarder.mxsc.json", + &InterpreterContext::default(), + ), + state: multiversx_sc_snippets::AutoSave::no_save_default(), + } } // Simple deploy test that runs using the chain simulator configuration. diff --git a/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs b/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs index bfbd939be9..1d4bad7892 100644 --- a/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs +++ b/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs @@ -70,12 +70,10 @@ pub struct PayableInteract { impl PayableInteract { pub async fn new() -> Self { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); - PayableInteract { + Self { interactor, config, state, diff --git a/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs b/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs index 9140d2f004..cb041e8388 100644 --- a/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs +++ b/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs @@ -11,10 +11,10 @@ fn chain_simulator_config() -> Config { } async fn cs_interactor() -> PayableInteract { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .with_config(chain_simulator_config()) - .build() + let config = chain_simulator_config(); + let interactor = HttpInteractor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&config) .await; PayableInteract { interactor, diff --git a/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs b/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs index fed7bfb3f9..3431aaeec4 100644 --- a/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs +++ b/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs @@ -64,12 +64,10 @@ pub struct TinyInteractor { impl TinyInteractor { pub async fn new() -> Self { - let (interactor, config) = HttpInteractorBuilder::new() - .crate_dir(env!("CARGO_MANIFEST_DIR")) - .build() - .await; + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); - TinyInteractor { + Self { interactor, config, state, diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs index 021e209a91..805cb37fdb 100644 --- a/framework/snippets/src/config/interactor_config.rs +++ b/framework/snippets/src/config/interactor_config.rs @@ -4,11 +4,11 @@ use crate::sdk::wallet::Wallet; use super::ConnectionConfig; -/// Trait implemented by a contract-specific `Config` struct to tell the -/// [`HttpInteractorBuilder`](crate::HttpInteractorBuilder) how to initialise the interactor. +/// Trait implemented by a contract-specific `Config` struct to provide initialization +/// details to the interactor. /// -/// The builder calls `connection()` to open the gateway and calls `register_wallets()` to -/// register every wallet that will sign transactions. +/// The `InteractorBase` calls `connection()` to configure the gateway and calls +/// `register_wallets()` to register every wallet that will sign transactions. pub trait InteractorConfig { /// Returns the connection settings (gateway URI, chain type). fn connection(&self) -> &ConnectionConfig; diff --git a/framework/snippets/src/http_interactor_builder.rs b/framework/snippets/src/http_interactor_builder.rs deleted file mode 100644 index 76ca01b9f3..0000000000 --- a/framework/snippets/src/http_interactor_builder.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::{ - io::Read, - path::{Path, PathBuf}, -}; - -use serde::de::DeserializeOwned; - -use crate::{InteractorBase, config::InteractorConfig}; - -const DEFAULT_CONFIG_FILE: &str = "config.toml"; - -/// Fluent builder that constructs an [`InteractorBase`] together with a typed `Config`. -/// -/// # Typical usage -/// -/// ```rust,ignore -/// let (interactor, config) = HttpInteractorBuilder::::new() -/// .crate_dir(env!("CARGO_MANIFEST_DIR")) -/// .build() -/// .await; -/// let state = interactor.load_state::(); -/// ``` -pub struct HttpInteractorBuilder { - /// Directory that contains `config.toml` and `state.toml`. - crate_dir: PathBuf, - - /// Name of the config file relative to `crate_dir`. Default: `"config.toml"`. - config_file: String, - - /// Optional config override. If set, skips file loading. - config: Option, -} - -impl Default for HttpInteractorBuilder { - fn default() -> Self { - HttpInteractorBuilder { - crate_dir: PathBuf::from("."), - config_file: DEFAULT_CONFIG_FILE.to_owned(), - config: None, - } - } -} - -impl HttpInteractorBuilder -where - Config: DeserializeOwned + InteractorConfig, -{ - /// Creates a new builder. - /// - /// Use `.crate_dir(env!("CARGO_MANIFEST_DIR"))` to set the directory for - /// config and state file resolution. Defaults to `"."` (current directory). - /// The config type is inferred from context. - pub fn new() -> Self { - HttpInteractorBuilder::default() - } - - /// Override the directory used for config / state file resolution. - pub fn crate_dir(mut self, path: impl Into) -> Self { - self.crate_dir = path.into(); - self - } - - /// Override the config file name (relative to `crate_dir`). Default: `"config.toml"`. - pub fn config_file(mut self, name: impl Into) -> Self { - self.config_file = name.into(); - self - } - - /// Override the config with a pre-built value, skipping file loading. - pub fn with_config(mut self, config: Config) -> Self { - self.config = Some(config); - self - } - - fn resolve_config(self) -> (Config, PathBuf) { - let config = match self.config { - Some(c) => c, - None => { - let path = self.crate_dir.join(&self.config_file); - load_toml(&path) - } - }; - (config, self.crate_dir) - } -} - -impl HttpInteractorBuilder -where - Config: DeserializeOwned + InteractorConfig, -{ - /// Builds the pair `(Interactor, Config)`. - /// - /// 1. Resolves `Config` (from file or pre-built). - /// 2. Connects to the gateway described by `Config::connection()`. - /// 3. Registers all wallets returned by `Config::register_wallets()`. - /// 4. Generates 2 initial blocks when running against the chain simulator - /// (no-op on a real network). - /// - /// Use [`InteractorBase::load_state`] afterwards to load state. - pub async fn build(self) -> (crate::Interactor, Config) { - let (config, crate_dir) = self.resolve_config(); - - let conn = config.connection(); - let mut interactor = - InteractorBase::::new(conn.gateway_uri()) - .await - .use_chain_simulator(conn.use_chain_simulator()); - - interactor.current_dir = crate_dir; - - for wallet in config.register_wallets() { - interactor.register_wallet(wallet).await; - } - - interactor.generate_blocks_until_all_activations().await; - - (interactor, config) - } -} - -// ── helpers ────────────────────────────────────────────────────────────────── - -fn load_toml(path: &Path) -> T { - let mut file = - std::fs::File::open(path).unwrap_or_else(|e| panic!("cannot open {}: {e}", path.display())); - let mut content = String::new(); - file.read_to_string(&mut content) - .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); - toml::from_str(&content).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display())) -} diff --git a/framework/snippets/src/imports.rs b/framework/snippets/src/imports.rs index b8a83cb18a..435434e977 100644 --- a/framework/snippets/src/imports.rs +++ b/framework/snippets/src/imports.rs @@ -11,7 +11,7 @@ pub use crate::sdk::{test_wallets, validator::Validator, wallet::Wallet}; pub use env_logger; #[cfg(feature = "http")] -pub use crate::{HttpInteractor, HttpInteractorBuilder, Interactor}; +pub use crate::{HttpInteractor, Interactor}; #[cfg(feature = "http")] pub use multiversx_sdk_http::GatewayHttpProxy; diff --git a/framework/snippets/src/lib.rs b/framework/snippets/src/lib.rs index 3087302eab..cd0a343cdd 100644 --- a/framework/snippets/src/lib.rs +++ b/framework/snippets/src/lib.rs @@ -33,11 +33,5 @@ pub type HttpInteractor = crate::InteractorBase; From 1972fba469e506011fb77596bae0267a24f6f83c Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 23 Jun 2026 17:08:32 +0300 Subject: [PATCH 089/195] interactor - replace set_current_dir_from_workspace with with_current_dir --- .../examples/multisig/interact/src/multisig_interact.rs | 2 +- contracts/examples/ping-pong-egld/dapp/src/interactor.rs | 5 +++-- .../forwarder-blind/dex-interactor/src/dex_interactor.rs | 7 +++---- .../mesh-interactor/src/mesh_interact_controller.rs | 6 ++---- .../src/delegation_sc_interact.rs | 5 ++--- .../src/governance_sc_interact.rs | 5 ++--- .../interactor-system-func-calls/src/system_sc_interact.rs | 5 ++--- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/contracts/examples/multisig/interact/src/multisig_interact.rs b/contracts/examples/multisig/interact/src/multisig_interact.rs index 49e806259a..22891d31ea 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact.rs @@ -88,10 +88,10 @@ impl MultisigInteract { let config = Config::load_config(); let mut interactor = Interactor::new(config.gateway_uri()) .await + .with_current_dir(env!("CARGO_MANIFEST_DIR")) .use_chain_simulator(config.use_chain_simulator()) .with_tracer(INTERACTOR_SCENARIO_TRACE_PATH) .await; - interactor.set_current_dir_from_workspace("contracts/examples/multisig/interact"); let wallet_address = interactor.register_wallet(test_wallets::mike()).await; let multisig_code = BytesValue::interpret_from( "mxsc:../output/multisig.mxsc.json", diff --git a/contracts/examples/ping-pong-egld/dapp/src/interactor.rs b/contracts/examples/ping-pong-egld/dapp/src/interactor.rs index 843540649e..d48305e899 100644 --- a/contracts/examples/ping-pong-egld/dapp/src/interactor.rs +++ b/contracts/examples/ping-pong-egld/dapp/src/interactor.rs @@ -42,8 +42,9 @@ pub struct ContractInteract { impl ContractInteract { pub async fn new() -> Self { let config = Config::new(); - let mut interactor = DappInteractor::new(&config.gateway).await; - interactor.set_current_dir_from_workspace("contracts/examples/ping-pong-egld/dapp"); + let mut interactor = DappInteractor::new(&config.gateway) + .await + .with_current_dir(env!("CARGO_MANIFEST_DIR")); let wallet_address = interactor.register_wallet(test_wallets::mike()).await; let contract_code = BytesValue::from(PING_PONG_CODE); diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs index ea95c31891..4d99a7054d 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs @@ -113,11 +113,10 @@ pub struct ContractInteract { impl ContractInteract { pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()).await; + let mut interactor = Interactor::new(config.gateway_uri()) + .await + .with_current_dir(env!("CARGO_MANIFEST_DIR")); interactor.gas_price *= 25; - interactor.set_current_dir_from_workspace( - "contracts/feature-tests/composability/forwarder-blind/dex-interactor", - ); let wallet_addresses: Vec = if config.wallet_pem_paths.is_empty() { println!("WARNING: no wallet_pem_paths configured — all operations will be skipped."); diff --git a/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs b/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs index 7884fbb140..ee17b86f62 100644 --- a/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs +++ b/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs @@ -15,10 +15,8 @@ impl ComposabilityInteract { let gateway_config = &config.gateway; let mut interactor = Interactor::new(&gateway_config.uri) .await - .use_chain_simulator(gateway_config.use_chain_simulator()); - interactor.set_current_dir_from_workspace( - "contracts/feature-tests/composability/mesh-interactor", - ); + .use_chain_simulator(gateway_config.use_chain_simulator()) + .with_current_dir(env!("CARGO_MANIFEST_DIR")); let shard_wallet_addresses = [ interactor .register_wallet(test_wallets::for_shard(0u32.into())) diff --git a/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs b/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs index 32d0feee21..0dbc24fb4e 100644 --- a/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs +++ b/tools/interactor-delegation-func-calls/src/delegation_sc_interact.rs @@ -134,9 +134,8 @@ impl DelegateCallsInteract { pub async fn new(config: Config) -> Self { let mut interactor = Interactor::new(config.gateway_uri()) .await - .use_chain_simulator(config.is_chain_simulator()); - - interactor.set_current_dir_from_workspace("tools/interactor-delegation-func-calls"); + .use_chain_simulator(config.is_chain_simulator()) + .with_current_dir(env!("CARGO_MANIFEST_DIR")); let wallet_address = interactor.register_wallet(test_wallets::alice()).await; let delegator1 = interactor.register_wallet(test_wallets::bob()).await; let delegator2 = interactor.register_wallet(test_wallets::dan()).await; diff --git a/tools/interactor-governance-func-calls/src/governance_sc_interact.rs b/tools/interactor-governance-func-calls/src/governance_sc_interact.rs index df33685b8e..62e726d055 100644 --- a/tools/interactor-governance-func-calls/src/governance_sc_interact.rs +++ b/tools/interactor-governance-func-calls/src/governance_sc_interact.rs @@ -89,9 +89,8 @@ impl GovernanceCallsInteract { pub async fn new(config: Config) -> Self { let mut interactor = Interactor::new(config.gateway_uri()) .await - .use_chain_simulator(config.is_chain_simulator()); - - interactor.set_current_dir_from_workspace("tools/interactor-governance-func-calls"); + .use_chain_simulator(config.is_chain_simulator()) + .with_current_dir(env!("CARGO_MANIFEST_DIR")); let owner = interactor.register_wallet(test_wallets::eve()).await; let user1 = interactor.register_wallet(test_wallets::mike()).await; let user2 = interactor.register_wallet(test_wallets::judy()).await; diff --git a/tools/interactor-system-func-calls/src/system_sc_interact.rs b/tools/interactor-system-func-calls/src/system_sc_interact.rs index 0ade6a184e..756ca6b8f5 100644 --- a/tools/interactor-system-func-calls/src/system_sc_interact.rs +++ b/tools/interactor-system-func-calls/src/system_sc_interact.rs @@ -238,9 +238,8 @@ impl SysFuncCallsInteract { pub async fn init(config: Config) -> Self { let mut interactor = Interactor::new(config.gateway_uri()) .await - .use_chain_simulator(config.is_chain_simulator()); - - interactor.set_current_dir_from_workspace("tools/interactor-system-func-calls"); + .use_chain_simulator(config.is_chain_simulator()) + .with_current_dir(env!("CARGO_MANIFEST_DIR")); let wallet_address = interactor.register_wallet(test_wallets::carol()).await; let other_wallet_address = interactor.register_wallet(test_wallets::carol()).await; From 86ef51118cc51bd42779adfc81217fad9800185d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 11:31:44 +0300 Subject: [PATCH 090/195] wallet miner tool --- Cargo.lock | 9 +++ Cargo.toml | 1 + tools/wallet-miner/Cargo.toml | 22 +++++++ tools/wallet-miner/src/main.rs | 61 +++++++++++++++++++ ...5a8831202cfb691ec8088cf09e6b6f159f0000.pem | 5 ++ ...ef61839cbc0fe22fc9ae4a4172b27bf4370000.pem | 5 ++ ...56bf323937042c8686fe27861c0501e38f0001.pem | 5 ++ ...75cd08653e2af110579d6e564f4726d3f70001.pem | 5 ++ 8 files changed, 113 insertions(+) create mode 100644 tools/wallet-miner/Cargo.toml create mode 100644 tools/wallet-miner/src/main.rs create mode 100644 tools/wallet-miner/wallet-0000-1ea73a6477ab57d4fc3e2b0ec45a8831202cfb691ec8088cf09e6b6f159f0000.pem create mode 100644 tools/wallet-miner/wallet-0000-4ddee4dc8db85c7fc1528912c0ef61839cbc0fe22fc9ae4a4172b27bf4370000.pem create mode 100644 tools/wallet-miner/wallet-0001-35c911a8f698db378ed3abcfb356bf323937042c8686fe27861c0501e38f0001.pem create mode 100644 tools/wallet-miner/wallet-0001-cfc162090cc8420c78150bd0ad75cd08653e2af110579d6e564f4726d3f70001.pem diff --git a/Cargo.lock b/Cargo.lock index 7d9d79e0fc..191b3a4c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5832,6 +5832,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wallet-miner" +version = "0.1.0" +dependencies = [ + "anyhow", + "multiversx-sc-meta", + "multiversx-sdk", +] + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index c4f22840d7..f8412f7b8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "tools/mxpy-snippet-generator", "tools/op-test-gen", "tools/strip-path", + "tools/wallet-miner", "tools/wat-gen", "contracts/modules", diff --git a/tools/wallet-miner/Cargo.toml b/tools/wallet-miner/Cargo.toml new file mode 100644 index 0000000000..3713e1d493 --- /dev/null +++ b/tools/wallet-miner/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "wallet-miner" +version = "0.1.0" +edition = "2024" +publish = false + +description = "Mines wallets with selected hex address suffixes" + +[[bin]] +name = "wallet-miner" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" + +[dependencies.multiversx-sc-meta] +version = "=0.66.2" +path = "../../framework/meta" + +[dependencies.multiversx-sdk] +version = "=0.16.1" +path = "../../sdk/core" diff --git a/tools/wallet-miner/src/main.rs b/tools/wallet-miner/src/main.rs new file mode 100644 index 0000000000..34e515c5d1 --- /dev/null +++ b/tools/wallet-miner/src/main.rs @@ -0,0 +1,61 @@ +use std::{ + fs::File, + io::{self, Write}, +}; + +use anyhow::Result; +use multiversx_sc_meta::cmd::wallet_cmd::generate_mnemonic; +use multiversx_sdk::{chain_core::std::Bech32Hrp, wallet::Wallet}; + +const SUFFIXES: [&str; 3] = ["0000", "0001", "0002"]; +const HRP: &str = "erd"; + +fn main() -> Result<()> { + let hrp = Bech32Hrp::try_from(HRP).expect("invalid HRP"); + let mut mined_count = 0usize; + let mut retries = 0u64; + + loop { + let mnemonic = generate_mnemonic(); + let wallet = Wallet::try_from(mnemonic.clone()).unwrap(); + let address_hex = wallet.address.to_hex(); + + for suffix in SUFFIXES { + if !address_hex.ends_with(suffix) { + continue; + } + + mined_count += 1; + + if retries >= 1000 { + println!("\rMining suffixes: found 0x{suffix} after {retries} retries"); + } + + println!("Mined wallet ending in 0x{suffix}:"); + println!("Mnemonic: {}", mnemonic); + println!("Wallet address:"); + println!(" - bech32: {}", wallet.address.to_bech32(hrp.clone())); + println!(" - hex: 0x{address_hex}"); + + let pem_content = wallet.to_pem(hrp.clone()).to_pem_str(); + write_pem(suffix, &address_hex, pem_content)?; + + println!(); + } + + retries += 1; + if retries % 100 == 0 { + print!("\rMining suffixes forever: {retries} retries, {mined_count} wallets mined"); + io::stdout().flush()?; + } + } +} + +fn write_pem(suffix: &str, address_hex: &str, contents: String) -> Result<()> { + let outfile = format!("wallet-{suffix}-{address_hex}.pem"); + let mut file = File::create(&outfile)?; + file.write_all(contents.as_bytes())?; + println!("Wallet saved to '{outfile}'"); + + Ok(()) +} diff --git a/tools/wallet-miner/wallet-0000-1ea73a6477ab57d4fc3e2b0ec45a8831202cfb691ec8088cf09e6b6f159f0000.pem b/tools/wallet-miner/wallet-0000-1ea73a6477ab57d4fc3e2b0ec45a8831202cfb691ec8088cf09e6b6f159f0000.pem new file mode 100644 index 0000000000..4e7c94fb27 --- /dev/null +++ b/tools/wallet-miner/wallet-0000-1ea73a6477ab57d4fc3e2b0ec45a8831202cfb691ec8088cf09e6b6f159f0000.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1r6nn5erh4dtaflp79v8vgk5gxysze7mfrmyq3r8sne4k79vlqqqq65skcs----- +ZGUyMzBiYTNhMzAzZmY3MzQwYTlhOTNjOWQ5YzUyZmVkNmQ4ODE4MDA2YjFiMDli +YTY3NjRhYWM2ZDM1ODc3YTFlYTczYTY0NzdhYjU3ZDRmYzNlMmIwZWM0NWE4ODMx +MjAyY2ZiNjkxZWM4MDg4Y2YwOWU2YjZmMTU5ZjAwMDA= +-----END PRIVATE KEY for erd1r6nn5erh4dtaflp79v8vgk5gxysze7mfrmyq3r8sne4k79vlqqqq65skcs----- diff --git a/tools/wallet-miner/wallet-0000-4ddee4dc8db85c7fc1528912c0ef61839cbc0fe22fc9ae4a4172b27bf4370000.pem b/tools/wallet-miner/wallet-0000-4ddee4dc8db85c7fc1528912c0ef61839cbc0fe22fc9ae4a4172b27bf4370000.pem new file mode 100644 index 0000000000..f97b1671dc --- /dev/null +++ b/tools/wallet-miner/wallet-0000-4ddee4dc8db85c7fc1528912c0ef61839cbc0fe22fc9ae4a4172b27bf4370000.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1fh0wfhydhpw8ls2j3yfvpmmpswwtcrlz9ly6ujjpw2e8haphqqqq33dy5u----- +MTUwNDdmMGMyYWYwMTVmZTQ2MTM3YzJlYWU1MTMyM2VmNDAyN2YyOWI5MzJkYWQ5 +MzA5MDZjNDY1YmE2OGEyNjRkZGVlNGRjOGRiODVjN2ZjMTUyODkxMmMwZWY2MTgz +OWNiYzBmZTIyZmM5YWU0YTQxNzJiMjdiZjQzNzAwMDA= +-----END PRIVATE KEY for erd1fh0wfhydhpw8ls2j3yfvpmmpswwtcrlz9ly6ujjpw2e8haphqqqq33dy5u----- diff --git a/tools/wallet-miner/wallet-0001-35c911a8f698db378ed3abcfb356bf323937042c8686fe27861c0501e38f0001.pem b/tools/wallet-miner/wallet-0001-35c911a8f698db378ed3abcfb356bf323937042c8686fe27861c0501e38f0001.pem new file mode 100644 index 0000000000..07e925c271 --- /dev/null +++ b/tools/wallet-miner/wallet-0001-35c911a8f698db378ed3abcfb356bf323937042c8686fe27861c0501e38f0001.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1xhy3r28knrdn0rkn408mx44lxgunwppvs6r0ufuxrszsrcu0qqqsay75y8----- +NWM0MTA5Njk4Mjk5YTdjNDc5Y2I4YzBhZWQ4MjAxOWIzY2IxYmFjNjUyMTdmN2Ez +YjBlNWRhOGJjYzk4YzljNjM1YzkxMWE4ZjY5OGRiMzc4ZWQzYWJjZmIzNTZiZjMy +MzkzNzA0MmM4Njg2ZmUyNzg2MWMwNTAxZTM4ZjAwMDE= +-----END PRIVATE KEY for erd1xhy3r28knrdn0rkn408mx44lxgunwppvs6r0ufuxrszsrcu0qqqsay75y8----- diff --git a/tools/wallet-miner/wallet-0001-cfc162090cc8420c78150bd0ad75cd08653e2af110579d6e564f4726d3f70001.pem b/tools/wallet-miner/wallet-0001-cfc162090cc8420c78150bd0ad75cd08653e2af110579d6e564f4726d3f70001.pem new file mode 100644 index 0000000000..a5609aace0 --- /dev/null +++ b/tools/wallet-miner/wallet-0001-cfc162090cc8420c78150bd0ad75cd08653e2af110579d6e564f4726d3f70001.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1elqkyzgveppqc7q4p0g26awdppjnu2h3zpte6mjkfarjd5lhqqqsmyk8vf----- +Mjc0Y2ZiMTM2Yjg4ZTUwZjE0OTFkNThmY2EwZGJmOWQzMGMzZGVhMGNjMDI3ZjM4 +MDQzYWRjOGE2NTQyNDkxZmNmYzE2MjA5MGNjODQyMGM3ODE1MGJkMGFkNzVjZDA4 +NjUzZTJhZjExMDU3OWQ2ZTU2NGY0NzI2ZDNmNzAwMDE= +-----END PRIVATE KEY for erd1elqkyzgveppqc7q4p0g26awdppjnu2h3zpte6mjkfarjd5lhqqqsmyk8vf----- From b1550d503d28cd3992c7a150eb0149f0a4b72301 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 11:44:39 +0300 Subject: [PATCH 091/195] more shard-based test wallets --- sdk/core/src/test_wallets.rs | 70 +++++++++++++++++++++++++ sdk/core/src/test_wallets/s0crates.pem | 5 ++ sdk/core/src/test_wallets/s0phocles.pem | 5 ++ sdk/core/src/test_wallets/s1lvia.pem | 5 ++ sdk/core/src/test_wallets/s1obhan.pem | 5 ++ sdk/core/src/test_wallets/s2abolcs.pem | 5 ++ sdk/core/src/test_wallets/s2ilard.pem | 5 ++ sdk/core/tests/wallet_test.rs | 12 +++++ tools/wallet-miner/src/main.rs | 9 ++-- 9 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 sdk/core/src/test_wallets/s0crates.pem create mode 100644 sdk/core/src/test_wallets/s0phocles.pem create mode 100644 sdk/core/src/test_wallets/s1lvia.pem create mode 100644 sdk/core/src/test_wallets/s1obhan.pem create mode 100644 sdk/core/src/test_wallets/s2abolcs.pem create mode 100644 sdk/core/src/test_wallets/s2ilard.pem diff --git a/sdk/core/src/test_wallets.rs b/sdk/core/src/test_wallets.rs index b0a8c25f4b..3b930e5ba5 100644 --- a/sdk/core/src/test_wallets.rs +++ b/sdk/core/src/test_wallets.rs @@ -14,8 +14,14 @@ const IVAN_PEM: &str = include_str!("test_wallets/ivan.pem"); const JUDY_PEM: &str = include_str!("test_wallets/judy.pem"); const MALLORY_PEM: &str = include_str!("test_wallets/mallory.pem"); const MIKE_PEM: &str = include_str!("test_wallets/mike.pem"); +const SOCRATES_PEM: &str = include_str!("test_wallets/s0crates.pem"); +const SOPHOCLES_PEM: &str = include_str!("test_wallets/s0phocles.pem"); const SOPHIE_PEM: &str = include_str!("test_wallets/s0phie.pem"); +const SILVIA_PEM: &str = include_str!("test_wallets/s1lvia.pem"); +const SIOBHAN_PEM: &str = include_str!("test_wallets/s1obhan.pem"); const SIMON_PEM: &str = include_str!("test_wallets/s1mon.pem"); +const SZABOLCS_PEM: &str = include_str!("test_wallets/s2abolcs.pem"); +const SZILARD_PEM: &str = include_str!("test_wallets/s2ilard.pem"); const SZONJA_PEM: &str = include_str!("test_wallets/s2onja.pem"); const WALLETS: &[(&str, &str)] = &[ @@ -31,10 +37,20 @@ const WALLETS: &[(&str, &str)] = &[ ("judy", JUDY_PEM), ("mallory", MALLORY_PEM), ("mike", MIKE_PEM), + ("s0crates", SOCRATES_PEM), + ("socrates", SOCRATES_PEM), + ("s0phocles", SOPHOCLES_PEM), + ("sophocles", SOPHOCLES_PEM), ("sophie", SOPHIE_PEM), ("s0phie", SOPHIE_PEM), + ("siobhan", SIOBHAN_PEM), + ("s1obhan", SIOBHAN_PEM), ("simon", SIMON_PEM), ("s1mon", SIMON_PEM), + ("szabolcs", SZABOLCS_PEM), + ("s2abolcs", SZABOLCS_PEM), + ("szilard", SZILARD_PEM), + ("s2ilard", SZILARD_PEM), ("szonja", SZONJA_PEM), ("s2onja", SZONJA_PEM), ]; @@ -99,6 +115,24 @@ pub fn mike() -> Wallet { Wallet::new_test_wallet("mike", MIKE_PEM) } +/// Test wallet. Do not use on mainnet. +/// +/// S0crates' wallet will always be in shard 0. +/// +/// Address: 0x3d55ffd949781d5f0b5eaf57a3f0797d1db2d76a759ae6df7e335302b7d90000 +pub fn socrates() -> Wallet { + Wallet::new_test_wallet("s0crates", SOCRATES_PEM) +} + +/// Test wallet. Do not use on mainnet. +/// +/// S0phocles' wallet will always be in shard 0. +/// +/// Address: 0x35e2358aa3191bcedf6eddf6e14f138765b38a704647ed691130703852620000 +pub fn sophocles() -> Wallet { + Wallet::new_test_wallet("s0phocles", SOPHOCLES_PEM) +} + /// Test wallet. Do not use on mainnet. /// /// Sophie's wallet will always be in shard 0. @@ -108,6 +142,24 @@ pub fn sophie() -> Wallet { Wallet::new_test_wallet("sophie", SOPHIE_PEM) } +/// Test wallet. Do not use on mainnet. +/// +/// S1obhan's wallet will always be in shard 1. +/// +/// Address: 0xe624b62f5dcad961ceaf9ce23e56db72377ea8a8dcc7065b73089778522d0001 +pub fn siobhan() -> Wallet { + Wallet::new_test_wallet("s1obhan", SIOBHAN_PEM) +} + +/// Test wallet. Do not use on mainnet. +/// +/// Silvia's wallet will always be in shard 1. +/// +/// Address: 0x7bbaef4fae6aa454929e0038bf01da4907e7814609db6e46c9990b5ae9d30001 +pub fn silvia() -> Wallet { + Wallet::new_test_wallet("silvia", SILVIA_PEM) +} + /// Test wallet. Do not use on mainnet. /// /// Simon's wallet will always be in shard 1. @@ -117,6 +169,24 @@ pub fn simon() -> Wallet { Wallet::new_test_wallet("simon", SIMON_PEM) } +/// Test wallet. Do not use on mainnet. +/// +/// Szabolcs' wallet will always be in shard 2. +/// +/// Address: 0x99337fe8455f5798fc548037c1ceea4d95d8f89ca468663877719f7d31eb0002 +pub fn szabolcs() -> Wallet { + Wallet::new_test_wallet("szabolcs", SZABOLCS_PEM) +} + +/// Test wallet. Do not use on mainnet. +/// +/// S2ilard's wallet will always be in shard 2. +/// +/// Address: 0xf7a7c49bb4d2f63fd82ca0859f2e01c13f320a79c0962dfdc43fcb621cde0002 +pub fn szilard() -> Wallet { + Wallet::new_test_wallet("s2ilard", SZILARD_PEM) +} + /// Test wallet. Do not use on mainnet. /// /// Szonja's wallet will always be in shard 2. diff --git a/sdk/core/src/test_wallets/s0crates.pem b/sdk/core/src/test_wallets/s0crates.pem new file mode 100644 index 0000000000..688a25aa2b --- /dev/null +++ b/sdk/core/src/test_wallets/s0crates.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1842llk2f0qw47z674at68ure05wm94m2wkdwdhm7xdfs9d7eqqqqs2ej24----- +Y2YwZWFhOWYwNjMzZmMxNDUxNGY5MmE1NjAzNjYzZjAyZmJjOTg0ODc0YTJjZmE2 +NDcwOTA3ZmY3OTBiMTdlMjNkNTVmZmQ5NDk3ODFkNWYwYjVlYWY1N2EzZjA3OTdk +MWRiMmQ3NmE3NTlhZTZkZjdlMzM1MzAyYjdkOTAwMDA= +-----END PRIVATE KEY for erd1842llk2f0qw47z674at68ure05wm94m2wkdwdhm7xdfs9d7eqqqqs2ej24----- diff --git a/sdk/core/src/test_wallets/s0phocles.pem b/sdk/core/src/test_wallets/s0phocles.pem new file mode 100644 index 0000000000..c8693b25a1 --- /dev/null +++ b/sdk/core/src/test_wallets/s0phocles.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1xh3rtz4rryduahmwmhmwzncnsajm8znsger766g3xpcrs5nzqqqq9me2a6----- +M2Y4MTczZjA0ZmUxOGZjOTk3NDA0M2UzNTIzNGY4YWM3YzBlNjAzYzE0MjljYjU5 +NGEzZjk2MjA2MGE1ZTc0YzM1ZTIzNThhYTMxOTFiY2VkZjZlZGRmNmUxNGYxMzg3 +NjViMzhhNzA0NjQ3ZWQ2OTExMzA3MDM4NTI2MjAwMDA= +-----END PRIVATE KEY for erd1xh3rtz4rryduahmwmhmwzncnsajm8znsger766g3xpcrs5nzqqqq9me2a6----- diff --git a/sdk/core/src/test_wallets/s1lvia.pem b/sdk/core/src/test_wallets/s1lvia.pem new file mode 100644 index 0000000000..5d0ae1adba --- /dev/null +++ b/sdk/core/src/test_wallets/s1lvia.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd10waw7nawd2j9fy57qqut7qw6fyr70q2xp8dku3kfny9446wnqqqsx93xxl----- +ZTNhZjA2Y2NhNzhlNDMzMzZhNDIyMGE2NTRhZmJlNTRhYTNiNDlmNDkzNGE0ODdi +ZTg2Yjc5MmQ2Y2UwZDgyNTdiYmFlZjRmYWU2YWE0NTQ5MjllMDAzOGJmMDFkYTQ5 +MDdlNzgxNDYwOWRiNmU0NmM5OTkwYjVhZTlkMzAwMDE= +-----END PRIVATE KEY for erd10waw7nawd2j9fy57qqut7qw6fyr70q2xp8dku3kfny9446wnqqqsx93xxl----- diff --git a/sdk/core/src/test_wallets/s1obhan.pem b/sdk/core/src/test_wallets/s1obhan.pem new file mode 100644 index 0000000000..32cefd8395 --- /dev/null +++ b/sdk/core/src/test_wallets/s1obhan.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1ucjtvt6aetvkrn40nn3ru4kmwgmha29gmnrsvkmnpzths53dqqqs03kssu----- +YjI4ZGMxOWQxZjgyZjNkZTdiN2RlNWYyMDIwMTYxYTQ1NjM0MWE0OWE4YjBmMjgy +OWNkNzhkNDViMmIyMDIwNWU2MjRiNjJmNWRjYWQ5NjFjZWFmOWNlMjNlNTZkYjcy +Mzc3ZWE4YThkY2M3MDY1YjczMDg5Nzc4NTIyZDAwMDE= +-----END PRIVATE KEY for erd1ucjtvt6aetvkrn40nn3ru4kmwgmha29gmnrsvkmnpzths53dqqqs03kssu----- diff --git a/sdk/core/src/test_wallets/s2abolcs.pem b/sdk/core/src/test_wallets/s2abolcs.pem new file mode 100644 index 0000000000..99bbf72b7e --- /dev/null +++ b/sdk/core/src/test_wallets/s2abolcs.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd1nyehl6z9tate3lz5sqmurnh2fk2a37yu535xvwrhwx0h6v0tqqpqmj2rw7----- +ZmFiMjVkM2MxYmU4OWM0Njc5MmI0MDUzOWU5MmQ0NjVlMWI3NmI2ZjZkOTNjMGJl +NzBkZjEyYzYzMGJkNDUzYjk5MzM3ZmU4NDU1ZjU3OThmYzU0ODAzN2MxY2VlYTRk +OTVkOGY4OWNhNDY4NjYzODc3NzE5ZjdkMzFlYjAwMDI= +-----END PRIVATE KEY for erd1nyehl6z9tate3lz5sqmurnh2fk2a37yu535xvwrhwx0h6v0tqqpqmj2rw7----- diff --git a/sdk/core/src/test_wallets/s2ilard.pem b/sdk/core/src/test_wallets/s2ilard.pem new file mode 100644 index 0000000000..34cfe722e5 --- /dev/null +++ b/sdk/core/src/test_wallets/s2ilard.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY for erd177nufxa56tmrlkpv5zze7tspcylnyznecztzmlwy8l9ky8x7qqpqlfthht----- +YTYwN2FjZWFjMDA2ZmYxZGZmNzIyZmRmZmIxODdlMGVlZDA5YjFhNGIxZWM2MDlj +MzU2ZTk2Mjk4MmQ4MDcxZmY3YTdjNDliYjRkMmY2M2ZkODJjYTA4NTlmMmUwMWMx +M2YzMjBhNzljMDk2MmRmZGM0M2ZjYjYyMWNkZTAwMDI= +-----END PRIVATE KEY for erd177nufxa56tmrlkpv5zze7tspcylnyznecztzmlwy8l9ky8x7qqpqlfthht----- diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index e51f5f4bad..3962a0612b 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -55,4 +55,16 @@ fn test_get_shard() { let mike = test_wallets::mike(); // [227, 42, 254, 220, 144, 79, 225, 147, 151, 70, 173, 151, 59, 235, 56, 53, 99, 207, 99, 100, 43, 166, 105, 179, 4, 15, 155, 148, 40, 165, 237, 96] assert_eq!(0, mike.address.shard_of_3().as_u32()); + + assert_eq!(0, test_wallets::socrates().address.shard_of_3().as_u32()); + assert_eq!(0, test_wallets::sophocles().address.shard_of_3().as_u32()); + assert_eq!(0, test_wallets::sophie().address.shard_of_3().as_u32()); + + assert_eq!(1, test_wallets::silvia().address.shard_of_3().as_u32()); + assert_eq!(1, test_wallets::siobhan().address.shard_of_3().as_u32()); + assert_eq!(1, test_wallets::simon().address.shard_of_3().as_u32()); + + assert_eq!(2, test_wallets::szabolcs().address.shard_of_3().as_u32()); + assert_eq!(2, test_wallets::szilard().address.shard_of_3().as_u32()); + assert_eq!(2, test_wallets::szonja().address.shard_of_3().as_u32()); } diff --git a/tools/wallet-miner/src/main.rs b/tools/wallet-miner/src/main.rs index 34e515c5d1..525885f31d 100644 --- a/tools/wallet-miner/src/main.rs +++ b/tools/wallet-miner/src/main.rs @@ -8,10 +8,9 @@ use multiversx_sc_meta::cmd::wallet_cmd::generate_mnemonic; use multiversx_sdk::{chain_core::std::Bech32Hrp, wallet::Wallet}; const SUFFIXES: [&str; 3] = ["0000", "0001", "0002"]; -const HRP: &str = "erd"; fn main() -> Result<()> { - let hrp = Bech32Hrp::try_from(HRP).expect("invalid HRP"); + let hrp = Bech32Hrp::default(); let mut mined_count = 0usize; let mut retries = 0u64; @@ -34,17 +33,17 @@ fn main() -> Result<()> { println!("Mined wallet ending in 0x{suffix}:"); println!("Mnemonic: {}", mnemonic); println!("Wallet address:"); - println!(" - bech32: {}", wallet.address.to_bech32(hrp.clone())); + println!(" - bech32: {}", wallet.address.to_bech32(hrp)); println!(" - hex: 0x{address_hex}"); - let pem_content = wallet.to_pem(hrp.clone()).to_pem_str(); + let pem_content = wallet.to_pem(hrp).to_pem_str(); write_pem(suffix, &address_hex, pem_content)?; println!(); } retries += 1; - if retries % 100 == 0 { + if retries.is_multiple_of(100) { print!("\rMining suffixes forever: {retries} retries, {mined_count} wallets mined"); io::stdout().flush()?; } From 95a7d023999a30a4a6c146384420cff168cb85d5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 11:49:22 +0300 Subject: [PATCH 092/195] test/clippy fix --- tools/wallet-miner/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/wallet-miner/src/main.rs b/tools/wallet-miner/src/main.rs index 525885f31d..be12691fd0 100644 --- a/tools/wallet-miner/src/main.rs +++ b/tools/wallet-miner/src/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::manual_is_multiple_of)] + use std::{ fs::File, io::{self, Write}, @@ -43,7 +45,7 @@ fn main() -> Result<()> { } retries += 1; - if retries.is_multiple_of(100) { + if retries % 100 == 0 { print!("\rMining suffixes forever: {retries} retries, {mined_count} wallets mined"); io::stdout().flush()?; } From dc3c7be2f4d5fd783afc95d74dac0aa170973174 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 12:07:37 +0300 Subject: [PATCH 093/195] fixes after review --- sdk/core/src/test_wallets.rs | 20 +++++++++++--------- tools/wallet-miner/src/main.rs | 5 +++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/sdk/core/src/test_wallets.rs b/sdk/core/src/test_wallets.rs index 3b930e5ba5..a1b57d4f62 100644 --- a/sdk/core/src/test_wallets.rs +++ b/sdk/core/src/test_wallets.rs @@ -45,6 +45,8 @@ const WALLETS: &[(&str, &str)] = &[ ("s0phie", SOPHIE_PEM), ("siobhan", SIOBHAN_PEM), ("s1obhan", SIOBHAN_PEM), + ("silvia", SILVIA_PEM), + ("s1lvia", SILVIA_PEM), ("simon", SIMON_PEM), ("s1mon", SIMON_PEM), ("szabolcs", SZABOLCS_PEM), @@ -117,20 +119,20 @@ pub fn mike() -> Wallet { /// Test wallet. Do not use on mainnet. /// -/// S0crates' wallet will always be in shard 0. +/// Socrates' wallet will always be in shard 0. /// /// Address: 0x3d55ffd949781d5f0b5eaf57a3f0797d1db2d76a759ae6df7e335302b7d90000 pub fn socrates() -> Wallet { - Wallet::new_test_wallet("s0crates", SOCRATES_PEM) + Wallet::new_test_wallet("socrates", SOCRATES_PEM) } /// Test wallet. Do not use on mainnet. /// -/// S0phocles' wallet will always be in shard 0. +/// Sophocles' wallet will always be in shard 0. /// /// Address: 0x35e2358aa3191bcedf6eddf6e14f138765b38a704647ed691130703852620000 pub fn sophocles() -> Wallet { - Wallet::new_test_wallet("s0phocles", SOPHOCLES_PEM) + Wallet::new_test_wallet("sophocles", SOPHOCLES_PEM) } /// Test wallet. Do not use on mainnet. @@ -144,11 +146,11 @@ pub fn sophie() -> Wallet { /// Test wallet. Do not use on mainnet. /// -/// S1obhan's wallet will always be in shard 1. +/// Siobhan's wallet will always be in shard 1. /// /// Address: 0xe624b62f5dcad961ceaf9ce23e56db72377ea8a8dcc7065b73089778522d0001 pub fn siobhan() -> Wallet { - Wallet::new_test_wallet("s1obhan", SIOBHAN_PEM) + Wallet::new_test_wallet("siobhan", SIOBHAN_PEM) } /// Test wallet. Do not use on mainnet. @@ -171,7 +173,7 @@ pub fn simon() -> Wallet { /// Test wallet. Do not use on mainnet. /// -/// Szabolcs' wallet will always be in shard 2. +/// Szabolcs's wallet will always be in shard 2. /// /// Address: 0x99337fe8455f5798fc548037c1ceea4d95d8f89ca468663877719f7d31eb0002 pub fn szabolcs() -> Wallet { @@ -180,11 +182,11 @@ pub fn szabolcs() -> Wallet { /// Test wallet. Do not use on mainnet. /// -/// S2ilard's wallet will always be in shard 2. +/// Szilárd's wallet will always be in shard 2. /// /// Address: 0xf7a7c49bb4d2f63fd82ca0859f2e01c13f320a79c0962dfdc43fcb621cde0002 pub fn szilard() -> Wallet { - Wallet::new_test_wallet("s2ilard", SZILARD_PEM) + Wallet::new_test_wallet("szilard", SZILARD_PEM) } /// Test wallet. Do not use on mainnet. diff --git a/tools/wallet-miner/src/main.rs b/tools/wallet-miner/src/main.rs index be12691fd0..a392e677cb 100644 --- a/tools/wallet-miner/src/main.rs +++ b/tools/wallet-miner/src/main.rs @@ -18,7 +18,7 @@ fn main() -> Result<()> { loop { let mnemonic = generate_mnemonic(); - let wallet = Wallet::try_from(mnemonic.clone()).unwrap(); + let wallet = Wallet::try_from(mnemonic.clone())?; let address_hex = wallet.address.to_hex(); for suffix in SUFFIXES { @@ -53,7 +53,8 @@ fn main() -> Result<()> { } fn write_pem(suffix: &str, address_hex: &str, contents: String) -> Result<()> { - let outfile = format!("wallet-{suffix}-{address_hex}.pem"); + let dir = env!("CARGO_MANIFEST_DIR"); + let outfile = format!("{dir}/wallet-{suffix}-{address_hex}.pem"); let mut file = File::create(&outfile)?; file.write_all(contents.as_bytes())?; println!("Wallet saved to '{outfile}'"); From 682e1a0a90ed1e343df5883a63b9a22958d4b636 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 12:22:44 +0300 Subject: [PATCH 094/195] test wallet update --- sdk/core/src/test_wallets.rs | 12 ++++++------ .../src/test_wallets/{s2abolcs.pem => s2idonia.pem} | 0 sdk/core/tests/wallet_test.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename sdk/core/src/test_wallets/{s2abolcs.pem => s2idonia.pem} (100%) diff --git a/sdk/core/src/test_wallets.rs b/sdk/core/src/test_wallets.rs index a1b57d4f62..416cd8162f 100644 --- a/sdk/core/src/test_wallets.rs +++ b/sdk/core/src/test_wallets.rs @@ -20,7 +20,7 @@ const SOPHIE_PEM: &str = include_str!("test_wallets/s0phie.pem"); const SILVIA_PEM: &str = include_str!("test_wallets/s1lvia.pem"); const SIOBHAN_PEM: &str = include_str!("test_wallets/s1obhan.pem"); const SIMON_PEM: &str = include_str!("test_wallets/s1mon.pem"); -const SZABOLCS_PEM: &str = include_str!("test_wallets/s2abolcs.pem"); +const SZIDONIA_PEM: &str = include_str!("test_wallets/s2idonia.pem"); const SZILARD_PEM: &str = include_str!("test_wallets/s2ilard.pem"); const SZONJA_PEM: &str = include_str!("test_wallets/s2onja.pem"); @@ -49,8 +49,8 @@ const WALLETS: &[(&str, &str)] = &[ ("s1lvia", SILVIA_PEM), ("simon", SIMON_PEM), ("s1mon", SIMON_PEM), - ("szabolcs", SZABOLCS_PEM), - ("s2abolcs", SZABOLCS_PEM), + ("szidonia", SZIDONIA_PEM), + ("s2idonia", SZIDONIA_PEM), ("szilard", SZILARD_PEM), ("s2ilard", SZILARD_PEM), ("szonja", SZONJA_PEM), @@ -173,11 +173,11 @@ pub fn simon() -> Wallet { /// Test wallet. Do not use on mainnet. /// -/// Szabolcs's wallet will always be in shard 2. +/// Szidonia's wallet will always be in shard 2. /// /// Address: 0x99337fe8455f5798fc548037c1ceea4d95d8f89ca468663877719f7d31eb0002 -pub fn szabolcs() -> Wallet { - Wallet::new_test_wallet("szabolcs", SZABOLCS_PEM) +pub fn szidonia() -> Wallet { + Wallet::new_test_wallet("szidonia", SZIDONIA_PEM) } /// Test wallet. Do not use on mainnet. diff --git a/sdk/core/src/test_wallets/s2abolcs.pem b/sdk/core/src/test_wallets/s2idonia.pem similarity index 100% rename from sdk/core/src/test_wallets/s2abolcs.pem rename to sdk/core/src/test_wallets/s2idonia.pem diff --git a/sdk/core/tests/wallet_test.rs b/sdk/core/tests/wallet_test.rs index 3962a0612b..523d59730c 100644 --- a/sdk/core/tests/wallet_test.rs +++ b/sdk/core/tests/wallet_test.rs @@ -64,7 +64,7 @@ fn test_get_shard() { assert_eq!(1, test_wallets::siobhan().address.shard_of_3().as_u32()); assert_eq!(1, test_wallets::simon().address.shard_of_3().as_u32()); - assert_eq!(2, test_wallets::szabolcs().address.shard_of_3().as_u32()); + assert_eq!(2, test_wallets::szidonia().address.shard_of_3().as_u32()); assert_eq!(2, test_wallets::szilard().address.shard_of_3().as_u32()); assert_eq!(2, test_wallets::szonja().address.shard_of_3().as_u32()); } From d1b46939e5e0fcf7e92a3dc754289d9e32855978 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 13:18:56 +0300 Subject: [PATCH 095/195] ping pong cs test refactor + fix --- .../tests/ping_pong_interact_cs_test.rs | 97 +++++++++++++++---- 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs index 91ac42c389..9aff1f81da 100644 --- a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs +++ b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs @@ -5,8 +5,8 @@ use serial_test::serial; fn chain_simulator_config() -> Config { Config { connection: ConnectionConfig::chain_simulator(), - owner: WalletConfig::from_test_wallet("eve"), - wallet: WalletConfig::from_test_wallet("mallory"), + owner: WalletConfig::from_test_wallet("simon"), + wallet: WalletConfig::from_test_wallet("siobhan"), } } @@ -20,21 +20,18 @@ async fn cs_interactor() -> PingPongEgldInteract { PingPongEgldInteract { interactor, config, - state: multiversx_sc_snippets::AutoSave::no_save_default(), + state: AutoSave::no_save_default(), } } #[tokio::test] #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] -async fn test_ping_pong_egld() { +async fn test_ping_unmatched_amount() { let mut interact = cs_interactor().await; let wallet_address = interact.config.wallet.address(); - let ping_pong_owner_address = interact.config.owner.address(); let ping_amount = 1u64; - - // test_ping_unmatched_amount let duration = DurationMillis::new(5000u64); let activation_timestamp = TimestampMillis::new(2000u64); let max_funds = 100_000u64; @@ -44,7 +41,7 @@ async fn test_ping_pong_egld() { ping_amount.into(), duration, Some(activation_timestamp), - multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + OptionalValue::Some(max_funds.into()), ) .await; @@ -55,8 +52,16 @@ async fn test_ping_pong_egld() { &wallet_address, ) .await; +} - // test_ping_inactive_contracts +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_ping_inactive_contract() { + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); + + let ping_amount = 1u64; let duration = DurationMillis::new(5000); let activation_timestamp = TimestampMillis::new(2_000_000_000_000); let max_funds = 100_000u64; @@ -66,15 +71,23 @@ async fn test_ping_pong_egld() { ping_amount.into(), duration, Some(activation_timestamp), - multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + OptionalValue::Some(max_funds.into()), ) .await; interact .ping(1u64, Some("smart contract not active yet"), &wallet_address) .await; +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_ping_passed_deadline() { + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); - // test_ping_passed_deadline + let ping_amount = 1u64; let duration = DurationMillis::new(5000); let activation_timestamp = TimestampMillis::new(2_000); let max_funds = 100_000u64; @@ -84,15 +97,22 @@ async fn test_ping_pong_egld() { ping_amount.into(), duration, Some(activation_timestamp), - multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + OptionalValue::Some(max_funds.into()), ) .await; interact .ping(1u64, Some("deadline has passed"), &wallet_address) .await; +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_ping_max_funds() { + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); - // test_ping_max_funds let ping_amount = 10u64; let duration = DurationMillis::new(30_000_000); let max_funds = 10u64; @@ -102,17 +122,24 @@ async fn test_ping_pong_egld() { ping_amount.into(), duration, None, - multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + OptionalValue::Some(max_funds.into()), ) .await; interact .ping(10u64, Some("smart contract full"), &wallet_address) .await; +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_ping_twice() { + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); - // test ping let ping_amount = 1u64; - let duration = DurationMillis::new(20_000); + let duration = DurationMillis::new(20_000_000); let max_funds = 100_000u64; interact @@ -120,7 +147,7 @@ async fn test_ping_pong_egld() { ping_amount.into(), duration, None, - multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + OptionalValue::Some(max_funds.into()), ) .await; @@ -128,16 +155,45 @@ async fn test_ping_pong_egld() { interact .ping(1u64, Some("can only ping once"), &wallet_address) .await; +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_pong_before_deadline() { + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); + + let ping_amount = 1u64; + let duration = DurationMillis::new(20_000_000); + let max_funds = 100_000u64; + + interact + .deploy( + ping_amount.into(), + duration, + None, + OptionalValue::Some(max_funds.into()), + ) + .await; + + interact.ping(1u64, None, &wallet_address).await; assert_eq!(interact.get_ping_amount().await, RustBigUint::from(1u64)); interact .pong(Some("can't withdraw before deadline"), &wallet_address) .await; +} - interact.pong(None, &wallet_address).await; +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn test_pong_all() { + let mut interact = cs_interactor().await; + let wallet_address = interact.config.wallet.address(); + let ping_pong_owner_address = interact.config.owner.address(); - // test_pong_all let ping_amount = 1u64; let duration = DurationMillis::new(18_000); let max_funds = 100_000u64; @@ -147,12 +203,11 @@ async fn test_ping_pong_egld() { ping_amount.into(), duration, None, - multiversx_sc_snippets::imports::OptionalValue::Some(max_funds.into()), + OptionalValue::Some(max_funds.into()), ) .await; interact.ping(1u64, None, &ping_pong_owner_address).await; - interact.ping(1u64, None, &wallet_address).await; interact.pong_all(None, &ping_pong_owner_address).await; From 26442491e7e24c455d9d2251e5ed8ed9a07702f0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 13:28:00 +0300 Subject: [PATCH 096/195] ping pong interactor refactor --- .../ping-pong-egld/interactor/src/interact.rs | 54 +++++++++++-------- .../tests/ping_pong_interact_cs_test.rs | 49 +++++++---------- framework/scenario/src/imports.rs | 2 +- 3 files changed, 51 insertions(+), 54 deletions(-) diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact.rs b/contracts/examples/ping-pong-egld/interactor/src/interact.rs index a53eb1c51d..a02fce65ed 100644 --- a/contracts/examples/ping-pong-egld/interactor/src/interact.rs +++ b/contracts/examples/ping-pong-egld/interactor/src/interact.rs @@ -71,17 +71,24 @@ pub async fn ping_pong_egld_cli() { } Some(interact_cli::InteractCliCommand::Ping(args)) => { let sender = interact.config.owner.address(); - interact - .ping(args.cost.unwrap_or_default(), None, &sender) - .await + match interact.ping(&sender, args.cost.unwrap_or_default()).await { + Ok(_) => println!("Ping successful!"), + Err(err) => println!("Ping failed with message: {}", err.message), + } } Some(interact_cli::InteractCliCommand::Pong) => { let sender = interact.config.owner.address(); - interact.pong(None, &sender).await; + match interact.pong(&sender).await { + Ok(_) => println!("Pong successful!"), + Err(err) => println!("Pong failed with message: {}", err.message), + } } Some(interact_cli::InteractCliCommand::PongAll) => { let sender = interact.config.owner.address(); - interact.pong_all(None, &sender).await; + match interact.pong_all(&sender).await { + Ok(_) => println!("Pong All successful!"), + Err(err) => println!("Pong All failed with message: {}", err.message), + } } Some(interact_cli::InteractCliCommand::GetUserAddresses) => { let user_addresses = interact.get_user_addresses().await; @@ -219,7 +226,11 @@ impl PingPongEgldInteract { println!("Result: {response:?}"); } - pub async fn ping(&mut self, egld_amount: u64, message: Option<&str>, sender: &Bech32Address) { + pub async fn ping( + &mut self, + sender: &Bech32Address, + egld_amount: u64, + ) -> Result<(), TxResponseStatus> { let _data: IgnoreValue = IgnoreValue; let response = self @@ -235,16 +246,15 @@ impl PingPongEgldInteract { .run() .await; - match response { + match &response { Ok(_) => println!("Ping successful!"), - Err(err) => { - println!("Ping failed with message: {}", err.message); - assert_eq!(message.unwrap_or_default(), err.message); - } + Err(err) => println!("Ping failed with message: {}", err.message), } + + response.map(|_| ()) } - pub async fn pong(&mut self, message: Option<&str>, sender: &Bech32Address) { + pub async fn pong(&mut self, sender: &Bech32Address) -> Result<(), TxResponseStatus> { let response = self .interactor .tx() @@ -257,16 +267,15 @@ impl PingPongEgldInteract { .run() .await; - match response { + match &response { Ok(_) => println!("Pong successful!"), - Err(err) => { - println!("Pong failed with message: {}", err.message); - assert_eq!(message.unwrap_or_default(), err.message); - } + Err(err) => println!("Pong failed with message: {}", err.message), } + + response.map(|_| ()) } - pub async fn pong_all(&mut self, message: Option, sender: &Bech32Address) { + pub async fn pong_all(&mut self, sender: &Bech32Address) -> Result<(), TxResponseStatus> { let response = self .interactor .tx() @@ -279,13 +288,12 @@ impl PingPongEgldInteract { .run() .await; - match response { + match &response { Ok(_) => println!("Pong All successful!"), - Err(err) => { - println!("Pong All failed with message: {}", err.message); - assert_eq!(message.unwrap_or_default(), err.message); - } + Err(err) => println!("Pong All failed with message: {}", err.message), } + + response.map(|_| ()) } pub async fn get_user_addresses(&mut self) -> Vec { diff --git a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs index 9aff1f81da..fad4d95755 100644 --- a/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs +++ b/contracts/examples/ping-pong-egld/interactor/tests/ping_pong_interact_cs_test.rs @@ -45,13 +45,8 @@ async fn test_ping_unmatched_amount() { ) .await; - interact - .ping( - 0u64, - Some("the payment must match the fixed sum"), - &wallet_address, - ) - .await; + let err = interact.ping(&wallet_address, 0u64).await.unwrap_err(); + assert_eq!(err.message, "the payment must match the fixed sum"); } #[tokio::test] @@ -75,9 +70,8 @@ async fn test_ping_inactive_contract() { ) .await; - interact - .ping(1u64, Some("smart contract not active yet"), &wallet_address) - .await; + let err = interact.ping(&wallet_address, 1u64).await.unwrap_err(); + assert_eq!(err.message, "smart contract not active yet"); } #[tokio::test] @@ -101,9 +95,8 @@ async fn test_ping_passed_deadline() { ) .await; - interact - .ping(1u64, Some("deadline has passed"), &wallet_address) - .await; + let err = interact.ping(&wallet_address, 1u64).await.unwrap_err(); + assert_eq!(err.message, "deadline has passed"); } #[tokio::test] @@ -126,9 +119,8 @@ async fn test_ping_max_funds() { ) .await; - interact - .ping(10u64, Some("smart contract full"), &wallet_address) - .await; + let err = interact.ping(&wallet_address, 10u64).await.unwrap_err(); + assert_eq!(err.message, "smart contract full"); } #[tokio::test] @@ -151,10 +143,9 @@ async fn test_ping_twice() { ) .await; - interact.ping(1u64, None, &wallet_address).await; - interact - .ping(1u64, Some("can only ping once"), &wallet_address) - .await; + interact.ping(&wallet_address, 1u64).await.unwrap(); + let err = interact.ping(&wallet_address, 1u64).await.unwrap_err(); + assert_eq!(err.message, "can only ping once"); } #[tokio::test] @@ -177,13 +168,12 @@ async fn test_pong_before_deadline() { ) .await; - interact.ping(1u64, None, &wallet_address).await; + interact.ping(&wallet_address, 1u64).await.unwrap(); assert_eq!(interact.get_ping_amount().await, RustBigUint::from(1u64)); - interact - .pong(Some("can't withdraw before deadline"), &wallet_address) - .await; + let err = interact.pong(&wallet_address).await.unwrap_err(); + assert_eq!(err.message, "can't withdraw before deadline"); } #[tokio::test] @@ -207,11 +197,10 @@ async fn test_pong_all() { ) .await; - interact.ping(1u64, None, &ping_pong_owner_address).await; - interact.ping(1u64, None, &wallet_address).await; + interact.ping(&ping_pong_owner_address, 1u64).await.unwrap(); + interact.ping(&wallet_address, 1u64).await.unwrap(); - interact.pong_all(None, &ping_pong_owner_address).await; - interact - .pong(Some("already withdrawn"), &wallet_address) - .await; + interact.pong_all(&ping_pong_owner_address).await.unwrap(); + let err = interact.pong(&wallet_address).await.unwrap_err(); + assert_eq!(err.message, "already withdrawn"); } diff --git a/framework/scenario/src/imports.rs b/framework/scenario/src/imports.rs index 64b3d06399..4ba2cdf88e 100644 --- a/framework/scenario/src/imports.rs +++ b/framework/scenario/src/imports.rs @@ -18,7 +18,7 @@ pub use crate::{ model::{ Account, AddressValue, BytesValue, CheckAccount, CheckStateStep, ScCallStep, ScDeployStep, ScQueryStep, Scenario, SetStateStep, TransferStep, TxESDT, TxExpect, - TypedResponse, + TxResponseStatus, TypedResponse, }, run_vm::ExecutorConfig, }, From 34fd317e050ea30e327960fe8e0a10c4338743d8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 14:39:44 +0300 Subject: [PATCH 097/195] multisig interactor refactor, config --- .../examples/multisig/interact/config.toml | 19 ++++++ .../interact/src/multisig_interact.rs | 63 +++++++------------ .../interact/src/multisig_interact_config.rs | 46 +++++--------- .../interact/src/multisig_interact_nfts.rs | 12 ++-- .../interact/src/multisig_interact_wegld.rs | 14 ++--- 5 files changed, 69 insertions(+), 85 deletions(-) diff --git a/contracts/examples/multisig/interact/config.toml b/contracts/examples/multisig/interact/config.toml index 148ebb5803..d0b1004a75 100644 --- a/contracts/examples/multisig/interact/config.toml +++ b/contracts/examples/multisig/interact/config.toml @@ -1,4 +1,23 @@ +[connection] chain_type = 'real' gateway_uri = 'https://devnet-gateway.multiversx.com' + +[general] +contract_path = '../output/multisig.mxsc.json' quorum = 2 wegld_address = "erd1qqqqqqqqqqqqqpgqqkwzsxkjc83vlfex9dmznwm7tjvxlqqkpauqx0n782" + +[wallet] +test_wallet = 'mike' + +[[board]] +test_wallet = 'mike' + +[[board]] +test_wallet = 'carol' + +[[board]] +test_wallet = 'dan' + +[[board]] +test_wallet = 'eve' diff --git a/contracts/examples/multisig/interact/src/multisig_interact.rs b/contracts/examples/multisig/interact/src/multisig_interact.rs index 22891d31ea..f806757e5a 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact.rs @@ -19,7 +19,6 @@ async fn main() { env_logger::init(); let mut multisig_interact = MultisigInteract::init().await; - multisig_interact.register_wallets().await; let cli = multisig_interact_cli::InteractCli::parse(); match &cli.command { @@ -76,7 +75,6 @@ async fn main() { struct MultisigInteract { interactor: Interactor, - wallet_address: Bech32Address, collection_token_identifier: String, multisig_code: BytesValue, config: Config, @@ -85,22 +83,16 @@ struct MultisigInteract { impl MultisigInteract { async fn init() -> Self { - let config = Config::load_config(); - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .with_current_dir(env!("CARGO_MANIFEST_DIR")) - .use_chain_simulator(config.use_chain_simulator()) - .with_tracer(INTERACTOR_SCENARIO_TRACE_PATH) - .await; - let wallet_address = interactor.register_wallet(test_wallets::mike()).await; + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; + let interactor = interactor.with_tracer(INTERACTOR_SCENARIO_TRACE_PATH).await; let multisig_code = BytesValue::interpret_from( - "mxsc:../output/multisig.mxsc.json", + format!("mxsc:{}", config.general.contract_path), &InterpreterContext::default(), ); Self { interactor, - wallet_address: wallet_address.into(), collection_token_identifier: String::new(), multisig_code, config, @@ -108,16 +100,6 @@ impl MultisigInteract { } } - async fn register_wallets(&mut self) { - let carol = test_wallets::carol(); - let dan = test_wallets::dan(); - let eve = test_wallets::eve(); - - for wallet in [carol, dan, eve] { - self.interactor.register_wallet(wallet).await; - } - } - async fn set_state(&mut self) { for board_member_address in self.board().iter() { println!( @@ -137,12 +119,12 @@ impl MultisigInteract { let board = self.board(); - let quorum = self.config.quorum; + let quorum = self.config.general.quorum; let new_address = self .interactor .tx() .id("deploy multisig") - .from(&self.wallet_address) + .from(&self.config.wallet.address()) .typed(multisig_proxy::MultisigProxy) .init(quorum, board) .code(&self.multisig_code) @@ -165,11 +147,11 @@ impl MultisigInteract { println!("deploying {count} contracts..."); let board = self.board(); - let quorum = Config::load_config().quorum; + let quorum = self.config.general.quorum; let mut buffer = self.interactor.homogenous_call_buffer(); for _ in 0..*count { buffer.push_tx(|tx| { - tx.from(&self.wallet_address) + tx.from(self.config.wallet.address()) .typed(multisig_proxy::MultisigProxy) .init(quorum, board.clone()) .code(&self.multisig_code) @@ -185,23 +167,20 @@ impl MultisigInteract { } } - fn board(&mut self) -> MultiValueVec
{ - let carol = test_wallets::carol(); - let dan = test_wallets::dan(); - let eve = test_wallets::eve(); - - MultiValueVec::from([ - self.wallet_address.to_address(), - carol.to_address(), - dan.to_address(), - eve.to_address(), - ]) + fn board(&self) -> MultiValueVec
{ + MultiValueVec::from( + self.config + .board + .iter() + .map(|w| w.wallet().to_address()) + .collect::>(), + ) } async fn feed_contract_egld(&mut self) { self.interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .egld(BigUint::from(50_000_000_000_000_000u64)) // 0,05 or 5 * 10^16 .run() @@ -216,7 +195,7 @@ impl MultisigInteract { self.interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(gas_expr) .typed(multisig_proxy::MultisigProxy) @@ -239,12 +218,12 @@ impl MultisigInteract { self.sign(&actions_no_quorum_reached).await; - let from = &self.wallet_address; + let from = self.config.wallet.address(); let mut buffer = self.interactor.homogenous_call_buffer(); let multisig_address = self.state.current_multisig_address(); for action_id in action_ids { buffer.push_tx(|tx| { - tx.from(from) + tx.from(&from) .to(multisig_address) .gas(gas_expr) .typed(multisig_proxy::MultisigProxy) @@ -334,7 +313,7 @@ impl MultisigInteract { let dns_address = dns_address_for_name(name); self.interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("30,000,000")) .typed(multisig_proxy::MultisigProxy) diff --git a/contracts/examples/multisig/interact/src/multisig_interact_config.rs b/contracts/examples/multisig/interact/src/multisig_interact_config.rs index c11c2c447f..938bafe979 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact_config.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact_config.rs @@ -1,45 +1,31 @@ use multiversx_sc_scenario::imports::Bech32Address; +use multiversx_sc_snippets::imports::{ConnectionConfig, Wallet, WalletConfig}; +use multiversx_sc_snippets::InteractorConfig; use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; +/// General settings for the multisig interact. #[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, +pub struct GeneralConfig { + pub contract_path: String, + pub quorum: usize, + pub wegld_address: Bech32Address, } /// Multisig Interact configuration #[derive(Debug, Deserialize)] pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, - pub quorum: usize, - pub wegld_address: Bech32Address, + pub connection: ConnectionConfig, + pub general: GeneralConfig, + pub wallet: WalletConfig, + pub board: Vec, } -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection } - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } + fn register_wallets(&self) -> Vec { + self.board.iter().map(|w| w.wallet().clone()).collect() } } diff --git a/contracts/examples/multisig/interact/src/multisig_interact_nfts.rs b/contracts/examples/multisig/interact/src/multisig_interact_nfts.rs index 50300238f3..2f78217ca9 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact_nfts.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact_nfts.rs @@ -36,7 +36,7 @@ impl MultisigInteract { let action_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("10,000,000")) .typed(multisig_proxy::MultisigProxy) @@ -68,7 +68,7 @@ impl MultisigInteract { let new_token_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("80,000,000")) .typed(multisig_proxy::MultisigProxy) @@ -88,7 +88,7 @@ impl MultisigInteract { let action_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("10,000,000")) .typed(multisig_proxy::MultisigProxy) @@ -118,7 +118,7 @@ impl MultisigInteract { let new_token_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("80,000,000")) .typed(multisig_proxy::MultisigProxy) @@ -139,7 +139,7 @@ impl MultisigInteract { let action_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("10,000,000")) .typed(multisig_proxy::MultisigProxy) @@ -179,7 +179,7 @@ impl MultisigInteract { ); buffer.push_tx(|tx| { - tx.from(&self.wallet_address) + tx.from(self.config.wallet.address()) .to(multisig_address) .gas(10_000_000u64) .typed(multisig_proxy::MultisigProxy) diff --git a/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs b/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs index 08fc637f6a..8ff5153a83 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs @@ -34,7 +34,7 @@ impl MultisigInteract { pub async fn wegld_swap_set_state(&mut self) { self.interactor - .retrieve_account(&self.config.wegld_address) + .retrieve_account(&self.config.general.wegld_address) .await; } @@ -42,7 +42,7 @@ impl MultisigInteract { let function_call = self .interactor .tx() - .to(&self.config.wegld_address) + .to(&self.config.general.wegld_address) .typed(wegld_proxy::EgldEsdtSwapProxy) .wrap_egld() .into_function_call(); @@ -50,11 +50,11 @@ impl MultisigInteract { let action_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("10,000,000")) .typed(multisig_proxy::MultisigProxy) - .propose_async_call(&self.config.wegld_address, WRAP_AMOUNT, function_call) + .propose_async_call(&self.config.general.wegld_address, WRAP_AMOUNT, function_call) .returns(ReturnsResult) .run() .await; @@ -67,7 +67,7 @@ impl MultisigInteract { let wegld_token_id = self .interactor .query() - .to(&self.config.wegld_address) + .to(&self.config.general.wegld_address) .typed(wegld_proxy::EgldEsdtSwapProxy) .wrapped_egld_token_id() .returns(ReturnsResult) @@ -85,7 +85,7 @@ impl MultisigInteract { let normalized_tx = self .interactor .tx() - .to(&self.config.wegld_address) + .to(&self.config.general.wegld_address) .typed(wegld_proxy::EgldEsdtSwapProxy) .unwrap_egld() .single_esdt(&wegld_token_id, 0u64, &UNWRAP_AMOUNT.into()) @@ -96,7 +96,7 @@ impl MultisigInteract { let action_id = self .interactor .tx() - .from(&self.wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) .gas(NumExpr("10,000,000")) .typed(multisig_proxy::MultisigProxy) From 3279f9ba6e155e8728695d554598d0cceacfb8c0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 24 Jun 2026 14:42:16 +0300 Subject: [PATCH 098/195] adder interactor config general section --- contracts/examples/adder/interactor/config.toml | 2 +- .../adder/interactor/src/basic_interactor.rs | 12 +++++++++--- .../interactor/tests/basic_interactor_cs_test.rs | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/contracts/examples/adder/interactor/config.toml b/contracts/examples/adder/interactor/config.toml index e9658c3e91..a7a12c26aa 100644 --- a/contracts/examples/adder/interactor/config.toml +++ b/contracts/examples/adder/interactor/config.toml @@ -1,3 +1,4 @@ +[general] contract_path = '../output/adder.mxsc.json' [connection] @@ -7,7 +8,6 @@ contract_path = '../output/adder.mxsc.json' chain_type = 'real' gateway_uri = 'https://devnet-gateway.multiversx.com' - # Owner wallet - used for deploying and upgrading the contract. # Provide exactly one of: test_wallet, pem, or keyfile. [owner] diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index 458969b245..476c616965 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -5,10 +5,16 @@ use clap::Parser; use multiversx_sc_snippets::imports::*; use serde::{Deserialize, Serialize}; +/// Adder Interact general settings +#[derive(Debug, Deserialize)] +pub struct GeneralConfig { + pub contract_path: String, +} + /// Adder Interact configuration #[derive(Debug, Deserialize)] pub struct Config { - pub contract_path: String, + pub general: GeneralConfig, pub connection: ConnectionConfig, pub owner: WalletConfig, pub wallet: WalletConfig, @@ -101,7 +107,7 @@ impl BasicInteractor { .gas(100_000_000) .typed(adder_proxy::AdderProxy) .init(0u64) - .code(MxscPath::new(&self.config.contract_path)) + .code(MxscPath::new(&self.config.general.contract_path)) .returns(ReturnsNewBech32Address) .run() .await; @@ -119,7 +125,7 @@ impl BasicInteractor { .gas(6_000_000) .typed(adder_proxy::AdderProxy) .upgrade(new_value) - .code(MxscPath::new(&self.config.contract_path)) + .code(MxscPath::new(&self.config.general.contract_path)) .code_metadata(CodeMetadata::UPGRADEABLE) .returns(ReturnsHandledOrError::new()) .run() diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index 6aab6070a8..bb3685f8ce 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -1,10 +1,12 @@ -use basic_interactor::{BasicInteractor, Config}; +use basic_interactor::{BasicInteractor, Config, GeneralConfig}; use multiversx_sc_snippets::{imports::*, sdk::gateway::SetStateAccount}; use serial_test::serial; fn chain_simulator_config() -> Config { Config { - contract_path: "../output/adder.mxsc.json".to_owned(), + general: GeneralConfig { + contract_path: "../output/adder.mxsc.json".to_owned(), + }, connection: ConnectionConfig::chain_simulator(), owner: WalletConfig::from_test_wallet("mike"), wallet: WalletConfig::from_test_wallet("ivan"), From e6fc7009b84032b2a9ca8fdf579b48682fd558c2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 25 Jun 2026 04:19:42 +0300 Subject: [PATCH 099/195] fmt --- .../multisig/interact/src/multisig_interact_config.rs | 2 +- .../multisig/interact/src/multisig_interact_wegld.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/examples/multisig/interact/src/multisig_interact_config.rs b/contracts/examples/multisig/interact/src/multisig_interact_config.rs index 938bafe979..8db7cac8bb 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact_config.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact_config.rs @@ -1,6 +1,6 @@ use multiversx_sc_scenario::imports::Bech32Address; -use multiversx_sc_snippets::imports::{ConnectionConfig, Wallet, WalletConfig}; use multiversx_sc_snippets::InteractorConfig; +use multiversx_sc_snippets::imports::{ConnectionConfig, Wallet, WalletConfig}; use serde::Deserialize; /// General settings for the multisig interact. diff --git a/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs b/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs index 8ff5153a83..59027790f8 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact_wegld.rs @@ -54,7 +54,11 @@ impl MultisigInteract { .to(self.state.current_multisig_address()) .gas(NumExpr("10,000,000")) .typed(multisig_proxy::MultisigProxy) - .propose_async_call(&self.config.general.wegld_address, WRAP_AMOUNT, function_call) + .propose_async_call( + &self.config.general.wegld_address, + WRAP_AMOUNT, + function_call, + ) .returns(ReturnsResult) .run() .await; From 279084644acc9ee13bc84bc590f7d0e9a2b94a91 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 25 Jun 2026 04:20:37 +0300 Subject: [PATCH 100/195] forwarder blind interactor update --- .../dex-interactor/config.toml | 22 ++-- .../dex-interactor/src/config.rs | 54 +++------ .../dex-interactor/src/dex_interactor.rs | 104 +++++++++--------- 3 files changed, 83 insertions(+), 97 deletions(-) diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/config.toml b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/config.toml index 6d2096c439..1da474e939 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/config.toml +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/config.toml @@ -1,18 +1,26 @@ +[connection] chain_type = 'real' gateway_uri = 'https://gateway.battleofnodes.com' + +[general] +contract_path = 'forwarder-blind-bon.wasm' wegld_address = 'erd1qqqqqqqqqqqqqpgqmuk0q2saj0mgutxm4teywre6dl8wqf58xamqdrukln' pair_address = 'erd1qqqqqqqqqqqqqpgqeel2kumf0r8ffyhth7pqdujjat9nx0862jpsg2pqaq' wegld_token_id = 'WEGLD-bd4d79' usdc_token_id = 'USDC-c76f1f' -# Optional: list of PEM file paths, one per wallet to use. -# Relative paths are resolved from the workspace root. +# Optional: list of wallets, one per entry. # If absent or empty, all operations are skipped with a warning. -wallet_pem_paths = [ - '../../../../../sdk/core/src/test_wallets/s0phie.pem', - '../../../../../sdk/core/src/test_wallets/s1mon.pem', - '../../../../../sdk/core/src/test_wallets/s2onja.pem', -] +# Provide exactly one of: test_wallet, pem, or keyfile per entry. + +[[wallets]] +test_wallet = 's0phie' + +[[wallets]] +test_wallet = 's1mon' + +[[wallets]] +test_wallet = 's2onja' contract_addresses = [ "erd1qqqqqqqqqqqqqpgqwlqhw27xz6j2cqh8w8v53l5fdulfu4rqqqqqdt5qat", diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs index 3d35813207..c6609a7b3a 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs @@ -1,54 +1,34 @@ -use multiversx_sc_snippets::imports::Bech32Address; +use multiversx_sc_snippets::InteractorConfig; +use multiversx_sc_snippets::imports::{Bech32Address, ConnectionConfig, Wallet, WalletConfig}; use serde::Deserialize; -use std::io::Read; - -/// Config file -const CONFIG_FILE: &str = "config.toml"; +/// General settings for the forwarder-blind dex interact. #[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, +pub struct GeneralConfig { + pub contract_path: String, + pub wegld_address: Bech32Address, + pub pair_address: Bech32Address, + pub wegld_token_id: String, + pub usdc_token_id: String, } /// Contract Interact configuration #[derive(Debug, Deserialize)] pub struct Config { - pub gateway_uri: String, - pub chain_type: ChainType, - pub wegld_address: Bech32Address, - pub pair_address: Bech32Address, - pub wegld_token_id: String, - pub usdc_token_id: String, - /// Optional list of PEM file paths, one per wallet. - /// If absent or empty, all operations are skipped with a warning. - #[serde(default)] - pub wallet_pem_paths: Vec, + pub connection: ConnectionConfig, + pub general: GeneralConfig, + pub wallets: Vec, /// Forwarder contract addresses to target for all swap transactions. #[serde(default)] pub contract_addresses: Vec, } -impl Config { - // Deserializes config from file - pub fn load_config() -> Self { - let mut file = std::fs::File::open(CONFIG_FILE).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - toml::from_str(&content).unwrap() - } - - // Returns the gateway URI - pub fn gateway_uri(&self) -> &str { - &self.gateway_uri +impl InteractorConfig for Config { + fn connection(&self) -> &ConnectionConfig { + &self.connection } - // Returns if chain type is chain simulator - pub fn use_chain_simulator(&self) -> bool { - match self.chain_type { - ChainType::Real => false, - ChainType::Simulator => true, - } + fn register_wallets(&self) -> Vec { + self.wallets.iter().map(|w| w.wallet().clone()).collect() } } diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs index 4d99a7054d..03e069623d 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs @@ -10,13 +10,10 @@ use multiversx_sc_snippets::imports::*; use proxies::*; use state::State; -const FORWARDER_BLIND_CODE_PATH: FilePath = FilePath("forwarder-blind-bon.wasm"); - pub async fn forwarder_blind_cli() { env_logger::init(); - let config = Config::load_config(); - let mut interact = ContractInteract::new(config).await; + let mut interact = ContractInteract::new().await; let cli = cli::InteractCli::parse(); match &cli.command { @@ -112,27 +109,22 @@ pub struct ContractInteract { } impl ContractInteract { - pub async fn new(config: Config) -> Self { - let mut interactor = Interactor::new(config.gateway_uri()) - .await - .with_current_dir(env!("CARGO_MANIFEST_DIR")); + pub async fn new() -> Self { + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: Config = interactor.load_config_toml().await; interactor.gas_price *= 25; - let wallet_addresses: Vec = if config.wallet_pem_paths.is_empty() { - println!("WARNING: no wallet_pem_paths configured — all operations will be skipped."); + let wallet_addresses: Vec = if config.wallets.is_empty() { + println!("WARNING: no wallets configured — all operations will be skipped."); Vec::new() } else { - let mut addrs = Vec::new(); - for pem_path in &config.wallet_pem_paths { - let wallet = Wallet::from_pem_file(pem_path) - .unwrap_or_else(|e| panic!("failed to load wallet from {pem_path}: {e}")); - addrs.push(interactor.register_wallet(wallet).await.into()); - } - addrs + config + .wallets + .iter() + .map(|w| w.wallet().to_address().to_bech32(interactor.get_hrp())) + .collect() }; - interactor.generate_blocks_until_all_activations().await; - ContractInteract { interactor, wallet_addresses, @@ -150,7 +142,7 @@ impl ContractInteract { .gas(80_000_000u64) .typed(forwarder_blind_proxy::ForwarderBlindProxy) .init() - .code(FORWARDER_BLIND_CODE_PATH) + .code(FilePath(&self.config.general.contract_path)) .code_metadata(CodeMetadata::PAYABLE) .returns(ReturnsNewBech32Address) }); @@ -164,7 +156,7 @@ impl ContractInteract { pub async fn wrap_egld(&mut self, amount: u64) { let wallet_addresses = self.wallet_addresses.clone(); - let wegld_address = self.config.wegld_address.clone(); + let wegld_address = self.config.general.wegld_address.clone(); let mut buffer = self.interactor.homogenous_call_buffer(); for wallet in &wallet_addresses { buffer.push_tx(|tx| { @@ -211,7 +203,7 @@ impl ContractInteract { .interactor .tx() .from(&wallet) - .to(&self.config.pair_address) + .to(&self.config.general.pair_address) .gas(50_000_000u64) .raw_data(fc) .payment( @@ -236,7 +228,7 @@ impl ContractInteract { ) { let wallet_addresses = self.wallet_addresses.clone(); let contract_addresses = self.config.contract_addresses.clone(); - let pair_address = self.config.pair_address.clone(); + let pair_address = self.config.general.pair_address.clone(); let fc = self.build_swap_function_call(&want_token_id, want_amount_min); let mut buffer = self.interactor.homogenous_call_buffer(); @@ -286,22 +278,22 @@ impl ContractInteract { } pub async fn swap1_direct(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let send_token = self.config.wegld_token_id.clone(); - let want_token = self.config.usdc_token_id.clone(); + let send_token = self.config.general.wegld_token_id.clone(); + let want_token = self.config.general.usdc_token_id.clone(); self.swap_direct_impl(send_token, wegld_amount, want_token, usdc_amount_min) .await; } pub async fn swap2_direct(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let send_token = self.config.usdc_token_id.clone(); - let want_token = self.config.wegld_token_id.clone(); + let send_token = self.config.general.usdc_token_id.clone(); + let want_token = self.config.general.wegld_token_id.clone(); self.swap_direct_impl(send_token, usdc_amount, want_token, wegld_amount_min) .await; } pub async fn swap1_sync(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let send_token = self.config.wegld_token_id.clone(); - let want_token = self.config.usdc_token_id.clone(); + let send_token = self.config.general.wegld_token_id.clone(); + let want_token = self.config.general.usdc_token_id.clone(); self.swap_via_forwarder_impl( send_token, wegld_amount, @@ -313,8 +305,8 @@ impl ContractInteract { } pub async fn swap2_sync(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let send_token = self.config.usdc_token_id.clone(); - let want_token = self.config.wegld_token_id.clone(); + let send_token = self.config.general.usdc_token_id.clone(); + let want_token = self.config.general.wegld_token_id.clone(); self.swap_via_forwarder_impl( send_token, usdc_amount, @@ -326,8 +318,8 @@ impl ContractInteract { } pub async fn swap1_async1(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let send_token = self.config.wegld_token_id.clone(); - let want_token = self.config.usdc_token_id.clone(); + let send_token = self.config.general.wegld_token_id.clone(); + let want_token = self.config.general.usdc_token_id.clone(); self.swap_via_forwarder_impl( send_token, wegld_amount, @@ -339,8 +331,8 @@ impl ContractInteract { } pub async fn swap2_async1(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let send_token = self.config.usdc_token_id.clone(); - let want_token = self.config.wegld_token_id.clone(); + let send_token = self.config.general.usdc_token_id.clone(); + let want_token = self.config.general.wegld_token_id.clone(); self.swap_via_forwarder_impl( send_token, usdc_amount, @@ -352,8 +344,8 @@ impl ContractInteract { } pub async fn swap1_async2(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let send_token = self.config.wegld_token_id.clone(); - let want_token = self.config.usdc_token_id.clone(); + let send_token = self.config.general.wegld_token_id.clone(); + let want_token = self.config.general.usdc_token_id.clone(); self.swap_via_forwarder_impl( send_token, wegld_amount, @@ -365,8 +357,8 @@ impl ContractInteract { } pub async fn swap2_async2(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let send_token = self.config.usdc_token_id.clone(); - let want_token = self.config.wegld_token_id.clone(); + let send_token = self.config.general.usdc_token_id.clone(); + let want_token = self.config.general.wegld_token_id.clone(); self.swap_via_forwarder_impl( send_token, usdc_amount, @@ -378,8 +370,8 @@ impl ContractInteract { } pub async fn swap1_te(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let send_token = self.config.wegld_token_id.clone(); - let want_token = self.config.usdc_token_id.clone(); + let send_token = self.config.general.wegld_token_id.clone(); + let want_token = self.config.general.usdc_token_id.clone(); self.swap_via_forwarder_impl( send_token, wegld_amount, @@ -391,8 +383,8 @@ impl ContractInteract { } pub async fn swap2_te(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let send_token = self.config.usdc_token_id.clone(); - let want_token = self.config.wegld_token_id.clone(); + let send_token = self.config.general.usdc_token_id.clone(); + let want_token = self.config.general.wegld_token_id.clone(); self.swap_via_forwarder_impl( send_token, usdc_amount, @@ -405,8 +397,8 @@ impl ContractInteract { pub async fn drain(&mut self) { let contract_addresses = self.config.contract_addresses.clone(); - let wegld_token_id = self.config.wegld_token_id.clone(); - let usdc_token_id = self.config.usdc_token_id.clone(); + let wegld_token_id = self.config.general.wegld_token_id.clone(); + let usdc_token_id = self.config.general.usdc_token_id.clone(); // For each contract, fetch its on-chain owner and check if we have it registered. let mut owner_per_contract: Vec<(Bech32Address, Bech32Address)> = Vec::new(); @@ -470,10 +462,10 @@ impl ContractInteract { let amount_out = self .interactor .query() - .to(&self.config.pair_address) + .to(&self.config.general.pair_address) .typed(pair_proxy::PairProxy) .get_amount_out_view( - EsdtTokenIdentifier::from(self.config.wegld_token_id.as_str()), + EsdtTokenIdentifier::from(self.config.general.wegld_token_id.as_str()), BigUint::from(wegld_amount), ) .returns(ReturnsResultUnmanaged) @@ -482,7 +474,7 @@ impl ContractInteract { println!( "{wegld_amount} {} -> {} {}", - self.config.wegld_token_id, amount_out, self.config.usdc_token_id + self.config.general.wegld_token_id, amount_out, self.config.general.usdc_token_id ); } @@ -490,7 +482,7 @@ impl ContractInteract { let (wegld_reserve, usdc_reserve, lp_supply) = self .interactor .query() - .to(&self.config.pair_address) + .to(&self.config.general.pair_address) .typed(pair_proxy::PairProxy) .get_reserves_and_total_supply() .returns(ReturnsResultUnmanaged) @@ -498,14 +490,20 @@ impl ContractInteract { .await .into_tuple(); - println!("{} reserve: {wegld_reserve}", self.config.wegld_token_id); - println!("{} reserve: {usdc_reserve}", self.config.usdc_token_id); + println!( + "{} reserve: {wegld_reserve}", + self.config.general.wegld_token_id + ); + println!( + "{} reserve: {usdc_reserve}", + self.config.general.usdc_token_id + ); println!("LP token supply: {lp_supply}"); } pub async fn balances(&mut self) { - let wegld_token_id = self.config.wegld_token_id.clone(); - let usdc_token_id = self.config.usdc_token_id.clone(); + let wegld_token_id = self.config.general.wegld_token_id.clone(); + let usdc_token_id = self.config.general.usdc_token_id.clone(); println!("=== Wallet Balances ==="); for wallet in &self.wallet_addresses.clone() { From 0dfa066818d7572eb0436af3d9251adb24559ef8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 25 Jun 2026 04:37:20 +0300 Subject: [PATCH 101/195] mesh interactor refactor --- .../composability/mesh-interactor/config.toml | 14 +-- .../mesh-interactor/src/call_tree_config.rs | 85 +++---------------- .../src/mesh_interact_controller.rs | 12 +-- .../mesh-interactor/src/mesh_interact_main.rs | 4 +- 4 files changed, 26 insertions(+), 89 deletions(-) diff --git a/contracts/feature-tests/composability/mesh-interactor/config.toml b/contracts/feature-tests/composability/mesh-interactor/config.toml index 01071db10c..fc951c962e 100644 --- a/contracts/feature-tests/composability/mesh-interactor/config.toml +++ b/contracts/feature-tests/composability/mesh-interactor/config.toml @@ -1,9 +1,11 @@ -call_tree_path = "layouts/async_v1_c1_smin_pay.toml" - -[gateway] -uri = "https://testnet-gateway.multiversx.com/" +[connection] +gateway_uri = "https://testnet-gateway.multiversx.com/" chain_type = "real" -# [gateway] -# uri = "https://gateway.battleofnodes.com" +# [connection] +# gateway_uri = "https://gateway.battleofnodes.com" # chain_type = "real" + +[general] +contract_path = "../mesh-node/output/mesh-node.mxsc.json" +call_tree_path = "layouts/async_v1_c1_smin_pay.toml" diff --git a/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs b/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs index 0645b67859..82cc0a2be3 100644 --- a/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs +++ b/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs @@ -1,93 +1,32 @@ use std::{ collections::BTreeMap, io::{Read, Write}, - path::Path, }; use multiversx_sc::types::ShardId; +use multiversx_sc_snippets::imports::*; use serde::{Deserialize, Serialize}; -/// Top-level config file path. -pub const CONFIG_FILE: &str = "config.toml"; - -/// Default call tree layout file path (used when `config.toml` is absent). -pub const CALL_TREE_FILE: &str = "call_tree.toml"; - /// Call tree state file (deployed addresses), always `current.toml`. pub const STATE_FILE: &str = "current.toml"; -/// Chain type for the gateway connection. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} - -/// Gateway connection settings. -#[derive(Debug, Serialize, Deserialize)] -pub struct GatewayConfig { - pub uri: String, - pub chain_type: ChainType, -} - -impl Default for GatewayConfig { - fn default() -> Self { - GatewayConfig { - uri: "https://testnet-gateway.multiversx.com".to_string(), - chain_type: ChainType::Real, - } - } -} - -impl GatewayConfig { - pub fn use_chain_simulator(&self) -> bool { - self.chain_type == ChainType::Simulator - } +/// General settings for the mesh interact. +#[derive(Debug, Deserialize)] +pub struct GeneralConfig { + pub contract_path: String, + pub call_tree_path: String, } /// Top-level interactor config, stored in `config.toml`. -/// -/// Contains the gateway settings and the path to the call tree layout file. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct InteractConfig { - pub gateway: GatewayConfig, - /// Path to the call tree layout TOML (defaults to `"call_tree.toml"`). - #[serde(default = "default_call_tree_path")] - pub call_tree_path: String, + pub connection: ConnectionConfig, + pub general: GeneralConfig, } -fn default_call_tree_path() -> String { - CALL_TREE_FILE.to_string() -} - -impl Default for InteractConfig { - fn default() -> Self { - InteractConfig { - gateway: GatewayConfig::default(), - call_tree_path: CALL_TREE_FILE.to_string(), - } - } -} - -impl InteractConfig { - pub fn load_from_file(path: &str) -> Self { - if !Path::new(path).exists() { - return Self::default(); - } - let mut file = std::fs::File::open(path).expect("failed to open config file"); - let mut content = String::new(); - file.read_to_string(&mut content) - .expect("failed to read config file"); - toml::from_str(&content).expect("failed to deserialize config") - } - - #[allow(dead_code)] - pub fn save_to_file(&self, path: &str) { - let toml_str = toml::to_string_pretty(self).expect("failed to serialize config"); - let mut file = std::fs::File::create(path).expect("failed to create config file"); - file.write_all(toml_str.as_bytes()) - .expect("failed to write config file"); +impl InteractorConfig for InteractConfig { + fn connection(&self) -> &ConnectionConfig { + &self.connection } } diff --git a/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs b/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs index ee17b86f62..10f933523e 100644 --- a/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs +++ b/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_controller.rs @@ -1,4 +1,4 @@ -use crate::call_tree_config::{CONFIG_FILE, InteractConfig}; +use crate::call_tree_config::InteractConfig; use multiversx_sc_snippets::imports::*; @@ -11,12 +11,8 @@ pub struct ComposabilityInteract { impl ComposabilityInteract { pub async fn init() -> Self { - let config = InteractConfig::load_from_file(CONFIG_FILE); - let gateway_config = &config.gateway; - let mut interactor = Interactor::new(&gateway_config.uri) - .await - .use_chain_simulator(gateway_config.use_chain_simulator()) - .with_current_dir(env!("CARGO_MANIFEST_DIR")); + let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); + let config: InteractConfig = interactor.load_config_toml().await; let shard_wallet_addresses = [ interactor .register_wallet(test_wallets::for_shard(0u32.into())) @@ -29,7 +25,7 @@ impl ComposabilityInteract { .await, ]; let forw_queue_code = BytesValue::interpret_from( - "mxsc:../mesh-node/output/mesh-node.mxsc.json", + format!("mxsc:{}", config.general.contract_path), &InterpreterContext::default(), ); diff --git a/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_main.rs b/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_main.rs index 874bb16fe0..406b71b33a 100644 --- a/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_main.rs +++ b/contracts/feature-tests/composability/mesh-interactor/src/mesh_interact_main.rs @@ -16,12 +16,12 @@ use multiversx_sc_snippets::imports::*; impl ComposabilityInteract { fn load_layout(&self) -> CallTreeLayout { - CallTreeLayout::load_from_file(&self.config.call_tree_path) + CallTreeLayout::load_from_file(&self.config.general.call_tree_path) } async fn cmd_update_gas(&mut self) { println!("Updating gas estimates..."); - let layout_path = self.config.call_tree_path.clone(); + let layout_path = self.config.general.call_tree_path.clone(); let mut layout = self.load_layout(); layout.fill_gas_estimates(); layout.save_to_file(&layout_path); From ab1a2f3d210e82c43dee360e13b111937c0ad884 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 25 Jun 2026 04:38:09 +0300 Subject: [PATCH 102/195] refactor in interactors --- contracts/examples/adder/interactor/src/basic_interactor.rs | 3 +-- .../examples/multisig/interact/src/multisig_interact.rs | 5 ++--- .../examples/ping-pong-egld/interactor/src/interact.rs | 3 +-- .../payable-features/interactor/src/payable_interactor.rs | 6 ++---- .../feature-tests/tiny/interactor/src/tiny_interactor.rs | 3 +-- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index 476c616965..e6142efbd6 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -143,11 +143,10 @@ impl BasicInteractor { } pub async fn add(&mut self, value: u32) { - let wallet_address = self.config.wallet.address(); self.interactor .tx() .id("interactor add") - .from(&wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_adder_address()) .gas(6_000_000u64) .typed(adder_proxy::AdderProxy) diff --git a/contracts/examples/multisig/interact/src/multisig_interact.rs b/contracts/examples/multisig/interact/src/multisig_interact.rs index f806757e5a..a75940355b 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact.rs @@ -124,7 +124,7 @@ impl MultisigInteract { .interactor .tx() .id("deploy multisig") - .from(&self.config.wallet.address()) + .from(self.config.wallet.address()) .typed(multisig_proxy::MultisigProxy) .init(quorum, board) .code(&self.multisig_code) @@ -218,12 +218,11 @@ impl MultisigInteract { self.sign(&actions_no_quorum_reached).await; - let from = self.config.wallet.address(); let mut buffer = self.interactor.homogenous_call_buffer(); let multisig_address = self.state.current_multisig_address(); for action_id in action_ids { buffer.push_tx(|tx| { - tx.from(&from) + tx.from(self.config.wallet.address()) .to(multisig_address) .gas(gas_expr) .typed(multisig_proxy::MultisigProxy) diff --git a/contracts/examples/ping-pong-egld/interactor/src/interact.rs b/contracts/examples/ping-pong-egld/interactor/src/interact.rs index a02fce65ed..1660e66269 100644 --- a/contracts/examples/ping-pong-egld/interactor/src/interact.rs +++ b/contracts/examples/ping-pong-egld/interactor/src/interact.rs @@ -209,12 +209,11 @@ impl PingPongEgldInteract { opt_activation_timestamp: Option, max_funds: OptionalValue, ) { - let wallet_address = self.config.wallet.address(); let response = self .interactor .tx() .to(self.state.current_ping_pong_egld_address()) - .from(&wallet_address) + .from(self.config.wallet.address()) .gas(30_000_000u64) .typed(proxy::PingPongEgldProxy) .upgrade(ping_amount, duration, opt_activation_timestamp, max_funds) diff --git a/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs b/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs index 1d4bad7892..fd3ba52a76 100644 --- a/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs +++ b/contracts/feature-tests/payable-features/interactor/src/payable_interactor.rs @@ -100,14 +100,13 @@ impl PayableInteract { } pub async fn check_multi_transfer_only_egld_transfer(&mut self) { - let wallet_address = self.config.wallet.address(); let mut payment = MultiEgldOrEsdtPayment::new(); payment.push(EgldOrEsdtTokenPayment::egld_payment(1_0000u64.into())); let result = self .interactor .tx() - .from(&wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_payable_features_address()) .gas(6_000_000u64) .typed(payable_features_proxy::PayableFeaturesProxy) @@ -121,7 +120,6 @@ impl PayableInteract { } pub async fn check_all_transfers(&mut self) { - let wallet_address = self.config.wallet.address(); let mut payment = MultiEgldOrEsdtPayment::new(); payment.push(EgldOrEsdtTokenPayment::egld_payment(1_0000u64.into())); payment.push(EgldOrEsdtTokenPayment::egld_payment(2_0000u64.into())); @@ -129,7 +127,7 @@ impl PayableInteract { let result = self .interactor .tx() - .from(&wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_payable_features_address()) .gas(6_000_000u64) .typed(payable_features_proxy::PayableFeaturesProxy) diff --git a/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs b/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs index 3431aaeec4..ee57ce797b 100644 --- a/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs +++ b/contracts/feature-tests/tiny/interactor/src/tiny_interactor.rs @@ -99,10 +99,9 @@ impl TinyInteractor { } pub async fn call_x(&mut self) { - let wallet_address = self.config.wallet.address(); self.interactor .tx() - .from(&wallet_address) + .from(self.config.wallet.address()) .to(self.state.current_contract_address()) .gas(1_100_000) .raw_call("x") From 88e1ad536b079b88aa1983a83801cb9bf7cd32e2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 25 Jun 2026 20:16:54 +0300 Subject: [PATCH 103/195] interactor config - WalletConfig path context --- .../snippets/src/config/interactor_config.rs | 24 ++++++- .../snippets/src/config/wallet_config.rs | 64 ++++++++++++++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs index 805cb37fdb..12a1f8df07 100644 --- a/framework/snippets/src/config/interactor_config.rs +++ b/framework/snippets/src/config/interactor_config.rs @@ -30,16 +30,34 @@ impl InteractorConfig for ConnectionConfig { } } +/// Deserializes a TOML config file into a value of type `C`. +/// +/// Before parsing, sets the config file's parent directory as the base for +/// resolving relative [`WalletConfig`] paths (see [`WalletConfig::set_config_base_dir`]). +/// The base directory is cleared after parsing. +/// +/// # Panics +/// +/// Panics if the file cannot be opened, read, or parsed as valid TOML for `C`. pub fn load_toml_config(config_path: &PathBuf) -> C where C: InteractorConfig + serde::de::DeserializeOwned, { + use super::wallet_config::WalletConfig; + use std::io::Read; + + let base_dir = config_path.parent().map(|p| p.to_path_buf()); + WalletConfig::set_config_base_dir(base_dir); + let mut file = std::fs::File::open(config_path) .unwrap_or_else(|e| panic!("cannot open {}: {e}", config_path.display())); let mut content = String::new(); - use std::io::Read; file.read_to_string(&mut content) .unwrap_or_else(|e| panic!("cannot read {}: {e}", config_path.display())); - toml::from_str(&content) - .unwrap_or_else(|e| panic!("cannot parse {}: {e}", config_path.display())) + let result = toml::from_str(&content) + .unwrap_or_else(|e| panic!("cannot parse {}: {e}", config_path.display())); + + WalletConfig::set_config_base_dir(None); + + result } diff --git a/framework/snippets/src/config/wallet_config.rs b/framework/snippets/src/config/wallet_config.rs index f065b5205f..1ee0a1e83d 100644 --- a/framework/snippets/src/config/wallet_config.rs +++ b/framework/snippets/src/config/wallet_config.rs @@ -1,15 +1,33 @@ use multiversx_sc_scenario::imports::Bech32Address; use serde::Deserialize; -use std::{path::PathBuf, sync::OnceLock}; +use std::{cell::RefCell, path::PathBuf, sync::OnceLock}; use crate::sdk::{ test_wallets, wallet::{Keystore, Wallet}, }; +thread_local! { + /// Set by [`load_toml_config`] to the parent directory of the config file + /// before deserialization begins. Consumed by [`WalletConfig`]'s `From` impl + /// to resolve relative `pem` / `keyfile` paths automatically, with no + /// per-config boilerplate needed. + pub static CONFIG_BASE_DIR: RefCell> = RefCell::new(None); +} + +/// Raw helper used only for serde deserialization. +#[derive(Deserialize)] +struct WalletConfigRaw { + test_wallet: Option, + pem: Option, + keyfile: Option, + keystore_password: Option, +} + /// Wallet configuration embeddable in a TOML/JSON config file. /// Mirrors `SenderArgs` from the CLI but uses `serde` instead of `clap`. #[derive(Debug, Deserialize)] +#[serde(from = "WalletConfigRaw")] pub struct WalletConfig { /// Name of a built-in test wallet (e.g. "alice", "bob", "mike"). /// See `multiversx_sdk::test_wallets::valid_names()` for the full list. @@ -24,11 +42,35 @@ pub struct WalletConfig { /// Keystore password (plain text). Required when `keyfile` is set. pub keystore_password: Option, - #[serde(skip)] cache: OnceLock, } +impl From for WalletConfig { + fn from(raw: WalletConfigRaw) -> Self { + let mut config = WalletConfig { + test_wallet: raw.test_wallet, + pem: raw.pem, + keyfile: raw.keyfile, + keystore_password: raw.keystore_password, + cache: OnceLock::new(), + }; + CONFIG_BASE_DIR.with(|cell| { + if let Some(ref base_dir) = *cell.borrow() { + config.set_base_dir(base_dir); + } + }); + config + } +} + impl WalletConfig { + /// Sets the base directory used to resolve relative `pem` / `keyfile` paths + /// during deserialization. Called by [`load_toml_config`] before parsing the + /// config file; pass `None` to clear after parsing. + pub(crate) fn set_config_base_dir(base_dir: Option) { + CONFIG_BASE_DIR.with(|cell| *cell.borrow_mut() = base_dir); + } + /// Creates a `WalletConfig` from a built-in test wallet name. pub fn from_test_wallet(name: impl Into) -> Self { WalletConfig { @@ -51,6 +93,24 @@ impl WalletConfig { } } + /// Resolves any relative `pem` / `keyfile` paths against `base_dir`. + /// + /// Call this after deserializing a `WalletConfig` from a TOML/JSON file, + /// passing the directory that contains the config file as `base_dir`. + /// Absolute paths and `test_wallet` names are left unchanged. + pub fn set_base_dir(&mut self, base_dir: &std::path::Path) { + if let Some(pem) = &self.pem { + if pem.is_relative() { + self.pem = Some(base_dir.join(pem)); + } + } + if let Some(keyfile) = &self.keyfile { + if keyfile.is_relative() { + self.keyfile = Some(base_dir.join(keyfile)); + } + } + } + /// Returns the wallet, loading and caching it on first call. /// /// Priority: `test_wallet` > `pem` > `keyfile`. From 76500e5b9e0199e2cbf44bf42d6f4e57a0e7ed18 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 25 Jun 2026 20:21:00 +0300 Subject: [PATCH 104/195] interactor ConfigPath --- framework/snippets/src/config.rs | 8 +- framework/snippets/src/config/config_path.rs | 93 +++++++++++++++++++ .../snippets/src/config/interactor_config.rs | 6 +- .../snippets/src/config/wallet_config.rs | 53 ++--------- 4 files changed, 109 insertions(+), 51 deletions(-) create mode 100644 framework/snippets/src/config/config_path.rs diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs index 9de8c5fed2..47e8ae3393 100644 --- a/framework/snippets/src/config.rs +++ b/framework/snippets/src/config.rs @@ -1,9 +1,11 @@ mod auto_save; +mod config_path; mod connection_config; mod interactor_config; mod wallet_config; pub use auto_save::*; -pub use connection_config::*; -pub use interactor_config::*; -pub use wallet_config::*; +pub use config_path::ConfigPath; +pub use connection_config::ConnectionConfig; +pub use interactor_config::{InteractorConfig, load_toml_config}; +pub use wallet_config::WalletConfig; diff --git a/framework/snippets/src/config/config_path.rs b/framework/snippets/src/config/config_path.rs new file mode 100644 index 0000000000..dd7a49543f --- /dev/null +++ b/framework/snippets/src/config/config_path.rs @@ -0,0 +1,93 @@ +use std::{ + cell::RefCell, + fmt, + ops::Deref, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +thread_local! { + /// Set by [`load_toml_config`] to the parent directory of the config file + /// before deserialization begins, then cleared afterwards. + /// + /// [`ConfigPath`]'s [`Deserialize`] impl reads this to resolve relative + /// paths at deserialization time, with no per-config boilerplate needed. + pub static CONFIG_BASE_DIR: RefCell> = RefCell::new(None); +} + +/// A [`PathBuf`] newtype that, when deserialized, automatically resolves +/// relative paths against the config file's directory. +/// +/// Use this type for any path field in a config struct that may be specified +/// relative to the config file. When the config is loaded via +/// [`load_toml_config`], [`CONFIG_BASE_DIR`] is set to the config file's parent +/// directory, and every `ConfigPath` field is resolved against it. +/// +/// Absolute paths pass through unchanged. Programmatically constructed +/// instances are never modified. +/// +/// # Example +/// +/// ```toml +/// [general] +/// pem = "wallets/owner.pem" # resolved relative to the config file location +/// ``` +#[derive(Debug, Clone)] +pub struct ConfigPath(pub PathBuf); + +impl ConfigPath { + /// Sets the base directory used to resolve relative paths during + /// deserialization. Called by [`load_toml_config`] before and after + /// parsing; pass `None` to clear. + pub(crate) fn set_config_base_dir(base_dir: Option) { + CONFIG_BASE_DIR.with(|cell| *cell.borrow_mut() = base_dir); + } +} + +impl AsRef for ConfigPath { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +impl Deref for ConfigPath { + type Target = Path; + fn deref(&self) -> &Path { + &self.0 + } +} + +impl fmt::Display for ConfigPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.display().fmt(f) + } +} + +impl From for ConfigPath { + fn from(p: PathBuf) -> Self { + ConfigPath(p) + } +} + +impl From for PathBuf { + fn from(p: ConfigPath) -> Self { + p.0 + } +} + +impl<'de> Deserialize<'de> for ConfigPath { + fn deserialize>(deserializer: D) -> Result { + let raw = PathBuf::deserialize(deserializer)?; + if raw.is_absolute() { + return Ok(ConfigPath(raw)); + } + let resolved = CONFIG_BASE_DIR.with(|cell| { + cell.borrow() + .as_ref() + .map(|base| base.join(&raw)) + .unwrap_or_else(|| raw.clone()) + }); + Ok(ConfigPath(resolved)) + } +} diff --git a/framework/snippets/src/config/interactor_config.rs b/framework/snippets/src/config/interactor_config.rs index 12a1f8df07..bfcadcb6e5 100644 --- a/framework/snippets/src/config/interactor_config.rs +++ b/framework/snippets/src/config/interactor_config.rs @@ -43,11 +43,11 @@ pub fn load_toml_config(config_path: &PathBuf) -> C where C: InteractorConfig + serde::de::DeserializeOwned, { - use super::wallet_config::WalletConfig; + use super::config_path::ConfigPath; use std::io::Read; let base_dir = config_path.parent().map(|p| p.to_path_buf()); - WalletConfig::set_config_base_dir(base_dir); + ConfigPath::set_config_base_dir(base_dir); let mut file = std::fs::File::open(config_path) .unwrap_or_else(|e| panic!("cannot open {}: {e}", config_path.display())); @@ -57,7 +57,7 @@ where let result = toml::from_str(&content) .unwrap_or_else(|e| panic!("cannot parse {}: {e}", config_path.display())); - WalletConfig::set_config_base_dir(None); + ConfigPath::set_config_base_dir(None); result } diff --git a/framework/snippets/src/config/wallet_config.rs b/framework/snippets/src/config/wallet_config.rs index 1ee0a1e83d..9c0ea408e2 100644 --- a/framework/snippets/src/config/wallet_config.rs +++ b/framework/snippets/src/config/wallet_config.rs @@ -1,26 +1,20 @@ use multiversx_sc_scenario::imports::Bech32Address; use serde::Deserialize; -use std::{cell::RefCell, path::PathBuf, sync::OnceLock}; +use std::{path::PathBuf, sync::OnceLock}; use crate::sdk::{ test_wallets, wallet::{Keystore, Wallet}, }; -thread_local! { - /// Set by [`load_toml_config`] to the parent directory of the config file - /// before deserialization begins. Consumed by [`WalletConfig`]'s `From` impl - /// to resolve relative `pem` / `keyfile` paths automatically, with no - /// per-config boilerplate needed. - pub static CONFIG_BASE_DIR: RefCell> = RefCell::new(None); -} +use super::ConfigPath; /// Raw helper used only for serde deserialization. #[derive(Deserialize)] struct WalletConfigRaw { test_wallet: Option, - pem: Option, - keyfile: Option, + pem: Option, + keyfile: Option, keystore_password: Option, } @@ -47,30 +41,17 @@ pub struct WalletConfig { impl From for WalletConfig { fn from(raw: WalletConfigRaw) -> Self { - let mut config = WalletConfig { + WalletConfig { test_wallet: raw.test_wallet, - pem: raw.pem, - keyfile: raw.keyfile, + pem: raw.pem.map(Into::into), + keyfile: raw.keyfile.map(Into::into), keystore_password: raw.keystore_password, cache: OnceLock::new(), - }; - CONFIG_BASE_DIR.with(|cell| { - if let Some(ref base_dir) = *cell.borrow() { - config.set_base_dir(base_dir); - } - }); - config + } } } impl WalletConfig { - /// Sets the base directory used to resolve relative `pem` / `keyfile` paths - /// during deserialization. Called by [`load_toml_config`] before parsing the - /// config file; pass `None` to clear after parsing. - pub(crate) fn set_config_base_dir(base_dir: Option) { - CONFIG_BASE_DIR.with(|cell| *cell.borrow_mut() = base_dir); - } - /// Creates a `WalletConfig` from a built-in test wallet name. pub fn from_test_wallet(name: impl Into) -> Self { WalletConfig { @@ -93,24 +74,6 @@ impl WalletConfig { } } - /// Resolves any relative `pem` / `keyfile` paths against `base_dir`. - /// - /// Call this after deserializing a `WalletConfig` from a TOML/JSON file, - /// passing the directory that contains the config file as `base_dir`. - /// Absolute paths and `test_wallet` names are left unchanged. - pub fn set_base_dir(&mut self, base_dir: &std::path::Path) { - if let Some(pem) = &self.pem { - if pem.is_relative() { - self.pem = Some(base_dir.join(pem)); - } - } - if let Some(keyfile) = &self.keyfile { - if keyfile.is_relative() { - self.keyfile = Some(base_dir.join(keyfile)); - } - } - } - /// Returns the wallet, loading and caching it on first call. /// /// Priority: `test_wallet` > `pem` > `keyfile`. From 6dbbb60cfeb08835480f222d17a5d39b03364bc3 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 02:11:00 +0300 Subject: [PATCH 105/195] workspace deps - initial implementation --- Cargo.toml | 9 ++ contracts/examples/adder/Cargo.toml | 6 +- .../src/cargo_toml/cargo_toml_contents.rs | 42 +++++++++ .../src/cargo_toml/cargo_toml_deps.rs | 5 ++ .../src/cargo_toml/cargo_toml_deps_raw.rs | 15 ++++ .../meta-lib/src/contract/meta_config.rs | 85 ++++++++++++++++++- framework/meta/src/cmd/print_util.rs | 3 + 7 files changed, 160 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f8412f7b8a..23ebf9d1c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -212,3 +212,12 @@ members = [ # "chain/wasmer-prod", ## not included, references wasmer 2.2 # "contracts/feature-tests/gas-tests", ## uncomment for easier development ] + +[workspace.dependencies.multiversx-sc] +version = "0.66.2" +path = "framework/base" + +[workspace.dependencies.multiversx-sc-scenario] +version = "0.66.2" +path = "framework/scenario" + diff --git a/contracts/examples/adder/Cargo.toml b/contracts/examples/adder/Cargo.toml index a385e8d948..06a8c11b70 100644 --- a/contracts/examples/adder/Cargo.toml +++ b/contracts/examples/adder/Cargo.toml @@ -9,10 +9,8 @@ publish = false path = "src/adder.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs index 776b8f4847..c872d6bcb6 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs @@ -12,6 +12,7 @@ use super::DependencyRawValue; pub const CARGO_TOML_DEPENDENCIES: &str = "dependencies"; pub const CARGO_TOML_DEV_DEPENDENCIES: &str = "dev-dependencies"; +pub const WORKSPACE: &str = "workspace"; pub const PACKAGE: &str = "package"; pub const AUTHORS: &str = "authors"; const DEFAULT_WORKSPACE_RESOLVER: &str = "3"; @@ -154,6 +155,14 @@ impl CargoTomlContents { .map(DependencyRawValue::parse_toml_value) } + pub fn workspace_dependency_raw_value(&self, crate_name: &str) -> Option { + self.toml_value + .get(WORKSPACE) + .and_then(|workspace| workspace.get(CARGO_TOML_DEPENDENCIES)) + .and_then(|deps| deps.get(crate_name)) + .map(DependencyRawValue::parse_toml_value) + } + pub fn insert_dependency_raw_value(&mut self, crate_name: &str, raw_value: DependencyRawValue) { self.dependencies_mut() .insert(crate_name.to_owned(), raw_value.into_toml_value()); @@ -458,9 +467,17 @@ version = "=0.54.1" git = "https://github.com/multiversx/repo1" rev = "85c31b9ce730bd5ffe41589c353d935a14baaa96" +[dependencies.by-workspace] +workspace = true +features = ["alloc", "std"] + [dependencies.by-path-1] path = "a/b/c" +[workspace.dependencies.by-workspace-root] +version = "0.54.4" +path = "g/h/i" + [dependencies] by-version-2 = "0.54.2" by-version-2-strict = "=0.54.3" @@ -568,6 +585,31 @@ by-git-commit-2 = { git = "https://github.com/multiversx/repo2", rev = "e990be82 }) ); + // workspace + let raw_value = cargo_toml.dependency_raw_value("by-workspace").unwrap(); + assert_eq!( + raw_value, + DependencyRawValue { + workspace: true, + features: ["alloc".to_owned(), "std".to_owned()].into(), + ..Default::default() + }, + ); + assert_eq!(raw_value.interpret(), DependencyReference::Workspace,); + + let raw_value = cargo_toml + .workspace_dependency_raw_value("by-workspace-root") + .unwrap(); + let path = Path::new("g").join("h").join("i"); + assert_eq!( + raw_value, + DependencyRawValue { + version: Some("0.54.4".to_owned()), + path: Some(path), + ..Default::default() + }, + ); + // path let raw_value = cargo_toml.dependency_raw_value("by-path-1").unwrap(); let path = Path::new("a").join("b").join("c"); diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_deps.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_deps.rs index 632b545e85..15f6476759 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_deps.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_deps.rs @@ -29,6 +29,7 @@ pub struct GitTagReference { #[derive(Debug, Clone, PartialEq, Eq)] pub enum DependencyReference { Version(VersionReq), + Workspace, GitCommit(GitCommitReference), GitBranch(GitBranchReference), GitTag(GitTagReference), @@ -57,6 +58,10 @@ impl DependencyReference { impl DependencyRawValue { /// Interprets the raw dependency value as one of several possible formats. pub fn interpret(self) -> DependencyReference { + if self.workspace { + return DependencyReference::Workspace; + } + // path is top priority if let Some(path) = self.path { return DependencyReference::Path(path); diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs index 40b02bf5b3..22f3efdce0 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeSet, path::PathBuf}; #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct DependencyRawValue { pub version: Option, + pub workspace: bool, pub git: Option, pub rev: Option, pub branch: Option, @@ -27,6 +28,9 @@ impl DependencyRawValue { if let Some(toml::Value::String(version)) = table.get("version") { result.version = Some(version.to_owned()); } + if let Some(toml::Value::Boolean(workspace)) = table.get("workspace") { + result.workspace = *workspace; + } if let Some(toml::Value::String(path)) = table.get("path") { result.path = Some(PathBuf::from(path)); } @@ -42,6 +46,13 @@ impl DependencyRawValue { if let Some(toml::Value::String(tag)) = table.get("tag") { result.tag = Some(tag.to_owned()); } + if let Some(toml::Value::Array(features)) = table.get("features") { + result.features = features + .iter() + .map(|feature| feature.as_str().expect("feature is not a string")) + .map(str::to_owned) + .collect(); + } result } _ => panic!("Unsupported dependency value"), @@ -55,6 +66,10 @@ impl DependencyRawValue { table.insert("version".to_string(), toml::Value::String(version)); } + if self.workspace { + table.insert("workspace".to_string(), toml::Value::Boolean(true)); + } + if let Some(git) = self.git { table.insert("git".to_string(), toml::Value::String(git)); } diff --git a/framework/meta-lib/src/contract/meta_config.rs b/framework/meta-lib/src/contract/meta_config.rs index e022714712..e194f7719b 100644 --- a/framework/meta-lib/src/contract/meta_config.rs +++ b/framework/meta-lib/src/contract/meta_config.rs @@ -6,7 +6,7 @@ use std::{ use multiversx_sc::abi::ContractAbi; use crate::{ - cargo_toml::CargoTomlContents, + cargo_toml::{CargoTomlContents, DependencyRawValue}, cli::BuildArgs, print_util::{print_removing_wasm_crate, print_workspace_target_dir}, tools::{check_tools_installed, find_current_workspace}, @@ -71,11 +71,26 @@ impl MetaConfig { let main_cargo_toml_contents = CargoTomlContents::load_from_file(Path::new("..").join("Cargo.toml")); let crate_name = main_cargo_toml_contents.package_name(); + let workspace_cargo_toml_contents = find_current_workspace().map(|workspace_path| { + ( + workspace_path.clone(), + CargoTomlContents::load_from_file(workspace_path.join("Cargo.toml")), + ) + }); for contract in self.sc_config.contracts.iter() { let mut framework_dependency = main_cargo_toml_contents .dependency_raw_value(FRAMEWORK_NAME_BASE) .expect("missing framework dependency in Cargo.toml"); + if framework_dependency.workspace { + framework_dependency = resolve_workspace_dependency( + FRAMEWORK_NAME_BASE, + framework_dependency, + workspace_cargo_toml_contents + .as_ref() + .expect("missing workspace for workspace dependency"), + ); + } if contract.settings.std { framework_dependency.features.insert("std".to_owned()); } @@ -206,6 +221,59 @@ fn copy_to_wasm_unmanaged_ei() { } } +fn resolve_workspace_dependency( + crate_name: &str, + local_dependency: DependencyRawValue, + (workspace_path, workspace_cargo_toml_contents): &(PathBuf, CargoTomlContents), +) -> DependencyRawValue { + let mut workspace_dependency = workspace_cargo_toml_contents + .workspace_dependency_raw_value(crate_name) + .unwrap_or_else(|| panic!("missing workspace dependency in Cargo.toml: {crate_name}")); + workspace_dependency + .features + .extend(local_dependency.features); + if let Some(path) = &mut workspace_dependency.path { + *path = path_relative_to_current_contract(workspace_path, path); + } + workspace_dependency +} + +fn path_relative_to_current_contract( + workspace_path: &Path, + workspace_dependency_path: &Path, +) -> PathBuf { + let current_dir = std::env::current_dir().expect("failed to get current directory"); + let contract_dir = current_dir + .join("..") + .canonicalize() + .expect("failed to resolve contract directory"); + let absolute_dependency_path = workspace_path + .join(workspace_dependency_path) + .canonicalize() + .expect("failed to resolve workspace dependency path"); + + path_relative_from_to(&absolute_dependency_path, &contract_dir) +} + +fn path_relative_from_to(path: &Path, base: &Path) -> PathBuf { + let path_components: Vec<_> = path.components().collect(); + let base_components: Vec<_> = base.components().collect(); + let common_len = path_components + .iter() + .zip(base_components.iter()) + .take_while(|(path_component, base_component)| path_component == base_component) + .count(); + + let mut relative_path = PathBuf::new(); + for _ in common_len..base_components.len() { + relative_path.push(".."); + } + for component in &path_components[common_len..] { + relative_path.push(component.as_os_str()); + } + relative_path +} + #[cfg(test)] mod tests { use std::path::Path; @@ -278,4 +346,19 @@ members = [\".\"] EXPECTED_CARGO_TOML_CONTENTS.to_string() ); } + + #[test] + fn test_path_relative_from_to() { + assert_eq!( + super::path_relative_from_to( + Path::new("/repo/framework/base"), + Path::new("/repo/contracts/examples/adder"), + ), + Path::new("..") + .join("..") + .join("..") + .join("framework") + .join("base"), + ); + } } diff --git a/framework/meta/src/cmd/print_util.rs b/framework/meta/src/cmd/print_util.rs index d0895cdf87..432dd6d089 100644 --- a/framework/meta/src/cmd/print_util.rs +++ b/framework/meta/src/cmd/print_util.rs @@ -43,6 +43,9 @@ pub fn print_tree_dir_metadata(dir: &RelevantDirectory, last_version: &Framework print!(" {}", version_string.red()); }; } + DependencyReference::Workspace => { + print!(" {}", "[workspace]".green()); + } DependencyReference::GitCommit(git_reference) => { let git_string = format!( "[git: {} rev: {}]", From 37873c2760e22ed4833292f92b5772962f9a2d35 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 03:58:32 +0300 Subject: [PATCH 106/195] workspace deps - refactor --- framework/meta-lib/src/cargo_toml.rs | 2 + .../src/cargo_toml/cargo_toml_contents.rs | 157 +++++++++++++++--- .../src/cargo_toml/workspace_dependencies.rs | 157 ++++++++++++++++++ .../meta-lib/src/contract/meta_config.rs | 91 +--------- .../meta/src/cmd/template/contract_creator.rs | 11 ++ .../src/cmd/template/template_adjuster.rs | 34 +++- 6 files changed, 343 insertions(+), 109 deletions(-) create mode 100644 framework/meta-lib/src/cargo_toml/workspace_dependencies.rs diff --git a/framework/meta-lib/src/cargo_toml.rs b/framework/meta-lib/src/cargo_toml.rs index 4bdb7da87d..bb9826bd30 100644 --- a/framework/meta-lib/src/cargo_toml.rs +++ b/framework/meta-lib/src/cargo_toml.rs @@ -2,6 +2,7 @@ mod cargo_toml_contents; mod cargo_toml_deps; mod cargo_toml_deps_raw; mod version_req; +mod workspace_dependencies; pub use cargo_toml_contents::{ CARGO_TOML_DEPENDENCIES, CARGO_TOML_DEV_DEPENDENCIES, CargoTomlContents, @@ -10,3 +11,4 @@ pub use cargo_toml_contents::{ pub use cargo_toml_deps::{DependencyReference, GitCommitReference}; pub use cargo_toml_deps_raw::DependencyRawValue; pub use version_req::VersionReq; +pub use workspace_dependencies::WorkspaceDependencies; diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs index c872d6bcb6..573db8b6ed 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs @@ -8,11 +8,10 @@ use toml::{Value, value::Table}; use crate::contract::sc_config::ContractVariantProfile; -use super::DependencyRawValue; +use super::{DependencyRawValue, WorkspaceDependencies}; pub const CARGO_TOML_DEPENDENCIES: &str = "dependencies"; pub const CARGO_TOML_DEV_DEPENDENCIES: &str = "dev-dependencies"; -pub const WORKSPACE: &str = "workspace"; pub const PACKAGE: &str = "package"; pub const AUTHORS: &str = "authors"; const DEFAULT_WORKSPACE_RESOLVER: &str = "3"; @@ -155,14 +154,76 @@ impl CargoTomlContents { .map(DependencyRawValue::parse_toml_value) } - pub fn workspace_dependency_raw_value(&self, crate_name: &str) -> Option { - self.toml_value - .get(WORKSPACE) - .and_then(|workspace| workspace.get(CARGO_TOML_DEPENDENCIES)) - .and_then(|deps| deps.get(crate_name)) - .map(DependencyRawValue::parse_toml_value) + /// Returns the directory containing this manifest. + pub fn manifest_dir(&self) -> &Path { + self.path.parent().unwrap_or_else(|| Path::new(".")) + } + + /// Resolves a single inherited `workspace = true` dependency to a concrete dependency value. + /// + /// The workspace-relative `path` entries are rebased relative to this manifest's directory. + pub fn resolve_dependency_from_workspace( + &self, + workspace_dependencies: &WorkspaceDependencies, + crate_name: &str, + local_dependency: DependencyRawValue, + ) -> DependencyRawValue { + workspace_dependencies.resolve_dependency(crate_name, local_dependency, self.manifest_dir()) + } + + /// Resolves inherited `workspace = true` dependencies in both `[dependencies]` and + /// `[dev-dependencies]` using dependencies declared in the workspace root. + pub fn resolve_workspace_dependencies( + &mut self, + workspace_dependencies: &WorkspaceDependencies, + ) { + if workspace_dependencies.is_empty() { + return; + } + + let manifest_dir = self.manifest_dir().to_path_buf(); + + if self.has_dependencies() { + resolve_workspace_dependencies_in_table( + self.dependencies_mut(), + workspace_dependencies, + &manifest_dir, + ); + } + if self.has_dev_dependencies() { + resolve_workspace_dependencies_in_table( + self.dev_dependencies_mut(), + workspace_dependencies, + &manifest_dir, + ); + } } +} +fn resolve_workspace_dependencies_in_table( + deps_map: &mut Table, + workspace_dependencies: &WorkspaceDependencies, + manifest_dir: &Path, +) { + for (crate_name, value) in deps_map { + if !dependency_uses_workspace(value) { + continue; + } + let local_dependency = DependencyRawValue::parse_toml_value(value); + let resolved_dependency = + workspace_dependencies.resolve_dependency(&crate_name, local_dependency, manifest_dir); + *value = resolved_dependency.into_toml_value(); + } +} + +fn dependency_uses_workspace(value: &Value) -> bool { + value + .get("workspace") + .and_then(|workspace| workspace.as_bool()) + .unwrap_or_default() +} + +impl CargoTomlContents { pub fn insert_dependency_raw_value(&mut self, crate_name: &str, raw_value: DependencyRawValue) { self.dependencies_mut() .insert(crate_name.to_owned(), raw_value.into_toml_value()); @@ -434,7 +495,9 @@ fn remove_quotes(var: &Value) -> String { #[cfg(test)] mod tests { use super::*; - use crate::cargo_toml::{DependencyReference, GitCommitReference, VersionReq}; + use crate::cargo_toml::{ + DependencyReference, GitCommitReference, VersionReq, WorkspaceDependencies, + }; #[test] fn test_change_from_base_to_adapter_path() { @@ -597,19 +660,6 @@ by-git-commit-2 = { git = "https://github.com/multiversx/repo2", rev = "e990be82 ); assert_eq!(raw_value.interpret(), DependencyReference::Workspace,); - let raw_value = cargo_toml - .workspace_dependency_raw_value("by-workspace-root") - .unwrap(); - let path = Path::new("g").join("h").join("i"); - assert_eq!( - raw_value, - DependencyRawValue { - version: Some("0.54.4".to_owned()), - path: Some(path), - ..Default::default() - }, - ); - // path let raw_value = cargo_toml.dependency_raw_value("by-path-1").unwrap(); let path = Path::new("a").join("b").join("c"); @@ -634,4 +684,67 @@ by-git-commit-2 = { git = "https://github.com/multiversx/repo2", rev = "e990be82 ); assert_eq!(raw_value.interpret(), DependencyReference::Path(path),); } + + #[test] + fn test_resolve_workspace_dependencies() { + let mut cargo_toml = CargoTomlContents::parse_string( + r#" +[dependencies.by-workspace] +workspace = true +features = ["std"] + +[dev-dependencies.by-dev-workspace] +workspace = true + +[workspace.dependencies.by-workspace] +version = "0.54.0" +path = "framework/base" +features = ["alloc"] + +[workspace.dependencies.by-dev-workspace] +version = "0.54.1" +path = "framework/scenario" +"#, + Path::new("/repo/contracts/examples/adder/Cargo.toml"), + ); + let workspace_dependencies = + WorkspaceDependencies::from_cargo_toml(Path::new("/repo"), &cargo_toml.clone()); + + cargo_toml.resolve_workspace_dependencies(&workspace_dependencies); + + let path = Path::new("..") + .join("..") + .join("..") + .join("framework") + .join("base"); + assert_eq!( + cargo_toml.dependency_raw_value("by-workspace").unwrap(), + DependencyRawValue { + version: Some("0.54.0".to_owned()), + path: Some(path), + features: ["alloc".to_owned(), "std".to_owned()].into(), + ..Default::default() + }, + ); + + let dev_deps = cargo_toml + .toml_value + .get(CARGO_TOML_DEV_DEPENDENCIES) + .unwrap() + .as_table() + .unwrap(); + let path = Path::new("..") + .join("..") + .join("..") + .join("framework") + .join("scenario"); + assert_eq!( + DependencyRawValue::parse_toml_value(dev_deps.get("by-dev-workspace").unwrap()), + DependencyRawValue { + version: Some("0.54.1".to_owned()), + path: Some(path), + ..Default::default() + }, + ); + } } diff --git a/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs b/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs new file mode 100644 index 0000000000..919b48da4a --- /dev/null +++ b/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs @@ -0,0 +1,157 @@ +use std::{ + collections::BTreeMap, + path::{Component, Path, PathBuf}, +}; + +use toml::value::Table; + +use super::{CARGO_TOML_DEPENDENCIES, CargoTomlContents, DependencyRawValue}; + +const WORKSPACE: &str = "workspace"; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct WorkspaceDependencies { + pub workspace_path: PathBuf, + pub dependencies: BTreeMap, +} + +impl WorkspaceDependencies { + pub fn load_from_dir(workspace_path: impl AsRef) -> Self { + let workspace_path = workspace_path.as_ref(); + let cargo_toml_contents = + CargoTomlContents::load_from_file(workspace_path.join("Cargo.toml")); + + Self::from_cargo_toml(workspace_path, &cargo_toml_contents) + } + + pub fn from_cargo_toml( + workspace_path: impl AsRef, + cargo_toml_contents: &CargoTomlContents, + ) -> Self { + let dependencies = Self::dependencies_table(cargo_toml_contents) + .map(Self::parse_dependencies_table) + .unwrap_or_default(); + + Self { + workspace_path: workspace_path.as_ref().to_path_buf(), + dependencies, + } + } + + pub fn is_empty(&self) -> bool { + self.dependencies.is_empty() + } + + pub fn get(&self, crate_name: &str) -> Option<&DependencyRawValue> { + self.dependencies.get(crate_name) + } + + /// Resolves a single dependency from workspace root to a concrete value. + /// + /// This rebases workspace-relative `path` entries to be relative to `relative_dir`. + pub(crate) fn resolve_dependency( + &self, + crate_name: &str, + local_dependency: DependencyRawValue, + relative_dir: &Path, + ) -> DependencyRawValue { + let mut workspace_dependency = self + .get(crate_name) + .unwrap_or_else(|| panic!("missing workspace dependency in Cargo.toml: {crate_name}")) + .clone(); + workspace_dependency + .features + .extend(local_dependency.features); + + if let Some(path) = &mut workspace_dependency.path { + *path = path_relative_from_to( + &absolute_path(&self.workspace_path).join(&path), + &absolute_path(relative_dir), + ); + } + + workspace_dependency + } + + fn dependencies_table(cargo_toml_contents: &CargoTomlContents) -> Option<&Table> { + cargo_toml_contents + .toml_value + .get(WORKSPACE) + .and_then(|workspace| workspace.get(CARGO_TOML_DEPENDENCIES)) + .and_then(|deps| deps.as_table()) + } + + fn parse_dependencies_table(deps: &Table) -> BTreeMap { + deps.iter() + .map(|(crate_name, value)| { + ( + crate_name.to_owned(), + DependencyRawValue::parse_toml_value(value), + ) + }) + .collect() + } +} + +fn absolute_path(path: &Path) -> PathBuf { + if path.is_absolute() { + normalize_path(path) + } else { + normalize_path(&std::env::current_dir().unwrap().join(path)) + } +} + +fn normalize_path(path: &Path) -> PathBuf { + let mut result = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::ParentDir => { + result.pop(); + } + _ => result.push(component.as_os_str()), + } + } + result +} + +fn path_relative_from_to(path: &Path, base: &Path) -> PathBuf { + let path = normalize_path(path); + let base = normalize_path(base); + let path_components: Vec<_> = path.components().collect(); + let base_components: Vec<_> = base.components().collect(); + let common_len = path_components + .iter() + .zip(base_components.iter()) + .take_while(|(path_component, base_component)| path_component == base_component) + .count(); + + let mut relative_path = PathBuf::new(); + for _ in common_len..base_components.len() { + relative_path.push(".."); + } + for component in &path_components[common_len..] { + relative_path.push(component.as_os_str()); + } + relative_path +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + #[test] + fn test_path_relative_from_to() { + assert_eq!( + super::path_relative_from_to( + Path::new("/repo/framework/base"), + Path::new("/repo/contracts/examples/adder"), + ), + Path::new("..") + .join("..") + .join("..") + .join("framework") + .join("base"), + ); + } +} diff --git a/framework/meta-lib/src/contract/meta_config.rs b/framework/meta-lib/src/contract/meta_config.rs index e194f7719b..d3277a1aea 100644 --- a/framework/meta-lib/src/contract/meta_config.rs +++ b/framework/meta-lib/src/contract/meta_config.rs @@ -6,7 +6,7 @@ use std::{ use multiversx_sc::abi::ContractAbi; use crate::{ - cargo_toml::{CargoTomlContents, DependencyRawValue}, + cargo_toml::{CargoTomlContents, WorkspaceDependencies}, cli::BuildArgs, print_util::{print_removing_wasm_crate, print_workspace_target_dir}, tools::{check_tools_installed, find_current_workspace}, @@ -68,29 +68,18 @@ impl MetaConfig { /// Cargo.toml files for all wasm crates are generated from the main contract Cargo.toml, /// by changing the package name. pub fn generate_cargo_toml_for_all_wasm_crates(&mut self) { - let main_cargo_toml_contents = + let mut main_cargo_toml_contents = CargoTomlContents::load_from_file(Path::new("..").join("Cargo.toml")); let crate_name = main_cargo_toml_contents.package_name(); - let workspace_cargo_toml_contents = find_current_workspace().map(|workspace_path| { - ( - workspace_path.clone(), - CargoTomlContents::load_from_file(workspace_path.join("Cargo.toml")), - ) - }); + if let Some(workspace_path) = find_current_workspace() { + let workspace_dependencies = WorkspaceDependencies::load_from_dir(workspace_path); + main_cargo_toml_contents.resolve_workspace_dependencies(&workspace_dependencies); + } for contract in self.sc_config.contracts.iter() { let mut framework_dependency = main_cargo_toml_contents .dependency_raw_value(FRAMEWORK_NAME_BASE) .expect("missing framework dependency in Cargo.toml"); - if framework_dependency.workspace { - framework_dependency = resolve_workspace_dependency( - FRAMEWORK_NAME_BASE, - framework_dependency, - workspace_cargo_toml_contents - .as_ref() - .expect("missing workspace for workspace dependency"), - ); - } if contract.settings.std { framework_dependency.features.insert("std".to_owned()); } @@ -221,59 +210,6 @@ fn copy_to_wasm_unmanaged_ei() { } } -fn resolve_workspace_dependency( - crate_name: &str, - local_dependency: DependencyRawValue, - (workspace_path, workspace_cargo_toml_contents): &(PathBuf, CargoTomlContents), -) -> DependencyRawValue { - let mut workspace_dependency = workspace_cargo_toml_contents - .workspace_dependency_raw_value(crate_name) - .unwrap_or_else(|| panic!("missing workspace dependency in Cargo.toml: {crate_name}")); - workspace_dependency - .features - .extend(local_dependency.features); - if let Some(path) = &mut workspace_dependency.path { - *path = path_relative_to_current_contract(workspace_path, path); - } - workspace_dependency -} - -fn path_relative_to_current_contract( - workspace_path: &Path, - workspace_dependency_path: &Path, -) -> PathBuf { - let current_dir = std::env::current_dir().expect("failed to get current directory"); - let contract_dir = current_dir - .join("..") - .canonicalize() - .expect("failed to resolve contract directory"); - let absolute_dependency_path = workspace_path - .join(workspace_dependency_path) - .canonicalize() - .expect("failed to resolve workspace dependency path"); - - path_relative_from_to(&absolute_dependency_path, &contract_dir) -} - -fn path_relative_from_to(path: &Path, base: &Path) -> PathBuf { - let path_components: Vec<_> = path.components().collect(); - let base_components: Vec<_> = base.components().collect(); - let common_len = path_components - .iter() - .zip(base_components.iter()) - .take_while(|(path_component, base_component)| path_component == base_component) - .count(); - - let mut relative_path = PathBuf::new(); - for _ in common_len..base_components.len() { - relative_path.push(".."); - } - for component in &path_components[common_len..] { - relative_path.push(component.as_os_str()); - } - relative_path -} - #[cfg(test)] mod tests { use std::path::Path; @@ -346,19 +282,4 @@ members = [\".\"] EXPECTED_CARGO_TOML_CONTENTS.to_string() ); } - - #[test] - fn test_path_relative_from_to() { - assert_eq!( - super::path_relative_from_to( - Path::new("/repo/framework/base"), - Path::new("/repo/contracts/examples/adder"), - ), - Path::new("..") - .join("..") - .join("..") - .join("framework") - .join("base"), - ); - } } diff --git a/framework/meta/src/cmd/template/contract_creator.rs b/framework/meta/src/cmd/template/contract_creator.rs index 0144b474a3..e22dd66ded 100644 --- a/framework/meta/src/cmd/template/contract_creator.rs +++ b/framework/meta/src/cmd/template/contract_creator.rs @@ -1,4 +1,5 @@ use crate::{cli::TemplateArgs, version::FrameworkVersion, version_history::LAST_TEMPLATE_VERSION}; +use multiversx_sc_meta_lib::cargo_toml::WorkspaceDependencies; use super::{ ContractCreatorTarget, RepoSource, RepoVersion, TemplateAdjuster, @@ -77,6 +78,9 @@ impl<'a> ContractCreator<'a> { .unwrap_or_else(|| panic!("Unknown template {template_name}")); let metadata = template_source.metadata.clone(); + let workspace_dependencies = + WorkspaceDependencies::load_from_dir(repo_source.repo_path()); + ContractCreator { repo_source, template_source, @@ -86,12 +90,14 @@ impl<'a> ContractCreator<'a> { target, keep_paths, new_author, + workspace_dependencies, }, } } pub fn create_contract(&self, args_tag: FrameworkVersion) { self.copy_template(args_tag); + self.resolve_workspace_dependencies(); self.update_dependencies(); self.rename_template(); } @@ -105,6 +111,11 @@ impl<'a> ContractCreator<'a> { self.adjuster.update_cargo_toml_files(); } + pub fn resolve_workspace_dependencies(&self) { + self.adjuster + .resolve_workspace_dependencies(); + } + pub fn rename_template(&self) { self.adjuster.rename_template_to(); } diff --git a/framework/meta/src/cmd/template/template_adjuster.rs b/framework/meta/src/cmd/template/template_adjuster.rs index bf82c791b6..e95d028b34 100644 --- a/framework/meta/src/cmd/template/template_adjuster.rs +++ b/framework/meta/src/cmd/template/template_adjuster.rs @@ -1,9 +1,9 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use super::{ContractCreatorTarget, template_metadata::TemplateMetadata}; use crate::cmd::upgrade::upgrade_common::{rename_files, replace_in_files}; use convert_case::{Case, Casing}; -use multiversx_sc_meta_lib::cargo_toml::CargoTomlContents; +use multiversx_sc_meta_lib::cargo_toml::{CargoTomlContents, WorkspaceDependencies}; use ruplacer::Query; use toml::value::Table; @@ -16,8 +16,21 @@ pub struct TemplateAdjuster { pub target: ContractCreatorTarget, pub keep_paths: bool, pub new_author: Option, + pub workspace_dependencies: WorkspaceDependencies, } impl TemplateAdjuster { + pub fn resolve_workspace_dependencies(&self) { + for cargo_toml_path in self.cargo_toml_paths() { + if !cargo_toml_path.exists() { + continue; + } + + let mut toml = CargoTomlContents::load_from_file(&cargo_toml_path); + toml.resolve_workspace_dependencies(&self.workspace_dependencies); + toml.save_to_file(&cargo_toml_path); + } + } + pub fn update_cargo_toml_files(&self) { let author_as_str = self .new_author @@ -29,6 +42,23 @@ impl TemplateAdjuster { self.update_cargo_toml_interact(author_as_str); } + fn cargo_toml_paths(&self) -> Vec { + let mut paths = vec![ + self.target.contract_dir().join(CARGO_TOML), + self.target.contract_dir().join("meta").join(CARGO_TOML), + self.target.contract_dir().join("wasm").join(CARGO_TOML), + ]; + if self.metadata.has_interactor { + paths.push( + self.target + .contract_dir() + .join("interactor") + .join(CARGO_TOML), + ); + } + paths + } + fn update_cargo_toml_root(&self, author: String) { let cargo_toml_path = self.target.contract_dir().join(CARGO_TOML); let mut toml = CargoTomlContents::load_from_file(&cargo_toml_path); From 1d099e550ceb89f30d5c1272f843e367745646d4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 04:25:25 +0300 Subject: [PATCH 107/195] interactor ConfigPath for code --- .../adder/interactor/src/basic_interactor.rs | 6 +- .../tests/basic_interactor_cs_test.rs | 2 +- .../interact/src/multisig_interact_config.rs | 6 +- .../dex-interactor/src/config.rs | 5 +- .../dex-interactor/src/dex_interactor.rs | 2 +- .../mesh-interactor/src/call_tree_config.rs | 9 +- framework/snippets/src/config/config_path.rs | 127 ++++++++++++++++-- 7 files changed, 132 insertions(+), 25 deletions(-) diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/basic_interactor.rs index e6142efbd6..6fd8769634 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/basic_interactor.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; /// Adder Interact general settings #[derive(Debug, Deserialize)] pub struct GeneralConfig { - pub contract_path: String, + pub contract_path: ConfigPath, } /// Adder Interact configuration @@ -107,7 +107,7 @@ impl BasicInteractor { .gas(100_000_000) .typed(adder_proxy::AdderProxy) .init(0u64) - .code(MxscPath::new(&self.config.general.contract_path)) + .code(&self.config.general.contract_path) .returns(ReturnsNewBech32Address) .run() .await; @@ -125,7 +125,7 @@ impl BasicInteractor { .gas(6_000_000) .typed(adder_proxy::AdderProxy) .upgrade(new_value) - .code(MxscPath::new(&self.config.general.contract_path)) + .code(&self.config.general.contract_path) .code_metadata(CodeMetadata::UPGRADEABLE) .returns(ReturnsHandledOrError::new()) .run() diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs index bb3685f8ce..3303232b6c 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs @@ -5,7 +5,7 @@ use serial_test::serial; fn chain_simulator_config() -> Config { Config { general: GeneralConfig { - contract_path: "../output/adder.mxsc.json".to_owned(), + contract_path: "../output/adder.mxsc.json".into(), }, connection: ConnectionConfig::chain_simulator(), owner: WalletConfig::from_test_wallet("mike"), diff --git a/contracts/examples/multisig/interact/src/multisig_interact_config.rs b/contracts/examples/multisig/interact/src/multisig_interact_config.rs index 8db7cac8bb..37e6ce3f58 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact_config.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact_config.rs @@ -1,12 +1,10 @@ -use multiversx_sc_scenario::imports::Bech32Address; -use multiversx_sc_snippets::InteractorConfig; -use multiversx_sc_snippets::imports::{ConnectionConfig, Wallet, WalletConfig}; +use multiversx_sc_snippets::imports::*; use serde::Deserialize; /// General settings for the multisig interact. #[derive(Debug, Deserialize)] pub struct GeneralConfig { - pub contract_path: String, + pub contract_path: ConfigPath, pub quorum: usize, pub wegld_address: Bech32Address, } diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs index c6609a7b3a..2eaca723db 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/config.rs @@ -1,11 +1,10 @@ -use multiversx_sc_snippets::InteractorConfig; -use multiversx_sc_snippets::imports::{Bech32Address, ConnectionConfig, Wallet, WalletConfig}; +use multiversx_sc_snippets::imports::*; use serde::Deserialize; /// General settings for the forwarder-blind dex interact. #[derive(Debug, Deserialize)] pub struct GeneralConfig { - pub contract_path: String, + pub contract_path: ConfigPath, pub wegld_address: Bech32Address, pub pair_address: Bech32Address, pub wegld_token_id: String, diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs index 03e069623d..ee7275bfd3 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/dex_interactor.rs @@ -142,7 +142,7 @@ impl ContractInteract { .gas(80_000_000u64) .typed(forwarder_blind_proxy::ForwarderBlindProxy) .init() - .code(FilePath(&self.config.general.contract_path)) + .code(&self.config.general.contract_path) .code_metadata(CodeMetadata::PAYABLE) .returns(ReturnsNewBech32Address) }); diff --git a/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs b/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs index 82cc0a2be3..5850ebee90 100644 --- a/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs +++ b/contracts/feature-tests/composability/mesh-interactor/src/call_tree_config.rs @@ -1,6 +1,7 @@ use std::{ collections::BTreeMap, io::{Read, Write}, + path::Path, }; use multiversx_sc::types::ShardId; @@ -13,8 +14,8 @@ pub const STATE_FILE: &str = "current.toml"; /// General settings for the mesh interact. #[derive(Debug, Deserialize)] pub struct GeneralConfig { - pub contract_path: String, - pub call_tree_path: String, + pub contract_path: ConfigPath, + pub call_tree_path: ConfigPath, } /// Top-level interactor config, stored in `config.toml`. @@ -153,7 +154,7 @@ pub struct CallTreeLayout { impl CallTreeLayout { /// Serialize the call tree layout to a TOML file. - pub fn save_to_file(&self, path: &str) { + pub fn save_to_file(&self, path: impl AsRef) { const HEADER: &str = "# Code generated by mesh-interactor. DO NOT EDIT.\n\n"; let toml_str = toml::to_string_pretty(self).expect("failed to serialize call tree layout"); let mut file = std::fs::File::create(path).expect("failed to create call tree layout file"); @@ -164,7 +165,7 @@ impl CallTreeLayout { } /// Load a call tree layout from a TOML file. - pub fn load_from_file(path: &str) -> Self { + pub fn load_from_file(path: impl AsRef) -> Self { let mut file = std::fs::File::open(path).expect("failed to open call tree layout file"); let mut content = String::new(); file.read_to_string(&mut content) diff --git a/framework/snippets/src/config/config_path.rs b/framework/snippets/src/config/config_path.rs index dd7a49543f..7b8b0eaa89 100644 --- a/framework/snippets/src/config/config_path.rs +++ b/framework/snippets/src/config/config_path.rs @@ -5,6 +5,9 @@ use std::{ path::{Path, PathBuf}, }; +use multiversx_chain_scenario_format::value_interpreter::interpret_string; +use multiversx_sc_scenario::ScenarioTxEnv; +use multiversx_sc_scenario::multiversx_sc::types::{AnnotatedValue, ManagedBuffer, TxCodeValue}; use serde::Deserialize; thread_local! { @@ -13,7 +16,7 @@ thread_local! { /// /// [`ConfigPath`]'s [`Deserialize`] impl reads this to resolve relative /// paths at deserialization time, with no per-config boilerplate needed. - pub static CONFIG_BASE_DIR: RefCell> = RefCell::new(None); + pub static CONFIG_BASE_DIR: RefCell> = const { RefCell::new(None) }; } /// A [`PathBuf`] newtype that, when deserialized, automatically resolves @@ -34,9 +37,64 @@ thread_local! { /// pem = "wallets/owner.pem" # resolved relative to the config file location /// ``` #[derive(Debug, Clone)] -pub struct ConfigPath(pub PathBuf); +pub struct ConfigPath { + /// The raw path as written in the config file (relative). + pub relative_path: PathBuf, + /// The fully-resolved path, joined against the config file's directory. + /// Equal to `relative_path` for absolute paths or when no base dir is set. + pub absolute_path: PathBuf, +} impl ConfigPath { + /// Returns the resolved path (absolute when a base dir was available). + pub fn absolute_path(&self) -> &Path { + &self.absolute_path + } + + /// Returns the scenario expression for this path, rebased relative to `base`. + /// + /// The prefix is chosen based on the file extension: + /// - `mxsc:` for `.mxsc.json` contract bundles + /// - `file:` for all other paths + pub fn scenario_expr(&self, base: &Path) -> String { + let rebased = self.rebase_to(base); + let prefix = match self.absolute_path.to_str() { + Some(s) if s.ends_with(".mxsc.json") => "mxsc:", + _ => "file:", + }; + format!("{prefix}{}", rebased.display()) + } + + /// Returns this path expressed relative to `base`. + /// + /// Walks up from `base` with `../` steps for each non-shared component, + /// then appends the remaining components of the absolute path. + /// Falls back to the absolute path if no common root exists (e.g. different + /// drive letters on Windows). + pub fn rebase_to(&self, base: &Path) -> PathBuf { + let base_comps: Vec<_> = base.components().collect(); + let abs_comps: Vec<_> = self.absolute_path.components().collect(); + + let common = base_comps + .iter() + .zip(abs_comps.iter()) + .take_while(|(b, a)| b == a) + .count(); + + if common == 0 { + return self.absolute_path.clone(); + } + + let mut rel = PathBuf::new(); + for _ in &base_comps[common..] { + rel.push(".."); + } + for comp in &abs_comps[common..] { + rel.push(comp.as_os_str()); + } + rel + } + /// Sets the base directory used to resolve relative paths during /// deserialization. Called by [`load_toml_config`] before and after /// parsing; pass `None` to clear. @@ -47,32 +105,41 @@ impl ConfigPath { impl AsRef for ConfigPath { fn as_ref(&self) -> &Path { - &self.0 + &self.absolute_path } } impl Deref for ConfigPath { type Target = Path; fn deref(&self) -> &Path { - &self.0 + &self.absolute_path } } impl fmt::Display for ConfigPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.display().fmt(f) + self.absolute_path.display().fmt(f) } } impl From for ConfigPath { fn from(p: PathBuf) -> Self { - ConfigPath(p) + ConfigPath { + relative_path: p.clone(), + absolute_path: p, + } } } impl From for PathBuf { fn from(p: ConfigPath) -> Self { - p.0 + p.absolute_path + } +} + +impl From<&str> for ConfigPath { + fn from(s: &str) -> Self { + ConfigPath::from(PathBuf::from(s)) } } @@ -80,7 +147,10 @@ impl<'de> Deserialize<'de> for ConfigPath { fn deserialize>(deserializer: D) -> Result { let raw = PathBuf::deserialize(deserializer)?; if raw.is_absolute() { - return Ok(ConfigPath(raw)); + return Ok(ConfigPath { + relative_path: raw.clone(), + absolute_path: raw, + }); } let resolved = CONFIG_BASE_DIR.with(|cell| { cell.borrow() @@ -88,6 +158,45 @@ impl<'de> Deserialize<'de> for ConfigPath { .map(|base| base.join(&raw)) .unwrap_or_else(|| raw.clone()) }); - Ok(ConfigPath(resolved)) + Ok(ConfigPath { + relative_path: raw, + absolute_path: resolved, + }) } } + +/// Allows `ConfigPath` to be passed directly to `.code(...)` in interactor +/// transactions. The annotation uses the `mxsc:` prefix with the resolved +/// absolute path; the value is loaded from that file via the scenario +/// interpreter (which parses `.mxsc.json` and extracts the wasm bytecode). +impl AnnotatedValue> for &ConfigPath +where + Env: ScenarioTxEnv, +{ + fn annotation(&self, env: &Env) -> ManagedBuffer { + let context = env.env_data().interpreter_context(); + self.scenario_expr(&context.context_path).into() + } + + fn to_value(&self, env: &Env) -> ManagedBuffer { + let context = env.env_data().interpreter_context(); + let expr = self.scenario_expr(&context.context_path); + interpret_string(&expr, &context).into() + } +} + +impl AnnotatedValue> for ConfigPath +where + Env: ScenarioTxEnv, +{ + fn annotation(&self, env: &Env) -> ManagedBuffer { + (&self).annotation(env) + } + + fn to_value(&self, env: &Env) -> ManagedBuffer { + (&self).to_value(env) + } +} + +impl TxCodeValue for &ConfigPath where Env: ScenarioTxEnv {} +impl TxCodeValue for ConfigPath where Env: ScenarioTxEnv {} From 50ff37781003a1cd51afc4a0b6f3251b2c71d502 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 04:52:50 +0300 Subject: [PATCH 108/195] workspace deps - cleanup & wasm crate generator bugfix --- .../src/cargo_toml/cargo_toml_contents.rs | 30 ++++--------------- .../src/cargo_toml/cargo_toml_deps_raw.rs | 11 +++++++ .../src/cargo_toml/workspace_dependencies.rs | 4 --- .../meta-lib/src/contract/meta_config.rs | 1 + .../meta/src/cmd/template/contract_creator.rs | 8 ++--- 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs index 573db8b6ed..27d7a996d3 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs @@ -159,18 +159,6 @@ impl CargoTomlContents { self.path.parent().unwrap_or_else(|| Path::new(".")) } - /// Resolves a single inherited `workspace = true` dependency to a concrete dependency value. - /// - /// The workspace-relative `path` entries are rebased relative to this manifest's directory. - pub fn resolve_dependency_from_workspace( - &self, - workspace_dependencies: &WorkspaceDependencies, - crate_name: &str, - local_dependency: DependencyRawValue, - ) -> DependencyRawValue { - workspace_dependencies.resolve_dependency(crate_name, local_dependency, self.manifest_dir()) - } - /// Resolves inherited `workspace = true` dependencies in both `[dependencies]` and /// `[dev-dependencies]` using dependencies declared in the workspace root. pub fn resolve_workspace_dependencies( @@ -206,21 +194,15 @@ fn resolve_workspace_dependencies_in_table( manifest_dir: &Path, ) { for (crate_name, value) in deps_map { - if !dependency_uses_workspace(value) { + let mut local_dep = DependencyRawValue::parse_toml_value(value); + if !local_dep.workspace { continue; } - let local_dependency = DependencyRawValue::parse_toml_value(value); - let resolved_dependency = - workspace_dependencies.resolve_dependency(&crate_name, local_dependency, manifest_dir); - *value = resolved_dependency.into_toml_value(); - } -} -fn dependency_uses_workspace(value: &Value) -> bool { - value - .get("workspace") - .and_then(|workspace| workspace.as_bool()) - .unwrap_or_default() + let workspace_dep = workspace_dependencies.resolve_dependency(&crate_name, manifest_dir); + local_dep.replace_workspace_dep(&workspace_dep); + *value = local_dep.into_toml_value(); + } } impl CargoTomlContents { diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs index 22f3efdce0..d893f8b7fc 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs @@ -107,4 +107,15 @@ impl DependencyRawValue { toml::Value::Table(table) } + + /// Removes the `workspace = true` flag and replaces the dependency fields from `workspace_dep`. + pub fn replace_workspace_dep(&mut self, workspace_dep: &DependencyRawValue) { + self.workspace = false; + self.version = workspace_dep.version.clone(); + self.git = workspace_dep.git.clone(); + self.rev = workspace_dep.rev.clone(); + self.branch = workspace_dep.branch.clone(); + self.tag = workspace_dep.tag.clone(); + self.path = workspace_dep.path.clone(); + } } diff --git a/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs b/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs index 919b48da4a..bf866be229 100644 --- a/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs +++ b/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs @@ -52,16 +52,12 @@ impl WorkspaceDependencies { pub(crate) fn resolve_dependency( &self, crate_name: &str, - local_dependency: DependencyRawValue, relative_dir: &Path, ) -> DependencyRawValue { let mut workspace_dependency = self .get(crate_name) .unwrap_or_else(|| panic!("missing workspace dependency in Cargo.toml: {crate_name}")) .clone(); - workspace_dependency - .features - .extend(local_dependency.features); if let Some(path) = &mut workspace_dependency.path { *path = path_relative_from_to( diff --git a/framework/meta-lib/src/contract/meta_config.rs b/framework/meta-lib/src/contract/meta_config.rs index d3277a1aea..737463b1bc 100644 --- a/framework/meta-lib/src/contract/meta_config.rs +++ b/framework/meta-lib/src/contract/meta_config.rs @@ -80,6 +80,7 @@ impl MetaConfig { let mut framework_dependency = main_cargo_toml_contents .dependency_raw_value(FRAMEWORK_NAME_BASE) .expect("missing framework dependency in Cargo.toml"); + framework_dependency.features.clear(); if contract.settings.std { framework_dependency.features.insert("std".to_owned()); } diff --git a/framework/meta/src/cmd/template/contract_creator.rs b/framework/meta/src/cmd/template/contract_creator.rs index e22dd66ded..936fb62c82 100644 --- a/framework/meta/src/cmd/template/contract_creator.rs +++ b/framework/meta/src/cmd/template/contract_creator.rs @@ -78,9 +78,8 @@ impl<'a> ContractCreator<'a> { .unwrap_or_else(|| panic!("Unknown template {template_name}")); let metadata = template_source.metadata.clone(); - let workspace_dependencies = - WorkspaceDependencies::load_from_dir(repo_source.repo_path()); - + let workspace_dependencies = WorkspaceDependencies::load_from_dir(repo_source.repo_path()); + ContractCreator { repo_source, template_source, @@ -112,8 +111,7 @@ impl<'a> ContractCreator<'a> { } pub fn resolve_workspace_dependencies(&self) { - self.adjuster - .resolve_workspace_dependencies(); + self.adjuster.resolve_workspace_dependencies(); } pub fn rename_template(&self) { From 08e2c354a929ef38e5771080ef5dca1151d3ee6d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 10:39:14 +0300 Subject: [PATCH 109/195] clippy fix --- framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs index 27d7a996d3..0c56aecf47 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs @@ -199,7 +199,7 @@ fn resolve_workspace_dependencies_in_table( continue; } - let workspace_dep = workspace_dependencies.resolve_dependency(&crate_name, manifest_dir); + let workspace_dep = workspace_dependencies.resolve_dependency(crate_name, manifest_dir); local_dep.replace_workspace_dep(&workspace_dep); *value = local_dep.into_toml_value(); } From dec693ae7d1bda81ee9dd52e4854e2b10df76277 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 12:47:27 +0300 Subject: [PATCH 110/195] workspace deps - fix repro build tests --- .../src/cargo_toml/cargo_toml_deps_raw.rs | 2 ++ .../src/cargo_toml/workspace_dependencies.rs | 19 +++++++++++++++ .../meta/src/folder_structure/strip_path.rs | 23 +++++++++++++------ framework/meta/src/lib.rs | 1 + framework/meta/tests/repro_build_test.rs | 7 +++++- tools/strip-path/src/main.rs | 9 +++++++- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs index d893f8b7fc..f025fe7dca 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs @@ -109,6 +109,8 @@ impl DependencyRawValue { } /// Removes the `workspace = true` flag and replaces the dependency fields from `workspace_dep`. + /// + /// Doesn't touch anything other than dependency specification fields (version, git, rev, branch, tag, path). No features. pub fn replace_workspace_dep(&mut self, workspace_dep: &DependencyRawValue) { self.workspace = false; self.version = workspace_dep.version.clone(); diff --git a/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs b/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs index bf866be229..b047ab6f7b 100644 --- a/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs +++ b/framework/meta-lib/src/cargo_toml/workspace_dependencies.rs @@ -5,6 +5,8 @@ use std::{ use toml::value::Table; +use crate::tools::find_workspace; + use super::{CARGO_TOML_DEPENDENCIES, CargoTomlContents, DependencyRawValue}; const WORKSPACE: &str = "workspace"; @@ -24,6 +26,23 @@ impl WorkspaceDependencies { Self::from_cargo_toml(workspace_path, &cargo_toml_contents) } + /// Like [`load_from_dir`](Self::load_from_dir), but returns an empty + /// `WorkspaceDependencies` if no `Cargo.toml` exists at `workspace_path` + /// instead of panicking. + pub fn try_load_from_dir(workspace_path: impl AsRef) -> Option { + let workspace_path = workspace_path.as_ref(); + let cargo_toml_path = workspace_path.join("Cargo.toml"); + if !cargo_toml_path.is_file() { + return None; + } + Some(Self::load_from_dir(workspace_path)) + } + + pub fn find_from_dir(dir: impl AsRef) -> Option { + let workspace_path = find_workspace(dir.as_ref())?; + Self::try_load_from_dir(workspace_path) + } + pub fn from_cargo_toml( workspace_path: impl AsRef, cargo_toml_contents: &CargoTomlContents, diff --git a/framework/meta/src/folder_structure/strip_path.rs b/framework/meta/src/folder_structure/strip_path.rs index f1ef6284e3..f78c2613cc 100644 --- a/framework/meta/src/folder_structure/strip_path.rs +++ b/framework/meta/src/folder_structure/strip_path.rs @@ -1,17 +1,18 @@ use super::{DirectoryType, FRAMEWORK_CRATE_NAMES, RelevantDirectories, RelevantDirectory}; -use multiversx_sc_meta_lib::cargo_toml::CargoTomlContents; +use multiversx_sc_meta_lib::cargo_toml::{CargoTomlContents, WorkspaceDependencies}; use std::path::Path; /// Recursively finds all crates under `path` that depend on a framework crate, -/// then removes `path = "..."` from those framework dependencies in each +/// resolves any `workspace = true` entries using `workspace_dependencies`, then +/// removes `path = "..."` from those framework dependencies in each /// `Cargo.toml`. Directories named in `ignore` are skipped during traversal. /// For contract crates, the `meta/` subdirectory is also processed. -pub fn strip_path(path: &Path, ignore: &[String]) { +pub fn strip_path(path: &Path, ignore: &[String], workspace_dependencies: &WorkspaceDependencies) { let dirs = RelevantDirectories::find_all(path, ignore); let mut count = 0; for dir in dirs.iter() { - if strip_framework_paths(dir) { + if strip_framework_paths(dir, workspace_dependencies) { count += 1; } // `populate_directories` stops recursing into a contract crate's children, @@ -21,7 +22,7 @@ pub fn strip_path(path: &Path, ignore: &[String]) { path: dir.meta_path(), ..dir.clone() }; - if strip_framework_paths(&meta_dir) { + if strip_framework_paths(&meta_dir, workspace_dependencies) { count += 1; } } @@ -30,20 +31,28 @@ pub fn strip_path(path: &Path, ignore: &[String]) { println!("Done. Stripped path from {count} Cargo.toml file(s)."); } -/// Removes `path = "..."` from framework dependencies in a directory's Cargo.toml. +/// Resolves `workspace = true` entries, then removes `path = "..."` from +/// framework dependencies in a directory's Cargo.toml. /// Returns `true` if the file was modified. -fn strip_framework_paths(dir: &RelevantDirectory) -> bool { +fn strip_framework_paths( + dir: &RelevantDirectory, + workspace_dependencies: &WorkspaceDependencies, +) -> bool { let cargo_toml_path = dir.path.join("Cargo.toml"); if !cargo_toml_path.is_file() { return false; } let mut cargo_toml = CargoTomlContents::load_from_file(&cargo_toml_path); + cargo_toml.resolve_workspace_dependencies(workspace_dependencies); let changed = cargo_toml.strip_dependency_paths(FRAMEWORK_CRATE_NAMES); if changed { println!("Stripped: {}", cargo_toml_path.display()); cargo_toml.save_to_file(&cargo_toml_path); + } else if !workspace_dependencies.is_empty() { + // Still save if workspace deps were resolved even if no path stripping occurred. + cargo_toml.save_to_file(&cargo_toml_path); } changed diff --git a/framework/meta/src/lib.rs b/framework/meta/src/lib.rs index dd07012de9..c5ddb7a557 100644 --- a/framework/meta/src/lib.rs +++ b/framework/meta/src/lib.rs @@ -8,6 +8,7 @@ pub mod cmd; pub mod folder_structure; pub use multiversx_sc_meta_lib::abi_json; +pub use multiversx_sc_meta_lib::cargo_toml; pub use multiversx_sc_meta_lib::ei; pub use multiversx_sc_meta_lib::ei_check_json; pub use multiversx_sc_meta_lib::version; diff --git a/framework/meta/tests/repro_build_test.rs b/framework/meta/tests/repro_build_test.rs index 567669377e..2ef11b44d9 100644 --- a/framework/meta/tests/repro_build_test.rs +++ b/framework/meta/tests/repro_build_test.rs @@ -20,6 +20,7 @@ use multiversx_sc_meta::{ folder_structure::{setup_workspace, strip_path}, }; use multiversx_sc_meta_lib::{ + cargo_toml::WorkspaceDependencies, cli::{BuildArgs, ContractCliAction}, tools::find_current_workspace, }; @@ -267,7 +268,11 @@ fn setup_build_dir(workspace: &Path, build_dir: &Path, contracts: &[&str]) { fs::remove_dir_all(&adder_interactor).unwrap(); } - strip_path(build_dir, &["target".to_string()]); + strip_path( + build_dir, + &["target".to_string()], + &WorkspaceDependencies::load_from_dir(workspace), + ); for contract in contracts { setup_workspace( diff --git a/tools/strip-path/src/main.rs b/tools/strip-path/src/main.rs index dbee2ada0e..c1e9f74b95 100644 --- a/tools/strip-path/src/main.rs +++ b/tools/strip-path/src/main.rs @@ -1,7 +1,14 @@ +use multiversx_sc_meta::cargo_toml::WorkspaceDependencies; use multiversx_sc_meta::folder_structure::strip_path; use std::path::Path; fn main() { let root = std::env::args().nth(1).unwrap_or_else(|| ".".to_string()); - strip_path(Path::new(&root), &["target".to_string()]); + let workspace_dependencies = + WorkspaceDependencies::find_from_dir(&root).expect("failed to find workspace"); + strip_path( + Path::new(&root), + &["target".to_string()], + &workspace_dependencies, + ); } From 0f628bf940b9b03c7636fc8e76043aeda051b5bd Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 13:01:32 +0300 Subject: [PATCH 111/195] workspace deps - adder test --- Cargo.toml | 7 +++++++ contracts/examples/adder/interactor/Cargo.toml | 3 +-- contracts/examples/adder/meta/Cargo.toml | 4 +--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23ebf9d1c0..98ebea4a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -221,3 +221,10 @@ path = "framework/base" version = "0.66.2" path = "framework/scenario" +[workspace.dependencies.multiversx-sc-meta-lib] +version = "0.66.2" +path = "framework/meta-lib" + +[workspace.dependencies.multiversx-sc-snippets] +version = "0.66.2" +path = "framework/snippets" diff --git a/contracts/examples/adder/interactor/Cargo.toml b/contracts/examples/adder/interactor/Cargo.toml index 0822398439..cf160416ac 100644 --- a/contracts/examples/adder/interactor/Cargo.toml +++ b/contracts/examples/adder/interactor/Cargo.toml @@ -16,8 +16,7 @@ path = "src/basic_interactor.rs" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/examples/adder/meta/Cargo.toml b/contracts/examples/adder/meta/Cargo.toml index f09422eae0..aa024c4e35 100644 --- a/contracts/examples/adder/meta/Cargo.toml +++ b/contracts/examples/adder/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true From 6ea939eceb19ad814fe7953a9a6b8579c45a1e79 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 13:07:36 +0300 Subject: [PATCH 112/195] test fix --- framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs index 0c56aecf47..7bbc1d5223 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs @@ -704,7 +704,7 @@ path = "framework/scenario" DependencyRawValue { version: Some("0.54.0".to_owned()), path: Some(path), - features: ["alloc".to_owned(), "std".to_owned()].into(), + features: ["std".to_owned()].into(), ..Default::default() }, ); From 64657eb71e9e6264a98c4fa601a45a2e5fa6c838 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:13:22 +0300 Subject: [PATCH 113/195] DependencyRawValue - add missing fields default_features, optional, package, registry --- .../src/cargo_toml/cargo_toml_deps_raw.rs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs index f025fe7dca..39c5093b64 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs @@ -10,8 +10,28 @@ pub struct DependencyRawValue { pub tag: Option, pub path: Option, pub features: BTreeSet, + pub default_features: Option, + pub optional: Option, + pub package: Option, + pub registry: Option, } +/// All dependency table keys recognized by Cargo and modeled in [`DependencyRawValue`]. +const KNOWN_DEP_KEYS: &[&str] = &[ + "version", + "workspace", + "path", + "git", + "rev", + "branch", + "tag", + "features", + "default-features", + "optional", + "package", + "registry", +]; + impl DependencyRawValue { pub fn from_version(version: &str) -> Self { Self { @@ -24,6 +44,12 @@ impl DependencyRawValue { match toml_value { toml::Value::String(version) => DependencyRawValue::from_version(version), toml::Value::Table(table) => { + for key in table.keys() { + assert!( + KNOWN_DEP_KEYS.contains(&key.as_str()), + "unknown dependency key: {key:?}" + ); + } let mut result = DependencyRawValue::default(); if let Some(toml::Value::String(version)) = table.get("version") { result.version = Some(version.to_owned()); @@ -53,6 +79,20 @@ impl DependencyRawValue { .map(str::to_owned) .collect(); } + if let Some(toml::Value::Boolean(default_features)) = + table.get("default-features") + { + result.default_features = Some(*default_features); + } + if let Some(toml::Value::Boolean(optional)) = table.get("optional") { + result.optional = Some(*optional); + } + if let Some(toml::Value::String(package)) = table.get("package") { + result.package = Some(package.to_owned()); + } + if let Some(toml::Value::String(registry)) = table.get("registry") { + result.registry = Some(registry.to_owned()); + } result } _ => panic!("Unsupported dependency value"), @@ -105,12 +145,33 @@ impl DependencyRawValue { ); } + if let Some(default_features) = self.default_features { + table.insert( + "default-features".to_string(), + toml::Value::Boolean(default_features), + ); + } + + if let Some(optional) = self.optional { + table.insert("optional".to_string(), toml::Value::Boolean(optional)); + } + + if let Some(package) = self.package { + table.insert("package".to_string(), toml::Value::String(package)); + } + + if let Some(registry) = self.registry { + table.insert("registry".to_string(), toml::Value::String(registry)); + } + toml::Value::Table(table) } /// Removes the `workspace = true` flag and replaces the dependency fields from `workspace_dep`. /// - /// Doesn't touch anything other than dependency specification fields (version, git, rev, branch, tag, path). No features. + /// Copies specification fields (version, git, rev, branch, tag, path, default-features, + /// package, registry) from the workspace dep. Does not touch `features` or `optional`, + /// which are local-only attributes that the crate controls independently. pub fn replace_workspace_dep(&mut self, workspace_dep: &DependencyRawValue) { self.workspace = false; self.version = workspace_dep.version.clone(); @@ -119,5 +180,8 @@ impl DependencyRawValue { self.branch = workspace_dep.branch.clone(); self.tag = workspace_dep.tag.clone(); self.path = workspace_dep.path.clone(); + self.default_features = workspace_dep.default_features; + self.package = workspace_dep.package.clone(); + self.registry = workspace_dep.registry.clone(); } } From d6b03b7bc0df6517513003ae247eacd8aa1aad3b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:16:02 +0300 Subject: [PATCH 114/195] fix after review --- tools/strip-path/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/strip-path/src/main.rs b/tools/strip-path/src/main.rs index c1e9f74b95..474880a359 100644 --- a/tools/strip-path/src/main.rs +++ b/tools/strip-path/src/main.rs @@ -5,7 +5,7 @@ use std::path::Path; fn main() { let root = std::env::args().nth(1).unwrap_or_else(|| ".".to_string()); let workspace_dependencies = - WorkspaceDependencies::find_from_dir(&root).expect("failed to find workspace"); + WorkspaceDependencies::find_from_dir(&root).unwrap_or_default(); strip_path( Path::new(&root), &["target".to_string()], From c05057fccfaec3cbd25ef4f6a8107aa4197899f5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:21:48 +0300 Subject: [PATCH 115/195] DependencyRawValue - uses serde derive --- .../src/cargo_toml/cargo_toml_deps_raw.rs | 160 +++--------------- 1 file changed, 25 insertions(+), 135 deletions(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs index 39c5093b64..96527ee94b 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_deps_raw.rs @@ -1,37 +1,40 @@ use std::{collections::BTreeSet, path::PathBuf}; -#[derive(Default, Debug, Clone, PartialEq, Eq)] +use serde::{Deserialize, Serialize}; + +fn is_false(b: &bool) -> bool { + !*b +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DependencyRawValue { + #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, + #[serde(default, skip_serializing_if = "is_false")] pub workspace: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub git: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub rev: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub branch: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub tag: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] pub features: BTreeSet, + #[serde(rename = "default-features", skip_serializing_if = "Option::is_none")] pub default_features: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub optional: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub package: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub registry: Option, } -/// All dependency table keys recognized by Cargo and modeled in [`DependencyRawValue`]. -const KNOWN_DEP_KEYS: &[&str] = &[ - "version", - "workspace", - "path", - "git", - "rev", - "branch", - "tag", - "features", - "default-features", - "optional", - "package", - "registry", -]; - impl DependencyRawValue { pub fn from_version(version: &str) -> Self { Self { @@ -43,128 +46,15 @@ impl DependencyRawValue { pub fn parse_toml_value(toml_value: &toml::Value) -> Self { match toml_value { toml::Value::String(version) => DependencyRawValue::from_version(version), - toml::Value::Table(table) => { - for key in table.keys() { - assert!( - KNOWN_DEP_KEYS.contains(&key.as_str()), - "unknown dependency key: {key:?}" - ); - } - let mut result = DependencyRawValue::default(); - if let Some(toml::Value::String(version)) = table.get("version") { - result.version = Some(version.to_owned()); - } - if let Some(toml::Value::Boolean(workspace)) = table.get("workspace") { - result.workspace = *workspace; - } - if let Some(toml::Value::String(path)) = table.get("path") { - result.path = Some(PathBuf::from(path)); - } - if let Some(toml::Value::String(git)) = table.get("git") { - result.git = Some(git.to_owned()); - } - if let Some(toml::Value::String(rev)) = table.get("rev") { - result.rev = Some(rev.to_owned()); - } - if let Some(toml::Value::String(branch)) = table.get("branch") { - result.branch = Some(branch.to_owned()); - } - if let Some(toml::Value::String(tag)) = table.get("tag") { - result.tag = Some(tag.to_owned()); - } - if let Some(toml::Value::Array(features)) = table.get("features") { - result.features = features - .iter() - .map(|feature| feature.as_str().expect("feature is not a string")) - .map(str::to_owned) - .collect(); - } - if let Some(toml::Value::Boolean(default_features)) = - table.get("default-features") - { - result.default_features = Some(*default_features); - } - if let Some(toml::Value::Boolean(optional)) = table.get("optional") { - result.optional = Some(*optional); - } - if let Some(toml::Value::String(package)) = table.get("package") { - result.package = Some(package.to_owned()); - } - if let Some(toml::Value::String(registry)) = table.get("registry") { - result.registry = Some(registry.to_owned()); - } - result - } - _ => panic!("Unsupported dependency value"), + _ => toml_value + .clone() + .try_into() + .unwrap_or_else(|e| panic!("failed to parse dependency value: {e}")), } } pub fn into_toml_value(self) -> toml::Value { - let mut table = toml::map::Map::new(); - - if let Some(version) = self.version { - table.insert("version".to_string(), toml::Value::String(version)); - } - - if self.workspace { - table.insert("workspace".to_string(), toml::Value::Boolean(true)); - } - - if let Some(git) = self.git { - table.insert("git".to_string(), toml::Value::String(git)); - } - - if let Some(rev) = self.rev { - table.insert("rev".to_string(), toml::Value::String(rev)); - } - - if let Some(branch) = self.branch { - table.insert("branch".to_string(), toml::Value::String(branch)); - } - - if let Some(tag) = self.tag { - table.insert("tag".to_string(), toml::Value::String(tag)); - } - - if let Some(path) = self.path { - table.insert( - "path".to_string(), - toml::Value::String(path.to_string_lossy().into_owned()), - ); - } - - if !self.features.is_empty() { - table.insert( - "features".to_string(), - toml::Value::Array( - self.features - .iter() - .map(|feature| toml::Value::String(feature.clone())) - .collect(), - ), - ); - } - - if let Some(default_features) = self.default_features { - table.insert( - "default-features".to_string(), - toml::Value::Boolean(default_features), - ); - } - - if let Some(optional) = self.optional { - table.insert("optional".to_string(), toml::Value::Boolean(optional)); - } - - if let Some(package) = self.package { - table.insert("package".to_string(), toml::Value::String(package)); - } - - if let Some(registry) = self.registry { - table.insert("registry".to_string(), toml::Value::String(registry)); - } - - toml::Value::Table(table) + toml::Value::try_from(self).expect("failed to serialize dependency value") } /// Removes the `workspace = true` flag and replaces the dependency fields from `workspace_dep`. From 59a27e56f37ff92e7c87ace53005f0b5e7394f73 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:25:52 +0300 Subject: [PATCH 116/195] workspace deps - strip path improvement --- .../src/cargo_toml/cargo_toml_contents.rs | 17 ++++++++++++----- .../meta/src/folder_structure/strip_path.rs | 13 ++++++------- tools/strip-path/src/main.rs | 3 +-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs index 7bbc1d5223..1208d4e988 100644 --- a/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs +++ b/framework/meta-lib/src/cargo_toml/cargo_toml_contents.rs @@ -161,30 +161,34 @@ impl CargoTomlContents { /// Resolves inherited `workspace = true` dependencies in both `[dependencies]` and /// `[dev-dependencies]` using dependencies declared in the workspace root. + /// Returns `true` if any entry was replaced. pub fn resolve_workspace_dependencies( &mut self, workspace_dependencies: &WorkspaceDependencies, - ) { + ) -> bool { if workspace_dependencies.is_empty() { - return; + return false; } let manifest_dir = self.manifest_dir().to_path_buf(); + let mut changed = false; if self.has_dependencies() { - resolve_workspace_dependencies_in_table( + changed |= resolve_workspace_dependencies_in_table( self.dependencies_mut(), workspace_dependencies, &manifest_dir, ); } if self.has_dev_dependencies() { - resolve_workspace_dependencies_in_table( + changed |= resolve_workspace_dependencies_in_table( self.dev_dependencies_mut(), workspace_dependencies, &manifest_dir, ); } + + changed } } @@ -192,7 +196,8 @@ fn resolve_workspace_dependencies_in_table( deps_map: &mut Table, workspace_dependencies: &WorkspaceDependencies, manifest_dir: &Path, -) { +) -> bool { + let mut changed = false; for (crate_name, value) in deps_map { let mut local_dep = DependencyRawValue::parse_toml_value(value); if !local_dep.workspace { @@ -202,7 +207,9 @@ fn resolve_workspace_dependencies_in_table( let workspace_dep = workspace_dependencies.resolve_dependency(crate_name, manifest_dir); local_dep.replace_workspace_dep(&workspace_dep); *value = local_dep.into_toml_value(); + changed = true; } + changed } impl CargoTomlContents { diff --git a/framework/meta/src/folder_structure/strip_path.rs b/framework/meta/src/folder_structure/strip_path.rs index f78c2613cc..3043f2f919 100644 --- a/framework/meta/src/folder_structure/strip_path.rs +++ b/framework/meta/src/folder_structure/strip_path.rs @@ -44,16 +44,15 @@ fn strip_framework_paths( } let mut cargo_toml = CargoTomlContents::load_from_file(&cargo_toml_path); - cargo_toml.resolve_workspace_dependencies(workspace_dependencies); - let changed = cargo_toml.strip_dependency_paths(FRAMEWORK_CRATE_NAMES); + let ws_changed = cargo_toml.resolve_workspace_dependencies(workspace_dependencies); + let paths_stripped = cargo_toml.strip_dependency_paths(FRAMEWORK_CRATE_NAMES); - if changed { + if paths_stripped { println!("Stripped: {}", cargo_toml_path.display()); - cargo_toml.save_to_file(&cargo_toml_path); - } else if !workspace_dependencies.is_empty() { - // Still save if workspace deps were resolved even if no path stripping occurred. + } + if ws_changed || paths_stripped { cargo_toml.save_to_file(&cargo_toml_path); } - changed + paths_stripped } diff --git a/tools/strip-path/src/main.rs b/tools/strip-path/src/main.rs index 474880a359..f200d8dbdb 100644 --- a/tools/strip-path/src/main.rs +++ b/tools/strip-path/src/main.rs @@ -4,8 +4,7 @@ use std::path::Path; fn main() { let root = std::env::args().nth(1).unwrap_or_else(|| ".".to_string()); - let workspace_dependencies = - WorkspaceDependencies::find_from_dir(&root).unwrap_or_default(); + let workspace_dependencies = WorkspaceDependencies::find_from_dir(&root).unwrap_or_default(); strip_path( Path::new(&root), &["target".to_string()], From 9e0319b466f565568d35faf8df08ba973fb330ad Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:45:16 +0300 Subject: [PATCH 117/195] interactor transfer tx pretty print --- .../interactor/interactor_scenario/interactor_transfer.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs index c61dcd8879..6b94b1c359 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs @@ -28,7 +28,11 @@ where .await .unwrap(); - println!("transfer tx hash: {tx_hash}"); + if let Some(ex) = &self.explorer_url { + println!("transfer: {}", ex.tx_url(&tx_hash)); + } else { + println!("transfer tx hash: {tx_hash}"); + } info!("transfer tx hash: {}", tx_hash); retrieve_tx_on_network(self.proxy(), tx_hash.clone()) From 55c8dfc97c343dc046df8a0e6ab487399952979b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:50:46 +0300 Subject: [PATCH 118/195] multisig interactor fixes --- contracts/examples/multisig/interact/config.toml | 2 +- contracts/examples/multisig/interact/src/multisig_interact.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/examples/multisig/interact/config.toml b/contracts/examples/multisig/interact/config.toml index d0b1004a75..ff2c5cc918 100644 --- a/contracts/examples/multisig/interact/config.toml +++ b/contracts/examples/multisig/interact/config.toml @@ -1,6 +1,6 @@ [connection] chain_type = 'real' -gateway_uri = 'https://devnet-gateway.multiversx.com' +gateway_uri = 'https://testnet-gateway.multiversx.com' [general] contract_path = '../output/multisig.mxsc.json' diff --git a/contracts/examples/multisig/interact/src/multisig_interact.rs b/contracts/examples/multisig/interact/src/multisig_interact.rs index a75940355b..1c07dd67c9 100644 --- a/contracts/examples/multisig/interact/src/multisig_interact.rs +++ b/contracts/examples/multisig/interact/src/multisig_interact.rs @@ -128,6 +128,7 @@ impl MultisigInteract { .typed(multisig_proxy::MultisigProxy) .init(quorum, board) .code(&self.multisig_code) + .code_metadata(CodeMetadata::PAYABLE) .gas(NumExpr("100,000,000")) .returns(ReturnsNewBech32Address) .run() @@ -182,7 +183,8 @@ impl MultisigInteract { .tx() .from(self.config.wallet.address()) .to(self.state.current_multisig_address()) - .egld(BigUint::from(50_000_000_000_000_000u64)) // 0,05 or 5 * 10^16 + .egld(50_000_000_000_000_000u64) // 0,05 or 5 * 10^16 + .gas(50_000) .run() .await; } From a46707e7f4116bfae9191195f5fd5428e089ae43 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 13:18:54 +0300 Subject: [PATCH 119/195] workspace deps - base --- contracts/benchmarks/large-storage/Cargo.toml | 3 +-- contracts/benchmarks/mappers/benchmark-common/Cargo.toml | 3 +-- contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/map-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/queue-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/set-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/single-value-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/vec-repeat/Cargo.toml | 3 +-- contracts/benchmarks/send-tx-repeat/Cargo.toml | 3 +-- contracts/benchmarks/str-repeat/Cargo.toml | 3 +-- contracts/benchmarks/very-large-storage/Cargo.toml | 3 +-- contracts/core/price-aggregator/Cargo.toml | 3 +-- contracts/core/price-aggregator/meta/Cargo.toml | 3 +-- contracts/core/wegld-swap/Cargo.toml | 3 +-- contracts/core/wegld-swap/meta/Cargo.toml | 3 +-- contracts/examples/bonding-curve-contract/Cargo.toml | 3 +-- contracts/examples/check-pause/Cargo.toml | 3 +-- contracts/examples/crowdfunding/Cargo.toml | 3 +-- contracts/examples/crypto-bubbles/Cargo.toml | 3 +-- contracts/examples/crypto-kitties/common/kitty/Cargo.toml | 3 +-- contracts/examples/crypto-kitties/common/random/Cargo.toml | 3 +-- contracts/examples/crypto-kitties/kitty-auction/Cargo.toml | 3 +-- contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml | 3 +-- contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml | 3 +-- contracts/examples/crypto-zombies/Cargo.toml | 3 +-- contracts/examples/digital-cash/Cargo.toml | 3 +-- contracts/examples/empty/Cargo.toml | 3 +-- contracts/examples/esdt-transfer-with-fee/Cargo.toml | 3 +-- contracts/examples/factorial/Cargo.toml | 3 +-- contracts/examples/fractional-nfts/Cargo.toml | 3 +-- contracts/examples/lottery/Cargo.toml | 3 +-- contracts/examples/multisig/Cargo.toml | 3 +-- contracts/examples/multisig/interact/Cargo.toml | 3 +-- contracts/examples/nft-minter/Cargo.toml | 3 +-- contracts/examples/nft-storage-prepay/Cargo.toml | 3 +-- contracts/examples/nft-subscription/Cargo.toml | 3 +-- contracts/examples/order-book/factory/Cargo.toml | 3 +-- contracts/examples/order-book/pair/Cargo.toml | 3 +-- contracts/examples/ping-pong-egld/Cargo.toml | 3 +-- contracts/examples/proxy-pause/Cargo.toml | 3 +-- contracts/examples/rewards-distribution/Cargo.toml | 3 +-- contracts/examples/seed-nft-minter/Cargo.toml | 3 +-- contracts/examples/token-release/Cargo.toml | 3 +-- contracts/feature-tests/abi-tester/Cargo.toml | 3 +-- contracts/feature-tests/alloc-features/Cargo.toml | 3 +-- contracts/feature-tests/basic-features/Cargo.toml | 3 +-- contracts/feature-tests/big-float-features/Cargo.toml | 3 +-- contracts/feature-tests/composability/Cargo.toml | 3 +-- .../composability/builtin-func-features/Cargo.toml | 3 +-- .../feature-tests/composability/esdt-contract-pair/Cargo.toml | 3 +-- .../composability/esdt-contract-pair/first-contract/Cargo.toml | 3 +-- .../esdt-contract-pair/first-contract/meta/Cargo.toml | 3 +-- .../esdt-contract-pair/second-contract/Cargo.toml | 3 +-- .../esdt-contract-pair/second-contract/meta/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/child/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/parent/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml | 3 +-- .../feature-tests/composability/forwarder-blind/Cargo.toml | 3 +-- .../composability/forwarder-blind/dex-interactor/Cargo.toml | 3 +-- .../composability/forwarder-interactor/Cargo.toml | 3 +-- .../feature-tests/composability/forwarder-legacy/Cargo.toml | 3 +-- contracts/feature-tests/composability/forwarder-raw/Cargo.toml | 3 +-- contracts/feature-tests/composability/forwarder/Cargo.toml | 3 +-- .../feature-tests/composability/local-esdt-and-nft/Cargo.toml | 3 +-- .../feature-tests/composability/mesh-interactor/Cargo.toml | 3 +-- contracts/feature-tests/composability/mesh-node/Cargo.toml | 3 +-- .../feature-tests/composability/proxy-test-first/Cargo.toml | 3 +-- .../feature-tests/composability/proxy-test-second/Cargo.toml | 3 +-- .../feature-tests/composability/recursive-caller/Cargo.toml | 3 +-- .../composability/transfer-role-features/Cargo.toml | 3 +-- contracts/feature-tests/composability/vault/Cargo.toml | 3 +-- .../erc-style-contracts/crowdfunding-erc20/Cargo.toml | 3 +-- .../erc-style-contracts/erc1155-marketplace/Cargo.toml | 3 +-- .../erc-style-contracts/erc1155-user-mock/Cargo.toml | 3 +-- contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml | 3 +-- contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml | 3 +-- contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml | 3 +-- .../feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml | 3 +-- contracts/feature-tests/esdt-system-sc-mock/Cargo.toml | 3 +-- contracts/feature-tests/exchange-features/Cargo.toml | 3 +-- contracts/feature-tests/formatted-message-features/Cargo.toml | 3 +-- contracts/feature-tests/gas-tests/Cargo.toml | 3 +-- contracts/feature-tests/multi-contract-features/Cargo.toml | 3 +-- contracts/feature-tests/panic-message-features/Cargo.toml | 3 +-- contracts/feature-tests/payable-features/Cargo.toml | 3 +-- .../feature-tests/rust-snippets-generator-test/Cargo.toml | 3 +-- .../feature-tests/rust-testing-framework-tester/Cargo.toml | 3 +-- contracts/feature-tests/scenario-tester/Cargo.toml | 3 +-- contracts/feature-tests/std-contract/Cargo.toml | 3 +-- contracts/feature-tests/use-module/Cargo.toml | 3 +-- contracts/modules/Cargo.toml | 3 +-- tools/managed-mem-bench/Cargo.toml | 3 +-- tools/mxpy-snippet-generator/Cargo.toml | 3 +-- tools/rust-debugger/format-tests/Cargo.toml | 3 +-- 96 files changed, 96 insertions(+), 192 deletions(-) diff --git a/contracts/benchmarks/large-storage/Cargo.toml b/contracts/benchmarks/large-storage/Cargo.toml index a8b3e2c826..33e643c044 100644 --- a/contracts/benchmarks/large-storage/Cargo.toml +++ b/contracts/benchmarks/large-storage/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/large_storage.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/benchmark-common/Cargo.toml b/contracts/benchmarks/mappers/benchmark-common/Cargo.toml index f7fa205a89..ed27adff7e 100644 --- a/contracts/benchmarks/mappers/benchmark-common/Cargo.toml +++ b/contracts/benchmarks/mappers/benchmark-common/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml b/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml index c942e012c2..35c1837d32 100644 --- a/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml @@ -13,8 +13,7 @@ path = "../benchmark-common" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/map-repeat/Cargo.toml b/contracts/benchmarks/mappers/map-repeat/Cargo.toml index 3390127a43..9c2cbd23fd 100644 --- a/contracts/benchmarks/mappers/map-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/map-repeat/Cargo.toml @@ -13,8 +13,7 @@ path = "../benchmark-common" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/queue-repeat/Cargo.toml b/contracts/benchmarks/mappers/queue-repeat/Cargo.toml index 5a4d7dd029..8debb6cc7a 100644 --- a/contracts/benchmarks/mappers/queue-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/queue-repeat/Cargo.toml @@ -13,8 +13,7 @@ path = "../benchmark-common" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/set-repeat/Cargo.toml b/contracts/benchmarks/mappers/set-repeat/Cargo.toml index 0835ae58e4..9fc9061112 100644 --- a/contracts/benchmarks/mappers/set-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/set-repeat/Cargo.toml @@ -13,8 +13,7 @@ path = "../benchmark-common" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml b/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml index 03302cc8bb..e85caa44da 100644 --- a/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml @@ -13,8 +13,7 @@ path = "../benchmark-common" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/mappers/vec-repeat/Cargo.toml b/contracts/benchmarks/mappers/vec-repeat/Cargo.toml index 7eea00f008..a311243450 100644 --- a/contracts/benchmarks/mappers/vec-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/vec-repeat/Cargo.toml @@ -13,8 +13,7 @@ path = "../benchmark-common" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/send-tx-repeat/Cargo.toml b/contracts/benchmarks/send-tx-repeat/Cargo.toml index feaab832e5..f6684f75d0 100644 --- a/contracts/benchmarks/send-tx-repeat/Cargo.toml +++ b/contracts/benchmarks/send-tx-repeat/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/send_tx_repeat.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/benchmarks/str-repeat/Cargo.toml b/contracts/benchmarks/str-repeat/Cargo.toml index 5b7f8b8767..c09bba9e04 100644 --- a/contracts/benchmarks/str-repeat/Cargo.toml +++ b/contracts/benchmarks/str-repeat/Cargo.toml @@ -12,8 +12,7 @@ path = "src/str_repeat.rs" managed-buffer-builder-cached = ["multiversx-sc/managed-buffer-builder-cached"] [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/benchmarks/very-large-storage/Cargo.toml b/contracts/benchmarks/very-large-storage/Cargo.toml index cc09504691..14f5e1ee4b 100644 --- a/contracts/benchmarks/very-large-storage/Cargo.toml +++ b/contracts/benchmarks/very-large-storage/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/very_large_storage.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/core/price-aggregator/Cargo.toml b/contracts/core/price-aggregator/Cargo.toml index 527875be58..8098e6e01c 100644 --- a/contracts/core/price-aggregator/Cargo.toml +++ b/contracts/core/price-aggregator/Cargo.toml @@ -19,8 +19,7 @@ edition = "2024" path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/core/price-aggregator/meta/Cargo.toml b/contracts/core/price-aggregator/meta/Cargo.toml index c2d1de3082..9abf3cfd7d 100644 --- a/contracts/core/price-aggregator/meta/Cargo.toml +++ b/contracts/core/price-aggregator/meta/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = ".." [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dependencies.multiversx-sc-meta-lib] version = "0.66.2" diff --git a/contracts/core/wegld-swap/Cargo.toml b/contracts/core/wegld-swap/Cargo.toml index c1b7727430..914166bff4 100644 --- a/contracts/core/wegld-swap/Cargo.toml +++ b/contracts/core/wegld-swap/Cargo.toml @@ -20,8 +20,7 @@ edition = "2024" path = "src/wegld.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/core/wegld-swap/meta/Cargo.toml b/contracts/core/wegld-swap/meta/Cargo.toml index baa6623eb4..5ac4ae564f 100644 --- a/contracts/core/wegld-swap/meta/Cargo.toml +++ b/contracts/core/wegld-swap/meta/Cargo.toml @@ -11,8 +11,7 @@ publish = false path = ".." [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dependencies.multiversx-sc-meta-lib] version = "0.66.2" diff --git a/contracts/examples/bonding-curve-contract/Cargo.toml b/contracts/examples/bonding-curve-contract/Cargo.toml index a7a06f3628..99118a56e8 100644 --- a/contracts/examples/bonding-curve-contract/Cargo.toml +++ b/contracts/examples/bonding-curve-contract/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/bonding_curve_contract.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/check-pause/Cargo.toml b/contracts/examples/check-pause/Cargo.toml index 686a0437ad..e7825463d8 100644 --- a/contracts/examples/check-pause/Cargo.toml +++ b/contracts/examples/check-pause/Cargo.toml @@ -12,8 +12,7 @@ path = "src/check_pause.rs" num-bigint = "0.4" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/crowdfunding/Cargo.toml b/contracts/examples/crowdfunding/Cargo.toml index 370771e2f6..fd79cdbb40 100644 --- a/contracts/examples/crowdfunding/Cargo.toml +++ b/contracts/examples/crowdfunding/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/crowdfunding.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/crypto-bubbles/Cargo.toml b/contracts/examples/crypto-bubbles/Cargo.toml index b904a147d3..e2579ce2fb 100644 --- a/contracts/examples/crypto-bubbles/Cargo.toml +++ b/contracts/examples/crypto-bubbles/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/crypto_bubbles.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/crypto-kitties/common/kitty/Cargo.toml b/contracts/examples/crypto-kitties/common/kitty/Cargo.toml index 81c110a70e..b80b374c90 100644 --- a/contracts/examples/crypto-kitties/common/kitty/Cargo.toml +++ b/contracts/examples/crypto-kitties/common/kitty/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true [dependencies.random] version = "0.0.0" diff --git a/contracts/examples/crypto-kitties/common/random/Cargo.toml b/contracts/examples/crypto-kitties/common/random/Cargo.toml index ec6d5cb4ab..d0c11e25f3 100644 --- a/contracts/examples/crypto-kitties/common/random/Cargo.toml +++ b/contracts/examples/crypto-kitties/common/random/Cargo.toml @@ -8,5 +8,4 @@ edition = "2024" path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml b/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml index 800b421835..2183e18dee 100644 --- a/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml @@ -17,8 +17,7 @@ version = "0.0.0" path = "../kitty-ownership" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml b/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml index a71cc91b77..4064c46ce1 100644 --- a/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml @@ -18,8 +18,7 @@ version = "0.0.0" path = "../common/random" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml b/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml index 3ef0c62290..aad15de3b0 100644 --- a/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml @@ -21,8 +21,7 @@ version = "0.0.0" path = "../kitty-genetic-alg" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/crypto-zombies/Cargo.toml b/contracts/examples/crypto-zombies/Cargo.toml index 4043e85c10..4c88724653 100644 --- a/contracts/examples/crypto-zombies/Cargo.toml +++ b/contracts/examples/crypto-zombies/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/digital-cash/Cargo.toml b/contracts/examples/digital-cash/Cargo.toml index 9227a06488..24c322d058 100644 --- a/contracts/examples/digital-cash/Cargo.toml +++ b/contracts/examples/digital-cash/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/digital_cash.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/empty/Cargo.toml b/contracts/examples/empty/Cargo.toml index c4c32c07a2..3480aea972 100644 --- a/contracts/examples/empty/Cargo.toml +++ b/contracts/examples/empty/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/empty.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/esdt-transfer-with-fee/Cargo.toml b/contracts/examples/esdt-transfer-with-fee/Cargo.toml index 04601f1211..073c6d6156 100644 --- a/contracts/examples/esdt-transfer-with-fee/Cargo.toml +++ b/contracts/examples/esdt-transfer-with-fee/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/esdt_transfer_with_fee.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/factorial/Cargo.toml b/contracts/examples/factorial/Cargo.toml index 572bd8015b..58d64af905 100644 --- a/contracts/examples/factorial/Cargo.toml +++ b/contracts/examples/factorial/Cargo.toml @@ -12,8 +12,7 @@ path = "src/factorial.rs" compiled-sc-tests = ["multiversx-sc-scenario/compiled-sc-tests"] [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/fractional-nfts/Cargo.toml b/contracts/examples/fractional-nfts/Cargo.toml index 494a76cc9a..ab9d2e5045 100644 --- a/contracts/examples/fractional-nfts/Cargo.toml +++ b/contracts/examples/fractional-nfts/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/fractional_nfts.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/lottery/Cargo.toml b/contracts/examples/lottery/Cargo.toml index cd3d42a65c..0b3e81ca4e 100644 --- a/contracts/examples/lottery/Cargo.toml +++ b/contracts/examples/lottery/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lottery.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/multisig/Cargo.toml b/contracts/examples/multisig/Cargo.toml index f775495e8c..27b0065b4c 100644 --- a/contracts/examples/multisig/Cargo.toml +++ b/contracts/examples/multisig/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/multisig.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/multisig/interact/Cargo.toml b/contracts/examples/multisig/interact/Cargo.toml index 24aabcf9fe..fb454307d2 100644 --- a/contracts/examples/multisig/interact/Cargo.toml +++ b/contracts/examples/multisig/interact/Cargo.toml @@ -31,5 +31,4 @@ version = "=0.66.2" path = "../../../../framework/scenario" [dependencies.multiversx-sc] -version = "=0.66.2" -path = "../../../../framework/base" +workspace = true diff --git a/contracts/examples/nft-minter/Cargo.toml b/contracts/examples/nft-minter/Cargo.toml index 5d85959d47..167a1cd622 100644 --- a/contracts/examples/nft-minter/Cargo.toml +++ b/contracts/examples/nft-minter/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/nft-storage-prepay/Cargo.toml b/contracts/examples/nft-storage-prepay/Cargo.toml index 60dfea0c12..5fe8ec71e6 100644 --- a/contracts/examples/nft-storage-prepay/Cargo.toml +++ b/contracts/examples/nft-storage-prepay/Cargo.toml @@ -10,8 +10,7 @@ path = "src/nft_storage_prepay.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/nft-subscription/Cargo.toml b/contracts/examples/nft-subscription/Cargo.toml index 86cd1a36c2..b74f985193 100644 --- a/contracts/examples/nft-subscription/Cargo.toml +++ b/contracts/examples/nft-subscription/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/order-book/factory/Cargo.toml b/contracts/examples/order-book/factory/Cargo.toml index 0ddde8253d..b633ab0aa0 100644 --- a/contracts/examples/order-book/factory/Cargo.toml +++ b/contracts/examples/order-book/factory/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/order-book/pair/Cargo.toml b/contracts/examples/order-book/pair/Cargo.toml index ae9835c009..de1accfc7e 100644 --- a/contracts/examples/order-book/pair/Cargo.toml +++ b/contracts/examples/order-book/pair/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/ping-pong-egld/Cargo.toml b/contracts/examples/ping-pong-egld/Cargo.toml index c79390af74..4a99a2c847 100644 --- a/contracts/examples/ping-pong-egld/Cargo.toml +++ b/contracts/examples/ping-pong-egld/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/ping_pong.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/proxy-pause/Cargo.toml b/contracts/examples/proxy-pause/Cargo.toml index 33e68c7412..5ff7937ba1 100644 --- a/contracts/examples/proxy-pause/Cargo.toml +++ b/contracts/examples/proxy-pause/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/proxy_pause.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/examples/rewards-distribution/Cargo.toml b/contracts/examples/rewards-distribution/Cargo.toml index 6e270e92ed..ea43a93bff 100644 --- a/contracts/examples/rewards-distribution/Cargo.toml +++ b/contracts/examples/rewards-distribution/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/rewards_distribution.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/seed-nft-minter/Cargo.toml b/contracts/examples/seed-nft-minter/Cargo.toml index c322bac2f9..22c028d482 100644 --- a/contracts/examples/seed-nft-minter/Cargo.toml +++ b/contracts/examples/seed-nft-minter/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/seed_nft_minter.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/examples/token-release/Cargo.toml b/contracts/examples/token-release/Cargo.toml index af5d693e25..c981665d3a 100644 --- a/contracts/examples/token-release/Cargo.toml +++ b/contracts/examples/token-release/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/token_release.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/abi-tester/Cargo.toml b/contracts/feature-tests/abi-tester/Cargo.toml index 26d5cbe237..2a6fb1e6f5 100644 --- a/contracts/feature-tests/abi-tester/Cargo.toml +++ b/contracts/feature-tests/abi-tester/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/abi_tester.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/alloc-features/Cargo.toml b/contracts/feature-tests/alloc-features/Cargo.toml index 62664d39e5..c09a352eaf 100644 --- a/contracts/feature-tests/alloc-features/Cargo.toml +++ b/contracts/feature-tests/alloc-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/alloc_features_main.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/basic-features/Cargo.toml b/contracts/feature-tests/basic-features/Cargo.toml index 8aab7488fd..3885cbddaa 100644 --- a/contracts/feature-tests/basic-features/Cargo.toml +++ b/contracts/feature-tests/basic-features/Cargo.toml @@ -12,8 +12,7 @@ path = "src/basic_features_main.rs" small-int-bug = ["multiversx-sc/small-int-bug"] [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true # BLS doesn't currently work on Windows [target.'cfg(not(target_os = "windows"))'.dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/big-float-features/Cargo.toml b/contracts/feature-tests/big-float-features/Cargo.toml index 269cb2abab..a7b73c5f98 100644 --- a/contracts/feature-tests/big-float-features/Cargo.toml +++ b/contracts/feature-tests/big-float-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/big_float_main.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/Cargo.toml b/contracts/feature-tests/composability/Cargo.toml index 2cc3620fe7..62a6db18ce 100644 --- a/contracts/feature-tests/composability/Cargo.toml +++ b/contracts/feature-tests/composability/Cargo.toml @@ -36,8 +36,7 @@ path = "recursive-caller" path = "vault" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/builtin-func-features/Cargo.toml b/contracts/feature-tests/composability/builtin-func-features/Cargo.toml index 782f86af78..c6a8eb3b61 100644 --- a/contracts/feature-tests/composability/builtin-func-features/Cargo.toml +++ b/contracts/feature-tests/composability/builtin-func-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/builtin_func_features.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml index ad2c2d46a4..9203be4dc7 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml @@ -12,8 +12,7 @@ path = "first-contract" path = "second-contract" [dev-dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml index 447f0fe8d0..240c3c5136 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml index d20082620f..769e3032e0 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = ".." [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../../framework/base" +workspace = true [dependencies.multiversx-sc-meta-lib] version = "0.66.2" diff --git a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml index 79c2437700..7bc2566d9a 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml @@ -10,8 +10,7 @@ path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml index ddba693202..a1dede2106 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = ".." [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../../framework/base" +workspace = true [dependencies.multiversx-sc-meta-lib] version = "0.66.2" diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml index 0a24a4fbcc..c0acf85e0a 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml @@ -16,8 +16,7 @@ path = "parent" path = "child" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml index 6c29d6e973..bbdcba733d 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml @@ -10,8 +10,7 @@ path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml index 4d8734d2b4..66e877788f 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = ".." [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../../framework/base" +workspace = true [dependencies.multiversx-sc-meta-lib] version = "0.66.2" diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml index a395f696ad..0d9f80555c 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml index 4066682ce8..6331b8b439 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = ".." [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../../framework/base" +workspace = true [dependencies.multiversx-sc-meta-lib] version = "0.66.2" diff --git a/contracts/feature-tests/composability/forwarder-blind/Cargo.toml b/contracts/feature-tests/composability/forwarder-blind/Cargo.toml index 773a5aaae2..b3d68e1dfa 100644 --- a/contracts/feature-tests/composability/forwarder-blind/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-blind/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/forwarder_blind.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml index e05a59f9ab..0936864bd7 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml @@ -20,8 +20,7 @@ version = "0.66.2" path = "../../../../../framework/snippets" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../../framework/base" +workspace = true [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml b/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml index 9e4c14c052..15d0860a57 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml @@ -20,8 +20,7 @@ version = "0.66.2" path = "../../../../framework/snippets" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml b/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml index ad6bc4d1e9..3ab26f3722 100644 --- a/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml @@ -12,8 +12,7 @@ path = "src/forwarder_legacy_main.rs" path = "../vault" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/forwarder-raw/Cargo.toml b/contracts/feature-tests/composability/forwarder-raw/Cargo.toml index 03fe6a1b32..46e274813b 100644 --- a/contracts/feature-tests/composability/forwarder-raw/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-raw/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/forwarder_raw.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/forwarder/Cargo.toml b/contracts/feature-tests/composability/forwarder/Cargo.toml index f8d05b2064..04172f9dbe 100644 --- a/contracts/feature-tests/composability/forwarder/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/forwarder_main.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml b/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml index b9841fb441..fd5741dd0c 100644 --- a/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml +++ b/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml @@ -10,8 +10,7 @@ path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/mesh-interactor/Cargo.toml b/contracts/feature-tests/composability/mesh-interactor/Cargo.toml index 7e27d28e43..9d4d6d55fe 100644 --- a/contracts/feature-tests/composability/mesh-interactor/Cargo.toml +++ b/contracts/feature-tests/composability/mesh-interactor/Cargo.toml @@ -16,8 +16,7 @@ tokio = { version = "1.24" } toml = "1.0" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dependencies.multiversx-sc-snippets] version = "0.66.2" diff --git a/contracts/feature-tests/composability/mesh-node/Cargo.toml b/contracts/feature-tests/composability/mesh-node/Cargo.toml index b7fd07792f..c67766dd78 100644 --- a/contracts/feature-tests/composability/mesh-node/Cargo.toml +++ b/contracts/feature-tests/composability/mesh-node/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/mesh_node.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dependencies.multiversx-sc-wasm-adapter] version = "0.66.2" diff --git a/contracts/feature-tests/composability/proxy-test-first/Cargo.toml b/contracts/feature-tests/composability/proxy-test-first/Cargo.toml index 8888d830d1..f56cdbdecd 100644 --- a/contracts/feature-tests/composability/proxy-test-first/Cargo.toml +++ b/contracts/feature-tests/composability/proxy-test-first/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/proxy-test-first.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/composability/proxy-test-second/Cargo.toml b/contracts/feature-tests/composability/proxy-test-second/Cargo.toml index d9e2c9fcf7..a463760c62 100644 --- a/contracts/feature-tests/composability/proxy-test-second/Cargo.toml +++ b/contracts/feature-tests/composability/proxy-test-second/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/proxy-test-second.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/composability/recursive-caller/Cargo.toml b/contracts/feature-tests/composability/recursive-caller/Cargo.toml index 9ce3bee4f4..b6ebb5d4c5 100644 --- a/contracts/feature-tests/composability/recursive-caller/Cargo.toml +++ b/contracts/feature-tests/composability/recursive-caller/Cargo.toml @@ -12,8 +12,7 @@ path = "src/recursive_caller.rs" path = "../vault" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/composability/transfer-role-features/Cargo.toml b/contracts/feature-tests/composability/transfer-role-features/Cargo.toml index 698a158586..4fa86c1aaa 100644 --- a/contracts/feature-tests/composability/transfer-role-features/Cargo.toml +++ b/contracts/feature-tests/composability/transfer-role-features/Cargo.toml @@ -12,8 +12,7 @@ path = "src/lib.rs" path = "../vault" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/feature-tests/composability/vault/Cargo.toml b/contracts/feature-tests/composability/vault/Cargo.toml index 3717d6a65c..b8b48b9a42 100644 --- a/contracts/feature-tests/composability/vault/Cargo.toml +++ b/contracts/feature-tests/composability/vault/Cargo.toml @@ -10,8 +10,7 @@ path = "src/vault.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml b/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml index 90080df945..0e94ac97af 100644 --- a/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml @@ -12,8 +12,7 @@ path = "src/crowdfunding_erc20.rs" path = "../erc20" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml index 5fbfc8255d..3d95f9e0aa 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml @@ -13,8 +13,7 @@ path = "src/lib.rs" path = "../erc1155" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml index 3c25dda777..a1f79232ea 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml index 5c357d14d6..f815b5269d 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/erc1155.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml index d79ebf7f49..7ad466888e 100644 --- a/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/erc20.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml index fafe2fd5a1..a6c1155365 100644 --- a/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml @@ -10,8 +10,7 @@ path = "src/erc721.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml b/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml index b830ff831c..2241d396f9 100644 --- a/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml @@ -12,8 +12,7 @@ path = "src/lottery.rs" path = "../erc20" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml b/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml index 88c5357b79..8e4c56864d 100644 --- a/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml +++ b/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/esdt_system_sc_mock.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/exchange-features/Cargo.toml b/contracts/feature-tests/exchange-features/Cargo.toml index 048848856d..d16e15d0ea 100644 --- a/contracts/feature-tests/exchange-features/Cargo.toml +++ b/contracts/feature-tests/exchange-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/exchange_features.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/formatted-message-features/Cargo.toml b/contracts/feature-tests/formatted-message-features/Cargo.toml index ec60e5c62a..3c6284652f 100644 --- a/contracts/feature-tests/formatted-message-features/Cargo.toml +++ b/contracts/feature-tests/formatted-message-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/formatted_message_features.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] diff --git a/contracts/feature-tests/gas-tests/Cargo.toml b/contracts/feature-tests/gas-tests/Cargo.toml index d3aaa95e7a..13909fcabd 100644 --- a/contracts/feature-tests/gas-tests/Cargo.toml +++ b/contracts/feature-tests/gas-tests/Cargo.toml @@ -8,8 +8,7 @@ publish = false path = "src/gas_tests.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/multi-contract-features/Cargo.toml b/contracts/feature-tests/multi-contract-features/Cargo.toml index 2e58d2f4a8..505847ec22 100644 --- a/contracts/feature-tests/multi-contract-features/Cargo.toml +++ b/contracts/feature-tests/multi-contract-features/Cargo.toml @@ -12,8 +12,7 @@ path = "src/multi_contract_features.rs" example_feature = [] [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/panic-message-features/Cargo.toml b/contracts/feature-tests/panic-message-features/Cargo.toml index 9cb262b522..f41299884c 100644 --- a/contracts/feature-tests/panic-message-features/Cargo.toml +++ b/contracts/feature-tests/panic-message-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/panic_features.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/payable-features/Cargo.toml b/contracts/feature-tests/payable-features/Cargo.toml index f66144b861..bf2c82a1ea 100644 --- a/contracts/feature-tests/payable-features/Cargo.toml +++ b/contracts/feature-tests/payable-features/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/payable_features.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml b/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml index 5854c501e1..1417322ebe 100644 --- a/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml +++ b/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml b/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml index 29f2b88b2d..f03fa5bb41 100644 --- a/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml +++ b/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml @@ -6,8 +6,7 @@ edition = "2024" publish = false [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true features = [ "alloc" ] [dev-dependencies.adder] diff --git a/contracts/feature-tests/scenario-tester/Cargo.toml b/contracts/feature-tests/scenario-tester/Cargo.toml index 7b84dbf8c1..d127b533f7 100644 --- a/contracts/feature-tests/scenario-tester/Cargo.toml +++ b/contracts/feature-tests/scenario-tester/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/std-contract/Cargo.toml b/contracts/feature-tests/std-contract/Cargo.toml index 2606f80efa..3991fe7175 100644 --- a/contracts/feature-tests/std-contract/Cargo.toml +++ b/contracts/feature-tests/std-contract/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/std_contract.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/feature-tests/use-module/Cargo.toml b/contracts/feature-tests/use-module/Cargo.toml index 9b7ae5999f..e7852c5985 100644 --- a/contracts/feature-tests/use-module/Cargo.toml +++ b/contracts/feature-tests/use-module/Cargo.toml @@ -13,8 +13,7 @@ version = "0.66.2" path = "../../../contracts/modules" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../../framework/base" +workspace = true [dev-dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/contracts/modules/Cargo.toml b/contracts/modules/Cargo.toml index bef70ee1dd..a89933a0f8 100644 --- a/contracts/modules/Cargo.toml +++ b/contracts/modules/Cargo.toml @@ -17,5 +17,4 @@ categories = ["no-std", "wasm", "cryptography::cryptocurrencies"] alloc = ["multiversx-sc/alloc"] [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../framework/base" +workspace = true diff --git a/tools/managed-mem-bench/Cargo.toml b/tools/managed-mem-bench/Cargo.toml index fde20c6b11..6cd8e586db 100644 --- a/tools/managed-mem-bench/Cargo.toml +++ b/tools/managed-mem-bench/Cargo.toml @@ -13,8 +13,7 @@ name = "bench-threading" path = "src/bench_threading.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../framework/base" +workspace = true [dependencies.multiversx-sc-scenario] version = "0.66.2" diff --git a/tools/mxpy-snippet-generator/Cargo.toml b/tools/mxpy-snippet-generator/Cargo.toml index 6d6ca7d502..de8c05482a 100644 --- a/tools/mxpy-snippet-generator/Cargo.toml +++ b/tools/mxpy-snippet-generator/Cargo.toml @@ -10,8 +10,7 @@ name = "mxpy-snippet-generator" path = "src/mxpy_snippet_generator.rs" [dependencies.multiversx-sc] -version = "0.66.2" -path = "../../framework/base" +workspace = true [dependencies] bech32 = "0.11" diff --git a/tools/rust-debugger/format-tests/Cargo.toml b/tools/rust-debugger/format-tests/Cargo.toml index 1b3b1c9b8b..26d8582abf 100644 --- a/tools/rust-debugger/format-tests/Cargo.toml +++ b/tools/rust-debugger/format-tests/Cargo.toml @@ -9,8 +9,7 @@ name = "format-tests" path = "src/format_tests.rs" [dependencies.multiversx-sc] -version = "=0.66.2" -path = "../../../framework/base" +workspace = true [dependencies.multiversx-sc-scenario] version = "=0.66.2" From 7ce6f0241b89eace24a2a4e0db81d4678893210b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 13:22:17 +0300 Subject: [PATCH 120/195] workspace deps - meta-lib --- contracts/benchmarks/large-storage/meta/Cargo.toml | 4 +--- .../benchmarks/mappers/linked-list-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/mappers/map-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/mappers/queue-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/mappers/set-repeat/meta/Cargo.toml | 4 +--- .../benchmarks/mappers/single-value-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/mappers/vec-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/send-tx-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/str-repeat/meta/Cargo.toml | 4 +--- contracts/benchmarks/very-large-storage/meta/Cargo.toml | 4 +--- contracts/core/price-aggregator/meta/Cargo.toml | 4 +--- contracts/core/wegld-swap/meta/Cargo.toml | 4 +--- contracts/examples/bonding-curve-contract/meta/Cargo.toml | 4 +--- contracts/examples/check-pause/meta/Cargo.toml | 4 +--- contracts/examples/crowdfunding/meta/Cargo.toml | 4 +--- contracts/examples/crypto-bubbles/meta/Cargo.toml | 4 +--- .../examples/crypto-kitties/kitty-auction/meta/Cargo.toml | 4 +--- .../examples/crypto-kitties/kitty-genetic-alg/meta/Cargo.toml | 4 +--- .../examples/crypto-kitties/kitty-ownership/meta/Cargo.toml | 4 +--- contracts/examples/crypto-zombies/meta/Cargo.toml | 4 +--- contracts/examples/digital-cash/meta/Cargo.toml | 4 +--- contracts/examples/empty/meta/Cargo.toml | 4 +--- contracts/examples/esdt-transfer-with-fee/meta/Cargo.toml | 4 +--- contracts/examples/factorial/meta/Cargo.toml | 4 +--- contracts/examples/fractional-nfts/meta/Cargo.toml | 4 +--- contracts/examples/lottery/meta/Cargo.toml | 4 +--- contracts/examples/multisig/meta/Cargo.toml | 4 +--- contracts/examples/nft-minter/meta/Cargo.toml | 4 +--- contracts/examples/nft-storage-prepay/meta/Cargo.toml | 4 +--- contracts/examples/nft-subscription/meta/Cargo.toml | 4 +--- contracts/examples/order-book/factory/meta/Cargo.toml | 4 +--- contracts/examples/order-book/pair/meta/Cargo.toml | 4 +--- contracts/examples/ping-pong-egld/meta/Cargo.toml | 4 +--- contracts/examples/proxy-pause/meta/Cargo.toml | 4 +--- contracts/examples/rewards-distribution/meta/Cargo.toml | 4 +--- contracts/examples/seed-nft-minter/meta/Cargo.toml | 4 +--- contracts/examples/token-release/meta/Cargo.toml | 4 +--- contracts/feature-tests/abi-tester/Cargo.toml | 3 +-- contracts/feature-tests/abi-tester/meta/Cargo.toml | 4 +--- contracts/feature-tests/alloc-features/meta/Cargo.toml | 4 +--- contracts/feature-tests/basic-features/meta/Cargo.toml | 4 +--- contracts/feature-tests/big-float-features/meta/Cargo.toml | 4 +--- .../composability/builtin-func-features/meta/Cargo.toml | 4 +--- .../esdt-contract-pair/first-contract/meta/Cargo.toml | 4 +--- .../esdt-contract-pair/second-contract/meta/Cargo.toml | 4 +--- .../execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml | 4 +--- .../parent/meta/Cargo.toml | 4 +--- .../composability/forwarder-blind/meta/Cargo.toml | 4 +--- .../composability/forwarder-legacy/meta/Cargo.toml | 4 +--- .../feature-tests/composability/forwarder-raw/meta/Cargo.toml | 4 +--- .../feature-tests/composability/forwarder/meta/Cargo.toml | 4 +--- .../composability/local-esdt-and-nft/meta/Cargo.toml | 4 +--- .../feature-tests/composability/mesh-node/meta/Cargo.toml | 4 +--- .../composability/proxy-test-first/meta/Cargo.toml | 4 +--- .../composability/proxy-test-second/meta/Cargo.toml | 4 +--- .../composability/recursive-caller/meta/Cargo.toml | 4 +--- .../composability/transfer-role-features/meta/Cargo.toml | 4 +--- contracts/feature-tests/composability/vault/meta/Cargo.toml | 4 +--- .../erc-style-contracts/crowdfunding-erc20/meta/Cargo.toml | 4 +--- .../erc-style-contracts/erc1155-marketplace/meta/Cargo.toml | 4 +--- .../erc-style-contracts/erc1155-user-mock/meta/Cargo.toml | 4 +--- .../feature-tests/erc-style-contracts/erc1155/meta/Cargo.toml | 4 +--- .../feature-tests/erc-style-contracts/erc20/meta/Cargo.toml | 4 +--- .../feature-tests/erc-style-contracts/erc721/meta/Cargo.toml | 4 +--- .../erc-style-contracts/lottery-erc20/meta/Cargo.toml | 4 +--- contracts/feature-tests/esdt-system-sc-mock/meta/Cargo.toml | 4 +--- contracts/feature-tests/exchange-features/meta/Cargo.toml | 4 +--- .../feature-tests/formatted-message-features/meta/Cargo.toml | 4 +--- .../feature-tests/multi-contract-features/meta/Cargo.toml | 4 +--- .../feature-tests/panic-message-features/meta/Cargo.toml | 4 +--- contracts/feature-tests/payable-features/meta/Cargo.toml | 4 +--- .../rust-snippets-generator-test/meta/Cargo.toml | 4 +--- .../rust-testing-framework-tester/meta/Cargo.toml | 4 +--- contracts/feature-tests/scenario-tester/meta/Cargo.toml | 4 +--- contracts/feature-tests/std-contract/meta/Cargo.toml | 4 +--- contracts/feature-tests/use-module/Cargo.toml | 3 +-- contracts/feature-tests/use-module/meta/Cargo.toml | 4 +--- tools/cs-test-runner/Cargo.toml | 3 +-- tools/wat-gen/Cargo.toml | 3 +-- 79 files changed, 79 insertions(+), 233 deletions(-) diff --git a/contracts/benchmarks/large-storage/meta/Cargo.toml b/contracts/benchmarks/large-storage/meta/Cargo.toml index 9cadd39896..443294c1a3 100644 --- a/contracts/benchmarks/large-storage/meta/Cargo.toml +++ b/contracts/benchmarks/large-storage/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/mappers/linked-list-repeat/meta/Cargo.toml b/contracts/benchmarks/mappers/linked-list-repeat/meta/Cargo.toml index 3f4d333edc..8e86dafb85 100644 --- a/contracts/benchmarks/mappers/linked-list-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/mappers/linked-list-repeat/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/mappers/map-repeat/meta/Cargo.toml b/contracts/benchmarks/mappers/map-repeat/meta/Cargo.toml index 83a5437593..4ca3374861 100644 --- a/contracts/benchmarks/mappers/map-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/mappers/map-repeat/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/mappers/queue-repeat/meta/Cargo.toml b/contracts/benchmarks/mappers/queue-repeat/meta/Cargo.toml index 14e79e81d7..1c072b24ee 100644 --- a/contracts/benchmarks/mappers/queue-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/mappers/queue-repeat/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/mappers/set-repeat/meta/Cargo.toml b/contracts/benchmarks/mappers/set-repeat/meta/Cargo.toml index 53489a69e3..f92eff3344 100644 --- a/contracts/benchmarks/mappers/set-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/mappers/set-repeat/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/mappers/single-value-repeat/meta/Cargo.toml b/contracts/benchmarks/mappers/single-value-repeat/meta/Cargo.toml index 56b1fe93ca..25748b8837 100644 --- a/contracts/benchmarks/mappers/single-value-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/mappers/single-value-repeat/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/mappers/vec-repeat/meta/Cargo.toml b/contracts/benchmarks/mappers/vec-repeat/meta/Cargo.toml index 50dc6a532b..359a287d07 100644 --- a/contracts/benchmarks/mappers/vec-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/mappers/vec-repeat/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/send-tx-repeat/meta/Cargo.toml b/contracts/benchmarks/send-tx-repeat/meta/Cargo.toml index cfa1286282..ea0989eb4f 100644 --- a/contracts/benchmarks/send-tx-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/send-tx-repeat/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/str-repeat/meta/Cargo.toml b/contracts/benchmarks/str-repeat/meta/Cargo.toml index 74896ce7d1..9a8ce52c5c 100644 --- a/contracts/benchmarks/str-repeat/meta/Cargo.toml +++ b/contracts/benchmarks/str-repeat/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/benchmarks/very-large-storage/meta/Cargo.toml b/contracts/benchmarks/very-large-storage/meta/Cargo.toml index ce75db5bc8..c070eb20f9 100644 --- a/contracts/benchmarks/very-large-storage/meta/Cargo.toml +++ b/contracts/benchmarks/very-large-storage/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/core/price-aggregator/meta/Cargo.toml b/contracts/core/price-aggregator/meta/Cargo.toml index 9abf3cfd7d..df6f218765 100644 --- a/contracts/core/price-aggregator/meta/Cargo.toml +++ b/contracts/core/price-aggregator/meta/Cargo.toml @@ -11,6 +11,4 @@ path = ".." workspace = true [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/core/wegld-swap/meta/Cargo.toml b/contracts/core/wegld-swap/meta/Cargo.toml index 5ac4ae564f..eeb60b10b6 100644 --- a/contracts/core/wegld-swap/meta/Cargo.toml +++ b/contracts/core/wegld-swap/meta/Cargo.toml @@ -14,6 +14,4 @@ path = ".." workspace = true [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/bonding-curve-contract/meta/Cargo.toml b/contracts/examples/bonding-curve-contract/meta/Cargo.toml index 986ca51fcd..ebe0051fdc 100644 --- a/contracts/examples/bonding-curve-contract/meta/Cargo.toml +++ b/contracts/examples/bonding-curve-contract/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/check-pause/meta/Cargo.toml b/contracts/examples/check-pause/meta/Cargo.toml index 9abae403c7..cd7ae907bc 100644 --- a/contracts/examples/check-pause/meta/Cargo.toml +++ b/contracts/examples/check-pause/meta/Cargo.toml @@ -9,6 +9,4 @@ authors = ["Alin Cruceat "] path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/crowdfunding/meta/Cargo.toml b/contracts/examples/crowdfunding/meta/Cargo.toml index 1e05116420..4e28badf87 100644 --- a/contracts/examples/crowdfunding/meta/Cargo.toml +++ b/contracts/examples/crowdfunding/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/crypto-bubbles/meta/Cargo.toml b/contracts/examples/crypto-bubbles/meta/Cargo.toml index 5201719d8a..99068deb16 100644 --- a/contracts/examples/crypto-bubbles/meta/Cargo.toml +++ b/contracts/examples/crypto-bubbles/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-auction/meta/Cargo.toml b/contracts/examples/crypto-kitties/kitty-auction/meta/Cargo.toml index e104da0f94..f1e63b0118 100644 --- a/contracts/examples/crypto-kitties/kitty-auction/meta/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-auction/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-genetic-alg/meta/Cargo.toml b/contracts/examples/crypto-kitties/kitty-genetic-alg/meta/Cargo.toml index 64c2956ed7..16af2e0fc7 100644 --- a/contracts/examples/crypto-kitties/kitty-genetic-alg/meta/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-genetic-alg/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-ownership/meta/Cargo.toml b/contracts/examples/crypto-kitties/kitty-ownership/meta/Cargo.toml index 5dacdb944e..9bb2fa9d89 100644 --- a/contracts/examples/crypto-kitties/kitty-ownership/meta/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-ownership/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/crypto-zombies/meta/Cargo.toml b/contracts/examples/crypto-zombies/meta/Cargo.toml index a3657233fd..3ec61c5f30 100644 --- a/contracts/examples/crypto-zombies/meta/Cargo.toml +++ b/contracts/examples/crypto-zombies/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/digital-cash/meta/Cargo.toml b/contracts/examples/digital-cash/meta/Cargo.toml index a8b37fbaa2..2799c3a856 100644 --- a/contracts/examples/digital-cash/meta/Cargo.toml +++ b/contracts/examples/digital-cash/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/empty/meta/Cargo.toml b/contracts/examples/empty/meta/Cargo.toml index c575a2ac7d..b27955b7f9 100644 --- a/contracts/examples/empty/meta/Cargo.toml +++ b/contracts/examples/empty/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/esdt-transfer-with-fee/meta/Cargo.toml b/contracts/examples/esdt-transfer-with-fee/meta/Cargo.toml index bd776a294d..6b5449d22e 100644 --- a/contracts/examples/esdt-transfer-with-fee/meta/Cargo.toml +++ b/contracts/examples/esdt-transfer-with-fee/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/factorial/meta/Cargo.toml b/contracts/examples/factorial/meta/Cargo.toml index 22c9c691da..14c8e03c1b 100644 --- a/contracts/examples/factorial/meta/Cargo.toml +++ b/contracts/examples/factorial/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/fractional-nfts/meta/Cargo.toml b/contracts/examples/fractional-nfts/meta/Cargo.toml index ecfa38697e..aa23d5eb22 100644 --- a/contracts/examples/fractional-nfts/meta/Cargo.toml +++ b/contracts/examples/fractional-nfts/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/lottery/meta/Cargo.toml b/contracts/examples/lottery/meta/Cargo.toml index 7b9dd21956..a48e579ef4 100644 --- a/contracts/examples/lottery/meta/Cargo.toml +++ b/contracts/examples/lottery/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/multisig/meta/Cargo.toml b/contracts/examples/multisig/meta/Cargo.toml index 28b433de40..7ba0f0e268 100644 --- a/contracts/examples/multisig/meta/Cargo.toml +++ b/contracts/examples/multisig/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/nft-minter/meta/Cargo.toml b/contracts/examples/nft-minter/meta/Cargo.toml index 07a366e95a..01aaebeb8c 100644 --- a/contracts/examples/nft-minter/meta/Cargo.toml +++ b/contracts/examples/nft-minter/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/nft-storage-prepay/meta/Cargo.toml b/contracts/examples/nft-storage-prepay/meta/Cargo.toml index 076aee3c50..ecafb25157 100644 --- a/contracts/examples/nft-storage-prepay/meta/Cargo.toml +++ b/contracts/examples/nft-storage-prepay/meta/Cargo.toml @@ -11,6 +11,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/nft-subscription/meta/Cargo.toml b/contracts/examples/nft-subscription/meta/Cargo.toml index 99cc8a9505..603253d4f4 100644 --- a/contracts/examples/nft-subscription/meta/Cargo.toml +++ b/contracts/examples/nft-subscription/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/order-book/factory/meta/Cargo.toml b/contracts/examples/order-book/factory/meta/Cargo.toml index 000a91d3d9..0ff9635110 100644 --- a/contracts/examples/order-book/factory/meta/Cargo.toml +++ b/contracts/examples/order-book/factory/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/order-book/pair/meta/Cargo.toml b/contracts/examples/order-book/pair/meta/Cargo.toml index dd0c3db12d..b3fd6b3528 100644 --- a/contracts/examples/order-book/pair/meta/Cargo.toml +++ b/contracts/examples/order-book/pair/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/ping-pong-egld/meta/Cargo.toml b/contracts/examples/ping-pong-egld/meta/Cargo.toml index e30dc7b8ee..0c412d4f13 100644 --- a/contracts/examples/ping-pong-egld/meta/Cargo.toml +++ b/contracts/examples/ping-pong-egld/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/proxy-pause/meta/Cargo.toml b/contracts/examples/proxy-pause/meta/Cargo.toml index dbd2b04009..aa44f26b62 100644 --- a/contracts/examples/proxy-pause/meta/Cargo.toml +++ b/contracts/examples/proxy-pause/meta/Cargo.toml @@ -11,6 +11,4 @@ authors = ["you"] path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/rewards-distribution/meta/Cargo.toml b/contracts/examples/rewards-distribution/meta/Cargo.toml index 37d6200f38..c8947c6511 100644 --- a/contracts/examples/rewards-distribution/meta/Cargo.toml +++ b/contracts/examples/rewards-distribution/meta/Cargo.toml @@ -11,6 +11,4 @@ authors = ["Claudiu-Marcel Bruda "] path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/seed-nft-minter/meta/Cargo.toml b/contracts/examples/seed-nft-minter/meta/Cargo.toml index 155a5d67f1..497ceb5f9d 100644 --- a/contracts/examples/seed-nft-minter/meta/Cargo.toml +++ b/contracts/examples/seed-nft-minter/meta/Cargo.toml @@ -11,6 +11,4 @@ authors = ["Claudiu-Marcel Bruda "] path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/examples/token-release/meta/Cargo.toml b/contracts/examples/token-release/meta/Cargo.toml index 8ec07047b5..c00b671af4 100644 --- a/contracts/examples/token-release/meta/Cargo.toml +++ b/contracts/examples/token-release/meta/Cargo.toml @@ -10,6 +10,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/abi-tester/Cargo.toml b/contracts/feature-tests/abi-tester/Cargo.toml index 2a6fb1e6f5..44bde300eb 100644 --- a/contracts/feature-tests/abi-tester/Cargo.toml +++ b/contracts/feature-tests/abi-tester/Cargo.toml @@ -17,8 +17,7 @@ version = "0.66.2" path = "../../../framework/scenario" [dev-dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../framework/meta-lib" +workspace = true [dependencies] bitflags = "2.9" diff --git a/contracts/feature-tests/abi-tester/meta/Cargo.toml b/contracts/feature-tests/abi-tester/meta/Cargo.toml index d79ef1e8c8..b04b386843 100644 --- a/contracts/feature-tests/abi-tester/meta/Cargo.toml +++ b/contracts/feature-tests/abi-tester/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/alloc-features/meta/Cargo.toml b/contracts/feature-tests/alloc-features/meta/Cargo.toml index d5ea96c4e1..5f88766d2b 100644 --- a/contracts/feature-tests/alloc-features/meta/Cargo.toml +++ b/contracts/feature-tests/alloc-features/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/basic-features/meta/Cargo.toml b/contracts/feature-tests/basic-features/meta/Cargo.toml index aef691503e..e54614f574 100644 --- a/contracts/feature-tests/basic-features/meta/Cargo.toml +++ b/contracts/feature-tests/basic-features/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/big-float-features/meta/Cargo.toml b/contracts/feature-tests/big-float-features/meta/Cargo.toml index 2ddccdc323..cec51db327 100644 --- a/contracts/feature-tests/big-float-features/meta/Cargo.toml +++ b/contracts/feature-tests/big-float-features/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/builtin-func-features/meta/Cargo.toml b/contracts/feature-tests/composability/builtin-func-features/meta/Cargo.toml index 1ff5ef0a01..78ad027e49 100644 --- a/contracts/feature-tests/composability/builtin-func-features/meta/Cargo.toml +++ b/contracts/feature-tests/composability/builtin-func-features/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml index 769e3032e0..40b9c7e7a8 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/meta/Cargo.toml @@ -11,6 +11,4 @@ path = ".." workspace = true [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml index a1dede2106..d6c72fee56 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/meta/Cargo.toml @@ -11,6 +11,4 @@ path = ".." workspace = true [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml index 66e877788f..af95cd17c2 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/meta/Cargo.toml @@ -11,6 +11,4 @@ path = ".." workspace = true [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml index 6331b8b439..30aec26048 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/meta/Cargo.toml @@ -11,6 +11,4 @@ path = ".." workspace = true [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/forwarder-blind/meta/Cargo.toml b/contracts/feature-tests/composability/forwarder-blind/meta/Cargo.toml index 49be17d8c3..7139305c71 100644 --- a/contracts/feature-tests/composability/forwarder-blind/meta/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-blind/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/forwarder-legacy/meta/Cargo.toml b/contracts/feature-tests/composability/forwarder-legacy/meta/Cargo.toml index ccd3a77fa2..3e51b40844 100644 --- a/contracts/feature-tests/composability/forwarder-legacy/meta/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-legacy/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/forwarder-raw/meta/Cargo.toml b/contracts/feature-tests/composability/forwarder-raw/meta/Cargo.toml index 472435fc65..3c06deb621 100644 --- a/contracts/feature-tests/composability/forwarder-raw/meta/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-raw/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/forwarder/meta/Cargo.toml b/contracts/feature-tests/composability/forwarder/meta/Cargo.toml index 8a551199e0..81ba03e2c4 100644 --- a/contracts/feature-tests/composability/forwarder/meta/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/local-esdt-and-nft/meta/Cargo.toml b/contracts/feature-tests/composability/local-esdt-and-nft/meta/Cargo.toml index ed5736a1ad..185ca79d22 100644 --- a/contracts/feature-tests/composability/local-esdt-and-nft/meta/Cargo.toml +++ b/contracts/feature-tests/composability/local-esdt-and-nft/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/mesh-node/meta/Cargo.toml b/contracts/feature-tests/composability/mesh-node/meta/Cargo.toml index d9f23a3429..914ef45d8d 100644 --- a/contracts/feature-tests/composability/mesh-node/meta/Cargo.toml +++ b/contracts/feature-tests/composability/mesh-node/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/proxy-test-first/meta/Cargo.toml b/contracts/feature-tests/composability/proxy-test-first/meta/Cargo.toml index 343633f10e..a72dbecff3 100644 --- a/contracts/feature-tests/composability/proxy-test-first/meta/Cargo.toml +++ b/contracts/feature-tests/composability/proxy-test-first/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/proxy-test-second/meta/Cargo.toml b/contracts/feature-tests/composability/proxy-test-second/meta/Cargo.toml index 1f5390d304..74af06e44b 100644 --- a/contracts/feature-tests/composability/proxy-test-second/meta/Cargo.toml +++ b/contracts/feature-tests/composability/proxy-test-second/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/recursive-caller/meta/Cargo.toml b/contracts/feature-tests/composability/recursive-caller/meta/Cargo.toml index c05ac1f868..536a77442a 100644 --- a/contracts/feature-tests/composability/recursive-caller/meta/Cargo.toml +++ b/contracts/feature-tests/composability/recursive-caller/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/transfer-role-features/meta/Cargo.toml b/contracts/feature-tests/composability/transfer-role-features/meta/Cargo.toml index 0ef8cc40bc..b0b56e2555 100644 --- a/contracts/feature-tests/composability/transfer-role-features/meta/Cargo.toml +++ b/contracts/feature-tests/composability/transfer-role-features/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/composability/vault/meta/Cargo.toml b/contracts/feature-tests/composability/vault/meta/Cargo.toml index 034128e231..f4e6040c0e 100644 --- a/contracts/feature-tests/composability/vault/meta/Cargo.toml +++ b/contracts/feature-tests/composability/vault/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/meta/Cargo.toml index 5c3d0f5983..5990f417f4 100644 --- a/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/meta/Cargo.toml index 4f76537f6b..bb461eb9b0 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/meta/Cargo.toml index e4ad4e0551..211f28a1ee 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc1155/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155/meta/Cargo.toml index 5352c174ff..d45c7d2407 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc20/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc20/meta/Cargo.toml index f5eae7af27..428e292c85 100644 --- a/contracts/feature-tests/erc-style-contracts/erc20/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc20/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc721/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc721/meta/Cargo.toml index d582e276bc..4b6c0b8d42 100644 --- a/contracts/feature-tests/erc-style-contracts/erc721/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc721/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/lottery-erc20/meta/Cargo.toml b/contracts/feature-tests/erc-style-contracts/lottery-erc20/meta/Cargo.toml index a77d41ebbe..4f5ded756c 100644 --- a/contracts/feature-tests/erc-style-contracts/lottery-erc20/meta/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/lottery-erc20/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/esdt-system-sc-mock/meta/Cargo.toml b/contracts/feature-tests/esdt-system-sc-mock/meta/Cargo.toml index 88019fcade..cecbc0c878 100644 --- a/contracts/feature-tests/esdt-system-sc-mock/meta/Cargo.toml +++ b/contracts/feature-tests/esdt-system-sc-mock/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/exchange-features/meta/Cargo.toml b/contracts/feature-tests/exchange-features/meta/Cargo.toml index 9905df5b1f..022b925bf2 100644 --- a/contracts/feature-tests/exchange-features/meta/Cargo.toml +++ b/contracts/feature-tests/exchange-features/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/formatted-message-features/meta/Cargo.toml b/contracts/feature-tests/formatted-message-features/meta/Cargo.toml index 1a7dc69ecf..4d1d65ce5a 100644 --- a/contracts/feature-tests/formatted-message-features/meta/Cargo.toml +++ b/contracts/feature-tests/formatted-message-features/meta/Cargo.toml @@ -11,6 +11,4 @@ authors = ["you"] path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/multi-contract-features/meta/Cargo.toml b/contracts/feature-tests/multi-contract-features/meta/Cargo.toml index df5d08b4cb..b54f8c442c 100644 --- a/contracts/feature-tests/multi-contract-features/meta/Cargo.toml +++ b/contracts/feature-tests/multi-contract-features/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/panic-message-features/meta/Cargo.toml b/contracts/feature-tests/panic-message-features/meta/Cargo.toml index fe4b8bc871..0e32d35129 100644 --- a/contracts/feature-tests/panic-message-features/meta/Cargo.toml +++ b/contracts/feature-tests/panic-message-features/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/payable-features/meta/Cargo.toml b/contracts/feature-tests/payable-features/meta/Cargo.toml index 1e4783f523..58ac86c4f6 100644 --- a/contracts/feature-tests/payable-features/meta/Cargo.toml +++ b/contracts/feature-tests/payable-features/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/rust-snippets-generator-test/meta/Cargo.toml b/contracts/feature-tests/rust-snippets-generator-test/meta/Cargo.toml index 8df110e7dc..db45479bac 100644 --- a/contracts/feature-tests/rust-snippets-generator-test/meta/Cargo.toml +++ b/contracts/feature-tests/rust-snippets-generator-test/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/rust-testing-framework-tester/meta/Cargo.toml b/contracts/feature-tests/rust-testing-framework-tester/meta/Cargo.toml index bf7b1f00e8..9556226b4f 100644 --- a/contracts/feature-tests/rust-testing-framework-tester/meta/Cargo.toml +++ b/contracts/feature-tests/rust-testing-framework-tester/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/scenario-tester/meta/Cargo.toml b/contracts/feature-tests/scenario-tester/meta/Cargo.toml index 61512f3dba..8a7c2a42c9 100644 --- a/contracts/feature-tests/scenario-tester/meta/Cargo.toml +++ b/contracts/feature-tests/scenario-tester/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/std-contract/meta/Cargo.toml b/contracts/feature-tests/std-contract/meta/Cargo.toml index 5be8b0443c..7612e7c8df 100644 --- a/contracts/feature-tests/std-contract/meta/Cargo.toml +++ b/contracts/feature-tests/std-contract/meta/Cargo.toml @@ -8,6 +8,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/contracts/feature-tests/use-module/Cargo.toml b/contracts/feature-tests/use-module/Cargo.toml index e7852c5985..97360c5a08 100644 --- a/contracts/feature-tests/use-module/Cargo.toml +++ b/contracts/feature-tests/use-module/Cargo.toml @@ -21,5 +21,4 @@ features = ["wasmer-experimental"] path = "../../../framework/scenario" [dev-dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../framework/meta-lib" +workspace = true diff --git a/contracts/feature-tests/use-module/meta/Cargo.toml b/contracts/feature-tests/use-module/meta/Cargo.toml index 0ebb64cbb9..0d5189b3ed 100644 --- a/contracts/feature-tests/use-module/meta/Cargo.toml +++ b/contracts/feature-tests/use-module/meta/Cargo.toml @@ -9,6 +9,4 @@ publish = false path = ".." [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../../../framework/meta-lib" -default-features = false +workspace = true diff --git a/tools/cs-test-runner/Cargo.toml b/tools/cs-test-runner/Cargo.toml index 8d7cc4ade3..a7ec62b3a2 100644 --- a/tools/cs-test-runner/Cargo.toml +++ b/tools/cs-test-runner/Cargo.toml @@ -10,5 +10,4 @@ name = "cs-test-runner" path = "src/main.rs" [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../framework/meta-lib" +workspace = true diff --git a/tools/wat-gen/Cargo.toml b/tools/wat-gen/Cargo.toml index 3fc3dd3342..20c31c2a75 100644 --- a/tools/wat-gen/Cargo.toml +++ b/tools/wat-gen/Cargo.toml @@ -14,5 +14,4 @@ path = "src/wat_gen_main.rs" wat = "1.235" [dependencies.multiversx-sc-meta-lib] -version = "0.66.2" -path = "../../framework/meta-lib" +workspace = true From 57b791630394881f953d1d3557e5cff3639ab58b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 13:30:12 +0300 Subject: [PATCH 121/195] workspace deps - scenario --- contracts/benchmarks/large-storage/Cargo.toml | 3 +-- contracts/benchmarks/mappers/benchmark-common/Cargo.toml | 3 +-- contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/map-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/queue-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/set-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/single-value-repeat/Cargo.toml | 3 +-- contracts/benchmarks/mappers/vec-repeat/Cargo.toml | 3 +-- contracts/benchmarks/send-tx-repeat/Cargo.toml | 3 +-- contracts/benchmarks/str-repeat/Cargo.toml | 3 +-- contracts/benchmarks/very-large-storage/Cargo.toml | 3 +-- contracts/core/price-aggregator/Cargo.toml | 3 +-- contracts/core/wegld-swap/Cargo.toml | 3 +-- contracts/examples/bonding-curve-contract/Cargo.toml | 3 +-- contracts/examples/check-pause/Cargo.toml | 3 +-- contracts/examples/crypto-bubbles/Cargo.toml | 3 +-- contracts/examples/crypto-kitties/kitty-auction/Cargo.toml | 3 +-- .../examples/crypto-kitties/kitty-genetic-alg/Cargo.toml | 3 +-- .../examples/crypto-kitties/kitty-ownership/Cargo.toml | 3 +-- contracts/examples/crypto-zombies/Cargo.toml | 3 +-- contracts/examples/digital-cash/Cargo.toml | 3 +-- contracts/examples/empty/Cargo.toml | 3 +-- contracts/examples/esdt-transfer-with-fee/Cargo.toml | 3 +-- contracts/examples/fractional-nfts/Cargo.toml | 3 +-- contracts/examples/lottery/Cargo.toml | 3 +-- contracts/examples/multisig/interact/Cargo.toml | 3 +-- contracts/examples/nft-minter/Cargo.toml | 3 +-- contracts/examples/nft-storage-prepay/Cargo.toml | 3 +-- contracts/examples/nft-subscription/Cargo.toml | 3 +-- contracts/examples/order-book/factory/Cargo.toml | 3 +-- contracts/examples/order-book/pair/Cargo.toml | 3 +-- contracts/examples/proxy-pause/Cargo.toml | 3 +-- contracts/examples/rewards-distribution/Cargo.toml | 3 +-- contracts/examples/seed-nft-minter/Cargo.toml | 3 +-- contracts/examples/token-release/Cargo.toml | 3 +-- contracts/feature-tests/abi-tester/Cargo.toml | 3 +-- contracts/feature-tests/basic-features/Cargo.toml | 6 ++---- contracts/feature-tests/composability/Cargo.toml | 3 +-- .../composability/builtin-func-features/Cargo.toml | 3 +-- .../composability/esdt-contract-pair/Cargo.toml | 3 +-- .../esdt-contract-pair/first-contract/Cargo.toml | 3 +-- .../esdt-contract-pair/second-contract/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/child/Cargo.toml | 3 +-- .../execute-on-dest-esdt-issue-callback/parent/Cargo.toml | 3 +-- .../feature-tests/composability/forwarder-blind/Cargo.toml | 3 +-- .../feature-tests/composability/forwarder-legacy/Cargo.toml | 3 +-- .../feature-tests/composability/forwarder-raw/Cargo.toml | 3 +-- contracts/feature-tests/composability/forwarder/Cargo.toml | 3 +-- .../composability/local-esdt-and-nft/Cargo.toml | 3 +-- contracts/feature-tests/composability/mesh-node/Cargo.toml | 3 +-- .../feature-tests/composability/proxy-test-first/Cargo.toml | 3 +-- .../composability/proxy-test-second/Cargo.toml | 3 +-- .../feature-tests/composability/recursive-caller/Cargo.toml | 3 +-- .../composability/transfer-role-features/Cargo.toml | 3 +-- contracts/feature-tests/composability/vault/Cargo.toml | 3 +-- .../erc-style-contracts/crowdfunding-erc20/Cargo.toml | 3 +-- .../erc-style-contracts/erc1155-marketplace/Cargo.toml | 3 +-- .../erc-style-contracts/erc1155-user-mock/Cargo.toml | 3 +-- .../feature-tests/erc-style-contracts/erc1155/Cargo.toml | 3 +-- .../feature-tests/erc-style-contracts/erc20/Cargo.toml | 3 +-- .../feature-tests/erc-style-contracts/erc721/Cargo.toml | 3 +-- .../erc-style-contracts/lottery-erc20/Cargo.toml | 3 +-- contracts/feature-tests/esdt-system-sc-mock/Cargo.toml | 3 +-- contracts/feature-tests/exchange-features/Cargo.toml | 3 +-- contracts/feature-tests/gas-tests/Cargo.toml | 3 +-- contracts/feature-tests/multi-contract-features/Cargo.toml | 3 +-- .../feature-tests/rust-snippets-generator-test/Cargo.toml | 3 +-- .../feature-tests/rust-testing-framework-tester/Cargo.toml | 3 +-- contracts/feature-tests/scenario-tester/Cargo.toml | 3 +-- contracts/feature-tests/std-contract/Cargo.toml | 3 +-- contracts/feature-tests/tiny/Cargo.toml | 3 +-- data/human-readable/Cargo.toml | 3 +-- tools/managed-mem-bench/Cargo.toml | 3 +-- tools/op-test-gen/Cargo.toml | 3 +-- tools/rust-debugger/format-tests/Cargo.toml | 3 +-- 76 files changed, 77 insertions(+), 154 deletions(-) diff --git a/contracts/benchmarks/large-storage/Cargo.toml b/contracts/benchmarks/large-storage/Cargo.toml index 33e643c044..a468db08cd 100644 --- a/contracts/benchmarks/large-storage/Cargo.toml +++ b/contracts/benchmarks/large-storage/Cargo.toml @@ -12,5 +12,4 @@ path = "src/large_storage.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/benchmark-common/Cargo.toml b/contracts/benchmarks/mappers/benchmark-common/Cargo.toml index ed27adff7e..d7c06fc6c4 100644 --- a/contracts/benchmarks/mappers/benchmark-common/Cargo.toml +++ b/contracts/benchmarks/mappers/benchmark-common/Cargo.toml @@ -12,5 +12,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml b/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml index 35c1837d32..aa81a41863 100644 --- a/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/linked-list-repeat/Cargo.toml @@ -16,5 +16,4 @@ path = "../benchmark-common" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/map-repeat/Cargo.toml b/contracts/benchmarks/mappers/map-repeat/Cargo.toml index 9c2cbd23fd..9a64ed2215 100644 --- a/contracts/benchmarks/mappers/map-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/map-repeat/Cargo.toml @@ -16,5 +16,4 @@ path = "../benchmark-common" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/queue-repeat/Cargo.toml b/contracts/benchmarks/mappers/queue-repeat/Cargo.toml index 8debb6cc7a..95a448899a 100644 --- a/contracts/benchmarks/mappers/queue-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/queue-repeat/Cargo.toml @@ -16,5 +16,4 @@ path = "../benchmark-common" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/set-repeat/Cargo.toml b/contracts/benchmarks/mappers/set-repeat/Cargo.toml index 9fc9061112..593998d9cb 100644 --- a/contracts/benchmarks/mappers/set-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/set-repeat/Cargo.toml @@ -16,5 +16,4 @@ path = "../benchmark-common" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml b/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml index e85caa44da..94b44613fa 100644 --- a/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/single-value-repeat/Cargo.toml @@ -16,5 +16,4 @@ path = "../benchmark-common" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/mappers/vec-repeat/Cargo.toml b/contracts/benchmarks/mappers/vec-repeat/Cargo.toml index a311243450..d828d90ea3 100644 --- a/contracts/benchmarks/mappers/vec-repeat/Cargo.toml +++ b/contracts/benchmarks/mappers/vec-repeat/Cargo.toml @@ -16,5 +16,4 @@ path = "../benchmark-common" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/send-tx-repeat/Cargo.toml b/contracts/benchmarks/send-tx-repeat/Cargo.toml index f6684f75d0..ebaab483b9 100644 --- a/contracts/benchmarks/send-tx-repeat/Cargo.toml +++ b/contracts/benchmarks/send-tx-repeat/Cargo.toml @@ -12,5 +12,4 @@ path = "src/send_tx_repeat.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/str-repeat/Cargo.toml b/contracts/benchmarks/str-repeat/Cargo.toml index c09bba9e04..6fa7cdb54c 100644 --- a/contracts/benchmarks/str-repeat/Cargo.toml +++ b/contracts/benchmarks/str-repeat/Cargo.toml @@ -16,5 +16,4 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/benchmarks/very-large-storage/Cargo.toml b/contracts/benchmarks/very-large-storage/Cargo.toml index 14f5e1ee4b..bdcfba21ab 100644 --- a/contracts/benchmarks/very-large-storage/Cargo.toml +++ b/contracts/benchmarks/very-large-storage/Cargo.toml @@ -12,5 +12,4 @@ path = "src/very_large_storage.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/core/price-aggregator/Cargo.toml b/contracts/core/price-aggregator/Cargo.toml index 8098e6e01c..7bf7acf88c 100644 --- a/contracts/core/price-aggregator/Cargo.toml +++ b/contracts/core/price-aggregator/Cargo.toml @@ -26,8 +26,7 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dev-dependencies] rand = { version = "0.10" } diff --git a/contracts/core/wegld-swap/Cargo.toml b/contracts/core/wegld-swap/Cargo.toml index 914166bff4..0ac8061bda 100644 --- a/contracts/core/wegld-swap/Cargo.toml +++ b/contracts/core/wegld-swap/Cargo.toml @@ -27,5 +27,4 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/bonding-curve-contract/Cargo.toml b/contracts/examples/bonding-curve-contract/Cargo.toml index 99118a56e8..bd8b235b6a 100644 --- a/contracts/examples/bonding-curve-contract/Cargo.toml +++ b/contracts/examples/bonding-curve-contract/Cargo.toml @@ -16,6 +16,5 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/check-pause/Cargo.toml b/contracts/examples/check-pause/Cargo.toml index e7825463d8..385dcaa30a 100644 --- a/contracts/examples/check-pause/Cargo.toml +++ b/contracts/examples/check-pause/Cargo.toml @@ -19,6 +19,5 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/crypto-bubbles/Cargo.toml b/contracts/examples/crypto-bubbles/Cargo.toml index e2579ce2fb..d7aa840096 100644 --- a/contracts/examples/crypto-bubbles/Cargo.toml +++ b/contracts/examples/crypto-bubbles/Cargo.toml @@ -12,5 +12,4 @@ path = "src/crypto_bubbles.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml b/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml index 2183e18dee..a2f93ee5c3 100644 --- a/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-auction/Cargo.toml @@ -20,5 +20,4 @@ path = "../kitty-ownership" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml b/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml index 4064c46ce1..f9c1f36e07 100644 --- a/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-genetic-alg/Cargo.toml @@ -21,5 +21,4 @@ path = "../common/random" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml b/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml index aad15de3b0..8dd681d59e 100644 --- a/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml +++ b/contracts/examples/crypto-kitties/kitty-ownership/Cargo.toml @@ -24,5 +24,4 @@ path = "../kitty-genetic-alg" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/examples/crypto-zombies/Cargo.toml b/contracts/examples/crypto-zombies/Cargo.toml index 4c88724653..9f2b29454b 100644 --- a/contracts/examples/crypto-zombies/Cargo.toml +++ b/contracts/examples/crypto-zombies/Cargo.toml @@ -12,5 +12,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/digital-cash/Cargo.toml b/contracts/examples/digital-cash/Cargo.toml index 24c322d058..3e374ff159 100644 --- a/contracts/examples/digital-cash/Cargo.toml +++ b/contracts/examples/digital-cash/Cargo.toml @@ -12,5 +12,4 @@ path = "src/digital_cash.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/empty/Cargo.toml b/contracts/examples/empty/Cargo.toml index 3480aea972..fb85f85ee1 100644 --- a/contracts/examples/empty/Cargo.toml +++ b/contracts/examples/empty/Cargo.toml @@ -12,8 +12,7 @@ path = "src/empty.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dev-dependencies] num-bigint = "0.4" diff --git a/contracts/examples/esdt-transfer-with-fee/Cargo.toml b/contracts/examples/esdt-transfer-with-fee/Cargo.toml index 073c6d6156..33ebdf9c56 100644 --- a/contracts/examples/esdt-transfer-with-fee/Cargo.toml +++ b/contracts/examples/esdt-transfer-with-fee/Cargo.toml @@ -12,5 +12,4 @@ path = "src/esdt_transfer_with_fee.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/fractional-nfts/Cargo.toml b/contracts/examples/fractional-nfts/Cargo.toml index ab9d2e5045..6e273cc153 100644 --- a/contracts/examples/fractional-nfts/Cargo.toml +++ b/contracts/examples/fractional-nfts/Cargo.toml @@ -16,5 +16,4 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/lottery/Cargo.toml b/contracts/examples/lottery/Cargo.toml index 0b3e81ca4e..075632ba06 100644 --- a/contracts/examples/lottery/Cargo.toml +++ b/contracts/examples/lottery/Cargo.toml @@ -12,5 +12,4 @@ path = "src/lottery.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/multisig/interact/Cargo.toml b/contracts/examples/multisig/interact/Cargo.toml index fb454307d2..210c859265 100644 --- a/contracts/examples/multisig/interact/Cargo.toml +++ b/contracts/examples/multisig/interact/Cargo.toml @@ -27,8 +27,7 @@ version = "0.66.2" path = "../../../../framework/snippets" [dependencies.multiversx-sc-scenario] -version = "=0.66.2" -path = "../../../../framework/scenario" +workspace = true [dependencies.multiversx-sc] workspace = true diff --git a/contracts/examples/nft-minter/Cargo.toml b/contracts/examples/nft-minter/Cargo.toml index 167a1cd622..0d78c3d6ec 100644 --- a/contracts/examples/nft-minter/Cargo.toml +++ b/contracts/examples/nft-minter/Cargo.toml @@ -12,5 +12,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/nft-storage-prepay/Cargo.toml b/contracts/examples/nft-storage-prepay/Cargo.toml index 5fe8ec71e6..96c231b44c 100644 --- a/contracts/examples/nft-storage-prepay/Cargo.toml +++ b/contracts/examples/nft-storage-prepay/Cargo.toml @@ -13,5 +13,4 @@ path = "src/nft_storage_prepay.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/nft-subscription/Cargo.toml b/contracts/examples/nft-subscription/Cargo.toml index b74f985193..69dcfde8c0 100644 --- a/contracts/examples/nft-subscription/Cargo.toml +++ b/contracts/examples/nft-subscription/Cargo.toml @@ -16,5 +16,4 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/order-book/factory/Cargo.toml b/contracts/examples/order-book/factory/Cargo.toml index b633ab0aa0..60becf2c6e 100644 --- a/contracts/examples/order-book/factory/Cargo.toml +++ b/contracts/examples/order-book/factory/Cargo.toml @@ -11,6 +11,5 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/examples/order-book/pair/Cargo.toml b/contracts/examples/order-book/pair/Cargo.toml index de1accfc7e..508aec9412 100644 --- a/contracts/examples/order-book/pair/Cargo.toml +++ b/contracts/examples/order-book/pair/Cargo.toml @@ -11,5 +11,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/examples/proxy-pause/Cargo.toml b/contracts/examples/proxy-pause/Cargo.toml index 5ff7937ba1..f79e7da241 100644 --- a/contracts/examples/proxy-pause/Cargo.toml +++ b/contracts/examples/proxy-pause/Cargo.toml @@ -12,8 +12,7 @@ path = "src/proxy_pause.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dev-dependencies.check-pause] path = "../check-pause" diff --git a/contracts/examples/rewards-distribution/Cargo.toml b/contracts/examples/rewards-distribution/Cargo.toml index ea43a93bff..925e77bf32 100644 --- a/contracts/examples/rewards-distribution/Cargo.toml +++ b/contracts/examples/rewards-distribution/Cargo.toml @@ -16,5 +16,4 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/seed-nft-minter/Cargo.toml b/contracts/examples/seed-nft-minter/Cargo.toml index 22c028d482..49a9831008 100644 --- a/contracts/examples/seed-nft-minter/Cargo.toml +++ b/contracts/examples/seed-nft-minter/Cargo.toml @@ -16,5 +16,4 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/examples/token-release/Cargo.toml b/contracts/examples/token-release/Cargo.toml index c981665d3a..c62f4c8f08 100644 --- a/contracts/examples/token-release/Cargo.toml +++ b/contracts/examples/token-release/Cargo.toml @@ -12,6 +12,5 @@ path = "src/token_release.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/abi-tester/Cargo.toml b/contracts/feature-tests/abi-tester/Cargo.toml index 44bde300eb..e475cb2aaa 100644 --- a/contracts/feature-tests/abi-tester/Cargo.toml +++ b/contracts/feature-tests/abi-tester/Cargo.toml @@ -13,8 +13,7 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dev-dependencies.multiversx-sc-meta-lib] workspace = true diff --git a/contracts/feature-tests/basic-features/Cargo.toml b/contracts/feature-tests/basic-features/Cargo.toml index 3885cbddaa..9ab28963b0 100644 --- a/contracts/feature-tests/basic-features/Cargo.toml +++ b/contracts/feature-tests/basic-features/Cargo.toml @@ -16,14 +16,12 @@ workspace = true # BLS doesn't currently work on Windows [target.'cfg(not(target_os = "windows"))'.dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental", "bls"] -path = "../../../framework/scenario" [target.'cfg(target_os = "windows")'.dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" [dependencies.multiversx-sc-modules] version = "0.66.2" diff --git a/contracts/feature-tests/composability/Cargo.toml b/contracts/feature-tests/composability/Cargo.toml index 62a6db18ce..2b879d8bc7 100644 --- a/contracts/feature-tests/composability/Cargo.toml +++ b/contracts/feature-tests/composability/Cargo.toml @@ -39,6 +39,5 @@ path = "vault" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true features = ["wasmer-experimental"] diff --git a/contracts/feature-tests/composability/builtin-func-features/Cargo.toml b/contracts/feature-tests/composability/builtin-func-features/Cargo.toml index c6a8eb3b61..4e860c3a5b 100644 --- a/contracts/feature-tests/composability/builtin-func-features/Cargo.toml +++ b/contracts/feature-tests/composability/builtin-func-features/Cargo.toml @@ -12,5 +12,4 @@ path = "src/builtin_func_features.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml index 9203be4dc7..9762b04906 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/Cargo.toml @@ -15,5 +15,4 @@ path = "second-contract" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml index 240c3c5136..72278618e5 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/first-contract/Cargo.toml @@ -12,6 +12,5 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml index 7bc2566d9a..ae1c708b6e 100644 --- a/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml +++ b/contracts/feature-tests/composability/esdt-contract-pair/second-contract/Cargo.toml @@ -13,6 +13,5 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml index c0acf85e0a..299eb8445f 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/Cargo.toml @@ -19,5 +19,4 @@ path = "child" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml index bbdcba733d..674492bd6a 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/child/Cargo.toml @@ -13,5 +13,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml index 0d9f80555c..b7c765e270 100644 --- a/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml +++ b/contracts/feature-tests/composability/execute-on-dest-esdt-issue-callback/parent/Cargo.toml @@ -12,6 +12,5 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/forwarder-blind/Cargo.toml b/contracts/feature-tests/composability/forwarder-blind/Cargo.toml index b3d68e1dfa..eef005119d 100644 --- a/contracts/feature-tests/composability/forwarder-blind/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-blind/Cargo.toml @@ -12,5 +12,4 @@ path = "src/forwarder_blind.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml b/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml index 3ab26f3722..9dca8c38b2 100644 --- a/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-legacy/Cargo.toml @@ -15,6 +15,5 @@ path = "../vault" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/forwarder-raw/Cargo.toml b/contracts/feature-tests/composability/forwarder-raw/Cargo.toml index 46e274813b..5099ffca87 100644 --- a/contracts/feature-tests/composability/forwarder-raw/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-raw/Cargo.toml @@ -12,5 +12,4 @@ path = "src/forwarder_raw.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/forwarder/Cargo.toml b/contracts/feature-tests/composability/forwarder/Cargo.toml index 04172f9dbe..bd5e8465db 100644 --- a/contracts/feature-tests/composability/forwarder/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder/Cargo.toml @@ -12,6 +12,5 @@ path = "src/forwarder_main.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml b/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml index fd5741dd0c..cb73378deb 100644 --- a/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml +++ b/contracts/feature-tests/composability/local-esdt-and-nft/Cargo.toml @@ -13,5 +13,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/mesh-node/Cargo.toml b/contracts/feature-tests/composability/mesh-node/Cargo.toml index c67766dd78..878ddc07f1 100644 --- a/contracts/feature-tests/composability/mesh-node/Cargo.toml +++ b/contracts/feature-tests/composability/mesh-node/Cargo.toml @@ -17,5 +17,4 @@ path = "../../../../framework/wasm-adapter" optional = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/proxy-test-first/Cargo.toml b/contracts/feature-tests/composability/proxy-test-first/Cargo.toml index f56cdbdecd..87643934fd 100644 --- a/contracts/feature-tests/composability/proxy-test-first/Cargo.toml +++ b/contracts/feature-tests/composability/proxy-test-first/Cargo.toml @@ -13,5 +13,4 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/proxy-test-second/Cargo.toml b/contracts/feature-tests/composability/proxy-test-second/Cargo.toml index a463760c62..2673f12497 100644 --- a/contracts/feature-tests/composability/proxy-test-second/Cargo.toml +++ b/contracts/feature-tests/composability/proxy-test-second/Cargo.toml @@ -13,5 +13,4 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/recursive-caller/Cargo.toml b/contracts/feature-tests/composability/recursive-caller/Cargo.toml index b6ebb5d4c5..dac4298df1 100644 --- a/contracts/feature-tests/composability/recursive-caller/Cargo.toml +++ b/contracts/feature-tests/composability/recursive-caller/Cargo.toml @@ -15,5 +15,4 @@ path = "../vault" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/transfer-role-features/Cargo.toml b/contracts/feature-tests/composability/transfer-role-features/Cargo.toml index 4fa86c1aaa..2e37fc4ff3 100644 --- a/contracts/feature-tests/composability/transfer-role-features/Cargo.toml +++ b/contracts/feature-tests/composability/transfer-role-features/Cargo.toml @@ -19,5 +19,4 @@ version = "0.66.2" path = "../../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/composability/vault/Cargo.toml b/contracts/feature-tests/composability/vault/Cargo.toml index b8b48b9a42..b48868d1a4 100644 --- a/contracts/feature-tests/composability/vault/Cargo.toml +++ b/contracts/feature-tests/composability/vault/Cargo.toml @@ -13,5 +13,4 @@ path = "src/vault.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml b/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml index 0e94ac97af..f1257c50e6 100644 --- a/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/crowdfunding-erc20/Cargo.toml @@ -15,6 +15,5 @@ path = "../erc20" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml index 3d95f9e0aa..34a068f5b1 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155-marketplace/Cargo.toml @@ -17,5 +17,4 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml index a1f79232ea..bd86ab13a9 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155-user-mock/Cargo.toml @@ -12,6 +12,5 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml index f815b5269d..91e1b8f4d0 100644 --- a/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc1155/Cargo.toml @@ -13,8 +13,7 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true [dev-dependencies.erc1155-user-mock] path="../erc1155-user-mock" diff --git a/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml index 7ad466888e..4802271f9e 100644 --- a/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc20/Cargo.toml @@ -12,5 +12,4 @@ path = "src/erc20.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml b/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml index a6c1155365..963e4c4ba3 100644 --- a/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/erc721/Cargo.toml @@ -13,5 +13,4 @@ path = "src/erc721.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml b/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml index 2241d396f9..3cd698d3be 100644 --- a/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml +++ b/contracts/feature-tests/erc-style-contracts/lottery-erc20/Cargo.toml @@ -16,5 +16,4 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml b/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml index 8e4c56864d..e0bebb4857 100644 --- a/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml +++ b/contracts/feature-tests/esdt-system-sc-mock/Cargo.toml @@ -12,5 +12,4 @@ path = "src/esdt_system_sc_mock.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/exchange-features/Cargo.toml b/contracts/feature-tests/exchange-features/Cargo.toml index d16e15d0ea..84860f9b14 100644 --- a/contracts/feature-tests/exchange-features/Cargo.toml +++ b/contracts/feature-tests/exchange-features/Cargo.toml @@ -12,5 +12,4 @@ path = "src/exchange_features.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/gas-tests/Cargo.toml b/contracts/feature-tests/gas-tests/Cargo.toml index 13909fcabd..524c7d189f 100644 --- a/contracts/feature-tests/gas-tests/Cargo.toml +++ b/contracts/feature-tests/gas-tests/Cargo.toml @@ -11,8 +11,7 @@ path = "src/gas_tests.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dependencies.multiversx-chain-vm-wasmer-prod] path = "../../../chain/wasmer-prod" diff --git a/contracts/feature-tests/multi-contract-features/Cargo.toml b/contracts/feature-tests/multi-contract-features/Cargo.toml index 505847ec22..defd3b90a4 100644 --- a/contracts/feature-tests/multi-contract-features/Cargo.toml +++ b/contracts/feature-tests/multi-contract-features/Cargo.toml @@ -15,5 +15,4 @@ example_feature = [] workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml b/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml index 1417322ebe..3c3e48eda0 100644 --- a/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml +++ b/contracts/feature-tests/rust-snippets-generator-test/Cargo.toml @@ -12,5 +12,4 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true diff --git a/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml b/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml index f03fa5bb41..692b4fcf70 100644 --- a/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml +++ b/contracts/feature-tests/rust-testing-framework-tester/Cargo.toml @@ -16,8 +16,7 @@ path = "../../examples/adder" path = "../../feature-tests/basic-features" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dev-dependencies] num-bigint = "0.4" diff --git a/contracts/feature-tests/scenario-tester/Cargo.toml b/contracts/feature-tests/scenario-tester/Cargo.toml index d127b533f7..69bbb7ced4 100644 --- a/contracts/feature-tests/scenario-tester/Cargo.toml +++ b/contracts/feature-tests/scenario-tester/Cargo.toml @@ -12,6 +12,5 @@ path = "src/lib.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental", "bls"] -path = "../../../framework/scenario" diff --git a/contracts/feature-tests/std-contract/Cargo.toml b/contracts/feature-tests/std-contract/Cargo.toml index 3991fe7175..918a8a5411 100644 --- a/contracts/feature-tests/std-contract/Cargo.toml +++ b/contracts/feature-tests/std-contract/Cargo.toml @@ -12,8 +12,7 @@ path = "src/std_contract.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true [dev-dependencies] num-bigint = "0.4" diff --git a/contracts/feature-tests/tiny/Cargo.toml b/contracts/feature-tests/tiny/Cargo.toml index 78b69a823d..422c5e51a4 100644 --- a/contracts/feature-tests/tiny/Cargo.toml +++ b/contracts/feature-tests/tiny/Cargo.toml @@ -8,6 +8,5 @@ publish = false path = "src/lib.rs" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../../framework/scenario" +workspace = true features = ["wasmer-experimental"] diff --git a/data/human-readable/Cargo.toml b/data/human-readable/Cargo.toml index c35153ad28..435a95367d 100644 --- a/data/human-readable/Cargo.toml +++ b/data/human-readable/Cargo.toml @@ -18,5 +18,4 @@ categories = ["cryptography::cryptocurrencies", "development-tools"] serde_json = { version = "1.0" } [dependencies.multiversx-sc-scenario] -version = "=0.66.2" -path = "../../framework/scenario" +workspace = true diff --git a/tools/managed-mem-bench/Cargo.toml b/tools/managed-mem-bench/Cargo.toml index 6cd8e586db..320257ecec 100644 --- a/tools/managed-mem-bench/Cargo.toml +++ b/tools/managed-mem-bench/Cargo.toml @@ -16,5 +16,4 @@ path = "src/bench_threading.rs" workspace = true [dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../framework/scenario" +workspace = true diff --git a/tools/op-test-gen/Cargo.toml b/tools/op-test-gen/Cargo.toml index c04e517ecf..963ded7c5e 100644 --- a/tools/op-test-gen/Cargo.toml +++ b/tools/op-test-gen/Cargo.toml @@ -17,5 +17,4 @@ num-bigint = "0.4" num-traits = "0.2" [dependencies.multiversx-sc-scenario] -version = "0.66.2" -path = "../../framework/scenario" +workspace = true diff --git a/tools/rust-debugger/format-tests/Cargo.toml b/tools/rust-debugger/format-tests/Cargo.toml index 26d8582abf..5fe9839ce6 100644 --- a/tools/rust-debugger/format-tests/Cargo.toml +++ b/tools/rust-debugger/format-tests/Cargo.toml @@ -12,8 +12,7 @@ path = "src/format_tests.rs" workspace = true [dependencies.multiversx-sc-scenario] -version = "=0.66.2" -path = "../../../framework/scenario" +workspace = true [dependencies.multiversx-chain-vm] version = "=0.23.1" From 008fc870240c6bdd10e5b5281300c4c2874f819b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 26 Jun 2026 17:20:59 +0300 Subject: [PATCH 122/195] workspace deps - scenario --- contracts/benchmarks/very-large-storage/interactor/Cargo.toml | 3 +-- contracts/examples/crowdfunding/Cargo.toml | 3 +-- contracts/examples/factorial/Cargo.toml | 3 +-- contracts/examples/multisig/Cargo.toml | 3 +-- contracts/examples/multisig/interact/Cargo.toml | 3 +-- contracts/examples/ping-pong-egld/Cargo.toml | 3 +-- contracts/examples/ping-pong-egld/interactor/Cargo.toml | 3 +-- contracts/feature-tests/alloc-features/Cargo.toml | 3 +-- contracts/feature-tests/basic-features/interact/Cargo.toml | 3 +-- contracts/feature-tests/big-float-features/Cargo.toml | 3 +-- .../composability/forwarder-blind/dex-interactor/Cargo.toml | 3 +-- .../composability/forwarder-interactor/Cargo.toml | 3 +-- .../feature-tests/composability/mesh-interactor/Cargo.toml | 3 +-- contracts/feature-tests/formatted-message-features/Cargo.toml | 3 +-- contracts/feature-tests/panic-message-features/Cargo.toml | 3 +-- contracts/feature-tests/payable-features/Cargo.toml | 3 +-- contracts/feature-tests/payable-features/interactor/Cargo.toml | 3 +-- .../rust-snippets-generator-test/interact-rs/Cargo.toml | 3 +-- contracts/feature-tests/tiny/interactor/Cargo.toml | 3 +-- contracts/feature-tests/use-module/Cargo.toml | 3 +-- tools/interactor-delegation-func-calls/Cargo.toml | 3 +-- tools/interactor-governance-func-calls/Cargo.toml | 3 +-- tools/interactor-system-func-calls/Cargo.toml | 3 +-- 23 files changed, 23 insertions(+), 46 deletions(-) diff --git a/contracts/benchmarks/very-large-storage/interactor/Cargo.toml b/contracts/benchmarks/very-large-storage/interactor/Cargo.toml index 90570839f6..12e9328b60 100644 --- a/contracts/benchmarks/very-large-storage/interactor/Cargo.toml +++ b/contracts/benchmarks/very-large-storage/interactor/Cargo.toml @@ -16,8 +16,7 @@ path = "src/vls_interactor.rs" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/examples/crowdfunding/Cargo.toml b/contracts/examples/crowdfunding/Cargo.toml index fd79cdbb40..7292249d6b 100644 --- a/contracts/examples/crowdfunding/Cargo.toml +++ b/contracts/examples/crowdfunding/Cargo.toml @@ -12,6 +12,5 @@ path = "src/crowdfunding.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/examples/factorial/Cargo.toml b/contracts/examples/factorial/Cargo.toml index 58d64af905..0807f54ae3 100644 --- a/contracts/examples/factorial/Cargo.toml +++ b/contracts/examples/factorial/Cargo.toml @@ -15,6 +15,5 @@ compiled-sc-tests = ["multiversx-sc-scenario/compiled-sc-tests"] workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/examples/multisig/Cargo.toml b/contracts/examples/multisig/Cargo.toml index 27b0065b4c..38e12370dd 100644 --- a/contracts/examples/multisig/Cargo.toml +++ b/contracts/examples/multisig/Cargo.toml @@ -16,6 +16,5 @@ version = "0.66.2" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/examples/multisig/interact/Cargo.toml b/contracts/examples/multisig/interact/Cargo.toml index 210c859265..f25847d0b1 100644 --- a/contracts/examples/multisig/interact/Cargo.toml +++ b/contracts/examples/multisig/interact/Cargo.toml @@ -23,8 +23,7 @@ version = "0.66.2" path = "../../../../contracts/modules" [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/ping-pong-egld/Cargo.toml b/contracts/examples/ping-pong-egld/Cargo.toml index 4a99a2c847..01d06dd5f2 100644 --- a/contracts/examples/ping-pong-egld/Cargo.toml +++ b/contracts/examples/ping-pong-egld/Cargo.toml @@ -12,7 +12,6 @@ path = "src/ping_pong.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/examples/ping-pong-egld/interactor/Cargo.toml b/contracts/examples/ping-pong-egld/interactor/Cargo.toml index 93d08cfaec..7775830113 100644 --- a/contracts/examples/ping-pong-egld/interactor/Cargo.toml +++ b/contracts/examples/ping-pong-egld/interactor/Cargo.toml @@ -25,8 +25,7 @@ serial_test = "3.2" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [features] chain-simulator-tests = [] diff --git a/contracts/feature-tests/alloc-features/Cargo.toml b/contracts/feature-tests/alloc-features/Cargo.toml index c09a352eaf..a568a620a3 100644 --- a/contracts/feature-tests/alloc-features/Cargo.toml +++ b/contracts/feature-tests/alloc-features/Cargo.toml @@ -13,6 +13,5 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/feature-tests/basic-features/interact/Cargo.toml b/contracts/feature-tests/basic-features/interact/Cargo.toml index ed3dfea7ff..639500c3f9 100644 --- a/contracts/feature-tests/basic-features/interact/Cargo.toml +++ b/contracts/feature-tests/basic-features/interact/Cargo.toml @@ -26,8 +26,7 @@ serial_test = "3.2" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [features] chain-simulator-tests = [] diff --git a/contracts/feature-tests/big-float-features/Cargo.toml b/contracts/feature-tests/big-float-features/Cargo.toml index a7b73c5f98..15bcdc6cd6 100644 --- a/contracts/feature-tests/big-float-features/Cargo.toml +++ b/contracts/feature-tests/big-float-features/Cargo.toml @@ -12,6 +12,5 @@ path = "src/big_float_main.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml index 0936864bd7..a6799fc7bd 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/Cargo.toml @@ -16,8 +16,7 @@ path = "src/dex_interactor.rs" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../../framework/snippets" +workspace = true [dependencies.multiversx-sc] workspace = true diff --git a/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml b/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml index 15d0860a57..db6eab0c32 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml +++ b/contracts/feature-tests/composability/forwarder-interactor/Cargo.toml @@ -16,8 +16,7 @@ path = "src/interact.rs" path = "../forwarder" [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies.multiversx-sc] workspace = true diff --git a/contracts/feature-tests/composability/mesh-interactor/Cargo.toml b/contracts/feature-tests/composability/mesh-interactor/Cargo.toml index 9d4d6d55fe..6afae0b343 100644 --- a/contracts/feature-tests/composability/mesh-interactor/Cargo.toml +++ b/contracts/feature-tests/composability/mesh-interactor/Cargo.toml @@ -19,8 +19,7 @@ toml = "1.0" workspace = true [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies.mesh-node] path = "../mesh-node" diff --git a/contracts/feature-tests/formatted-message-features/Cargo.toml b/contracts/feature-tests/formatted-message-features/Cargo.toml index 3c6284652f..08bb1def64 100644 --- a/contracts/feature-tests/formatted-message-features/Cargo.toml +++ b/contracts/feature-tests/formatted-message-features/Cargo.toml @@ -13,6 +13,5 @@ workspace = true features = ["alloc"] [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/feature-tests/panic-message-features/Cargo.toml b/contracts/feature-tests/panic-message-features/Cargo.toml index f41299884c..81ece5a907 100644 --- a/contracts/feature-tests/panic-message-features/Cargo.toml +++ b/contracts/feature-tests/panic-message-features/Cargo.toml @@ -12,6 +12,5 @@ path = "src/panic_features.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/feature-tests/payable-features/Cargo.toml b/contracts/feature-tests/payable-features/Cargo.toml index bf2c82a1ea..f0cf237c0c 100644 --- a/contracts/feature-tests/payable-features/Cargo.toml +++ b/contracts/feature-tests/payable-features/Cargo.toml @@ -12,6 +12,5 @@ path = "src/payable_features.rs" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" diff --git a/contracts/feature-tests/payable-features/interactor/Cargo.toml b/contracts/feature-tests/payable-features/interactor/Cargo.toml index ce1b6f87cb..341b9c29f3 100644 --- a/contracts/feature-tests/payable-features/interactor/Cargo.toml +++ b/contracts/feature-tests/payable-features/interactor/Cargo.toml @@ -16,8 +16,7 @@ path = "src/payable_interactor.rs" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/feature-tests/rust-snippets-generator-test/interact-rs/Cargo.toml b/contracts/feature-tests/rust-snippets-generator-test/interact-rs/Cargo.toml index 97777d0dbe..8ae32f16d3 100644 --- a/contracts/feature-tests/rust-snippets-generator-test/interact-rs/Cargo.toml +++ b/contracts/feature-tests/rust-snippets-generator-test/interact-rs/Cargo.toml @@ -13,8 +13,7 @@ path = "src/interactor_main.rs" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true # [workspace] diff --git a/contracts/feature-tests/tiny/interactor/Cargo.toml b/contracts/feature-tests/tiny/interactor/Cargo.toml index 71f2147ba1..a7f6478572 100644 --- a/contracts/feature-tests/tiny/interactor/Cargo.toml +++ b/contracts/feature-tests/tiny/interactor/Cargo.toml @@ -13,8 +13,7 @@ path = "src/tiny_interactor_main.rs" path = "src/tiny_interactor.rs" [dependencies.multiversx-sc-snippets] -version = "0.66.2" -path = "../../../../framework/snippets" +workspace = true [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/feature-tests/use-module/Cargo.toml b/contracts/feature-tests/use-module/Cargo.toml index 97360c5a08..115c2dc344 100644 --- a/contracts/feature-tests/use-module/Cargo.toml +++ b/contracts/feature-tests/use-module/Cargo.toml @@ -16,9 +16,8 @@ path = "../../../contracts/modules" workspace = true [dev-dependencies.multiversx-sc-scenario] -version = "0.66.2" +workspace = true features = ["wasmer-experimental"] -path = "../../../framework/scenario" [dev-dependencies.multiversx-sc-meta-lib] workspace = true diff --git a/tools/interactor-delegation-func-calls/Cargo.toml b/tools/interactor-delegation-func-calls/Cargo.toml index fe4d31976e..c9937ce580 100644 --- a/tools/interactor-delegation-func-calls/Cargo.toml +++ b/tools/interactor-delegation-func-calls/Cargo.toml @@ -24,8 +24,7 @@ version = "1.0" features = ["derive"] [dependencies.multiversx-sc-snippets] -version = "=0.66.2" -path = "../../framework/snippets" +workspace = true [features] chain-simulator-tests = [] diff --git a/tools/interactor-governance-func-calls/Cargo.toml b/tools/interactor-governance-func-calls/Cargo.toml index 82eba77f29..3eb54f9f91 100644 --- a/tools/interactor-governance-func-calls/Cargo.toml +++ b/tools/interactor-governance-func-calls/Cargo.toml @@ -24,8 +24,7 @@ version = "1.0" features = ["derive"] [dependencies.multiversx-sc-snippets] -version = "=0.66.2" -path = "../../framework/snippets" +workspace = true [dependencies.delegation-sc-interact] path = "../interactor-delegation-func-calls" diff --git a/tools/interactor-system-func-calls/Cargo.toml b/tools/interactor-system-func-calls/Cargo.toml index e94744f286..18d7621a0d 100644 --- a/tools/interactor-system-func-calls/Cargo.toml +++ b/tools/interactor-system-func-calls/Cargo.toml @@ -25,8 +25,7 @@ version = "1.0" features = ["derive"] [dependencies.multiversx-sc-snippets] -version = "=0.66.2" -path = "../../framework/snippets" +workspace = true [features] chain-simulator-tests = [] From 901cfc734ad3d7207601d8ee983a7cef0c237ec6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 04:57:03 +0300 Subject: [PATCH 123/195] workspace deps - modules --- Cargo.toml | 4 ++++ contracts/core/price-aggregator/Cargo.toml | 3 +-- contracts/core/wegld-swap/Cargo.toml | 3 +-- contracts/examples/bonding-curve-contract/Cargo.toml | 3 +-- contracts/examples/check-pause/Cargo.toml | 3 +-- contracts/examples/fractional-nfts/Cargo.toml | 3 +-- contracts/examples/multisig/Cargo.toml | 3 +-- contracts/examples/multisig/interact/Cargo.toml | 3 +-- contracts/examples/nft-subscription/Cargo.toml | 3 +-- contracts/examples/rewards-distribution/Cargo.toml | 3 +-- contracts/examples/seed-nft-minter/Cargo.toml | 3 +-- contracts/feature-tests/basic-features/Cargo.toml | 3 +-- .../composability/transfer-role-features/Cargo.toml | 3 +-- contracts/feature-tests/use-module/Cargo.toml | 3 +-- 14 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 98ebea4a50..68bf4a89ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -228,3 +228,7 @@ path = "framework/meta-lib" [workspace.dependencies.multiversx-sc-snippets] version = "0.66.2" path = "framework/snippets" + +[workspace.dependencies.multiversx-sc-modules] +version = "0.66.2" +path = "contracts/modules" diff --git a/contracts/core/price-aggregator/Cargo.toml b/contracts/core/price-aggregator/Cargo.toml index 7bf7acf88c..30c4037db4 100644 --- a/contracts/core/price-aggregator/Cargo.toml +++ b/contracts/core/price-aggregator/Cargo.toml @@ -22,8 +22,7 @@ path = "src/lib.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/core/wegld-swap/Cargo.toml b/contracts/core/wegld-swap/Cargo.toml index 0ac8061bda..65a9cc8b1a 100644 --- a/contracts/core/wegld-swap/Cargo.toml +++ b/contracts/core/wegld-swap/Cargo.toml @@ -23,8 +23,7 @@ path = "src/wegld.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/bonding-curve-contract/Cargo.toml b/contracts/examples/bonding-curve-contract/Cargo.toml index bd8b235b6a..b9e0490cc4 100644 --- a/contracts/examples/bonding-curve-contract/Cargo.toml +++ b/contracts/examples/bonding-curve-contract/Cargo.toml @@ -12,8 +12,7 @@ path = "src/bonding_curve_contract.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/check-pause/Cargo.toml b/contracts/examples/check-pause/Cargo.toml index 385dcaa30a..9a7863312d 100644 --- a/contracts/examples/check-pause/Cargo.toml +++ b/contracts/examples/check-pause/Cargo.toml @@ -15,8 +15,7 @@ num-bigint = "0.4" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/fractional-nfts/Cargo.toml b/contracts/examples/fractional-nfts/Cargo.toml index 6e273cc153..fca8e485ec 100644 --- a/contracts/examples/fractional-nfts/Cargo.toml +++ b/contracts/examples/fractional-nfts/Cargo.toml @@ -12,8 +12,7 @@ path = "src/fractional_nfts.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/multisig/Cargo.toml b/contracts/examples/multisig/Cargo.toml index 38e12370dd..dc858fe6ed 100644 --- a/contracts/examples/multisig/Cargo.toml +++ b/contracts/examples/multisig/Cargo.toml @@ -12,8 +12,7 @@ path = "src/multisig.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/multisig/interact/Cargo.toml b/contracts/examples/multisig/interact/Cargo.toml index f25847d0b1..6b594d49a3 100644 --- a/contracts/examples/multisig/interact/Cargo.toml +++ b/contracts/examples/multisig/interact/Cargo.toml @@ -19,8 +19,7 @@ tokio = { version = "1.24" } path = ".." [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../../contracts/modules" +workspace = true [dependencies.multiversx-sc-snippets] workspace = true diff --git a/contracts/examples/nft-subscription/Cargo.toml b/contracts/examples/nft-subscription/Cargo.toml index 69dcfde8c0..d5fc750326 100644 --- a/contracts/examples/nft-subscription/Cargo.toml +++ b/contracts/examples/nft-subscription/Cargo.toml @@ -12,8 +12,7 @@ path = "src/lib.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/rewards-distribution/Cargo.toml b/contracts/examples/rewards-distribution/Cargo.toml index 925e77bf32..f90addf004 100644 --- a/contracts/examples/rewards-distribution/Cargo.toml +++ b/contracts/examples/rewards-distribution/Cargo.toml @@ -12,8 +12,7 @@ path = "src/rewards_distribution.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/examples/seed-nft-minter/Cargo.toml b/contracts/examples/seed-nft-minter/Cargo.toml index 49a9831008..49651a36a9 100644 --- a/contracts/examples/seed-nft-minter/Cargo.toml +++ b/contracts/examples/seed-nft-minter/Cargo.toml @@ -12,8 +12,7 @@ path = "src/seed_nft_minter.rs" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/feature-tests/basic-features/Cargo.toml b/contracts/feature-tests/basic-features/Cargo.toml index 9ab28963b0..67f45bc12e 100644 --- a/contracts/feature-tests/basic-features/Cargo.toml +++ b/contracts/feature-tests/basic-features/Cargo.toml @@ -24,8 +24,7 @@ workspace = true features = ["wasmer-experimental"] [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dev-dependencies.esdt-system-sc-mock] path = "../esdt-system-sc-mock" diff --git a/contracts/feature-tests/composability/transfer-role-features/Cargo.toml b/contracts/feature-tests/composability/transfer-role-features/Cargo.toml index 2e37fc4ff3..23c28d5669 100644 --- a/contracts/feature-tests/composability/transfer-role-features/Cargo.toml +++ b/contracts/feature-tests/composability/transfer-role-features/Cargo.toml @@ -15,8 +15,7 @@ path = "../vault" workspace = true [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../../contracts/modules" +workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true diff --git a/contracts/feature-tests/use-module/Cargo.toml b/contracts/feature-tests/use-module/Cargo.toml index 115c2dc344..f49bdbf5a4 100644 --- a/contracts/feature-tests/use-module/Cargo.toml +++ b/contracts/feature-tests/use-module/Cargo.toml @@ -9,8 +9,7 @@ publish = false path = "src/use_module.rs" [dependencies.multiversx-sc-modules] -version = "0.66.2" -path = "../../../contracts/modules" +workspace = true [dependencies.multiversx-sc] workspace = true From 0f438ae30826a9e6d943debfe8d9138c9dbb0f7a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sat, 27 Jun 2026 05:04:09 +0300 Subject: [PATCH 124/195] workspace deps - revert standalone workspaces --- contracts/feature-tests/gas-tests/Cargo.lock | 442 +++++++------------ contracts/feature-tests/gas-tests/Cargo.toml | 6 +- tools/rust-debugger/format-tests/Cargo.lock | 305 +++---------- tools/rust-debugger/format-tests/Cargo.toml | 6 +- 4 files changed, 225 insertions(+), 534 deletions(-) diff --git a/contracts/feature-tests/gas-tests/Cargo.lock b/contracts/feature-tests/gas-tests/Cargo.lock index 453ef3737f..f7ec9aad68 100644 --- a/contracts/feature-tests/gas-tests/Cargo.lock +++ b/contracts/feature-tests/gas-tests/Cargo.lock @@ -101,21 +101,21 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" @@ -132,6 +132,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.3" @@ -150,7 +156,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -160,8 +166,8 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", - "syn 2.0.117", + "shlex 1.3.0", + "syn 2.0.118", ] [[package]] @@ -172,15 +178,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -208,18 +214,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecheck" @@ -263,7 +269,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -274,18 +280,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -305,9 +311,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -356,7 +362,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -421,9 +427,9 @@ dependencies = [ [[package]] name = "corosensei" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" +checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34" dependencies = [ "autocfg", "cfg-if", @@ -536,7 +542,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -559,7 +565,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -570,7 +576,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -605,7 +611,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -627,7 +633,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -648,7 +654,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", ] @@ -695,13 +701,13 @@ version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d36219658beb39702975c707dee7895943ca281ca46eebbc5ea395171b9c182b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "lazy_static", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -724,7 +730,7 @@ dependencies = [ "byteorder", "dynasm 4.0.2", "fnv", - "memmap2 0.9.10", + "memmap2 0.9.11", ] [[package]] @@ -801,7 +807,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -870,12 +876,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -933,9 +933,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" dependencies = [ "rustversion", "typenum", @@ -956,16 +956,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] @@ -1018,22 +1016,13 @@ dependencies = [ "ahash 0.8.12", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash 0.2.0", + "foldhash", "serde", "serde_core", ] @@ -1075,12 +1064,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1142,13 +1125,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1170,9 +1152,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" [[package]] name = "leb128fmt" @@ -1208,9 +1190,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -1244,9 +1226,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "loupe" @@ -1291,9 +1273,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -1315,9 +1297,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -1364,11 +1346,13 @@ checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" [[package]] name = "multiversx-chain-core" -version = "0.22.1" +version = "0.23.1" dependencies = [ + "base64", "bech32", - "bitflags 2.11.1", + "bitflags 2.13.0", "blake2", + "ed25519-dalek", "hex", "multiversx-sc-codec", "serde", @@ -1378,7 +1362,7 @@ dependencies = [ [[package]] name = "multiversx-chain-scenario-format" -version = "0.26.0" +version = "0.26.1" dependencies = [ "bech32", "hex", @@ -1391,12 +1375,11 @@ dependencies = [ [[package]] name = "multiversx-chain-vm" -version = "0.22.1" +version = "0.23.1" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "colored", - "ed25519-dalek", "hex", "itertools 0.14.0", "log", @@ -1462,10 +1445,10 @@ dependencies = [ [[package]] name = "multiversx-sc" -version = "0.65.1" +version = "0.66.2" dependencies = [ - "bitflags 2.11.1", - "generic-array 1.4.1", + "bitflags 2.13.0", + "generic-array 1.4.3", "multiversx-chain-core", "multiversx-sc-codec", "multiversx-sc-derive", @@ -1478,7 +1461,7 @@ name = "multiversx-sc-codec" version = "0.25.0" dependencies = [ "arrayvec", - "bitflags 2.11.1", + "bitflags 2.13.0", "multiversx-sc-codec-derive", "num-bigint", "num-traits", @@ -1492,23 +1475,23 @@ dependencies = [ "hex", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "multiversx-sc-derive" -version = "0.65.1" +version = "0.66.2" dependencies = [ "hex", "proc-macro2", "quote", "radix_trie", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "multiversx-sc-meta-lib" -version = "0.65.1" +version = "0.66.2" dependencies = [ "clap", "colored", @@ -1529,7 +1512,7 @@ dependencies = [ [[package]] name = "multiversx-sc-scenario" -version = "0.65.1" +version = "0.66.2" dependencies = [ "colored", "hex", @@ -1567,7 +1550,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1718,7 +1701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1764,7 +1747,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1813,14 +1796,14 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1863,7 +1846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -1923,7 +1906,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -1939,9 +1922,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1962,9 +1945,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "region" @@ -2052,7 +2035,7 @@ checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2082,7 +2065,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2095,7 +2078,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -2191,14 +2174,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2265,6 +2248,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signature" version = "2.2.0" @@ -2300,9 +2289,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "spki" @@ -2357,9 +2346,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2396,7 +2385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -2437,7 +2426,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2448,7 +2437,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2525,7 +2514,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2549,9 +2538,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -2561,9 +2550,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -2591,9 +2580,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "js-sys", "wasm-bindgen", @@ -2611,29 +2600,11 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2644,9 +2615,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2654,56 +2625,34 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser 0.244.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.249.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69830ccbbf41c55eb585991659fb70867ef628193af3a495f09a6956f7615e59" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.249.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder 0.244.0", - "wasmparser 0.244.0", + "wasmparser 0.252.0", ] [[package]] @@ -3042,32 +2991,31 @@ version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.249.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "30538cae9a794215f490b532df01c557e2e2bfac92569482554acd0992a102ea" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", + "bitflags 2.13.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "semver", + "serde", ] [[package]] name = "wasmparser" -version = "0.249.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30538cae9a794215f490b532df01c557e2e2bfac92569482554acd0992a102ea" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.17.1", + "bitflags 2.13.0", "indexmap 2.14.0", "semver", - "serde", ] [[package]] @@ -3083,22 +3031,22 @@ dependencies = [ [[package]] name = "wast" -version = "249.0.0" +version = "252.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2474a321bf9ae2808e9fa23ac4ec2b27300e70985e30bcb5a38d43b76bfc901a" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.249.0", + "wasm-encoder", ] [[package]] name = "wat" -version = "1.249.0" +version = "1.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28af699d0a9c7e4e250b7b8e36167ae5215fbb4b7ae526bb4ce7b234ba0afc90" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" dependencies = [ "wast", ] @@ -3315,100 +3263,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.244.0", - "wasm-metadata", - "wasmparser 0.244.0", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.244.0", -] - [[package]] name = "wyz" version = "0.5.1" @@ -3436,29 +3290,29 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" diff --git a/contracts/feature-tests/gas-tests/Cargo.toml b/contracts/feature-tests/gas-tests/Cargo.toml index 524c7d189f..d3aaa95e7a 100644 --- a/contracts/feature-tests/gas-tests/Cargo.toml +++ b/contracts/feature-tests/gas-tests/Cargo.toml @@ -8,10 +8,12 @@ publish = false path = "src/gas_tests.rs" [dependencies.multiversx-sc] -workspace = true +version = "0.66.2" +path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -workspace = true +version = "0.66.2" +path = "../../../framework/scenario" [dependencies.multiversx-chain-vm-wasmer-prod] path = "../../../chain/wasmer-prod" diff --git a/tools/rust-debugger/format-tests/Cargo.lock b/tools/rust-debugger/format-tests/Cargo.lock index 921efa8301..1b61dcdc33 100644 --- a/tools/rust-debugger/format-tests/Cargo.lock +++ b/tools/rust-debugger/format-tests/Cargo.lock @@ -54,21 +54,27 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -84,9 +90,9 @@ checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "blake2" @@ -108,18 +114,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cfg-if" @@ -129,9 +135,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -305,7 +311,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", ] @@ -358,12 +364,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" dependencies = [ "rustversion", "typenum", @@ -414,16 +414,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] @@ -432,22 +430,13 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash 0.2.0", + "foldhash", "serde", "serde_core", ] @@ -479,12 +468,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "indexmap" version = "2.14.0" @@ -492,7 +475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown", "serde", "serde_core", ] @@ -548,23 +531,25 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "multiversx-chain-core" -version = "0.23.0" +version = "0.23.1" dependencies = [ + "base64", "bech32", "bitflags", "blake2", + "ed25519-dalek", "hex", "multiversx-sc-codec", "serde", @@ -587,12 +572,11 @@ dependencies = [ [[package]] name = "multiversx-chain-vm" -version = "0.23.0" +version = "0.23.1" dependencies = [ "anyhow", "bitflags", "colored", - "ed25519-dalek", "hex", "itertools", "log", @@ -617,10 +601,10 @@ dependencies = [ [[package]] name = "multiversx-sc" -version = "0.66.0" +version = "0.66.2" dependencies = [ "bitflags", - "generic-array 1.4.1", + "generic-array 1.4.3", "multiversx-chain-core", "multiversx-sc-codec", "multiversx-sc-derive", @@ -652,7 +636,7 @@ dependencies = [ [[package]] name = "multiversx-sc-derive" -version = "0.66.0" +version = "0.66.2" dependencies = [ "hex", "proc-macro2", @@ -663,7 +647,7 @@ dependencies = [ [[package]] name = "multiversx-sc-meta-lib" -version = "0.66.0" +version = "0.66.2" dependencies = [ "clap", "colored", @@ -684,7 +668,7 @@ dependencies = [ [[package]] name = "multiversx-sc-scenario" -version = "0.66.0" +version = "0.66.2" dependencies = [ "colored", "hex", @@ -764,16 +748,6 @@ dependencies = [ "spki", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -785,9 +759,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -815,7 +789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -896,9 +870,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -966,9 +940,9 @@ checksum = "69da7c8ef9e55986dcaa55dd095bbf7d321e80cc91644f25ce26a83dbe9e7f14" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "spki" @@ -1000,9 +974,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -1059,9 +1033,9 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -1071,9 +1045,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -1081,12 +1055,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unwrap-infallible" version = "1.0.0" @@ -1111,79 +1079,38 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser 0.244.0", -] - [[package]] name = "wasm-encoder" -version = "0.249.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69830ccbbf41c55eb585991659fb70867ef628193af3a495f09a6956f7615e59" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.249.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder 0.244.0", - "wasmparser 0.244.0", + "wasmparser 0.252.0", ] [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.249.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "30538cae9a794215f490b532df01c557e2e2bfac92569482554acd0992a102ea" dependencies = [ "bitflags", - "hashbrown 0.15.5", + "hashbrown", "indexmap", "semver", + "serde", ] [[package]] name = "wasmparser" -version = "0.249.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30538cae9a794215f490b532df01c557e2e2bfac92569482554acd0992a102ea" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ "bitflags", - "hashbrown 0.17.1", "indexmap", "semver", - "serde", ] [[package]] @@ -1199,22 +1126,22 @@ dependencies = [ [[package]] name = "wast" -version = "249.0.0" +version = "252.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2474a321bf9ae2808e9fa23ac4ec2b27300e70985e30bcb5a38d43b76bfc901a" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.249.0", + "wasm-encoder", ] [[package]] name = "wat" -version = "1.249.0" +version = "1.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28af699d0a9c7e4e250b7b8e36167ae5215fbb4b7ae526bb4ce7b234ba0afc90" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" dependencies = [ "wast", ] @@ -1249,105 +1176,11 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.244.0", - "wasm-metadata", - "wasmparser 0.244.0", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.244.0", -] - [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" diff --git a/tools/rust-debugger/format-tests/Cargo.toml b/tools/rust-debugger/format-tests/Cargo.toml index 5fe9839ce6..1b3b1c9b8b 100644 --- a/tools/rust-debugger/format-tests/Cargo.toml +++ b/tools/rust-debugger/format-tests/Cargo.toml @@ -9,10 +9,12 @@ name = "format-tests" path = "src/format_tests.rs" [dependencies.multiversx-sc] -workspace = true +version = "=0.66.2" +path = "../../../framework/base" [dependencies.multiversx-sc-scenario] -workspace = true +version = "=0.66.2" +path = "../../../framework/scenario" [dependencies.multiversx-chain-vm] version = "=0.23.1" From b01ee0f368a4774559e6ca4952217cd5b49e686d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 28 Jun 2026 19:49:45 +0300 Subject: [PATCH 125/195] interactor cleanup (rebase_to) --- Cargo.lock | 1 + framework/meta/Cargo.toml | 6 ++-- framework/snippets/Cargo.toml | 1 + framework/snippets/src/config/config_path.rs | 33 ++------------------ 4 files changed, 7 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c75ed6521..370d172afb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3364,6 +3364,7 @@ dependencies = [ "multiversx-sdk", "multiversx-sdk-dapp", "multiversx-sdk-http", + "pathdiff", "serde", "serde_json", "tokio", diff --git a/framework/meta/Cargo.toml b/framework/meta/Cargo.toml index 76319d9f10..90c486318f 100644 --- a/framework/meta/Cargo.toml +++ b/framework/meta/Cargo.toml @@ -39,9 +39,9 @@ semver = "1.0.20" ruplacer = { version = "0.10", default-features = false } regex = "1.11" reqwest = { version = "0.13", features = ["rustls", "blocking", "json"] } -copy_dir = "0.1.2" -pathdiff = "0.2.1" -common-path = "1.0.0" +copy_dir = "0.1" +pathdiff = "0.2" +common-path = "1.0" bip39 = { version = "2.0", features = ["rand"] } rand = { version = "0.10", default-features = false } uuid = { version = "1.10", features = ["v4"] } diff --git a/framework/snippets/Cargo.toml b/framework/snippets/Cargo.toml index cfeafd1695..d0a78ff91c 100644 --- a/framework/snippets/Cargo.toml +++ b/framework/snippets/Cargo.toml @@ -28,6 +28,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "1.0" colored = "3.0" +pathdiff = "0.2" tokio = { version = "1.24", features = ["full"], optional = true } diff --git a/framework/snippets/src/config/config_path.rs b/framework/snippets/src/config/config_path.rs index 7b8b0eaa89..2e2e88e87f 100644 --- a/framework/snippets/src/config/config_path.rs +++ b/framework/snippets/src/config/config_path.rs @@ -57,7 +57,8 @@ impl ConfigPath { /// - `mxsc:` for `.mxsc.json` contract bundles /// - `file:` for all other paths pub fn scenario_expr(&self, base: &Path) -> String { - let rebased = self.rebase_to(base); + let rebased = pathdiff::diff_paths(&self.absolute_path, base) + .unwrap_or_else(|| self.absolute_path.clone()); let prefix = match self.absolute_path.to_str() { Some(s) if s.ends_with(".mxsc.json") => "mxsc:", _ => "file:", @@ -65,36 +66,6 @@ impl ConfigPath { format!("{prefix}{}", rebased.display()) } - /// Returns this path expressed relative to `base`. - /// - /// Walks up from `base` with `../` steps for each non-shared component, - /// then appends the remaining components of the absolute path. - /// Falls back to the absolute path if no common root exists (e.g. different - /// drive letters on Windows). - pub fn rebase_to(&self, base: &Path) -> PathBuf { - let base_comps: Vec<_> = base.components().collect(); - let abs_comps: Vec<_> = self.absolute_path.components().collect(); - - let common = base_comps - .iter() - .zip(abs_comps.iter()) - .take_while(|(b, a)| b == a) - .count(); - - if common == 0 { - return self.absolute_path.clone(); - } - - let mut rel = PathBuf::new(); - for _ in &base_comps[common..] { - rel.push(".."); - } - for comp in &abs_comps[common..] { - rel.push(comp.as_os_str()); - } - rel - } - /// Sets the base directory used to resolve relative paths during /// deserialization. Called by [`load_toml_config`] before and after /// parsing; pass `None` to clear. From 5bc3bad6f65a271eca738268f38cc803fd13eb04 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 28 Jun 2026 20:08:40 +0300 Subject: [PATCH 126/195] interactor config cleanup --- .../feature-tests/basic-features/interact/config.toml | 10 +++++----- .../payable-features/interactor/config.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/feature-tests/basic-features/interact/config.toml b/contracts/feature-tests/basic-features/interact/config.toml index 366fe2b4a6..9e6b79b4f3 100644 --- a/contracts/feature-tests/basic-features/interact/config.toml +++ b/contracts/feature-tests/basic-features/interact/config.toml @@ -1,10 +1,10 @@ [connection] -chain_type = 'real' -gateway_uri = 'https://devnet-gateway.multiversx.com' +# chain_type = 'real' +# gateway_uri = 'https://devnet-gateway.multiversx.com' + +chain_type = 'simulator' +gateway_uri = 'http://localhost:8085' [wallet] test_wallet = 'mike' -# [connection] -# chain_type = 'simulator' -# gateway_uri = 'http://localhost:8085' diff --git a/contracts/feature-tests/payable-features/interactor/config.toml b/contracts/feature-tests/payable-features/interactor/config.toml index 04f61527c3..cafbb342e1 100644 --- a/contracts/feature-tests/payable-features/interactor/config.toml +++ b/contracts/feature-tests/payable-features/interactor/config.toml @@ -3,7 +3,7 @@ # gateway_uri = 'http://localhost:8085' chain_type = 'real' -gateway_uri = 'https://testnet-gateway.multiversx.com' +gateway_uri = 'https://devnet-gateway.multiversx.com' [owner] test_wallet = 'heidi' From 276c44b7e755a3190a7a560c2712b7f139d14765 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 28 Jun 2026 20:40:57 +0300 Subject: [PATCH 127/195] rename basic-interactor -> adder-interactor, templating fix --- Cargo.lock | 26 +++++++++---------- .../examples/adder/interactor/Cargo.toml | 8 +++--- ...asic_interactor.rs => adder_interactor.rs} | 12 ++++----- ...eractor_cli.rs => adder_interactor_cli.rs} | 0 .../interactor/src/adder_interactor_main.rs | 6 +++++ .../interactor/src/basic_interactor_main.rs | 6 ----- ...cs_test.rs => adder_interactor_cs_test.rs} | 2 +- .../src/cmd/template/template_adjuster.rs | 19 ++++++++++++-- 8 files changed, 47 insertions(+), 32 deletions(-) rename contracts/examples/adder/interactor/src/{basic_interactor.rs => adder_interactor.rs} (93%) rename contracts/examples/adder/interactor/src/{basic_interactor_cli.rs => adder_interactor_cli.rs} (100%) create mode 100644 contracts/examples/adder/interactor/src/adder_interactor_main.rs delete mode 100644 contracts/examples/adder/interactor/src/basic_interactor_main.rs rename contracts/examples/adder/interactor/tests/{basic_interactor_cs_test.rs => adder_interactor_cs_test.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index 370d172afb..29db4d1021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,19 @@ dependencies = [ "multiversx-sc-scenario", ] +[[package]] +name = "adder-interactor" +version = "0.0.0" +dependencies = [ + "adder", + "clap", + "multiversx-sc-snippets", + "serde", + "serial_test", + "tokio", + "toml", +] + [[package]] name = "adder-meta" version = "0.0.0" @@ -264,19 +277,6 @@ dependencies = [ "multiversx-sc-meta-lib", ] -[[package]] -name = "basic-interactor" -version = "0.0.0" -dependencies = [ - "adder", - "clap", - "multiversx-sc-snippets", - "serde", - "serial_test", - "tokio", - "toml", -] - [[package]] name = "bech32" version = "0.11.1" diff --git a/contracts/examples/adder/interactor/Cargo.toml b/contracts/examples/adder/interactor/Cargo.toml index cf160416ac..912770fa90 100644 --- a/contracts/examples/adder/interactor/Cargo.toml +++ b/contracts/examples/adder/interactor/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "basic-interactor" +name = "adder-interactor" version = "0.0.0" authors = ["MultiversX "] edition = "2024" publish = false [[bin]] -name = "basic-interactor" -path = "src/basic_interactor_main.rs" +name = "adder-interactor" +path = "src/adder_interactor_main.rs" [lib] -path = "src/basic_interactor.rs" +path = "src/adder_interactor.rs" [dependencies.adder] path = ".." diff --git a/contracts/examples/adder/interactor/src/basic_interactor.rs b/contracts/examples/adder/interactor/src/adder_interactor.rs similarity index 93% rename from contracts/examples/adder/interactor/src/basic_interactor.rs rename to contracts/examples/adder/interactor/src/adder_interactor.rs index 6fd8769634..af6706b1c1 100644 --- a/contracts/examples/adder/interactor/src/basic_interactor.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor.rs @@ -1,4 +1,4 @@ -mod basic_interactor_cli; +mod adder_interactor_cli; use adder::adder_proxy; use clap::Parser; @@ -50,21 +50,21 @@ pub async fn adder_cli() { let mut basic_interact = BasicInteractor::from_config().await; - let cli = basic_interactor_cli::InteractCli::parse(); + let cli = adder_interactor_cli::InteractCli::parse(); match &cli.command { - Some(basic_interactor_cli::InteractCliCommand::Deploy) => { + Some(adder_interactor_cli::InteractCliCommand::Deploy) => { basic_interact.deploy().await; } - Some(basic_interactor_cli::InteractCliCommand::Upgrade(args)) => { + Some(adder_interactor_cli::InteractCliCommand::Upgrade(args)) => { let owner_address = basic_interact.config.owner.address(); basic_interact .upgrade(args.value, &owner_address, None) .await } - Some(basic_interactor_cli::InteractCliCommand::Add(args)) => { + Some(adder_interactor_cli::InteractCliCommand::Add(args)) => { basic_interact.add(args.value).await; } - Some(basic_interactor_cli::InteractCliCommand::Sum) => { + Some(adder_interactor_cli::InteractCliCommand::Sum) => { let sum = basic_interact.get_sum().await; println!("sum: {sum}"); } diff --git a/contracts/examples/adder/interactor/src/basic_interactor_cli.rs b/contracts/examples/adder/interactor/src/adder_interactor_cli.rs similarity index 100% rename from contracts/examples/adder/interactor/src/basic_interactor_cli.rs rename to contracts/examples/adder/interactor/src/adder_interactor_cli.rs diff --git a/contracts/examples/adder/interactor/src/adder_interactor_main.rs b/contracts/examples/adder/interactor/src/adder_interactor_main.rs new file mode 100644 index 0000000000..538f55ba07 --- /dev/null +++ b/contracts/examples/adder/interactor/src/adder_interactor_main.rs @@ -0,0 +1,6 @@ +extern crate adder_interactor; + +#[tokio::main] +pub async fn main() { + adder_interactor::adder_cli().await; +} diff --git a/contracts/examples/adder/interactor/src/basic_interactor_main.rs b/contracts/examples/adder/interactor/src/basic_interactor_main.rs deleted file mode 100644 index 72c8291332..0000000000 --- a/contracts/examples/adder/interactor/src/basic_interactor_main.rs +++ /dev/null @@ -1,6 +0,0 @@ -extern crate basic_interactor; - -#[tokio::main] -pub async fn main() { - basic_interactor::adder_cli().await; -} diff --git a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs similarity index 99% rename from contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs rename to contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs index 3303232b6c..06586cd189 100644 --- a/contracts/examples/adder/interactor/tests/basic_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs @@ -1,4 +1,4 @@ -use basic_interactor::{BasicInteractor, Config, GeneralConfig}; +use adder_interactor::{BasicInteractor, Config, GeneralConfig}; use multiversx_sc_snippets::{imports::*, sdk::gateway::SetStateAccount}; use serial_test::serial; diff --git a/framework/meta/src/cmd/template/template_adjuster.rs b/framework/meta/src/cmd/template/template_adjuster.rs index e95d028b34..a9d10ce430 100644 --- a/framework/meta/src/cmd/template/template_adjuster.rs +++ b/framework/meta/src/cmd/template/template_adjuster.rs @@ -131,7 +131,6 @@ impl TemplateAdjuster { let old_snake = self.metadata.name.to_case(Case::Snake); let new_trait = self.target.new_name.to_case(Case::UpperCamel); let old_trait = &self.metadata.contract_trait; - let new_src_file = rs_file_name(&new_snake); let old_wasm = wasm_file_name(&self.metadata.name); let new_wasm = wasm_file_name(&self.target.new_name); let old_mxsc = mxsc_file_name(&self.metadata.name); @@ -149,6 +148,10 @@ impl TemplateAdjuster { Query::simple(old_trait, &new_trait), Query::simple(&format!("{old_snake}::"), &format!("{new_snake}::")), Query::simple(&format!("{old_snake}_proxy"), &format!("{new_snake}_proxy")), + Query::simple( + &format!("{old_snake}_interactor"), + &format!("{new_snake}_interactor"), + ), Query::simple( &as_path(&self.metadata.name), &as_path(&self.target.new_name), @@ -176,7 +179,15 @@ impl TemplateAdjuster { &package_name_expr(&self.metadata.name), &package_name_expr(&self.target.new_name), ), - Query::simple(&self.metadata.src_file, &new_src_file), + Query::simple(&self.metadata.src_file, &rs_file_name(&new_snake)), + Query::simple( + &format!("{old_snake}_interactor.rs"), + &format!("{new_snake}_interactor.rs"), + ), + Query::simple( + &format!("{old_snake}_interactor_main.rs"), + &format!("{new_snake}_interactor_main.rs"), + ), Query::simple( &package_name_expr(&format!("{}-meta", self.metadata.name)), &package_name_expr(&format!("{}-meta", self.target.new_name)), @@ -189,6 +200,10 @@ impl TemplateAdjuster { &package_name_expr(&format!("{}-wasm", self.metadata.name)), &package_name_expr(&format!("{}-wasm", self.target.new_name)), ), + Query::simple( + &package_name_expr(&format!("{}-interactor", self.metadata.name)), + &package_name_expr(&format!("{}-interactor", self.target.new_name)), + ), ], ); From 5fa51fbd140dece71b8227cd3127dfdabdf81bb2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 29 Jun 2026 00:43:44 +0300 Subject: [PATCH 128/195] cs-test-runner main rename --- tools/cs-test-runner/Cargo.toml | 2 +- tools/cs-test-runner/src/{main.rs => cs_test_runner.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tools/cs-test-runner/src/{main.rs => cs_test_runner.rs} (100%) diff --git a/tools/cs-test-runner/Cargo.toml b/tools/cs-test-runner/Cargo.toml index a7ec62b3a2..ff892cb2e9 100644 --- a/tools/cs-test-runner/Cargo.toml +++ b/tools/cs-test-runner/Cargo.toml @@ -7,7 +7,7 @@ authors = ["MultiversX "] [[bin]] name = "cs-test-runner" -path = "src/main.rs" +path = "src/cs_test_runner.rs" [dependencies.multiversx-sc-meta-lib] workspace = true diff --git a/tools/cs-test-runner/src/main.rs b/tools/cs-test-runner/src/cs_test_runner.rs similarity index 100% rename from tools/cs-test-runner/src/main.rs rename to tools/cs-test-runner/src/cs_test_runner.rs From 4d6e2106f0a21db0cbc696a9ef21d637018bb2e6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 01:46:49 +0300 Subject: [PATCH 129/195] imports cleanup --- .../adder/interactor/tests/adder_interactor_cs_test.rs | 6 +++--- .../basic-features/interact/tests/bf_interact_cs_test.rs | 2 +- .../forwarder-interactor/tests/interact_cs_tests.rs | 2 +- .../interactor/tests/payable_interactor_cs_test.rs | 2 +- framework/snippets/src/imports.rs | 4 +++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs index 06586cd189..7995f93317 100644 --- a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs @@ -1,5 +1,5 @@ use adder_interactor::{BasicInteractor, Config, GeneralConfig}; -use multiversx_sc_snippets::{imports::*, sdk::gateway::SetStateAccount}; +use multiversx_sc_snippets::imports::*; use serial_test::serial; fn chain_simulator_config() -> Config { @@ -22,7 +22,7 @@ async fn test_interactor() -> BasicInteractor { BasicInteractor { interactor, config, - state: multiversx_sc_snippets::AutoSave::no_save_default(), + state: AutoSave::no_save_default(), } } @@ -32,7 +32,7 @@ async fn real_chain_reader_interactor() -> BasicInteractor { BasicInteractor { interactor, config, - state: multiversx_sc_snippets::AutoSave::no_save_default(), + state: AutoSave::no_save_default(), } } diff --git a/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs b/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs index 4c32d8d7a5..5690fc79fe 100644 --- a/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs +++ b/contracts/feature-tests/basic-features/interact/tests/bf_interact_cs_test.rs @@ -24,7 +24,7 @@ async fn cs_interactor() -> BasicFeaturesInteract { BasicFeaturesInteract { interactor, wallet_address, - state: multiversx_sc_snippets::AutoSave::no_save_default(), + state: AutoSave::no_save_default(), large_storage_payload: Vec::new(), } } diff --git a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs index 6c01a5beb3..5fb1ca32e0 100644 --- a/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs +++ b/contracts/feature-tests/composability/forwarder-interactor/tests/interact_cs_tests.rs @@ -23,7 +23,7 @@ async fn cs_interactor() -> ContractInteract { "mxsc:../forwarder/output/forwarder.mxsc.json", &InterpreterContext::default(), ), - state: multiversx_sc_snippets::AutoSave::no_save_default(), + state: AutoSave::no_save_default(), } } diff --git a/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs b/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs index cb041e8388..623cfe6909 100644 --- a/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs +++ b/contracts/feature-tests/payable-features/interactor/tests/payable_interactor_cs_test.rs @@ -19,7 +19,7 @@ async fn cs_interactor() -> PayableInteract { PayableInteract { interactor, config, - state: multiversx_sc_snippets::AutoSave::no_save_default(), + state: AutoSave::no_save_default(), } } diff --git a/framework/snippets/src/imports.rs b/framework/snippets/src/imports.rs index 435434e977..faaf47e41b 100644 --- a/framework/snippets/src/imports.rs +++ b/framework/snippets/src/imports.rs @@ -6,7 +6,9 @@ pub use crate::{ }; pub use crate::config::*; -pub use crate::sdk::{test_wallets, validator::Validator, wallet::Wallet}; +pub use crate::sdk::{ + gateway::SetStateAccount, test_wallets, validator::Validator, wallet::Wallet, +}; pub use env_logger; From 2466d39ee1f25e11d576c20d25d6c125d387c180 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 01:51:22 +0300 Subject: [PATCH 130/195] cs-test-runner fix --- tools/cs-test-runner/src/cs_test_runner.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cs-test-runner/src/cs_test_runner.rs b/tools/cs-test-runner/src/cs_test_runner.rs index b2ae8d33a9..e631198392 100644 --- a/tools/cs-test-runner/src/cs_test_runner.rs +++ b/tools/cs-test-runner/src/cs_test_runner.rs @@ -23,8 +23,8 @@ static CS_TESTS: &[CsTest] = &[ }, CsTest { build_paths: &["contracts/examples/adder"], - package: "basic-interactor", - test_file: "basic_interactor_cs_test", + package: "adder-interactor", + test_file: "adder_interactor_cs_test", }, CsTest { build_paths: &["contracts/examples/ping-pong-egld"], From ce49b3f1b7ae1e9a105da645437db8d8e615f3a8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 13:27:16 +0300 Subject: [PATCH 131/195] interactor - chain type refactor, docs --- framework/snippets/src/config.rs | 2 ++ framework/snippets/src/config/chain_type.rs | 10 ++++++++++ framework/snippets/src/config/connection_config.rs | 7 +------ 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 framework/snippets/src/config/chain_type.rs diff --git a/framework/snippets/src/config.rs b/framework/snippets/src/config.rs index 47e8ae3393..1461c5cc9a 100644 --- a/framework/snippets/src/config.rs +++ b/framework/snippets/src/config.rs @@ -1,10 +1,12 @@ mod auto_save; +mod chain_type; mod config_path; mod connection_config; mod interactor_config; mod wallet_config; pub use auto_save::*; +pub use chain_type::ChainType; pub use config_path::ConfigPath; pub use connection_config::ConnectionConfig; pub use interactor_config::{InteractorConfig, load_toml_config}; diff --git a/framework/snippets/src/config/chain_type.rs b/framework/snippets/src/config/chain_type.rs new file mode 100644 index 0000000000..e77edf3f8c --- /dev/null +++ b/framework/snippets/src/config/chain_type.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +/// Specifies to an interactor whether or not to activate additional chain simulator features, +/// e.g. the ability to set state for accounts, generate blocks, etc. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ChainType { + Real, + Simulator, +} diff --git a/framework/snippets/src/config/connection_config.rs b/framework/snippets/src/config/connection_config.rs index 135dfcdac3..335f9f4be5 100644 --- a/framework/snippets/src/config/connection_config.rs +++ b/framework/snippets/src/config/connection_config.rs @@ -1,11 +1,6 @@ use serde::Deserialize; -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ChainType { - Real, - Simulator, -} +use super::ChainType; /// Configuration for connecting to the MultiversX network. #[derive(Debug, Deserialize)] From a47f8a2e517326217d38e97d85ef61949916fc7e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 13:42:18 +0300 Subject: [PATCH 132/195] interactor set state tests moved to multiversx-sc-snippets --- Cargo.lock | 1 + .../tests/adder_interactor_cs_test.rs | 182 +----------------- framework/snippets/.gitignore | 2 + framework/snippets/Cargo.toml | 4 + framework/snippets/tests/set_state_cs_test.rs | 156 +++++++++++++++ tools/cs-test-runner/src/cs_test_runner.rs | 18 +- 6 files changed, 176 insertions(+), 187 deletions(-) create mode 100644 framework/snippets/.gitignore create mode 100644 framework/snippets/tests/set_state_cs_test.rs diff --git a/Cargo.lock b/Cargo.lock index 29db4d1021..5b389cf269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3367,6 +3367,7 @@ dependencies = [ "pathdiff", "serde", "serde_json", + "serial_test", "tokio", "toml", ] diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs index 7995f93317..88b91a3c88 100644 --- a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs @@ -26,20 +26,10 @@ async fn test_interactor() -> BasicInteractor { } } -async fn real_chain_reader_interactor() -> BasicInteractor { - let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); - let config: Config = interactor.load_config_toml().await; - BasicInteractor { - interactor, - config, - state: AutoSave::no_save_default(), - } -} - #[tokio::test] #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] -async fn simulator_upgrade_test() { +async fn deploy_add_upgrade_test() { let mut basic_interact = test_interactor().await; basic_interact.generate_blocks(2).await; @@ -81,173 +71,3 @@ async fn simulator_upgrade_test() { let sum = basic_interact.get_sum().await; assert_eq!(sum, 7u32.into()); } - -#[tokio::test] -#[serial] -#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] -async fn set_state_cs_test() { - let account_address = test_wallets::mike(); - - let real_chain_interact = real_chain_reader_interactor().await; - let simulator_interact = test_interactor().await; - - let account = real_chain_interact - .interactor - .get_account(&account_address.to_address()) - .await; - let pairs = real_chain_interact - .interactor - .get_account_storage(&account_address.to_address()) - .await; - - let set_state_account = SetStateAccount::from(account).with_storage(pairs); - let vec_state = vec![set_state_account]; - - let set_state_response = simulator_interact.interactor.set_state(vec_state).await; - - simulator_interact - .interactor - .generate_blocks(2u64) - .await - .unwrap(); - - assert!(set_state_response.is_ok()); - - let storage = simulator_interact - .interactor - .get_account_storage(&account_address.to_address()) - .await; - - assert!(storage.len() > 1); - - println!("mike's storage keys in chain simulator {:#?}", storage); -} - -#[tokio::test] -#[serial] -#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] -async fn set_state_from_file_cs_test() { - let account_address = test_wallets::mike(); - let account_address_2 = test_wallets::ivan(); - - let mut real_chain_interact = real_chain_reader_interactor().await; - let simulator_interact = test_interactor().await; - - // now we should have current mike account in the set state file - real_chain_interact - .interactor - .retrieve_account(&Bech32Address::from(&account_address.to_address())) - .await; - - real_chain_interact - .interactor - .retrieve_account(&Bech32Address::from(&account_address_2.to_address())) - .await; - - let set_state_response = simulator_interact - .interactor - .set_state_for_saved_accounts() - .await; - - simulator_interact - .interactor - .generate_blocks(2u64) - .await - .unwrap(); - - assert!(set_state_response.is_ok()); - - let storage = simulator_interact - .interactor - .get_account_storage(&account_address.to_address()) - .await; - - assert!(storage.len() > 1); - - println!("mike's storage keys in chain simulator {:#?}", storage); -} - -#[tokio::test] -#[serial] -#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] -async fn set_state_overwrite_cs_test() { - let account_address = test_wallets::mike(); - let account_address_2 = test_wallets::ivan(); - - let mut real_chain_interact = real_chain_reader_interactor().await; - let simulator_interact = test_interactor().await; - - // now we should have current mike and ivan accounts in the set state file - real_chain_interact - .interactor - .retrieve_account(&Bech32Address::from(&account_address.to_address())) - .await; - - real_chain_interact - .interactor - .retrieve_account(&Bech32Address::from(&account_address_2.to_address())) - .await; - - let set_state_response = simulator_interact - .interactor - .set_state_for_saved_accounts() - .await; - - simulator_interact - .interactor - .generate_blocks(2u64) - .await - .unwrap(); - - assert!(set_state_response.is_ok()); - - let storage = simulator_interact - .interactor - .get_account_storage(&account_address.to_address()) - .await; - - assert!(storage.len() > 1); - - println!("mike's storage keys in chain simulator {:#?}", storage); - - // overwrite accounts with empty - let account_1 = SetStateAccount::from_address( - Bech32Address::from(&account_address.to_address()).to_bech32_string(), - ); - let account_2 = SetStateAccount::from_address( - Bech32Address::from(&account_address_2.to_address()).to_bech32_string(), - ); - - let overwrite_vec = vec![account_1, account_2]; - - simulator_interact - .interactor - .set_state_overwrite(overwrite_vec) - .await - .unwrap(); - - simulator_interact - .interactor - .generate_blocks(2u64) - .await - .unwrap(); - - // verify keys - let storage_1 = simulator_interact - .interactor - .get_account_storage(&account_address.to_address()) - .await; - - assert!(storage_1.is_empty()); - - println!("mike's storage keys in chain simulator {:#?}", storage_1); - - let storage_2 = simulator_interact - .interactor - .get_account_storage(&account_address.to_address()) - .await; - - assert!(storage_2.is_empty()); - - println!("ivan's storage keys in chain simulator {:#?}", storage_2); -} diff --git a/framework/snippets/.gitignore b/framework/snippets/.gitignore new file mode 100644 index 0000000000..8867508db8 --- /dev/null +++ b/framework/snippets/.gitignore @@ -0,0 +1,2 @@ +# Test output +set_state.json diff --git a/framework/snippets/Cargo.toml b/framework/snippets/Cargo.toml index d0a78ff91c..6e87161487 100644 --- a/framework/snippets/Cargo.toml +++ b/framework/snippets/Cargo.toml @@ -17,6 +17,7 @@ categories = ["cryptography::cryptocurrencies"] default = ["http"] http = ["multiversx-sdk-http", "tokio"] dapp = ["multiversx-sdk-dapp"] +chain-simulator-tests = [] [dependencies] hex = "0.4" @@ -54,3 +55,6 @@ default-features = false version = "=0.16.1" path = "../../sdk/dapp" optional = true + +[dev-dependencies] +serial_test = "3.2" diff --git a/framework/snippets/tests/set_state_cs_test.rs b/framework/snippets/tests/set_state_cs_test.rs new file mode 100644 index 0000000000..19777227ef --- /dev/null +++ b/framework/snippets/tests/set_state_cs_test.rs @@ -0,0 +1,156 @@ +// use adder_interactor::{BasicInteractor, Config, GeneralConfig}; +use multiversx_sc_snippets::imports::*; +use serial_test::serial; + +async fn cs_interactor() -> HttpInteractor { + HttpInteractor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&ConnectionConfig::chain_simulator()) + .await +} + +async fn real_chain_reader_interactor() -> HttpInteractor { + HttpInteractor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&ConnectionConfig { + gateway_uri: "https://devnet-gateway.multiversx.com".to_owned(), + chain_type: ChainType::Real, + }) + .await +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn set_state_cs_test() { + let account_address = test_wallets::mike(); + + let real_chain_interact = real_chain_reader_interactor().await; + let simulator_interact = cs_interactor().await; + + let account = real_chain_interact + .get_account(&account_address.to_address()) + .await; + let pairs = real_chain_interact + .get_account_storage(&account_address.to_address()) + .await; + + let set_state_account = SetStateAccount::from(account).with_storage(pairs); + let vec_state = vec![set_state_account]; + + let set_state_response = simulator_interact.set_state(vec_state).await; + + simulator_interact.generate_blocks(2u64).await.unwrap(); + + assert!(set_state_response.is_ok()); + + let storage = simulator_interact + .get_account_storage(&account_address.to_address()) + .await; + + assert!(storage.len() > 1); + + println!("mike's storage keys in chain simulator {:#?}", storage); +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn set_state_from_file_cs_test() { + let account_address = test_wallets::mike(); + let account_address_2 = test_wallets::ivan(); + + let mut real_chain_interact = real_chain_reader_interactor().await; + let simulator_interact = cs_interactor().await; + + // now we should have current mike account in the set state file + real_chain_interact + .retrieve_account(&Bech32Address::from(&account_address.to_address())) + .await; + + real_chain_interact + .retrieve_account(&Bech32Address::from(&account_address_2.to_address())) + .await; + + let set_state_response = simulator_interact.set_state_for_saved_accounts().await; + + simulator_interact.generate_blocks(2u64).await.unwrap(); + + assert!(set_state_response.is_ok()); + + let storage = simulator_interact + .get_account_storage(&account_address.to_address()) + .await; + + assert!(storage.len() > 1); + + println!("mike's storage keys in chain simulator {:#?}", storage); +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn set_state_overwrite_cs_test() { + let account_address = test_wallets::mike(); + let account_address_2 = test_wallets::ivan(); + + let mut real_chain_interact = real_chain_reader_interactor().await; + let simulator_interact = cs_interactor().await; + + // now we should have current mike and ivan accounts in the set state file + real_chain_interact + .retrieve_account(&Bech32Address::from(&account_address.to_address())) + .await; + + real_chain_interact + .retrieve_account(&Bech32Address::from(&account_address_2.to_address())) + .await; + + let set_state_response = simulator_interact.set_state_for_saved_accounts().await; + + simulator_interact.generate_blocks(2u64).await.unwrap(); + + assert!(set_state_response.is_ok()); + + let storage = simulator_interact + .get_account_storage(&account_address.to_address()) + .await; + + assert!(storage.len() > 1); + + println!("mike's storage keys in chain simulator {:#?}", storage); + + // overwrite accounts with empty + let account_1 = SetStateAccount::from_address( + Bech32Address::from(&account_address.to_address()).to_bech32_string(), + ); + let account_2 = SetStateAccount::from_address( + Bech32Address::from(&account_address_2.to_address()).to_bech32_string(), + ); + + let overwrite_vec = vec![account_1, account_2]; + + simulator_interact + .set_state_overwrite(overwrite_vec) + .await + .unwrap(); + + simulator_interact.generate_blocks(2u64).await.unwrap(); + + // verify keys + let storage_1 = simulator_interact + .get_account_storage(&account_address.to_address()) + .await; + + assert!(storage_1.is_empty()); + + println!("mike's storage keys in chain simulator {:#?}", storage_1); + + let storage_2 = simulator_interact + .get_account_storage(&account_address.to_address()) + .await; + + assert!(storage_2.is_empty()); + + println!("ivan's storage keys in chain simulator {:#?}", storage_2); +} diff --git a/tools/cs-test-runner/src/cs_test_runner.rs b/tools/cs-test-runner/src/cs_test_runner.rs index e631198392..468f1039f9 100644 --- a/tools/cs-test-runner/src/cs_test_runner.rs +++ b/tools/cs-test-runner/src/cs_test_runner.rs @@ -21,6 +21,18 @@ static CS_TESTS: &[CsTest] = &[ package: "multiversx-sc-meta", test_file: "cs_tx_cli_test", }, + // Tests framework functionality directly, no contracts + CsTest { + build_paths: &[], + package: "multiversx-sc-snippets", + test_file: "set_state_cs_test", + }, + // Tests built-in system contracts — no wasm build step required. + CsTest { + build_paths: &[], + package: "system-sc-interact", + test_file: "chain_simulator_token_tests", + }, CsTest { build_paths: &["contracts/examples/adder"], package: "adder-interactor", @@ -46,12 +58,6 @@ static CS_TESTS: &[CsTest] = &[ package: "payable-interactor", test_file: "payable_interactor_cs_test", }, - // Tests built-in system contracts — no wasm build step required. - CsTest { - build_paths: &[], - package: "system-sc-interact", - test_file: "chain_simulator_token_tests", - }, ]; fn build_contract(path: &Path, cwd: &Path) -> bool { From 8e49c9457f29c921c64042192e164466a8df38ed Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 13:49:26 +0300 Subject: [PATCH 133/195] adder interactor refactor --- .../adder/interactor/src/adder_interactor.rs | 18 +++++----- .../tests/adder_interactor_cs_test.rs | 36 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/contracts/examples/adder/interactor/src/adder_interactor.rs b/contracts/examples/adder/interactor/src/adder_interactor.rs index af6706b1c1..a09854b2ea 100644 --- a/contracts/examples/adder/interactor/src/adder_interactor.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor.rs @@ -48,42 +48,42 @@ impl State { pub async fn adder_cli() { env_logger::init(); - let mut basic_interact = BasicInteractor::from_config().await; + let mut adder_interact = AdderInteractor::from_config().await; let cli = adder_interactor_cli::InteractCli::parse(); match &cli.command { Some(adder_interactor_cli::InteractCliCommand::Deploy) => { - basic_interact.deploy().await; + adder_interact.deploy().await; } Some(adder_interactor_cli::InteractCliCommand::Upgrade(args)) => { - let owner_address = basic_interact.config.owner.address(); - basic_interact + let owner_address = adder_interact.config.owner.address(); + adder_interact .upgrade(args.value, &owner_address, None) .await } Some(adder_interactor_cli::InteractCliCommand::Add(args)) => { - basic_interact.add(args.value).await; + adder_interact.add(args.value).await; } Some(adder_interactor_cli::InteractCliCommand::Sum) => { - let sum = basic_interact.get_sum().await; + let sum = adder_interact.get_sum().await; println!("sum: {sum}"); } None => {} } } -pub struct BasicInteractor { +pub struct AdderInteractor { pub interactor: Interactor, pub config: Config, pub state: AutoSave, } -impl BasicInteractor { +impl AdderInteractor { pub async fn from_config() -> Self { let mut interactor = Interactor::empty().with_current_dir(env!("CARGO_MANIFEST_DIR")); let config: Config = interactor.load_config_toml().await; let state = interactor.load_state::(); - BasicInteractor { + AdderInteractor { interactor, config, state, diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs index 88b91a3c88..956ea66e27 100644 --- a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs @@ -1,4 +1,4 @@ -use adder_interactor::{BasicInteractor, Config, GeneralConfig}; +use adder_interactor::{AdderInteractor, Config, GeneralConfig}; use multiversx_sc_snippets::imports::*; use serial_test::serial; @@ -13,13 +13,13 @@ fn chain_simulator_config() -> Config { } } -async fn test_interactor() -> BasicInteractor { +async fn test_interactor() -> AdderInteractor { let config = chain_simulator_config(); let interactor = Interactor::empty() .with_current_dir(env!("CARGO_MANIFEST_DIR")) .with_config(&config) .await; - BasicInteractor { + AdderInteractor { interactor, config, state: AutoSave::no_save_default(), @@ -30,44 +30,44 @@ async fn test_interactor() -> BasicInteractor { #[serial] #[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] async fn deploy_add_upgrade_test() { - let mut basic_interact = test_interactor().await; + let mut adder_interact = test_interactor().await; - basic_interact.generate_blocks(2).await; + adder_interact.generate_blocks(2).await; - basic_interact.deploy().await; + adder_interact.deploy().await; - basic_interact.generate_blocks(2).await; + adder_interact.generate_blocks(2).await; - basic_interact.add(1u32).await; + adder_interact.add(1u32).await; - basic_interact.generate_blocks(2).await; + adder_interact.generate_blocks(2).await; // Sum will be 1 - let sum = basic_interact.get_sum().await; + let sum = adder_interact.get_sum().await; assert_eq!(sum, 1u32.into()); - basic_interact - .upgrade(7u32, &basic_interact.config.owner.address(), None) + adder_interact + .upgrade(7u32, &adder_interact.config.owner.address(), None) .await; - basic_interact.generate_blocks(2).await; + adder_interact.generate_blocks(2).await; // Sum will be the updated value of 7 - let sum = basic_interact.get_sum().await; + let sum = adder_interact.get_sum().await; assert_eq!(sum, 7u32.into()); // Upgrade fails - basic_interact + adder_interact .upgrade( 10u32, - &basic_interact.config.wallet.address(), + &adder_interact.config.wallet.address(), Some("upgrade is allowed only for owner"), ) .await; - basic_interact.generate_blocks(2).await; + adder_interact.generate_blocks(2).await; // Sum will remain 7 - let sum = basic_interact.get_sum().await; + let sum = adder_interact.get_sum().await; assert_eq!(sum, 7u32.into()); } From 3d61a11ccffffa4306f8d8cd3cf03d1afbcef751 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 17:24:55 +0300 Subject: [PATCH 134/195] dependency upgrade --- Cargo.lock | 590 ++++++++++-------------- chain/core/Cargo.toml | 2 +- chain/vm/Cargo.toml | 2 +- framework/meta-lib/Cargo.toml | 4 +- framework/scenario/Cargo.toml | 2 +- sdk/core/Cargo.toml | 2 +- sdk/dapp/Cargo.toml | 2 +- sdk/http/Cargo.toml | 2 +- sdk/scenario-format/Cargo.toml | 2 +- tools/mxpy-snippet-generator/Cargo.toml | 2 +- 10 files changed, 242 insertions(+), 368 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b389cf269..e0f8dacc52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ dependencies = [ name = "abi-tester" version = "0.0.0" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "multiversx-sc", "multiversx-sc-meta-lib", "multiversx-sc-scenario", @@ -174,15 +174,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "atomic-waker" @@ -198,9 +198,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -208,14 +208,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -279,9 +280,9 @@ dependencies = [ [[package]] name = "bech32" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +checksum = "efbd3e1070bbdf4cd88a75264e18e8a26f7cb5c6949eadf0ceb85fb159cf08f8" [[package]] name = "benchmark-common" @@ -322,7 +323,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -333,7 +334,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex 1.3.0", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -351,9 +352,9 @@ dependencies = [ [[package]] name = "bitcoin_hashes" -version = "0.14.100" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "hex-conservative", ] @@ -366,9 +367,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "blake2" @@ -390,9 +391,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -416,12 +417,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -488,7 +489,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -499,15 +500,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cc" -version = "1.2.63" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -538,9 +539,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -588,7 +589,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "crypto-common 0.2.2", "inout", ] @@ -635,7 +636,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1016,7 +1017,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1039,7 +1040,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1050,7 +1051,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1067,6 +1068,38 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "delegation-sc-interact" version = "0.0.0" @@ -1096,7 +1129,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1118,7 +1151,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -1139,7 +1172,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", @@ -1169,7 +1202,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1184,13 +1217,13 @@ version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d36219658beb39702975c707dee7895943ca281ca46eebbc5ea395171b9c182b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "lazy_static", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1202,7 +1235,7 @@ dependencies = [ "byteorder", "dynasm", "fnv", - "memmap2 0.9.10", + "memmap2 0.9.11", ] [[package]] @@ -1305,14 +1338,14 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "env_filter" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -1320,9 +1353,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -1573,12 +1606,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1780,7 +1807,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1869,17 +1896,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", "wasm-bindgen", ] @@ -2032,7 +2057,7 @@ dependencies = [ "futures-core", "futures-sink", "gloo-utils 0.3.0", - "http 1.4.1", + "http 1.4.2", "js-sys", "pin-project", "serde", @@ -2134,7 +2159,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2152,16 +2177,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.4.1", + "http 1.4.2", "indexmap", "slab", "tokio", @@ -2178,22 +2203,13 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash 0.2.0", + "foldhash", "serde", "serde_core", ] @@ -2253,9 +2269,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2268,7 +2284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.1", + "http 1.4.2", ] [[package]] @@ -2279,7 +2295,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.1", + "http 1.4.2", "http-body", "pin-project-lite", ] @@ -2292,9 +2308,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] @@ -2310,7 +2326,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.1", + "http 1.4.2", "http-body", "httparse", "itoa", @@ -2326,7 +2342,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.1", + "http 1.4.2", "hyper", "hyper-util", "rustls", @@ -2345,7 +2361,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.1", + "http 1.4.2", "http-body", "hyper", "ipnet", @@ -2441,12 +2457,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2476,9 +2486,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" dependencies = [ "crossbeam-deque", "globset", @@ -2507,7 +2517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2554,9 +2564,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] @@ -2569,10 +2579,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.28" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" dependencies = [ + "defmt", "jiff-static", "log", "portable-atomic", @@ -2582,13 +2593,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.28" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2618,7 +2629,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2637,7 +2648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2652,13 +2663,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2759,9 +2769,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" [[package]] name = "leb128fmt" @@ -2847,9 +2857,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lottery" @@ -2957,9 +2967,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -2972,9 +2982,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -3117,7 +3127,7 @@ version = "0.23.1" dependencies = [ "base64", "bech32", - "bitflags 2.11.1", + "bitflags 2.13.0", "blake2", "ed25519-dalek", "hex", @@ -3147,10 +3157,10 @@ name = "multiversx-chain-vm" version = "0.23.1" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "colored 3.1.1", "hex", - "itertools 0.14.0", + "itertools 0.15.0", "log", "multiversx-bls", "multiversx-chain-core", @@ -3192,7 +3202,7 @@ dependencies = [ name = "multiversx-price-aggregator-sc" version = "0.66.2" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "multiversx-sc", "multiversx-sc-modules", "multiversx-sc-scenario", @@ -3212,7 +3222,7 @@ dependencies = [ name = "multiversx-sc" version = "0.66.2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "generic-array 1.4.3", "multiversx-chain-core", "multiversx-sc-codec", @@ -3226,7 +3236,7 @@ name = "multiversx-sc-codec" version = "0.25.0" dependencies = [ "arrayvec", - "bitflags 2.11.1", + "bitflags 2.13.0", "multiversx-sc-codec-derive", "num-bigint", "num-traits", @@ -3240,7 +3250,7 @@ dependencies = [ "hex", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3259,7 +3269,7 @@ dependencies = [ "proc-macro2", "quote", "radix_trie", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3313,7 +3323,7 @@ dependencies = [ "serde", "serde_json", "toml", - "wasmparser 0.249.0", + "wasmparser 0.252.0", "wasmprinter", "wat", ] @@ -3332,7 +3342,7 @@ dependencies = [ "colored 3.1.1", "hex", "hex-literal", - "itertools 0.14.0", + "itertools 0.15.0", "log", "multiversx-chain-scenario-format", "multiversx-chain-vm", @@ -3389,7 +3399,7 @@ dependencies = [ "ctr", "hex", "hmac", - "itertools 0.14.0", + "itertools 0.15.0", "log", "multiversx-chain-core", "pbkdf2", @@ -3408,10 +3418,10 @@ name = "multiversx-sdk-dapp" version = "0.16.1" dependencies = [ "anyhow", - "getrandom 0.4.2", + "getrandom 0.4.3", "gloo-net 0.7.0", "hex", - "itertools 0.14.0", + "itertools 0.15.0", "js-sys", "log", "multiversx-sdk", @@ -3426,7 +3436,7 @@ version = "0.16.1" dependencies = [ "anyhow", "hex", - "itertools 0.14.0", + "itertools 0.15.0", "log", "multiversx-sdk", "reqwest", @@ -3469,7 +3479,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3803,7 +3813,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3876,6 +3886,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3916,7 +3932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3972,7 +3988,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4070,7 +4086,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4092,9 +4108,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -4112,9 +4128,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", @@ -4148,9 +4164,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -4214,7 +4230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -4327,14 +4343,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -4355,9 +4371,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "region" @@ -4393,7 +4409,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 1.4.1", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -4480,7 +4496,7 @@ checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4596,7 +4612,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -4605,9 +4621,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", @@ -4619,9 +4635,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -4786,7 +4802,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4886,7 +4902,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4911,7 +4927,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4957,7 +4973,7 @@ checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5104,9 +5120,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" @@ -5211,9 +5227,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -5237,7 +5253,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5246,7 +5262,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5324,7 +5340,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5335,7 +5351,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5423,7 +5439,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5554,10 +5570,10 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-util", - "http 1.4.1", + "http 1.4.2", "http-body", "pin-project-lite", "tower", @@ -5597,7 +5613,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5672,9 +5688,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -5750,11 +5766,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -5866,23 +5882,14 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -5893,9 +5900,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5903,9 +5910,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5913,56 +5920,34 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.244.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.251.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" -dependencies = [ - "leb128fmt", - "wasmparser 0.251.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder 0.244.0", - "wasmparser 0.244.0", + "wasmparser 0.252.0", ] [[package]] @@ -6114,74 +6099,51 @@ version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", + "bitflags 2.13.0", ] [[package]] name = "wasmparser" -version = "0.249.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30538cae9a794215f490b532df01c557e2e2bfac92569482554acd0992a102ea" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "hashbrown 0.17.1", "indexmap", "semver", "serde", ] -[[package]] -name = "wasmparser" -version = "0.251.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" -dependencies = [ - "bitflags 2.11.1", - "indexmap", - "semver", -] - [[package]] name = "wasmprinter" -version = "0.249.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e227e800fc74c0d60117391f71d60e4c956333cfc3be796ae72d269002aa52" +checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.249.0", + "wasmparser 0.252.0", ] [[package]] name = "wast" -version = "251.0.0" +version = "252.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc7467dda0a96142eb2c980329dfb62480b1e1d3622fdeb1a44e2bca6ceed74" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.251.0", + "wasm-encoder", ] [[package]] name = "wat" -version = "1.251.0" +version = "1.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b1086c9e85b95bd6a229a928bc6c6d0662e42af0250c88d067b418831ea4d4" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" dependencies = [ "wast", ] @@ -6196,9 +6158,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -6216,9 +6178,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -6519,94 +6481,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.244.0", - "wasm-metadata", - "wasmparser 0.244.0", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.244.0", -] - [[package]] name = "writeable" version = "0.6.3" @@ -6665,7 +6539,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6695,14 +6569,14 @@ checksum = "8399f1d134ab8e69abc7cded19f114621d520bd9c79c5a0f34091bf664d7325b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6717,28 +6591,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6758,15 +6632,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -6798,7 +6672,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6817,9 +6691,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" [[package]] name = "zmij" diff --git a/chain/core/Cargo.toml b/chain/core/Cargo.toml index 2183acc7fb..7d58a6d5d9 100644 --- a/chain/core/Cargo.toml +++ b/chain/core/Cargo.toml @@ -22,7 +22,7 @@ std = ["base64", "bech32", "serde", "hex", "blake2", "sha2", "sha3", "ed25519-da [dependencies] bitflags = "2.9" base64 = { version = "0.22", optional = true } -bech32 = { version = "0.11", optional = true } +bech32 = { version = "0.12", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } hex = { version = "0.4", optional = true } blake2 = { version = "0.10", optional = true } diff --git a/chain/vm/Cargo.toml b/chain/vm/Cargo.toml index f8369a07d9..30243f73fc 100644 --- a/chain/vm/Cargo.toml +++ b/chain/vm/Cargo.toml @@ -27,7 +27,7 @@ wasmer-experimental = ["multiversx-chain-vm-executor-wasmer-experimental"] num-bigint = "0.4" num-traits = "0.2" hex = "0.4" -itertools = "0.14" +itertools = "0.15" bitflags = "2.9" colored = "3.0" rand = { version = "0.10", optional = true } diff --git a/framework/meta-lib/Cargo.toml b/framework/meta-lib/Cargo.toml index 650207888a..4c8d773701 100644 --- a/framework/meta-lib/Cargo.toml +++ b/framework/meta-lib/Cargo.toml @@ -26,8 +26,8 @@ colored = "3.0" lazy_static = "1.4.0" convert_case = "0.11" hex = "0.4" -wasmparser = "0.249" -wasmprinter = "0.249" +wasmparser = "0.252" +wasmprinter = "0.252" semver = "1.0.20" wat = "1.217.0" diff --git a/framework/scenario/Cargo.toml b/framework/scenario/Cargo.toml index ff68ca20ea..13b85cebbe 100644 --- a/framework/scenario/Cargo.toml +++ b/framework/scenario/Cargo.toml @@ -35,7 +35,7 @@ log = { version = "0.4", features = ["std"] } serde = "1.0" serde_json = "1.0" pathdiff = "0.2" -itertools = "0.14" +itertools = "0.15" colored = "3.0" unwrap-infallible = "1.0" simple-error = "0.3" diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 1eb60008ed..af3940cc51 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -27,7 +27,7 @@ hmac = "0.13" hex = "0.4" pbkdf2 = { version = "0.13", default-features = false } zeroize = "1.4" -itertools = "0.14" +itertools = "0.15" pem = "3.0" log = "0.4" scrypt = { version = "0.12", default-features = false } diff --git a/sdk/dapp/Cargo.toml b/sdk/dapp/Cargo.toml index a995548506..3dc0354682 100644 --- a/sdk/dapp/Cargo.toml +++ b/sdk/dapp/Cargo.toml @@ -22,7 +22,7 @@ wasm-bindgen-futures = { version = "0.4" } gloo-net = { version = "0.7" } anyhow = "1.0" hex = "0.4" -itertools = "0.14" +itertools = "0.15" log = "0.4" [dependencies.multiversx-sdk] diff --git a/sdk/http/Cargo.toml b/sdk/http/Cargo.toml index 30f5f4878f..03656322ef 100644 --- a/sdk/http/Cargo.toml +++ b/sdk/http/Cargo.toml @@ -23,7 +23,7 @@ tokio = { version = "1.24", features = ["full"] } reqwest = { version = "0.13", features = ["rustls", "blocking", "json"], default-features = false } anyhow = "1.0" hex = "0.4" -itertools = "0.14" +itertools = "0.15" log = "0.4" serde_json = "1.0" diff --git a/sdk/scenario-format/Cargo.toml b/sdk/scenario-format/Cargo.toml index 09b46cdcbe..a5d1a4ab50 100644 --- a/sdk/scenario-format/Cargo.toml +++ b/sdk/scenario-format/Cargo.toml @@ -20,4 +20,4 @@ num-bigint = "0.4" num-traits = "0.2" hex = "0.4" sha3 = "0.12" -bech32 = "0.11" +bech32 = "0.12" diff --git a/tools/mxpy-snippet-generator/Cargo.toml b/tools/mxpy-snippet-generator/Cargo.toml index de8c05482a..181b80a176 100644 --- a/tools/mxpy-snippet-generator/Cargo.toml +++ b/tools/mxpy-snippet-generator/Cargo.toml @@ -13,7 +13,7 @@ path = "src/mxpy_snippet_generator.rs" workspace = true [dependencies] -bech32 = "0.11" +bech32 = "0.12" num-bigint = "0.4" num-traits = "0.2" hex = "0.4" From 55ee27d44627f2291f152e6bc1d3e6c2cce4c57b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 30 Jun 2026 18:19:08 +0300 Subject: [PATCH 135/195] QueueMapper leak test, VM account access refactor --- chain/vm/src/blockchain/state/account_data.rs | 12 ++++++++++ .../src/blockchain/state/blockchain_state.rs | 11 ++-------- chain/vm/src/host/vm_hooks/vh_tx_context.rs | 7 +++--- .../src/api/impl_vh/vh_single_tx_api.rs | 7 +++--- .../src/scenario/run_vm/check_state.rs | 10 +++------ .../scenario/tests/single_tx_api_test.rs | 14 ++++-------- framework/scenario/tests/test_queue_mapper.rs | 22 +++++++++++++++++++ 7 files changed, 49 insertions(+), 34 deletions(-) diff --git a/chain/vm/src/blockchain/state/account_data.rs b/chain/vm/src/blockchain/state/account_data.rs index 0c890e55d3..2f9e561d1e 100644 --- a/chain/vm/src/blockchain/state/account_data.rs +++ b/chain/vm/src/blockchain/state/account_data.rs @@ -39,6 +39,18 @@ impl AccountData { developer_rewards: BigUint::zero(), } } + + pub fn storage_get(&self, key: &[u8]) -> Vec { + self.storage.get(key).cloned().unwrap_or_default() + } + + pub fn storage_set(&mut self, key: &[u8], value: &[u8]) { + if value.is_empty() { + self.storage.remove(key); + } else { + self.storage.insert(key.to_vec(), value.to_vec()); + } + } } impl fmt::Display for AccountData { diff --git a/chain/vm/src/blockchain/state/blockchain_state.rs b/chain/vm/src/blockchain/state/blockchain_state.rs index 0df1a0e3b8..6fb44e9d1d 100644 --- a/chain/vm/src/blockchain/state/blockchain_state.rs +++ b/chain/vm/src/blockchain/state/blockchain_state.rs @@ -75,16 +75,9 @@ impl BlockchainState { .get_mut(address) .unwrap_or_else(|| panic!("Account not found: {address}")); account.egld_balance += amount; - let mut storage_v_rew = - if let Some(old_storage_value) = account.storage.get(STORAGE_REWARD_KEY) { - BigUint::from_bytes_be(old_storage_value) - } else { - BigUint::zero() - }; + let mut storage_v_rew = BigUint::from_bytes_be(&account.storage_get(STORAGE_REWARD_KEY)); storage_v_rew += amount; - account - .storage - .insert(STORAGE_REWARD_KEY.to_vec(), storage_v_rew.to_bytes_be()); + account.storage_set(STORAGE_REWARD_KEY, &storage_v_rew.to_bytes_be()); } pub fn put_new_token_identifier(&mut self, token_identifier: String) { diff --git a/chain/vm/src/host/vm_hooks/vh_tx_context.rs b/chain/vm/src/host/vm_hooks/vh_tx_context.rs index f2d5a2b660..a90173dadc 100644 --- a/chain/vm/src/host/vm_hooks/vh_tx_context.rs +++ b/chain/vm/src/host/vm_hooks/vh_tx_context.rs @@ -99,9 +99,8 @@ impl VMHooksContext for TxVMHooksContext { } fn storage_read_any_address(&self, address: &Address, key: &[u8]) -> Vec { - self.tx_context_ref.with_account_mut(address, |account| { - account.storage.get(key).cloned().unwrap_or_default() - }) + self.tx_context_ref + .with_account_mut(address, |account| account.storage_get(key)) } fn storage_write(&mut self, key: &[u8], value: &[u8]) -> Result<(), VMHooksEarlyExit> { @@ -109,7 +108,7 @@ impl VMHooksContext for TxVMHooksContext { self.check_not_readonly()?; self.tx_context_ref.with_contract_account_mut(|account| { - account.storage.insert(key.to_vec(), value.to_vec()); + account.storage_set(key, value); }); Ok(()) } diff --git a/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs b/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs index 573c5c3978..10038e9afc 100644 --- a/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs +++ b/framework/scenario/src/api/impl_vh/vh_single_tx_api.rs @@ -92,14 +92,13 @@ impl VMHooksContext for SingleTxApiVMHooksContext { } fn storage_read_any_address(&self, address: &Address, key: &[u8]) -> Vec { - self.0.with_account_mut(address, |account| { - account.storage.get(key).cloned().unwrap_or_default() - }) + self.0 + .with_account_mut(address, |account| account.storage_get(key)) } fn storage_write(&mut self, key: &[u8], value: &[u8]) -> Result<(), VMHooksEarlyExit> { self.0.with_account_mut(&self.0.tx_input_box.to, |account| { - account.storage.insert(key.to_vec(), value.to_vec()); + account.storage_set(key, value); }); Ok(()) } diff --git a/framework/scenario/src/scenario/run_vm/check_state.rs b/framework/scenario/src/scenario/run_vm/check_state.rs index 2a70013fba..f5bbdc2106 100644 --- a/framework/scenario/src/scenario/run_vm/check_state.rs +++ b/framework/scenario/src/scenario/run_vm/check_state.rs @@ -78,19 +78,15 @@ fn execute(state: &BlockchainState, accounts: &CheckAccounts) { ); if let CheckStorage::Equal(eq) = &expected_account.storage { - let default_value = &Vec::new(); for (expected_key, expected_value) in eq.storages.iter() { - let actual_value = account - .storage - .get(&expected_key.value) - .unwrap_or(default_value); + let actual_value = account.storage_get(&expected_key.value); assert!( - expected_value.check(actual_value), + expected_value.check(&actual_value), "bad storage value. Address: {}. Key: {}. Want: {}. Have: {}", expected_address, expected_key, expected_value, - verbose_hex(actual_value) + verbose_hex(&actual_value) ); } diff --git a/framework/scenario/tests/single_tx_api_test.rs b/framework/scenario/tests/single_tx_api_test.rs index 35bee42e5f..ac38737071 100644 --- a/framework/scenario/tests/single_tx_api_test.rs +++ b/framework/scenario/tests/single_tx_api_test.rs @@ -17,13 +17,11 @@ fn single_tx_api_test() { // check directly in storage SingleTxApi::with_global_default_account(|account| { - let value = account.storage.get(storage_key.as_bytes()).unwrap(); - assert_eq!(value, &vec![5u8]); + let value = account.storage_get(storage_key.as_bytes()); + assert_eq!(value, vec![5u8]); // change value directly in storage - account - .storage - .insert(storage_key.as_bytes().to_vec(), vec![7u8]); + account.storage_set(storage_key.as_bytes(), &[7u8]); }); // read again @@ -37,11 +35,7 @@ fn single_tx_api_test() { // checking directly in storage SingleTxApi::with_global_default_account(|account| { - let value = account - .storage - .get(storage_key.as_bytes()) - .cloned() - .unwrap_or_default(); + let value = account.storage_get(storage_key.as_bytes()); assert!(value.is_empty()); }); } diff --git a/framework/scenario/tests/test_queue_mapper.rs b/framework/scenario/tests/test_queue_mapper.rs index b32e28e3c3..5c91b7d82c 100644 --- a/framework/scenario/tests/test_queue_mapper.rs +++ b/framework/scenario/tests/test_queue_mapper.rs @@ -100,3 +100,25 @@ fn test_queue_clear() { assert_eq!(queue.len(), 0); assert!(queue.is_empty()); } + +#[test] +fn test_queue_clear_no_info_leak() { + SingleTxApi::clear_global(); + + let mut queue = create_queue(); + queue.push_back(1u64); + queue.push_back(2u64); + queue.push_back(3u64); + queue.clear(); + + // After clearing, the ".info" storage slot must be fully deleted (not merely + // reset to a default-encoded value), so no residual key should remain. + let info_key = b"my_queue.info".to_vec(); + let no_residual = SingleTxApi::with_global_default_account(|account| { + account.storage_get(&info_key).is_empty() + }); + assert!( + no_residual, + "QueueMapper::clear() left a residual '.info' storage slot" + ); +} From 052bf861cc955cd653ead885690de64277b112d0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 12 Jul 2026 06:57:26 +0300 Subject: [PATCH 136/195] storage-examples contract --- Cargo.lock | 16 + Cargo.toml | 2 + .../examples/storage-examples/.gitignore | 4 + .../examples/storage-examples/Cargo.toml | 15 + contracts/examples/storage-examples/README.md | 170 +++++++++++ .../examples/storage-examples/meta/Cargo.toml | 11 + .../storage-examples/meta/src/main.rs | 3 + .../examples/storage-examples/multiversx.json | 3 + .../examples/storage-examples/sc-config.toml | 2 + .../storage-examples/src/storage_examples.rs | 131 +++++++++ .../src/storage_examples_proxy.rs | 278 ++++++++++++++++++ .../tests/storage_mappers_blackbox_test.rs | 265 +++++++++++++++++ .../examples/storage-examples/wasm/Cargo.toml | 35 +++ .../examples/storage-examples/wasm/src/lib.rs | 42 +++ 14 files changed, 977 insertions(+) create mode 100644 contracts/examples/storage-examples/.gitignore create mode 100644 contracts/examples/storage-examples/Cargo.toml create mode 100644 contracts/examples/storage-examples/README.md create mode 100644 contracts/examples/storage-examples/meta/Cargo.toml create mode 100644 contracts/examples/storage-examples/meta/src/main.rs create mode 100644 contracts/examples/storage-examples/multiversx.json create mode 100644 contracts/examples/storage-examples/sc-config.toml create mode 100644 contracts/examples/storage-examples/src/storage_examples.rs create mode 100644 contracts/examples/storage-examples/src/storage_examples_proxy.rs create mode 100644 contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs create mode 100644 contracts/examples/storage-examples/wasm/Cargo.toml create mode 100644 contracts/examples/storage-examples/wasm/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e0f8dacc52..071cb70843 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5179,6 +5179,22 @@ dependencies = [ "std-contract", ] +[[package]] +name = "storage-examples" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-scenario", +] + +[[package]] +name = "storage-examples-meta" +version = "0.0.0" +dependencies = [ + "multiversx-sc-meta-lib", + "storage-examples", +] + [[package]] name = "str-repeat" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 68bf4a89ae..1cd22b4cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,8 @@ members = [ "contracts/examples/rewards-distribution/meta", "contracts/examples/seed-nft-minter", "contracts/examples/seed-nft-minter/meta", + "contracts/examples/storage-examples", + "contracts/examples/storage-examples/meta", "contracts/examples/token-release", "contracts/examples/token-release/meta", diff --git a/contracts/examples/storage-examples/.gitignore b/contracts/examples/storage-examples/.gitignore new file mode 100644 index 0000000000..aeba8c9681 --- /dev/null +++ b/contracts/examples/storage-examples/.gitignore @@ -0,0 +1,4 @@ +# Build artifacts — regenerate with `sc-meta all build`. Same convention +# this Cookbook's TypeScript recipes use for node_modules/ and dist/. +target/ +output/ diff --git a/contracts/examples/storage-examples/Cargo.toml b/contracts/examples/storage-examples/Cargo.toml new file mode 100644 index 0000000000..4748f3b7ff --- /dev/null +++ b/contracts/examples/storage-examples/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "storage-examples" +version = "0.0.0" +edition = "2024" +publish = false +authors = ["you"] + +[lib] +path = "src/storage_examples.rs" + +[dependencies.multiversx-sc] +workspace = true + +[dev-dependencies.multiversx-sc-scenario] +workspace = true diff --git a/contracts/examples/storage-examples/README.md b/contracts/examples/storage-examples/README.md new file mode 100644 index 0000000000..6807880295 --- /dev/null +++ b/contracts/examples/storage-examples/README.md @@ -0,0 +1,170 @@ +# Recipe: Storage mappers — which to pick, when + +A working, tested contract exercising six storage mappers side by side, +plus a decision table built from the real `multiversx-sc` crate source +doc comments (`~/.cargo/registry/.../multiversx-sc-0.64.2/src/storage/mappers/*.rs`, +read directly while authoring this recipe) — not just CLAUDE.md's +summary. **Two of CLAUDE.md's own table entries turned out to be wrong** +against that source; see "Corrections to CLAUDE.md's table" below. + +## Prerequisites + +- Rust via `rustup`, with the `wasm32v1-none` target installed. +- `sc-meta` (`cargo install multiversx-sc-meta`). +- Familiarity with [New contract from `sc-meta new --template empty`](/smart-contracts-rust/new-contract-from-template/) — + this recipe assumes that scaffolding workflow and jumps straight to the + mapper comparison. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/storage-mapper-decision-table +sc-meta all build +cargo test +``` + +## The decision table + +| Mapper | Ordering | Membership check | Storage entries for N items | Iterable? | Pick it when | +| --- | --- | --- | --- | --- | --- | +| `SingleValueMapper` | n/a (one value) | n/a | 1 | n/a | You need exactly one value — a counter, a config flag, a total. | +| `VecMapper` | Insertion order | Linear scan only (no built-in `contains`) | N + 1 (`.len` + one `.item{i}` per element) | Yes, 1 to `len()` | You need ordered, indexable, append-friendly storage and don't need fast membership checks — a log, a history, a queue you only ever read front-to-back. **Indexes start at 1, not 0.** | +| `SetMapper` | Insertion order (doubly-linked internally) | **O(1)** — confirmed from source, contra CLAUDE.md (see below) | ~3N + 1 (info + node_links×N + value×N + node_id-lookup×N) | Yes, in insertion order, plus `next()`/`previous()` navigation | You need both ordered iteration AND fast membership checks, and can afford the higher per-element storage cost — an audit trail of unique events, a whitelist where insertion order itself is meaningful. | +| `UnorderedSetMapper` | None | O(1) | 2N + 1 (`.len` + `.item{i}`×N + `.index{value}`×N) — CLAUDE.md says N+1, undercounting the reverse-lookup keys needed for O(1) `contains` (see below) | Yes, arbitrary order | You need fast membership checks and don't care about order — deduping, a processed-IDs set, a role list you only ever check `contains()` against. | +| `WhitelistMapper` | n/a | O(1), most storage-efficient of the set-shaped mappers | N (one boolean-flag key per item, nothing else) | **No** — cannot enumerate members at all | You only ever need "is X allowed?" and never "list everyone allowed" — permission gates, feature flags, admin checks. | +| `MapMapper` | Insertion order of keys (built on `SetMapper` internally) | O(1) via `contains_key()` | ~4N + 1 (a full `SetMapper` for keys, ~3N+1, plus one `.mapped{key}` value entry per item) | Yes — `.iter()`, `.keys()`, `.values()` | You need a real key→value store with iteration — balances, per-user settings, anything you'd reach for a `HashMap` for off-chain. Confirmed as the one CLAUDE.md entry that checks out exactly ("4N+1 entries (expensive!)"). | +| `LinkedListMapper` | Insertion order, efficient front/back ops | Not built in | ~2N + 1 (info + one node key per element) | Yes | You need efficient push/pop from BOTH ends — a FIFO/LIFO combined structure. Not exercised with working code in this recipe (see "What this recipe didn't test" below); described from CLAUDE.md plus the source module's struct layout only. | + +**Storage-cost column is expressed in unique storage keys, not bytes** — +each entry's actual byte cost also depends on the encoded size of `T`. +The point of this column is relative ordering between mapper choices for +the same logical data, not an absolute gas number. + +## Corrections to CLAUDE.md's table + +Read directly from `multiversx-sc-0.64.2`'s source doc comments while +authoring this recipe — CLAUDE.md's own "Available Storage Mappers" +table has two claims that don't match: + +1. **`SetMapper`'s `contains()` is O(1), not O(n).** CLAUDE.md lists + it as `"O(n) contains"`. The real source's doc comment states + plainly: `"Contains: contains(value) - Checks membership. O(1) with + one storage read."`, and lists `"O(1) insert, remove, and contains"` + as a Pro. `SetMapper` maintains a separate value→node-ID lookup + specifically to make `contains()` O(1) — that's the entire reason its + storage layout is more complex than a plain ordered list. This + recipe's `set_mapper_contains_and_ordering` test exercises `contains()` + for both a present and an absent value; functional correctness is + confirmed, though this recipe doesn't independently benchmark + complexity — the O(1) claim itself is the crate's own documented + design intent, not a claim this recipe re-derived from scratch. + +2. **`UnorderedSetMapper` costs `2N + 1` storage entries, not `N + 1`.** + CLAUDE.md's table lists `"N+1 entries"`. The real source's storage + layout section lists two distinct key groups: value storage + (`.len` + `.item{index}` per element — this is the `N + 1` CLAUDE.md + counted) AND a separate `.index{encoded_value}` reverse-lookup key + per element, which is what actually delivers the O(1) `contains()` + both CLAUDE.md and this recipe agree the mapper provides. You cannot + get O(1) membership testing from `N + 1` keys with no reverse + index — the extra `N` keys are the cost of that guarantee, not an + optional extra. + +Everything else in CLAUDE.md's table (that this recipe independently +checked against source) held up, including `MapMapper`'s "4N+1 entries +(expensive!)" — genuinely the most expensive mapper here per element, +confirmed by reading how it's built on top of `SetMapper` internally +plus its own value storage. + +## Files + +`src/storage_mappers.rs` — one endpoint pair per mapper. +Every method name (`.insert()`, `.contains()`, `.contains_key()`, +`.add()`, `.push()`, `.get()`) is copied from the real crate source's own +doc-comment examples, not guessed — worth calling out since, for +instance, `SetMapper`/`UnorderedSetMapper` both use `.contains()` while +`MapMapper` uses `.contains_key()` instead — an easy name to get wrong by +assuming symmetry across mapper types. + +`tests/storage_mappers_blackbox_test.rs` — six tests, one +per mapper, each proving the specific behavioral claim in the table above +(not just "it compiles"): `VecMapper`'s first push really does land at +index 1; `SetMapper`/`UnorderedSetMapper`'s `contains()` really does +return `true`/`false` correctly for present/absent values; +`WhitelistMapper` really has no enumeration method (only checked +implicitly — the endpoint surface here is `contains()`-only by design, +matching the mapper's own "non-iterable" nature); `MapMapper`'s +`get_balance()` on a key that was never inserted returns `0` +(`unwrap_or_default()`), which is indistinguishable from a real zero +balance unless you also check `contains_key()` — a real design trap +worth testing explicitly, not just describing. + +## Pitfalls + +1. **`VecMapper` is 1-indexed. Index 0 is invalid and panics.** Confirmed + directly: `vec_mapper_is_one_indexed` pushes two items and reads back + index 1 as the first one pushed. + +2. **`MapMapper.get()` returning a default-via-`unwrap_or_default()` value + looks identical to a real stored zero.** If "never set" and "set to + zero" need to be distinguishable in your contract, check + `contains_key()` explicitly rather than trusting a default-valued read + — `map_mapper_insert_get_and_contains_key` tests exactly this gap. + +3. **`SetMapper` and `UnorderedSetMapper` both offer `contains()` at + O(1), so the deciding factor between them is ordering and storage + cost, not lookup speed.** Pick `UnorderedSetMapper` unless you + specifically need insertion-order iteration or `next()`/`previous()` + navigation — it's cheaper per element. + +4. **`WhitelistMapper` cannot list its members at all**, not even + inefficiently. If you might ever need to enumerate — even rarely, even + just for an admin dashboard — use `SetMapper` or `UnorderedSetMapper` + instead from the start; there's no way to add enumeration to a + `WhitelistMapper` later without migrating storage. + +5. **Two different mapper types can use different names for what looks + like the same operation.** `SetMapper`/`UnorderedSetMapper.contains()` + vs `MapMapper.contains_key()` — check the exact mapper's own method + names rather than assuming consistency across the family (the same + general caution CLAUDE.md's own pitfall #14 raises for sdk-core's + Controller/Factory casing, here inside sdk-rs instead). + +## What this recipe didn't test + +`LinkedListMapper`, `QueueMapper`, `UserMapper`, `UniqueIdMapper`, and +`BiDiMapper` are in CLAUDE.md's table and are real, exported mapper +types (confirmed present in the crate's module listing), but this recipe +doesn't ship working code exercising them — six mappers with real, +tested endpoints was already substantial scope for one recipe, and the +first six cover the large majority of real contract storage needs per +this Cookbook's own reading of the example contracts in `mx-sdk-rs`. +`FungibleTokenMapper`/`NonFungibleTokenMapper`/`TokenAttributesMapper` +are deliberately out of scope here too — they need real ESDT system +contract interaction to demonstrate meaningfully, which belongs in a +token-issuance-from-a-contract recipe (Wave 3 per V1-RECIPE-LIST.md's +"What v1 explicitly defers"), not a storage-mapper comparison. + +## See also + +- [New contract from `sc-meta new --template empty`](/smart-contracts-rust/new-contract-from-template/) — + the scaffolding this recipe's contract builds on, including the same + `sc-config.toml` / proxy-generation ordering. +- CLAUDE.md §"Storage Mapper Efficiency" and §"Available Storage + Mappers" — the summary this recipe verifies against real source and + partially corrects. + +## Validation + +`cargo check` passed on the first attempt for every mapper's endpoint +code — every method name used was taken directly from the real crate +source's own doc-comment examples rather than guessed first and fixed +later. `sc-meta all build` produces a real WASM + ABI + `.mxsc.json` +(contract size 5,836 bytes). `cargo test` runs 8/8 passing: the 6 +blackbox tests described above, plus the 2 scaffold-provided scenario +tests (unchanged from `sc-meta new --template empty`'s output). No +`npm`/`tsc`/`eslint` gate applies — this is Rust, verified with +`sc-meta`/`cargo` per this Cookbook's TypeScript-vs-Rust verification +split (see PROTOTYPE-NOTES.md's FINAL SUMMARY). `target/`/`output/` are +gitignored (regenerable); `Cargo.lock`/`wasm/Cargo.lock` are kept. diff --git a/contracts/examples/storage-examples/meta/Cargo.toml b/contracts/examples/storage-examples/meta/Cargo.toml new file mode 100644 index 0000000000..5d5dcfa284 --- /dev/null +++ b/contracts/examples/storage-examples/meta/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "storage-examples-meta" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies.storage-examples] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +workspace = true diff --git a/contracts/examples/storage-examples/meta/src/main.rs b/contracts/examples/storage-examples/meta/src/main.rs new file mode 100644 index 0000000000..1fd696d25a --- /dev/null +++ b/contracts/examples/storage-examples/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} diff --git a/contracts/examples/storage-examples/multiversx.json b/contracts/examples/storage-examples/multiversx.json new file mode 100644 index 0000000000..7365539625 --- /dev/null +++ b/contracts/examples/storage-examples/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/examples/storage-examples/sc-config.toml b/contracts/examples/storage-examples/sc-config.toml new file mode 100644 index 0000000000..657417343b --- /dev/null +++ b/contracts/examples/storage-examples/sc-config.toml @@ -0,0 +1,2 @@ +[[proxy]] +path = "src/storage_examples_proxy.rs" diff --git a/contracts/examples/storage-examples/src/storage_examples.rs b/contracts/examples/storage-examples/src/storage_examples.rs new file mode 100644 index 0000000000..1e01e51726 --- /dev/null +++ b/contracts/examples/storage-examples/src/storage_examples.rs @@ -0,0 +1,131 @@ +#![no_std] + +use multiversx_sc::imports::*; + +pub mod storage_examples_proxy; + +/// One endpoint pair per mapper type this recipe covers with working, +/// tested code. Method names and complexity claims below are taken +/// directly from the real `multiversx-sc` crate source doc comments +/// (`~/.cargo/registry/.../multiversx-sc-0.64.2/src/storage/mappers/*.rs`, +/// read while authoring this recipe) — several correct CLAUDE.md's own +/// summary table; see this recipe's README for exactly which claims and +/// why. +#[multiversx_sc::contract] +pub trait StorageMappers { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + // ---- SingleValueMapper: one value, no parameters. Simplest mapper, + // baseline for storage cost (1 entry). ---- + #[endpoint(setCounter)] + fn set_counter(&self, value: BigUint) { + self.counter().set(value); + } + + #[view(getCounter)] + #[storage_mapper("counter")] + fn counter(&self) -> SingleValueMapper; + + // ---- VecMapper: ordered, 1-indexed, allows duplicates, random + // access by index. ---- + #[endpoint(pushItem)] + fn push_item(&self, item: ManagedBuffer) -> usize { + self.items().push(&item) + } + + #[view(getItem)] + fn get_item(&self, index: usize) -> ManagedBuffer { + self.items().get(index) + } + + #[view(itemCount)] + fn item_count(&self) -> usize { + self.items().len() + } + + #[storage_mapper("items")] + fn items(&self) -> VecMapper; + + // ---- SetMapper: ordered (insertion order) set. Real source confirms + // O(1) contains via an internal value->node_id lookup, CONTRA + // CLAUDE.md's table, which claims O(n) — see README. ---- + #[endpoint(addToOrderedSet)] + fn add_to_ordered_set(&self, value: u64) -> bool { + self.ordered_set().insert(value) + } + + #[view(orderedSetContains)] + fn ordered_set_contains(&self, value: u64) -> bool { + self.ordered_set().contains(&value) + } + + #[view(orderedSetLen)] + fn ordered_set_len(&self) -> usize { + self.ordered_set().len() + } + + #[storage_mapper("ordered_set")] + fn ordered_set(&self) -> SetMapper; + + // ---- UnorderedSetMapper: no ordering guarantee, O(1) contains via + // VecMapper + a reverse index lookup (2N+1 entries total — see + // README for why this differs from CLAUDE.md's "N+1"). ---- + #[endpoint(addToUnorderedSet)] + fn add_to_unordered_set(&self, value: u64) -> bool { + self.unordered_set().insert(value) + } + + #[view(unorderedSetContains)] + fn unordered_set_contains(&self, value: u64) -> bool { + self.unordered_set().contains(&value) + } + + #[view(unorderedSetLen)] + fn unordered_set_len(&self) -> usize { + self.unordered_set().len() + } + + #[storage_mapper("unordered_set")] + fn unordered_set(&self) -> UnorderedSetMapper; + + // ---- WhitelistMapper: membership-only, no iteration, most + // space-efficient of the set-shaped mappers. ---- + #[endpoint(addToWhitelist)] + fn add_to_whitelist(&self, address: ManagedAddress) { + self.whitelist().add(&address); + } + + #[view(isWhitelisted)] + fn is_whitelisted(&self, address: ManagedAddress) -> bool { + self.whitelist().contains(&address) + } + + #[storage_mapper("whitelist")] + fn whitelist(&self) -> WhitelistMapper; + + // ---- MapMapper: key-value with iteration, HashMap-like API. Uses a + // SetMapper internally for key tracking plus separate value + // storage — real source confirms this is where CLAUDE.md's + // "4N+1 entries" claim actually checks out. ---- + #[endpoint(setBalance)] + fn set_balance(&self, address: ManagedAddress, amount: BigUint) { + self.balances().insert(address, amount); + } + + #[view(getBalance)] + fn get_balance(&self, address: ManagedAddress) -> BigUint { + self.balances().get(&address).unwrap_or_default() + } + + #[view(hasBalanceEntry)] + fn has_balance_entry(&self, address: ManagedAddress) -> bool { + self.balances().contains_key(&address) + } + + #[storage_mapper("balances")] + fn balances(&self) -> MapMapper; +} diff --git a/contracts/examples/storage-examples/src/storage_examples_proxy.rs b/contracts/examples/storage-examples/src/storage_examples_proxy.rs new file mode 100644 index 0000000000..59038f7459 --- /dev/null +++ b/contracts/examples/storage-examples/src/storage_examples_proxy.rs @@ -0,0 +1,278 @@ +// Code generated by the multiversx-sc proxy generator. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +#![allow(dead_code)] +#![allow(clippy::all)] + +use multiversx_sc::proxy_imports::*; + +pub struct StorageMappersProxy; + +impl TxProxyTrait for StorageMappersProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = StorageMappersProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + StorageMappersProxyMethods { wrapped_tx: tx } + } +} + +pub struct StorageMappersProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl StorageMappersProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init( + self, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .original_result() + } +} + +#[rustfmt::skip] +impl StorageMappersProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn upgrade( + self, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .original_result() + } +} + +#[rustfmt::skip] +impl StorageMappersProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn set_counter< + Arg0: ProxyArg>, + >( + self, + value: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("setCounter") + .argument(&value) + .original_result() + } + + pub fn counter( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getCounter") + .original_result() + } + + pub fn push_item< + Arg0: ProxyArg>, + >( + self, + item: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("pushItem") + .argument(&item) + .original_result() + } + + pub fn get_item< + Arg0: ProxyArg, + >( + self, + index: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getItem") + .argument(&index) + .original_result() + } + + pub fn item_count( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("itemCount") + .original_result() + } + + pub fn add_to_ordered_set< + Arg0: ProxyArg, + >( + self, + value: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("addToOrderedSet") + .argument(&value) + .original_result() + } + + pub fn ordered_set_contains< + Arg0: ProxyArg, + >( + self, + value: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("orderedSetContains") + .argument(&value) + .original_result() + } + + pub fn ordered_set_len( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("orderedSetLen") + .original_result() + } + + pub fn add_to_unordered_set< + Arg0: ProxyArg, + >( + self, + value: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("addToUnorderedSet") + .argument(&value) + .original_result() + } + + pub fn unordered_set_contains< + Arg0: ProxyArg, + >( + self, + value: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("unorderedSetContains") + .argument(&value) + .original_result() + } + + pub fn unordered_set_len( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("unorderedSetLen") + .original_result() + } + + pub fn add_to_whitelist< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("addToWhitelist") + .argument(&address) + .original_result() + } + + pub fn is_whitelisted< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("isWhitelisted") + .argument(&address) + .original_result() + } + + pub fn set_balance< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + address: Arg0, + amount: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("setBalance") + .argument(&address) + .argument(&amount) + .original_result() + } + + pub fn get_balance< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getBalance") + .argument(&address) + .original_result() + } + + pub fn has_balance_entry< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("hasBalanceEntry") + .argument(&address) + .original_result() + } +} diff --git a/contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs b/contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs new file mode 100644 index 0000000000..52adfc9513 --- /dev/null +++ b/contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs @@ -0,0 +1,265 @@ +// tests/storage_mappers_blackbox_test.rs — one test per mapper this +// recipe covers, each proving the specific behavioral claim its README +// entry makes (not just "it compiles"). + +use multiversx_sc_scenario::imports::*; + +use storage_examples::storage_examples_proxy; + +const OWNER: TestAddress = TestAddress::new("owner"); +const CONTRACT: TestSCAddress = TestSCAddress::new("storage-examples-contract"); +const CODE_PATH: MxscPath = MxscPath::new("output/storage-examples.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(CODE_PATH, storage_examples::ContractBuilder); + blockchain +} + +fn deploy(world: &mut ScenarioWorld) { + world.account(OWNER).nonce(1); + world + .tx() + .from(OWNER) + .typed(storage_examples_proxy::StorageMappersProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); +} + +#[test] +fn single_value_mapper_set_and_get() { + let mut world = world(); + deploy(&mut world); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .set_counter(BigUint::::from(42u64)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .counter() + .returns(ExpectValue(BigUint::::from(42u64))) + .run(); +} + +#[test] +fn vec_mapper_is_one_indexed() { + let mut world = world(); + deploy(&mut world); + + // Push three items; VecMapper's own doc comment says indexes start + // at 1 — confirm index 1 is the FIRST push, not the second. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .push_item(ManagedBuffer::::from(b"first")) + .run(); + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .push_item(ManagedBuffer::::from(b"second")) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .item_count() + .returns(ExpectValue(2usize)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .get_item(1usize) + .returns(ExpectValue(ManagedBuffer::::from(b"first"))) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .get_item(2usize) + .returns(ExpectValue(ManagedBuffer::::from(b"second"))) + .run(); +} + +#[test] +fn set_mapper_contains_and_ordering() { + let mut world = world(); + deploy(&mut world); + + for value in [30u64, 10u64, 20u64] { + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .add_to_ordered_set(value) + .run(); + } + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .ordered_set_contains(10u64) + .returns(ExpectValue(true)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .ordered_set_contains(99u64) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .ordered_set_len() + .returns(ExpectValue(3usize)) + .run(); +} + +#[test] +fn unordered_set_mapper_contains_after_insert_and_absent_value() { + let mut world = world(); + deploy(&mut world); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .add_to_unordered_set(7u64) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .unordered_set_contains(7u64) + .returns(ExpectValue(true)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .unordered_set_contains(8u64) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .unordered_set_len() + .returns(ExpectValue(1usize)) + .run(); +} + +#[test] +fn whitelist_mapper_membership_only() { + let mut world = world(); + deploy(&mut world); + let allowed: TestAddress = TestAddress::new("allowed-user"); + let stranger: TestAddress = TestAddress::new("stranger"); + world.account(allowed).nonce(1); + world.account(stranger).nonce(1); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .add_to_whitelist(allowed.to_address()) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .is_whitelisted(allowed.to_address()) + .returns(ExpectValue(true)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .is_whitelisted(stranger.to_address()) + .returns(ExpectValue(false)) + .run(); +} + +#[test] +fn map_mapper_insert_get_and_contains_key() { + let mut world = world(); + deploy(&mut world); + let holder: TestAddress = TestAddress::new("balance-holder"); + let nobody: TestAddress = TestAddress::new("no-balance"); + world.account(holder).nonce(1); + world.account(nobody).nonce(1); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .set_balance(holder.to_address(), BigUint::::from(500u64)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .get_balance(holder.to_address()) + .returns(ExpectValue(BigUint::::from(500u64))) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .has_balance_entry(holder.to_address()) + .returns(ExpectValue(true)) + .run(); + + // A key that was never inserted: contains_key is false, and the + // convenience getter's unwrap_or_default() reads as zero rather than + // erroring — a real design choice worth testing explicitly, since it + // means "balance of zero" and "never had an entry" are + // indistinguishable through get_balance alone. + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .has_balance_entry(nobody.to_address()) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .get_balance(nobody.to_address()) + .returns(ExpectValue(BigUint::::from(0u64))) + .run(); +} diff --git a/contracts/examples/storage-examples/wasm/Cargo.toml b/contracts/examples/storage-examples/wasm/Cargo.toml new file mode 100644 index 0000000000..abe9683ed1 --- /dev/null +++ b/contracts/examples/storage-examples/wasm/Cargo.toml @@ -0,0 +1,35 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "storage-examples-wasm" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[profile.dev] +panic = "abort" + +[dependencies.storage-examples] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.66.2" +path = "../../../../framework/wasm-adapter" + +[workspace] +members = ["."] diff --git a/contracts/examples/storage-examples/wasm/src/lib.rs b/contracts/examples/storage-examples/wasm/src/lib.rs new file mode 100644 index 0000000000..05982514c2 --- /dev/null +++ b/contracts/examples/storage-examples/wasm/src/lib.rs @@ -0,0 +1,42 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Upgrade: 1 +// Endpoints: 16 +// Async Callback (empty): 1 +// Total number of exported functions: 19 + +#![no_std] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + storage_examples + ( + init => init + upgrade => upgrade + setCounter => set_counter + getCounter => counter + pushItem => push_item + getItem => get_item + itemCount => item_count + addToOrderedSet => add_to_ordered_set + orderedSetContains => ordered_set_contains + orderedSetLen => ordered_set_len + addToUnorderedSet => add_to_unordered_set + unorderedSetContains => unordered_set_contains + unorderedSetLen => unordered_set_len + addToWhitelist => add_to_whitelist + isWhitelisted => is_whitelisted + setBalance => set_balance + getBalance => get_balance + hasBalanceEntry => has_balance_entry + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {} From 30418a850bec20d0b0be8da46e025544b6c9f23e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 12 Jul 2026 07:04:10 +0300 Subject: [PATCH 137/195] storage examples docs, more tests --- .../storage-examples/src/storage_examples.rs | 69 +++++++++++------ ...t.rs => storage_examples_blackbox_test.rs} | 76 ++++++++++++++++++- 2 files changed, 120 insertions(+), 25 deletions(-) rename contracts/examples/storage-examples/tests/{storage_mappers_blackbox_test.rs => storage_examples_blackbox_test.rs} (77%) diff --git a/contracts/examples/storage-examples/src/storage_examples.rs b/contracts/examples/storage-examples/src/storage_examples.rs index 1e01e51726..2c119b74c9 100644 --- a/contracts/examples/storage-examples/src/storage_examples.rs +++ b/contracts/examples/storage-examples/src/storage_examples.rs @@ -4,13 +4,19 @@ use multiversx_sc::imports::*; pub mod storage_examples_proxy; -/// One endpoint pair per mapper type this recipe covers with working, -/// tested code. Method names and complexity claims below are taken -/// directly from the real `multiversx-sc` crate source doc comments -/// (`~/.cargo/registry/.../multiversx-sc-0.64.2/src/storage/mappers/*.rs`, -/// read while authoring this recipe) — several correct CLAUDE.md's own -/// summary table; see this recipe's README for exactly which claims and -/// why. +/// Demonstrates the six most commonly used storage mapper types in +/// `multiversx-sc`, one endpoint group per mapper. Each group exposes a +/// minimal but realistic API so the companion blackbox tests can verify the +/// behavioural guarantees documented in the mapper source. +/// +/// | Mapper | Iteration | Duplicates | `contains` | Storage entries | +/// |----------------------|------------|------------|------------|-------------------| +/// | `SingleValueMapper` | — | one slot | — | 1 | +/// | `VecMapper` | sequential | allowed | O(n) | N + 1 | +/// | `SetMapper` | ordered | rejected | O(1) | 4N + 1 | +/// | `UnorderedSetMapper` | unordered | rejected | O(1) | 2N + 1 | +/// | `WhitelistMapper` | — | rejected | O(1) | N | +/// | `MapMapper` | keys only | — | O(1) | 4N + 1 | #[multiversx_sc::contract] pub trait StorageMappers { #[init] @@ -19,8 +25,11 @@ pub trait StorageMappers { #[upgrade] fn upgrade(&self) {} - // ---- SingleValueMapper: one value, no parameters. Simplest mapper, - // baseline for storage cost (1 entry). ---- + // ---- SingleValueMapper ----------------------------------------------- + // Stores exactly one value under a fixed key. The simplest mapper: + // one storage entry, no iteration, O(1) read/write. + // `set` overwrites any previous value; `get` returns the default if + // the slot has never been written. #[endpoint(setCounter)] fn set_counter(&self, value: BigUint) { self.counter().set(value); @@ -30,8 +39,10 @@ pub trait StorageMappers { #[storage_mapper("counter")] fn counter(&self) -> SingleValueMapper; - // ---- VecMapper: ordered, 1-indexed, allows duplicates, random - // access by index. ---- + // ---- VecMapper --------------------------------------------------------- + // An ordered, 1-indexed sequence that allows duplicate values. + // `push` appends to the end and returns the new item's 1-based index. + // Random access by index is O(1); `contains` is O(n) (linear scan). #[endpoint(pushItem)] fn push_item(&self, item: ManagedBuffer) -> usize { self.items().push(&item) @@ -50,9 +61,12 @@ pub trait StorageMappers { #[storage_mapper("items")] fn items(&self) -> VecMapper; - // ---- SetMapper: ordered (insertion order) set. Real source confirms - // O(1) contains via an internal value->node_id lookup, CONTRA - // CLAUDE.md's table, which claims O(n) — see README. ---- + // ---- SetMapper -------------------------------------------------------- + // An ordered set (insertion order) that rejects duplicates. + // Internally maintains a doubly-linked list of nodes plus a + // value→node-id index, giving O(1) `contains`, `insert`, and `remove`. + // `insert` returns `true` when the value was newly added, `false` if + // it was already present. Uses ~4N + 1 storage entries for N elements. #[endpoint(addToOrderedSet)] fn add_to_ordered_set(&self, value: u64) -> bool { self.ordered_set().insert(value) @@ -71,9 +85,13 @@ pub trait StorageMappers { #[storage_mapper("ordered_set")] fn ordered_set(&self) -> SetMapper; - // ---- UnorderedSetMapper: no ordering guarantee, O(1) contains via - // VecMapper + a reverse index lookup (2N+1 entries total — see - // README for why this differs from CLAUDE.md's "N+1"). ---- + // ---- UnorderedSetMapper ----------------------------------------------- + // A set with no ordering guarantee. Backed by a `VecMapper` plus a + // reverse value→index map, giving O(1) `contains`, `insert`, and + // `remove` (swap-remove strategy) at the cost of ~2N + 1 storage + // entries. Cheaper than `SetMapper` when iteration order does not + // matter. `insert` returns `true` for a new value, `false` for a + // duplicate. #[endpoint(addToUnorderedSet)] fn add_to_unordered_set(&self, value: u64) -> bool { self.unordered_set().insert(value) @@ -92,8 +110,11 @@ pub trait StorageMappers { #[storage_mapper("unordered_set")] fn unordered_set(&self) -> UnorderedSetMapper; - // ---- WhitelistMapper: membership-only, no iteration, most - // space-efficient of the set-shaped mappers. ---- + // ---- WhitelistMapper -------------------------------------------------- + // Membership-only set: supports `add`, `remove`, and `contains` but + // provides no iteration. Each member costs exactly one storage entry + // (a boolean flag), making it the most space-efficient option when + // you only need to answer "is this address a member?". #[endpoint(addToWhitelist)] fn add_to_whitelist(&self, address: ManagedAddress) { self.whitelist().add(&address); @@ -107,10 +128,12 @@ pub trait StorageMappers { #[storage_mapper("whitelist")] fn whitelist(&self) -> WhitelistMapper; - // ---- MapMapper: key-value with iteration, HashMap-like API. Uses a - // SetMapper internally for key tracking plus separate value - // storage — real source confirms this is where CLAUDE.md's - // "4N+1 entries" claim actually checks out. ---- + // ---- MapMapper -------------------------------------------------------- + // Key-value store with iterable keys. Uses a `SetMapper` internally + // to track the key set plus one extra slot per value, totalling ~4N + 1 + // storage entries for N pairs. `insert` and `get` are O(1); iterating + // all keys is O(N). `get` returns `Option` — use `unwrap_or_default` + // when a missing key should be treated as a zero/empty value. #[endpoint(setBalance)] fn set_balance(&self, address: ManagedAddress, amount: BigUint) { self.balances().insert(address, amount); diff --git a/contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs b/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs similarity index 77% rename from contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs rename to contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs index 52adfc9513..3530153197 100644 --- a/contracts/examples/storage-examples/tests/storage_mappers_blackbox_test.rs +++ b/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs @@ -50,19 +50,50 @@ fn single_value_mapper_set_and_get() { .run(); } +#[test] +fn single_value_mapper_overwrite() { + let mut world = world(); + deploy(&mut world); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .set_counter(BigUint::::from(10u64)) + .run(); + + // A second set must overwrite the first — not accumulate. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .set_counter(BigUint::::from(99u64)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .counter() + .returns(ExpectValue(BigUint::::from(99u64))) + .run(); +} + #[test] fn vec_mapper_is_one_indexed() { let mut world = world(); deploy(&mut world); - // Push three items; VecMapper's own doc comment says indexes start - // at 1 — confirm index 1 is the FIRST push, not the second. + // Push returns the 1-based index of the newly appended item. world .tx() .from(OWNER) .to(CONTRACT) .typed(storage_examples_proxy::StorageMappersProxy) .push_item(ManagedBuffer::::from(b"first")) + .returns(ExpectValue(1usize)) .run(); world .tx() @@ -70,6 +101,7 @@ fn vec_mapper_is_one_indexed() { .to(CONTRACT) .typed(storage_examples_proxy::StorageMappersProxy) .push_item(ManagedBuffer::::from(b"second")) + .returns(ExpectValue(2usize)) .run(); world @@ -109,6 +141,8 @@ fn set_mapper_contains_and_ordering() { .to(CONTRACT) .typed(storage_examples_proxy::StorageMappersProxy) .add_to_ordered_set(value) + // A fresh insert must return true. + .returns(ExpectValue(true)) .run(); } @@ -135,6 +169,24 @@ fn set_mapper_contains_and_ordering() { .ordered_set_len() .returns(ExpectValue(3usize)) .run(); + + // Inserting a duplicate must return false and leave the length unchanged. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .add_to_ordered_set(10u64) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .ordered_set_len() + .returns(ExpectValue(3usize)) + .run(); } #[test] @@ -148,6 +200,8 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { .to(CONTRACT) .typed(storage_examples_proxy::StorageMappersProxy) .add_to_unordered_set(7u64) + // A fresh insert must return true. + .returns(ExpectValue(true)) .run(); world @@ -173,6 +227,24 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { .unordered_set_len() .returns(ExpectValue(1usize)) .run(); + + // Inserting a duplicate must return false and leave the length unchanged. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .add_to_unordered_set(7u64) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_examples_proxy::StorageMappersProxy) + .unordered_set_len() + .returns(ExpectValue(1usize)) + .run(); } #[test] From 7b7cf4f825b0e693819e7ad2ea76df04e7628c0e Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 12 Jul 2026 07:16:38 +0300 Subject: [PATCH 138/195] greeter contract --- Cargo.lock | 23 +- Cargo.toml | 2 + contracts/examples/check-pause/Cargo.toml | 3 - contracts/examples/empty/Cargo.toml | 3 - contracts/examples/greeter/.gitignore | 5 + contracts/examples/greeter/Cargo.toml | 15 + contracts/examples/greeter/README.md | 301 ++++++++++++++++++ contracts/examples/greeter/meta/Cargo.toml | 11 + contracts/examples/greeter/meta/src/main.rs | 3 + contracts/examples/greeter/multiversx.json | 3 + contracts/examples/greeter/sc-config.toml | 7 + contracts/examples/greeter/src/greeter.rs | 33 ++ .../examples/greeter/src/greeter_proxy.rs | 113 +++++++ .../greeter/tests/greeter_blackbox_test.rs | 110 +++++++ contracts/examples/greeter/wasm/Cargo.lock | 205 ++++++++++++ contracts/examples/greeter/wasm/Cargo.toml | 35 ++ contracts/examples/greeter/wasm/src/lib.rs | 28 ++ .../feature-tests/std-contract/Cargo.toml | 3 - 18 files changed, 889 insertions(+), 14 deletions(-) create mode 100644 contracts/examples/greeter/.gitignore create mode 100644 contracts/examples/greeter/Cargo.toml create mode 100644 contracts/examples/greeter/README.md create mode 100644 contracts/examples/greeter/meta/Cargo.toml create mode 100644 contracts/examples/greeter/meta/src/main.rs create mode 100644 contracts/examples/greeter/multiversx.json create mode 100644 contracts/examples/greeter/sc-config.toml create mode 100644 contracts/examples/greeter/src/greeter.rs create mode 100644 contracts/examples/greeter/src/greeter_proxy.rs create mode 100644 contracts/examples/greeter/tests/greeter_blackbox_test.rs create mode 100644 contracts/examples/greeter/wasm/Cargo.lock create mode 100644 contracts/examples/greeter/wasm/Cargo.toml create mode 100644 contracts/examples/greeter/wasm/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 071cb70843..6a7c6a3695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,9 +180,9 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" -version = "0.7.7" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "atomic-waker" @@ -555,7 +555,6 @@ dependencies = [ "multiversx-sc", "multiversx-sc-modules", "multiversx-sc-scenario", - "num-bigint", ] [[package]] @@ -1274,7 +1273,6 @@ version = "0.0.0" dependencies = [ "multiversx-sc", "multiversx-sc-scenario", - "num-bigint", ] [[package]] @@ -2175,6 +2173,22 @@ dependencies = [ "toml", ] +[[package]] +name = "greeter" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-scenario", +] + +[[package]] +name = "greeter-meta" +version = "0.0.0" +dependencies = [ + "greeter", + "multiversx-sc-meta-lib", +] + [[package]] name = "h2" version = "0.4.15" @@ -5168,7 +5182,6 @@ version = "0.0.0" dependencies = [ "multiversx-sc", "multiversx-sc-scenario", - "num-bigint", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1cd22b4cc8..49e4c43ed3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,8 @@ members = [ "contracts/examples/factorial/meta", "contracts/examples/fractional-nfts", "contracts/examples/fractional-nfts/meta", + "contracts/examples/greeter", + "contracts/examples/greeter/meta", "contracts/examples/lottery", "contracts/examples/lottery/meta", "contracts/examples/multisig", diff --git a/contracts/examples/check-pause/Cargo.toml b/contracts/examples/check-pause/Cargo.toml index 9a7863312d..2013a646b6 100644 --- a/contracts/examples/check-pause/Cargo.toml +++ b/contracts/examples/check-pause/Cargo.toml @@ -8,9 +8,6 @@ publish = false [lib] path = "src/check_pause.rs" -[dev-dependencies] -num-bigint = "0.4" - [dependencies.multiversx-sc] workspace = true diff --git a/contracts/examples/empty/Cargo.toml b/contracts/examples/empty/Cargo.toml index fb85f85ee1..233db5a206 100644 --- a/contracts/examples/empty/Cargo.toml +++ b/contracts/examples/empty/Cargo.toml @@ -13,6 +13,3 @@ workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true - -[dev-dependencies] -num-bigint = "0.4" diff --git a/contracts/examples/greeter/.gitignore b/contracts/examples/greeter/.gitignore new file mode 100644 index 0000000000..eda1e811c3 --- /dev/null +++ b/contracts/examples/greeter/.gitignore @@ -0,0 +1,5 @@ +# Build artifacts — regenerate with `sc-meta all build`. +target/ +output/ + +# Keep Cargo.lock and wasm/Cargo.lock tracked (reproducible builds) diff --git a/contracts/examples/greeter/Cargo.toml b/contracts/examples/greeter/Cargo.toml new file mode 100644 index 0000000000..a2cbdbf7c1 --- /dev/null +++ b/contracts/examples/greeter/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "greeter" +version = "0.0.0" +edition = "2024" +publish = false +authors = ["you"] + +[lib] +path = "src/greeter.rs" + +[dependencies.multiversx-sc] +workspace = true + +[dev-dependencies.multiversx-sc-scenario] +workspace = true diff --git a/contracts/examples/greeter/README.md b/contracts/examples/greeter/README.md new file mode 100644 index 0000000000..556e490ad9 --- /dev/null +++ b/contracts/examples/greeter/README.md @@ -0,0 +1,301 @@ +# Recipe: New contract from `sc-meta new --template empty` + +The real, unedited output of `sc-meta new --template empty`, then the +smallest real customization on top (one storage mapper, one endpoint, +one view) — because a genuinely empty contract doesn't prove the +workflow actually works end to end. Every command below was run for +real in this environment (`rustc 1.93.0`, `cargo 1.93.0`, +`multiversx-sc-meta 0.64.1`) while authoring this recipe — see +"Validation" for exactly what that means and what it doesn't. + +## Prerequisites + +- Rust via `rustup`, with the `wasm32v1-none` target installed (see + "A version note" below for why not `wasm32-unknown-unknown`). +- `sc-meta` (`cargo install multiversx-sc-meta`). +- Optional: `wasm-opt`, for size-optimized release builds (this recipe + was verified without it — see Pitfall 3). + +## Install + +This recipe's own directory ships the already-generated-and-customized +project directly at its root (`Cargo.toml`, `src/`, `meta/`, etc. — the +same convention every other recipe in this Cookbook uses: clone and go, +no extra scaffolding step required): + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/new-contract-from-template +sc-meta all build +cargo test +``` + +To reproduce the scaffold from scratch instead (what Step 1 below +walks through): `sc-meta new --template empty --name greeter` creates a +**new subdirectory** named after `--name` — run it in an empty directory +of your own and treat the resulting `greeter/` as your project root +(this recipe's own root is exactly that directory's contents, moved up +one level for consistency with this Cookbook's other recipes). + +## Step 1 — the bare scaffold + +`sc-meta new --template empty --name greeter` does more than copy files: +it renames the trait, the crate names, the module paths, and the +scenario references throughout, all in one pass. This is the real, +captured output of running it standalone (the `greeter/` directory the +command itself creates — this recipe's own root, one level up, IS this +directory's contents, per "Install" above): + +``` +greeter/ +├── Cargo.toml +├── multiversx.json +├── meta/ +│ ├── Cargo.toml +│ └── src/main.rs +├── src/ +│ └── greeter.rs +├── scenarios/ +│ └── greeter.scen.json +└── tests/ + ├── greeter_scenario_go_test.rs + └── greeter_scenario_rs_test.rs +``` + +`src/greeter.rs`, unmodified from the template: + +```rust +#![no_std] + +#[allow(unused_imports)] +use multiversx_sc::imports::*; + +/// An empty contract. To be used as a template when starting a new contract from scratch. +#[multiversx_sc::contract] +pub trait Greeter { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} +} +``` + +Two things worth noting immediately, both confirmed by actually running +the command rather than reading about it: + +- **There is no `sc-config.toml`, no `wasm/`, and no `output/` yet.** + CLAUDE.md's "Smart Contract Project Anatomy" shows these as if every + contract project always has them; a bare `sc-meta new` output doesn't. + `wasm/` and `output/` appear the first time you build (Step 3); + `sc-config.toml` only exists once you need it (Step 2). +- **The scenario test files' function names don't get renamed.** Look at + `tests/greeter_scenario_go_test.rs`: the *file* was renamed from + `empty_scenario_go_test.rs`, and the scenario path inside was updated + to `scenarios/greeter.scen.json` — but the test function itself is + still `fn empty_go()`, not `fn greeter_go()`. The rename pass covers + file names, crate names, and module/trait identifiers; it does not + rewrite every string inside test bodies. Not a bug — just something to + expect rather than be surprised by when you diff a freshly generated + project. + +## Step 2 — add a storage mapper, an endpoint, and a proxy + +`src/greeter.rs` in this recipe adds one parameterized `SingleValueMapper` +(a per-caller greeting) and two members exposing it — the smallest real +next step past the bare scaffold, and the natural bridge into +[Storage mappers: which to pick, when](/smart-contracts-rust/storage-mapper-decision-table/): + +```rust +#![no_std] + +use multiversx_sc::imports::*; + +pub mod greeter_proxy; + +#[multiversx_sc::contract] +pub trait Greeter { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + #[endpoint(setGreeting)] + fn set_greeting(&self, message: ManagedBuffer) { + let caller = self.blockchain().get_caller(); + self.greeting(&caller).set(message); + } + + #[view(getGreeting)] + #[storage_mapper("greeting")] + fn greeting(&self, address: &ManagedAddress) -> SingleValueMapper; +} +``` + +**Generating the proxy has a real chicken-and-egg order to it.** Adding +`pub mod greeter_proxy;` before the file exists fails the build +(`error[E0583]: file not found for module`) — confirmed by hitting this +error directly while authoring this recipe. The actual working order: + +1. Add `sc-config.toml`: + ```toml + [[proxy]] + path = "src/greeter_proxy.rs" + ``` +2. Write the contract logic WITHOUT the `pub mod greeter_proxy;` line yet. +3. Run `sc-meta all proxy` — this compiles the contract (which doesn't + yet reference the proxy module, so nothing is missing) and generates + `src/greeter_proxy.rs` from the resulting ABI. +4. Add `pub mod greeter_proxy;` now that the file exists. + +## Step 3 — build + +```bash +sc-meta all build +``` + +Real output, captured while authoring this recipe: + +``` +Building greeter.wasm in .../greeter/wasm ... +RUSTFLAGS="-C link-arg=-s -C link-arg=-zstack-size=131072" cargo +1.93-aarch64-apple-darwin build --target=wasm32v1-none --release ... + Compiling greeter v0.0.0 (...) + Compiling greeter-wasm v0.0.0 (...) + Finished `release` profile [optimized] target(s) in 6.78s +Copying .../target/wasm32v1-none/release/greeter_wasm.wasm to ../output/greeter.wasm ... +Warning: wasm-opt not installed. +Extracting imports to ../output/greeter.imports.json ... +Checking EI version: 1.5 ... OK +Packing ../output/greeter.mxsc.json ... +Contract size: 996 bytes. +``` + +`sc-meta all build` also regenerates `wasm/src/lib.rs`'s endpoint list on +every run — confirmed by diffing it before and after adding +`setGreeting`/`getGreeting`: it picked both up automatically without any +manual edit: + +```rust +// Code generated by the multiversx-sc build system. DO NOT EDIT. +multiversx_sc_wasm_adapter::endpoints! { + greeter + ( + init => init + upgrade => upgrade + setGreeting => set_greeting + getGreeting => greeting + ) +} +``` + +## Step 4 — test + +This recipe adds `tests/greeter_blackbox_test.rs` — the scaffold's own +`greeter_scenario_*_test.rs` files only exercise deploy, not the added +logic. Two blackbox tests, following CLAUDE.md §"Blackbox Tests +(RECOMMENDED)" exactly: one confirms `setGreeting` → `getGreeting` +round-trips per caller, the other confirms two different callers get +independent storage (proving the parameterized mapper's key really does +include the address, not just in theory). + +```bash +cargo test +``` + +Real output: + +``` +running 2 tests +test greeting_is_keyed_per_caller ... ok +test set_and_get_greeting ... ok +test result: ok. 2 passed; 0 failed; ... + +running 1 test +test empty_go ... ok + +running 1 test +test empty_rs ... ok +``` + +All 4 tests pass: the 2 new blackbox tests plus the 2 scaffold-provided +scenario tests (whose function names are still `empty_go`/`empty_rs` — +see Step 1). + +## A version note + +Three different version numbers showed up while authoring this recipe, +and they disagree: + +| Source | `multiversx-sc` version | +| --- | --- | +| CLAUDE.md, "Current Version" | 0.65.0 | +| `mx-sdk-rs` GitHub `master` branch, `contracts/examples/empty/Cargo.toml` | 0.66.2 | +| This environment's installed `sc-meta` (0.64.1), and what it actually generated | 0.64.1 | + +`sc-meta new` bundles its own template, versioned to match whatever +`multiversx-sc-meta` release you have installed — it does not fetch the +latest framework version from anywhere. If your installed `sc-meta` is +older than the framework's current release (as this environment's is), +your freshly scaffolded contract pins an older `multiversx-sc` than +CLAUDE.md calls "current." Run `sc-meta upgrade` after scaffolding if you +want the latest framework version, or pass `sc-meta new --tag ` +to pin one explicitly at generation time. + +Also worth noting: the actual build command targets `wasm32v1-none`, not +`wasm32-unknown-unknown` as CLAUDE.md's "Build System" section states — +confirmed directly from the real `sc-meta all build` invocation logged +above. Install the right target via `rustup target add wasm32v1-none` if +`sc-meta install all` hasn't already done it for you. + +## Pitfalls + +1. **Adding `pub mod X_proxy;` before generating the proxy file breaks + the build.** See Step 2's ordering — generate first, declare the + module second. + +2. **A bare `sc-meta new` output has no `sc-config.toml`.** CLAUDE.md's + project anatomy diagram shows one unconditionally; you only need it + once you want proxy generation or a multi-contract build. + +3. **`wasm-opt` not being installed doesn't fail the build** — it's a + warning, and `sc-meta all build` still produces a valid, deployable + `.wasm`/`.mxsc.json`, just without size optimization. Verified + directly: this recipe's build ran with no `wasm-opt` on `PATH` and + still produced a working, tested contract. Install it before shipping + to mainnet if contract size matters to you (bytecode storage has a + real, per-byte on-chain cost). + +4. **Scenario test function names surviving a rename are not a sign + something went wrong.** See Step 1's second bullet. + +5. **`sc-meta new`'s bundled template version may lag the framework's + documented "current" version.** See "A version note" above — check + what actually got generated (`cat Cargo.toml`) rather than assuming it + matches the newest release. + +## See also + +- [Storage mappers: which to pick, when](/smart-contracts-rust/storage-mapper-decision-table/) — + the natural next recipe: this one introduces exactly one mapper + (`SingleValueMapper`, parameterized); that one covers the rest and when + to reach for each. +- [Sign and send a transaction](/start-here/sign-and-send/) — the dApp + side that would eventually call an endpoint like `setGreeting` from a + connected wallet. + +## Validation + +Every command in this README was run for real in this environment: +`sc-meta new --template empty --name greeter` (captured file listing and +diff above), `sc-meta all proxy` (real generated +`src/greeter_proxy.rs`, shown in full in the source), `sc-meta all build` +(real WASM + ABI + `.mxsc.json` output, contract size 996 bytes), and +`cargo test` (4/4 tests passing: 2 new blackbox tests plus the 2 +scaffold-provided scenario tests). This recipe has no `npm install`/ +`tsc`/`eslint` gate — it's Rust, verified with `sc-meta`/`cargo` instead, +per this Cookbook's TypeScript-vs-Rust verification split (see +PROTOTYPE-NOTES.md's FINAL SUMMARY). `target/` and `output/` are +gitignored (regenerable build artifacts, the same convention this +Cookbook's TypeScript recipes use for `node_modules/`/`dist/`); `Cargo.lock` +and `wasm/Cargo.lock` are kept, mirroring `package-lock.json`. diff --git a/contracts/examples/greeter/meta/Cargo.toml b/contracts/examples/greeter/meta/Cargo.toml new file mode 100644 index 0000000000..4ef4c0662d --- /dev/null +++ b/contracts/examples/greeter/meta/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "greeter-meta" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies.greeter] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +workspace = true diff --git a/contracts/examples/greeter/meta/src/main.rs b/contracts/examples/greeter/meta/src/main.rs new file mode 100644 index 0000000000..b76002a4db --- /dev/null +++ b/contracts/examples/greeter/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} diff --git a/contracts/examples/greeter/multiversx.json b/contracts/examples/greeter/multiversx.json new file mode 100644 index 0000000000..7365539625 --- /dev/null +++ b/contracts/examples/greeter/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/examples/greeter/sc-config.toml b/contracts/examples/greeter/sc-config.toml new file mode 100644 index 0000000000..7f61cffe27 --- /dev/null +++ b/contracts/examples/greeter/sc-config.toml @@ -0,0 +1,7 @@ +# Not generated by `sc-meta new --template empty` — confirmed empirically +# while authoring this recipe: a freshly scaffolded "empty" contract has +# no sc-config.toml at all. It's only needed once you want proxy +# generation (or a multi-contract build), so it's added here as the +# first real step past the bare scaffold. +[[proxy]] +path = "src/greeter_proxy.rs" diff --git a/contracts/examples/greeter/src/greeter.rs b/contracts/examples/greeter/src/greeter.rs new file mode 100644 index 0000000000..a4f6bece71 --- /dev/null +++ b/contracts/examples/greeter/src/greeter.rs @@ -0,0 +1,33 @@ +#![no_std] + +use multiversx_sc::imports::*; + +pub mod greeter_proxy; + +/// A minimal greeter contract. Starts from `sc-meta new --template empty +/// --name greeter` (see this recipe's README for the exact, unmodified +/// output of that command) with one endpoint and one storage mapper added +/// on top — the smallest real next step after scaffolding, and the +/// natural bridge into the storage-mapper-decision-table recipe. +#[multiversx_sc::contract] +pub trait Greeter { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + /// Stores a greeting message for the calling address. Overwrites any + /// previous greeting for the same caller. + #[endpoint(setGreeting)] + fn set_greeting(&self, message: ManagedBuffer) { + let caller = self.blockchain().get_caller(); + self.greeting(&caller).set(message); + } + + /// Reads back the greeting message stored for `address`. Returns an + /// empty ManagedBuffer if that address never called setGreeting. + #[view(getGreeting)] + #[storage_mapper("greeting")] + fn greeting(&self, address: &ManagedAddress) -> SingleValueMapper; +} diff --git a/contracts/examples/greeter/src/greeter_proxy.rs b/contracts/examples/greeter/src/greeter_proxy.rs new file mode 100644 index 0000000000..c3f1239a40 --- /dev/null +++ b/contracts/examples/greeter/src/greeter_proxy.rs @@ -0,0 +1,113 @@ +// Code generated by the multiversx-sc proxy generator. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +#![allow(dead_code)] +#![allow(clippy::all)] + +use multiversx_sc::proxy_imports::*; + +pub struct GreeterProxy; + +impl TxProxyTrait for GreeterProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = GreeterProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + GreeterProxyMethods { wrapped_tx: tx } + } +} + +pub struct GreeterProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl GreeterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init( + self, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .original_result() + } +} + +#[rustfmt::skip] +impl GreeterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn upgrade( + self, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .original_result() + } +} + +#[rustfmt::skip] +impl GreeterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + /// Stores a greeting message for the calling address. Overwrites any + /// previous greeting for the same caller. + pub fn set_greeting< + Arg0: ProxyArg>, + >( + self, + message: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("setGreeting") + .argument(&message) + .original_result() + } + + /// Reads back the greeting message stored for `address`. Returns an + /// empty ManagedBuffer if that address never called setGreeting. + pub fn greeting< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getGreeting") + .argument(&address) + .original_result() + } +} diff --git a/contracts/examples/greeter/tests/greeter_blackbox_test.rs b/contracts/examples/greeter/tests/greeter_blackbox_test.rs new file mode 100644 index 0000000000..9025eebc78 --- /dev/null +++ b/contracts/examples/greeter/tests/greeter_blackbox_test.rs @@ -0,0 +1,110 @@ +// tests/greeter_blackbox_test.rs — a hand-written blackbox test for the +// endpoint added on top of the bare `empty` scaffold. The two +// `greeter_scenario_*_test.rs` files (unmodified from `sc-meta new +// --template empty`) only exercise deploy; this test exercises the +// actual custom logic, following CLAUDE.md §"Blackbox Tests +// (RECOMMENDED)" pattern exactly. + +use multiversx_sc_scenario::imports::*; + +use greeter::greeter_proxy; + +const OWNER: TestAddress = TestAddress::new("owner"); +const CONTRACT: TestSCAddress = TestSCAddress::new("greeter-contract"); +const CODE_PATH: MxscPath = MxscPath::new("output/greeter.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(CODE_PATH, greeter::ContractBuilder); + blockchain +} + +#[test] +fn set_and_get_greeting() { + let mut world = world(); + world.account(OWNER).nonce(1); + + // Deploy. + world + .tx() + .from(OWNER) + .typed(greeter_proxy::GreeterProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); + + // Before any call, the greeting for OWNER is empty. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::new())) + .run(); + + // Call setGreeting as OWNER. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .set_greeting(ManagedBuffer::::from(b"hello devnet")) + .run(); + + // getGreeting now returns what we stored, keyed by the caller address. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::from( + b"hello devnet", + ))) + .run(); +} + +#[test] +fn greeting_is_keyed_per_caller() { + let mut world = world(); + world.account(OWNER).nonce(1); + let other: TestAddress = TestAddress::new("other-caller"); + world.account(other).nonce(1); + + world + .tx() + .from(OWNER) + .typed(greeter_proxy::GreeterProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .set_greeting("from owner") + .run(); + + // A different caller who never called setGreeting still reads back + // empty — this is what CLAUDE.md §"Storage Mappers" means by "the key + // includes the parameter automatically": each address gets its own + // storage slot, not a shared one. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(other.to_address()) + .returns(ExpectValue("")) + .run(); + + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue("from owner")) + .run(); +} diff --git a/contracts/examples/greeter/wasm/Cargo.lock b/contracts/examples/greeter/wasm/Cargo.lock new file mode 100644 index 0000000000..6e8955755d --- /dev/null +++ b/contracts/examples/greeter/wasm/Cargo.lock @@ -0,0 +1,205 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "endian-type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + +[[package]] +name = "generic-array" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" +dependencies = [ + "rustversion", + "typenum", +] + +[[package]] +name = "greeter" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "greeter-wasm" +version = "0.0.0" +dependencies = [ + "greeter", + "multiversx-sc-wasm-adapter", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "multiversx-chain-core" +version = "0.23.1" +dependencies = [ + "bitflags", + "multiversx-sc-codec", +] + +[[package]] +name = "multiversx-sc" +version = "0.66.2" +dependencies = [ + "bitflags", + "generic-array", + "multiversx-chain-core", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.25.0" +dependencies = [ + "arrayvec", + "bitflags", + "multiversx-sc-codec-derive", + "num-traits", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.25.0" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.66.2" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.66.2" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unwrap-infallible" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e497bb1f828cc9fb236722c2eaa100dcf201563f38f4da6252357a59037adf31" diff --git a/contracts/examples/greeter/wasm/Cargo.toml b/contracts/examples/greeter/wasm/Cargo.toml new file mode 100644 index 0000000000..eec3775f63 --- /dev/null +++ b/contracts/examples/greeter/wasm/Cargo.toml @@ -0,0 +1,35 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "greeter-wasm" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[profile.dev] +panic = "abort" + +[dependencies.greeter] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.66.2" +path = "../../../../framework/wasm-adapter" + +[workspace] +members = ["."] diff --git a/contracts/examples/greeter/wasm/src/lib.rs b/contracts/examples/greeter/wasm/src/lib.rs new file mode 100644 index 0000000000..ab3c2a2a7b --- /dev/null +++ b/contracts/examples/greeter/wasm/src/lib.rs @@ -0,0 +1,28 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Upgrade: 1 +// Endpoints: 2 +// Async Callback (empty): 1 +// Total number of exported functions: 5 + +#![no_std] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + greeter + ( + init => init + upgrade => upgrade + setGreeting => set_greeting + getGreeting => greeting + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {} diff --git a/contracts/feature-tests/std-contract/Cargo.toml b/contracts/feature-tests/std-contract/Cargo.toml index 918a8a5411..cbf21fc324 100644 --- a/contracts/feature-tests/std-contract/Cargo.toml +++ b/contracts/feature-tests/std-contract/Cargo.toml @@ -13,6 +13,3 @@ workspace = true [dev-dependencies.multiversx-sc-scenario] workspace = true - -[dev-dependencies] -num-bigint = "0.4" From 1e6d7e15766f5cdb9fb4edce0c7926a79cdcc0b1 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 12 Jul 2026 07:42:56 +0300 Subject: [PATCH 139/195] new templates: greeter, storage-examples --- contracts/examples/empty/README.md | 47 +++ contracts/examples/empty/mxsc-template.toml | 3 +- contracts/examples/greeter/README.md | 303 ++---------------- contracts/examples/greeter/mxsc-template.toml | 13 + contracts/examples/storage-examples/README.md | 221 ++++--------- .../storage-examples/mxsc-template.toml | 13 + .../{storage_examples_proxy.rs => proxy.rs} | 0 .../storage-examples/src/storage_examples.rs | 8 +- .../tests/storage_examples_blackbox_test.rs | 64 ++-- framework/meta/tests/template_test.rs | 36 +++ 10 files changed, 238 insertions(+), 470 deletions(-) create mode 100644 contracts/examples/empty/README.md create mode 100644 contracts/examples/greeter/mxsc-template.toml create mode 100644 contracts/examples/storage-examples/mxsc-template.toml rename contracts/examples/storage-examples/src/{storage_examples_proxy.rs => proxy.rs} (100%) diff --git a/contracts/examples/empty/README.md b/contracts/examples/empty/README.md new file mode 100644 index 0000000000..f2b20f31ea --- /dev/null +++ b/contracts/examples/empty/README.md @@ -0,0 +1,47 @@ +# Empty Contract + +A minimal MultiversX smart contract with no logic. Intended as a **starting template** when creating a new contract from scratch. + +## Contract + +The contract defines a single trait `EmptyContract` with: + +- `#[init]` — empty initializer, called on deploy +- `#[upgrade]` — empty upgrade handler, called on contract upgrade + +## Project structure + +``` +empty/ +├── src/ +│ └── empty.rs # Contract source +├── tests/ +│ ├── empty_scenario_go_test.rs # Scenario tests (Go runner) +│ └── empty_scenario_rs_test.rs # Scenario tests (Rust runner) +├── scenarios/ +│ └── empty.scen.json # Scenario definition +├── meta/ # Build metadata +├── wasm/ # Wasm build output +├── Cargo.toml +└── multiversx.json +``` + +## Building + +```bash +sc-meta all build +``` + +## Testing + +```bash +cargo test +``` + +## Using as a template + +This contract is registered as an `sc-meta` template under the name `empty`. To scaffold a new project from it: + +```bash +sc-meta new --template empty --name my-contract +``` diff --git a/contracts/examples/empty/mxsc-template.toml b/contracts/examples/empty/mxsc-template.toml index ca48a357fc..6735cf26d5 100644 --- a/contracts/examples/empty/mxsc-template.toml +++ b/contracts/examples/empty/mxsc-template.toml @@ -12,7 +12,8 @@ files_include = [ "scenarios", "src", "tests", + "wasm/Cargo.lock", "Cargo.toml", "multiversx.json", - "wasm/Cargo.lock", + ] diff --git a/contracts/examples/greeter/README.md b/contracts/examples/greeter/README.md index 556e490ad9..45861ded81 100644 --- a/contracts/examples/greeter/README.md +++ b/contracts/examples/greeter/README.md @@ -1,301 +1,58 @@ -# Recipe: New contract from `sc-meta new --template empty` +# Greeter Contract -The real, unedited output of `sc-meta new --template empty`, then the -smallest real customization on top (one storage mapper, one endpoint, -one view) — because a genuinely empty contract doesn't prove the -workflow actually works end to end. Every command below was run for -real in this environment (`rustc 1.93.0`, `cargo 1.93.0`, -`multiversx-sc-meta 0.64.1`) while authoring this recipe — see -"Validation" for exactly what that means and what it doesn't. +A minimal MultiversX smart contract that stores and retrieves a per-caller greeting message. Built on top of the `empty` template by adding one endpoint, one view, and one storage mapper — the smallest meaningful step beyond a bare scaffold. -## Prerequisites +It is also available as an `sc-meta` template (see "Using as a template" below). -- Rust via `rustup`, with the `wasm32v1-none` target installed (see - "A version note" below for why not `wasm32-unknown-unknown`). -- `sc-meta` (`cargo install multiversx-sc-meta`). -- Optional: `wasm-opt`, for size-optimized release builds (this recipe - was verified without it — see Pitfall 3). +## Contract -## Install +The `Greeter` trait exposes: -This recipe's own directory ships the already-generated-and-customized -project directly at its root (`Cargo.toml`, `src/`, `meta/`, etc. — the -same convention every other recipe in this Cookbook uses: clone and go, -no extra scaffolding step required): +| Member | Type | Description | +|---|---|---| +| `init` | `#[init]` | Empty initializer, called on deploy | +| `upgrade` | `#[upgrade]` | Empty upgrade handler | +| `setGreeting(message)` | `#[endpoint]` | Stores a greeting for the calling address | +| `getGreeting(address)` | `#[view]` | Returns the greeting stored for `address`, or an empty buffer if none | -```bash -git clone https://github.com/multiversx/cookbook.git -cd cookbook/recipes/new-contract-from-template -sc-meta all build -cargo test -``` +Storage is keyed per address via a parameterized `SingleValueMapper`, so each caller has an independent greeting. -To reproduce the scaffold from scratch instead (what Step 1 below -walks through): `sc-meta new --template empty --name greeter` creates a -**new subdirectory** named after `--name` — run it in an empty directory -of your own and treat the resulting `greeter/` as your project root -(this recipe's own root is exactly that directory's contents, moved up -one level for consistency with this Cookbook's other recipes). - -## Step 1 — the bare scaffold - -`sc-meta new --template empty --name greeter` does more than copy files: -it renames the trait, the crate names, the module paths, and the -scenario references throughout, all in one pass. This is the real, -captured output of running it standalone (the `greeter/` directory the -command itself creates — this recipe's own root, one level up, IS this -directory's contents, per "Install" above): +## Project structure ``` greeter/ -├── Cargo.toml -├── multiversx.json -├── meta/ -│ ├── Cargo.toml -│ └── src/main.rs ├── src/ -│ └── greeter.rs -├── scenarios/ -│ └── greeter.scen.json -└── tests/ - ├── greeter_scenario_go_test.rs - └── greeter_scenario_rs_test.rs -``` - -`src/greeter.rs`, unmodified from the template: - -```rust -#![no_std] - -#[allow(unused_imports)] -use multiversx_sc::imports::*; - -/// An empty contract. To be used as a template when starting a new contract from scratch. -#[multiversx_sc::contract] -pub trait Greeter { - #[init] - fn init(&self) {} - - #[upgrade] - fn upgrade(&self) {} -} -``` - -Two things worth noting immediately, both confirmed by actually running -the command rather than reading about it: - -- **There is no `sc-config.toml`, no `wasm/`, and no `output/` yet.** - CLAUDE.md's "Smart Contract Project Anatomy" shows these as if every - contract project always has them; a bare `sc-meta new` output doesn't. - `wasm/` and `output/` appear the first time you build (Step 3); - `sc-config.toml` only exists once you need it (Step 2). -- **The scenario test files' function names don't get renamed.** Look at - `tests/greeter_scenario_go_test.rs`: the *file* was renamed from - `empty_scenario_go_test.rs`, and the scenario path inside was updated - to `scenarios/greeter.scen.json` — but the test function itself is - still `fn empty_go()`, not `fn greeter_go()`. The rename pass covers - file names, crate names, and module/trait identifiers; it does not - rewrite every string inside test bodies. Not a bug — just something to - expect rather than be surprised by when you diff a freshly generated - project. - -## Step 2 — add a storage mapper, an endpoint, and a proxy - -`src/greeter.rs` in this recipe adds one parameterized `SingleValueMapper` -(a per-caller greeting) and two members exposing it — the smallest real -next step past the bare scaffold, and the natural bridge into -[Storage mappers: which to pick, when](/smart-contracts-rust/storage-mapper-decision-table/): - -```rust -#![no_std] - -use multiversx_sc::imports::*; - -pub mod greeter_proxy; - -#[multiversx_sc::contract] -pub trait Greeter { - #[init] - fn init(&self) {} - - #[upgrade] - fn upgrade(&self) {} - - #[endpoint(setGreeting)] - fn set_greeting(&self, message: ManagedBuffer) { - let caller = self.blockchain().get_caller(); - self.greeting(&caller).set(message); - } - - #[view(getGreeting)] - #[storage_mapper("greeting")] - fn greeting(&self, address: &ManagedAddress) -> SingleValueMapper; -} +│ ├── greeter.rs # Contract source +│ └── greeter_proxy.rs # Auto-generated proxy (sc-meta all proxy) +├── tests/ +│ └── greeter_blackbox_test.rs +├── meta/ # Build metadata +├── wasm/ # Wasm build output +├── sc-config.toml # Proxy output path +├── Cargo.toml +└── multiversx.json ``` -**Generating the proxy has a real chicken-and-egg order to it.** Adding -`pub mod greeter_proxy;` before the file exists fails the build -(`error[E0583]: file not found for module`) — confirmed by hitting this -error directly while authoring this recipe. The actual working order: - -1. Add `sc-config.toml`: - ```toml - [[proxy]] - path = "src/greeter_proxy.rs" - ``` -2. Write the contract logic WITHOUT the `pub mod greeter_proxy;` line yet. -3. Run `sc-meta all proxy` — this compiles the contract (which doesn't - yet reference the proxy module, so nothing is missing) and generates - `src/greeter_proxy.rs` from the resulting ABI. -4. Add `pub mod greeter_proxy;` now that the file exists. - -## Step 3 — build +## Building ```bash sc-meta all build ``` -Real output, captured while authoring this recipe: - -``` -Building greeter.wasm in .../greeter/wasm ... -RUSTFLAGS="-C link-arg=-s -C link-arg=-zstack-size=131072" cargo +1.93-aarch64-apple-darwin build --target=wasm32v1-none --release ... - Compiling greeter v0.0.0 (...) - Compiling greeter-wasm v0.0.0 (...) - Finished `release` profile [optimized] target(s) in 6.78s -Copying .../target/wasm32v1-none/release/greeter_wasm.wasm to ../output/greeter.wasm ... -Warning: wasm-opt not installed. -Extracting imports to ../output/greeter.imports.json ... -Checking EI version: 1.5 ... OK -Packing ../output/greeter.mxsc.json ... -Contract size: 996 bytes. -``` +## Regenerating the proxy -`sc-meta all build` also regenerates `wasm/src/lib.rs`'s endpoint list on -every run — confirmed by diffing it before and after adding -`setGreeting`/`getGreeting`: it picked both up automatically without any -manual edit: - -```rust -// Code generated by the multiversx-sc build system. DO NOT EDIT. -multiversx_sc_wasm_adapter::endpoints! { - greeter - ( - init => init - upgrade => upgrade - setGreeting => set_greeting - getGreeting => greeting - ) -} +```bash +sc-meta all proxy ``` -## Step 4 — test - -This recipe adds `tests/greeter_blackbox_test.rs` — the scaffold's own -`greeter_scenario_*_test.rs` files only exercise deploy, not the added -logic. Two blackbox tests, following CLAUDE.md §"Blackbox Tests -(RECOMMENDED)" exactly: one confirms `setGreeting` → `getGreeting` -round-trips per caller, the other confirms two different callers get -independent storage (proving the parameterized mapper's key really does -include the address, not just in theory). +## Testing ```bash cargo test ``` -Real output: +## Using as a template +```bash +sc-meta new --template greeter --name my-contract ``` -running 2 tests -test greeting_is_keyed_per_caller ... ok -test set_and_get_greeting ... ok -test result: ok. 2 passed; 0 failed; ... - -running 1 test -test empty_go ... ok - -running 1 test -test empty_rs ... ok -``` - -All 4 tests pass: the 2 new blackbox tests plus the 2 scaffold-provided -scenario tests (whose function names are still `empty_go`/`empty_rs` — -see Step 1). - -## A version note - -Three different version numbers showed up while authoring this recipe, -and they disagree: - -| Source | `multiversx-sc` version | -| --- | --- | -| CLAUDE.md, "Current Version" | 0.65.0 | -| `mx-sdk-rs` GitHub `master` branch, `contracts/examples/empty/Cargo.toml` | 0.66.2 | -| This environment's installed `sc-meta` (0.64.1), and what it actually generated | 0.64.1 | - -`sc-meta new` bundles its own template, versioned to match whatever -`multiversx-sc-meta` release you have installed — it does not fetch the -latest framework version from anywhere. If your installed `sc-meta` is -older than the framework's current release (as this environment's is), -your freshly scaffolded contract pins an older `multiversx-sc` than -CLAUDE.md calls "current." Run `sc-meta upgrade` after scaffolding if you -want the latest framework version, or pass `sc-meta new --tag ` -to pin one explicitly at generation time. - -Also worth noting: the actual build command targets `wasm32v1-none`, not -`wasm32-unknown-unknown` as CLAUDE.md's "Build System" section states — -confirmed directly from the real `sc-meta all build` invocation logged -above. Install the right target via `rustup target add wasm32v1-none` if -`sc-meta install all` hasn't already done it for you. - -## Pitfalls - -1. **Adding `pub mod X_proxy;` before generating the proxy file breaks - the build.** See Step 2's ordering — generate first, declare the - module second. - -2. **A bare `sc-meta new` output has no `sc-config.toml`.** CLAUDE.md's - project anatomy diagram shows one unconditionally; you only need it - once you want proxy generation or a multi-contract build. - -3. **`wasm-opt` not being installed doesn't fail the build** — it's a - warning, and `sc-meta all build` still produces a valid, deployable - `.wasm`/`.mxsc.json`, just without size optimization. Verified - directly: this recipe's build ran with no `wasm-opt` on `PATH` and - still produced a working, tested contract. Install it before shipping - to mainnet if contract size matters to you (bytecode storage has a - real, per-byte on-chain cost). - -4. **Scenario test function names surviving a rename are not a sign - something went wrong.** See Step 1's second bullet. - -5. **`sc-meta new`'s bundled template version may lag the framework's - documented "current" version.** See "A version note" above — check - what actually got generated (`cat Cargo.toml`) rather than assuming it - matches the newest release. - -## See also - -- [Storage mappers: which to pick, when](/smart-contracts-rust/storage-mapper-decision-table/) — - the natural next recipe: this one introduces exactly one mapper - (`SingleValueMapper`, parameterized); that one covers the rest and when - to reach for each. -- [Sign and send a transaction](/start-here/sign-and-send/) — the dApp - side that would eventually call an endpoint like `setGreeting` from a - connected wallet. - -## Validation - -Every command in this README was run for real in this environment: -`sc-meta new --template empty --name greeter` (captured file listing and -diff above), `sc-meta all proxy` (real generated -`src/greeter_proxy.rs`, shown in full in the source), `sc-meta all build` -(real WASM + ABI + `.mxsc.json` output, contract size 996 bytes), and -`cargo test` (4/4 tests passing: 2 new blackbox tests plus the 2 -scaffold-provided scenario tests). This recipe has no `npm install`/ -`tsc`/`eslint` gate — it's Rust, verified with `sc-meta`/`cargo` instead, -per this Cookbook's TypeScript-vs-Rust verification split (see -PROTOTYPE-NOTES.md's FINAL SUMMARY). `target/` and `output/` are -gitignored (regenerable build artifacts, the same convention this -Cookbook's TypeScript recipes use for `node_modules/`/`dist/`); `Cargo.lock` -and `wasm/Cargo.lock` are kept, mirroring `package-lock.json`. diff --git a/contracts/examples/greeter/mxsc-template.toml b/contracts/examples/greeter/mxsc-template.toml new file mode 100644 index 0000000000..307a0e0ad8 --- /dev/null +++ b/contracts/examples/greeter/mxsc-template.toml @@ -0,0 +1,13 @@ +name = "greeter" +contract_trait = "Greeter" +src_file = "greeter.rs" +rename_pairs = [] +files_include = [ + "meta", + "scenarios", + "src", + "tests", + "wasm/Cargo.lock", + "Cargo.toml", + "multiversx.json", +] diff --git a/contracts/examples/storage-examples/README.md b/contracts/examples/storage-examples/README.md index 6807880295..d6ce5bcbe4 100644 --- a/contracts/examples/storage-examples/README.md +++ b/contracts/examples/storage-examples/README.md @@ -1,170 +1,71 @@ -# Recipe: Storage mappers — which to pick, when +# Storage Examples Contract -A working, tested contract exercising six storage mappers side by side, -plus a decision table built from the real `multiversx-sc` crate source -doc comments (`~/.cargo/registry/.../multiversx-sc-0.64.2/src/storage/mappers/*.rs`, -read directly while authoring this recipe) — not just CLAUDE.md's -summary. **Two of CLAUDE.md's own table entries turned out to be wrong** -against that source; see "Corrections to CLAUDE.md's table" below. +A working MultiversX smart contract demonstrating the six most commonly used storage mapper types side by side. Each mapper gets one endpoint group so the companion blackbox tests can verify the behavioral guarantees in the decision table below. -## Prerequisites +It is also available as an `sc-meta` template (see "Using as a template" below). -- Rust via `rustup`, with the `wasm32v1-none` target installed. -- `sc-meta` (`cargo install multiversx-sc-meta`). -- Familiarity with [New contract from `sc-meta new --template empty`](/smart-contracts-rust/new-contract-from-template/) — - this recipe assumes that scaffolding workflow and jumps straight to the - mapper comparison. +## Storage mapper decision table -## Install +| Mapper | Ordering | `contains` | Storage entries (N items) | Iterable | Pick it when | +|---|---|---|---|---|---| +| `SingleValueMapper` | n/a | n/a | 1 | n/a | Exactly one value: a counter, a config flag, a total. | +| `VecMapper` | Insertion order | O(n) | N + 1 | Yes (1-based) | Ordered, indexable, append-friendly; no fast membership check. **Indexes start at 1, not 0.** | +| `SetMapper` | Insertion order | O(1) | ~3N + 1 | Yes, in insertion order | Ordered iteration AND fast membership checks; higher per-element cost than `UnorderedSetMapper`. | +| `UnorderedSetMapper` | None | O(1) | ~2N + 1 | Yes, arbitrary order | Fast membership checks without caring about order — deduplication, processed-ID sets. | +| `WhitelistMapper` | n/a | O(1) | N | **No** | Permission gates where you only ever ask "is X allowed?" and never enumerate members. | +| `MapMapper` | Insertion order of keys | O(1) via `contains_key()` | ~4N + 1 | Yes — `iter()`, `keys()`, `values()` | Key→value store with iteration — balances, per-user settings. Most expensive per element. | + +**Storage entries = unique storage keys, not bytes.** Actual byte cost also depends on the encoded size of `T`. + +Note: `SetMapper` and `UnorderedSetMapper` both provide O(1) `contains()` — the deciding factor between them is ordering and storage cost, not lookup speed. Prefer `UnorderedSetMapper` when insertion order does not matter. + +## Contract endpoints + +| Mapper | Endpoints / Views | +|---|---| +| `SingleValueMapper` | `setCounter`, `getCounter` | +| `VecMapper` | `pushItem`, `getItem`, `itemCount` | +| `SetMapper` | `addToOrderedSet`, `orderedSetContains`, `orderedSetLen` | +| `UnorderedSetMapper` | `addToUnorderedSet`, `unorderedSetContains`, `unorderedSetLen` | +| `WhitelistMapper` | `addToWhitelist`, `isWhitelisted` | +| `MapMapper` | `setBalance`, `getBalance`, `hasBalanceEntry` | + +## Project structure + +``` +storage-examples/ +├── src/ +│ ├── storage_examples.rs # Contract source +│ └── storage_examples_proxy.rs # Auto-generated proxy (sc-meta all proxy) +├── tests/ +│ └── storage_examples_blackbox_test.rs +├── meta/ # Build metadata +├── wasm/ # Wasm build output +├── sc-config.toml # Proxy output path +├── Cargo.toml +└── multiversx.json +``` + +## Building ```bash -git clone https://github.com/multiversx/cookbook.git -cd cookbook/recipes/storage-mapper-decision-table sc-meta all build +``` + +## Regenerating the proxy + +```bash +sc-meta all proxy +``` + +## Testing + +```bash cargo test ``` -## The decision table - -| Mapper | Ordering | Membership check | Storage entries for N items | Iterable? | Pick it when | -| --- | --- | --- | --- | --- | --- | -| `SingleValueMapper` | n/a (one value) | n/a | 1 | n/a | You need exactly one value — a counter, a config flag, a total. | -| `VecMapper` | Insertion order | Linear scan only (no built-in `contains`) | N + 1 (`.len` + one `.item{i}` per element) | Yes, 1 to `len()` | You need ordered, indexable, append-friendly storage and don't need fast membership checks — a log, a history, a queue you only ever read front-to-back. **Indexes start at 1, not 0.** | -| `SetMapper` | Insertion order (doubly-linked internally) | **O(1)** — confirmed from source, contra CLAUDE.md (see below) | ~3N + 1 (info + node_links×N + value×N + node_id-lookup×N) | Yes, in insertion order, plus `next()`/`previous()` navigation | You need both ordered iteration AND fast membership checks, and can afford the higher per-element storage cost — an audit trail of unique events, a whitelist where insertion order itself is meaningful. | -| `UnorderedSetMapper` | None | O(1) | 2N + 1 (`.len` + `.item{i}`×N + `.index{value}`×N) — CLAUDE.md says N+1, undercounting the reverse-lookup keys needed for O(1) `contains` (see below) | Yes, arbitrary order | You need fast membership checks and don't care about order — deduping, a processed-IDs set, a role list you only ever check `contains()` against. | -| `WhitelistMapper` | n/a | O(1), most storage-efficient of the set-shaped mappers | N (one boolean-flag key per item, nothing else) | **No** — cannot enumerate members at all | You only ever need "is X allowed?" and never "list everyone allowed" — permission gates, feature flags, admin checks. | -| `MapMapper` | Insertion order of keys (built on `SetMapper` internally) | O(1) via `contains_key()` | ~4N + 1 (a full `SetMapper` for keys, ~3N+1, plus one `.mapped{key}` value entry per item) | Yes — `.iter()`, `.keys()`, `.values()` | You need a real key→value store with iteration — balances, per-user settings, anything you'd reach for a `HashMap` for off-chain. Confirmed as the one CLAUDE.md entry that checks out exactly ("4N+1 entries (expensive!)"). | -| `LinkedListMapper` | Insertion order, efficient front/back ops | Not built in | ~2N + 1 (info + one node key per element) | Yes | You need efficient push/pop from BOTH ends — a FIFO/LIFO combined structure. Not exercised with working code in this recipe (see "What this recipe didn't test" below); described from CLAUDE.md plus the source module's struct layout only. | - -**Storage-cost column is expressed in unique storage keys, not bytes** — -each entry's actual byte cost also depends on the encoded size of `T`. -The point of this column is relative ordering between mapper choices for -the same logical data, not an absolute gas number. - -## Corrections to CLAUDE.md's table - -Read directly from `multiversx-sc-0.64.2`'s source doc comments while -authoring this recipe — CLAUDE.md's own "Available Storage Mappers" -table has two claims that don't match: - -1. **`SetMapper`'s `contains()` is O(1), not O(n).** CLAUDE.md lists - it as `"O(n) contains"`. The real source's doc comment states - plainly: `"Contains: contains(value) - Checks membership. O(1) with - one storage read."`, and lists `"O(1) insert, remove, and contains"` - as a Pro. `SetMapper` maintains a separate value→node-ID lookup - specifically to make `contains()` O(1) — that's the entire reason its - storage layout is more complex than a plain ordered list. This - recipe's `set_mapper_contains_and_ordering` test exercises `contains()` - for both a present and an absent value; functional correctness is - confirmed, though this recipe doesn't independently benchmark - complexity — the O(1) claim itself is the crate's own documented - design intent, not a claim this recipe re-derived from scratch. - -2. **`UnorderedSetMapper` costs `2N + 1` storage entries, not `N + 1`.** - CLAUDE.md's table lists `"N+1 entries"`. The real source's storage - layout section lists two distinct key groups: value storage - (`.len` + `.item{index}` per element — this is the `N + 1` CLAUDE.md - counted) AND a separate `.index{encoded_value}` reverse-lookup key - per element, which is what actually delivers the O(1) `contains()` - both CLAUDE.md and this recipe agree the mapper provides. You cannot - get O(1) membership testing from `N + 1` keys with no reverse - index — the extra `N` keys are the cost of that guarantee, not an - optional extra. - -Everything else in CLAUDE.md's table (that this recipe independently -checked against source) held up, including `MapMapper`'s "4N+1 entries -(expensive!)" — genuinely the most expensive mapper here per element, -confirmed by reading how it's built on top of `SetMapper` internally -plus its own value storage. - -## Files - -`src/storage_mappers.rs` — one endpoint pair per mapper. -Every method name (`.insert()`, `.contains()`, `.contains_key()`, -`.add()`, `.push()`, `.get()`) is copied from the real crate source's own -doc-comment examples, not guessed — worth calling out since, for -instance, `SetMapper`/`UnorderedSetMapper` both use `.contains()` while -`MapMapper` uses `.contains_key()` instead — an easy name to get wrong by -assuming symmetry across mapper types. - -`tests/storage_mappers_blackbox_test.rs` — six tests, one -per mapper, each proving the specific behavioral claim in the table above -(not just "it compiles"): `VecMapper`'s first push really does land at -index 1; `SetMapper`/`UnorderedSetMapper`'s `contains()` really does -return `true`/`false` correctly for present/absent values; -`WhitelistMapper` really has no enumeration method (only checked -implicitly — the endpoint surface here is `contains()`-only by design, -matching the mapper's own "non-iterable" nature); `MapMapper`'s -`get_balance()` on a key that was never inserted returns `0` -(`unwrap_or_default()`), which is indistinguishable from a real zero -balance unless you also check `contains_key()` — a real design trap -worth testing explicitly, not just describing. - -## Pitfalls - -1. **`VecMapper` is 1-indexed. Index 0 is invalid and panics.** Confirmed - directly: `vec_mapper_is_one_indexed` pushes two items and reads back - index 1 as the first one pushed. - -2. **`MapMapper.get()` returning a default-via-`unwrap_or_default()` value - looks identical to a real stored zero.** If "never set" and "set to - zero" need to be distinguishable in your contract, check - `contains_key()` explicitly rather than trusting a default-valued read - — `map_mapper_insert_get_and_contains_key` tests exactly this gap. - -3. **`SetMapper` and `UnorderedSetMapper` both offer `contains()` at - O(1), so the deciding factor between them is ordering and storage - cost, not lookup speed.** Pick `UnorderedSetMapper` unless you - specifically need insertion-order iteration or `next()`/`previous()` - navigation — it's cheaper per element. - -4. **`WhitelistMapper` cannot list its members at all**, not even - inefficiently. If you might ever need to enumerate — even rarely, even - just for an admin dashboard — use `SetMapper` or `UnorderedSetMapper` - instead from the start; there's no way to add enumeration to a - `WhitelistMapper` later without migrating storage. - -5. **Two different mapper types can use different names for what looks - like the same operation.** `SetMapper`/`UnorderedSetMapper.contains()` - vs `MapMapper.contains_key()` — check the exact mapper's own method - names rather than assuming consistency across the family (the same - general caution CLAUDE.md's own pitfall #14 raises for sdk-core's - Controller/Factory casing, here inside sdk-rs instead). - -## What this recipe didn't test - -`LinkedListMapper`, `QueueMapper`, `UserMapper`, `UniqueIdMapper`, and -`BiDiMapper` are in CLAUDE.md's table and are real, exported mapper -types (confirmed present in the crate's module listing), but this recipe -doesn't ship working code exercising them — six mappers with real, -tested endpoints was already substantial scope for one recipe, and the -first six cover the large majority of real contract storage needs per -this Cookbook's own reading of the example contracts in `mx-sdk-rs`. -`FungibleTokenMapper`/`NonFungibleTokenMapper`/`TokenAttributesMapper` -are deliberately out of scope here too — they need real ESDT system -contract interaction to demonstrate meaningfully, which belongs in a -token-issuance-from-a-contract recipe (Wave 3 per V1-RECIPE-LIST.md's -"What v1 explicitly defers"), not a storage-mapper comparison. - -## See also - -- [New contract from `sc-meta new --template empty`](/smart-contracts-rust/new-contract-from-template/) — - the scaffolding this recipe's contract builds on, including the same - `sc-config.toml` / proxy-generation ordering. -- CLAUDE.md §"Storage Mapper Efficiency" and §"Available Storage - Mappers" — the summary this recipe verifies against real source and - partially corrects. - -## Validation - -`cargo check` passed on the first attempt for every mapper's endpoint -code — every method name used was taken directly from the real crate -source's own doc-comment examples rather than guessed first and fixed -later. `sc-meta all build` produces a real WASM + ABI + `.mxsc.json` -(contract size 5,836 bytes). `cargo test` runs 8/8 passing: the 6 -blackbox tests described above, plus the 2 scaffold-provided scenario -tests (unchanged from `sc-meta new --template empty`'s output). No -`npm`/`tsc`/`eslint` gate applies — this is Rust, verified with -`sc-meta`/`cargo` per this Cookbook's TypeScript-vs-Rust verification -split (see PROTOTYPE-NOTES.md's FINAL SUMMARY). `target/`/`output/` are -gitignored (regenerable); `Cargo.lock`/`wasm/Cargo.lock` are kept. +## Using as a template + +```bash +sc-meta new --template storage-examples --name my-contract +``` diff --git a/contracts/examples/storage-examples/mxsc-template.toml b/contracts/examples/storage-examples/mxsc-template.toml new file mode 100644 index 0000000000..8ffc93ab30 --- /dev/null +++ b/contracts/examples/storage-examples/mxsc-template.toml @@ -0,0 +1,13 @@ +name = "storage-examples" +contract_trait = "StorageExamples" +src_file = "storage_examples.rs" +rename_pairs = [] +files_include = [ + "meta", + "scenarios", + "src", + "tests", + "wasm/Cargo.lock", + "Cargo.toml", + "multiversx.json", +] diff --git a/contracts/examples/storage-examples/src/storage_examples_proxy.rs b/contracts/examples/storage-examples/src/proxy.rs similarity index 100% rename from contracts/examples/storage-examples/src/storage_examples_proxy.rs rename to contracts/examples/storage-examples/src/proxy.rs diff --git a/contracts/examples/storage-examples/src/storage_examples.rs b/contracts/examples/storage-examples/src/storage_examples.rs index 2c119b74c9..8a255bd6c0 100644 --- a/contracts/examples/storage-examples/src/storage_examples.rs +++ b/contracts/examples/storage-examples/src/storage_examples.rs @@ -2,7 +2,7 @@ use multiversx_sc::imports::*; -pub mod storage_examples_proxy; +pub mod proxy; /// Demonstrates the six most commonly used storage mapper types in /// `multiversx-sc`, one endpoint group per mapper. Each group exposes a @@ -13,12 +13,12 @@ pub mod storage_examples_proxy; /// |----------------------|------------|------------|------------|-------------------| /// | `SingleValueMapper` | — | one slot | — | 1 | /// | `VecMapper` | sequential | allowed | O(n) | N + 1 | -/// | `SetMapper` | ordered | rejected | O(1) | 4N + 1 | +/// | `SetMapper` | ordered | rejected | O(1) | ~3N + 1 | /// | `UnorderedSetMapper` | unordered | rejected | O(1) | 2N + 1 | /// | `WhitelistMapper` | — | rejected | O(1) | N | /// | `MapMapper` | keys only | — | O(1) | 4N + 1 | #[multiversx_sc::contract] -pub trait StorageMappers { +pub trait StorageExamples { #[init] fn init(&self) {} @@ -66,7 +66,7 @@ pub trait StorageMappers { // Internally maintains a doubly-linked list of nodes plus a // value→node-id index, giving O(1) `contains`, `insert`, and `remove`. // `insert` returns `true` when the value was newly added, `false` if - // it was already present. Uses ~4N + 1 storage entries for N elements. + // it was already present. Uses ~3N + 1 storage entries for N elements. #[endpoint(addToOrderedSet)] fn add_to_ordered_set(&self, value: u64) -> bool { self.ordered_set().insert(value) diff --git a/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs b/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs index 3530153197..e02cb5ac3b 100644 --- a/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs +++ b/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs @@ -4,7 +4,7 @@ use multiversx_sc_scenario::imports::*; -use storage_examples::storage_examples_proxy; +use storage_examples::proxy; const OWNER: TestAddress = TestAddress::new("owner"); const CONTRACT: TestSCAddress = TestSCAddress::new("storage-examples-contract"); @@ -21,7 +21,7 @@ fn deploy(world: &mut ScenarioWorld) { world .tx() .from(OWNER) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .init() .code(CODE_PATH) .new_address(CONTRACT) @@ -37,14 +37,14 @@ fn single_value_mapper_set_and_get() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .set_counter(BigUint::::from(42u64)) .run(); world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .counter() .returns(ExpectValue(BigUint::::from(42u64))) .run(); @@ -59,7 +59,7 @@ fn single_value_mapper_overwrite() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .set_counter(BigUint::::from(10u64)) .run(); @@ -68,14 +68,14 @@ fn single_value_mapper_overwrite() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .set_counter(BigUint::::from(99u64)) .run(); world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .counter() .returns(ExpectValue(BigUint::::from(99u64))) .run(); @@ -91,7 +91,7 @@ fn vec_mapper_is_one_indexed() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .push_item(ManagedBuffer::::from(b"first")) .returns(ExpectValue(1usize)) .run(); @@ -99,7 +99,7 @@ fn vec_mapper_is_one_indexed() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .push_item(ManagedBuffer::::from(b"second")) .returns(ExpectValue(2usize)) .run(); @@ -107,7 +107,7 @@ fn vec_mapper_is_one_indexed() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .item_count() .returns(ExpectValue(2usize)) .run(); @@ -115,7 +115,7 @@ fn vec_mapper_is_one_indexed() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .get_item(1usize) .returns(ExpectValue(ManagedBuffer::::from(b"first"))) .run(); @@ -123,7 +123,7 @@ fn vec_mapper_is_one_indexed() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .get_item(2usize) .returns(ExpectValue(ManagedBuffer::::from(b"second"))) .run(); @@ -139,7 +139,7 @@ fn set_mapper_contains_and_ordering() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .add_to_ordered_set(value) // A fresh insert must return true. .returns(ExpectValue(true)) @@ -149,7 +149,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .ordered_set_contains(10u64) .returns(ExpectValue(true)) .run(); @@ -157,7 +157,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .ordered_set_contains(99u64) .returns(ExpectValue(false)) .run(); @@ -165,7 +165,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .ordered_set_len() .returns(ExpectValue(3usize)) .run(); @@ -175,7 +175,7 @@ fn set_mapper_contains_and_ordering() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .add_to_ordered_set(10u64) .returns(ExpectValue(false)) .run(); @@ -183,7 +183,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .ordered_set_len() .returns(ExpectValue(3usize)) .run(); @@ -198,7 +198,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .add_to_unordered_set(7u64) // A fresh insert must return true. .returns(ExpectValue(true)) @@ -207,7 +207,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .unordered_set_contains(7u64) .returns(ExpectValue(true)) .run(); @@ -215,7 +215,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .unordered_set_contains(8u64) .returns(ExpectValue(false)) .run(); @@ -223,7 +223,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .unordered_set_len() .returns(ExpectValue(1usize)) .run(); @@ -233,7 +233,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .add_to_unordered_set(7u64) .returns(ExpectValue(false)) .run(); @@ -241,7 +241,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .unordered_set_len() .returns(ExpectValue(1usize)) .run(); @@ -260,14 +260,14 @@ fn whitelist_mapper_membership_only() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .add_to_whitelist(allowed.to_address()) .run(); world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .is_whitelisted(allowed.to_address()) .returns(ExpectValue(true)) .run(); @@ -275,7 +275,7 @@ fn whitelist_mapper_membership_only() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .is_whitelisted(stranger.to_address()) .returns(ExpectValue(false)) .run(); @@ -294,14 +294,14 @@ fn map_mapper_insert_get_and_contains_key() { .tx() .from(OWNER) .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .set_balance(holder.to_address(), BigUint::::from(500u64)) .run(); world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .get_balance(holder.to_address()) .returns(ExpectValue(BigUint::::from(500u64))) .run(); @@ -309,7 +309,7 @@ fn map_mapper_insert_get_and_contains_key() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .has_balance_entry(holder.to_address()) .returns(ExpectValue(true)) .run(); @@ -322,7 +322,7 @@ fn map_mapper_insert_get_and_contains_key() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .has_balance_entry(nobody.to_address()) .returns(ExpectValue(false)) .run(); @@ -330,7 +330,7 @@ fn map_mapper_insert_get_and_contains_key() { world .query() .to(CONTRACT) - .typed(storage_examples_proxy::StorageMappersProxy) + .typed(proxy::StorageMappersProxy) .get_balance(nobody.to_address()) .returns(ExpectValue(BigUint::::from(0u64))) .run(); diff --git a/framework/meta/tests/template_test.rs b/framework/meta/tests/template_test.rs index c6948e71cd..08e065da98 100644 --- a/framework/meta/tests/template_test.rs +++ b/framework/meta/tests/template_test.rs @@ -25,7 +25,9 @@ fn test_template_list() { "adder".to_string(), "crypto-zombies".to_string(), "empty".to_string(), + "greeter".to_string(), "ping-pong-egld".to_string(), + "storage-examples".to_string(), ] ); } @@ -54,6 +56,12 @@ fn template_current_empty() { template_test_current("empty", TEMPLATE_TEST_CURRENT_SUB_PATH, "new-empty"); } +#[test] +#[cfg_attr(not(feature = "template-test-current"), ignore)] +fn template_current_greeter() { + template_test_current("greeter", TEMPLATE_TEST_CURRENT_SUB_PATH, "new-greeter"); +} + #[test] #[cfg_attr(not(feature = "template-test-current"), ignore)] fn template_current_ping_pong_egld() { @@ -64,6 +72,16 @@ fn template_current_ping_pong_egld() { ); } +#[test] +#[cfg_attr(not(feature = "template-test-current"), ignore)] +fn template_current_storage_examples() { + template_test_current( + "storage-examples", + TEMPLATE_TEST_CURRENT_SUB_PATH, + "new-storage-examples", + ); +} + #[test] #[cfg_attr(not(feature = "template-test-current"), ignore)] fn test_correct_naming() { @@ -163,6 +181,24 @@ async fn template_released_empty() { template_test_released("empty", "released-empty").await; } +#[tokio::test] +#[cfg_attr(not(feature = "template-test-released"), ignore)] +async fn template_released_greeter() { + template_test_released("greeter", "released-greeter").await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "template-test-released"), ignore)] +async fn template_released_ping_pong_egld() { + template_test_released("ping-pong-egld", "released-ping-pong-egld").await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "template-test-released"), ignore)] +async fn template_released_storage_examples() { + template_test_released("storage-examples", "released-storage-examples").await; +} + /// These tests fully replicate the templating process. They /// - download the last released version of the repo, /// - create proper contracts, From f71b416276bdf5b0d6d383bbcc894492a2c696b6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 14 Jul 2026 07:37:36 +0300 Subject: [PATCH 140/195] template readme files --- contracts/examples/crypto-zombies/README.md | 56 ++++++++++++++ .../crypto-zombies/mxsc-template.toml | 1 + contracts/examples/empty/mxsc-template.toml | 1 + contracts/examples/greeter/mxsc-template.toml | 2 + contracts/examples/ping-pong-egld/README.md | 77 ++++++++++++++++++- .../storage-examples/mxsc-template.toml | 2 + framework/meta/tests/template_test.rs | 26 ++++++- 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 contracts/examples/crypto-zombies/README.md diff --git a/contracts/examples/crypto-zombies/README.md b/contracts/examples/crypto-zombies/README.md new file mode 100644 index 0000000000..d19887860a --- /dev/null +++ b/contracts/examples/crypto-zombies/README.md @@ -0,0 +1,56 @@ +# Crypto Zombies Contract + +A MultiversX smart contract adapted from the [CryptoZombies](https://cryptozombies.io) tutorial. Demonstrates multi-module contract composition, NFT-style ownership, combat mechanics, and ESDT kitty feeding — making it a practical reference for contracts split across multiple trait modules. + +It is also available as an `sc-meta` template (see "Using as a template" below). + +## Contract modules + +| Module | Description | +|---|---| +| `ZombieFactory` | Create zombies with random DNA; one zombie per address | +| `ZombieFeeding` | Feed a zombie a CryptoKitty to level it up and mutate its DNA | +| `ZombieHelper` | Level-up fee collection; change zombie name/DNA at higher levels | +| `ZombieAttack` | Attack other zombies; win/loss tracking with cooldown | +| `Storage` | All storage mappers shared across modules | + +## Project structure + +``` +crypto-zombies/ +├── src/ +│ ├── lib.rs # Contract entry point +│ ├── zombie.rs # Zombie struct +│ ├── zombie_factory.rs +│ ├── zombie_feeding.rs +│ ├── zombie_helper.rs +│ ├── zombie_attack.rs +│ ├── storage.rs +│ ├── kitty_obj.rs # CryptoKitty struct (for ESDT callback) +│ ├── kitty_ownership_proxy.rs # Proxy for the kitty ownership contract +│ └── proxy.rs # Auto-generated proxy (sc-meta all proxy) +├── tests/ +│ └── crypto_zombies_blackbox_test.rs +├── meta/ # Build metadata +├── wasm/ # Wasm build output +├── Cargo.toml +└── multiversx.json +``` + +## Building + +```bash +sc-meta all build +``` + +## Testing + +```bash +cargo test +``` + +## Using as a template + +```bash +sc-meta new --template crypto-zombies --name my-contract +``` diff --git a/contracts/examples/crypto-zombies/mxsc-template.toml b/contracts/examples/crypto-zombies/mxsc-template.toml index 475743584c..e1ee56e910 100644 --- a/contracts/examples/crypto-zombies/mxsc-template.toml +++ b/contracts/examples/crypto-zombies/mxsc-template.toml @@ -9,5 +9,6 @@ files_include = [ "tests", "Cargo.toml", "multiversx.json", + "README.md", "wasm/Cargo.lock", ] diff --git a/contracts/examples/empty/mxsc-template.toml b/contracts/examples/empty/mxsc-template.toml index 6735cf26d5..dc8c95b3f0 100644 --- a/contracts/examples/empty/mxsc-template.toml +++ b/contracts/examples/empty/mxsc-template.toml @@ -15,5 +15,6 @@ files_include = [ "wasm/Cargo.lock", "Cargo.toml", "multiversx.json", + "README.md", ] diff --git a/contracts/examples/greeter/mxsc-template.toml b/contracts/examples/greeter/mxsc-template.toml index 307a0e0ad8..d3164ea7cb 100644 --- a/contracts/examples/greeter/mxsc-template.toml +++ b/contracts/examples/greeter/mxsc-template.toml @@ -10,4 +10,6 @@ files_include = [ "wasm/Cargo.lock", "Cargo.toml", "multiversx.json", + "README.md", + "sc-config.toml", ] diff --git a/contracts/examples/ping-pong-egld/README.md b/contracts/examples/ping-pong-egld/README.md index 7fadb4a01f..0691304330 100644 --- a/contracts/examples/ping-pong-egld/README.md +++ b/contracts/examples/ping-pong-egld/README.md @@ -1,3 +1,76 @@ -# PingPong +# Ping-Pong EGLD Contract -`PingPong` is a simple Smart Contract. +A MultiversX smart contract that accepts a fixed EGLD amount from users ("ping"), locks it for a configurable duration, then allows users to reclaim their funds ("pong") after the deadline passes. + +Key design points: +- Each address can ping **exactly once**. +- Only the exact `ping_amount` set at deploy is accepted — no more, no less. +- An optional funding cap prevents the contract from accepting more than `max_funds` total. +- An optional `activation_timestamp` delays when pinging opens. +- `pongAll` sends funds back to all registered users in one call, with gas-interrupt-and-resume support for large user sets. + +It is also available as an `sc-meta` template (see "Using as a template" below). + +## Endpoints and views + +| Name | Type | Description | +|---|---|---| +| `init(ping_amount, duration, opt_activation_timestamp, max_funds)` | `#[init]` | Configure the contract on deploy | +| `upgrade(...)` | `#[upgrade]` | Re-configure with the same parameters as `init` | +| `ping` | `#[payable("EGLD")] #[endpoint]` | Lock the exact `ping_amount`; callable once per address while the contract is active | +| `pong` | `#[endpoint]` | Reclaim your `ping_amount` after the deadline | +| `pongAll` | `#[endpoint]` | Send back funds to every registered user; returns `completed` or `interrupted` if gas runs low (resumable) | +| `getUserAddresses` | `#[view]` | List all addresses that pinged, in order | +| `getContractState` | `#[view]` | Snapshot of all configuration and progress fields | +| `getPingAmount` | `#[view]` | The required ping payment | +| `getDeadline` | `#[view]` | Timestamp after which pong is allowed | +| `getActivationTimestamp` | `#[view]` | Timestamp from which ping is allowed | +| `getMaxFunds` | `#[view]` | Optional total funding cap | +| `getUserStatus(user_id)` | `#[view]` | Per-user state: `New` / `Registered` / `Withdrawn` | +| `pongAllLastUser` | `#[view]` | Last user index processed by `pongAll` (0 = idle or complete) | + +## Project structure + +``` +ping-pong-egld/ +├── src/ +│ ├── ping_pong.rs # Contract logic +│ ├── proxy.rs # Auto-generated proxy (sc-meta all proxy) +│ └── types.rs # ContractState, UserStatus types +├── tests/ +│ ├── ping_pong_egld_blackbox_from_scenarios.rs +│ ├── ping_pong_egld_scenario_go_test.rs +│ └── ping_pong_egld_scenario_rs_test.rs +├── scenarios/ # JSON scenario files +├── interactor/ # Interactor (live network) tool +├── snippets/ # Shell snippets +├── meta/ # Build metadata +├── wasm/ # Wasm build output +├── sc-config.toml # Proxy output path +├── Cargo.toml +└── multiversx.json +``` + +## Building + +```bash +sc-meta all build +``` + +## Regenerating the proxy + +```bash +sc-meta all proxy +``` + +## Testing + +```bash +cargo test +``` + +## Using as a template + +```bash +sc-meta new --template ping-pong-egld --name my-contract +``` diff --git a/contracts/examples/storage-examples/mxsc-template.toml b/contracts/examples/storage-examples/mxsc-template.toml index 8ffc93ab30..2bd9a77bf1 100644 --- a/contracts/examples/storage-examples/mxsc-template.toml +++ b/contracts/examples/storage-examples/mxsc-template.toml @@ -10,4 +10,6 @@ files_include = [ "wasm/Cargo.lock", "Cargo.toml", "multiversx.json", + "README.md", + "sc-config.toml", ] diff --git a/framework/meta/tests/template_test.rs b/framework/meta/tests/template_test.rs index 08e065da98..fc8669e3d0 100644 --- a/framework/meta/tests/template_test.rs +++ b/framework/meta/tests/template_test.rs @@ -150,11 +150,31 @@ fn setup_template_test_current( target } +fn check_files_exist(target: &ContractCreatorTarget, template_name: &str) { + assert!( + target.contract_dir().join("README.md").exists(), + "README.md not found in generated contract for template '{template_name}'" + ); + + assert!( + target.contract_dir().join("multiversx.json").exists(), + "multiversx.json not found in generated contract for template '{template_name}'" + ); + + assert!( + !target.contract_dir().join("mxsc-template.toml").exists(), + "mxsc-template.toml should not be present in generated contract for template '{template_name}'" + ); +} + /// Recreates the folder structure in `contracts`, on the same level. /// This way, the relative paths are still valid in this case, /// and we can test the templates with the framework version of the current branch. fn template_test_current(template_name: &str, sub_path: &str, new_name: &str) { let target = setup_template_test_current(template_name, sub_path, new_name); + + check_files_exist(&target, template_name); + if BUILD_CONTRACTS { build_contract(&target); } @@ -182,7 +202,8 @@ async fn template_released_empty() { } #[tokio::test] -#[cfg_attr(not(feature = "template-test-released"), ignore)] +#[ignore = "TODO: un-ignore after release"] +// #[cfg_attr(not(feature = "template-test-released"), ignore)] async fn template_released_greeter() { template_test_released("greeter", "released-greeter").await; } @@ -194,7 +215,8 @@ async fn template_released_ping_pong_egld() { } #[tokio::test] -#[cfg_attr(not(feature = "template-test-released"), ignore)] +#[ignore = "TODO: un-ignore after release"] +// #[cfg_attr(not(feature = "template-test-released"), ignore)] async fn template_released_storage_examples() { template_test_released("storage-examples", "released-storage-examples").await; } From 2e456cda842e7a8bc94286ed23b40286b90a05f7 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 14 Jul 2026 07:45:55 +0300 Subject: [PATCH 141/195] cleanup --- contracts/examples/empty/mxsc-template.toml | 1 - contracts/examples/storage-examples/README.md | 2 +- .../examples/storage-examples/sc-config.toml | 2 +- contracts/examples/storage-examples/src/proxy.rs | 16 ++++++++-------- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/examples/empty/mxsc-template.toml b/contracts/examples/empty/mxsc-template.toml index dc8c95b3f0..ec2af2ea1d 100644 --- a/contracts/examples/empty/mxsc-template.toml +++ b/contracts/examples/empty/mxsc-template.toml @@ -16,5 +16,4 @@ files_include = [ "Cargo.toml", "multiversx.json", "README.md", - ] diff --git a/contracts/examples/storage-examples/README.md b/contracts/examples/storage-examples/README.md index d6ce5bcbe4..10e5b02045 100644 --- a/contracts/examples/storage-examples/README.md +++ b/contracts/examples/storage-examples/README.md @@ -36,7 +36,7 @@ Note: `SetMapper` and `UnorderedSetMapper` both provide O(1) `contains()` — th storage-examples/ ├── src/ │ ├── storage_examples.rs # Contract source -│ └── storage_examples_proxy.rs # Auto-generated proxy (sc-meta all proxy) +│ └── proxy.rs # Auto-generated proxy (sc-meta all proxy) ├── tests/ │ └── storage_examples_blackbox_test.rs ├── meta/ # Build metadata diff --git a/contracts/examples/storage-examples/sc-config.toml b/contracts/examples/storage-examples/sc-config.toml index 657417343b..7556fbf520 100644 --- a/contracts/examples/storage-examples/sc-config.toml +++ b/contracts/examples/storage-examples/sc-config.toml @@ -1,2 +1,2 @@ [[proxy]] -path = "src/storage_examples_proxy.rs" +path = "src/proxy.rs" diff --git a/contracts/examples/storage-examples/src/proxy.rs b/contracts/examples/storage-examples/src/proxy.rs index 59038f7459..e7843840d0 100644 --- a/contracts/examples/storage-examples/src/proxy.rs +++ b/contracts/examples/storage-examples/src/proxy.rs @@ -9,23 +9,23 @@ use multiversx_sc::proxy_imports::*; -pub struct StorageMappersProxy; +pub struct StorageExamplesProxy; -impl TxProxyTrait for StorageMappersProxy +impl TxProxyTrait for StorageExamplesProxy where Env: TxEnv, From: TxFrom, To: TxTo, Gas: TxGas, { - type TxProxyMethods = StorageMappersProxyMethods; + type TxProxyMethods = StorageExamplesProxyMethods; fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { - StorageMappersProxyMethods { wrapped_tx: tx } + StorageExamplesProxyMethods { wrapped_tx: tx } } } -pub struct StorageMappersProxyMethods +pub struct StorageExamplesProxyMethods where Env: TxEnv, From: TxFrom, @@ -36,7 +36,7 @@ where } #[rustfmt::skip] -impl StorageMappersProxyMethods +impl StorageExamplesProxyMethods where Env: TxEnv, Env::Api: VMApi, @@ -54,7 +54,7 @@ where } #[rustfmt::skip] -impl StorageMappersProxyMethods +impl StorageExamplesProxyMethods where Env: TxEnv, Env::Api: VMApi, @@ -73,7 +73,7 @@ where } #[rustfmt::skip] -impl StorageMappersProxyMethods +impl StorageExamplesProxyMethods where Env: TxEnv, Env::Api: VMApi, From 01882dcfc28d8175042de279224ec68b0cd0dffc Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 14 Jul 2026 08:50:33 +0300 Subject: [PATCH 142/195] cleanup --- .../tests/storage_examples_blackbox_test.rs | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs b/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs index e02cb5ac3b..3fecb7410d 100644 --- a/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs +++ b/contracts/examples/storage-examples/tests/storage_examples_blackbox_test.rs @@ -21,7 +21,7 @@ fn deploy(world: &mut ScenarioWorld) { world .tx() .from(OWNER) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .init() .code(CODE_PATH) .new_address(CONTRACT) @@ -37,14 +37,14 @@ fn single_value_mapper_set_and_get() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .set_counter(BigUint::::from(42u64)) .run(); world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .counter() .returns(ExpectValue(BigUint::::from(42u64))) .run(); @@ -59,7 +59,7 @@ fn single_value_mapper_overwrite() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .set_counter(BigUint::::from(10u64)) .run(); @@ -68,14 +68,14 @@ fn single_value_mapper_overwrite() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .set_counter(BigUint::::from(99u64)) .run(); world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .counter() .returns(ExpectValue(BigUint::::from(99u64))) .run(); @@ -91,7 +91,7 @@ fn vec_mapper_is_one_indexed() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .push_item(ManagedBuffer::::from(b"first")) .returns(ExpectValue(1usize)) .run(); @@ -99,7 +99,7 @@ fn vec_mapper_is_one_indexed() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .push_item(ManagedBuffer::::from(b"second")) .returns(ExpectValue(2usize)) .run(); @@ -107,7 +107,7 @@ fn vec_mapper_is_one_indexed() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .item_count() .returns(ExpectValue(2usize)) .run(); @@ -115,7 +115,7 @@ fn vec_mapper_is_one_indexed() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .get_item(1usize) .returns(ExpectValue(ManagedBuffer::::from(b"first"))) .run(); @@ -123,7 +123,7 @@ fn vec_mapper_is_one_indexed() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .get_item(2usize) .returns(ExpectValue(ManagedBuffer::::from(b"second"))) .run(); @@ -139,7 +139,7 @@ fn set_mapper_contains_and_ordering() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .add_to_ordered_set(value) // A fresh insert must return true. .returns(ExpectValue(true)) @@ -149,7 +149,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .ordered_set_contains(10u64) .returns(ExpectValue(true)) .run(); @@ -157,7 +157,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .ordered_set_contains(99u64) .returns(ExpectValue(false)) .run(); @@ -165,7 +165,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .ordered_set_len() .returns(ExpectValue(3usize)) .run(); @@ -175,7 +175,7 @@ fn set_mapper_contains_and_ordering() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .add_to_ordered_set(10u64) .returns(ExpectValue(false)) .run(); @@ -183,7 +183,7 @@ fn set_mapper_contains_and_ordering() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .ordered_set_len() .returns(ExpectValue(3usize)) .run(); @@ -198,7 +198,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .add_to_unordered_set(7u64) // A fresh insert must return true. .returns(ExpectValue(true)) @@ -207,7 +207,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .unordered_set_contains(7u64) .returns(ExpectValue(true)) .run(); @@ -215,7 +215,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .unordered_set_contains(8u64) .returns(ExpectValue(false)) .run(); @@ -223,7 +223,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .unordered_set_len() .returns(ExpectValue(1usize)) .run(); @@ -233,7 +233,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .add_to_unordered_set(7u64) .returns(ExpectValue(false)) .run(); @@ -241,7 +241,7 @@ fn unordered_set_mapper_contains_after_insert_and_absent_value() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .unordered_set_len() .returns(ExpectValue(1usize)) .run(); @@ -260,14 +260,14 @@ fn whitelist_mapper_membership_only() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .add_to_whitelist(allowed.to_address()) .run(); world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .is_whitelisted(allowed.to_address()) .returns(ExpectValue(true)) .run(); @@ -275,7 +275,7 @@ fn whitelist_mapper_membership_only() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .is_whitelisted(stranger.to_address()) .returns(ExpectValue(false)) .run(); @@ -294,14 +294,14 @@ fn map_mapper_insert_get_and_contains_key() { .tx() .from(OWNER) .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .set_balance(holder.to_address(), BigUint::::from(500u64)) .run(); world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .get_balance(holder.to_address()) .returns(ExpectValue(BigUint::::from(500u64))) .run(); @@ -309,7 +309,7 @@ fn map_mapper_insert_get_and_contains_key() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .has_balance_entry(holder.to_address()) .returns(ExpectValue(true)) .run(); @@ -322,7 +322,7 @@ fn map_mapper_insert_get_and_contains_key() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .has_balance_entry(nobody.to_address()) .returns(ExpectValue(false)) .run(); @@ -330,7 +330,7 @@ fn map_mapper_insert_get_and_contains_key() { world .query() .to(CONTRACT) - .typed(proxy::StorageMappersProxy) + .typed(proxy::StorageExamplesProxy) .get_balance(nobody.to_address()) .returns(ExpectValue(BigUint::::from(0u64))) .run(); From 56b9446c014f46e2e7a55c25c97cf897d7b03dab Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 14 Jul 2026 13:17:10 +0300 Subject: [PATCH 143/195] ledger implementation plan --- framework/meta/ledger.md | 420 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 framework/meta/ledger.md diff --git a/framework/meta/ledger.md b/framework/meta/ledger.md new file mode 100644 index 0000000000..1319bb3914 --- /dev/null +++ b/framework/meta/ledger.md @@ -0,0 +1,420 @@ +# Ledger Hardware Wallet Integration Plan + +## Overview + +This document outlines the plan for implementing Ledger hardware wallet support, translated from the Python SDK (`mx-sdk-py` + `mx-sdk-py-cli`). + +The goal is: +1. A `LedgerApp` struct in `sdk/core` (feature-gated) that handles raw APDU communication. +2. A `Signer` enum embedded in `Wallet`, replacing the `private_key` field, that holds either a `PrivateKey` or a Ledger signer. +3. A `--ledger` flag on `SenderArgs` (and a `ledger` field in `WalletConfig`) so any signing path can use the Ledger device. +4. A `sc-meta ledger` CLI subcommand to list addresses and query app version. + +Guardians and relayers are out of scope. + +--- + +## Python Reference + +### `LedgerApp` (`multiversx_sdk/ledger/ledger_app.py`) + +The core class. Wraps raw HID communication via the `ledgercomm` Python library. + +**APDU constants** (`CLA = 0xED`): + +| Instruction | INS | Description | +|--------------------------|------|-------------------------------------| +| `GET_APP_CONFIG` | 0x02 | App version, data activated, indexes| +| `GET_ADDRESS` | 0x03 | Get bech32 address for an index | +| `SET_ADDRESS` | 0x05 | Set active address index on device | +| `SIGN_MESSAGE` | 0x06 | Sign arbitrary message | +| `SIGN_HASH_TX` | 0x07 | Sign transaction (hash-based) | +| `PROVIDE_ESDT_INFO` | 0x08 | (Out of scope) | +| `GET_ADDRESS_AUTH_TOKEN` | 0x09 | (Out of scope) | + +**Chunked signing protocol:** +- Maximum chunk size: 150 bytes +- First chunk: `p1 = 0x00` +- Subsequent chunks: `p1 = 0x80` +- `p2 = 0x00` always +- Response: 65 bytes — byte 0 is `0x40` (length prefix), bytes 1..=64 are the ed25519 signature + +**Address derivation path:** `m/44'/508'/account'/0'/address_index'` +- `account` is always `0` for MultiversX +- `address_index` selects the address + +**Error codes:** + +| Code | Meaning | +|--------|--------------------------------| +| 0x9000 | Success | +| 0x6985 | User denied | +| 0x6D00 | Unknown instruction | +| 0x6E00 | Wrong CLA | +| 0x6E10 | Signature failed | +| 0x6E01 | Invalid arguments | +| 0x6E02 | Invalid message | +| 0x6E03 | Invalid p1 | +| 0x6E04 | Message too long | +| 0x6E07 | Contract data disabled | +| 0x6E09 | Wrong tx version | +| 0x6E0F | Wrong tx options | +| 0x6E11 | Regular signing is deprecated | + +### `LedgerAccount` (`multiversx_sdk/accounts/ledger_account.py`) + +Wraps `LedgerApp` and exposes `sign_transaction` / `sign_message`. + +- `sign_transaction` requires hash-signing options set: `tx.version >= 2` and `tx.options & 1 == 1`. +- Message signing prepends a 4-byte big-endian length prefix to the message data. + +### CLI (`multiversx_sdk_cli/cli_ledger.py`) + +Two subcommands: +- `ledger addresses [--num-addresses N]` — prints `account_index | address_index | address` for N addresses. +- `ledger version` — prints the app version string (`MAJOR.MINOR.PATCH`). + +--- + +## Rust Crates + +### Option A: `ledger-transport-hid` + `ledger-apdu` (Recommended) + +- **`ledger-apdu`** (crates.io, Zondax): `APDUCommand` and `APDUAnswer` types with proper framing. +- **`ledger-transport-hid`** (crates.io, Zondax): HID transport over `hidapi`. Handles Ledger's 64-byte HID packet framing automatically (non-trivial to implement from scratch). + +These are maintained by Zondax — the same company that maintains the official [MultiversX Ledger app](https://github.com/LedgerHQ/app-multiversx). + +```toml +[dependencies] +ledger-transport-hid = "0.10" +ledger-apdu = "0.10" +``` + +### Option B: `hidapi` directly + +Use the raw `hidapi` crate. Requires manually implementing the Ledger HID framing protocol (64-byte packets with channel/sequence headers). Not recommended — `ledger-transport-hid` does this correctly. + +### Feature flag + +The Ledger crates link against `libhidapi` (a C library; on Linux it also requires `libudev`). This must not be forced on all users of `multiversx-sdk` or `multiversx-sc-snippets`. Gate behind a `ledger` feature in each relevant crate: + +| Crate | Feature | Default | +|--------------------------|-----------|---------| +| `multiversx-sdk` | `ledger` | no | +| `multiversx-sc-snippets` | `ledger` | no | +| `multiversx-sc-meta` | `ledger` | **yes** | + +`sc-meta` enables it by default because it is a standalone developer tool — Ledger support is expected to be available out of the box when running `sc-meta tx sign --ledger` or `sc-meta ledger addresses`. + +CI pipelines running `sc-meta` builds may need `libhidapi-dev` (and `libudev-dev` on Linux) installed. + +--- + +## Testing Without a Physical Device + +### Speculos (Official Ledger Emulator) + +- **Repo:** https://github.com/LedgerHQ/speculos +- Emulates Ledger Nano S/X/S+ using QEMU + ARM cross-compilation. +- Exposes an APDU endpoint on TCP port 9999. +- The `ledger-transport-tcp` crate (Zondax) connects to it. +- **Requires:** the MultiversX Ledger app `.elf` binary (from https://github.com/LedgerHQ/app-multiversx), Python, QEMU. +- Suitable for integration tests in CI if the app binary is bundled. + +### Mock Transport (Unit Tests) + +Abstract the transport behind a trait so unit tests can inject mock responses: + +```rust +pub trait LedgerTransport { + fn exchange(&self, command: &APDUCommand) -> Result; +} + +pub struct LedgerApp { transport: T } +``` + +This allows testing APDU encoding/decoding, chunking logic, error mapping, and response parsing without any device or emulator. + +### Integration Test Strategy + +| Scenario | Approach | +|-------------------------------|--------------------------------------------| +| APDU encoding/chunking | Unit tests with mock transport | +| Response parsing / error codes| Unit tests with mock transport | +| Full sign flow | Integration test against Speculos (CI opt-in, guarded by feature/env var) | +| Physical device smoke test | Manual test only | + +--- + +## Implementation Plan + +### 1. `Signer` enum and `Wallet` restructure (in `sdk/core`) + +Replace the `private_key: PrivateKey` field in `Wallet` with a `signer: Signer` field. + +**`Signer` enum** — always compiled, no `#[cfg]` on the type or its variants: + +```rust +pub enum Signer { + PrivateKey(PrivateKey), + Ledger { address_index: u32 }, +} +``` + +The `Ledger` variant holds only a `u32`, so it has no compile-time dependency on `libhidapi`. The enum is always available, which means `--ledger` flags in configs and CLIs work unconditionally. Runtime signing with `Signer::Ledger` will return an error if the `ledger` feature is absent (see §4). + +**Updated `Wallet` struct:** + +```rust +pub struct Wallet { + pub signer: Signer, // replaces private_key: PrivateKey + pub address: Address, + pub source: WalletSource, +} +``` + +`Wallet::new(private_key, source)` remains for the `PrivateKey` path (address is still derived from the key). A new `Wallet::new_ledger(address, address_index)` constructor is added (always available — it just stores the address and `Signer::Ledger { address_index }`). + +**Impact on existing `Wallet` methods:** + +| Method | Change | +|---------------------|------------------------------------------------------------------------| +| `private_key_hex()` | Returns `Option` — `None` for Ledger | +| `public_key()` | Returns `Option` — `None` for Ledger | +| `public_key_hex()` | Returns `Option` — `None` for Ledger | +| `to_pem()` | Returns `Result` — `Err` for Ledger | +| `sign_tx()` | Dispatches on `Signer`; see §4 | +| `sign_bytes()` | Dispatches on `Signer`; see §4 | + +### 2. `WalletSource::Ledger` (in `sdk/core`) + +Add a new variant to `WalletSource` (always compiled, no `#[cfg]`): + +```rust +pub enum WalletSource { + Mnemonic, + PrivateKey, + PemFile(Bech32Hrp), + TestWallet(&'static str), + Keystore(Bech32Hrp), + Ledger { address_index: u32, hrp: Bech32Hrp }, // NEW +} +``` + +`WalletSource` serves as display / bech32-HRP metadata, consistent with `PemFile` and `Keystore`. `Wallet::to_bech32()` reads `hrp` from the `Ledger` variant just like the other two. The `hrp` is populated from the bech32 address returned by the device when constructing the wallet. + +### 3. `LedgerApp` (in `sdk/core`, feature-gated) + +New module: `sdk/core/src/wallet/ledger/` + +``` +sdk/core/src/wallet/ledger/ + mod.rs — re-exports; entire module gated with #[cfg(feature = "ledger")] + ledger_app.rs — LedgerApp struct, APDU communication + ledger_error.rs — LedgerError enum, error code mapping + ledger_config.rs — LedgerAppConfiguration struct +``` + +**`LedgerApp` API** (only exists when `feature = "ledger"` is active): + +```rust +pub struct LedgerApp { /* HID transport */ } + +impl LedgerApp { + pub fn new() -> Result; + pub fn get_app_configuration(&self) -> Result; + pub fn get_version(&self) -> Result; + pub fn get_address(&self, address_index: u32) -> Result; + pub fn set_address(&self, address_index: u32) -> Result<(), LedgerError>; + pub fn sign_transaction(&self, tx_bytes: &[u8]) -> Result<[u8; 64], LedgerError>; + pub fn sign_message(&self, message_bytes: &[u8]) -> Result<[u8; 64], LedgerError>; +} +``` + +Internally `sign_transaction` and `sign_message` share a `do_sign(data, ins)` helper that: +1. Splits `data` into chunks of at most 150 bytes. +2. Sends APDUs: `p1=0x00` for first chunk, `p1=0x80` for subsequent. +3. Parses the 65-byte response: skips byte 0 (`0x40`), returns bytes 1..=64. + +### 4. Signing dispatch in `Wallet::sign_tx` and `sign_bytes` + +`Wallet::sign_tx` dispatches on `self.signer`: + +```rust +pub fn sign_tx(&self, tx: &Transaction) -> Result { + match &self.signer { + Signer::PrivateKey(pk) => { + // existing logic: serialize → optionally keccak256 → sign + } + Signer::Ledger { address_index } => { + #[cfg(feature = "ledger")] + { + // enforce hash-signing options, open LedgerApp, sign + } + #[cfg(not(feature = "ledger"))] + { + anyhow::bail!( + "Ledger signing is not available; \ + recompile with the `ledger` feature enabled" + ) + } + } + } +} +``` + +The same pattern applies to `sign_bytes`. + +**Transaction signing requirements:** The MultiversX Ledger app requires hash-based signing: +- `tx.version >= 2` and `tx.options & 1 == 1` +- The raw JSON bytes (not the hash) are sent to the device — the device hashes internally. +- If the options are not set, `sign_tx` returns an `Err` (matching Python behaviour). + +### 5. `SenderArgs` extension (in `framework/meta`) + +The new fields are **always** present — no `#[cfg]`: + +```rust +pub struct SenderArgs { + #[arg(long, group = "wallet_source")] + pub pem: Option, + + #[arg(long, group = "wallet_source")] + pub keyfile: Option, + + #[arg(long = "keystore-password")] + pub keystore_password: Option, + + /// Use Ledger hardware wallet for signing. + #[arg(long, group = "wallet_source")] + pub ledger: bool, + + /// Address index to use on the Ledger device (default: 0). + #[arg(long = "ledger-address-index", default_value = "0")] + pub ledger_address_index: u32, +} +``` + +`load_wallet` constructs a `Wallet` with `Signer::Ledger` when `sender.ledger` is true. The address is fetched from the device at that point (requires the `ledger` feature; returns `Err` otherwise). Signing then goes through `Wallet::sign_tx` as normal, which also checks the feature at the point of signing. + +### 6. `WalletConfig` extension (in `framework/snippets`) + +The new fields are **always** present — no `#[cfg]`: + +```rust +pub struct WalletConfig { + pub test_wallet: Option, + pub pem: Option, + pub keyfile: Option, + pub keystore_password: Option, + pub ledger: bool, // NEW + pub ledger_address_index: u32, // NEW (default 0) + cache: OnceLock, +} +``` + +`WalletConfig::load_wallet` adds a branch for `ledger == true` that constructs a `Wallet::new_ledger(...)`. Attempting to use this branch without `feature = "ledger"` produces the same runtime error as §4. + +### 7. `sc-meta ledger` CLI subcommand + +New variant in `StandaloneCliAction`: + +```rust +#[command(name = "ledger", about = "Interact with a Ledger hardware wallet.")] +Ledger(LedgerArgs), +``` + +New file: `framework/meta/src/cli/cli_args_ledger.rs` + +```rust +#[derive(Args)] +pub struct LedgerArgs { + #[command(subcommand)] + pub command: LedgerAction, +} + +#[derive(Subcommand)] +pub enum LedgerAction { + #[command(about = "List addresses stored on the Ledger device.")] + Addresses(LedgerAddressesArgs), + + #[command(about = "Print the version of the MultiversX Ledger app.")] + Version, +} + +#[derive(Args)] +pub struct LedgerAddressesArgs { + /// Number of addresses to retrieve (default: 10). + #[arg(long = "num-addresses", default_value = "10")] + pub num_addresses: u32, +} +``` + +The `sc-meta ledger` subcommand handlers live in `framework/meta/src/cmd/ledger_cmd.rs` and are **always compiled** — no `#[cfg]` on the functions or the CLI types. Only the inner calls to `LedgerApp::new()` are guarded, returning a clear error when the `ledger` feature is absent (same pattern as `Wallet::sign_tx` in §4). Since `sc-meta` enables the `ledger` feature by default, the guard is never hit in normal usage. + +Output format (matching Python): +``` +account index = 0 | address index = 0 | address: erd1... +account index = 0 | address index = 1 | address: erd1... +``` + +--- + +## File Structure Summary + +``` +sdk/core/src/wallet/ + signer.rs — NEW: Signer enum (PrivateKey | Ledger) + wallet_source.rs — add Ledger { address_index: u32 } variant + wallet_impl.rs — replace private_key field; update sign_tx/sign_bytes + ledger/ + mod.rs — NEW: #[cfg(feature = "ledger")] + ledger_app.rs — NEW: LedgerApp, APDU protocol, chunked signing + ledger_config.rs — NEW: LedgerAppConfiguration + ledger_error.rs — NEW: LedgerError, error code mapping +sdk/core/Cargo.toml — add ledger-transport-hid, ledger-apdu as optional deps + +framework/snippets/src/config/wallet_config.rs — add ledger / ledger_address_index fields +framework/snippets/Cargo.toml — add ledger feature forwarding to multiversx-sdk + +framework/meta/src/cli/cli_args_ledger.rs — NEW: LedgerArgs, LedgerAction, LedgerAddressesArgs +framework/meta/src/cli/cli_args_sender.rs — add --ledger / --ledger-address-index flags +framework/meta/src/cli/cli_args_standalone.rs — add Ledger subcommand +framework/meta/src/cmd/ledger_cmd.rs — NEW: CLI command handlers +framework/meta/Cargo.toml — ledger feature default; forward to multiversx-sdk +``` + +--- + +## Cargo.toml Changes + +**`sdk/core/Cargo.toml`:** + +```toml +[features] +ledger = ["dep:ledger-transport-hid", "dep:ledger-apdu"] + +[dependencies] +ledger-transport-hid = { version = "0.10", optional = true } +ledger-apdu = { version = "0.10", optional = true } +``` + +**`framework/snippets/Cargo.toml`:** + +```toml +[features] +ledger = ["multiversx-sdk/ledger"] +``` + +**`framework/meta/Cargo.toml`:** + +```toml +[features] +default = ["ledger"] +ledger = ["multiversx-sdk/ledger"] +``` + +> **Note on `libhidapi`:** `ledger-transport-hid` links against `hidapi` (a C library). On Linux it also requires `libudev`. CI pipelines building `sc-meta` (which enables `ledger` by default) may need `libhidapi-dev` / `libudev-dev` installed. + + From 6250277d81719a0aa009b8d38325304fd2cefca6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 15 Jul 2026 12:24:57 +0300 Subject: [PATCH 144/195] ledger implementation --- Cargo.lock | 132 +++++++++++++- framework/meta/Cargo.toml | 2 + framework/meta/src/cli.rs | 2 + framework/meta/src/cli/cli_args_ledger.rs | 24 +++ framework/meta/src/cli/cli_args_sender.rs | 37 +++- framework/meta/src/cli/cli_args_standalone.rs | 7 + framework/meta/src/cli/cli_standalone_main.rs | 4 + framework/meta/src/cmd.rs | 1 + framework/meta/src/cmd/ledger_cmd.rs | 47 +++++ .../src/cmd/reproducible_builds/publish.rs | 4 +- .../src/cmd/reproducible_builds/unpublish.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_common.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_new.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_sign.rs | 2 +- framework/meta/src/cmd/wallet_cmd.rs | 10 +- .../meta/tests/wallet_convert_cli_test.rs | 8 +- .../tests/wallet_encrypt_keystore_test.rs | 14 +- framework/snippets/Cargo.toml | 1 + .../snippets/src/config/wallet_config.rs | 43 ++++- .../src/interactor/interactor_sender.rs | 5 +- .../src/multi/interactor_multi_sc_process.rs | 5 +- sdk/core/Cargo.toml | 5 + sdk/core/src/wallet.rs | 5 + sdk/core/src/wallet/ledger.rs | 7 + sdk/core/src/wallet/ledger/ledger_app.rs | 165 ++++++++++++++++++ sdk/core/src/wallet/ledger/ledger_config.rs | 9 + sdk/core/src/wallet/ledger/ledger_error.rs | 65 +++++++ sdk/core/src/wallet/signer.rs | 20 +++ sdk/core/src/wallet/wallet_impl.rs | 155 +++++++++++++--- sdk/core/src/wallet/wallet_source.rs | 6 + sdk/core/tests/wallet_keystore_test.rs | 10 +- sdk/core/tests/wallet_pem_test.rs | 14 +- sdk/core/tests/wallet_sign_test.rs | 46 +++-- sdk/http/examples/sign_tx.rs | 2 +- sdk/http/examples/sign_txs.rs | 4 +- tools/wallet-miner/src/main.rs | 2 +- 36 files changed, 786 insertions(+), 83 deletions(-) create mode 100644 framework/meta/src/cli/cli_args_ledger.rs create mode 100644 framework/meta/src/cmd/ledger_cmd.rs create mode 100644 sdk/core/src/wallet/ledger.rs create mode 100644 sdk/core/src/wallet/ledger/ledger_app.rs create mode 100644 sdk/core/src/wallet/ledger/ledger_config.rs create mode 100644 sdk/core/src/wallet/ledger/ledger_error.rs create mode 100644 sdk/core/src/wallet/signer.rs diff --git a/Cargo.lock b/Cargo.lock index 6a7c6a3695..17c056891c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,12 +178,29 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -632,7 +649,7 @@ version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.118", @@ -1204,6 +1221,12 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + [[package]] name = "dunce" version = "1.0.5" @@ -2228,6 +2251,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2261,6 +2290,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "hidapi" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "winapi", +] + [[package]] name = "hmac" version = "0.13.0" @@ -2793,6 +2834,43 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "ledger-apdu" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu", +] + +[[package]] +name = "ledger-transport" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +dependencies = [ + "byteorder", + "cfg-if", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror 1.0.69", +] + [[package]] name = "libc" version = "0.2.186" @@ -3414,6 +3492,8 @@ dependencies = [ "hex", "hmac", "itertools 0.15.0", + "ledger-apdu", + "ledger-transport-hid", "log", "multiversx-chain-core", "pbkdf2", @@ -3565,6 +3645,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -5138,6 +5224,28 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.6.4" @@ -6214,6 +6322,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -6223,6 +6347,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/framework/meta/Cargo.toml b/framework/meta/Cargo.toml index 90c486318f..1273fc43b6 100644 --- a/framework/meta/Cargo.toml +++ b/framework/meta/Cargo.toml @@ -21,6 +21,8 @@ name = "sc-meta" path = "src/main.rs" [features] +default = ["ledger"] +ledger = ["multiversx-sdk/ledger"] template-test-current = [] template-test-released = [] chain-simulator-tests = [] diff --git a/framework/meta/src/cli.rs b/framework/meta/src/cli.rs index bfbddd6d30..9f98ed4a61 100644 --- a/framework/meta/src/cli.rs +++ b/framework/meta/src/cli.rs @@ -1,3 +1,4 @@ +pub mod cli_args_ledger; pub mod cli_args_reproducible_builds; pub mod cli_args_sender; mod cli_args_standalone; @@ -5,6 +6,7 @@ pub mod cli_args_tx; mod cli_args_validate; mod cli_standalone_main; +pub use cli_args_ledger::*; pub use cli_args_standalone::*; pub use cli_args_validate::*; pub use cli_standalone_main::*; diff --git a/framework/meta/src/cli/cli_args_ledger.rs b/framework/meta/src/cli/cli_args_ledger.rs new file mode 100644 index 0000000000..f04461a4d2 --- /dev/null +++ b/framework/meta/src/cli/cli_args_ledger.rs @@ -0,0 +1,24 @@ +use clap::{Args, Subcommand}; + +/// Arguments for the `sc-meta ledger` subcommand group. +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct LedgerArgs { + #[command(subcommand)] + pub command: LedgerAction, +} + +#[derive(Clone, PartialEq, Eq, Debug, Subcommand)] +pub enum LedgerAction { + #[command(about = "List addresses stored on the Ledger device.")] + Addresses(LedgerAddressesArgs), + + #[command(about = "Print the version of the MultiversX Ledger app.")] + Version, +} + +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct LedgerAddressesArgs { + /// Number of addresses to retrieve. + #[arg(long = "num-addresses", default_value = "10")] + pub num_addresses: u32, +} diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index 2dd337c077..69992e9d36 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -17,9 +17,17 @@ pub struct SenderArgs { /// Keystore password (plain text). If omitted, will prompt interactively. #[arg(long = "keystore-password", verbatim_doc_comment)] pub keystore_password: Option, + + /// Use a Ledger hardware wallet for signing. + #[arg(long, group = "wallet_source")] + pub ledger: bool, + + /// Address index to use on the Ledger device. + #[arg(long = "ledger-address-index", default_value = "0")] + pub ledger_address_index: u32, } -/// Load a wallet from a PEM file or JSON keystore. +/// Load a wallet from a PEM file, JSON keystore, or Ledger device. pub fn load_wallet(sender: &SenderArgs) -> Result { if let Some(pem) = &sender.pem { Wallet::from_pem_file(pem).context("failed to load PEM wallet") @@ -32,11 +40,36 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { keystore .decrypt_wallet(&password) .context("failed to load keystore wallet") + } else if sender.ledger { + load_ledger_wallet(sender.ledger_address_index) } else { - Err(anyhow!("a wallet is required: use --pem or --keyfile")) + Err(anyhow!( + "a wallet is required: use --pem, --keyfile, or --ledger" + )) } } +#[cfg(feature = "ledger")] +fn load_ledger_wallet(address_index: u32) -> Result { + use multiversx_chain_core::std::Bech32Address; + use multiversx_sc_snippets::sdk::wallet::ledger::LedgerApp; + + let app = LedgerApp::new().map_err(|e| anyhow!("{e}"))?; + let bech32_str = app.get_address(address_index).map_err(|e| anyhow!("{e}"))?; + let bech32_addr = Bech32Address::from_bech32_string(bech32_str); + let hrp = bech32_addr.hrp; + let address = bech32_addr.address; + Ok(Wallet::new_ledger(address, hrp, address_index)) +} + +#[cfg(not(feature = "ledger"))] +fn load_ledger_wallet(address_index: u32) -> Result { + let _ = address_index; + Err(anyhow!( + "Ledger support is not available; recompile with the `ledger` feature enabled" + )) +} + pub fn get_keystore_password() -> String { print!("Insert password: "); std::io::stdout().flush().unwrap(); diff --git a/framework/meta/src/cli/cli_args_standalone.rs b/framework/meta/src/cli/cli_args_standalone.rs index fb2ea896d7..dff7d1a1d7 100644 --- a/framework/meta/src/cli/cli_args_standalone.rs +++ b/framework/meta/src/cli/cli_args_standalone.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; pub use super::cli_args_reproducible_builds::*; +use crate::cli::cli_args_ledger::LedgerArgs; use crate::cli::cli_args_tx::TxCliArgs; use multiversx_sc_meta_lib::cli::{CliArgsToRaw, ContractCliAction}; @@ -113,6 +114,12 @@ pub enum StandaloneCliAction { )] Tx(TxCliArgs), + #[command( + name = "ledger", + about = "Interact with a Ledger hardware wallet (list addresses, get app version)." + )] + Ledger(LedgerArgs), + #[command( name = "data", about = "Manages local key-value data storage (store / load / parse)." diff --git a/framework/meta/src/cli/cli_standalone_main.rs b/framework/meta/src/cli/cli_standalone_main.rs index acc611aed9..bed5c02928 100644 --- a/framework/meta/src/cli/cli_standalone_main.rs +++ b/framework/meta/src/cli/cli_standalone_main.rs @@ -1,6 +1,7 @@ use crate::cli::{ReproducibleBuildCliAction, StandaloneCliAction, StandaloneCliArgs}; use crate::cmd::chain_simulator::chain_simulator; use crate::cmd::data::data_cli; +use crate::cmd::ledger_cmd::ledger_cmd; use crate::cmd::retrieve_address::retrieve_address; use crate::cmd::scen_blackbox::scen_blackbox_tool; use crate::cmd::tx::tx_cli; @@ -90,6 +91,9 @@ pub async fn cli_main_standalone() { Some(StandaloneCliAction::Tx(args)) => { tx_cli(args).await; } + Some(StandaloneCliAction::Ledger(args)) => { + ledger_cmd(args); + } Some(StandaloneCliAction::Data(args)) => { data_cli(args); } diff --git a/framework/meta/src/cmd.rs b/framework/meta/src/cmd.rs index ca2c2d2071..7e97b1cd07 100644 --- a/framework/meta/src/cmd.rs +++ b/framework/meta/src/cmd.rs @@ -5,6 +5,7 @@ pub mod code_report; pub mod data; pub mod info; pub mod install; +pub mod ledger_cmd; pub mod print_util; pub mod reproducible_builds; pub mod retrieve_address; diff --git a/framework/meta/src/cmd/ledger_cmd.rs b/framework/meta/src/cmd/ledger_cmd.rs new file mode 100644 index 0000000000..b0c4c41439 --- /dev/null +++ b/framework/meta/src/cmd/ledger_cmd.rs @@ -0,0 +1,47 @@ +use crate::cli::{LedgerAction, LedgerArgs}; + +pub fn ledger_cmd(args: &LedgerArgs) { + if let Err(e) = ledger_cmd_inner(args) { + eprintln!("Error: {e:#}"); + std::process::exit(1); + } +} + +fn ledger_cmd_inner(args: &LedgerArgs) -> anyhow::Result<()> { + match &args.command { + LedgerAction::Addresses(a) => print_addresses(a.num_addresses), + LedgerAction::Version => print_version(), + } +} + +#[cfg(feature = "ledger")] +fn print_addresses(num_addresses: u32) -> anyhow::Result<()> { + use multiversx_sc_snippets::sdk::wallet::ledger::LedgerApp; + + let app = LedgerApp::new()?; + for i in 0..num_addresses { + let address = app.get_address(i)?; + println!("account index = 0 | address index = {i} | address: {address}"); + } + Ok(()) +} + +#[cfg(not(feature = "ledger"))] +fn print_addresses(_num_addresses: u32) -> anyhow::Result<()> { + anyhow::bail!("Ledger support is not available; recompile with the `ledger` feature enabled") +} + +#[cfg(feature = "ledger")] +fn print_version() -> anyhow::Result<()> { + use multiversx_sc_snippets::sdk::wallet::ledger::LedgerApp; + + let app = LedgerApp::new()?; + let version = app.get_version()?; + println!("MultiversX App version: {version}"); + Ok(()) +} + +#[cfg(not(feature = "ledger"))] +fn print_version() -> anyhow::Result<()> { + anyhow::bail!("Ledger support is not available; recompile with the `ledger` feature enabled") +} diff --git a/framework/meta/src/cmd/reproducible_builds/publish.rs b/framework/meta/src/cmd/reproducible_builds/publish.rs index f284ee9b67..c463e5f05d 100644 --- a/framework/meta/src/cmd/reproducible_builds/publish.rs +++ b/framework/meta/src/cmd/reproducible_builds/publish.rs @@ -113,7 +113,9 @@ pub(super) fn sign_payload( payload: &str, ) -> WalletSignature { let bytes_to_sign = compute_bytes_for_signing(contract, payload); - wallet.sign_bytes(bytes_to_sign) + wallet + .sign_bytes(bytes_to_sign) + .expect("failed to sign payload") } /// POSTs the verification request and returns the HTTP status code and parsed response body. diff --git a/framework/meta/src/cmd/reproducible_builds/unpublish.rs b/framework/meta/src/cmd/reproducible_builds/unpublish.rs index 0f96a19a60..a799138662 100644 --- a/framework/meta/src/cmd/reproducible_builds/unpublish.rs +++ b/framework/meta/src/cmd/reproducible_builds/unpublish.rs @@ -37,7 +37,7 @@ pub async fn unpublish_contract(args: &ReproducibleBuildUnpublishArgs) { // Sign using the same MultiversX message signing protocol as verify. let bytes_to_sign = compute_bytes_for_signing(&contract, &payload); - let signature = wallet.sign_bytes(bytes_to_sign); + let signature = wallet.sign_bytes(bytes_to_sign).expect("failed to sign"); let request_body = serde_json::json!({ "signature": signature, diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index adf8c9405a..6c2d8c4b1f 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -165,7 +165,7 @@ pub async fn sign_and_dispatch( } }; - let sig = wallet.sign_tx(&tx); + let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); let output = TxOutputFile { diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 966d756893..35147ad623 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -68,7 +68,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { tx.chain_id = chain_id.clone(); } - let sig = wallet.sign_tx(&tx); + let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); let output = TxOutputFile { diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 13cff560ee..26e8cf2f51 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -32,7 +32,7 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { tx.chain_id = chain_id.clone(); } - let sig = wallet.sign_tx(&tx); + let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); let decoded_data = match &tx.data { diff --git a/framework/meta/src/cmd/wallet_cmd.rs b/framework/meta/src/cmd/wallet_cmd.rs index 1b2827c7c9..08eef88e63 100644 --- a/framework/meta/src/cmd/wallet_cmd.rs +++ b/framework/meta/src/cmd/wallet_cmd.rs @@ -51,7 +51,7 @@ fn convert(convert_args: &WalletConvertArgs) { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); let wallet = Wallet::try_from(Mnemonic::parse(&mnemonic_str).unwrap()).unwrap(); - write_resulted_pem(wallet.to_pem(hrp), outfile); + write_resulted_pem(wallet.to_pem(hrp).unwrap(), outfile); } None => { println!( @@ -59,7 +59,7 @@ fn convert(convert_args: &WalletConvertArgs) { ); _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); let wallet = Wallet::try_from(Mnemonic::parse(&mnemonic_str).unwrap()).unwrap(); - write_resulted_pem(wallet.to_pem(hrp), outfile); + write_resulted_pem(wallet.to_pem(hrp).unwrap(), outfile); } }, ("keystore-secret", "pem") => match infile { @@ -68,7 +68,7 @@ fn convert(convert_args: &WalletConvertArgs) { match keystore.decrypt_wallet(&get_keystore_password()) { Ok(wallet) => { println!("Password is correct"); - write_resulted_pem(wallet.to_pem(hrp), outfile); + write_resulted_pem(wallet.to_pem(hrp).unwrap(), outfile); } Err(KeystoreError::InvalidPassword) => { panic!("Password is incorrect"); @@ -220,7 +220,7 @@ fn new(new_args: &WalletNewArgs) { match format { Some("pem") => { - write_resulted_pem(new_wallet_info.wallet.to_pem(hrp), outfile); + write_resulted_pem(new_wallet_info.wallet.to_pem(hrp).unwrap(), outfile); if let Some(outfile) = outfile { println!("Wallet saved to '{outfile}'"); } @@ -228,7 +228,7 @@ fn new(new_args: &WalletNewArgs) { Some("keystore-secret") => { let randomness = new_keystore_randomness(); let json_result = Keystore::encrypt( - &new_wallet_info.wallet.private_key, + new_wallet_info.wallet.private_key().unwrap(), hrp, &get_keystore_password(), randomness, diff --git a/framework/meta/tests/wallet_convert_cli_test.rs b/framework/meta/tests/wallet_convert_cli_test.rs index 492533def7..2e2aa35bc5 100644 --- a/framework/meta/tests/wallet_convert_cli_test.rs +++ b/framework/meta/tests/wallet_convert_cli_test.rs @@ -58,8 +58,8 @@ fn test_convert_mnemonic_to_pem_file_in_file_out() { }); let wallet = Wallet::from_pem_file(&output_pem).unwrap(); - assert_eq!(wallet.private_key.to_seed_hex(), KNOWN_PRIVATE_KEY); - assert_eq!(wallet.public_key().to_hex(), KNOWN_PUBLIC_KEY); + assert_eq!(wallet.private_key_hex().unwrap(), KNOWN_PRIVATE_KEY); + assert_eq!(wallet.public_key().unwrap().to_hex(), KNOWN_PUBLIC_KEY); let pem_content = fs::read_to_string(&output_pem).unwrap(); assert!( @@ -141,8 +141,8 @@ fn test_convert_mnemonic_to_pem_custom_hrp() { // Key material must be the same regardless of the HRP let wallet = Wallet::from_pem_file(&output_pem).unwrap(); - assert_eq!(wallet.private_key_hex(), KNOWN_PRIVATE_KEY); - assert_eq!(wallet.public_key_hex(), KNOWN_PUBLIC_KEY); + assert_eq!(wallet.private_key_hex().unwrap(), KNOWN_PRIVATE_KEY); + assert_eq!(wallet.public_key_hex().unwrap(), KNOWN_PUBLIC_KEY); remove_if_exists(&mnemonic_file); remove_if_exists(&output_pem); diff --git a/framework/meta/tests/wallet_encrypt_keystore_test.rs b/framework/meta/tests/wallet_encrypt_keystore_test.rs index 92c6087393..a5e8596c07 100644 --- a/framework/meta/tests/wallet_encrypt_keystore_test.rs +++ b/framework/meta/tests/wallet_encrypt_keystore_test.rs @@ -17,7 +17,7 @@ const ALICE_PRIVATE_KEY: &str = "413f42575f7f26fad3317a778771212fdb80245850981e4 fn create_keystore_file_from_scratch(hrp: &str, file: &str) -> Address { let wallet = Wallet::from(PrivateKey::from_seed_hex_str(ALICE_PRIVATE_KEY).unwrap()); let json_result = Keystore::encrypt( - &wallet.private_key, + wallet.private_key().unwrap(), hrp.try_into().expect("invalid HRP"), KEYSTORE_PASSWORD, new_keystore_randomness(), @@ -36,8 +36,9 @@ fn test_wallet_convert_pem_to_keystore() { .unwrap() .decrypt_wallet(KEYSTORE_PASSWORD) .unwrap() - .private_key, - wallet_pem.private_key + .private_key() + .unwrap(), + wallet_pem.private_key().unwrap() ); fs::remove_file(ALICE_KEYSTORE_PATH_TEST_1).unwrap(); } @@ -50,13 +51,14 @@ fn test_wallet_convert_keystore_to_pem() { .unwrap() .decrypt_wallet(KEYSTORE_PASSWORD) .unwrap(); - let pem_content = wallet.to_pem(Bech32Hrp::default()).to_pem_str(); + let pem_content = wallet.to_pem(Bech32Hrp::default()).unwrap().to_pem_str(); write_to_file(&pem_content, ALICE_PEM_PATH_TEST); assert_eq!( - wallet.private_key, + wallet.private_key().unwrap(), Wallet::from_pem_file(ALICE_PEM_PATH_TEST) .unwrap() - .private_key + .private_key() + .unwrap() ); fs::remove_file(ALICE_PEM_PATH_TEST).unwrap(); diff --git a/framework/snippets/Cargo.toml b/framework/snippets/Cargo.toml index 6e87161487..406c9d1b45 100644 --- a/framework/snippets/Cargo.toml +++ b/framework/snippets/Cargo.toml @@ -18,6 +18,7 @@ default = ["http"] http = ["multiversx-sdk-http", "tokio"] dapp = ["multiversx-sdk-dapp"] chain-simulator-tests = [] +ledger = ["multiversx-sdk/ledger"] [dependencies] hex = "0.4" diff --git a/framework/snippets/src/config/wallet_config.rs b/framework/snippets/src/config/wallet_config.rs index 9c0ea408e2..7d211bda44 100644 --- a/framework/snippets/src/config/wallet_config.rs +++ b/framework/snippets/src/config/wallet_config.rs @@ -16,6 +16,10 @@ struct WalletConfigRaw { pem: Option, keyfile: Option, keystore_password: Option, + #[serde(default)] + ledger: bool, + #[serde(default)] + ledger_address_index: u32, } /// Wallet configuration embeddable in a TOML/JSON config file. @@ -36,6 +40,12 @@ pub struct WalletConfig { /// Keystore password (plain text). Required when `keyfile` is set. pub keystore_password: Option, + /// Use the Ledger hardware device for signing. + pub ledger: bool, + + /// Address index to use on the Ledger device (default: 0). + pub ledger_address_index: u32, + cache: OnceLock, } @@ -46,6 +56,8 @@ impl From for WalletConfig { pem: raw.pem.map(Into::into), keyfile: raw.keyfile.map(Into::into), keystore_password: raw.keystore_password, + ledger: raw.ledger, + ledger_address_index: raw.ledger_address_index, cache: OnceLock::new(), } } @@ -59,6 +71,8 @@ impl WalletConfig { pem: None, keyfile: None, keystore_password: None, + ledger: false, + ledger_address_index: 0, cache: OnceLock::new(), } } @@ -70,13 +84,15 @@ impl WalletConfig { pem: Some(path.into()), keyfile: None, keystore_password: None, + ledger: false, + ledger_address_index: 0, cache: OnceLock::new(), } } /// Returns the wallet, loading and caching it on first call. /// - /// Priority: `test_wallet` > `pem` > `keyfile`. + /// Priority: `test_wallet` > `pem` > `keyfile` > `ledger`. /// Panics if none of the sources are set, or if loading fails. pub fn wallet(&self) -> &Wallet { self.cache.get_or_init(|| self.load_wallet()) @@ -102,8 +118,31 @@ impl WalletConfig { keystore .decrypt_wallet(password) .expect("failed to decrypt wallet") + } else if self.ledger { + self.load_ledger_wallet() } else { - panic!("WalletConfig requires one of: `test_wallet`, `pem`, or `keyfile`") + panic!("WalletConfig requires one of: `test_wallet`, `pem`, `keyfile`, or `ledger`") } } + + #[cfg(feature = "ledger")] + fn load_ledger_wallet(&self) -> Wallet { + use crate::sdk::wallet::ledger::LedgerApp; + use multiversx_chain_core::std::Bech32Hrp; + + let app = LedgerApp::new().expect("failed to connect to Ledger device"); + let bech32_str = app + .get_address(self.ledger_address_index) + .expect("failed to get address from Ledger"); + let bech32_addr = + multiversx_sc_scenario::imports::Bech32Address::from_bech32_string(bech32_str); + let hrp: Bech32Hrp = bech32_addr.hrp; + let address = bech32_addr.address; + Wallet::new_ledger(address, hrp, self.ledger_address_index) + } + + #[cfg(not(feature = "ledger"))] + fn load_ledger_wallet(&self) -> Wallet { + panic!("Ledger support is not compiled in; rebuild with the `ledger` feature enabled") + } } diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 8b1bbe9c46..4b8e476ac8 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -109,7 +109,10 @@ where .expect("the wallet that was supposed to sign is not registered"); // sign - let signature = sender.wallet.sign_tx(transaction); + let signature = sender + .wallet + .sign_tx(transaction) + .expect("failed to sign transaction"); transaction.signature = Some(signature); } } diff --git a/framework/snippets/src/multi/interactor_multi_sc_process.rs b/framework/snippets/src/multi/interactor_multi_sc_process.rs index dc2b43e22e..46f086b6ed 100644 --- a/framework/snippets/src/multi/interactor_multi_sc_process.rs +++ b/framework/snippets/src/multi/interactor_multi_sc_process.rs @@ -56,6 +56,9 @@ pub(crate) fn update_nonces_and_sign_tx(transaction: &mut Transaction, sender: & transaction.nonce = sender.current_nonce.unwrap(); sender.current_nonce = Some(sender.current_nonce.unwrap() + 1); - let signature = sender.wallet.sign_tx(&*transaction); + let signature = sender + .wallet + .sign_tx(&*transaction) + .expect("failed to sign transaction"); transaction.signature = Some(signature); } diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index af3940cc51..9732fe4efb 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -15,6 +15,9 @@ description = "SDK for interacting with the MultiversX blockchain" categories = ["cryptography::cryptocurrencies", "api-bindings"] keywords = ["multiversx", "blockchain", "sdk", "api"] +[features] +ledger = ["dep:ledger-transport-hid", "dep:ledger-apdu"] + [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } @@ -33,6 +36,8 @@ log = "0.4" scrypt = { version = "0.12", default-features = false } aes = "0.9" ctr = "0.10" +ledger-transport-hid = { version = "0.10", optional = true } +ledger-apdu = { version = "0.10", optional = true } [dependencies.multiversx-chain-core] version = "=0.23.1" diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index cc3a0fd97b..618198e5e4 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -4,11 +4,15 @@ mod keystore_json; mod mnemonic; mod private_key; mod public_key; +mod signer; mod wallet_impl; mod wallet_pem; mod wallet_signature; mod wallet_source; +#[cfg(feature = "ledger")] +pub mod ledger; + pub use keystore::Keystore; pub use keystore::KeystoreRandomness; pub use keystore_error::KeystoreError; @@ -16,6 +20,7 @@ pub use keystore_json::*; pub use mnemonic::Mnemonic; pub use private_key::{PRIVATE_KEY_LENGTH, PrivateKey, SEED_LENGTH}; pub use public_key::{PUBLIC_KEY_LENGTH, PublicKey}; +pub use signer::Signer; pub use wallet_impl::Wallet; pub use wallet_pem::WalletPem; pub use wallet_signature::WalletSignature; diff --git a/sdk/core/src/wallet/ledger.rs b/sdk/core/src/wallet/ledger.rs new file mode 100644 index 0000000000..9a8fe142d4 --- /dev/null +++ b/sdk/core/src/wallet/ledger.rs @@ -0,0 +1,7 @@ +mod ledger_app; +mod ledger_config; +mod ledger_error; + +pub use ledger_app::LedgerApp; +pub use ledger_config::LedgerAppConfiguration; +pub use ledger_error::LedgerError; diff --git a/sdk/core/src/wallet/ledger/ledger_app.rs b/sdk/core/src/wallet/ledger/ledger_app.rs new file mode 100644 index 0000000000..1a8126f894 --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_app.rs @@ -0,0 +1,165 @@ +use ledger_apdu::APDUCommand; +use ledger_transport_hid::{TransportNativeHID, hidapi::HidApi}; + +use super::{LedgerAppConfiguration, LedgerError}; + +const CLA: u8 = 0xED; +const INS_GET_APP_CONFIG: u8 = 0x02; +const INS_GET_ADDRESS: u8 = 0x03; +const INS_SET_ADDRESS: u8 = 0x05; +const INS_SIGN_MESSAGE: u8 = 0x06; +const INS_SIGN_HASH_TX: u8 = 0x07; + +// Account index is always 0 for MultiversX. +const DEFAULT_ACCOUNT_INDEX: u32 = 0; + +const MAX_CHUNK_SIZE: usize = 150; +// Ledger responses: first byte is the length prefix (0x40 = 64), followed by 64 signature bytes. +const EXPECTED_SIG_FIRST_BYTE: u8 = 0x40; +const SIG_RESPONSE_LEN: usize = 65; +const SIG_LEN: usize = 64; + +/// Communicates with the MultiversX app on a connected Ledger hardware device +/// over the HID transport layer. +/// +/// Only available when the `ledger` feature is enabled. +pub struct LedgerApp { + transport: TransportNativeHID, +} + +impl LedgerApp { + /// Opens a connection to the first available Ledger device. + /// + /// Returns [`LedgerError::DeviceNotFound`] if no device is found or if the + /// MultiversX app is not open on it. + pub fn new() -> Result { + let hidapi = HidApi::new().map_err(|e| LedgerError::Transport(e.to_string()))?; + let transport = + TransportNativeHID::new(&hidapi).map_err(|_| LedgerError::DeviceNotFound)?; + Ok(LedgerApp { transport }) + } + + /// Returns the app version string (`"MAJOR.MINOR.PATCH"`). + pub fn get_version(&self) -> Result { + Ok(self.get_app_configuration()?.version) + } + + /// Returns the full app configuration (version, data flag, account/address indexes). + pub fn get_app_configuration(&self) -> Result { + let response = self.exchange(INS_GET_APP_CONFIG, 0x00, 0x00, &[])?; + if response.len() < 6 { + return Err(LedgerError::InvalidResponse(format!( + "GET_APP_CONFIG: expected ≥6 bytes, got {}", + response.len() + ))); + } + let data_activated = response[0] == 0x01; + let account_index = response[1]; + let address_index = response[2]; + let version = format!("{}.{}.{}", response[3], response[4], response[5]); + Ok(LedgerAppConfiguration { + data_activated, + account_index, + address_index, + version, + }) + } + + /// Returns the bech32 address for the given `address_index` (account 0). + pub fn get_address(&self, address_index: u32) -> Result { + let data = build_account_address_data(address_index); + let response = self.exchange(INS_GET_ADDRESS, 0x00, 0x00, &data)?; + // First byte is the length of the address string. + let address_bytes = response + .get(1..) + .ok_or_else(|| LedgerError::InvalidResponse("empty GET_ADDRESS response".into()))?; + String::from_utf8(address_bytes.to_vec()) + .map_err(|e| LedgerError::InvalidResponse(format!("non-UTF8 address: {e}"))) + } + + /// Sets the active address index on the device. + pub fn set_address(&self, address_index: u32) -> Result<(), LedgerError> { + let data = build_account_address_data(address_index); + self.exchange(INS_SET_ADDRESS, 0x00, 0x00, &data)?; + Ok(()) + } + + /// Signs a serialised transaction (raw JSON bytes) using the hash-based + /// signing instruction (`0x07`). The device hashes the payload internally. + /// + /// Returns the 64-byte ed25519 signature. + pub fn sign_transaction(&self, tx_bytes: &[u8]) -> Result<[u8; SIG_LEN], LedgerError> { + self.do_sign(tx_bytes, INS_SIGN_HASH_TX) + } + + /// Signs an arbitrary message using the message signing instruction (`0x06`). + /// + /// Prepend the 4-byte big-endian length prefix before calling this function + /// (the caller is responsible, matching the Python SDK). + /// + /// Returns the 64-byte ed25519 signature. + pub fn sign_message(&self, message_bytes: &[u8]) -> Result<[u8; SIG_LEN], LedgerError> { + self.do_sign(message_bytes, INS_SIGN_MESSAGE) + } + + // ── Internal helpers ──────────────────────────────────────────────────── + + /// Sends `data` to the device in chunks (≤150 bytes each) using `ins` as + /// the signing instruction. Returns the 64-byte signature. + fn do_sign(&self, data: &[u8], ins: u8) -> Result<[u8; SIG_LEN], LedgerError> { + let chunks: Vec<&[u8]> = data.chunks(MAX_CHUNK_SIZE).collect(); + let chunk_count = chunks.len(); + + let mut last_response = Vec::new(); + for (i, chunk) in chunks.into_iter().enumerate() { + let p1: u8 = if i == 0 { 0x00 } else { 0x80 }; + last_response = self.exchange(ins, p1, 0x00, chunk)?; + } + + // After all chunks the device sends the signature: [0x40, sig[0..64]] + if last_response.len() != SIG_RESPONSE_LEN || last_response[0] != EXPECTED_SIG_FIRST_BYTE { + if chunk_count == 0 { + return Err(LedgerError::InvalidResponse("empty data to sign".into())); + } + return Err(LedgerError::InvalidResponse(format!( + "unexpected signature response length {} or first byte 0x{:02X}", + last_response.len(), + last_response.first().copied().unwrap_or(0) + ))); + } + + let mut sig = [0u8; SIG_LEN]; + sig.copy_from_slice(&last_response[1..]); + Ok(sig) + } + + /// Sends a single APDU command and returns the response payload (status word + /// is checked; `0x9000` is success, anything else is an error). + fn exchange(&self, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Result, LedgerError> { + let command = APDUCommand { + cla: CLA, + ins, + p1, + p2, + data: data.to_vec(), + }; + let answer = self + .transport + .exchange(&command) + .map_err(|e| LedgerError::Transport(e.to_string()))?; + + let sw = answer.retcode(); + if sw != 0x9000 { + return Err(LedgerError::from_status_word(sw)); + } + + Ok(answer.data().to_vec()) + } +} + +fn build_account_address_data(address_index: u32) -> Vec { + let mut data = Vec::with_capacity(8); + data.extend_from_slice(&DEFAULT_ACCOUNT_INDEX.to_be_bytes()); + data.extend_from_slice(&address_index.to_be_bytes()); + data +} diff --git a/sdk/core/src/wallet/ledger/ledger_config.rs b/sdk/core/src/wallet/ledger/ledger_config.rs new file mode 100644 index 0000000000..d95c49003c --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_config.rs @@ -0,0 +1,9 @@ +/// Configuration returned by the `GET_APP_CONFIG` (`0x02`) instruction. +#[derive(Debug, Clone)] +pub struct LedgerAppConfiguration { + pub data_activated: bool, + pub account_index: u8, + pub address_index: u8, + /// Version string in the form `"MAJOR.MINOR.PATCH"`. + pub version: String, +} diff --git a/sdk/core/src/wallet/ledger/ledger_error.rs b/sdk/core/src/wallet/ledger/ledger_error.rs new file mode 100644 index 0000000000..708593d4c3 --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_error.rs @@ -0,0 +1,65 @@ +use std::fmt; + +/// Errors that can be returned by [`LedgerApp`](super::LedgerApp) operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LedgerError { + /// The device could not be found or opened (not plugged in, locked, or wrong app). + DeviceNotFound, + /// The device returned a non-success status word. + StatusWord(u16), + /// A lower-level HID transport error. + Transport(String), + /// The APDU response had an unexpected format. + InvalidResponse(String), +} + +impl LedgerError { + pub(super) fn from_status_word(sw: u16) -> Self { + LedgerError::StatusWord(sw) + } +} + +impl fmt::Display for LedgerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LedgerError::DeviceNotFound => write!( + f, + "Ledger device not found; check that it is plugged in, unlocked, and on the MultiversX app" + ), + LedgerError::StatusWord(sw) => { + write!(f, "Ledger error (0x{sw:04X}): {}", status_word_message(*sw)) + } + LedgerError::Transport(msg) => write!(f, "Ledger transport error: {msg}"), + LedgerError::InvalidResponse(msg) => write!(f, "Ledger invalid response: {msg}"), + } + } +} + +impl std::error::Error for LedgerError {} + +fn status_word_message(sw: u16) -> &'static str { + match sw { + 0x9000 => "success", + 0x6985 => "user denied", + 0x6D00 => "unknown instruction", + 0x6E00 => "wrong CLA", + 0x6E10 => "signature failed", + 0x6E01 => "invalid arguments", + 0x6E02 => "invalid message", + 0x6E03 => "invalid p1", + 0x6E04 => "message too long", + 0x6E05 => "receiver too long", + 0x6E06 => "amount too long", + 0x6E07 => "contract data disabled", + 0x6E08 => "message incomplete", + 0x6E09 => "wrong tx version", + 0x6E0A => "nonce too long", + 0x6E0B => "invalid amount", + 0x6E0C => "invalid fee", + 0x6E0D => "pretty failed", + 0x6E0E => "data too long", + 0x6E0F => "wrong tx options", + 0x6E11 => "regular signing is deprecated", + _ => "unknown error", + } +} diff --git a/sdk/core/src/wallet/signer.rs b/sdk/core/src/wallet/signer.rs new file mode 100644 index 0000000000..0fc81c1b49 --- /dev/null +++ b/sdk/core/src/wallet/signer.rs @@ -0,0 +1,20 @@ +use super::PrivateKey; + +/// The mechanism used to sign transactions and messages. +/// +/// Always compiled — no `#[cfg]` on the type or its variants. +/// The `Ledger` variant holds only a `u32` address index, so there is no +/// compile-time dependency on `libhidapi`. +/// +/// Attempting to *sign* with `Signer::Ledger` when the `ledger` feature is +/// not active will return an informative runtime error from [`Wallet::sign_tx`] +/// and [`Wallet::sign_bytes`]. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Signer { + /// Signs locally using an in-memory ed25519 private key. + PrivateKey(Box), + + /// Signs on a connected Ledger hardware device at the given BIP-44 address + /// index (account is always 0 for MultiversX). + Ledger { address_index: u32 }, +} diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index b65ffceb0c..bb4ecaa599 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -1,7 +1,7 @@ use core::str; use std::path::Path; -use anyhow::Result; +use anyhow::{Result, anyhow}; use multiversx_chain_core::{ std::{Bech32Address, Bech32Hrp, crypto}, types::Address, @@ -10,25 +10,40 @@ use serde_json::json; use crate::{ data::transaction::Transaction, - wallet::{Mnemonic, PrivateKey, PublicKey, WalletPem, WalletSignature, WalletSource}, + wallet::{Mnemonic, PrivateKey, PublicKey, Signer, WalletPem, WalletSignature, WalletSource}, }; #[derive(Clone, PartialEq, Eq, Debug)] pub struct Wallet { - pub private_key: PrivateKey, + pub signer: Signer, pub address: Address, pub source: WalletSource, } impl Wallet { + /// Creates a wallet backed by a local private key. + /// The on-chain address is derived automatically from the key. pub fn new(private_key: PrivateKey, source: WalletSource) -> Self { let address = PublicKey::from(&private_key).to_address(); Wallet { - private_key, + signer: Signer::PrivateKey(Box::new(private_key)), address, source, } } + + /// Creates a wallet backed by a Ledger hardware device. + /// + /// The `address` must already be fetched from the device (via + /// [`LedgerApp::get_address`]); it is stored as-is and used for + /// transaction-sender validation. + pub fn new_ledger(address: Address, hrp: Bech32Hrp, address_index: u32) -> Self { + Wallet { + signer: Signer::Ledger { address_index }, + address, + source: WalletSource::Ledger { address_index, hrp }, + } + } } impl From for Wallet { @@ -90,50 +105,136 @@ impl Wallet { self.address.clone() } - /// Returns the address as a [`Bech32Address`], using the HRP from the wallet source - /// (`PemFile` or `Keystore`) when available, and the default HRP (`"erd"`) otherwise. + /// Returns the address as a [`Bech32Address`], using the HRP from the wallet + /// source (`PemFile`, `Keystore`, or `Ledger`) when available, and the + /// default HRP (`"erd"`) otherwise. pub fn to_bech32(&self) -> Bech32Address { let hrp = match &self.source { - WalletSource::PemFile(hrp) | WalletSource::Keystore(hrp) => *hrp, + WalletSource::PemFile(hrp) + | WalletSource::Keystore(hrp) + | WalletSource::Ledger { hrp, .. } => *hrp, _ => Bech32Hrp::default(), }; Bech32Address::encode_address(hrp, self.address.clone()) } - pub fn private_key_hex(&self) -> String { - self.private_key.to_seed_hex() + /// Returns a reference to the private key, or `None` for Ledger wallets. + pub fn private_key(&self) -> Option<&PrivateKey> { + match &self.signer { + Signer::PrivateKey(pk) => Some(pk), + Signer::Ledger { .. } => None, + } } - pub fn public_key(&self) -> PublicKey { - PublicKey::from(&self.private_key) + /// Returns the private key as a hex-encoded seed string, or `None` for Ledger wallets. + pub fn private_key_hex(&self) -> Option { + self.private_key().map(|pk| pk.to_seed_hex()) } - pub fn public_key_hex(&self) -> String { - PublicKey::from(&self.private_key).to_hex() + /// Returns the public key derived from the private key, or `None` for Ledger wallets. + pub fn public_key(&self) -> Option { + self.private_key().map(PublicKey::from) } - pub fn sign_tx(&self, unsign_tx: &Transaction) -> WalletSignature { + /// Returns the public key as a hex string, or `None` for Ledger wallets. + pub fn public_key_hex(&self) -> Option { + self.public_key().map(|pk| pk.to_hex()) + } + + /// Signs a transaction. + /// + /// For `PrivateKey` wallets the signing is local and infallible (barring + /// bugs). For `Ledger` wallets the device must be connected and the user + /// must confirm on-screen; errors are returned as `Err`. + /// + /// The Ledger path requires `tx.version >= 2` and `tx.options & 1 == 1` + /// (hash-based signing). Returns an error if those are not set. + pub fn sign_tx(&self, unsign_tx: &Transaction) -> Result { let mut unsign_tx = unsign_tx.clone(); unsign_tx.signature = None; - let mut tx_bytes = json!(unsign_tx).to_string().as_bytes().to_vec(); - - let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0; - if should_sign_on_tx_hash { - tx_bytes = crypto::keccak256(&tx_bytes).to_vec(); + match &self.signer { + Signer::PrivateKey(pk) => { + let mut tx_bytes = json!(unsign_tx).to_string().into_bytes(); + let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0; + if should_sign_on_tx_hash { + tx_bytes = crypto::keccak256(&tx_bytes).to_vec(); + } + Ok(pk.sign(tx_bytes)) + } + Signer::Ledger { address_index } => ledger_sign_tx(*address_index, &unsign_tx), } - - self.private_key.sign(tx_bytes) } - pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> WalletSignature { - self.private_key.sign(data) + /// Signs arbitrary bytes. + /// + /// For `Ledger` wallets the message bytes are prefixed with a 4-byte + /// big-endian length before being sent to the device (matching the Python SDK). + pub fn sign_bytes(&self, data: impl AsRef<[u8]>) -> Result { + match &self.signer { + Signer::PrivateKey(pk) => Ok(pk.sign(data)), + Signer::Ledger { address_index } => ledger_sign_bytes(*address_index, data), + } } - pub fn to_pem(&self, hrp: Bech32Hrp) -> WalletPem { - WalletPem { - private_key: self.private_key.clone(), + /// Converts this wallet to a PEM representation. + /// Returns an error for Ledger wallets (the private key is never exported). + pub fn to_pem(&self, hrp: Bech32Hrp) -> Result { + let pk = self.private_key().ok_or_else(|| { + anyhow!("cannot export a Ledger wallet to PEM: the private key is not available") + })?; + Ok(WalletPem { + private_key: pk.clone(), address: Bech32Address::encode_address(hrp, self.address.clone()), - } + }) } } + +#[cfg(feature = "ledger")] +fn ledger_sign_tx(address_index: u32, tx: &Transaction) -> Result { + use crate::wallet::ledger::LedgerApp; + + if tx.version < 2 || tx.options & 1 == 0 { + return Err(anyhow!( + "Ledger signing requires hash-based options: \ + set tx.version >= 2 and tx.options bit 0 to 1" + )); + } + let tx_bytes = json!(tx).to_string().into_bytes(); + let app = LedgerApp::new().map_err(|e| anyhow!("{e}"))?; + app.set_address(address_index).map_err(|e| anyhow!("{e}"))?; + let sig_bytes = app + .sign_transaction(&tx_bytes) + .map_err(|e| anyhow!("{e}"))?; + Ok(WalletSignature::from_bytes(sig_bytes)) +} + +#[cfg(not(feature = "ledger"))] +fn ledger_sign_tx(_address_index: u32, _tx: &Transaction) -> Result { + Err(anyhow!( + "Ledger signing is not available; recompile with the `ledger` feature enabled" + )) +} + +#[cfg(feature = "ledger")] +fn ledger_sign_bytes(address_index: u32, data: impl AsRef<[u8]>) -> Result { + use crate::wallet::ledger::LedgerApp; + + let data_ref = data.as_ref(); + let len_prefix = (data_ref.len() as u32).to_be_bytes(); + let mut msg_bytes = Vec::with_capacity(4 + data_ref.len()); + msg_bytes.extend_from_slice(&len_prefix); + msg_bytes.extend_from_slice(data_ref); + + let app = LedgerApp::new().map_err(|e| anyhow!("{e}"))?; + app.set_address(address_index).map_err(|e| anyhow!("{e}"))?; + let sig_bytes = app.sign_message(&msg_bytes).map_err(|e| anyhow!("{e}"))?; + Ok(WalletSignature::from_bytes(sig_bytes)) +} + +#[cfg(not(feature = "ledger"))] +fn ledger_sign_bytes(_address_index: u32, _data: impl AsRef<[u8]>) -> Result { + Err(anyhow!( + "Ledger signing is not available; recompile with the `ledger` feature enabled" + )) +} diff --git a/sdk/core/src/wallet/wallet_source.rs b/sdk/core/src/wallet/wallet_source.rs index 8b62d29346..ad4945b26f 100644 --- a/sdk/core/src/wallet/wallet_source.rs +++ b/sdk/core/src/wallet/wallet_source.rs @@ -8,4 +8,10 @@ pub enum WalletSource { PemFile(Bech32Hrp), TestWallet(&'static str), Keystore(Bech32Hrp), + /// Wallet loaded from a Ledger hardware device. Carries the HRP (for bech32 + /// encoding) and the BIP-44 address index used on the device. + Ledger { + address_index: u32, + hrp: Bech32Hrp, + }, } diff --git a/sdk/core/tests/wallet_keystore_test.rs b/sdk/core/tests/wallet_keystore_test.rs index 93c6b0901b..04be4ded20 100644 --- a/sdk/core/tests/wallet_keystore_test.rs +++ b/sdk/core/tests/wallet_keystore_test.rs @@ -42,7 +42,7 @@ fn test_keystore_encrypt_decrypt_roundtrip() { let wallet = keystore.decrypt_wallet(KEYSTORE_PASSWORD).unwrap(); - assert_eq!(wallet.private_key, private_key); + assert_eq!(wallet.private_key().unwrap(), &private_key); assert_eq!( wallet.source, WalletSource::Keystore(Bech32Hrp::try_from("erd").unwrap()) @@ -84,7 +84,7 @@ fn test_keystore_serialize_deserialize_roundtrip() { let keystore2: Keystore = Keystore::from_json(serde_json::from_str(&json).unwrap()).unwrap(); let wallet = keystore2.decrypt_wallet(KEYSTORE_PASSWORD).unwrap(); - assert_eq!(wallet.private_key, private_key); + assert_eq!(wallet.private_key().unwrap(), &private_key); } #[test] @@ -119,13 +119,13 @@ fn test_full_encrypt_serialize_deserialize_decrypt_sign() { // Decrypt into a Wallet. let wallet = keystore2.decrypt_wallet(KEYSTORE_PASSWORD).unwrap(); - assert_eq!(wallet.private_key, alice_private_key()); + assert_eq!(wallet.private_key().unwrap(), &alice_private_key()); // Sign the message. - let sig = wallet.sign_bytes(MESSAGE); + let sig = wallet.sign_bytes(MESSAGE).unwrap(); // Ed25519 is deterministic: same key + same message always gives the same signature. - assert_eq!(sig, wallet.sign_bytes(MESSAGE)); + assert_eq!(sig, wallet.sign_bytes(MESSAGE).unwrap()); // Snapshot: exact signature bytes must match. assert_eq!(sig.to_hex(), EXPECTED_SIGNATURE_HEX); diff --git a/sdk/core/tests/wallet_pem_test.rs b/sdk/core/tests/wallet_pem_test.rs index f5d016f7db..61a4ec7e9a 100644 --- a/sdk/core/tests/wallet_pem_test.rs +++ b/sdk/core/tests/wallet_pem_test.rs @@ -60,7 +60,10 @@ fn test_pem_str_roundtrip() { fn test_wallet_from_pem_fields() { let pem = WalletPem::from_pem_file(ALICE_PEM_PATH).unwrap(); let wallet = Wallet::from(pem); - assert_eq!(wallet.private_key.to_seed_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!( + wallet.private_key().unwrap().to_seed_hex(), + ALICE_PRIVATE_KEY_HEX + ); assert_eq!(wallet.address.to_bech32_default().bech32, ALICE_BECH32); } @@ -76,7 +79,7 @@ fn test_wallet_from_pem_source() { fn test_wallet_to_pem_roundtrip() { let wallet = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); let hrp = Bech32Hrp::try_from("erd").unwrap(); - let pem = wallet.to_pem(hrp); + let pem = wallet.to_pem(hrp).unwrap(); assert_eq!(pem.private_key_hex(), ALICE_PRIVATE_KEY_HEX); assert_eq!(pem.address.to_bech32_str(), ALICE_BECH32); } @@ -85,7 +88,10 @@ fn test_wallet_to_pem_roundtrip() { fn test_wallet_to_pem_str_roundtrip() { let wallet = Wallet::from_pem_file(ALICE_PEM_PATH).unwrap(); let hrp = Bech32Hrp::try_from("erd").unwrap(); - let pem_str = wallet.to_pem(hrp).to_pem_str(); + let pem_str = wallet.to_pem(hrp).unwrap().to_pem_str(); let reparsed = Wallet::from(WalletPem::from_pem_str(&pem_str).unwrap()); - assert_eq!(reparsed.private_key.to_seed_hex(), ALICE_PRIVATE_KEY_HEX); + assert_eq!( + reparsed.private_key().unwrap().to_seed_hex(), + ALICE_PRIVATE_KEY_HEX + ); } diff --git a/sdk/core/tests/wallet_sign_test.rs b/sdk/core/tests/wallet_sign_test.rs index 1cde364b2c..0043fe0a54 100644 --- a/sdk/core/tests/wallet_sign_test.rs +++ b/sdk/core/tests/wallet_sign_test.rs @@ -6,8 +6,8 @@ fn test_sign_bytes_snapshot() { let bob = test_wallets::bob(); // Signatures are deterministic: same key + same message always produces the same bytes. - let sig_alice = alice.sign_bytes(b"hello multiversx"); - let sig_bob = bob.sign_bytes(b"hello multiversx"); + let sig_alice = alice.sign_bytes(b"hello multiversx").unwrap(); + let sig_bob = bob.sign_bytes(b"hello multiversx").unwrap(); assert_eq!(sig_alice.to_hex(), SIG_ALICE_HELLO); assert_eq!(sig_bob.to_hex(), SIG_BOB_HELLO); @@ -17,8 +17,8 @@ fn test_sign_bytes_snapshot() { fn test_sign_bytes_deterministic() { let alice = test_wallets::alice(); assert_eq!( - alice.sign_bytes(b"deterministic"), - alice.sign_bytes(b"deterministic"), + alice.sign_bytes(b"deterministic").unwrap(), + alice.sign_bytes(b"deterministic").unwrap(), ); } @@ -26,16 +26,16 @@ fn test_sign_bytes_deterministic() { fn test_sign_bytes_different_messages_differ() { let alice = test_wallets::alice(); assert_ne!( - alice.sign_bytes(b"message one"), - alice.sign_bytes(b"message two"), + alice.sign_bytes(b"message one").unwrap(), + alice.sign_bytes(b"message two").unwrap(), ); } #[test] fn test_sign_bytes_different_keys_differ() { assert_ne!( - test_wallets::alice().sign_bytes(b"same message"), - test_wallets::bob().sign_bytes(b"same message"), + test_wallets::alice().sign_bytes(b"same message").unwrap(), + test_wallets::bob().sign_bytes(b"same message").unwrap(), ); } @@ -43,23 +43,28 @@ fn test_sign_bytes_different_keys_differ() { fn test_verify_valid_signature() { let alice = test_wallets::alice(); let message = b"hello multiversx"; - let sig = alice.sign_bytes(message); - assert!(alice.public_key().verify(message, &sig)); + let sig = alice.sign_bytes(message).unwrap(); + assert!(alice.public_key().unwrap().verify(message, &sig)); } #[test] fn test_verify_wrong_message_fails() { let alice = test_wallets::alice(); - let sig = alice.sign_bytes(b"hello multiversx"); - assert!(!alice.public_key().verify(b"different message", &sig)); + let sig = alice.sign_bytes(b"hello multiversx").unwrap(); + assert!( + !alice + .public_key() + .unwrap() + .verify(b"different message", &sig) + ); } #[test] fn test_verify_wrong_key_fails() { let alice = test_wallets::alice(); let bob = test_wallets::bob(); - let sig = alice.sign_bytes(b"hello multiversx"); - assert!(!bob.public_key().verify(b"hello multiversx", &sig)); + let sig = alice.sign_bytes(b"hello multiversx").unwrap(); + assert!(!bob.public_key().unwrap().verify(b"hello multiversx", &sig)); } #[test] @@ -68,8 +73,17 @@ fn test_verify_snapshot_signatures() { let bob = test_wallets::bob(); let alice_sig = WalletSignature::from_hex_str(SIG_ALICE_HELLO).unwrap(); let bob_sig = WalletSignature::from_hex_str(SIG_BOB_HELLO).unwrap(); - assert!(alice.public_key().verify(b"hello multiversx", &alice_sig)); - assert!(bob.public_key().verify(b"hello multiversx", &bob_sig)); + assert!( + alice + .public_key() + .unwrap() + .verify(b"hello multiversx", &alice_sig) + ); + assert!( + bob.public_key() + .unwrap() + .verify(b"hello multiversx", &bob_sig) + ); } // Snapshots captured with fixed test-wallet keys. diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index d69ae7f78c..f8a5d178f8 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> { options: arg.options, }; - let signature = wallet.sign_tx(&unsign_tx); + let signature = wallet.sign_tx(&unsign_tx)?; unsign_tx.signature = Some(signature); let tx_hash = blockchain.send_transaction(&unsign_tx).await?; diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index fb99c8db48..b48d62b809 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -33,7 +33,7 @@ async fn main() -> anyhow::Result<()> { let mut txs: Vec = vec![]; - let signature = wallet.sign_tx(&unsign_tx); + let signature = wallet.sign_tx(&unsign_tx)?; unsign_tx.signature = Some(signature); txs.push(unsign_tx.clone()); @@ -41,7 +41,7 @@ async fn main() -> anyhow::Result<()> { unsign_tx.options = 1; unsign_tx.nonce += 1; - let signature = wallet.sign_tx(&unsign_tx); + let signature = wallet.sign_tx(&unsign_tx)?; unsign_tx.signature = Some(signature); txs.push(unsign_tx.clone()); diff --git a/tools/wallet-miner/src/main.rs b/tools/wallet-miner/src/main.rs index a392e677cb..ac6445066b 100644 --- a/tools/wallet-miner/src/main.rs +++ b/tools/wallet-miner/src/main.rs @@ -38,7 +38,7 @@ fn main() -> Result<()> { println!(" - bech32: {}", wallet.address.to_bech32(hrp)); println!(" - hex: 0x{address_hex}"); - let pem_content = wallet.to_pem(hrp).to_pem_str(); + let pem_content = wallet.to_pem(hrp).unwrap().to_pem_str(); write_pem(suffix, &address_hex, pem_content)?; println!(); From 5672ce918baf60a95b4d43f07ac891a0f79f0999 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 15 Jul 2026 20:36:12 +0300 Subject: [PATCH 145/195] MapStorageMapper - orphaned storage issue fix & test --- .../src/storage/mappers/map_storage_mapper.rs | 35 +++++- .../tests/test_hash_map_storage_mapper.rs | 103 +++++++++++++++++- 2 files changed, 133 insertions(+), 5 deletions(-) diff --git a/framework/base/src/storage/mappers/map_storage_mapper.rs b/framework/base/src/storage/mappers/map_storage_mapper.rs index 5635ace7f6..74feac8221 100644 --- a/framework/base/src/storage/mappers/map_storage_mapper.rs +++ b/framework/base/src/storage/mappers/map_storage_mapper.rs @@ -166,8 +166,19 @@ where /// If the map did not have this key present, `true` is returned. /// /// If the map did have this value present, `false` is returned. + /// + /// Any residual storage at the nested namespace for this key is cleared before + /// the key is inserted, ensuring the new entry always starts from a clean state. pub fn insert_default(&mut self, k: K) -> bool { - self.keys_set.insert(k) + if !self.keys_set.contains(&k) { + // Clear any residual nested storage before inserting the key to prevent + // stale state from a previous occupant of the same key from becoming visible. + self.get_mapped_storage_value(&k).clear(); + self.keys_set.insert(k); + true + } else { + false + } } /// Removes the entry from the map. @@ -530,6 +541,19 @@ where } /// Gets the value in the entry. + /// + /// Returns an owned mapper handle that points to the deterministic nested storage + /// namespace for this key. + /// + /// # Warning + /// + /// The returned mapper is a lightweight handle to a derived storage key prefix. + /// It is not invalidated when the entry is subsequently removed via [`OccupiedEntry::remove`]. + /// Writing through a mapper obtained before removal will recreate storage at the same + /// namespace even though the key is no longer tracked in `keys_set`, producing hidden + /// state that is invisible to normal map lookups until the key is reinserted. + /// Do **not** retain and write through a mapper handle after calling `remove()` on + /// the entry it was obtained from. pub fn get(&self) -> V { self.map.get(&self.key).unwrap() } @@ -549,7 +573,14 @@ where f(&mut value) } - /// Removes the entry from the map. + /// Removes the entry from the map and clears all nested storage under the entry's key. + /// + /// # Warning + /// + /// Any mapper handle previously obtained via [`OccupiedEntry::get`] for this entry + /// still points to the same storage prefix. Writing through such a handle after + /// calling `remove()` recreates storage at that namespace outside `keys_set` tracking. + /// Ensure all mapper handles obtained from this entry are dropped before calling `remove()`. pub fn remove(self) { self.map.remove(&self.key); } diff --git a/framework/scenario/tests/test_hash_map_storage_mapper.rs b/framework/scenario/tests/test_hash_map_storage_mapper.rs index a6b9981b7f..be676c84af 100644 --- a/framework/scenario/tests/test_hash_map_storage_mapper.rs +++ b/framework/scenario/tests/test_hash_map_storage_mapper.rs @@ -1,12 +1,17 @@ use multiversx_sc::storage::{ StorageKey, - mappers::{MapMapper, MapStorageMapper, StorageClearable, StorageMapper}, + mappers::{MapMapper, MapStorageMapper, StorageClearable, StorageMapper, VecMapper}, }; use multiversx_sc_scenario::api::SingleTxApi; +fn create_map_storage_custom_key( + key: &[u8], +) -> MapStorageMapper> { + MapStorageMapper::new(StorageKey::new(key)) +} + fn create_map_storage() -> MapStorageMapper> { - let base_key = StorageKey::new(&b"my_map_storage"[..]); - MapStorageMapper::new(base_key) + create_map_storage_custom_key(b"my_map_storage") } #[test] @@ -55,3 +60,95 @@ fn test_map_storage_clear() { assert_eq!(map.len(), 0); assert!(map.is_empty()); } + +/// `MapStorageMapper::remove` must fully clear the nested storage namespace. +/// A mapper handle obtained before removal reflects the cleared state because it +/// points to the same deterministic storage prefix. +#[test] +fn test_remove_clears_nested_storage() { + let mut map = create_map_storage_custom_key(b"remove_clears_test"); + + map.insert_default(10); + let mut nested = map.get(&10).unwrap(); + nested.insert(1, 100); + nested.insert(2, 200); + assert_eq!(nested.len(), 2); + + // remove() must clear the nested storage, not just drop the keys_set entry. + assert!(map.remove(&10)); + assert!(!map.contains_key(&10)); + + // The previously obtained handle still points to the same prefix; it should now be empty. + assert_eq!(nested.len(), 0); + assert!(nested.is_empty()); +} + +/// `OccupiedEntry` manipulation via `and_modify` followed by `MapStorageMapper::remove` +/// must fully clear the nested storage. +#[test] +fn test_entry_remove_clears_nested_storage() { + let mut map = create_map_storage_custom_key(b"entry_remove_clears_test"); + + // Use the entry API to insert and populate the nested mapper. + let occupied = map.entry(20).or_insert_default(); + let mut nested = occupied.get(); + nested.insert(3, 300); + assert_eq!(nested.len(), 1); + + // Remove via MapStorageMapper::remove (which OccupiedEntry::remove delegates to). + assert!(map.remove(&20)); + assert!(!map.contains_key(&20)); + + // Nested storage must be cleared; the previously obtained handle reflects this. + assert_eq!(nested.len(), 0); +} + +/// `insert_default` must clear any residual nested storage so that a (re-)inserted key +/// always starts with an empty nested mapper. +/// +/// This test injects a stale `.len` value directly into the raw storage backend to +/// simulate residual data at the nested `VecMapper` namespace, bypassing the mapper's +/// own write path. `VecMapper` stores its length at `base_key + ".len"` as a +/// top-encoded `usize` (minimal big-endian, so `1` is `[0x01]`). +/// +/// Without the fix, `insert_default` would leave that data intact and the nested mapper +/// would report a non-zero length. With the fix, `insert_default` calls `clear()` on +/// the nested mapper before inserting the key, wiping the stale length. +#[test] +fn test_insert_default_clears_residual_nested_storage() { + // Unique base key for this test to avoid interfering with other tests. + let base_key = b"stale_state_test_vec"; + + // Build the raw storage key for the nested VecMapper's `.len` slot. + // Layout: + b".storage" + NestedEncode(99u64) + b".len" + // NestedEncode of u64 is 8 big-endian bytes. + let stale_len_key: Vec = { + let mut k = base_key.to_vec(); + k.extend_from_slice(b".storage"); + k.extend_from_slice(&99u64.to_be_bytes()); + k.extend_from_slice(b".len"); + k + }; + + // TopEncode of `1usize` is a single byte `[0x01]` (minimal big-endian). + // Writing this directly simulates residual data at the nested VecMapper namespace. + SingleTxApi::with_global_default_account(|account| { + account.storage.insert(stale_len_key, vec![0x01]); + }); + + // Now insert key 99 via the public API using a VecMapper as the nested type. + // The fix ensures insert_default clears the nested namespace before registering + // the key, so the nested mapper is empty regardless of any residual data. + let mut map = MapStorageMapper::>::new( + StorageKey::new(&base_key[..]), + ); + assert!(map.insert_default(99)); + + let nested = map.get(&99).unwrap(); + assert_eq!( + nested.len(), + 0, + "insert_default must clear residual nested storage; nested mapper must start empty" + ); + assert!(nested.is_empty()); +} From 8a9ff6eeff4ec1ce6efbc46787c7e8f6533726ff Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 15 Jul 2026 21:07:26 +0300 Subject: [PATCH 146/195] fixes after review --- .../tests/test_hash_map_storage_mapper.rs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/framework/scenario/tests/test_hash_map_storage_mapper.rs b/framework/scenario/tests/test_hash_map_storage_mapper.rs index be676c84af..152b3c407b 100644 --- a/framework/scenario/tests/test_hash_map_storage_mapper.rs +++ b/framework/scenario/tests/test_hash_map_storage_mapper.rs @@ -83,8 +83,7 @@ fn test_remove_clears_nested_storage() { assert!(nested.is_empty()); } -/// `OccupiedEntry` manipulation via `and_modify` followed by `MapStorageMapper::remove` -/// must fully clear the nested storage. +/// `OccupiedEntry` manipulation via `or_insert_default` followed by `MapStorageMapper::remove` #[test] fn test_entry_remove_clears_nested_storage() { let mut map = create_map_storage_custom_key(b"entry_remove_clears_test"); @@ -122,19 +121,13 @@ fn test_insert_default_clears_residual_nested_storage() { // Build the raw storage key for the nested VecMapper's `.len` slot. // Layout: + b".storage" + NestedEncode(99u64) + b".len" // NestedEncode of u64 is 8 big-endian bytes. - let stale_len_key: Vec = { - let mut k = base_key.to_vec(); - k.extend_from_slice(b".storage"); - k.extend_from_slice(&99u64.to_be_bytes()); - k.extend_from_slice(b".len"); - k - }; - - // TopEncode of `1usize` is a single byte `[0x01]` (minimal big-endian). - // Writing this directly simulates residual data at the nested VecMapper namespace. - SingleTxApi::with_global_default_account(|account| { - account.storage.insert(stale_len_key, vec![0x01]); - }); + let mut nested_base_key = StorageKey::new(&base_key[..]); + nested_base_key.append_bytes(b".storage"); + nested_base_key.append_item(&99u64); + + let mut stale_vec = VecMapper::::new(nested_base_key); + stale_vec.push(&123u64); + assert_eq!(stale_vec.len(), 1); // Now insert key 99 via the public API using a VecMapper as the nested type. // The fix ensures insert_default clears the nested namespace before registering From 9072f6054cb86c98f5d347670324c2ecaaec7a05 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 15 Jul 2026 21:08:46 +0300 Subject: [PATCH 147/195] MapStorageMapper insert_default optimization --- .../base/src/storage/mappers/map_storage_mapper.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/framework/base/src/storage/mappers/map_storage_mapper.rs b/framework/base/src/storage/mappers/map_storage_mapper.rs index 74feac8221..ce93efcae2 100644 --- a/framework/base/src/storage/mappers/map_storage_mapper.rs +++ b/framework/base/src/storage/mappers/map_storage_mapper.rs @@ -170,11 +170,14 @@ where /// Any residual storage at the nested namespace for this key is cleared before /// the key is inserted, ensuring the new entry always starts from a clean state. pub fn insert_default(&mut self, k: K) -> bool { - if !self.keys_set.contains(&k) { - // Clear any residual nested storage before inserting the key to prevent - // stale state from a previous occupant of the same key from becoming visible. - self.get_mapped_storage_value(&k).clear(); - self.keys_set.insert(k); + // Build the mapper handle before consuming `k` so the key is encoded once. + // `SetMapper::insert` already performs the `contains` check internally, + // avoiding a redundant storage read compared to calling `contains` first. + let mut mapped = self.get_mapped_storage_value(&k); + if self.keys_set.insert(k) { + // Clear any residual nested storage to prevent stale state from a previous + // occupant of the same key from becoming visible. + mapped.clear(); true } else { false From 65be8965789fb0421810386f54195184d2e03f67 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 16 Jul 2026 17:31:23 +0300 Subject: [PATCH 148/195] import fix --- framework/base/src/imports.rs | 2 +- framework/snippets/src/config/wallet_config.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/base/src/imports.rs b/framework/base/src/imports.rs index dcddd2afa5..1bd6ed160a 100644 --- a/framework/base/src/imports.rs +++ b/framework/base/src/imports.rs @@ -20,7 +20,7 @@ pub use crate::{ }; #[cfg(feature = "std")] -pub use multiversx_chain_core::std::Bech32Address; +pub use multiversx_chain_core::std::{Bech32Address, Bech32Hrp}; pub use core::ops::{ Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Div, DivAssign, diff --git a/framework/snippets/src/config/wallet_config.rs b/framework/snippets/src/config/wallet_config.rs index 7d211bda44..d5eb504fa0 100644 --- a/framework/snippets/src/config/wallet_config.rs +++ b/framework/snippets/src/config/wallet_config.rs @@ -128,14 +128,14 @@ impl WalletConfig { #[cfg(feature = "ledger")] fn load_ledger_wallet(&self) -> Wallet { use crate::sdk::wallet::ledger::LedgerApp; - use multiversx_chain_core::std::Bech32Hrp; + use multiversx_sdk::chain_core::std::Bech32Hrp; let app = LedgerApp::new().expect("failed to connect to Ledger device"); let bech32_str = app .get_address(self.ledger_address_index) .expect("failed to get address from Ledger"); let bech32_addr = - multiversx_sc_scenario::imports::Bech32Address::from_bech32_string(bech32_str); + multiversx_sdk::chain_core::std::Bech32Address::from_bech32_string(bech32_str); let hrp: Bech32Hrp = bech32_addr.hrp; let address = bech32_addr.address; Wallet::new_ledger(address, hrp, self.ledger_address_index) From 384f61061726080325228b3cf0ee346c3d2e59e4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Sun, 19 Jul 2026 15:46:37 +0300 Subject: [PATCH 149/195] ledger default feature off --- framework/meta/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/framework/meta/Cargo.toml b/framework/meta/Cargo.toml index 1273fc43b6..70ad61ff46 100644 --- a/framework/meta/Cargo.toml +++ b/framework/meta/Cargo.toml @@ -21,13 +21,12 @@ name = "sc-meta" path = "src/main.rs" [features] -default = ["ledger"] -ledger = ["multiversx-sdk/ledger"] template-test-current = [] template-test-released = [] chain-simulator-tests = [] repro-build-test = [] install-debugger-test = [] +ledger = ["multiversx-sdk/ledger"] [dependencies] clap = { version = "4.4", features = ["derive"] } From a532bc53fa0b01543b8e0cafec4577fd800c121b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 29 Jul 2026 15:15:15 +0300 Subject: [PATCH 150/195] transaction option & version types, ledger fixes --- Cargo.lock | 1 + chain/core/Cargo.toml | 2 +- chain/vm/Cargo.toml | 2 +- .../examples/adder/interactor/Cargo.toml | 1 + contracts/feature-tests/abi-tester/Cargo.toml | 2 +- data/codec/Cargo.toml | 2 +- framework/base/Cargo.toml | 2 +- framework/meta/src/cli/cli_args_sender.rs | 6 +- .../interactor_scenario/interactor_sc_call.rs | 5 +- .../interactor_sc_deploy.rs | 5 +- sdk/core/Cargo.toml | 1 + sdk/core/src/data/transaction.rs | 4 ++ .../transaction/arg_create_transaction.rs | 6 +- .../data/transaction/transaction_options.rs | 43 +++++++++++++ .../data/transaction/transaction_request.rs | 24 ++++--- .../data/transaction/transaction_version.rs | 62 +++++++++++++++++++ sdk/core/src/wallet/wallet_impl.rs | 20 +++--- sdk/http/examples/sign_tx.rs | 6 +- sdk/http/examples/sign_txs.rs | 11 ++-- sdk/http/examples/tx_cost.rs | 6 +- sdk/http/src/gateway_http_proxy/http_tx.rs | 18 +++--- 21 files changed, 177 insertions(+), 52 deletions(-) create mode 100644 sdk/core/src/data/transaction/transaction_options.rs create mode 100644 sdk/core/src/data/transaction/transaction_version.rs diff --git a/Cargo.lock b/Cargo.lock index 17c056891c..b475e70503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3488,6 +3488,7 @@ dependencies = [ "aes", "anyhow", "bip39", + "bitflags 2.13.0", "ctr", "hex", "hmac", diff --git a/chain/core/Cargo.toml b/chain/core/Cargo.toml index 7d58a6d5d9..26ef3f099b 100644 --- a/chain/core/Cargo.toml +++ b/chain/core/Cargo.toml @@ -20,7 +20,7 @@ categories = ["cryptography::cryptocurrencies", "development-tools::debugging"] std = ["base64", "bech32", "serde", "hex", "blake2", "sha2", "sha3", "ed25519-dalek"] [dependencies] -bitflags = "2.9" +bitflags = "2.13" base64 = { version = "0.22", optional = true } bech32 = { version = "0.12", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/chain/vm/Cargo.toml b/chain/vm/Cargo.toml index 30243f73fc..e86c14dbd6 100644 --- a/chain/vm/Cargo.toml +++ b/chain/vm/Cargo.toml @@ -28,7 +28,7 @@ num-bigint = "0.4" num-traits = "0.2" hex = "0.4" itertools = "0.15" -bitflags = "2.9" +bitflags = "2.13" colored = "3.0" rand = { version = "0.10", optional = true } rand_seeder = "0.5.0" diff --git a/contracts/examples/adder/interactor/Cargo.toml b/contracts/examples/adder/interactor/Cargo.toml index 912770fa90..8e6e63676c 100644 --- a/contracts/examples/adder/interactor/Cargo.toml +++ b/contracts/examples/adder/interactor/Cargo.toml @@ -17,6 +17,7 @@ path = ".." [dependencies.multiversx-sc-snippets] workspace = true +features = ["ledger"] [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/feature-tests/abi-tester/Cargo.toml b/contracts/feature-tests/abi-tester/Cargo.toml index e475cb2aaa..3f1632fab8 100644 --- a/contracts/feature-tests/abi-tester/Cargo.toml +++ b/contracts/feature-tests/abi-tester/Cargo.toml @@ -19,4 +19,4 @@ workspace = true workspace = true [dependencies] -bitflags = "2.9" +bitflags = "2.13" diff --git a/data/codec/Cargo.toml b/data/codec/Cargo.toml index eb8bf0fd3b..1d254e337e 100644 --- a/data/codec/Cargo.toml +++ b/data/codec/Cargo.toml @@ -34,7 +34,7 @@ arrayvec = { version = "0.7", default-features = false } num-bigint = { version = "0.4", optional = true } # can only be used in std contexts num-traits = { version = "0.2", default-features = false } unwrap-infallible = "1.0" -bitflags = "2.9" +bitflags = "2.13" [dev-dependencies.multiversx-sc-codec-derive] path = "../codec-derive" diff --git a/framework/base/Cargo.toml b/framework/base/Cargo.toml index c946c9785b..6f5fe22a1c 100644 --- a/framework/base/Cargo.toml +++ b/framework/base/Cargo.toml @@ -27,7 +27,7 @@ contract-call-legacy = [] small-int-bug = [] # can be used for testing the buggy mBufferFromSmallIntSigned VM hook [dependencies] -bitflags = "2.9" +bitflags = "2.13" num-traits = { version = "0.2", default-features = false } unwrap-infallible = "1.0" generic-array = "1.2" diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index 69992e9d36..0727fe7f67 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -23,8 +23,8 @@ pub struct SenderArgs { pub ledger: bool, /// Address index to use on the Ledger device. - #[arg(long = "ledger-address-index", default_value = "0")] - pub ledger_address_index: u32, + #[arg(long, default_value = "0")] + pub sender_wallet_index: u32, } /// Load a wallet from a PEM file, JSON keystore, or Ledger device. @@ -41,7 +41,7 @@ pub fn load_wallet(sender: &SenderArgs) -> Result { .decrypt_wallet(&password) .context("failed to load keystore wallet") } else if sender.ledger { - load_ledger_wallet(sender.ledger_address_index) + load_ledger_wallet(sender.sender_wallet_index) } else { Err(anyhow!( "a wallet is required: use --pem, --keyfile, or --ledger" diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index 92b2360195..df7a4631e6 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -8,6 +8,7 @@ use multiversx_sc_scenario::{ scenario_model::{ScCallStep, SetStateStep, TxCall}, }; use multiversx_sdk::chain_core::std::base64_encode; +use multiversx_sdk::data::transaction::{TransactionOptions, TransactionVersion}; use multiversx_sdk::{data::transaction::Transaction, gateway::SimulateTxRequest}; use multiversx_sdk::{ gateway::{GatewayAsyncService, SendTxRequest}, @@ -132,8 +133,8 @@ where data, signature: None, chain_id: self.network_config().chain_id.clone(), - version: self.network_config().min_transaction_version, - options: 0, + version: TransactionVersion::V2, + options: Some(TransactionOptions::SIGN_WITH_HASH), } } } diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index 9fb0bb6fa7..3901182081 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -8,6 +8,7 @@ use multiversx_sc_scenario::{ scenario_model::{ScDeployStep, SetStateStep}, }; use multiversx_sdk::chain_core::std::base64_encode; +use multiversx_sdk::data::transaction::{TransactionOptions, TransactionVersion}; use multiversx_sdk::{data::transaction::Transaction, gateway::SimulateTxRequest}; use multiversx_sdk::{ gateway::{GatewayAsyncService, SendTxRequest}, @@ -31,8 +32,8 @@ where data: Some(base64_encode(sc_deploy_step.tx.to_tx_data())), signature: None, chain_id: self.network_config().chain_id.clone(), - version: self.network_config().min_transaction_version, - options: 0, + version: TransactionVersion::V2, + options: Some(TransactionOptions::SIGN_WITH_HASH), } } diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 9732fe4efb..60ecac4cbc 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -36,6 +36,7 @@ log = "0.4" scrypt = { version = "0.12", default-features = false } aes = "0.9" ctr = "0.10" +bitflags = "2.13" ledger-transport-hid = { version = "0.10", optional = true } ledger-apdu = { version = "0.10", optional = true } diff --git a/sdk/core/src/data/transaction.rs b/sdk/core/src/data/transaction.rs index 77d0d1d406..d63f3ba1dd 100644 --- a/sdk/core/src/data/transaction.rs +++ b/sdk/core/src/data/transaction.rs @@ -7,10 +7,12 @@ mod get_transaction_response; mod log_data; mod multiple_transactions; mod simulate_gas; +mod transaction_options; mod transaction_process_status; mod transaction_request; mod transaction_response_data; mod transaction_status; +mod transaction_version; mod tx_cost; pub use api_logs::ApiLogs; @@ -22,10 +24,12 @@ pub use get_transaction_response::{GetTransactionResponse, GetTransactionRespons pub use log_data::LogData; pub use multiple_transactions::{MultipleTransactionsResponseData, ResponseMultipleTransactions}; pub use simulate_gas::{SimulateGasTransactionData, SimulateGasTransactionResponse}; +pub use transaction_options::TransactionOptions; pub use transaction_process_status::{ProcessStatusResponse, TransactionProcessStatus}; pub use transaction_request::Transaction; pub use transaction_response_data::{ResponseTransaction, TransactionResponseData}; pub use transaction_status::{ResponseTxStatus, TransactionStatus}; +pub use transaction_version::{MIN_VERSION_FOR_OPTIONS, TransactionVersion}; pub use tx_cost::{ResponseTxCost, TxCostResponseData}; #[deprecated( diff --git a/sdk/core/src/data/transaction/arg_create_transaction.rs b/sdk/core/src/data/transaction/arg_create_transaction.rs index a1b03c312d..b7e6a8e371 100644 --- a/sdk/core/src/data/transaction/arg_create_transaction.rs +++ b/sdk/core/src/data/transaction/arg_create_transaction.rs @@ -1,6 +1,8 @@ use multiversx_chain_core::std::Bech32Address; use serde::{Deserialize, Serialize}; +use crate::data::transaction::{TransactionOptions, TransactionVersion}; + // ArgCreateTransaction will hold the transaction fields #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ArgCreateTransaction { @@ -13,7 +15,7 @@ pub struct ArgCreateTransaction { pub data: Option, pub signature: String, pub chain_id: String, - pub version: u32, - pub options: u32, + pub version: TransactionVersion, + pub options: Option, pub available_balance: String, } diff --git a/sdk/core/src/data/transaction/transaction_options.rs b/sdk/core/src/data/transaction/transaction_options.rs new file mode 100644 index 0000000000..f15bc86d5f --- /dev/null +++ b/sdk/core/src/data/transaction/transaction_options.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +bitflags::bitflags! { + /// Bit-flags that modify how a transaction is signed and processed. + /// + /// | Bit | Constant | Value | Meaning | + /// |-----|------------------|-------|---------------------------------------------------| + /// | 0 | `SIGN_WITH_HASH` | `1` | Sign over keccak256 hash instead of raw JSON. | + /// | 1 | `GUARDED` | `2` | Transaction has a guardian co-signer. | + /// + /// Requires [`TransactionVersion::V2`] or higher. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct TransactionOptions: u32 { + /// No special options (default for version-1 transactions). + const DEFAULT = 0b0000; + + /// Sign over the keccak256 hash of the serialised transaction. + /// Required when signing with a Ledger hardware wallet. + const SIGN_WITH_HASH = 0b0001; + + /// Transaction is protected by a guardian (co-signer). + const GUARDED = 0b0010; + } +} + +impl Serialize for TransactionOptions { + fn serialize(&self, s: S) -> Result { + s.serialize_u32(self.bits()) + } +} + +impl<'de> Deserialize<'de> for TransactionOptions { + fn deserialize>(d: D) -> Result { + let bits = u32::deserialize(d)?; + Ok(TransactionOptions::from_bits_retain(bits)) + } +} + +impl TransactionOptions { + pub fn sign_with_hash(&self) -> bool { + self.contains(TransactionOptions::SIGN_WITH_HASH) + } +} diff --git a/sdk/core/src/data/transaction/transaction_request.rs b/sdk/core/src/data/transaction/transaction_request.rs index a9aed0fb87..7471a34e9f 100644 --- a/sdk/core/src/data/transaction/transaction_request.rs +++ b/sdk/core/src/data/transaction/transaction_request.rs @@ -2,6 +2,9 @@ use crate::wallet::WalletSignature; use multiversx_chain_core::std::Bech32Address; use serde::{Deserialize, Serialize}; +use super::transaction_options::TransactionOptions; +use super::transaction_version::TransactionVersion; + /// Represents the structure that maps and validates user input for publishing a new transaction. /// /// Corresponds to [`Transaction`](https://github.com/multiversx/mx-chain-proxy-go/blob/master/data/transaction.go) in mx-chain-proxy-go. @@ -20,13 +23,20 @@ pub struct Transaction { pub signature: Option, #[serde(rename = "chainID")] pub chain_id: String, - pub version: u32, - #[serde(skip_serializing_if = "is_zero", default)] - pub options: u32, + pub version: TransactionVersion, + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, } -/// This is only used for serialization. -#[allow(clippy::trivially_copy_pass_by_ref)] -fn is_zero(num: &u32) -> bool { - *num == 0 +impl Transaction { + pub fn should_sign_with_hash(&self) -> bool { + if !self.version.supports_options() { + return false; + } + + self.options + .as_ref() + .map(|o| o.sign_with_hash()) + .unwrap_or(false) + } } diff --git a/sdk/core/src/data/transaction/transaction_version.rs b/sdk/core/src/data/transaction/transaction_version.rs new file mode 100644 index 0000000000..ccbf0d101a --- /dev/null +++ b/sdk/core/src/data/transaction/transaction_version.rs @@ -0,0 +1,62 @@ +use anyhow::anyhow; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Minimum transaction version required to use [`TransactionOptions`]. +pub const MIN_VERSION_FOR_OPTIONS: u32 = 2; + +/// Transaction protocol version. +/// +/// `V2` is the default and required for hash-signing and guardian features. +/// `V1` is kept for compatibility when deserialising legacy transactions. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransactionVersion { + V1, + #[default] + V2, +} + +impl TransactionVersion { + /// Returns the numeric value (`1` or `2`). + pub fn as_u32(self) -> u32 { + match self { + TransactionVersion::V1 => 1, + TransactionVersion::V2 => 2, + } + } + + /// Returns `true` if this version supports [`TransactionOptions`]. + pub fn supports_options(self) -> bool { + self.as_u32() >= MIN_VERSION_FOR_OPTIONS + } +} + +impl TryFrom for TransactionVersion { + type Error = anyhow::Error; + + fn try_from(v: u32) -> Result { + match v { + 1 => Ok(TransactionVersion::V1), + 2 => Ok(TransactionVersion::V2), + other => Err(anyhow!("unknown transaction version: {other}")), + } + } +} + +impl From for u32 { + fn from(v: TransactionVersion) -> Self { + v.as_u32() + } +} + +impl Serialize for TransactionVersion { + fn serialize(&self, s: S) -> Result { + s.serialize_u32(self.as_u32()) + } +} + +impl<'de> Deserialize<'de> for TransactionVersion { + fn deserialize>(d: D) -> Result { + let v = u32::deserialize(d)?; + TransactionVersion::try_from(v).map_err(serde::de::Error::custom) + } +} diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index bb4ecaa599..b98a672f9a 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -146,9 +146,6 @@ impl Wallet { /// For `PrivateKey` wallets the signing is local and infallible (barring /// bugs). For `Ledger` wallets the device must be connected and the user /// must confirm on-screen; errors are returned as `Err`. - /// - /// The Ledger path requires `tx.version >= 2` and `tx.options & 1 == 1` - /// (hash-based signing). Returns an error if those are not set. pub fn sign_tx(&self, unsign_tx: &Transaction) -> Result { let mut unsign_tx = unsign_tx.clone(); unsign_tx.signature = None; @@ -156,8 +153,7 @@ impl Wallet { match &self.signer { Signer::PrivateKey(pk) => { let mut tx_bytes = json!(unsign_tx).to_string().into_bytes(); - let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0; - if should_sign_on_tx_hash { + if unsign_tx.should_sign_with_hash() { tx_bytes = crypto::keccak256(&tx_bytes).to_vec(); } Ok(pk.sign(tx_bytes)) @@ -192,20 +188,24 @@ impl Wallet { #[cfg(feature = "ledger")] fn ledger_sign_tx(address_index: u32, tx: &Transaction) -> Result { + use anyhow::Context as _; use crate::wallet::ledger::LedgerApp; - if tx.version < 2 || tx.options & 1 == 0 { + if !tx.should_sign_with_hash() { return Err(anyhow!( "Ledger signing requires hash-based options: \ - set tx.version >= 2 and tx.options bit 0 to 1" + set version to V2 and options to SIGN_WITH_HASH" )); } + let tx_bytes = json!(tx).to_string().into_bytes(); - let app = LedgerApp::new().map_err(|e| anyhow!("{e}"))?; - app.set_address(address_index).map_err(|e| anyhow!("{e}"))?; + let app = LedgerApp::new() + .context("failed to open Ledger device — check that it is plugged in, unlocked, and the MultiversX app is open")?; + app.set_address(address_index) + .with_context(|| format!("failed to set Ledger address index {address_index}"))?; let sig_bytes = app .sign_transaction(&tx_bytes) - .map_err(|e| anyhow!("{e}"))?; + .context("Ledger rejected the transaction — confirm it on the device screen or check that the transaction is valid")?; Ok(WalletSignature::from_bytes(sig_bytes)) } diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index f8a5d178f8..83c68c1e93 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -1,4 +1,8 @@ -use multiversx_sdk::{data::transaction::Transaction, wallet::PrivateKey, wallet::Wallet}; +use multiversx_sdk::{ + data::transaction::Transaction, + wallet::{PrivateKey, Wallet}, +}; + use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; #[tokio::main] diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index b48d62b809..ce1f02dd1b 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -1,6 +1,5 @@ use multiversx_sdk::{ - data::transaction::Transaction, - wallet::{PrivateKey, Wallet}, + data::transaction::{Transaction, TransactionOptions, TransactionVersion}, wallet::{PrivateKey, Wallet}, }; use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; @@ -27,8 +26,8 @@ async fn main() -> anyhow::Result<()> { data: arg.data, signature: None, chain_id: arg.chain_id, - version: arg.version, - options: arg.options, + version: TransactionVersion::V1, + options: None, }; let mut txs: Vec = vec![]; @@ -37,8 +36,8 @@ async fn main() -> anyhow::Result<()> { unsign_tx.signature = Some(signature); txs.push(unsign_tx.clone()); - unsign_tx.version = 2; - unsign_tx.options = 1; + unsign_tx.version = TransactionVersion::V2; + unsign_tx.options = Some(TransactionOptions::SIGN_WITH_HASH); unsign_tx.nonce += 1; let signature = wallet.sign_tx(&unsign_tx)?; diff --git a/sdk/http/examples/tx_cost.rs b/sdk/http/examples/tx_cost.rs index 5501650f07..95c0b87d94 100644 --- a/sdk/http/examples/tx_cost.rs +++ b/sdk/http/examples/tx_cost.rs @@ -1,6 +1,6 @@ use multiversx_sdk::{ chain_core::std::{Bech32Address, base64_encode}, - data::transaction::Transaction, + data::transaction::{Transaction, TransactionOptions, TransactionVersion}, }; use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; @@ -17,8 +17,8 @@ async fn main() { ), data: Some(base64_encode("hello")), chain_id: "1".to_string(), - version: 1, - options: 0, + version: TransactionVersion::default(), + options: Some(TransactionOptions::default()), gas_limit: 0, gas_price: 0, signature: None, diff --git a/sdk/http/src/gateway_http_proxy/http_tx.rs b/sdk/http/src/gateway_http_proxy/http_tx.rs index cba874cf72..587bb2a94a 100644 --- a/sdk/http/src/gateway_http_proxy/http_tx.rs +++ b/sdk/http/src/gateway_http_proxy/http_tx.rs @@ -1,14 +1,10 @@ use anyhow::Result; use multiversx_sdk::{ - chain_core::types::Address, - data::{ - network_config::NetworkConfig, - transaction::{ - ApiTransactionResult, ArgCreateTransaction, Transaction, TxCostResponseData, - }, - vm::{VMQueryInput, VmValuesResponseData}, - }, - gateway::{ + chain_core::types::Address, data::{ + network_config::NetworkConfig, transaction::{ + ApiTransactionResult, ArgCreateTransaction, Transaction, TransactionOptions, TransactionVersion, TxCostResponseData, + }, vm::{VMQueryInput, VmValuesResponseData}, + }, gateway::{ GetTxCost, GetTxInfo, GetTxProcessStatus, GetTxStatus, SendMultiTxRequest, SendTxRequest, VMQueryRequest, }, @@ -67,8 +63,8 @@ impl GatewayHttpProxy { data: None, signature: "".to_string(), chain_id: network_configs.chain_id.clone(), - version: network_configs.min_transaction_version, - options: 0, + version: TransactionVersion::default(), + options: Some(TransactionOptions::default()), available_balance: account.balance, }) } From 673c7043184f2df6a9d4432ac2bf5fa59b42aa75 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 29 Jul 2026 15:17:46 +0300 Subject: [PATCH 151/195] adder snippets - ledger --- .../examples/adder/snippets/adder.snippets.sh | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/contracts/examples/adder/snippets/adder.snippets.sh b/contracts/examples/adder/snippets/adder.snippets.sh index ea048346ad..a1695e325f 100644 --- a/contracts/examples/adder/snippets/adder.snippets.sh +++ b/contracts/examples/adder/snippets/adder.snippets.sh @@ -123,3 +123,23 @@ add_v2() { --infile="${OUTFILE_CALL_SIGNED}" \ || return } + +# Signs the add call with a Ledger hardware wallet instead of a PEM file. +# Uses --ledger (address index 0 by default); override with LEDGER_ADDRESS_INDEX. +add_ledger() { + NUMBER=5 + LEDGER_ADDRESS_INDEX="${LEDGER_ADDRESS_INDEX:-0}" + + "${TX_TOOL[@]}" call "${ADDRESS}" \ + --ledger \ + --sender-wallet-index "${LEDGER_ADDRESS_INDEX}" \ + --gas-limit=5000000 \ + --function="add" \ + --arguments "${NUMBER}" \ + --proxy="${PROXY}" \ + --chain="${CHAIN}" \ + --send \ + --outfile="${OUTFILE_CALL}" \ + --wait-result \ + || return +} From c2b5faec303b67effc9248412dcdd1546c191d73 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 29 Jul 2026 15:20:36 +0300 Subject: [PATCH 152/195] temporarily disable ledger feature --- contracts/examples/adder/interactor/Cargo.toml | 2 +- contracts/examples/adder/interactor/config.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/examples/adder/interactor/Cargo.toml b/contracts/examples/adder/interactor/Cargo.toml index 8e6e63676c..e0bc061a43 100644 --- a/contracts/examples/adder/interactor/Cargo.toml +++ b/contracts/examples/adder/interactor/Cargo.toml @@ -17,7 +17,7 @@ path = ".." [dependencies.multiversx-sc-snippets] workspace = true -features = ["ledger"] +# features = ["ledger"] # TODO: fix CI [dependencies] clap = { version = "4.4", features = ["derive"] } diff --git a/contracts/examples/adder/interactor/config.toml b/contracts/examples/adder/interactor/config.toml index a7a12c26aa..28583a9d7d 100644 --- a/contracts/examples/adder/interactor/config.toml +++ b/contracts/examples/adder/interactor/config.toml @@ -24,3 +24,4 @@ test_wallet = 'ivan' # pem = 'path/to/wallet.pem' # keyfile = 'path/to/wallet.json' # keystore_password = 'password' +# ledger = true From a22f8f7ecde85c09efcc39c9b3a198937799bcc5 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 29 Jul 2026 15:21:54 +0300 Subject: [PATCH 153/195] fmt --- sdk/core/src/wallet/wallet_impl.rs | 2 +- sdk/http/examples/sign_txs.rs | 3 ++- sdk/http/src/gateway_http_proxy/http_tx.rs | 15 ++++++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index b98a672f9a..8f2853fd25 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -188,8 +188,8 @@ impl Wallet { #[cfg(feature = "ledger")] fn ledger_sign_tx(address_index: u32, tx: &Transaction) -> Result { - use anyhow::Context as _; use crate::wallet::ledger::LedgerApp; + use anyhow::Context as _; if !tx.should_sign_with_hash() { return Err(anyhow!( diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index ce1f02dd1b..46c2b9915d 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -1,5 +1,6 @@ use multiversx_sdk::{ - data::transaction::{Transaction, TransactionOptions, TransactionVersion}, wallet::{PrivateKey, Wallet}, + data::transaction::{Transaction, TransactionOptions, TransactionVersion}, + wallet::{PrivateKey, Wallet}, }; use multiversx_sdk_http::{DEVNET_GATEWAY, GatewayHttpProxy}; diff --git a/sdk/http/src/gateway_http_proxy/http_tx.rs b/sdk/http/src/gateway_http_proxy/http_tx.rs index 587bb2a94a..774350aab7 100644 --- a/sdk/http/src/gateway_http_proxy/http_tx.rs +++ b/sdk/http/src/gateway_http_proxy/http_tx.rs @@ -1,10 +1,15 @@ use anyhow::Result; use multiversx_sdk::{ - chain_core::types::Address, data::{ - network_config::NetworkConfig, transaction::{ - ApiTransactionResult, ArgCreateTransaction, Transaction, TransactionOptions, TransactionVersion, TxCostResponseData, - }, vm::{VMQueryInput, VmValuesResponseData}, - }, gateway::{ + chain_core::types::Address, + data::{ + network_config::NetworkConfig, + transaction::{ + ApiTransactionResult, ArgCreateTransaction, Transaction, TransactionOptions, + TransactionVersion, TxCostResponseData, + }, + vm::{VMQueryInput, VmValuesResponseData}, + }, + gateway::{ GetTxCost, GetTxInfo, GetTxProcessStatus, GetTxStatus, SendMultiTxRequest, SendTxRequest, VMQueryRequest, }, From d5ecf4d1914e67c9a736c21731132fee7bcf77ed Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 30 Jul 2026 23:14:58 +0300 Subject: [PATCH 154/195] adder interactor add-ledger cmd, cleanup --- .../examples/adder/interactor/Cargo.toml | 8 ++-- .../examples/adder/interactor/config.toml | 4 +- .../adder/interactor/src/adder_interactor.rs | 47 +++++++++++++++---- .../interactor/src/adder_interactor_cli.rs | 2 + .../tests/adder_interactor_cs_test.rs | 1 + 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/contracts/examples/adder/interactor/Cargo.toml b/contracts/examples/adder/interactor/Cargo.toml index e0bc061a43..f64fa0c2c3 100644 --- a/contracts/examples/adder/interactor/Cargo.toml +++ b/contracts/examples/adder/interactor/Cargo.toml @@ -12,12 +12,15 @@ path = "src/adder_interactor_main.rs" [lib] path = "src/adder_interactor.rs" +[features] +ledger = ["multiversx-sc-snippets/ledger"] +chain-simulator-tests = [] + [dependencies.adder] path = ".." [dependencies.multiversx-sc-snippets] workspace = true -# features = ["ledger"] # TODO: fix CI [dependencies] clap = { version = "4.4", features = ["derive"] } @@ -27,6 +30,3 @@ tokio = { version = "1.24" } [dev-dependencies] serial_test = "3.2" - -[features] -chain-simulator-tests = [] diff --git a/contracts/examples/adder/interactor/config.toml b/contracts/examples/adder/interactor/config.toml index 28583a9d7d..64c496b999 100644 --- a/contracts/examples/adder/interactor/config.toml +++ b/contracts/examples/adder/interactor/config.toml @@ -24,4 +24,6 @@ test_wallet = 'ivan' # pem = 'path/to/wallet.pem' # keyfile = 'path/to/wallet.json' # keystore_password = 'password' -# ledger = true + +[ledger-wallet] +ledger = true diff --git a/contracts/examples/adder/interactor/src/adder_interactor.rs b/contracts/examples/adder/interactor/src/adder_interactor.rs index a09854b2ea..751d82135f 100644 --- a/contracts/examples/adder/interactor/src/adder_interactor.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor.rs @@ -18,6 +18,8 @@ pub struct Config { pub connection: ConnectionConfig, pub owner: WalletConfig, pub wallet: WalletConfig, + #[serde(rename = "ledger-wallet")] + pub ledger_wallet: Option, } impl InteractorConfig for Config { @@ -26,7 +28,11 @@ impl InteractorConfig for Config { } fn register_wallets(&self) -> Vec { - vec![self.owner.wallet().clone(), self.wallet.wallet().clone()] + let mut wallets = vec![self.owner.wallet().clone(), self.wallet.wallet().clone()]; + if let Some(ledger_wallet) = &self.ledger_wallet { + wallets.push(ledger_wallet.wallet().clone()); + } + wallets } } @@ -61,13 +67,16 @@ pub async fn adder_cli() { .upgrade(args.value, &owner_address, None) .await } - Some(adder_interactor_cli::InteractCliCommand::Add(args)) => { - adder_interact.add(args.value).await; - } Some(adder_interactor_cli::InteractCliCommand::Sum) => { let sum = adder_interact.get_sum().await; println!("sum: {sum}"); } + Some(adder_interactor_cli::InteractCliCommand::Add(args)) => { + adder_interact.add(args.value).await; + } + Some(adder_interactor_cli::InteractCliCommand::AddLedger(args)) => { + adder_interact.add_ledger(args.value).await; + } None => {} } } @@ -142,6 +151,17 @@ impl AdderInteractor { } } + pub async fn get_sum(&mut self) -> RustBigUint { + self.interactor + .query() + .to(self.state.current_adder_address()) + .typed(adder_proxy::AdderProxy) + .sum() + .returns(ReturnsResultUnmanaged) + .run() + .await + } + pub async fn add(&mut self, value: u32) { self.interactor .tx() @@ -157,14 +177,23 @@ impl AdderInteractor { println!("Successfully performed add"); } - pub async fn get_sum(&mut self) -> RustBigUint { + pub async fn add_ledger(&mut self, value: u32) { + let ledger_wallet = self + .config + .ledger_wallet + .as_ref() + .expect("ledger wallet not configured"); self.interactor - .query() + .tx() + .id("interactor add") + .from(ledger_wallet.address()) .to(self.state.current_adder_address()) + .gas(6_000_000u64) .typed(adder_proxy::AdderProxy) - .sum() - .returns(ReturnsResultUnmanaged) + .add(value) .run() - .await + .await; + + println!("Successfully performed add via ledger"); } } diff --git a/contracts/examples/adder/interactor/src/adder_interactor_cli.rs b/contracts/examples/adder/interactor/src/adder_interactor_cli.rs index 3a4a98e502..00b215d2c9 100644 --- a/contracts/examples/adder/interactor/src/adder_interactor_cli.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor_cli.rs @@ -20,6 +20,8 @@ pub enum InteractCliCommand { Sum, #[command(name = "add", about = "Add value")] Add(AddArgs), + #[command(name = "add-ledger", about = "Add value via ledger device")] + AddLedger(AddArgs), } #[derive(Default, Clone, PartialEq, Eq, Debug, Args)] diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs index 956ea66e27..cc6121bd5a 100644 --- a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs @@ -10,6 +10,7 @@ fn chain_simulator_config() -> Config { connection: ConnectionConfig::chain_simulator(), owner: WalletConfig::from_test_wallet("mike"), wallet: WalletConfig::from_test_wallet("ivan"), + ledger_wallet: None, } } From 7e055c77f9fa7855400b144f1204bc70966e8499 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 4 Aug 2026 12:31:14 +0300 Subject: [PATCH 155/195] error handling fix --- tools/wallet-miner/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/wallet-miner/src/main.rs b/tools/wallet-miner/src/main.rs index ac6445066b..1c88ce873c 100644 --- a/tools/wallet-miner/src/main.rs +++ b/tools/wallet-miner/src/main.rs @@ -38,7 +38,7 @@ fn main() -> Result<()> { println!(" - bech32: {}", wallet.address.to_bech32(hrp)); println!(" - hex: 0x{address_hex}"); - let pem_content = wallet.to_pem(hrp).unwrap().to_pem_str(); + let pem_content = wallet.to_pem(hrp)?.to_pem_str(); write_pem(suffix, &address_hex, pem_content)?; println!(); From 24758175db1e70f62dd9dbe149ed4ed2319d016f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 4 Aug 2026 13:31:54 +0300 Subject: [PATCH 156/195] ledger testing, 1st impl --- sdk/core/src/wallet/ledger.rs | 2 + sdk/core/src/wallet/ledger/ledger_app.rs | 292 ++++++++++++++++-- .../src/wallet/ledger/ledger_transport.rs | 27 ++ 3 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 sdk/core/src/wallet/ledger/ledger_transport.rs diff --git a/sdk/core/src/wallet/ledger.rs b/sdk/core/src/wallet/ledger.rs index 9a8fe142d4..3d251b1671 100644 --- a/sdk/core/src/wallet/ledger.rs +++ b/sdk/core/src/wallet/ledger.rs @@ -1,7 +1,9 @@ mod ledger_app; mod ledger_config; mod ledger_error; +mod ledger_transport; pub use ledger_app::LedgerApp; pub use ledger_config::LedgerAppConfiguration; pub use ledger_error::LedgerError; +pub use ledger_transport::LedgerTransport; diff --git a/sdk/core/src/wallet/ledger/ledger_app.rs b/sdk/core/src/wallet/ledger/ledger_app.rs index 1a8126f894..06e81b2e58 100644 --- a/sdk/core/src/wallet/ledger/ledger_app.rs +++ b/sdk/core/src/wallet/ledger/ledger_app.rs @@ -1,7 +1,7 @@ use ledger_apdu::APDUCommand; use ledger_transport_hid::{TransportNativeHID, hidapi::HidApi}; -use super::{LedgerAppConfiguration, LedgerError}; +use super::{LedgerAppConfiguration, LedgerError, LedgerTransport}; const CLA: u8 = 0xED; const INS_GET_APP_CONFIG: u8 = 0x02; @@ -19,15 +19,20 @@ const EXPECTED_SIG_FIRST_BYTE: u8 = 0x40; const SIG_RESPONSE_LEN: usize = 65; const SIG_LEN: usize = 64; -/// Communicates with the MultiversX app on a connected Ledger hardware device -/// over the HID transport layer. +/// Communicates with the MultiversX app on a connected Ledger hardware device. +/// +/// Generic over [`LedgerTransport`] so that unit tests can inject a +/// [`MockTransport`] instead of a real HID device. +/// +/// The default type parameter is the production [`TransportNativeHID`]. +/// Outside of tests, construct with [`LedgerApp::new`]. /// /// Only available when the `ledger` feature is enabled. -pub struct LedgerApp { - transport: TransportNativeHID, +pub struct LedgerApp { + transport: T, } -impl LedgerApp { +impl LedgerApp { /// Opens a connection to the first available Ledger device. /// /// Returns [`LedgerError::DeviceNotFound`] if no device is found or if the @@ -38,7 +43,9 @@ impl LedgerApp { TransportNativeHID::new(&hidapi).map_err(|_| LedgerError::DeviceNotFound)?; Ok(LedgerApp { transport }) } +} +impl LedgerApp { /// Returns the app version string (`"MAJOR.MINOR.PATCH"`). pub fn get_version(&self) -> Result { Ok(self.get_app_configuration()?.version) @@ -133,8 +140,8 @@ impl LedgerApp { Ok(sig) } - /// Sends a single APDU command and returns the response payload (status word - /// is checked; `0x9000` is success, anything else is an error). + /// Delegates to the transport, building the [`APDUCommand`] from the + /// separate fields. fn exchange(&self, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Result, LedgerError> { let command = APDUCommand { cla: CLA, @@ -143,17 +150,7 @@ impl LedgerApp { p2, data: data.to_vec(), }; - let answer = self - .transport - .exchange(&command) - .map_err(|e| LedgerError::Transport(e.to_string()))?; - - let sw = answer.retcode(); - if sw != 0x9000 { - return Err(LedgerError::from_status_word(sw)); - } - - Ok(answer.data().to_vec()) + self.transport.exchange(&command) } } @@ -163,3 +160,260 @@ fn build_account_address_data(address_index: u32) -> Vec { data.extend_from_slice(&address_index.to_be_bytes()); data } + +// ── Unit tests ─────────────────────────────────────────────────────────────── + +#[cfg(test)] +pub mod mock { + use std::cell::RefCell; + use std::collections::VecDeque; + + use ledger_apdu::APDUCommand; + + use super::super::LedgerError; + use super::{LedgerApp, LedgerTransport, SIG_LEN}; + + /// A deterministic mock transport for unit testing. + /// + /// Responses are queued up at construction time and consumed one-by-one as + /// [`exchange`](LedgerTransport::exchange) is called. Exhausting the queue + /// returns a `Transport` error. + /// + /// The mock also records every command it receives so tests can assert on + /// which APDUs were sent. + pub struct MockTransport { + responses: RefCell, LedgerError>>>, + pub commands: RefCell>>>, + } + + impl MockTransport { + pub fn new(responses: impl IntoIterator, LedgerError>>) -> Self { + MockTransport { + responses: RefCell::new(responses.into_iter().collect()), + commands: RefCell::new(Vec::new()), + } + } + + /// Convenience: queue a success response with the given payload bytes. + pub fn ok(bytes: impl Into>) -> Result, LedgerError> { + Ok(bytes.into()) + } + + /// Convenience: queue a status-word error response. + pub fn err(sw: u16) -> Result, LedgerError> { + Err(LedgerError::from_status_word(sw)) + } + } + + impl LedgerTransport for MockTransport { + fn exchange(&self, command: &APDUCommand>) -> Result, LedgerError> { + self.commands.borrow_mut().push(command.clone()); + self.responses + .borrow_mut() + .pop_front() + .unwrap_or_else(|| Err(LedgerError::Transport("mock: no more responses".into()))) + } + } + + /// Creates a valid 65-byte mock signature response for a given 64-byte + /// signature payload. + pub fn sig_response(sig: [u8; SIG_LEN]) -> Vec { + let mut r = vec![0x40u8]; + r.extend_from_slice(&sig); + r + } + + /// Returns a fixed 64-byte dummy signature (all `0xAB`). + pub fn dummy_sig() -> [u8; SIG_LEN] { + [0xABu8; SIG_LEN] + } + + pub fn app_with(responses: impl IntoIterator, LedgerError>>) -> LedgerApp { + LedgerApp { + transport: MockTransport::new(responses), + } + } +} + +#[cfg(test)] +mod tests { + use super::mock::*; + use super::*; + + // ── get_app_configuration ──────────────────────────────────────────────── + + #[test] + fn test_get_app_configuration_parses_response() { + // Response: data_activated=true, account=0, address=0, version=1.2.5 + let app = app_with([MockTransport::ok(vec![0x01, 0x00, 0x00, 0x01, 0x02, 0x05])]); + let config = app.get_app_configuration().unwrap(); + assert!(config.data_activated); + assert_eq!(config.account_index, 0); + assert_eq!(config.address_index, 0); + assert_eq!(config.version, "1.2.5"); + } + + #[test] + fn test_get_app_configuration_data_not_activated() { + let app = app_with([MockTransport::ok(vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x01])]); + let config = app.get_app_configuration().unwrap(); + assert!(!config.data_activated); + assert_eq!(config.version, "2.0.1"); + } + + #[test] + fn test_get_app_configuration_too_short_returns_error() { + let app = app_with([MockTransport::ok(vec![0x01, 0x00])]); + assert!(matches!( + app.get_app_configuration(), + Err(LedgerError::InvalidResponse(_)) + )); + } + + // ── get_address ────────────────────────────────────────────────────────── + + #[test] + fn test_get_address_parses_response() { + let address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + // Response: [length_byte, address_bytes...] + let mut response = vec![address.len() as u8]; + response.extend_from_slice(address.as_bytes()); + + let app = app_with([MockTransport::ok(response)]); + assert_eq!(app.get_address(0).unwrap(), address); + } + + #[test] + fn test_get_address_sends_correct_apdu() { + let address = "erd1abc"; + let mut response = vec![address.len() as u8]; + response.extend_from_slice(address.as_bytes()); + + let transport = MockTransport::new([MockTransport::ok(response)]); + let app = LedgerApp { transport }; + let _ = app.get_address(3).unwrap(); + + let cmd = &app.transport.commands.borrow()[0]; + assert_eq!(cmd.cla, CLA); + assert_eq!(cmd.ins, INS_GET_ADDRESS); + assert_eq!(cmd.p1, 0x00); + assert_eq!(cmd.p2, 0x00); + // data = account_index(4 BE) + address_index(4 BE) = 0x00000000 + 0x00000003 + assert_eq!(cmd.data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]); + } + + // ── sign_transaction chunking ──────────────────────────────────────────── + + #[test] + fn test_sign_single_chunk() { + let data = vec![0x01u8; 100]; // under 150 bytes → one chunk + let app = app_with([MockTransport::ok(sig_response(dummy_sig()))]); + let sig = app.sign_transaction(&data).unwrap(); + assert_eq!(sig, dummy_sig()); + } + + #[test] + fn test_sign_sends_correct_p1_for_first_chunk() { + let data = vec![0x01u8; 100]; + let transport = MockTransport::new([MockTransport::ok(sig_response(dummy_sig()))]); + let app = LedgerApp { transport }; + let _ = app.sign_transaction(&data).unwrap(); + + let cmd = &app.transport.commands.borrow()[0]; + assert_eq!(cmd.ins, INS_SIGN_HASH_TX); + assert_eq!(cmd.p1, 0x00); // first chunk + } + + #[test] + fn test_sign_two_chunks_correct_p1() { + // 200 bytes → chunk 1: 150 bytes (p1=0x00), chunk 2: 50 bytes (p1=0x80) + let data = vec![0x42u8; 200]; + let transport = MockTransport::new([ + MockTransport::ok(vec![]), // intermediate chunk + MockTransport::ok(sig_response(dummy_sig())), // final chunk with signature + ]); + let app = LedgerApp { transport }; + let sig = app.sign_transaction(&data).unwrap(); + assert_eq!(sig, dummy_sig()); + + let cmds = app.transport.commands.borrow(); + assert_eq!(cmds.len(), 2); + assert_eq!(cmds[0].p1, 0x00); // first chunk + assert_eq!(cmds[0].data.len(), 150); + assert_eq!(cmds[1].p1, 0x80); // continuation chunk + assert_eq!(cmds[1].data.len(), 50); + } + + #[test] + fn test_sign_three_chunks() { + // 400 bytes → 3 chunks: 150 + 150 + 100 + let data = vec![0xFFu8; 400]; + let transport = MockTransport::new([ + MockTransport::ok(vec![]), + MockTransport::ok(vec![]), + MockTransport::ok(sig_response(dummy_sig())), + ]); + let app = LedgerApp { transport }; + let sig = app.sign_transaction(&data).unwrap(); + assert_eq!(sig, dummy_sig()); + + let cmds = app.transport.commands.borrow(); + assert_eq!(cmds[0].p1, 0x00); + assert_eq!(cmds[1].p1, 0x80); + assert_eq!(cmds[2].p1, 0x80); + assert_eq!(cmds[0].data.len(), 150); + assert_eq!(cmds[1].data.len(), 150); + assert_eq!(cmds[2].data.len(), 100); + } + + #[test] + fn test_sign_empty_data_returns_error() { + let app = app_with([] as [Result, LedgerError>; 0]); + assert!(matches!( + app.sign_transaction(&[]), + Err(LedgerError::InvalidResponse(_)) + )); + } + + // ── sign_message ───────────────────────────────────────────────────────── + + #[test] + fn test_sign_message_uses_correct_ins() { + let data = b"hello"; + let transport = MockTransport::new([MockTransport::ok(sig_response(dummy_sig()))]); + let app = LedgerApp { transport }; + let _ = app.sign_message(data).unwrap(); + + assert_eq!(app.transport.commands.borrow()[0].ins, INS_SIGN_MESSAGE); + } + + // ── error mapping ──────────────────────────────────────────────────────── + + #[test] + fn test_user_denied_error() { + let app = app_with([MockTransport::err(0x6985)]); + assert!(matches!( + app.get_version(), + Err(LedgerError::StatusWord(0x6985)) + )); + } + + #[test] + fn test_wrong_tx_options_error() { + let app = app_with([MockTransport::err(0x6E0F)]); + assert!(matches!( + app.sign_transaction(b"test"), + Err(LedgerError::StatusWord(0x6E0F)) + )); + } + + // ── build_account_address_data ─────────────────────────────────────────── + + #[test] + fn test_address_data_encoding() { + let data = build_account_address_data(7); + // account index 0 (4 bytes BE) + address index 7 (4 bytes BE) + assert_eq!(data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]); + } +} + diff --git a/sdk/core/src/wallet/ledger/ledger_transport.rs b/sdk/core/src/wallet/ledger/ledger_transport.rs new file mode 100644 index 0000000000..82cc6a1502 --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_transport.rs @@ -0,0 +1,27 @@ +use ledger_apdu::APDUCommand; +use ledger_transport_hid::TransportNativeHID; + +use super::LedgerError; + +/// Abstraction over the physical transport layer. +/// +/// Implementors send a single APDU command and return the response payload +/// (already status-word-checked). The only production implementation is +/// [`TransportNativeHID`]; tests provide a [`MockTransport`]. +pub trait LedgerTransport { + fn exchange(&self, command: &APDUCommand>) -> Result, LedgerError>; +} + +impl LedgerTransport for TransportNativeHID { + fn exchange(&self, command: &APDUCommand>) -> Result, LedgerError> { + let answer = TransportNativeHID::exchange(self, command) + .map_err(|e| LedgerError::Transport(e.to_string()))?; + + let sw = answer.retcode(); + if sw != 0x9000 { + return Err(LedgerError::from_status_word(sw)); + } + + Ok(answer.data().to_vec()) + } +} From 124310e02910584462e4834c47952c81e8bb7d40 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 5 Aug 2026 00:30:58 +0300 Subject: [PATCH 157/195] ledger transport mock based on wallet + tests --- .github/workflows/interactor-tests-ledger.yml | 62 ++++ .../tests/adder_interactor_ledger_test.rs | 77 +++++ contracts/examples/adder/mxsc-template.toml | 2 +- .../snippets/src/config/wallet_config.rs | 2 +- sdk/core/src/wallet/ledger.rs | 3 + sdk/core/src/wallet/ledger/ledger_app.rs | 308 ++---------------- .../src/wallet/ledger/ledger_transport.rs | 27 -- .../wallet/ledger/ledger_transport_mock.rs | 261 +++++++++++++++ .../wallet/ledger/ledger_transport_native.rs | 19 ++ .../wallet/ledger/ledger_transport_wallet.rs | 205 ++++++++++++ sdk/core/src/wallet/wallet_impl.rs | 4 +- 11 files changed, 661 insertions(+), 309 deletions(-) create mode 100644 .github/workflows/interactor-tests-ledger.yml create mode 100644 contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs delete mode 100644 sdk/core/src/wallet/ledger/ledger_transport.rs create mode 100644 sdk/core/src/wallet/ledger/ledger_transport_mock.rs create mode 100644 sdk/core/src/wallet/ledger/ledger_transport_native.rs create mode 100644 sdk/core/src/wallet/ledger/ledger_transport_wallet.rs diff --git a/.github/workflows/interactor-tests-ledger.yml b/.github/workflows/interactor-tests-ledger.yml new file mode 100644 index 0000000000..78b513e41c --- /dev/null +++ b/.github/workflows/interactor-tests-ledger.yml @@ -0,0 +1,62 @@ +name: Interactor Tests, with ledger mock + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +jobs: + adder-ledger-interactor-test: + name: Adder ledger interactor test (chain simulator) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.96 + target: wasm32v1-none + + - name: Install sc-meta + run: cargo install --path framework/meta --locked + + - name: Install wasm-opt + run: cargo install wasm-opt + + - name: Build adder contract + env: + RUSTFLAGS: "" + run: sc-meta all build --path contracts/examples/adder + + - name: Pull chain simulator docker image + env: + RUSTFLAGS: "" + run: sc-meta cs install + + - name: Start chain simulator + env: + RUSTFLAGS: "" + run: sc-meta cs start & + + - name: Wait for chain simulator to be ready + run: sleep 5 + + - name: Run ledger interactor test + env: + RUSTFLAGS: "" + run: > + cargo test + -p adder-interactor + --test adder_interactor_ledger_test + --features ledger,chain-simulator-tests + + - name: Stop chain simulator + if: always() + env: + RUSTFLAGS: "" + run: sc-meta cs stop diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs new file mode 100644 index 0000000000..13483cd761 --- /dev/null +++ b/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs @@ -0,0 +1,77 @@ +//! Note: Currently not included in the template. +#![cfg(feature = "ledger")] + +use adder_interactor::{AdderInteractor, Config, GeneralConfig}; +use multiversx_sc_snippets::imports::*; +use multiversx_sc_snippets::sdk::wallet::ledger::{LedgerApp, WalletTransport}; +use serial_test::serial; + +/// Builds a chain-simulator config that uses carol's test wallet as the mock +/// ledger signer. `WalletTransport::new(carol)` simulates the Ledger app at +/// the APDU level; the interactor uses the same backing private key for signing. +fn chain_simulator_ledger_config() -> Config { + Config { + general: GeneralConfig { + contract_path: "../output/adder.mxsc.json".into(), + }, + connection: ConnectionConfig::chain_simulator(), + owner: WalletConfig::from_test_wallet("mike"), + wallet: WalletConfig::from_test_wallet("ivan"), + ledger_wallet: Some(WalletConfig::from_test_wallet("carol")), + } +} + +async fn test_interactor() -> AdderInteractor { + let config = chain_simulator_ledger_config(); + let interactor = Interactor::empty() + .with_current_dir(env!("CARGO_MANIFEST_DIR")) + .with_config(&config) + .await; + AdderInteractor { + interactor, + config, + state: AutoSave::no_save_default(), + } +} + +#[tokio::test] +#[serial] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +async fn add_ledger_mock_test() { + // Verify that WalletTransport correctly simulates Ledger address lookup. + // In production the interactor would call LedgerApp::new() to open the + // physical device; here we inject WalletTransport so no hardware is needed. + let carol = test_wallets::carol(); + let transport = WalletTransport::new(carol.clone()); + let mut mock_app = LedgerApp::with_transport(Box::new(transport)); + let ledger_addr = mock_app.get_address(0).unwrap(); + assert_eq!( + ledger_addr, + carol.to_bech32().to_string(), + "WalletTransport must report the same address as its backing wallet" + ); + + let mut adder_interact = test_interactor().await; + + adder_interact.generate_blocks(2).await; + + adder_interact.deploy().await; + + adder_interact.generate_blocks(2).await; + + // add_ledger sends the transaction from carol's address (the mock ledger wallet). + adder_interact.add_ledger(5u32).await; + + adder_interact.generate_blocks(2).await; + + let sum = adder_interact.get_sum().await; + assert_eq!(sum, 5u32.into()); + + // A second add to confirm accumulation. + adder_interact.add_ledger(3u32).await; + + adder_interact.generate_blocks(2).await; + + let sum = adder_interact.get_sum().await; + assert_eq!(sum, 8u32.into()); +} diff --git a/contracts/examples/adder/mxsc-template.toml b/contracts/examples/adder/mxsc-template.toml index 3609879fef..218a267aff 100644 --- a/contracts/examples/adder/mxsc-template.toml +++ b/contracts/examples/adder/mxsc-template.toml @@ -25,7 +25,7 @@ files_include = [ "interactor/config.toml", "interactor/.gitignore", "interactor/src", - "interactor/tests", + "interactor/tests/adder_interactor_cs_test.rs", "snippets/adder.snippets.sh", ] has_interactor = true diff --git a/framework/snippets/src/config/wallet_config.rs b/framework/snippets/src/config/wallet_config.rs index d5eb504fa0..96ee12bd22 100644 --- a/framework/snippets/src/config/wallet_config.rs +++ b/framework/snippets/src/config/wallet_config.rs @@ -130,7 +130,7 @@ impl WalletConfig { use crate::sdk::wallet::ledger::LedgerApp; use multiversx_sdk::chain_core::std::Bech32Hrp; - let app = LedgerApp::new().expect("failed to connect to Ledger device"); + let mut app = LedgerApp::new().expect("failed to connect to Ledger device"); let bech32_str = app .get_address(self.ledger_address_index) .expect("failed to get address from Ledger"); diff --git a/sdk/core/src/wallet/ledger.rs b/sdk/core/src/wallet/ledger.rs index 3d251b1671..a8cc8d3aac 100644 --- a/sdk/core/src/wallet/ledger.rs +++ b/sdk/core/src/wallet/ledger.rs @@ -2,8 +2,11 @@ mod ledger_app; mod ledger_config; mod ledger_error; mod ledger_transport; +mod ledger_transport_mock; +mod ledger_transport_wallet; pub use ledger_app::LedgerApp; pub use ledger_config::LedgerAppConfiguration; pub use ledger_error::LedgerError; pub use ledger_transport::LedgerTransport; +pub use ledger_transport_wallet::WalletTransport; diff --git a/sdk/core/src/wallet/ledger/ledger_app.rs b/sdk/core/src/wallet/ledger/ledger_app.rs index 06e81b2e58..2547518135 100644 --- a/sdk/core/src/wallet/ledger/ledger_app.rs +++ b/sdk/core/src/wallet/ledger/ledger_app.rs @@ -19,20 +19,27 @@ const EXPECTED_SIG_FIRST_BYTE: u8 = 0x40; const SIG_RESPONSE_LEN: usize = 65; const SIG_LEN: usize = 64; -/// Communicates with the MultiversX app on a connected Ledger hardware device. +/// Abstraction over the physical transport layer. /// -/// Generic over [`LedgerTransport`] so that unit tests can inject a -/// [`MockTransport`] instead of a real HID device. +/// Implementors send a single APDU command and return the response payload +/// (already status-word-checked). The only production implementation is +/// [`TransportNativeHID`]; tests provide a [`MockTransport`]. +pub trait LedgerTransport { + fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError>; +} + +/// Communicates with the MultiversX app on a connected Ledger hardware device. /// -/// The default type parameter is the production [`TransportNativeHID`]. -/// Outside of tests, construct with [`LedgerApp::new`]. +/// Uses dynamic dispatch over [`LedgerTransport`] so different transports +/// (real HID device, [`WalletTransport`], or a test mock) can be plugged in +/// without changing the type signature of callers. /// /// Only available when the `ledger` feature is enabled. -pub struct LedgerApp { - transport: T, +pub struct LedgerApp { + transport: Box, } -impl LedgerApp { +impl LedgerApp { /// Opens a connection to the first available Ledger device. /// /// Returns [`LedgerError::DeviceNotFound`] if no device is found or if the @@ -41,18 +48,22 @@ impl LedgerApp { let hidapi = HidApi::new().map_err(|e| LedgerError::Transport(e.to_string()))?; let transport = TransportNativeHID::new(&hidapi).map_err(|_| LedgerError::DeviceNotFound)?; - Ok(LedgerApp { transport }) + Ok(LedgerApp { + transport: Box::new(transport), + }) } -} -impl LedgerApp { + /// Creates a [`LedgerApp`] backed by any [`LedgerTransport`] implementation. + pub fn with_transport(transport: Box) -> Self { + LedgerApp { transport } + } /// Returns the app version string (`"MAJOR.MINOR.PATCH"`). - pub fn get_version(&self) -> Result { + pub fn get_version(&mut self) -> Result { Ok(self.get_app_configuration()?.version) } /// Returns the full app configuration (version, data flag, account/address indexes). - pub fn get_app_configuration(&self) -> Result { + pub fn get_app_configuration(&mut self) -> Result { let response = self.exchange(INS_GET_APP_CONFIG, 0x00, 0x00, &[])?; if response.len() < 6 { return Err(LedgerError::InvalidResponse(format!( @@ -73,7 +84,7 @@ impl LedgerApp { } /// Returns the bech32 address for the given `address_index` (account 0). - pub fn get_address(&self, address_index: u32) -> Result { + pub fn get_address(&mut self, address_index: u32) -> Result { let data = build_account_address_data(address_index); let response = self.exchange(INS_GET_ADDRESS, 0x00, 0x00, &data)?; // First byte is the length of the address string. @@ -85,7 +96,7 @@ impl LedgerApp { } /// Sets the active address index on the device. - pub fn set_address(&self, address_index: u32) -> Result<(), LedgerError> { + pub fn set_address(&mut self, address_index: u32) -> Result<(), LedgerError> { let data = build_account_address_data(address_index); self.exchange(INS_SET_ADDRESS, 0x00, 0x00, &data)?; Ok(()) @@ -95,7 +106,7 @@ impl LedgerApp { /// signing instruction (`0x07`). The device hashes the payload internally. /// /// Returns the 64-byte ed25519 signature. - pub fn sign_transaction(&self, tx_bytes: &[u8]) -> Result<[u8; SIG_LEN], LedgerError> { + pub fn sign_transaction(&mut self, tx_bytes: &[u8]) -> Result<[u8; SIG_LEN], LedgerError> { self.do_sign(tx_bytes, INS_SIGN_HASH_TX) } @@ -105,7 +116,7 @@ impl LedgerApp { /// (the caller is responsible, matching the Python SDK). /// /// Returns the 64-byte ed25519 signature. - pub fn sign_message(&self, message_bytes: &[u8]) -> Result<[u8; SIG_LEN], LedgerError> { + pub fn sign_message(&mut self, message_bytes: &[u8]) -> Result<[u8; SIG_LEN], LedgerError> { self.do_sign(message_bytes, INS_SIGN_MESSAGE) } @@ -113,7 +124,7 @@ impl LedgerApp { /// Sends `data` to the device in chunks (≤150 bytes each) using `ins` as /// the signing instruction. Returns the 64-byte signature. - fn do_sign(&self, data: &[u8], ins: u8) -> Result<[u8; SIG_LEN], LedgerError> { + fn do_sign(&mut self, data: &[u8], ins: u8) -> Result<[u8; SIG_LEN], LedgerError> { let chunks: Vec<&[u8]> = data.chunks(MAX_CHUNK_SIZE).collect(); let chunk_count = chunks.len(); @@ -140,9 +151,7 @@ impl LedgerApp { Ok(sig) } - /// Delegates to the transport, building the [`APDUCommand`] from the - /// separate fields. - fn exchange(&self, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Result, LedgerError> { + fn exchange(&mut self, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Result, LedgerError> { let command = APDUCommand { cla: CLA, ins, @@ -160,260 +169,3 @@ fn build_account_address_data(address_index: u32) -> Vec { data.extend_from_slice(&address_index.to_be_bytes()); data } - -// ── Unit tests ─────────────────────────────────────────────────────────────── - -#[cfg(test)] -pub mod mock { - use std::cell::RefCell; - use std::collections::VecDeque; - - use ledger_apdu::APDUCommand; - - use super::super::LedgerError; - use super::{LedgerApp, LedgerTransport, SIG_LEN}; - - /// A deterministic mock transport for unit testing. - /// - /// Responses are queued up at construction time and consumed one-by-one as - /// [`exchange`](LedgerTransport::exchange) is called. Exhausting the queue - /// returns a `Transport` error. - /// - /// The mock also records every command it receives so tests can assert on - /// which APDUs were sent. - pub struct MockTransport { - responses: RefCell, LedgerError>>>, - pub commands: RefCell>>>, - } - - impl MockTransport { - pub fn new(responses: impl IntoIterator, LedgerError>>) -> Self { - MockTransport { - responses: RefCell::new(responses.into_iter().collect()), - commands: RefCell::new(Vec::new()), - } - } - - /// Convenience: queue a success response with the given payload bytes. - pub fn ok(bytes: impl Into>) -> Result, LedgerError> { - Ok(bytes.into()) - } - - /// Convenience: queue a status-word error response. - pub fn err(sw: u16) -> Result, LedgerError> { - Err(LedgerError::from_status_word(sw)) - } - } - - impl LedgerTransport for MockTransport { - fn exchange(&self, command: &APDUCommand>) -> Result, LedgerError> { - self.commands.borrow_mut().push(command.clone()); - self.responses - .borrow_mut() - .pop_front() - .unwrap_or_else(|| Err(LedgerError::Transport("mock: no more responses".into()))) - } - } - - /// Creates a valid 65-byte mock signature response for a given 64-byte - /// signature payload. - pub fn sig_response(sig: [u8; SIG_LEN]) -> Vec { - let mut r = vec![0x40u8]; - r.extend_from_slice(&sig); - r - } - - /// Returns a fixed 64-byte dummy signature (all `0xAB`). - pub fn dummy_sig() -> [u8; SIG_LEN] { - [0xABu8; SIG_LEN] - } - - pub fn app_with(responses: impl IntoIterator, LedgerError>>) -> LedgerApp { - LedgerApp { - transport: MockTransport::new(responses), - } - } -} - -#[cfg(test)] -mod tests { - use super::mock::*; - use super::*; - - // ── get_app_configuration ──────────────────────────────────────────────── - - #[test] - fn test_get_app_configuration_parses_response() { - // Response: data_activated=true, account=0, address=0, version=1.2.5 - let app = app_with([MockTransport::ok(vec![0x01, 0x00, 0x00, 0x01, 0x02, 0x05])]); - let config = app.get_app_configuration().unwrap(); - assert!(config.data_activated); - assert_eq!(config.account_index, 0); - assert_eq!(config.address_index, 0); - assert_eq!(config.version, "1.2.5"); - } - - #[test] - fn test_get_app_configuration_data_not_activated() { - let app = app_with([MockTransport::ok(vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x01])]); - let config = app.get_app_configuration().unwrap(); - assert!(!config.data_activated); - assert_eq!(config.version, "2.0.1"); - } - - #[test] - fn test_get_app_configuration_too_short_returns_error() { - let app = app_with([MockTransport::ok(vec![0x01, 0x00])]); - assert!(matches!( - app.get_app_configuration(), - Err(LedgerError::InvalidResponse(_)) - )); - } - - // ── get_address ────────────────────────────────────────────────────────── - - #[test] - fn test_get_address_parses_response() { - let address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; - // Response: [length_byte, address_bytes...] - let mut response = vec![address.len() as u8]; - response.extend_from_slice(address.as_bytes()); - - let app = app_with([MockTransport::ok(response)]); - assert_eq!(app.get_address(0).unwrap(), address); - } - - #[test] - fn test_get_address_sends_correct_apdu() { - let address = "erd1abc"; - let mut response = vec![address.len() as u8]; - response.extend_from_slice(address.as_bytes()); - - let transport = MockTransport::new([MockTransport::ok(response)]); - let app = LedgerApp { transport }; - let _ = app.get_address(3).unwrap(); - - let cmd = &app.transport.commands.borrow()[0]; - assert_eq!(cmd.cla, CLA); - assert_eq!(cmd.ins, INS_GET_ADDRESS); - assert_eq!(cmd.p1, 0x00); - assert_eq!(cmd.p2, 0x00); - // data = account_index(4 BE) + address_index(4 BE) = 0x00000000 + 0x00000003 - assert_eq!(cmd.data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]); - } - - // ── sign_transaction chunking ──────────────────────────────────────────── - - #[test] - fn test_sign_single_chunk() { - let data = vec![0x01u8; 100]; // under 150 bytes → one chunk - let app = app_with([MockTransport::ok(sig_response(dummy_sig()))]); - let sig = app.sign_transaction(&data).unwrap(); - assert_eq!(sig, dummy_sig()); - } - - #[test] - fn test_sign_sends_correct_p1_for_first_chunk() { - let data = vec![0x01u8; 100]; - let transport = MockTransport::new([MockTransport::ok(sig_response(dummy_sig()))]); - let app = LedgerApp { transport }; - let _ = app.sign_transaction(&data).unwrap(); - - let cmd = &app.transport.commands.borrow()[0]; - assert_eq!(cmd.ins, INS_SIGN_HASH_TX); - assert_eq!(cmd.p1, 0x00); // first chunk - } - - #[test] - fn test_sign_two_chunks_correct_p1() { - // 200 bytes → chunk 1: 150 bytes (p1=0x00), chunk 2: 50 bytes (p1=0x80) - let data = vec![0x42u8; 200]; - let transport = MockTransport::new([ - MockTransport::ok(vec![]), // intermediate chunk - MockTransport::ok(sig_response(dummy_sig())), // final chunk with signature - ]); - let app = LedgerApp { transport }; - let sig = app.sign_transaction(&data).unwrap(); - assert_eq!(sig, dummy_sig()); - - let cmds = app.transport.commands.borrow(); - assert_eq!(cmds.len(), 2); - assert_eq!(cmds[0].p1, 0x00); // first chunk - assert_eq!(cmds[0].data.len(), 150); - assert_eq!(cmds[1].p1, 0x80); // continuation chunk - assert_eq!(cmds[1].data.len(), 50); - } - - #[test] - fn test_sign_three_chunks() { - // 400 bytes → 3 chunks: 150 + 150 + 100 - let data = vec![0xFFu8; 400]; - let transport = MockTransport::new([ - MockTransport::ok(vec![]), - MockTransport::ok(vec![]), - MockTransport::ok(sig_response(dummy_sig())), - ]); - let app = LedgerApp { transport }; - let sig = app.sign_transaction(&data).unwrap(); - assert_eq!(sig, dummy_sig()); - - let cmds = app.transport.commands.borrow(); - assert_eq!(cmds[0].p1, 0x00); - assert_eq!(cmds[1].p1, 0x80); - assert_eq!(cmds[2].p1, 0x80); - assert_eq!(cmds[0].data.len(), 150); - assert_eq!(cmds[1].data.len(), 150); - assert_eq!(cmds[2].data.len(), 100); - } - - #[test] - fn test_sign_empty_data_returns_error() { - let app = app_with([] as [Result, LedgerError>; 0]); - assert!(matches!( - app.sign_transaction(&[]), - Err(LedgerError::InvalidResponse(_)) - )); - } - - // ── sign_message ───────────────────────────────────────────────────────── - - #[test] - fn test_sign_message_uses_correct_ins() { - let data = b"hello"; - let transport = MockTransport::new([MockTransport::ok(sig_response(dummy_sig()))]); - let app = LedgerApp { transport }; - let _ = app.sign_message(data).unwrap(); - - assert_eq!(app.transport.commands.borrow()[0].ins, INS_SIGN_MESSAGE); - } - - // ── error mapping ──────────────────────────────────────────────────────── - - #[test] - fn test_user_denied_error() { - let app = app_with([MockTransport::err(0x6985)]); - assert!(matches!( - app.get_version(), - Err(LedgerError::StatusWord(0x6985)) - )); - } - - #[test] - fn test_wrong_tx_options_error() { - let app = app_with([MockTransport::err(0x6E0F)]); - assert!(matches!( - app.sign_transaction(b"test"), - Err(LedgerError::StatusWord(0x6E0F)) - )); - } - - // ── build_account_address_data ─────────────────────────────────────────── - - #[test] - fn test_address_data_encoding() { - let data = build_account_address_data(7); - // account index 0 (4 bytes BE) + address index 7 (4 bytes BE) - assert_eq!(data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]); - } -} - diff --git a/sdk/core/src/wallet/ledger/ledger_transport.rs b/sdk/core/src/wallet/ledger/ledger_transport.rs deleted file mode 100644 index 82cc6a1502..0000000000 --- a/sdk/core/src/wallet/ledger/ledger_transport.rs +++ /dev/null @@ -1,27 +0,0 @@ -use ledger_apdu::APDUCommand; -use ledger_transport_hid::TransportNativeHID; - -use super::LedgerError; - -/// Abstraction over the physical transport layer. -/// -/// Implementors send a single APDU command and return the response payload -/// (already status-word-checked). The only production implementation is -/// [`TransportNativeHID`]; tests provide a [`MockTransport`]. -pub trait LedgerTransport { - fn exchange(&self, command: &APDUCommand>) -> Result, LedgerError>; -} - -impl LedgerTransport for TransportNativeHID { - fn exchange(&self, command: &APDUCommand>) -> Result, LedgerError> { - let answer = TransportNativeHID::exchange(self, command) - .map_err(|e| LedgerError::Transport(e.to_string()))?; - - let sw = answer.retcode(); - if sw != 0x9000 { - return Err(LedgerError::from_status_word(sw)); - } - - Ok(answer.data().to_vec()) - } -} diff --git a/sdk/core/src/wallet/ledger/ledger_transport_mock.rs b/sdk/core/src/wallet/ledger/ledger_transport_mock.rs new file mode 100644 index 0000000000..e38b2b1d4d --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_transport_mock.rs @@ -0,0 +1,261 @@ +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; + +use ledger_apdu::APDUCommand; + +use super::super::LedgerError; +use super::{LedgerApp, LedgerTransport, SIG_LEN}; + +/// A deterministic mock transport for unit testing. +/// +/// Responses are queued up at construction time and consumed one-by-one as +/// [`exchange`](LedgerTransport::exchange) is called. Exhausting the queue +/// returns a `Transport` error. +/// +/// Call [`commands_spy`](MockTransport::commands_spy) before boxing to +/// retain a handle for asserting on recorded APDUs after the transport has +/// been moved into a [`LedgerApp`]. +pub struct MockTransport { + responses: VecDeque, LedgerError>>, + commands: Rc>>>>, +} + +impl MockTransport { + pub fn new(responses: impl IntoIterator, LedgerError>>) -> Self { + MockTransport { + responses: responses.into_iter().collect(), + commands: Rc::new(RefCell::new(Vec::new())), + } + } + + /// Returns a clone of the shared command recorder for post-hoc assertion. + pub fn commands_spy(&self) -> Rc>>>> { + Rc::clone(&self.commands) + } + + /// Convenience: queue a success response with the given payload bytes. + pub fn ok(bytes: impl Into>) -> Result, LedgerError> { + Ok(bytes.into()) + } + + /// Convenience: queue a status-word error response. + pub fn err(sw: u16) -> Result, LedgerError> { + Err(LedgerError::from_status_word(sw)) + } +} + +impl LedgerTransport for MockTransport { + fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError> { + self.commands.borrow_mut().push(command.clone()); + self.responses + .pop_front() + .unwrap_or_else(|| Err(LedgerError::Transport("mock: no more responses".into()))) + } +} + +/// Creates a valid 65-byte mock signature response for a given 64-byte +/// signature payload. +pub fn sig_response(sig: [u8; SIG_LEN]) -> Vec { + let mut r = vec![0x40u8]; + r.extend_from_slice(&sig); + r +} + +/// Returns a fixed 64-byte dummy signature (all `0xAB`). +pub fn dummy_sig() -> [u8; SIG_LEN] { + [0xABu8; SIG_LEN] +} + +pub fn app_with(responses: impl IntoIterator, LedgerError>>) -> LedgerApp { + LedgerApp::with_transport(Box::new(MockTransport::new(responses))) +} + +#[cfg(test)] +mod tests { + use super::mock::*; + use super::*; + + // ── get_app_configuration ──────────────────────────────────────────────── + + #[test] + fn test_get_app_configuration_parses_response() { + // Response: data_activated=true, account=0, address=0, version=1.2.5 + let mut app = app_with([MockTransport::ok(vec![0x01, 0x00, 0x00, 0x01, 0x02, 0x05])]); + let config = app.get_app_configuration().unwrap(); + assert!(config.data_activated); + assert_eq!(config.account_index, 0); + assert_eq!(config.address_index, 0); + assert_eq!(config.version, "1.2.5"); + } + + #[test] + fn test_get_app_configuration_data_not_activated() { + let mut app = app_with([MockTransport::ok(vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x01])]); + let config = app.get_app_configuration().unwrap(); + assert!(!config.data_activated); + assert_eq!(config.version, "2.0.1"); + } + + #[test] + fn test_get_app_configuration_too_short_returns_error() { + let mut app = app_with([MockTransport::ok(vec![0x01, 0x00])]); + assert!(matches!( + app.get_app_configuration(), + Err(LedgerError::InvalidResponse(_)) + )); + } + + // ── get_address ────────────────────────────────────────────────────────── + + #[test] + fn test_get_address_parses_response() { + let address = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + // Response: [length_byte, address_bytes...] + let mut response = vec![address.len() as u8]; + response.extend_from_slice(address.as_bytes()); + + let mut app = app_with([MockTransport::ok(response)]); + assert_eq!(app.get_address(0).unwrap(), address); + } + + #[test] + fn test_get_address_sends_correct_apdu() { + let address = "erd1abc"; + let mut response = vec![address.len() as u8]; + response.extend_from_slice(address.as_bytes()); + + let transport = MockTransport::new([MockTransport::ok(response)]); + let commands = transport.commands_spy(); + let mut app = LedgerApp::with_transport(Box::new(transport)); + let _ = app.get_address(3).unwrap(); + + let cmds = commands.borrow(); + let cmd = &cmds[0]; + assert_eq!(cmd.cla, CLA); + assert_eq!(cmd.ins, INS_GET_ADDRESS); + assert_eq!(cmd.p1, 0x00); + assert_eq!(cmd.p2, 0x00); + // data = account_index(4 BE) + address_index(4 BE) = 0x00000000 + 0x00000003 + assert_eq!(cmd.data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03]); + } + + // ── sign_transaction chunking ──────────────────────────────────────────── + + #[test] + fn test_sign_single_chunk() { + let data = vec![0x01u8; 100]; // under 150 bytes → one chunk + let mut app = app_with([MockTransport::ok(sig_response(dummy_sig()))]); + let sig = app.sign_transaction(&data).unwrap(); + assert_eq!(sig, dummy_sig()); + } + + #[test] + fn test_sign_sends_correct_p1_for_first_chunk() { + let data = vec![0x01u8; 100]; + let transport = MockTransport::new([MockTransport::ok(sig_response(dummy_sig()))]); + let commands = transport.commands_spy(); + let mut app = LedgerApp::with_transport(Box::new(transport)); + let _ = app.sign_transaction(&data).unwrap(); + + let cmds = commands.borrow(); + let cmd = &cmds[0]; + assert_eq!(cmd.ins, INS_SIGN_HASH_TX); + assert_eq!(cmd.p1, 0x00); // first chunk + } + + #[test] + fn test_sign_two_chunks_correct_p1() { + // 200 bytes → chunk 1: 150 bytes (p1=0x00), chunk 2: 50 bytes (p1=0x80) + let data = vec![0x42u8; 200]; + let transport = MockTransport::new([ + MockTransport::ok(vec![]), // intermediate chunk + MockTransport::ok(sig_response(dummy_sig())), // final chunk with signature + ]); + let commands = transport.commands_spy(); + let mut app = LedgerApp::with_transport(Box::new(transport)); + let sig = app.sign_transaction(&data).unwrap(); + assert_eq!(sig, dummy_sig()); + + let cmds = commands.borrow(); + assert_eq!(cmds.len(), 2); + assert_eq!(cmds[0].p1, 0x00); // first chunk + assert_eq!(cmds[0].data.len(), 150); + assert_eq!(cmds[1].p1, 0x80); // continuation chunk + assert_eq!(cmds[1].data.len(), 50); + } + + #[test] + fn test_sign_three_chunks() { + // 400 bytes → 3 chunks: 150 + 150 + 100 + let data = vec![0xFFu8; 400]; + let transport = MockTransport::new([ + MockTransport::ok(vec![]), + MockTransport::ok(vec![]), + MockTransport::ok(sig_response(dummy_sig())), + ]); + let commands = transport.commands_spy(); + let mut app = LedgerApp::with_transport(Box::new(transport)); + let sig = app.sign_transaction(&data).unwrap(); + assert_eq!(sig, dummy_sig()); + + let cmds = commands.borrow(); + assert_eq!(cmds[0].p1, 0x00); + assert_eq!(cmds[1].p1, 0x80); + assert_eq!(cmds[2].p1, 0x80); + assert_eq!(cmds[0].data.len(), 150); + assert_eq!(cmds[1].data.len(), 150); + assert_eq!(cmds[2].data.len(), 100); + } + + #[test] + fn test_sign_empty_data_returns_error() { + let mut app = app_with([] as [Result, LedgerError>; 0]); + assert!(matches!( + app.sign_transaction(&[]), + Err(LedgerError::InvalidResponse(_)) + )); + } + + // ── sign_message ───────────────────────────────────────────────────────── + + #[test] + fn test_sign_message_uses_correct_ins() { + let data = b"hello"; + let transport = MockTransport::new([MockTransport::ok(sig_response(dummy_sig()))]); + let commands = transport.commands_spy(); + let mut app = LedgerApp::with_transport(Box::new(transport)); + let _ = app.sign_message(data).unwrap(); + + assert_eq!(commands.borrow()[0].ins, INS_SIGN_MESSAGE); + } + + // ── error mapping ──────────────────────────────────────────────────────── + + #[test] + fn test_user_denied_error() { + let mut app = app_with([MockTransport::err(0x6985)]); + assert!(matches!( + app.get_version(), + Err(LedgerError::StatusWord(0x6985)) + )); + } + + #[test] + fn test_wrong_tx_options_error() { + let mut app = app_with([MockTransport::err(0x6E0F)]); + assert!(matches!( + app.sign_transaction(b"test"), + Err(LedgerError::StatusWord(0x6E0F)) + )); + } + + // ── build_account_address_data ─────────────────────────────────────────── + + #[test] + fn test_address_data_encoding() { + let data = build_account_address_data(7); + // account index 0 (4 bytes BE) + address index 7 (4 bytes BE) + assert_eq!(data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]); + } +} diff --git a/sdk/core/src/wallet/ledger/ledger_transport_native.rs b/sdk/core/src/wallet/ledger/ledger_transport_native.rs new file mode 100644 index 0000000000..11277d0a94 --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_transport_native.rs @@ -0,0 +1,19 @@ +use ledger_apdu::APDUCommand; +use ledger_transport_hid::TransportNativeHID; + +use super::LedgerError; + + +impl LedgerTransport for TransportNativeHID { + fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError> { + let answer = TransportNativeHID::exchange(self, command) + .map_err(|e| LedgerError::Transport(e.to_string()))?; + + let sw = answer.retcode(); + if sw != 0x9000 { + return Err(LedgerError::from_status_word(sw)); + } + + Ok(answer.data().to_vec()) + } +} diff --git a/sdk/core/src/wallet/ledger/ledger_transport_wallet.rs b/sdk/core/src/wallet/ledger/ledger_transport_wallet.rs new file mode 100644 index 0000000000..bae9a9bc1a --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_transport_wallet.rs @@ -0,0 +1,205 @@ +use ledger_apdu::APDUCommand; +use multiversx_chain_core::std::crypto; + +use super::{LedgerError, LedgerTransport}; +use crate::wallet::Wallet; + +const INS_GET_APP_CONFIG: u8 = 0x02; +const INS_GET_ADDRESS: u8 = 0x03; +const INS_SET_ADDRESS: u8 = 0x05; +const INS_SIGN_MESSAGE: u8 = 0x06; +const INS_SIGN_HASH_TX: u8 = 0x07; + +/// A [`LedgerTransport`] backed by a software [`Wallet`]. +/// +/// Simulates the MultiversX Ledger app at the APDU level so that code written +/// against [`LedgerApp`](super::LedgerApp) can be tested end-to-end without +/// physical hardware. Signing behaviour matches the real device: +/// +/// - `INS_SIGN_HASH_TX`: signs `keccak256(payload)` with the wallet key. +/// - `INS_SIGN_MESSAGE`: signs the payload bytes directly. +/// +/// Multi-chunk payloads are accumulated across calls (p1 `0x00` = first chunk, +/// `0x80` = continuation) and signed after each exchange, so the caller always +/// receives a valid signature response — only the last one matters. +pub struct WalletTransport { + wallet: Wallet, + sign_buffer: Vec, +} + +impl WalletTransport { + pub fn new(wallet: Wallet) -> Self { + WalletTransport { + wallet, + sign_buffer: Vec::new(), + } + } +} + +impl LedgerTransport for WalletTransport { + fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError> { + match command.ins { + INS_GET_APP_CONFIG => { + // data_activated=true, account=0, address=0, version=0.0.0 + Ok(vec![0x01, 0x00, 0x00, 0x00, 0x00, 0x00]) + } + INS_GET_ADDRESS => { + let bech32 = self.wallet.to_bech32().to_string(); + let bytes = bech32.as_bytes(); + let mut response = Vec::with_capacity(1 + bytes.len()); + response.push(bytes.len() as u8); + response.extend_from_slice(bytes); + Ok(response) + } + INS_SET_ADDRESS => Ok(vec![]), + INS_SIGN_HASH_TX | INS_SIGN_MESSAGE => { + if command.p1 == 0x00 { + self.sign_buffer.clear(); + } + self.sign_buffer.extend_from_slice(&command.data); + + let pk = self.wallet.private_key().ok_or_else(|| { + LedgerError::Transport( + "WalletTransport requires a private-key wallet, not a Ledger wallet".into(), + ) + })?; + + let data_to_sign: Vec = if command.ins == INS_SIGN_HASH_TX { + crypto::keccak256(&self.sign_buffer).to_vec() + } else { + self.sign_buffer.clone() + }; + + let sig = pk.sign(&data_to_sign).to_bytes(); + let mut response = Vec::with_capacity(65); + response.push(0x40); + response.extend_from_slice(&sig); + Ok(response) + } + ins => Err(LedgerError::InvalidResponse(format!( + "WalletTransport: unknown instruction 0x{ins:02X}" + ))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wallet::{PrivateKey, WalletSignature}; + use multiversx_chain_core::std::crypto; + + fn test_wallet() -> Wallet { + // Known test key: all-zero seed. + Wallet::from(PrivateKey::from_seed_bytes(&[0u8; 32])) + } + + #[test] + fn test_get_app_config() { + let mut transport = WalletTransport::new(test_wallet()); + let cmd = APDUCommand { + cla: 0xED, + ins: INS_GET_APP_CONFIG, + p1: 0, + p2: 0, + data: vec![], + }; + let response = transport.exchange(&cmd).unwrap(); + assert_eq!(response.len(), 6); + assert_eq!(response[0], 0x01); // data_activated + } + + #[test] + fn test_get_address_returns_bech32() { + let mut transport = WalletTransport::new(test_wallet()); + let cmd = APDUCommand { + cla: 0xED, + ins: INS_GET_ADDRESS, + p1: 0, + p2: 0, + data: vec![], + }; + let response = transport.exchange(&cmd).unwrap(); + let len = response[0] as usize; + let addr = std::str::from_utf8(&response[1..1 + len]).unwrap(); + assert!( + addr.starts_with("erd1"), + "expected bech32 with 'erd1' prefix, got {addr}" + ); + } + + #[test] + fn test_sign_hash_tx_matches_wallet_keccak() { + let wallet = test_wallet(); + let payload = b"hello tx"; + + let mut transport = WalletTransport::new(wallet.clone()); + let cmd = APDUCommand { + cla: 0xED, + ins: INS_SIGN_HASH_TX, + p1: 0x00, + p2: 0, + data: payload.to_vec(), + }; + let response = transport.exchange(&cmd).unwrap(); + assert_eq!(response[0], 0x40); + let ledger_sig = WalletSignature::from_bytes(response[1..].try_into().unwrap()); + + // The wallet signs keccak256(payload) for hash-based transactions. + let hash = crypto::keccak256(payload); + let expected_sig = wallet.private_key().unwrap().sign(hash); + assert_eq!(ledger_sig.to_bytes(), expected_sig.to_bytes()); + } + + #[test] + fn test_sign_message_matches_wallet_direct() { + let wallet = test_wallet(); + let payload = b"hello message"; + + let mut transport = WalletTransport::new(wallet.clone()); + let cmd = APDUCommand { + cla: 0xED, + ins: INS_SIGN_MESSAGE, + p1: 0x00, + p2: 0, + data: payload.to_vec(), + }; + let response = transport.exchange(&cmd).unwrap(); + assert_eq!(response[0], 0x40); + let ledger_sig = WalletSignature::from_bytes(response[1..].try_into().unwrap()); + + let expected_sig = wallet.private_key().unwrap().sign(payload); + assert_eq!(ledger_sig.to_bytes(), expected_sig.to_bytes()); + } + + #[test] + fn test_multi_chunk_accumulates_correctly() { + let wallet = test_wallet(); + let chunk1 = vec![0xAAu8; 150]; + let chunk2 = vec![0xBBu8; 50]; + let full_payload = [chunk1.as_slice(), chunk2.as_slice()].concat(); + + let mut transport = WalletTransport::new(wallet.clone()); + let cmd1 = APDUCommand { + cla: 0xED, + ins: INS_SIGN_HASH_TX, + p1: 0x00, + p2: 0, + data: chunk1, + }; + transport.exchange(&cmd1).unwrap(); // intermediate — discard + let cmd2 = APDUCommand { + cla: 0xED, + ins: INS_SIGN_HASH_TX, + p1: 0x80, + p2: 0, + data: chunk2, + }; + let response = transport.exchange(&cmd2).unwrap(); + + let ledger_sig = WalletSignature::from_bytes(response[1..].try_into().unwrap()); + let hash = crypto::keccak256(&full_payload); + let expected_sig = wallet.private_key().unwrap().sign(hash); + assert_eq!(ledger_sig.to_bytes(), expected_sig.to_bytes()); + } +} diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index 8f2853fd25..8e2a552962 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -199,7 +199,7 @@ fn ledger_sign_tx(address_index: u32, tx: &Transaction) -> Result) -> Result Date: Wed, 5 Aug 2026 00:44:45 +0300 Subject: [PATCH 158/195] ledger cleanup & fixes --- framework/meta/src/cli/cli_args_sender.rs | 2 +- framework/meta/src/cmd/ledger_cmd.rs | 4 +- sdk/core/src/wallet/ledger.rs | 2 + sdk/core/src/wallet/ledger/ledger_app.rs | 44 +++++++--------- .../src/wallet/ledger/ledger_transport.rs | 12 +++++ .../wallet/ledger/ledger_transport_mock.rs | 52 ++++++++++--------- .../wallet/ledger/ledger_transport_native.rs | 3 +- 7 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 sdk/core/src/wallet/ledger/ledger_transport.rs diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index 0727fe7f67..06dd918f50 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -54,7 +54,7 @@ fn load_ledger_wallet(address_index: u32) -> Result { use multiversx_chain_core::std::Bech32Address; use multiversx_sc_snippets::sdk::wallet::ledger::LedgerApp; - let app = LedgerApp::new().map_err(|e| anyhow!("{e}"))?; + let mut app = LedgerApp::new().map_err(|e| anyhow!("{e}"))?; let bech32_str = app.get_address(address_index).map_err(|e| anyhow!("{e}"))?; let bech32_addr = Bech32Address::from_bech32_string(bech32_str); let hrp = bech32_addr.hrp; diff --git a/framework/meta/src/cmd/ledger_cmd.rs b/framework/meta/src/cmd/ledger_cmd.rs index b0c4c41439..3819c466c7 100644 --- a/framework/meta/src/cmd/ledger_cmd.rs +++ b/framework/meta/src/cmd/ledger_cmd.rs @@ -18,7 +18,7 @@ fn ledger_cmd_inner(args: &LedgerArgs) -> anyhow::Result<()> { fn print_addresses(num_addresses: u32) -> anyhow::Result<()> { use multiversx_sc_snippets::sdk::wallet::ledger::LedgerApp; - let app = LedgerApp::new()?; + let mut app = LedgerApp::new()?; for i in 0..num_addresses { let address = app.get_address(i)?; println!("account index = 0 | address index = {i} | address: {address}"); @@ -35,7 +35,7 @@ fn print_addresses(_num_addresses: u32) -> anyhow::Result<()> { fn print_version() -> anyhow::Result<()> { use multiversx_sc_snippets::sdk::wallet::ledger::LedgerApp; - let app = LedgerApp::new()?; + let mut app = LedgerApp::new()?; let version = app.get_version()?; println!("MultiversX App version: {version}"); Ok(()) diff --git a/sdk/core/src/wallet/ledger.rs b/sdk/core/src/wallet/ledger.rs index a8cc8d3aac..38f9940703 100644 --- a/sdk/core/src/wallet/ledger.rs +++ b/sdk/core/src/wallet/ledger.rs @@ -3,10 +3,12 @@ mod ledger_config; mod ledger_error; mod ledger_transport; mod ledger_transport_mock; +mod ledger_transport_native; mod ledger_transport_wallet; pub use ledger_app::LedgerApp; pub use ledger_config::LedgerAppConfiguration; pub use ledger_error::LedgerError; pub use ledger_transport::LedgerTransport; +pub use ledger_transport_mock::MockTransport; pub use ledger_transport_wallet::WalletTransport; diff --git a/sdk/core/src/wallet/ledger/ledger_app.rs b/sdk/core/src/wallet/ledger/ledger_app.rs index 2547518135..9ee1070368 100644 --- a/sdk/core/src/wallet/ledger/ledger_app.rs +++ b/sdk/core/src/wallet/ledger/ledger_app.rs @@ -3,30 +3,21 @@ use ledger_transport_hid::{TransportNativeHID, hidapi::HidApi}; use super::{LedgerAppConfiguration, LedgerError, LedgerTransport}; -const CLA: u8 = 0xED; -const INS_GET_APP_CONFIG: u8 = 0x02; -const INS_GET_ADDRESS: u8 = 0x03; -const INS_SET_ADDRESS: u8 = 0x05; -const INS_SIGN_MESSAGE: u8 = 0x06; -const INS_SIGN_HASH_TX: u8 = 0x07; +pub(super) const CLA: u8 = 0xED; +pub(super) const INS_GET_APP_CONFIG: u8 = 0x02; +pub(super) const INS_GET_ADDRESS: u8 = 0x03; +pub(super) const INS_SET_ADDRESS: u8 = 0x05; +pub(super) const INS_SIGN_MESSAGE: u8 = 0x06; +pub(super) const INS_SIGN_HASH_TX: u8 = 0x07; // Account index is always 0 for MultiversX. const DEFAULT_ACCOUNT_INDEX: u32 = 0; -const MAX_CHUNK_SIZE: usize = 150; +pub(super) const MAX_CHUNK_SIZE: usize = 150; // Ledger responses: first byte is the length prefix (0x40 = 64), followed by 64 signature bytes. -const EXPECTED_SIG_FIRST_BYTE: u8 = 0x40; -const SIG_RESPONSE_LEN: usize = 65; -const SIG_LEN: usize = 64; - -/// Abstraction over the physical transport layer. -/// -/// Implementors send a single APDU command and return the response payload -/// (already status-word-checked). The only production implementation is -/// [`TransportNativeHID`]; tests provide a [`MockTransport`]. -pub trait LedgerTransport { - fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError>; -} +pub(super) const EXPECTED_SIG_FIRST_BYTE: u8 = 0x40; +pub(super) const SIG_RESPONSE_LEN: usize = 65; +pub(super) const SIG_LEN: usize = 64; /// Communicates with the MultiversX app on a connected Ledger hardware device. /// @@ -87,10 +78,15 @@ impl LedgerApp { pub fn get_address(&mut self, address_index: u32) -> Result { let data = build_account_address_data(address_index); let response = self.exchange(INS_GET_ADDRESS, 0x00, 0x00, &data)?; - // First byte is the length of the address string. - let address_bytes = response - .get(1..) - .ok_or_else(|| LedgerError::InvalidResponse("empty GET_ADDRESS response".into()))?; + let len = *response + .first() + .ok_or_else(|| LedgerError::InvalidResponse("empty GET_ADDRESS response".into()))? + as usize; + let address_bytes = response.get(1..1 + len).ok_or_else(|| { + LedgerError::InvalidResponse(format!( + "GET_ADDRESS: response too short for length prefix {len}" + )) + })?; String::from_utf8(address_bytes.to_vec()) .map_err(|e| LedgerError::InvalidResponse(format!("non-UTF8 address: {e}"))) } @@ -163,7 +159,7 @@ impl LedgerApp { } } -fn build_account_address_data(address_index: u32) -> Vec { +pub(super) fn build_account_address_data(address_index: u32) -> Vec { let mut data = Vec::with_capacity(8); data.extend_from_slice(&DEFAULT_ACCOUNT_INDEX.to_be_bytes()); data.extend_from_slice(&address_index.to_be_bytes()); diff --git a/sdk/core/src/wallet/ledger/ledger_transport.rs b/sdk/core/src/wallet/ledger/ledger_transport.rs new file mode 100644 index 0000000000..6dc33bf659 --- /dev/null +++ b/sdk/core/src/wallet/ledger/ledger_transport.rs @@ -0,0 +1,12 @@ +use ledger_apdu::APDUCommand; + +use super::LedgerError; + +/// Abstraction over the physical transport layer. +/// +/// Implementors send a single APDU command and return the response payload +/// (already status-word-checked). The only production implementation is +/// [`TransportNativeHID`]; tests provide a [`MockTransport`]. +pub trait LedgerTransport { + fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError>; +} diff --git a/sdk/core/src/wallet/ledger/ledger_transport_mock.rs b/sdk/core/src/wallet/ledger/ledger_transport_mock.rs index e38b2b1d4d..de2d9c226e 100644 --- a/sdk/core/src/wallet/ledger/ledger_transport_mock.rs +++ b/sdk/core/src/wallet/ledger/ledger_transport_mock.rs @@ -4,8 +4,7 @@ use std::rc::Rc; use ledger_apdu::APDUCommand; -use super::super::LedgerError; -use super::{LedgerApp, LedgerTransport, SIG_LEN}; +use super::{LedgerError, LedgerTransport}; /// A deterministic mock transport for unit testing. /// @@ -54,28 +53,31 @@ impl LedgerTransport for MockTransport { } } -/// Creates a valid 65-byte mock signature response for a given 64-byte -/// signature payload. -pub fn sig_response(sig: [u8; SIG_LEN]) -> Vec { - let mut r = vec![0x40u8]; - r.extend_from_slice(&sig); - r -} - -/// Returns a fixed 64-byte dummy signature (all `0xAB`). -pub fn dummy_sig() -> [u8; SIG_LEN] { - [0xABu8; SIG_LEN] -} - -pub fn app_with(responses: impl IntoIterator, LedgerError>>) -> LedgerApp { - LedgerApp::with_transport(Box::new(MockTransport::new(responses))) -} - #[cfg(test)] mod tests { - use super::mock::*; + use crate::wallet::ledger::{LedgerApp, ledger_app}; + use super::*; + /// Creates a valid 65-byte mock signature response for a given 64-byte + /// signature payload. + pub fn sig_response(sig: [u8; ledger_app::SIG_LEN]) -> Vec { + let mut r = vec![0x40u8]; + r.extend_from_slice(&sig); + r + } + + /// Returns a fixed 64-byte dummy signature (all `0xAB`). + pub fn dummy_sig() -> [u8; ledger_app::SIG_LEN] { + [0xABu8; ledger_app::SIG_LEN] + } + + pub fn app_with( + responses: impl IntoIterator, LedgerError>>, + ) -> LedgerApp { + LedgerApp::with_transport(Box::new(MockTransport::new(responses))) + } + // ── get_app_configuration ──────────────────────────────────────────────── #[test] @@ -132,8 +134,8 @@ mod tests { let cmds = commands.borrow(); let cmd = &cmds[0]; - assert_eq!(cmd.cla, CLA); - assert_eq!(cmd.ins, INS_GET_ADDRESS); + assert_eq!(cmd.cla, ledger_app::CLA); + assert_eq!(cmd.ins, ledger_app::INS_GET_ADDRESS); assert_eq!(cmd.p1, 0x00); assert_eq!(cmd.p2, 0x00); // data = account_index(4 BE) + address_index(4 BE) = 0x00000000 + 0x00000003 @@ -160,7 +162,7 @@ mod tests { let cmds = commands.borrow(); let cmd = &cmds[0]; - assert_eq!(cmd.ins, INS_SIGN_HASH_TX); + assert_eq!(cmd.ins, ledger_app::INS_SIGN_HASH_TX); assert_eq!(cmd.p1, 0x00); // first chunk } @@ -227,7 +229,7 @@ mod tests { let mut app = LedgerApp::with_transport(Box::new(transport)); let _ = app.sign_message(data).unwrap(); - assert_eq!(commands.borrow()[0].ins, INS_SIGN_MESSAGE); + assert_eq!(commands.borrow()[0].ins, ledger_app::INS_SIGN_MESSAGE); } // ── error mapping ──────────────────────────────────────────────────────── @@ -254,7 +256,7 @@ mod tests { #[test] fn test_address_data_encoding() { - let data = build_account_address_data(7); + let data = ledger_app::build_account_address_data(7); // account index 0 (4 bytes BE) + address index 7 (4 bytes BE) assert_eq!(data, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]); } diff --git a/sdk/core/src/wallet/ledger/ledger_transport_native.rs b/sdk/core/src/wallet/ledger/ledger_transport_native.rs index 11277d0a94..834e36b197 100644 --- a/sdk/core/src/wallet/ledger/ledger_transport_native.rs +++ b/sdk/core/src/wallet/ledger/ledger_transport_native.rs @@ -1,8 +1,7 @@ use ledger_apdu::APDUCommand; use ledger_transport_hid::TransportNativeHID; -use super::LedgerError; - +use super::{LedgerError, LedgerTransport}; impl LedgerTransport for TransportNativeHID { fn exchange(&mut self, command: &APDUCommand>) -> Result, LedgerError> { From 7df41c7696f5d8ae5ebb2a0794ab957408bb947f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 5 Aug 2026 00:45:01 +0300 Subject: [PATCH 159/195] ledger CI fix --- .github/workflows/interactor-tests-ledger.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/interactor-tests-ledger.yml b/.github/workflows/interactor-tests-ledger.yml index 78b513e41c..1a84501f6b 100644 --- a/.github/workflows/interactor-tests-ledger.yml +++ b/.github/workflows/interactor-tests-ledger.yml @@ -22,6 +22,9 @@ jobs: toolchain: 1.96 target: wasm32v1-none + - name: Install libudev (required by hidapi/ledger feature) + run: sudo apt-get update && sudo apt-get install -y libudev-dev + - name: Install sc-meta run: cargo install --path framework/meta --locked From 07223fee6be522cec7b569c8585aa46fb223e9ea Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 27 Jul 2026 18:26:33 +0300 Subject: [PATCH 160/195] relayer implementation plan --- framework/meta/relayer.md | 343 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 framework/meta/relayer.md diff --git a/framework/meta/relayer.md b/framework/meta/relayer.md new file mode 100644 index 0000000000..343b040df2 --- /dev/null +++ b/framework/meta/relayer.md @@ -0,0 +1,343 @@ +# Relayed Transactions v3 — Implementation Plan + +## Overview + +Relayed transactions v3 add two optional fields to an existing transaction: +- `relayer` — bech32 address of the account that pays the gas fees +- `relayerSignature` — hex signature proving the relayer's agreement + +No inner transaction wrapping is needed (unlike v1/v2). The sender signs first, then the relayer signs. + +Gas: `gas_limit = base_cost + base_cost + cost_per_byte * len(data)` + +Signing order: +1. Sender signs over the full transaction bytes (which already include the `relayer` field). +2. Relayer signs over the same transaction bytes (with `relayer` set but `relayerSignature` still empty/absent). + +--- + +## Phase 1 — SDK: Extend the `Transaction` struct + +**File:** `sdk/core/src/data/transaction/transaction_request.rs` + +Add two new optional fields, skipped during serialization when absent: + +```rust +#[serde(skip_serializing_if = "Option::is_none")] +pub relayer: Option, + +#[serde(skip_serializing_if = "Option::is_none")] +pub relayer_signature: Option, +``` + +The existing `Wallet::sign_tx` method already signs over the serialized transaction bytes via protobuf. The protobuf schema already supports `Relayer` and `RelayerSignature` fields (confirmed in `chain/vm`). No new signing method is needed — the same `sign_tx` is called by both sender and relayer (at different stages, with `relayer_signature` absent during both signing calls). + +Verify that the protobuf serializer (`sdk/core/src/data/transaction/`) includes `relayer` in the bytes-to-sign and excludes `relayer_signature` from both signing payloads. + +--- + +## Phase 2 — sc-meta CLI + +### 2a. `RelayerArgs` struct + +**File:** `framework/meta/src/cli/cli_args_sender.rs` +*(mirrors the existing `SenderArgs`)* + +```rust +/// Wallet / relayer arguments for commands that add a relayer signature. +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct RelayerArgs { + /// Path to a PEM wallet file for the relayer. + #[arg(long = "relayer-pem", group = "relayer_source")] + pub pem: Option, + + /// Path to a JSON keystore wallet file for the relayer. + #[arg(long = "relayer-keyfile", group = "relayer_source")] + pub keyfile: Option, + + /// Relayer keystore password (plain text). If omitted, prompted interactively. + #[arg(long = "relayer-keystore-password")] + pub keystore_password: Option, +} +``` + +Add a companion `load_relayer_wallet(args: &RelayerArgs) -> Result>` function alongside the existing `load_wallet`. Returns `None` when no relayer args are set (relayer wallet is optional in most commands). + +### 2b. `--relayer` address field + +**File:** `framework/meta/src/cli/cli_args_tx.rs` + +Add to `TxArgs`: +```rust +/// Bech32 address of the relayer. If set, the transaction gas limit must +/// include the extra base cost for the relayed operation. +#[arg(long)] +pub relayer: Option, +``` + +This field is used by `tx new`, `tx deploy`, `tx call`, `tx upgrade` to set `tx.relayer` before the sender signs. + +### 2c. Extend existing commands to accept a relayer wallet + +**Files:** `cli_args_tx.rs`, `tx_cli_new.rs`, `tx_cli_deploy.rs`, `tx_cli_call.rs`, `tx_cli_upgrade.rs` + +For each of `DeployArgs`, `CallArgs`, `UpgradeArgs`, `NewArgs`: + +```rust +#[command(flatten)] +pub relayer: RelayerArgs, +``` + +In their respective `tx_cli_*.rs` implementations: +1. If `args.tx.relayer` is set, write it to `tx.relayer` before the sender signs. +2. After the sender signs, call `load_relayer_wallet(&args.relayer)?`. +3. If a relayer wallet is present, validate its address matches `tx.relayer`, then call `wallet.sign_tx(&tx)` and write the result to `tx.relayer_signature`. + +**File:** `framework/meta/src/cli/cli_args_tx.rs` — extend `SignArgs`: + +```rust +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct SignArgs { + #[arg(long)] + pub infile: PathBuf, + + #[command(flatten)] + pub gateway: GatewayArgs, + + #[command(flatten)] + pub sender: SenderArgs, // existing + + #[command(flatten)] + pub relayer: RelayerArgs, // NEW + + // ... existing send/wait_result/outfile +} +``` + +In `tx_cli_sign.rs`: +- After the sender signs (existing behaviour), optionally load the relayer wallet and add `relayer_signature`. + +### 2d. New `tx relay` subcommand + +This is a dedicated command for the relayer party: they receive a pre-built, sender-signed transaction file and add their signature. + +**`cli_args_tx.rs`** — add variant to `TxCliAction`: + +```rust +#[command(about = "Adds the relayer signature to a previously signed transaction.")] +Relay(RelayArgs), +``` + +**New `RelayArgs` struct:** + +```rust +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct RelayArgs { + /// Path to the input transaction file (must already have sender signature and relayer field set). + #[arg(long)] + pub infile: PathBuf, + + #[command(flatten)] + pub relayer: RelayerArgs, + + #[command(flatten)] + pub gateway: GatewayArgs, + + /// If set, the transaction is broadcast after signing. + #[arg(long, default_value = "false")] + pub send: bool, + + /// Wait for the transaction result. Requires --send. + #[arg(long, default_value = "false", requires = "send")] + pub wait_result: bool, + + /// Path to write the output to. Defaults to stdout. + #[arg(long)] + pub outfile: Option, +} +``` + +**New file:** `framework/meta/src/cmd/tx/tx_cli_relay.rs` + +Logic (`tx_relay_inner`): +1. Load `RelayerArgs` wallet — error if no wallet provided. +2. Load transaction from `--infile`. +3. Validate `tx.relayer` is set. +4. Validate relayer wallet address == `tx.relayer`. +5. Validate `tx.signature` is present (sender must have already signed). +6. Call `relayer_wallet.sign_tx(&tx)` and set `tx.relayer_signature`. +7. Save / broadcast via existing `save_output` / `broadcast_and_save` helpers. + +**`tx.rs`** — wire up: + +```rust +TxCliAction::Relay(relay_args) => tx_relay(relay_args).await, +``` + +--- + +## Phase 3 — Tx environment trait for relayer + +**File:** `framework/base/src/types/interaction/tx_env.rs` + +Add a new trait analogous to `TxEnvWithTxHash`, to carry the relayer address through the tx building pipeline: + +```rust +pub trait TxEnvWithRelayer: TxEnv { + fn set_relayer_address(&mut self, relayer: ManagedAddress); + + /// Retrieves the relayer address, while resetting it in self. + fn take_relayer_address(&mut self) -> Option>; +} +``` + +This trait is implemented wherever `TxEnvWithTxHash` is already implemented: + +- **`ScenarioTxEnvData`** (`framework/scenario/src/facade/world_tx/scenario_tx_env.rs`) + Add `relayer: Option>` field; implement the two methods. + +- **`ScenarioEnvExec`** (`framework/scenario/src/facade/world_tx/scenario_exec_call.rs`) + Forward to `self.data`. + +- **`InteractorEnvExec`** (`framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs`) + Forward to `self.data`. + +**`Tx` builder method** (`framework/base/src/types/interaction/tx.rs`) + +Mirroring `.id()` and `.tx_hash()` (which require `Env: TxEnvWithTxHash`), add a `.relayer()` method that requires `Env: TxEnvWithRelayer`: + +```rust +impl Tx +where + Env: TxEnvWithRelayer, + // ... other bounds +{ + /// Sets the relayer address for a relayed v3 transaction. + /// + /// The relayer pays the gas fees. After the sender signs the transaction, + /// the relayer must also sign it (adding `relayerSignature`) before broadcasting. + pub fn relayer(mut self, relayer: A) -> Self + where + A: AnnotatedValue>, + { + let relayer_addr = relayer.into_value(&self.env); + self.env.set_relayer_address(relayer_addr); + self + } +} +``` + +The relayer address stored in the env is read by the interactor execution pipeline (just before signing) to populate `tx.relayer` on the SDK `Transaction`. + +--- + +## Phase 4 — Interactor Framework + +### 4a. `Transaction` struct already extended (Phase 1) + +No further struct changes needed here. + +### 4b. Relayer registration — reuse `sender_map` + +No new map or registration method is needed. The relayer is registered via the same `register_wallet` / `register_wallets` path as any other sender. The `InteractorBase` simply stores the chosen relayer address: + +```rust +pub relayer_address: Option
, // added to InteractorBase +``` + +A dedicated setter: +```rust +pub fn set_relayer(&mut self, address: Address) { + // address must already be registered in sender_map + assert!(self.sender_map.contains_key(&address), "relayer wallet not registered"); + self.relayer_address = Some(address); +} +``` + +Users register the wallet normally and then call `set_relayer`: +```rust +let relayer_addr = interactor.register_wallet(relayer_wallet).await; +interactor.set_relayer(relayer_addr); +``` + +### 4c. `InteractorConfig` trait — no change + +**File:** `framework/snippets/src/config/interactor_config.rs` + +The `InteractorConfig` trait is **not** extended. There is no `relayer_wallet()` method. Contract-specific `Config` structs include a relayer wallet as a regular `WalletConfig` field (e.g. `pub relayer: WalletConfig`) and return it inside `register_wallets()`. The interactor setup code then calls `set_relayer` explicitly after loading the config. + +Example in contract interactor setup code: +```rust +let relayer_addr = interactor.register_wallet(config.relayer.wallet().clone()).await; +interactor.set_relayer(relayer_addr); +``` + +### 4d. Transaction dispatch + +**File:** `framework/snippets/src/interactor/interactor_sender.rs` + +After the sender signs, if `relayer_address` is set on the interactor: +1. `tx.relayer` is populated from the env (via `take_relayer_address`, or from `interactor.relayer_address`). +2. `sign_tx_as_relayer` signs using the relayer's entry in `sender_map`: + +```rust +pub(crate) fn sign_tx_as_relayer(&self, tx: &mut Transaction) { + let Some(relayer_address) = &self.relayer_address else { return }; + let relayer = self.sender_map + .get(relayer_address) + .expect("relayer wallet not registered"); + let sig = relayer.wallet.sign_tx(tx); + tx.relayer_signature = Some(sig); +} +``` + +--- + +## Summary of new files + +| File | Purpose | +|---|---| +| `framework/meta/src/cmd/tx/tx_cli_relay.rs` | Implementation of `tx relay` command | + +## Summary of modified files + +| File | Change | +|---|---| +| `sdk/core/src/data/transaction/transaction_request.rs` | Add `relayer`, `relayer_signature` fields | +| `framework/base/src/types/interaction/tx_env.rs` | Add `TxEnvWithRelayer` trait | +| `framework/base/src/types/interaction/tx.rs` | Add `.relayer()` method on `Tx` (requires `Env: TxEnvWithRelayer`) | +| `framework/scenario/src/facade/world_tx/scenario_tx_env.rs` | Add `relayer` field to `ScenarioTxEnvData`; implement `TxEnvWithRelayer` | +| `framework/scenario/src/facade/world_tx/scenario_exec_call.rs` | Implement `TxEnvWithRelayer` for `ScenarioEnvExec` | +| `framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs` | Implement `TxEnvWithRelayer` for `InteractorEnvExec` | +| `framework/meta/src/cli/cli_args_sender.rs` | Add `RelayerArgs`, `load_relayer_wallet` | +| `framework/meta/src/cli/cli_args_tx.rs` | Add `--relayer` to `TxArgs`; add `RelayerArgs` flatten to `DeployArgs`/`CallArgs`/`UpgradeArgs`/`NewArgs`/`SignArgs`; add `RelayArgs` and `TxCliAction::Relay` | +| `framework/meta/src/cmd/tx/tx.rs` | Wire `TxCliAction::Relay` | +| `framework/meta/src/cmd/tx/tx_cli_new.rs` | Set `tx.relayer`, sign as relayer if wallet provided | +| `framework/meta/src/cmd/tx/tx_cli_deploy.rs` | Same | +| `framework/meta/src/cmd/tx/tx_cli_call.rs` | Same | +| `framework/meta/src/cmd/tx/tx_cli_upgrade.rs` | Same | +| `framework/meta/src/cmd/tx/tx_cli_sign.rs` | Optionally sign as relayer if `RelayerArgs` provided | +| `framework/snippets/src/interactor/interactor_base.rs` | Add `relayer_address: Option
` field | +| `framework/snippets/src/interactor/interactor_sender.rs` | Add `set_relayer`, `sign_tx_as_relayer` | + +--- + +## Key design decisions + +1. **`RelayerArgs` mirrors `SenderArgs`** — same fields, same loading logic, different prefix (`--relayer-pem` etc.). This follows the Python CLI pattern exactly. + +2. **Reuse `sender_map` for relayers** — no new map, no new registration path. A relayer is just another registered wallet identified by its address. + +3. **No `relayer_wallet()` in `InteractorConfig`** — the trait is not extended. Relayer wallets are returned by `register_wallets()` alongside senders. The interactor setup code calls `set_relayer(address)` explicitly. + +4. **`TxEnvWithRelayer` as a first-class trait** — mirrors the `TxEnvWithTxHash` pattern. The relayer address is carried in the environment, not as a separate parameter, so the tx builder `.relayer()` method integrates cleanly into the existing `Tx` chain. + +5. **`--relayer` address vs `--relayer-pem`** — the address flag lets the sender declare the intended relayer without providing the key (two-step workflow). The wallet flags are only needed when both signatures happen in one command or via `tx relay`. + +6. **Two-step vs one-step CLI workflow:** + - *One-step:* `sc-meta tx new --relayer erd1... --relayer-pem relayer.pem ...` — both signatures in one go. + - *Two-step:* `sc-meta tx new --relayer erd1... ...` (sender signs, saves to file) → `sc-meta tx relay --infile tx.json --relayer-pem relayer.pem` (relayer signs, broadcasts). + +7. **Gas limit** — the caller is responsible for providing the correct gas limit including the extra base cost. No automatic adjustment is added at this stage; documentation/comments will explain the formula. + +8. **Broadcast validation** — `broadcast_and_save` should additionally check that if `tx.relayer` is set, `tx.relayer_signature` is also present before broadcasting. From e7c75a05ea0f3e28a8179cd7aac45e55c4e3fb60 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 5 Aug 2026 20:37:46 +0300 Subject: [PATCH 161/195] adder interactor ledger fix --- contracts/examples/adder/interactor/src/adder_interactor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/examples/adder/interactor/src/adder_interactor.rs b/contracts/examples/adder/interactor/src/adder_interactor.rs index 751d82135f..35451deee8 100644 --- a/contracts/examples/adder/interactor/src/adder_interactor.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor.rs @@ -29,6 +29,7 @@ impl InteractorConfig for Config { fn register_wallets(&self) -> Vec { let mut wallets = vec![self.owner.wallet().clone(), self.wallet.wallet().clone()]; + #[cfg(feature = "ledger")] if let Some(ledger_wallet) = &self.ledger_wallet { wallets.push(ledger_wallet.wallet().clone()); } From 7c9e7abea45ca9cf0170355fac7ad9f5a848130a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 5 Aug 2026 20:59:51 +0300 Subject: [PATCH 162/195] relayed v3 support in sc-meta CLI & interactors --- .../examples/adder/interactor/config.toml | 7 ++ .../adder/interactor/src/adder_interactor.rs | 31 +++++++ .../interactor/src/adder_interactor_cli.rs | 5 ++ .../tests/adder_interactor_cs_test.rs | 9 ++ .../examples/adder/snippets/adder.snippets.sh | 59 +++++++++++++ framework/base/src/types/interaction/tx.rs | 30 ++++++- .../base/src/types/interaction/tx_env.rs | 7 ++ framework/meta/src/cli/cli_args_sender.rs | 37 ++++++++ framework/meta/src/cli/cli_args_tx.rs | 54 +++++++++++- framework/meta/src/cmd/tx.rs | 3 + framework/meta/src/cmd/tx/tx_cli_call.rs | 14 +++- framework/meta/src/cmd/tx/tx_cli_common.rs | 40 ++++++++- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 4 +- framework/meta/src/cmd/tx/tx_cli_new.rs | 29 ++++++- framework/meta/src/cmd/tx/tx_cli_relay.rs | 84 +++++++++++++++++++ framework/meta/src/cmd/tx/tx_cli_sign.rs | 24 +++++- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 14 +++- .../src/facade/world_tx/scenario_exec_call.rs | 14 +++- .../src/facade/world_tx/scenario_tx_env.rs | 17 +++- .../src/interactor/interactor_base.rs | 7 ++ .../interactor_scenario/interactor_sc_call.rs | 9 +- .../interactor_sc_deploy.rs | 4 + .../src/interactor/interactor_sender.rs | 34 ++++++++ .../interactor_tx/interactor_exec_env.rs | 16 +++- .../interactor_tx/interactor_run_trait.rs | 1 + .../data/transaction/transaction_request.rs | 4 + sdk/core/src/wallet/wallet_impl.rs | 1 + sdk/http/examples/sign_tx.rs | 2 + sdk/http/examples/sign_txs.rs | 2 + sdk/http/examples/tx_cost.rs | 2 + 30 files changed, 545 insertions(+), 19 deletions(-) create mode 100644 framework/meta/src/cmd/tx/tx_cli_relay.rs diff --git a/contracts/examples/adder/interactor/config.toml b/contracts/examples/adder/interactor/config.toml index 64c496b999..a099214ce9 100644 --- a/contracts/examples/adder/interactor/config.toml +++ b/contracts/examples/adder/interactor/config.toml @@ -27,3 +27,10 @@ test_wallet = 'ivan' [ledger-wallet] ledger = true + +# Optional relayer wallet, used in `add-relayed`. +[relayer] +test_wallet = 's1mon' # ivan is in shard 1 +# pem = 'path/to/relayer.pem' +# keyfile = 'path/to/relayer.json' +# keystore_password = 'password' diff --git a/contracts/examples/adder/interactor/src/adder_interactor.rs b/contracts/examples/adder/interactor/src/adder_interactor.rs index 35451deee8..8b1183c298 100644 --- a/contracts/examples/adder/interactor/src/adder_interactor.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor.rs @@ -20,6 +20,7 @@ pub struct Config { pub wallet: WalletConfig, #[serde(rename = "ledger-wallet")] pub ledger_wallet: Option, + pub relayer: Option, } impl InteractorConfig for Config { @@ -33,6 +34,9 @@ impl InteractorConfig for Config { if let Some(ledger_wallet) = &self.ledger_wallet { wallets.push(ledger_wallet.wallet().clone()); } + if let Some(relayer) = &self.relayer { + wallets.push(relayer.wallet().clone()); + } wallets } } @@ -75,6 +79,9 @@ pub async fn adder_cli() { Some(adder_interactor_cli::InteractCliCommand::Add(args)) => { adder_interact.add(args.value).await; } + Some(adder_interactor_cli::InteractCliCommand::AddRelayed(args)) => { + adder_interact.add_relayed(args.value).await; + } Some(adder_interactor_cli::InteractCliCommand::AddLedger(args)) => { adder_interact.add_ledger(args.value).await; } @@ -197,4 +204,28 @@ impl AdderInteractor { println!("Successfully performed add via ledger"); } + + /// Same as `add`, but uses the configured relayer to pay the gas. + /// Requires `[relayer]` to be set in config.toml. + /// The gas limit includes an extra base cost (50 000) for the relay operation. + pub async fn add_relayed(&mut self, value: u32) { + let relayer = self + .config + .relayer + .as_ref() + .expect("relayer expected in config"); + self.interactor + .tx() + .id("interactor add relayed") + .from(self.config.wallet.address()) + .relayer(relayer.address()) + .to(self.state.current_adder_address()) + .gas(6_050_000u64) + .typed(adder_proxy::AdderProxy) + .add(value) + .run() + .await; + + println!("Successfully performed relayed add"); + } } diff --git a/contracts/examples/adder/interactor/src/adder_interactor_cli.rs b/contracts/examples/adder/interactor/src/adder_interactor_cli.rs index 00b215d2c9..5a0c8ededb 100644 --- a/contracts/examples/adder/interactor/src/adder_interactor_cli.rs +++ b/contracts/examples/adder/interactor/src/adder_interactor_cli.rs @@ -22,6 +22,11 @@ pub enum InteractCliCommand { Add(AddArgs), #[command(name = "add-ledger", about = "Add value via ledger device")] AddLedger(AddArgs), + #[command( + name = "add-relayed", + about = "Add value via a relayed transaction (relayer pays gas)" + )] + AddRelayed(AddArgs), } #[derive(Default, Clone, PartialEq, Eq, Debug, Args)] diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs index cc6121bd5a..0ec88400b1 100644 --- a/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_cs_test.rs @@ -11,6 +11,7 @@ fn chain_simulator_config() -> Config { owner: WalletConfig::from_test_wallet("mike"), wallet: WalletConfig::from_test_wallet("ivan"), ledger_wallet: None, + relayer: Some(WalletConfig::from_test_wallet("s1mon")), } } @@ -71,4 +72,12 @@ async fn deploy_add_upgrade_test() { // Sum will remain 7 let sum = adder_interact.get_sum().await; assert_eq!(sum, 7u32.into()); + + adder_interact.add_relayed(3u32).await; + + adder_interact.generate_blocks(2).await; + + // Sum will be 7 + 3 = 10 + let sum = adder_interact.get_sum().await; + assert_eq!(sum, 10u32.into()); } diff --git a/contracts/examples/adder/snippets/adder.snippets.sh b/contracts/examples/adder/snippets/adder.snippets.sh index a1695e325f..4a22b74dc1 100644 --- a/contracts/examples/adder/snippets/adder.snippets.sh +++ b/contracts/examples/adder/snippets/adder.snippets.sh @@ -14,10 +14,12 @@ DATA_TOOL=("${BASE[@]}" data) # ────────────────────────────────────────────────────────────────────────────── PEM="alice.pem" +RELAYER_PEM="s1mon.pem" # Creates a test wallet (alice.pem). Only works with sc-meta. setup() { sc-meta wallet test-wallet --name alice + sc-meta wallet test-wallet --name s1mon # will act as the relayer } case "${NETWORK}" in @@ -143,3 +145,60 @@ add_ledger() { --wait-result \ || return } + +# Demonstrates relayed v3 transactions (sc-meta only): +# Bob (relayer) pays the gas so Alice (sender) needs no EGLD for fees. +# +# Two-step flow: +# 1. Alice builds + signs the call with --relayer pointing to Bob's address. +# The gas limit includes an extra base cost (50 000) for the relay operation. +# The transaction is saved to a file — not broadcast yet. +# 2. Bob signs as relayer with `tx relay` and broadcasts. +add_relayed() { + NUMBER=5 + RELAYER_ADDRESS="erd1fwdty5j20525zmah34xcsfyacvp890twuxu2ql2rgtpn5s9qqqqsajtqv5" + OUTFILE_CALL_UNSIGNED="call-unsigned-${NETWORK}.interaction.json" + OUTFILE_CALL_RELAYED="call-relayed-${NETWORK}.interaction.json" + + # Step 1: Alice signs (gas = 5 000 000 execution + 50 000 extra relay base cost). + "${TX_TOOL[@]}" call "${ADDRESS}" \ + --pem="${PEM}" \ + --gas-limit=5050000 \ + --function="add" \ + --arguments "${NUMBER}" \ + --relayer="${RELAYER_ADDRESS}" \ + --proxy="${PROXY}" \ + --chain="${CHAIN}" \ + --outfile="${OUTFILE_CALL_UNSIGNED}" \ + || return + + # Step 2: Bob adds his relayer signature and broadcasts. + "${BASE[@]}" tx relay \ + --infile="${OUTFILE_CALL_UNSIGNED}" \ + --relayer-pem="${RELAYER_PEM}" \ + --proxy="${PROXY}" \ + --send \ + --outfile="${OUTFILE_CALL_RELAYED}" \ + --wait-result \ + || return +} + +# One-step variant: Alice signs and Bob co-signs in a single command. +# Requires both PEM files to be present on the same machine. +# The relayer address is derived automatically from --relayer-pem. +add_relayed_onestep() { + NUMBER=5 + + "${TX_TOOL[@]}" call "${ADDRESS}" \ + --pem="${PEM}" \ + --gas-limit=5050000 \ + --function="add" \ + --arguments "${NUMBER}" \ + --relayer-pem="${RELAYER_PEM}" \ + --proxy="${PROXY}" \ + --chain="${CHAIN}" \ + --send \ + --outfile="${OUTFILE_CALL}" \ + --wait-result \ + || return +} diff --git a/framework/base/src/types/interaction/tx.rs b/framework/base/src/types/interaction/tx.rs index 490b99ab8c..467ec01155 100644 --- a/framework/base/src/types/interaction/tx.rs +++ b/framework/base/src/types/interaction/tx.rs @@ -12,9 +12,9 @@ use super::{ AnnotatedValue, Code, DeployCall, Egld, EgldPayment, ExplicitGas, FromSource, FunctionCall, ManagedArgBuffer, OriginalResultMarker, RHList, RHListAppendNoRet, RHListAppendRet, RHListItem, TxCodeSource, TxCodeValue, TxData, TxDataFunctionCall, TxEgldValue, TxEnv, - TxEnvMockDeployAddress, TxEnvWithTxHash, TxFrom, TxFromSourceValue, TxFromSpecified, TxGas, - TxGasValue, TxPayment, TxPaymentEgldOnly, TxProxyTrait, TxResultHandler, TxTo, TxToSpecified, - UpgradeCall, + TxEnvMockDeployAddress, TxEnvWithRelayer, TxEnvWithTxHash, TxFrom, TxFromSourceValue, + TxFromSpecified, TxGas, TxGasValue, TxPayment, TxPaymentEgldOnly, TxProxyTrait, + TxResultHandler, TxTo, TxToSpecified, UpgradeCall, }; /// Universal representation of a blockchain transaction. @@ -993,3 +993,27 @@ where self } } + +impl Tx +where + Env: TxEnvWithRelayer, + From: TxFrom, + To: TxTo, + Payment: TxPayment, + Gas: TxGas, + Data: TxData, + RH: TxResultHandler, +{ + /// Sets the relayer address for a relayed v3 transaction. + /// + /// The relayer pays the gas fees. After the sender signs the transaction, + /// the relayer must also sign it (adding `relayerSignature`) before broadcasting. + pub fn relayer(mut self, relayer: A) -> Self + where + A: AnnotatedValue>, + { + let relayer_addr = relayer.into_value(&self.env); + self.env.set_relayer_address(relayer_addr); + self + } +} diff --git a/framework/base/src/types/interaction/tx_env.rs b/framework/base/src/types/interaction/tx_env.rs index d1e06c9f69..d3a56a1952 100644 --- a/framework/base/src/types/interaction/tx_env.rs +++ b/framework/base/src/types/interaction/tx_env.rs @@ -38,3 +38,10 @@ pub trait TxEnvWithTxHash: TxEnv { /// Retrieves current tx hash, while resetting it in self. fn take_tx_hash(&mut self) -> Option; } + +pub trait TxEnvWithRelayer: TxEnv { + fn set_relayer_address(&mut self, relayer: ManagedAddress); + + /// Retrieves the relayer address, while resetting it in self. + fn take_relayer_address(&mut self) -> Option>; +} diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index 06dd918f50..b6b9936296 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -28,6 +28,23 @@ pub struct SenderArgs { } /// Load a wallet from a PEM file, JSON keystore, or Ledger device. +/// Wallet arguments for the relayer. Mirrors [`SenderArgs`] with a `--relayer-*` prefix. +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct RelayerArgs { + /// Path to a PEM wallet file for the relayer. + #[arg(long = "relayer-pem", group = "relayer_source")] + pub relayer_pem: Option, + + /// Path to a JSON keystore wallet file for the relayer. + #[arg(long = "relayer-keyfile", group = "relayer_source")] + pub relayer_keyfile: Option, + + /// Relayer keystore password (plain text). If omitted, will prompt interactively. + #[arg(long = "relayer-keystore-password")] + pub relayer_keystore_password: Option, +} + +/// Load a wallet from a PEM file or JSON keystore. pub fn load_wallet(sender: &SenderArgs) -> Result { if let Some(pem) = &sender.pem { Wallet::from_pem_file(pem).context("failed to load PEM wallet") @@ -70,6 +87,26 @@ fn load_ledger_wallet(address_index: u32) -> Result { )) } +/// Load a relayer wallet from [`RelayerArgs`], returning `None` if no relayer args are set. +pub fn load_relayer_wallet(args: &RelayerArgs) -> Result> { + if let Some(pem) = &args.relayer_pem { + let wallet = Wallet::from_pem_file(pem).context("failed to load relayer PEM wallet")?; + Ok(Some(wallet)) + } else if let Some(keyfile) = &args.relayer_keyfile { + let password = match &args.relayer_keystore_password { + Some(pw) => pw.clone(), + None => get_keystore_password(), + }; + let keystore = Keystore::from_file(keyfile)?; + let wallet = keystore + .decrypt_wallet(&password) + .context("failed to load relayer keystore wallet")?; + Ok(Some(wallet)) + } else { + Ok(None) + } +} + pub fn get_keystore_password() -> String { print!("Insert password: "); std::io::stdout().flush().unwrap(); diff --git a/framework/meta/src/cli/cli_args_tx.rs b/framework/meta/src/cli/cli_args_tx.rs index 6894a299dc..dff73fa8a9 100644 --- a/framework/meta/src/cli/cli_args_tx.rs +++ b/framework/meta/src/cli/cli_args_tx.rs @@ -1,7 +1,7 @@ use clap::{Args, Subcommand}; use std::path::PathBuf; -pub use super::cli_args_sender::SenderArgs; +pub use super::cli_args_sender::{RelayerArgs, SenderArgs}; #[derive(Clone, PartialEq, Eq, Debug, Args)] pub struct TxCliArgs { @@ -31,6 +31,11 @@ pub enum TxCliAction { #[command(about = "Signs an unsigned transaction from a file.")] Sign(SignArgs), + + #[command( + about = "Adds the relayer signature to a previously signed transaction (relayed v3)." + )] + Relay(RelayArgs), } /// Gateway / network arguments shared by commands that talk to the blockchain. @@ -62,6 +67,11 @@ pub struct TxArgs { #[arg(long)] pub nonce: Option, + /// Bech32 address of the relayer that will pay the gas fees (relayed v3). + /// The gas limit must include the extra base cost for the relayed operation. + #[arg(long)] + pub relayer: Option, + /// If set, the transaction is broadcast to the network. /// Without this flag the signed tx JSON is written to --outfile or stdout. #[arg(long, default_value = "false")] @@ -130,6 +140,9 @@ pub struct DeployArgs { #[command(flatten)] pub sender: SenderArgs, + #[command(flatten)] + pub relayer: RelayerArgs, + #[command(flatten)] pub tx: TxArgs, @@ -159,6 +172,9 @@ pub struct CallArgs { #[command(flatten)] pub sender: SenderArgs, + #[command(flatten)] + pub relayer: RelayerArgs, + #[command(flatten)] pub tx: TxArgs, @@ -189,6 +205,9 @@ pub struct UpgradeArgs { #[command(flatten)] pub sender: SenderArgs, + #[command(flatten)] + pub relayer: RelayerArgs, + #[command(flatten)] pub tx: TxArgs, @@ -236,6 +255,9 @@ pub struct NewArgs { #[command(flatten)] pub sender: SenderArgs, + #[command(flatten)] + pub relayer: RelayerArgs, + #[command(flatten)] pub tx: TxArgs, @@ -283,6 +305,36 @@ pub struct SignArgs { #[command(flatten)] pub sender: SenderArgs, + #[command(flatten)] + pub relayer: RelayerArgs, + #[command(flatten)] pub gateway: GatewayArgs, } + +/// Arguments for the `tx relay` command: sign a transaction as the relayer. +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct RelayArgs { + /// Path to the input transaction JSON file. + /// Must already contain the sender signature and the `relayer` field. + #[arg(long)] + pub infile: PathBuf, + + #[command(flatten)] + pub relayer: RelayerArgs, + + #[command(flatten)] + pub gateway: GatewayArgs, + + /// If set, the transaction is broadcast after signing. + #[arg(long, default_value = "false")] + pub send: bool, + + /// Wait for the transaction result. Requires --send. + #[arg(long, default_value = "false", requires = "send")] + pub wait_result: bool, + + /// Path to write the output to. Defaults to stdout. + #[arg(long)] + pub outfile: Option, +} diff --git a/framework/meta/src/cmd/tx.rs b/framework/meta/src/cmd/tx.rs index 90b264de26..907e771a0a 100644 --- a/framework/meta/src/cmd/tx.rs +++ b/framework/meta/src/cmd/tx.rs @@ -6,6 +6,7 @@ mod tx_cli_common; mod tx_cli_deploy; mod tx_cli_new; mod tx_cli_query; +mod tx_cli_relay; mod tx_cli_send; mod tx_cli_sign; mod tx_cli_upgrade; @@ -15,6 +16,7 @@ use tx_cli_call::tx_call; use tx_cli_deploy::tx_deploy; use tx_cli_new::tx_new; use tx_cli_query::tx_query; +use tx_cli_relay::tx_relay; use tx_cli_send::tx_send; use tx_cli_sign::tx_sign; use tx_cli_upgrade::tx_upgrade; @@ -28,5 +30,6 @@ pub async fn tx_cli(args: &TxCliArgs) { TxCliAction::New(new_args) => tx_new(new_args).await, TxCliAction::Send(send_args) => tx_send(send_args).await, TxCliAction::Sign(sign_args) => tx_sign(sign_args).await, + TxCliAction::Relay(relay_args) => tx_relay(relay_args).await, } } diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 0785c6aa89..043c5a793f 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -2,7 +2,7 @@ use anyhow::Result; use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use super::parse_payments::parse_all_payment_args; -use super::tx_cli_common::{build_arg_buffer, load_wallet, sign_and_dispatch}; +use super::tx_cli_common::{build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch}; use crate::cli::cli_args_tx::CallArgs; pub async fn tx_call(args: &CallArgs) { @@ -43,5 +43,15 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { .arguments_raw(arg_buffer) .into_sdk_transaction(); - sign_and_dispatch(wallet, tx, nonce, &args.tx, &args.gateway, None).await + let relayer_wallet = load_relayer_wallet(&args.relayer)?; + sign_and_dispatch( + wallet, + relayer_wallet, + tx, + nonce, + &args.tx, + &args.gateway, + None, + ) + .await } diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 6c2d8c4b1f..d4e0f6e34e 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -2,6 +2,7 @@ use std::fs; use anyhow::{Context, Result, anyhow}; use multiversx_chain_core::std::base64_decode; +use multiversx_sc::imports::Bech32Address; use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::{ hex, @@ -21,7 +22,7 @@ use multiversx_sc_snippets::network_response; use serde_json::Value; use super::output::TxOutputFile; -pub use crate::cli::cli_args_sender::load_wallet; +pub use crate::cli::cli_args_sender::{load_relayer_wallet, load_wallet}; use crate::cli::cli_args_tx::{GatewayArgs, TxArgs}; /// Load a transaction from an mxpy-compatible interaction JSON file. @@ -80,6 +81,13 @@ pub(super) async fn broadcast_and_save( "transaction is not signed; sign it before broadcasting" )); } + if output.emitted_transaction.relayer.is_some() + && output.emitted_transaction.relayer_signature.is_none() + { + return Err(anyhow!( + "relayed transaction is missing relayer signature; use `tx relay` to add it" + )); + } let proxy = GatewayHttpProxy::new(proxy_url.to_string()); let tx_hash = proxy @@ -141,8 +149,10 @@ pub fn build_arg_buffer(arguments: &[String]) -> Result, mut tx: Transaction, nonce: u64, tx_args: &TxArgs, @@ -157,6 +167,17 @@ pub async fn sign_and_dispatch( tx.chain_id = chain_id.clone(); } + // Set relayer address before signing — it is included in the signing bytes. + // Priority: explicit --relayer flag > address derived from --relayer-pem. + if let Some(relayer_str) = &tx_args.relayer { + let relayer_addr = Bech32Address::try_from_bech32_string(relayer_str.clone()) + .map_err(|e| anyhow!("invalid --relayer address: {e}"))?; + tx.relayer = Some(relayer_addr); + } else if let Some(ref rw) = relayer_wallet { + // Derive the relayer address automatically from the wallet. + tx.relayer = Some(rw.to_address().to_bech32_default()); + } + let decoded_data = match &tx.data { None => String::new(), Some(d) => { @@ -168,6 +189,23 @@ pub async fn sign_and_dispatch( let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); + // Optionally sign as relayer. + if let Some(relayer_w) = relayer_wallet { + let relayer_addr = relayer_w.to_address().to_bech32_default(); + // Validate only when --relayer was set explicitly (mismatch guard). + if let Some(tx_relayer) = &tx.relayer { + if relayer_addr != *tx_relayer { + return Err(anyhow!( + "relayer wallet address {} does not match --relayer {}", + relayer_addr.to_bech32_str(), + tx_relayer.to_bech32_str(), + )); + } + } + let relayer_sig = relayer_w.sign_tx(&tx)?; + tx.relayer_signature = Some(relayer_sig); + } + let output = TxOutputFile { emitted_transaction: tx, emitted_transaction_data: decoded_data, diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index d36c8a27f1..932cd3328b 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -6,7 +6,7 @@ use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::imports::{BytesValue, Interactor, InteractorIntoSdkTransaction}; use super::parse_code_metadata::parse_code_metadata; -use super::tx_cli_common::{build_arg_buffer, load_wallet, sign_and_dispatch}; +use super::tx_cli_common::{build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch}; use crate::cli::cli_args_tx::DeployArgs; pub async fn tx_deploy(args: &DeployArgs) { @@ -64,8 +64,10 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { println!("new contract address: {contract_address}"); } + let relayer_wallet = load_relayer_wallet(&args.relayer)?; sign_and_dispatch( wallet, + relayer_wallet, tx, nonce, &args.tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 35147ad623..28e4f64713 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -5,7 +5,7 @@ use multiversx_chain_core::std::{base64_decode, base64_encode}; use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; -use crate::cmd::tx::tx_cli_common::load_wallet; +use crate::cmd::tx::tx_cli_common::{load_relayer_wallet, load_wallet}; use super::{ output::TxOutputFile, @@ -68,9 +68,36 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { tx.chain_id = chain_id.clone(); } + // Set relayer address before signing — it is included in the signing bytes. + // Priority: explicit --relayer flag > address derived from --relayer-pem. + let relayer_wallet = load_relayer_wallet(&args.relayer)?; + if let Some(relayer_str) = &args.tx.relayer { + let relayer_addr = Bech32Address::try_from_bech32_string(relayer_str.clone()) + .map_err(|e| anyhow::anyhow!("invalid --relayer address: {e}"))?; + tx.relayer = Some(relayer_addr); + } else if let Some(ref rw) = relayer_wallet { + tx.relayer = Some(rw.to_address().to_bech32_default()); + } + let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); + // Optionally sign as relayer. + if let Some(relayer_w) = relayer_wallet { + let relayer_addr = relayer_w.to_address().to_bech32_default(); + if let Some(tx_relayer) = &tx.relayer { + if relayer_addr != *tx_relayer { + return Err(anyhow::anyhow!( + "relayer wallet address {} does not match --relayer {}", + relayer_addr.to_bech32_str(), + tx_relayer.to_bech32_str(), + )); + } + } + let relayer_sig = relayer_w.sign_tx(&tx)?; + tx.relayer_signature = Some(relayer_sig); + } + let output = TxOutputFile { emitted_transaction: tx, emitted_transaction_data: decoded_data, diff --git a/framework/meta/src/cmd/tx/tx_cli_relay.rs b/framework/meta/src/cmd/tx/tx_cli_relay.rs new file mode 100644 index 0000000000..0c6342faf5 --- /dev/null +++ b/framework/meta/src/cmd/tx/tx_cli_relay.rs @@ -0,0 +1,84 @@ +use anyhow::{Result, anyhow}; +use multiversx_chain_core::std::base64_decode; + +use super::{ + output::TxOutputFile, + tx_cli_common::{ + broadcast_and_save, load_relayer_wallet, load_transaction_from_file, save_output, + }, +}; +use crate::cli::cli_args_tx::RelayArgs; + +pub async fn tx_relay(args: &RelayArgs) { + if let Err(e) = tx_relay_inner(args).await { + eprintln!("Error: {e:#}"); + std::process::exit(1); + } +} + +async fn tx_relay_inner(args: &RelayArgs) -> Result<()> { + let relayer_wallet = load_relayer_wallet(&args.relayer)?.ok_or_else(|| { + anyhow!("a relayer wallet is required: use --relayer-pem or --relayer-keyfile") + })?; + + let mut tx = load_transaction_from_file(&args.infile)?; + + // Validate that the transaction has a relayer field set. + let tx_relayer = tx + .relayer + .clone() + .ok_or_else(|| anyhow!("transaction does not have a relayer field; set it when building the transaction with --relayer "))?; + + // Validate that the sender has already signed. + if tx.signature.is_none() { + return Err(anyhow!( + "transaction is not signed by the sender; sign it first with `tx sign`" + )); + } + + // Validate that the relayer wallet matches the transaction's relayer field. + let relayer_addr = relayer_wallet.to_address().to_bech32_default(); + if relayer_addr != tx_relayer { + return Err(anyhow!( + "relayer wallet address {} does not match transaction relayer {}", + relayer_addr.to_bech32_str(), + tx_relayer.to_bech32_str(), + )); + } + + if let Some(chain_id) = &args.gateway.chain { + tx.chain_id = chain_id.clone(); + } + + let relayer_sig = relayer_wallet.sign_tx(&tx)?; + tx.relayer_signature = Some(relayer_sig); + + let decoded_data = match &tx.data { + None => String::new(), + Some(d) => { + let bytes = base64_decode(d)?; + String::from_utf8_lossy(&bytes).into_owned() + } + }; + + let output = TxOutputFile { + emitted_transaction: tx, + emitted_transaction_data: decoded_data, + emitted_transaction_hash: String::new(), + contract_address: None, + transaction_on_network: None, + }; + + if args.send { + broadcast_and_save( + output, + &args.gateway.proxy, + args.outfile.as_deref(), + args.wait_result, + ) + .await?; + } else { + save_output(&output, args.outfile.as_deref())?; + } + Ok(()) +} diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 26e8cf2f51..5695ee05c1 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -3,7 +3,10 @@ use multiversx_chain_core::std::base64_decode; use super::{ output::TxOutputFile, - tx_cli_common::{broadcast_and_save, load_transaction_from_file, load_wallet, save_output}, + tx_cli_common::{ + broadcast_and_save, load_relayer_wallet, load_transaction_from_file, load_wallet, + save_output, + }, }; use crate::cli::cli_args_tx::SignArgs; @@ -35,6 +38,25 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); + // Optionally sign as relayer. + if let Some(relayer_w) = load_relayer_wallet(&args.relayer)? { + let relayer_addr = relayer_w.to_address().to_bech32_default(); + if let Some(tx_relayer) = &tx.relayer { + if relayer_addr != *tx_relayer { + return Err(anyhow!( + "relayer wallet address {} does not match transaction relayer {}", + relayer_addr.to_bech32_str(), + tx_relayer.to_bech32_str(), + )); + } + } else { + // No relayer field in the tx yet — derive it from the wallet. + tx.relayer = Some(relayer_addr); + } + let relayer_sig = relayer_w.sign_tx(&tx)?; + tx.relayer_signature = Some(relayer_sig); + } + let decoded_data = match &tx.data { None => String::new(), Some(d) => { diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 71bceb7430..510cb70407 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -7,7 +7,7 @@ use multiversx_sc_snippets::imports::{ }; use super::parse_code_metadata::parse_code_metadata; -use super::tx_cli_common::{build_arg_buffer, load_wallet, sign_and_dispatch}; +use super::tx_cli_common::{build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch}; use crate::cli::cli_args_tx::UpgradeArgs; pub async fn tx_upgrade(args: &UpgradeArgs) { @@ -56,5 +56,15 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { let tx = tx_builder.into_sdk_transaction(); - sign_and_dispatch(wallet, tx, nonce, &args.tx, &args.gateway, None).await + let relayer_wallet = load_relayer_wallet(&args.relayer)?; + sign_and_dispatch( + wallet, + relayer_wallet, + tx, + nonce, + &args.tx, + &args.gateway, + None, + ) + .await } diff --git a/framework/scenario/src/facade/world_tx/scenario_exec_call.rs b/framework/scenario/src/facade/world_tx/scenario_exec_call.rs index c0dd8757a9..a0dd10ae20 100644 --- a/framework/scenario/src/facade/world_tx/scenario_exec_call.rs +++ b/framework/scenario/src/facade/world_tx/scenario_exec_call.rs @@ -2,8 +2,8 @@ use multiversx_sc::{ tuple_util::NestedTupleFlatten, types::{ Code, FunctionCall, ManagedAddress, ManagedBuffer, NotPayable, RHListExec, Tx, - TxBaseWithEnv, TxEnv, TxEnvMockDeployAddress, TxEnvWithTxHash, TxFromSpecified, TxGas, - TxId, TxPayment, TxToSpecified, UpgradeCall, heap::H256, + TxBaseWithEnv, TxEnv, TxEnvMockDeployAddress, TxEnvWithRelayer, TxEnvWithTxHash, + TxFromSpecified, TxGas, TxId, TxPayment, TxToSpecified, UpgradeCall, heap::H256, }, }; @@ -115,6 +115,16 @@ where } } +impl TxEnvWithRelayer for ScenarioEnvExec<'_> { + fn set_relayer_address(&mut self, relayer: ManagedAddress) { + self.data.set_relayer_address(relayer); + } + + fn take_relayer_address(&mut self) -> Option> { + self.data.take_relayer_address() + } +} + impl TxEnvWithTxHash for ScenarioEnvExec<'_> { fn set_tx_id(&mut self, tx_id: TxId) { self.data.set_tx_id(tx_id); diff --git a/framework/scenario/src/facade/world_tx/scenario_tx_env.rs b/framework/scenario/src/facade/world_tx/scenario_tx_env.rs index 9d1b867dfb..f844a35adc 100644 --- a/framework/scenario/src/facade/world_tx/scenario_tx_env.rs +++ b/framework/scenario/src/facade/world_tx/scenario_tx_env.rs @@ -1,5 +1,7 @@ use multiversx_chain_scenario_format::interpret_trait::InterpreterContext; -use multiversx_sc::types::{H256, ManagedAddress, ManagedBuffer, TxEnv, TxEnvWithTxHash, TxId}; +use multiversx_sc::types::{ + H256, ManagedAddress, ManagedBuffer, TxEnv, TxEnvWithRelayer, TxEnvWithTxHash, TxId, +}; use crate::{ScenarioWorld, api::StaticApi, scenario_model::TxExpect}; @@ -14,6 +16,7 @@ pub struct ScenarioTxEnvData { pub interpreter_context: InterpreterContext, pub tx_id: Option, pub tx_hash: Option, + pub relayer: Option>, } impl TxEnv for ScenarioTxEnvData { @@ -54,6 +57,17 @@ impl TxEnvWithTxHash for ScenarioTxEnvData { } } +impl TxEnvWithRelayer for ScenarioTxEnvData { + fn set_relayer_address(&mut self, relayer: ManagedAddress) { + assert!(self.relayer.is_none(), "relayer set twice"); + self.relayer = Some(relayer); + } + + fn take_relayer_address(&mut self) -> Option> { + core::mem::take(&mut self.relayer) + } +} + impl ScenarioTxEnvData { pub fn interpreter_context(&self) -> InterpreterContext { self.interpreter_context.clone() @@ -74,6 +88,7 @@ impl ScenarioWorld { .with_allowed_missing_files(), tx_id: None, tx_hash: None, + relayer: None, } } } diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 7de0bcf887..d3353ef13b 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -85,6 +85,12 @@ where /// address. Populated by [`Self::register_wallet`]. pub sender_map: HashMap, + /// Optional relayer address. When set, every transaction signed by this + /// interactor is also signed by the relayer wallet (relayed v3). + /// The relayer wallet must already be registered in `sender_map` via + /// [`Self::register_wallet`] before calling [`Self::set_relayer`]. + pub relayer_address: Option
, + /// Scenario runners executed **before** each step (e.g. pre-condition checks /// or state loading). pub pre_runners: ScenarioRunnerList, @@ -113,6 +119,7 @@ where connection: None, use_chain_simulator: false, sender_map: HashMap::new(), + relayer_address: None, waiting_time_ms: 0, pre_runners: ScenarioRunnerList::empty(), post_runners: ScenarioRunnerList::empty(), diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index df7a4631e6..f57c0bb53f 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -3,7 +3,6 @@ use std::process; use super::error_message::{sc_call_err_message, simulate_gas_sc_call_err_message}; use crate::{InteractorBase, SimulateGas, network_response}; use multiversx_sc_scenario::{ - imports::Bech32Address, scenario::ScenarioRunner, scenario_model::{ScCallStep, SetStateStep, TxCall}, }; @@ -66,9 +65,11 @@ where async fn tx_call_to_blockchain_signed_tx(&mut self, sc_call_step: &ScCallStep) -> Transaction { let sender_address = &sc_call_step.tx.from.value; let mut transaction = self.tx_call_to_blockchain_tx(&sc_call_step.tx); + // Set relayer field before signing — it is included in the signing bytes. self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; self.sign_tx(sender_address, &mut transaction); + self.sign_tx_as_relayer(&mut transaction); transaction } @@ -126,8 +127,8 @@ where Transaction { nonce: 0, value: normalized.egld_value.value.to_string(), - sender: Bech32Address::encode_address(hrp, normalized.from.to_address()), - receiver: Bech32Address::encode_address(hrp, normalized.to.to_address()), + sender: normalized.from.to_address().to_bech32(hrp), + receiver: normalized.to.to_address().to_bech32(hrp), gas_price: self.gas_price, gas_limit: normalized.gas_limit.value, data, @@ -135,6 +136,8 @@ where chain_id: self.network_config().chain_id.clone(), version: TransactionVersion::V2, options: Some(TransactionOptions::SIGN_WITH_HASH), + relayer: self.relayer_address.as_ref().map(|a| a.to_bech32(hrp)), + relayer_signature: None, } } } diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index 3901182081..716a658253 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -34,6 +34,8 @@ where chain_id: self.network_config().chain_id.clone(), version: TransactionVersion::V2, options: Some(TransactionOptions::SIGN_WITH_HASH), + relayer: self.relayer_address.as_ref().map(|a| a.to_bech32(hrp)), + relayer_signature: None, } } @@ -43,9 +45,11 @@ where ) -> Transaction { let sender_address = &sc_deploy_step.tx.from.value; let mut transaction = self.sc_deploy_to_blockchain_tx(sc_deploy_step); + // Set relayer field before signing — it is included in the signing bytes. self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; self.sign_tx(sender_address, &mut transaction); + self.sign_tx_as_relayer(&mut transaction); transaction } diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 4b8e476ac8..b8343e62e5 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -115,4 +115,38 @@ where .expect("failed to sign transaction"); transaction.signature = Some(signature); } + + /// Designates a registered wallet as the relayer for all transactions from this interactor. + /// + /// The wallet must already be registered via [`Self::register_wallet`]. + /// Call with `None` to clear the relayer. + pub fn set_relayer(&mut self, address: impl Into>) { + let address = address.into(); + if let Some(ref addr) = address { + assert!( + self.sender_map.contains_key(addr), + "relayer wallet not registered; call register_wallet first" + ); + } + self.relayer_address = address; + } + + /// Signs the transaction as the relayer (adds `relayer_signature`). + /// + /// No-op if no relayer is configured. Must be called **after** the sender has signed, + /// and after [`Self::apply_relayer_address`] has set the `relayer` field. + pub(crate) fn sign_tx_as_relayer(&self, transaction: &mut Transaction) { + let Some(relayer_address) = &self.relayer_address else { + return; + }; + let relayer = self + .sender_map + .get(relayer_address) + .expect("relayer wallet not registered"); + let sig = relayer + .wallet + .sign_tx(transaction) + .expect("failed to sign as relayer"); + transaction.relayer_signature = Some(sig); + } } diff --git a/framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs b/framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs index c1b742cda8..3799818dc2 100644 --- a/framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs +++ b/framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs @@ -3,7 +3,8 @@ use multiversx_sc_scenario::{ api::StaticApi, imports::TxId, multiversx_sc::types::{ - H256, ManagedAddress, ManagedBuffer, Tx, TxBaseWithEnv, TxEnv, TxEnvWithTxHash, + H256, ManagedAddress, ManagedBuffer, Tx, TxBaseWithEnv, TxEnv, TxEnvWithRelayer, + TxEnvWithTxHash, }, scenario_model::TxExpect, }; @@ -61,6 +62,19 @@ where } } +impl TxEnvWithRelayer for InteractorEnvExec<'_, GatewayProxy> +where + GatewayProxy: GatewayAsyncService, +{ + fn set_relayer_address(&mut self, relayer: ManagedAddress) { + self.data.set_relayer_address(relayer); + } + + fn take_relayer_address(&mut self) -> Option> { + self.data.take_relayer_address() + } +} + impl TxEnvWithTxHash for InteractorEnvExec<'_, GatewayProxy> where GatewayProxy: GatewayAsyncService, diff --git a/framework/snippets/src/interactor/interactor_tx/interactor_run_trait.rs b/framework/snippets/src/interactor/interactor_tx/interactor_run_trait.rs index 3e1286305b..03bee0efa0 100644 --- a/framework/snippets/src/interactor/interactor_tx/interactor_run_trait.rs +++ b/framework/snippets/src/interactor/interactor_tx/interactor_run_trait.rs @@ -15,6 +15,7 @@ where interpreter_context: InterpreterContext::new().with_dir(self.current_dir.clone()), tx_id: None, tx_hash: None, + relayer: None, } } } diff --git a/sdk/core/src/data/transaction/transaction_request.rs b/sdk/core/src/data/transaction/transaction_request.rs index 7471a34e9f..2a914f16c5 100644 --- a/sdk/core/src/data/transaction/transaction_request.rs +++ b/sdk/core/src/data/transaction/transaction_request.rs @@ -26,6 +26,10 @@ pub struct Transaction { pub version: TransactionVersion, #[serde(skip_serializing_if = "Option::is_none")] pub options: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub relayer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub relayer_signature: Option, } impl Transaction { diff --git a/sdk/core/src/wallet/wallet_impl.rs b/sdk/core/src/wallet/wallet_impl.rs index 8e2a552962..293912c8c5 100644 --- a/sdk/core/src/wallet/wallet_impl.rs +++ b/sdk/core/src/wallet/wallet_impl.rs @@ -149,6 +149,7 @@ impl Wallet { pub fn sign_tx(&self, unsign_tx: &Transaction) -> Result { let mut unsign_tx = unsign_tx.clone(); unsign_tx.signature = None; + unsign_tx.relayer_signature = None; match &self.signer { Signer::PrivateKey(pk) => { diff --git a/sdk/http/examples/sign_tx.rs b/sdk/http/examples/sign_tx.rs index 83c68c1e93..9554c1a6fe 100644 --- a/sdk/http/examples/sign_tx.rs +++ b/sdk/http/examples/sign_tx.rs @@ -30,6 +30,8 @@ async fn main() -> anyhow::Result<()> { chain_id: arg.chain_id, version: arg.version, options: arg.options, + relayer: None, + relayer_signature: None, }; let signature = wallet.sign_tx(&unsign_tx)?; diff --git a/sdk/http/examples/sign_txs.rs b/sdk/http/examples/sign_txs.rs index 46c2b9915d..a4348b2910 100644 --- a/sdk/http/examples/sign_txs.rs +++ b/sdk/http/examples/sign_txs.rs @@ -29,6 +29,8 @@ async fn main() -> anyhow::Result<()> { chain_id: arg.chain_id, version: TransactionVersion::V1, options: None, + relayer: None, + relayer_signature: None, }; let mut txs: Vec = vec![]; diff --git a/sdk/http/examples/tx_cost.rs b/sdk/http/examples/tx_cost.rs index 95c0b87d94..98ea3e7383 100644 --- a/sdk/http/examples/tx_cost.rs +++ b/sdk/http/examples/tx_cost.rs @@ -22,6 +22,8 @@ async fn main() { gas_limit: 0, gas_price: 0, signature: None, + relayer: None, + relayer_signature: None, }; let blockchain = GatewayHttpProxy::new(DEVNET_GATEWAY.to_string()); From 280a326a08959afdfc19c8fa213a74e6e2515647 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 6 Aug 2026 01:07:13 +0300 Subject: [PATCH 163/195] interactor refactor (sign_tx) --- .../src/interactor/interactor_scenario/interactor_sc_call.rs | 5 ++--- .../interactor/interactor_scenario/interactor_sc_deploy.rs | 5 ++--- .../interactor/interactor_scenario/interactor_transfer.rs | 2 +- framework/snippets/src/interactor/interactor_sender.rs | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index f57c0bb53f..d6f58dca65 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -28,8 +28,7 @@ where transaction.gas_limit = gas; // sign again, because gas changed - let sender_address = &sc_call_step.tx.from.value; - self.sign_tx(sender_address, &mut transaction); + self.sign_tx(&mut transaction); } self.pre_runners.run_sc_call_step(sc_call_step); @@ -68,7 +67,7 @@ where // Set relayer field before signing — it is included in the signing bytes. self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; - self.sign_tx(sender_address, &mut transaction); + self.sign_tx(&mut transaction); self.sign_tx_as_relayer(&mut transaction); transaction diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index 716a658253..bc9eb192d9 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -48,7 +48,7 @@ where // Set relayer field before signing — it is included in the signing bytes. self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; - self.sign_tx(sender_address, &mut transaction); + self.sign_tx(&mut transaction); self.sign_tx_as_relayer(&mut transaction); transaction @@ -86,8 +86,7 @@ where transaction.gas_limit = gas; // sign again, because gas changed - let sender_address = &sc_deploy_step.tx.from.value; - self.sign_tx(sender_address, &mut transaction); + self.sign_tx(&mut transaction); } self.pre_runners.run_sc_deploy_step(sc_deploy_step); diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs index 6b94b1c359..0a209b7c1e 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs @@ -67,7 +67,7 @@ where let mut transaction = self.tx_call_to_blockchain_tx(&transfer_step.tx.to_tx_call()); self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; - self.sign_tx(sender_address, &mut transaction); + self.sign_tx(&mut transaction); transaction } diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index b8343e62e5..139f4afdc2 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -101,11 +101,11 @@ where sender.current_nonce = Some(nonce + 1); } - pub(crate) fn sign_tx(&self, sender_address: &Address, transaction: &mut Transaction) { + pub(crate) fn sign_tx(&self, transaction: &mut Transaction) { // read let sender = self .sender_map - .get(sender_address) + .get(transaction.sender.as_address()) .expect("the wallet that was supposed to sign is not registered"); // sign From 08c9be4bd082d30a0f52d454c7f896ae49d928e0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 6 Aug 2026 01:09:43 +0300 Subject: [PATCH 164/195] test fix --- .../adder/interactor/tests/adder_interactor_ledger_test.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs b/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs index 13483cd761..c5aec4d4e9 100644 --- a/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs +++ b/contracts/examples/adder/interactor/tests/adder_interactor_ledger_test.rs @@ -18,6 +18,7 @@ fn chain_simulator_ledger_config() -> Config { owner: WalletConfig::from_test_wallet("mike"), wallet: WalletConfig::from_test_wallet("ivan"), ledger_wallet: Some(WalletConfig::from_test_wallet("carol")), + relayer: None, } } From c3be894a500b9004d8fa602143ab789be4d67359 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 6 Aug 2026 01:53:25 +0300 Subject: [PATCH 165/195] relayer sc-meta CLI test --- framework/meta/.gitignore | 2 +- framework/meta/tests/cs_tx_cli_test.rs | 101 ++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/framework/meta/.gitignore b/framework/meta/.gitignore index 70153de369..70b43db1f8 100644 --- a/framework/meta/.gitignore +++ b/framework/meta/.gitignore @@ -1,3 +1,3 @@ tests/cs_tx_cli_test/outfiles -tests/cs_tx_cli_test/alice.pem +tests/cs_tx_cli_test/*.pem tests/temp/ diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 17f07ef97f..5165ae8ea4 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -266,6 +266,76 @@ async fn test_adder_deploy_add_get_sum() { vec!["05"], "getSum returned unexpected value after upgrade" ); + + // ── add(3) via relayed transaction (one-step) ───────────────────────────── + let relayer_pem_path = test_artefacts_dir.join("s1mon.pem"); + let outfile_call_relayed = outfiles_dir.join("adder-call-relayed-cs.interaction.json"); + + generate_test_wallet(sc_meta_bin, "s1mon", &relayer_pem_path); + + let relayer_wallet = Wallet::from_pem_file(&relayer_pem_path).unwrap(); + let relayer_address = relayer_wallet.to_address().to_bech32_default(); + interactor.send_user_funds(&relayer_address).await.unwrap(); + + interactor.generate_blocks(10).await.unwrap(); + + let status = Command::new(sc_meta_bin) + .args([ + "tx", + "call", + contract_address, + "--pem", + wallet_pem_path.to_str().unwrap(), + "--relayer-pem", + relayer_pem_path.to_str().unwrap(), + "--proxy", + CHAIN_SIMULATOR_URL, + "--chain", + CHAIN_SIMULATOR_CHAIN_ID, + "--gas-limit", + "5050000", + "--function", + "add", + "--arguments", + "3", + "--send", + "--outfile", + outfile_call_relayed.to_str().unwrap(), + ]) + .status() + .expect("failed to execute sc-meta tx call (relayed)"); + + assert!(status.success(), "relayed add call failed"); + + interactor.generate_blocks(10).await.unwrap(); + + // getSum must return 5 + 3 = 8 after the relayed add. + let query_after_relayed = Command::new(sc_meta_bin) + .args([ + "tx", + "query", + contract_address, + "--proxy", + CHAIN_SIMULATOR_URL, + "--function", + "getSum", + ]) + .output() + .expect("failed to execute sc-meta tx query after relayed add"); + + assert!( + query_after_relayed.status.success(), + "getSum query after relayed add failed" + ); + + let stdout_relayed = String::from_utf8_lossy(&query_after_relayed.stdout); + let result_relayed: Vec = + serde_json::from_str(stdout_relayed.trim()).expect("failed to parse query output as JSON"); + assert_eq!( + result_relayed, + vec!["08"], + "getSum returned unexpected value after relayed add" + ); } /// Sends a small amount of EGLD from Alice to Bob via the `sc-meta tx new` CLI command @@ -281,18 +351,7 @@ async fn test_egld_transfer_alice_to_bob() { let sc_meta_bin = env!("CARGO_BIN_EXE_sc-meta"); // Generate alice.pem via the CLI so the file is not stored in the repo. - let status = Command::new(sc_meta_bin) - .args([ - "wallet", - "test-wallet", - "--name", - "alice", - "--path", - alice_pem_path.to_str().unwrap(), - ]) - .status() - .expect("failed to generate alice.pem"); - assert!(status.success(), "sc-meta wallet test-wallet failed"); + generate_test_wallet(sc_meta_bin, "alice", &alice_pem_path); // Connect to the chain simulator. let interactor = Interactor::new(CHAIN_SIMULATOR_URL) @@ -390,3 +449,21 @@ async fn test_egld_transfer_alice_to_bob() { "Alice's balance did not decrease by at least the transfer amount" ); } + +fn generate_test_wallet(sc_meta_bin: &str, name: &str, path: &std::path::Path) { + let status = Command::new(sc_meta_bin) + .args([ + "wallet", + "test-wallet", + "--name", + name, + "--path", + path.to_str().unwrap(), + ]) + .status() + .unwrap_or_else(|_| panic!("failed to generate {name}.pem")); + assert!( + status.success(), + "sc-meta wallet test-wallet failed for {name}" + ); +} From e4ded20572ccc8166cb74be6fa6a91ef4df57c17 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 6 Aug 2026 02:36:44 +0300 Subject: [PATCH 166/195] relayer tx config fix and cleanup --- .../src/scenario/model/transaction/tx_call.rs | 4 +++ .../scenario/model/transaction/tx_deploy.rs | 3 ++ .../scenario/model/transaction/tx_transfer.rs | 3 ++ .../scenario/tx_to_step/tx_to_step_call.rs | 10 +++++-- .../scenario/tx_to_step/tx_to_step_deploy.rs | 10 +++++-- .../tx_to_step/tx_to_step_transfer.rs | 14 ++++++--- .../scenario/tx_to_step/tx_to_step_upgrade.rs | 12 +++++--- .../src/interactor/interactor_base.rs | 7 ----- .../interactor_scenario/interactor_sc_call.rs | 6 ++-- .../interactor_sc_deploy.rs | 7 +++-- .../src/interactor/interactor_sender.rs | 29 +++++-------------- 11 files changed, 59 insertions(+), 46 deletions(-) diff --git a/framework/scenario/src/scenario/model/transaction/tx_call.rs b/framework/scenario/src/scenario/model/transaction/tx_call.rs index 963e7f92bd..79aa2213ec 100644 --- a/framework/scenario/src/scenario/model/transaction/tx_call.rs +++ b/framework/scenario/src/scenario/model/transaction/tx_call.rs @@ -25,6 +25,7 @@ pub struct TxCall { pub arguments: Vec, pub gas_limit: U64Value, pub gas_price: U64Value, + pub relayer: Option, } impl Default for TxCall { @@ -38,6 +39,7 @@ impl Default for TxCall { arguments: Default::default(), gas_limit: U64Value::from(DEFAULT_GAS_EXPR), gas_price: Default::default(), + relayer: None, } } } @@ -61,6 +63,7 @@ impl InterpretableFrom for TxCall { .collect(), gas_limit: U64Value::interpret_from(from.gas_limit, context), gas_price: U64Value::interpret_from(from.gas_price.unwrap_or_default(), context), + relayer: None, } } } @@ -154,6 +157,7 @@ impl TxCall { arguments, gas_limit: self.gas_limit.clone(), gas_price: self.gas_price.clone(), + relayer: self.relayer.clone(), } } diff --git a/framework/scenario/src/scenario/model/transaction/tx_deploy.rs b/framework/scenario/src/scenario/model/transaction/tx_deploy.rs index 52617151ed..e34a9df1ee 100644 --- a/framework/scenario/src/scenario/model/transaction/tx_deploy.rs +++ b/framework/scenario/src/scenario/model/transaction/tx_deploy.rs @@ -18,6 +18,7 @@ pub struct TxDeploy { pub arguments: Vec, pub gas_limit: U64Value, pub gas_price: U64Value, + pub relayer: Option, } impl Default for TxDeploy { @@ -30,6 +31,7 @@ impl Default for TxDeploy { arguments: Default::default(), gas_limit: U64Value::from(DEFAULT_GAS_EXPR), gas_price: Default::default(), + relayer: None, } } } @@ -48,6 +50,7 @@ impl InterpretableFrom for TxDeploy { .collect(), gas_limit: U64Value::interpret_from(from.gas_limit, context), gas_price: U64Value::interpret_from(from.gas_price.unwrap_or_default(), context), + relayer: None, } } } diff --git a/framework/scenario/src/scenario/model/transaction/tx_transfer.rs b/framework/scenario/src/scenario/model/transaction/tx_transfer.rs index 468ef876d7..01801ff99b 100644 --- a/framework/scenario/src/scenario/model/transaction/tx_transfer.rs +++ b/framework/scenario/src/scenario/model/transaction/tx_transfer.rs @@ -16,6 +16,7 @@ pub struct TxTransfer { pub esdt_value: Vec, pub gas_limit: U64Value, pub gas_price: U64Value, + pub relayer: Option, } impl InterpretableFrom for TxTransfer { @@ -31,6 +32,7 @@ impl InterpretableFrom for TxTransfer { .collect(), gas_limit: U64Value::interpret_from(from.gas_limit.unwrap_or_default(), context), gas_price: U64Value::interpret_from(from.gas_price.unwrap_or_default(), context), + relayer: None, } } } @@ -65,6 +67,7 @@ impl TxTransfer { arguments: Vec::new(), gas_limit: self.gas_limit.clone(), gas_price: self.gas_price.clone(), + relayer: self.relayer.clone(), } } } diff --git a/framework/scenario/src/scenario/tx_to_step/tx_to_step_call.rs b/framework/scenario/src/scenario/tx_to_step/tx_to_step_call.rs index 2f7cf36199..a6360abbd0 100644 --- a/framework/scenario/src/scenario/tx_to_step/tx_to_step_call.rs +++ b/framework/scenario/src/scenario/tx_to_step/tx_to_step_call.rs @@ -1,6 +1,6 @@ use multiversx_sc::types::{ - FunctionCall, RHListExec, Tx, TxEnv, TxEnvWithTxHash, TxFromSpecified, TxGas, TxPayment, - TxToSpecified, + FunctionCall, RHListExec, Tx, TxEnv, TxEnvWithRelayer, TxEnvWithTxHash, TxFromSpecified, TxGas, + TxPayment, TxToSpecified, }; use crate::scenario_model::{ScCallStep, TxESDT, TxExpect, TxResponse}; @@ -10,7 +10,7 @@ use super::{StepWrapper, TxToStep, address_annotated, gas_annotated}; impl TxToStep for Tx, RH> where - Env: TxEnvWithTxHash, + Env: TxEnvWithTxHash + TxEnvWithRelayer, From: TxFromSpecified, To: TxToSpecified, Payment: TxPayment, @@ -30,6 +30,10 @@ where ); step.tx_id = self.env.take_tx_id(); step.explicit_tx_hash = self.env.take_tx_hash(); + step.tx.relayer = self + .env + .take_relayer_address() + .map(|a| address_annotated(&self.env, &a)); step.expect = Some(self.result_handler.list_preprocessing()); StepWrapper { diff --git a/framework/scenario/src/scenario/tx_to_step/tx_to_step_deploy.rs b/framework/scenario/src/scenario/tx_to_step/tx_to_step_deploy.rs index d04856daad..d1856e30d2 100644 --- a/framework/scenario/src/scenario/tx_to_step/tx_to_step_deploy.rs +++ b/framework/scenario/src/scenario/tx_to_step/tx_to_step_deploy.rs @@ -1,6 +1,6 @@ use multiversx_sc::types::{ - Code, DeployCall, RHListExec, Tx, TxCodeValue, TxEnv, TxEnvWithTxHash, TxFromSpecified, TxGas, - TxPayment, + Code, DeployCall, RHListExec, Tx, TxCodeValue, TxEnv, TxEnvWithRelayer, TxEnvWithTxHash, + TxFromSpecified, TxGas, TxPayment, }; use crate::scenario_model::{ScDeployStep, TxExpect, TxResponse}; @@ -10,7 +10,7 @@ use super::{StepWrapper, TxToStep, address_annotated, code_annotated, gas_annota impl TxToStep for Tx>, RH> where - Env: TxEnvWithTxHash, + Env: TxEnvWithTxHash + TxEnvWithRelayer, From: TxFromSpecified, Payment: TxPayment, Gas: TxGas, @@ -24,6 +24,10 @@ where tx_to_sc_deploy_step(&self.env, self.from, self.payment, self.gas, self.data); step.tx_id = self.env.take_tx_id(); step.explicit_tx_hash = self.env.take_tx_hash(); + step.tx.relayer = self + .env + .take_relayer_address() + .map(|a| address_annotated(&self.env, &a)); step.expect = Some(self.result_handler.list_preprocessing()); StepWrapper { diff --git a/framework/scenario/src/scenario/tx_to_step/tx_to_step_transfer.rs b/framework/scenario/src/scenario/tx_to_step/tx_to_step_transfer.rs index 28e3aa7dc2..89a2a76ed2 100644 --- a/framework/scenario/src/scenario/tx_to_step/tx_to_step_transfer.rs +++ b/framework/scenario/src/scenario/tx_to_step/tx_to_step_transfer.rs @@ -1,4 +1,6 @@ -use multiversx_sc::types::{Tx, TxEnv, TxFromSpecified, TxGas, TxPayment, TxToSpecified}; +use multiversx_sc::types::{ + Tx, TxEnv, TxEnvWithRelayer, TxFromSpecified, TxGas, TxPayment, TxToSpecified, +}; use crate::{imports::TxESDT, scenario_model::TransferStep}; @@ -6,7 +8,7 @@ use super::{StepWrapper, TxToStep, address_annotated, gas_annotated}; impl TxToStep for Tx where - Env: TxEnv, + Env: TxEnv + TxEnvWithRelayer, From: TxFromSpecified, To: TxToSpecified, Payment: TxPayment, @@ -14,8 +16,12 @@ where { type Step = TransferStep; - fn tx_to_step(self) -> StepWrapper { - let step = tx_to_transfer_step(&self.env, self.from, self.to, self.payment, self.gas); + fn tx_to_step(mut self) -> StepWrapper { + let mut step = tx_to_transfer_step(&self.env, self.from, self.to, self.payment, self.gas); + step.tx.relayer = self + .env + .take_relayer_address() + .map(|a| address_annotated(&self.env, &a)); StepWrapper { env: self.env, diff --git a/framework/scenario/src/scenario/tx_to_step/tx_to_step_upgrade.rs b/framework/scenario/src/scenario/tx_to_step/tx_to_step_upgrade.rs index d646fbf7d6..4c96e77a45 100644 --- a/framework/scenario/src/scenario/tx_to_step/tx_to_step_upgrade.rs +++ b/framework/scenario/src/scenario/tx_to_step/tx_to_step_upgrade.rs @@ -1,6 +1,6 @@ use multiversx_sc::types::{ - Code, NotPayable, RHListExec, Tx, TxCodeValue, TxEnv, TxFromSpecified, TxGas, TxToSpecified, - UpgradeCall, + Code, NotPayable, RHListExec, Tx, TxCodeValue, TxEnv, TxEnvWithRelayer, TxFromSpecified, TxGas, + TxToSpecified, UpgradeCall, }; use crate::{ @@ -13,7 +13,7 @@ use super::{StepWrapper, TxToStep, address_annotated, code_annotated, gas_annota impl TxToStep for Tx>, RH> where - Env: TxEnv, + Env: TxEnv + TxEnvWithRelayer, From: TxFromSpecified, To: TxToSpecified, Gas: TxGas, @@ -22,9 +22,13 @@ where { type Step = ScCallStep; - fn tx_to_step(self) -> StepWrapper { + fn tx_to_step(mut self) -> StepWrapper { let mut step = tx_to_sc_call_upgrade_step(&self.env, self.from, self.to, self.gas, self.data); + step.tx.relayer = self + .env + .take_relayer_address() + .map(|a| address_annotated(&self.env, &a)); step.expect = Some(self.result_handler.list_preprocessing()); StepWrapper { diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index d3353ef13b..7de0bcf887 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -85,12 +85,6 @@ where /// address. Populated by [`Self::register_wallet`]. pub sender_map: HashMap, - /// Optional relayer address. When set, every transaction signed by this - /// interactor is also signed by the relayer wallet (relayed v3). - /// The relayer wallet must already be registered in `sender_map` via - /// [`Self::register_wallet`] before calling [`Self::set_relayer`]. - pub relayer_address: Option
, - /// Scenario runners executed **before** each step (e.g. pre-condition checks /// or state loading). pub pre_runners: ScenarioRunnerList, @@ -119,7 +113,6 @@ where connection: None, use_chain_simulator: false, sender_map: HashMap::new(), - relayer_address: None, waiting_time_ms: 0, pre_runners: ScenarioRunnerList::empty(), post_runners: ScenarioRunnerList::empty(), diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index d6f58dca65..896c30cf1c 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -68,7 +68,6 @@ where self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; self.sign_tx(&mut transaction); - self.sign_tx_as_relayer(&mut transaction); transaction } @@ -135,7 +134,10 @@ where chain_id: self.network_config().chain_id.clone(), version: TransactionVersion::V2, options: Some(TransactionOptions::SIGN_WITH_HASH), - relayer: self.relayer_address.as_ref().map(|a| a.to_bech32(hrp)), + relayer: normalized + .relayer + .as_ref() + .map(|a| a.to_address().to_bech32(hrp)), relayer_signature: None, } } diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index bc9eb192d9..98189e96d5 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -34,7 +34,11 @@ where chain_id: self.network_config().chain_id.clone(), version: TransactionVersion::V2, options: Some(TransactionOptions::SIGN_WITH_HASH), - relayer: self.relayer_address.as_ref().map(|a| a.to_bech32(hrp)), + relayer: sc_deploy_step + .tx + .relayer + .as_ref() + .map(|a| a.to_address().to_bech32(hrp)), relayer_signature: None, } } @@ -49,7 +53,6 @@ where self.set_tx_nonce_update_sender(sender_address, &mut transaction) .await; self.sign_tx(&mut transaction); - self.sign_tx_as_relayer(&mut transaction); transaction } diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 139f4afdc2..7ed15783c7 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -102,6 +102,11 @@ where } pub(crate) fn sign_tx(&self, transaction: &mut Transaction) { + self.sign_tx_for_sender(transaction); + self.sign_tx_for_relayer(transaction); + } + + fn sign_tx_for_sender(&self, transaction: &mut Transaction) { // read let sender = self .sender_map @@ -116,32 +121,14 @@ where transaction.signature = Some(signature); } - /// Designates a registered wallet as the relayer for all transactions from this interactor. - /// - /// The wallet must already be registered via [`Self::register_wallet`]. - /// Call with `None` to clear the relayer. - pub fn set_relayer(&mut self, address: impl Into>) { - let address = address.into(); - if let Some(ref addr) = address { - assert!( - self.sender_map.contains_key(addr), - "relayer wallet not registered; call register_wallet first" - ); - } - self.relayer_address = address; - } - /// Signs the transaction as the relayer (adds `relayer_signature`). - /// - /// No-op if no relayer is configured. Must be called **after** the sender has signed, - /// and after [`Self::apply_relayer_address`] has set the `relayer` field. - pub(crate) fn sign_tx_as_relayer(&self, transaction: &mut Transaction) { - let Some(relayer_address) = &self.relayer_address else { + fn sign_tx_for_relayer(&self, transaction: &mut Transaction) { + let Some(relayer_address) = &transaction.relayer else { return; }; let relayer = self .sender_map - .get(relayer_address) + .get(relayer_address.as_address()) .expect("relayer wallet not registered"); let sig = relayer .wallet From 4e3d04a85841274092d3ed64a2e63de760153ba0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 6 Aug 2026 02:38:54 +0300 Subject: [PATCH 167/195] clippy fix --- framework/meta/src/cli/cli_args_standalone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/meta/src/cli/cli_args_standalone.rs b/framework/meta/src/cli/cli_args_standalone.rs index dff7d1a1d7..2d3e59c27f 100644 --- a/framework/meta/src/cli/cli_args_standalone.rs +++ b/framework/meta/src/cli/cli_args_standalone.rs @@ -44,7 +44,7 @@ pub enum StandaloneCliAction { #[command( about = "Calls the meta crates for all contracts under given path with the given arguments." )] - All(AllArgs), + All(Box), #[command( about = "Upgrades a contract to the latest version. Multiple contract crates are allowed." @@ -112,7 +112,7 @@ pub enum StandaloneCliAction { name = "tx", about = "Deploy, call, upgrade, query contracts or create/send/sign transactions." )] - Tx(TxCliArgs), + Tx(Box), #[command( name = "ledger", From a9a8022696a97a8c338fbc5de054337f7837ab19 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 6 Aug 2026 20:21:38 +0300 Subject: [PATCH 168/195] sc-meta CLI relayer and signing refactor --- framework/base/src/types/interaction/tx.rs | 15 +++++++ framework/meta/src/cmd/tx/tx_cli_call.rs | 19 +++----- framework/meta/src/cmd/tx/tx_cli_common.rs | 40 +++-------------- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 20 +++++---- framework/meta/src/cmd/tx/tx_cli_new.rs | 44 +++++-------------- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 24 ++++------ .../src/interactor/interactor_base.rs | 25 +++++++++++ .../src/interactor/interactor_sender.rs | 12 ++++- 8 files changed, 93 insertions(+), 106 deletions(-) diff --git a/framework/base/src/types/interaction/tx.rs b/framework/base/src/types/interaction/tx.rs index 467ec01155..9e280b88ae 100644 --- a/framework/base/src/types/interaction/tx.rs +++ b/framework/base/src/types/interaction/tx.rs @@ -1016,4 +1016,19 @@ where self.env.set_relayer_address(relayer_addr); self } + + /// Optionally sets the relayer address for a relayed v3 transaction. + /// + /// The relayer pays the gas fees. After the sender signs the transaction, + /// the relayer must also sign it (adding `relayerSignature`) before broadcasting. + pub fn opt_relayer(self, relayer: Option) -> Self + where + A: AnnotatedValue>, + { + if let Some(relayer) = relayer { + self.relayer(relayer) + } else { + self + } + } } diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 043c5a793f..90e1d46fb2 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -13,12 +13,14 @@ pub async fn tx_call(args: &CallArgs) { } async fn tx_call_inner(args: &CallArgs) -> Result<()> { - let wallet = load_wallet(&args.sender)?; + let sender_wallet = load_wallet(&args.sender)?; + let relayer_wallet = load_relayer_wallet(&args.relayer)?; // Create the interactor – fetches network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet.clone()).await; + let sender_address = interactor.register_wallet(sender_wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); + let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; // Determine nonce. let nonce = if let Some(n) = args.tx.nonce { @@ -41,17 +43,8 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { .payment(payments) .raw_call(args.function.as_str()) .arguments_raw(arg_buffer) + .opt_relayer(relayer_address_opt) .into_sdk_transaction(); - let relayer_wallet = load_relayer_wallet(&args.relayer)?; - sign_and_dispatch( - wallet, - relayer_wallet, - tx, - nonce, - &args.tx, - &args.gateway, - None, - ) - .await + sign_and_dispatch(&interactor, tx, nonce, &args.tx, &args.gateway, None).await } diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index d4e0f6e34e..a9d08cce76 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -7,13 +7,10 @@ use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::{ hex, imports::{ - BytesValue, GatewayHttpProxy, InterpretableFrom, InterpreterContext, ManagedArgBuffer, - ManagedBuffer, StaticApi, - }, - sdk::{ - data::transaction::{ApiTransactionResult, Transaction}, - wallet::Wallet, + BytesValue, GatewayHttpProxy, Interactor, InterpretableFrom, InterpreterContext, + ManagedArgBuffer, ManagedBuffer, StaticApi, }, + sdk::data::transaction::{ApiTransactionResult, Transaction}, }; use serde::Serialize; @@ -149,10 +146,9 @@ pub fn build_arg_buffer(arguments: &[String]) -> Result, + interactor: &Interactor, mut tx: Transaction, nonce: u64, tx_args: &TxArgs, @@ -167,15 +163,11 @@ pub async fn sign_and_dispatch( tx.chain_id = chain_id.clone(); } - // Set relayer address before signing — it is included in the signing bytes. - // Priority: explicit --relayer flag > address derived from --relayer-pem. + // Explicit --relayer address override (takes priority over the address set in the tx builder). if let Some(relayer_str) = &tx_args.relayer { let relayer_addr = Bech32Address::try_from_bech32_string(relayer_str.clone()) .map_err(|e| anyhow!("invalid --relayer address: {e}"))?; tx.relayer = Some(relayer_addr); - } else if let Some(ref rw) = relayer_wallet { - // Derive the relayer address automatically from the wallet. - tx.relayer = Some(rw.to_address().to_bech32_default()); } let decoded_data = match &tx.data { @@ -186,25 +178,7 @@ pub async fn sign_and_dispatch( } }; - let sig = wallet.sign_tx(&tx)?; - tx.signature = Some(sig); - - // Optionally sign as relayer. - if let Some(relayer_w) = relayer_wallet { - let relayer_addr = relayer_w.to_address().to_bech32_default(); - // Validate only when --relayer was set explicitly (mismatch guard). - if let Some(tx_relayer) = &tx.relayer { - if relayer_addr != *tx_relayer { - return Err(anyhow!( - "relayer wallet address {} does not match --relayer {}", - relayer_addr.to_bech32_str(), - tx_relayer.to_bech32_str(), - )); - } - } - let relayer_sig = relayer_w.sign_tx(&tx)?; - tx.relayer_signature = Some(relayer_sig); - } + interactor.sign_tx(&mut tx); let output = TxOutputFile { emitted_transaction: tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index 932cd3328b..83d36f28f8 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -17,18 +17,21 @@ pub async fn tx_deploy(args: &DeployArgs) { } async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { - let wallet = load_wallet(&args.sender)?; + let sender_wallet = load_wallet(&args.sender)?; + let relayer_wallet = load_relayer_wallet(&args.relayer)?; // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet.clone()).await; - let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); + let sender_address = interactor + .register_wallet_bech32(sender_wallet.clone()) + .await; + let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; // Determine nonce. let nonce = if let Some(n) = args.tx.nonce { n } else { - interactor.recall_nonce(&sender_address).await + interactor.recall_nonce(&sender_address.address).await }; // Read bytecode file and wrap in BytesValue so it implements TxCodeValue. @@ -43,13 +46,14 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { let arg_buffer = build_arg_buffer(&args.arguments)?; let tx_builder = interactor .tx() - .from(&sender_bech32) + .from(&sender_address) .gas(args.tx.gas_limit) .egld(args.payment.value) .raw_deploy() .code(code) .code_metadata(code_metadata) - .arguments_raw(arg_buffer); + .arguments_raw(arg_buffer) + .opt_relayer(relayer_address_opt); let tx = tx_builder.into_sdk_transaction(); @@ -64,10 +68,8 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { println!("new contract address: {contract_address}"); } - let relayer_wallet = load_relayer_wallet(&args.relayer)?; sign_and_dispatch( - wallet, - relayer_wallet, + &interactor, tx, nonce, &args.tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 28e4f64713..6759e65aa7 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -21,29 +21,33 @@ pub async fn tx_new(args: &NewArgs) { } async fn tx_new_inner(args: &NewArgs) -> Result<()> { - let wallet = load_wallet(&args.sender)?; + let sender_wallet = load_wallet(&args.sender)?; + let relayer_wallet = load_relayer_wallet(&args.relayer)?; let receiver = Bech32Address::try_from_bech32_string(args.receiver.clone())?; // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet.clone()).await; - let sender = sender_address.to_bech32(interactor.get_hrp()); + let sender_address = interactor + .register_wallet_bech32(sender_wallet.clone()) + .await; + let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; // Determine nonce (explicit override or recalled from network). let nonce = if let Some(n) = args.tx.nonce { n } else { - interactor.recall_nonce(&sender_address).await + interactor.recall_nonce(&sender_address.address).await }; // Build Transaction via unified Tx syntax (resembles interactor code). let payments = parse_all_payment_args(&args.payment)?; let mut tx = interactor .tx() - .from(&sender) + .from(&sender_address) .to(&receiver) .gas(args.tx.gas_limit) .payment(payments) + .opt_relayer(relayer_address_opt) .into_sdk_transaction(); // Data field (mutually exclusive with --token-transfers; overrides only when provided). @@ -68,35 +72,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { tx.chain_id = chain_id.clone(); } - // Set relayer address before signing — it is included in the signing bytes. - // Priority: explicit --relayer flag > address derived from --relayer-pem. - let relayer_wallet = load_relayer_wallet(&args.relayer)?; - if let Some(relayer_str) = &args.tx.relayer { - let relayer_addr = Bech32Address::try_from_bech32_string(relayer_str.clone()) - .map_err(|e| anyhow::anyhow!("invalid --relayer address: {e}"))?; - tx.relayer = Some(relayer_addr); - } else if let Some(ref rw) = relayer_wallet { - tx.relayer = Some(rw.to_address().to_bech32_default()); - } - - let sig = wallet.sign_tx(&tx)?; - tx.signature = Some(sig); - - // Optionally sign as relayer. - if let Some(relayer_w) = relayer_wallet { - let relayer_addr = relayer_w.to_address().to_bech32_default(); - if let Some(tx_relayer) = &tx.relayer { - if relayer_addr != *tx_relayer { - return Err(anyhow::anyhow!( - "relayer wallet address {} does not match --relayer {}", - relayer_addr.to_bech32_str(), - tx_relayer.to_bech32_str(), - )); - } - } - let relayer_sig = relayer_w.sign_tx(&tx)?; - tx.relayer_signature = Some(relayer_sig); - } + interactor.sign_tx(&mut tx); let output = TxOutputFile { emitted_transaction: tx, diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 510cb70407..ab235a44c4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -19,11 +19,13 @@ pub async fn tx_upgrade(args: &UpgradeArgs) { async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { let wallet = load_wallet(&args.sender)?; + let relayer_wallet = load_relayer_wallet(&args.relayer)?; // Create the interactor – this fetches the network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; - let sender_address = interactor.register_wallet(wallet.clone()).await; + let sender_address = interactor.register_wallet(wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); + let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; // Determine nonce. let nonce = if let Some(n) = args.tx.nonce { @@ -43,7 +45,7 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { // Build upgrade transaction — same layout as deploy but sent to the existing contract address. let arg_buffer = build_arg_buffer(&args.arguments)?; - let tx_builder = interactor + let tx = interactor .tx() .from(&sender_bech32) .to(&contract) @@ -52,19 +54,9 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { .raw_upgrade() .code(code) .code_metadata(code_metadata) - .arguments_raw(arg_buffer); - - let tx = tx_builder.into_sdk_transaction(); + .arguments_raw(arg_buffer) + .opt_relayer(relayer_address_opt) + .into_sdk_transaction(); - let relayer_wallet = load_relayer_wallet(&args.relayer)?; - sign_and_dispatch( - wallet, - relayer_wallet, - tx, - nonce, - &args.tx, - &args.gateway, - None, - ) - .await + sign_and_dispatch(&interactor, tx, nonce, &args.tx, &args.gateway, None).await } diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 7de0bcf887..57f62a1790 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -296,6 +296,31 @@ where address } + /// Registers a wallet as a transaction sender and funds it in simulator mode. + /// + /// Returns the wallet address encoded as bech32 using this interactor's + /// configured network HRP. + pub async fn register_wallet_bech32(&mut self, wallet: Wallet) -> Bech32Address { + self.register_wallet(wallet).await.to_bech32(self.get_hrp()) + } + + /// Registers an optional wallet as a transaction sender and funds it in + /// simulator mode. + /// + /// Returns `Some` containing the wallet address encoded as bech32 using + /// this interactor's configured network HRP. Returns `None` when no wallet + /// is provided. + pub async fn register_wallet_bech32_opt( + &mut self, + wallet: Option, + ) -> Option { + if let Some(w) = wallet { + Some(self.register_wallet_bech32(w).await) + } else { + None + } + } + /// Sleeps for `duration` using the configured gateway and accumulates waited time. pub async fn sleep(&mut self, duration: Duration) { let millis = duration.as_millis() as u64; diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 7ed15783c7..96f3ced2ec 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -101,7 +101,17 @@ where sender.current_nonce = Some(nonce + 1); } - pub(crate) fn sign_tx(&self, transaction: &mut Transaction) { + /// Signs the transaction with the sender's wallet and, if present, the relayer's wallet. + /// + /// Updates the transaction's `signature` field with the sender's signature and the + /// `relayer_signature` field if a relayer address is set. Both wallets must be + /// registered with this interactor. + /// + /// # Panics + /// + /// Panics if the sender's wallet is not registered or if a relayer is specified + /// but not registered. + pub fn sign_tx(&self, transaction: &mut Transaction) { self.sign_tx_for_sender(transaction); self.sign_tx_for_relayer(transaction); } From 8bf199320469576cc8ee23619cd9895a26ce58a6 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 11 Aug 2026 10:58:22 +0300 Subject: [PATCH 169/195] sc-meta CLI gas price refactor --- framework/meta/src/cmd/tx/tx_cli_call.rs | 6 +++++- framework/meta/src/cmd/tx/tx_cli_common.rs | 11 +++++++---- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 6 +++++- framework/meta/src/cmd/tx/tx_cli_new.rs | 7 +++---- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 6 +++++- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 90e1d46fb2..89fb72e409 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -2,7 +2,9 @@ use anyhow::Result; use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use super::parse_payments::parse_all_payment_args; -use super::tx_cli_common::{build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch}; +use super::tx_cli_common::{ + apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, +}; use crate::cli::cli_args_tx::CallArgs; pub async fn tx_call(args: &CallArgs) { @@ -29,6 +31,8 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { interactor.recall_nonce(&sender_address).await }; + apply_gas_price(&mut interactor, &args.tx); + let contract = Bech32Address::try_from_bech32_string(args.contract.clone())?; // Build call transaction. diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index a9d08cce76..217b90072d 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -143,7 +143,13 @@ pub fn build_arg_buffer(arguments: &[String]) -> Result, ) -> Result<()> { tx.nonce = nonce; - if let Some(gas_price) = tx_args.gas_price { - tx.gas_price = gas_price; - } if let Some(chain_id) = &gateway_args.chain { tx.chain_id = chain_id.clone(); } diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index 83d36f28f8..d010eb3755 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -6,7 +6,9 @@ use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::imports::{BytesValue, Interactor, InteractorIntoSdkTransaction}; use super::parse_code_metadata::parse_code_metadata; -use super::tx_cli_common::{build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch}; +use super::tx_cli_common::{ + apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, +}; use crate::cli::cli_args_tx::DeployArgs; pub async fn tx_deploy(args: &DeployArgs) { @@ -34,6 +36,8 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { interactor.recall_nonce(&sender_address.address).await }; + apply_gas_price(&mut interactor, &args.tx); + // Read bytecode file and wrap in BytesValue so it implements TxCodeValue. let bytecode = fs::read(&args.bytecode) .with_context(|| format!("failed to read bytecode from {}", args.bytecode.display()))?; diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 6759e65aa7..43758fe8a4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -5,7 +5,7 @@ use multiversx_chain_core::std::{base64_decode, base64_encode}; use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; -use crate::cmd::tx::tx_cli_common::{load_relayer_wallet, load_wallet}; +use crate::cmd::tx::tx_cli_common::{apply_gas_price, load_relayer_wallet, load_wallet}; use super::{ output::TxOutputFile, @@ -39,6 +39,8 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { interactor.recall_nonce(&sender_address.address).await }; + apply_gas_price(&mut interactor, &args.tx); + // Build Transaction via unified Tx syntax (resembles interactor code). let payments = parse_all_payment_args(&args.payment)?; let mut tx = interactor @@ -65,9 +67,6 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { } }; tx.nonce = nonce; - if let Some(gas_price) = args.tx.gas_price { - tx.gas_price = gas_price; - } if let Some(chain_id) = &args.gateway.chain { tx.chain_id = chain_id.clone(); } diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index ab235a44c4..be6357dc5b 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -7,7 +7,9 @@ use multiversx_sc_snippets::imports::{ }; use super::parse_code_metadata::parse_code_metadata; -use super::tx_cli_common::{build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch}; +use super::tx_cli_common::{ + apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, +}; use crate::cli::cli_args_tx::UpgradeArgs; pub async fn tx_upgrade(args: &UpgradeArgs) { @@ -34,6 +36,8 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { interactor.recall_nonce(&sender_address).await }; + apply_gas_price(&mut interactor, &args.tx); + let contract = Bech32Address::try_from_bech32_string(args.contract.clone())?; // Read bytecode. From 97bdaf3cd158ef91feff16ee7e75c08fae3ee9aa Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 11 Aug 2026 11:06:39 +0300 Subject: [PATCH 170/195] sc-meta CLI chain id refactor --- framework/meta/src/cmd/tx/tx_cli_call.rs | 2 ++ framework/meta/src/cmd/tx/tx_cli_common.rs | 12 ++++++++---- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 8 +++----- framework/meta/src/cmd/tx/tx_cli_new.rs | 8 ++++---- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 2 ++ framework/snippets/src/interactor/interactor_base.rs | 11 +++++++++++ 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 89fb72e409..c872b75243 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -4,6 +4,7 @@ use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoS use super::parse_payments::parse_all_payment_args; use super::tx_cli_common::{ apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, + validate_chain_id, }; use crate::cli::cli_args_tx::CallArgs; @@ -32,6 +33,7 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { }; apply_gas_price(&mut interactor, &args.tx); + validate_chain_id(&interactor, &args.gateway)?; let contract = Bech32Address::try_from_bech32_string(args.contract.clone())?; diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 217b90072d..bce4ae53f6 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -149,7 +149,14 @@ pub fn apply_gas_price(interactor: &mut Interactor, tx_args: &TxArgs) { } } -/// Apply nonce / chain-id overrides, sign the transaction, then +pub fn validate_chain_id(interactor: &Interactor, gateway_args: &GatewayArgs) -> Result<()> { + if let Some(chain_id) = &gateway_args.chain { + interactor.validate_chain_id(chain_id)?; + } + Ok(()) +} + +/// Apply the nonce, sign the transaction, then /// write / print / broadcast it according to the `TxArgs` flags. /// `contract_address` should be `Some(bech32)` for deploy transactions. /// All wallets (sender and optional relayer) must already be registered with `interactor`. @@ -162,9 +169,6 @@ pub async fn sign_and_dispatch( contract_address: Option, ) -> Result<()> { tx.nonce = nonce; - if let Some(chain_id) = &gateway_args.chain { - tx.chain_id = chain_id.clone(); - } // Explicit --relayer address override (takes priority over the address set in the tx builder). if let Some(relayer_str) = &tx_args.relayer { diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index d010eb3755..380eb8ca38 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -2,12 +2,12 @@ use std::fs; use anyhow::{Context, Result}; use multiversx_sc::chain_core::std::new_address::compute_new_address_bech32; -use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::imports::{BytesValue, Interactor, InteractorIntoSdkTransaction}; use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, + validate_chain_id, }; use crate::cli::cli_args_tx::DeployArgs; @@ -37,6 +37,7 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { }; apply_gas_price(&mut interactor, &args.tx); + validate_chain_id(&interactor, &args.gateway)?; // Read bytecode file and wrap in BytesValue so it implements TxCodeValue. let bytecode = fs::read(&args.bytecode) @@ -63,10 +64,7 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { let contract_address = compute_new_address_bech32(&tx.sender, nonce); - // TODO: refactor, by working with an interactor object in the future, - // who keeps the effective network config/chain ID - let effective_chain_id = args.gateway.chain.as_deref().unwrap_or(&tx.chain_id); - if let Some(ex) = ExplorerUrl::from_chain_id(effective_chain_id) { + if let Some(ex) = &interactor.explorer_url { println!("new contract: {}", ex.address_url(&contract_address)); } else { println!("new contract address: {contract_address}"); diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 43758fe8a4..d74db9f39c 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -5,7 +5,9 @@ use multiversx_chain_core::std::{base64_decode, base64_encode}; use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; -use crate::cmd::tx::tx_cli_common::{apply_gas_price, load_relayer_wallet, load_wallet}; +use crate::cmd::tx::tx_cli_common::{ + apply_gas_price, load_relayer_wallet, load_wallet, validate_chain_id, +}; use super::{ output::TxOutputFile, @@ -40,6 +42,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { }; apply_gas_price(&mut interactor, &args.tx); + validate_chain_id(&interactor, &args.gateway)?; // Build Transaction via unified Tx syntax (resembles interactor code). let payments = parse_all_payment_args(&args.payment)?; @@ -67,9 +70,6 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { } }; tx.nonce = nonce; - if let Some(chain_id) = &args.gateway.chain { - tx.chain_id = chain_id.clone(); - } interactor.sign_tx(&mut tx); diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index be6357dc5b..3bae4ad8d0 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -9,6 +9,7 @@ use multiversx_sc_snippets::imports::{ use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, + validate_chain_id, }; use crate::cli::cli_args_tx::UpgradeArgs; @@ -37,6 +38,7 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { }; apply_gas_price(&mut interactor, &args.tx); + validate_chain_id(&interactor, &args.gateway)?; let contract = Bech32Address::try_from_bech32_string(args.contract.clone())?; diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 57f62a1790..44d34d5fde 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -268,6 +268,17 @@ where .network_config } + /// Validates that `chain_id` matches the chain ID reported by the gateway. + pub fn validate_chain_id(&self, chain_id: &str) -> anyhow::Result<()> { + let network_chain_id = &self.network_config().chain_id; + if chain_id != network_chain_id { + anyhow::bail!( + "chain ID mismatch: CLI specifies {chain_id}, but gateway reports {network_chain_id}" + ); + } + Ok(()) + } + /// Enables or disables chain-simulator mode. pub fn use_chain_simulator(mut self, use_chain_simulator: bool) -> Self { self.use_chain_simulator = use_chain_simulator; From f24ca6259da611f8fd5800db6b376f1d7e8a32f4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 11 Aug 2026 12:37:27 +0300 Subject: [PATCH 171/195] relayer CLI - unsigned refactor --- framework/meta/src/cli/cli_args_sender.rs | 7 ++ framework/meta/src/cli/cli_args_tx.rs | 5 -- framework/meta/src/cmd/tx/tx_cli_call.rs | 5 +- framework/meta/src/cmd/tx/tx_cli_common.rs | 68 ++++++++++++++++--- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 5 +- framework/meta/src/cmd/tx/tx_cli_new.rs | 46 ++----------- framework/meta/src/cmd/tx/tx_cli_sign.rs | 8 ++- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 5 +- .../src/interactor/interactor_sender.rs | 47 ++++++++++--- 9 files changed, 123 insertions(+), 73 deletions(-) diff --git a/framework/meta/src/cli/cli_args_sender.rs b/framework/meta/src/cli/cli_args_sender.rs index b6b9936296..a05f485dac 100644 --- a/framework/meta/src/cli/cli_args_sender.rs +++ b/framework/meta/src/cli/cli_args_sender.rs @@ -31,6 +31,13 @@ pub struct SenderArgs { /// Wallet arguments for the relayer. Mirrors [`SenderArgs`] with a `--relayer-*` prefix. #[derive(Clone, PartialEq, Eq, Debug, Args)] pub struct RelayerArgs { + /// Bech32 address of the relayer that will pay the gas fees (relayed v3). + /// The gas limit must include the extra base cost for the relayed operation. + /// Allows specifying a relayer address without a wallet, + /// in which case the transaction will remain unsigned by the relayer. + #[arg(long)] + pub relayer: Option, + /// Path to a PEM wallet file for the relayer. #[arg(long = "relayer-pem", group = "relayer_source")] pub relayer_pem: Option, diff --git a/framework/meta/src/cli/cli_args_tx.rs b/framework/meta/src/cli/cli_args_tx.rs index dff73fa8a9..bffc52b8ee 100644 --- a/framework/meta/src/cli/cli_args_tx.rs +++ b/framework/meta/src/cli/cli_args_tx.rs @@ -67,11 +67,6 @@ pub struct TxArgs { #[arg(long)] pub nonce: Option, - /// Bech32 address of the relayer that will pay the gas fees (relayed v3). - /// The gas limit must include the extra base cost for the relayed operation. - #[arg(long)] - pub relayer: Option, - /// If set, the transaction is broadcast to the network. /// Without this flag the signed tx JSON is written to --outfile or stdout. #[arg(long, default_value = "false")] diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index c872b75243..01af8dd7c4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -3,7 +3,7 @@ use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoS use super::parse_payments::parse_all_payment_args; use super::tx_cli_common::{ - apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, + apply_gas_price, build_arg_buffer, load_relayer_for_interactor, load_wallet, sign_and_dispatch, validate_chain_id, }; use crate::cli::cli_args_tx::CallArgs; @@ -17,13 +17,12 @@ pub async fn tx_call(args: &CallArgs) { async fn tx_call_inner(args: &CallArgs) -> Result<()> { let sender_wallet = load_wallet(&args.sender)?; - let relayer_wallet = load_relayer_wallet(&args.relayer)?; // Create the interactor – fetches network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; let sender_address = interactor.register_wallet(sender_wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); - let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; + let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; // Determine nonce. let nonce = if let Some(n) = args.tx.nonce { diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index bce4ae53f6..8a73f4d7f4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -20,7 +20,7 @@ use serde_json::Value; use super::output::TxOutputFile; pub use crate::cli::cli_args_sender::{load_relayer_wallet, load_wallet}; -use crate::cli::cli_args_tx::{GatewayArgs, TxArgs}; +use crate::cli::cli_args_tx::{GatewayArgs, RelayerArgs, TxArgs}; /// Load a transaction from an mxpy-compatible interaction JSON file. /// Accepts both `{"emittedTransaction": {...}}` and `{"tx": {...}}` wrappers. @@ -156,10 +156,43 @@ pub fn validate_chain_id(interactor: &Interactor, gateway_args: &GatewayArgs) -> Ok(()) } +pub async fn load_relayer_for_interactor( + interactor: &mut Interactor, + relayer_args: &RelayerArgs, +) -> Result> { + let explicit_address = relayer_args + .relayer + .as_ref() + .map(|address| { + Bech32Address::try_from_bech32_string(address.clone()) + .map_err(|e| anyhow!("invalid --relayer address: {e}")) + }) + .transpose()?; + + let Some(wallet) = load_relayer_wallet(relayer_args)? else { + return Ok(explicit_address); + }; + + let wallet_address = interactor.register_wallet_bech32(wallet).await; + if let Some(explicit_address) = explicit_address { + if explicit_address != wallet_address { + return Err(anyhow!( + "relayer wallet address {} does not match --relayer address {}", + wallet_address.to_bech32_str(), + explicit_address.to_bech32_str(), + )); + } + Ok(Some(explicit_address)) + } else { + Ok(Some(wallet_address)) + } +} + /// Apply the nonce, sign the transaction, then /// write / print / broadcast it according to the `TxArgs` flags. /// `contract_address` should be `Some(bech32)` for deploy transactions. -/// All wallets (sender and optional relayer) must already be registered with `interactor`. +/// The sender wallet and any relayer wallet must already be registered with `interactor`. +/// A relayer address without a registered wallet is preserved without a relayer signature. pub async fn sign_and_dispatch( interactor: &Interactor, mut tx: Transaction, @@ -170,13 +203,6 @@ pub async fn sign_and_dispatch( ) -> Result<()> { tx.nonce = nonce; - // Explicit --relayer address override (takes priority over the address set in the tx builder). - if let Some(relayer_str) = &tx_args.relayer { - let relayer_addr = Bech32Address::try_from_bech32_string(relayer_str.clone()) - .map_err(|e| anyhow!("invalid --relayer address: {e}"))?; - tx.relayer = Some(relayer_addr); - } - let decoded_data = match &tx.data { None => String::new(), Some(d) => { @@ -220,3 +246,27 @@ pub(super) fn print_tx_results(tx_response: &multiversx_sc_scenario::scenario_mo println!("Result[{i}]: 0x{}", hex::encode(result)); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn explicit_relayer_address_does_not_register_wallet() { + let mut interactor = Interactor::empty(); + let relayer = Bech32Address::zero_default_hrp(); + let args = RelayerArgs { + relayer: Some(relayer.to_bech32_string()), + relayer_pem: None, + relayer_keyfile: None, + relayer_keystore_password: None, + }; + + let loaded_relayer = load_relayer_for_interactor(&mut interactor, &args) + .await + .unwrap(); + + assert_eq!(loaded_relayer, Some(relayer)); + assert!(interactor.sender_map.is_empty()); + } +} diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index 380eb8ca38..5fd852cee5 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -6,7 +6,7 @@ use multiversx_sc_snippets::imports::{BytesValue, Interactor, InteractorIntoSdkT use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ - apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, + apply_gas_price, build_arg_buffer, load_relayer_for_interactor, load_wallet, sign_and_dispatch, validate_chain_id, }; use crate::cli::cli_args_tx::DeployArgs; @@ -20,14 +20,13 @@ pub async fn tx_deploy(args: &DeployArgs) { async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { let sender_wallet = load_wallet(&args.sender)?; - let relayer_wallet = load_relayer_wallet(&args.relayer)?; // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; let sender_address = interactor .register_wallet_bech32(sender_wallet.clone()) .await; - let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; + let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; // Determine nonce. let nonce = if let Some(n) = args.tx.nonce { diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index d74db9f39c..abc8dd6c8a 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -1,19 +1,15 @@ use std::fs; use anyhow::{Context, Result}; -use multiversx_chain_core::std::{base64_decode, base64_encode}; +use multiversx_chain_core::std::base64_encode; use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; use crate::cmd::tx::tx_cli_common::{ - apply_gas_price, load_relayer_wallet, load_wallet, validate_chain_id, + apply_gas_price, load_relayer_for_interactor, load_wallet, sign_and_dispatch, validate_chain_id, }; -use super::{ - output::TxOutputFile, - parse_payments::parse_all_payment_args, - tx_cli_common::{broadcast_and_save, save_output}, -}; +use super::parse_payments::parse_all_payment_args; pub async fn tx_new(args: &NewArgs) { if let Err(e) = tx_new_inner(args).await { @@ -24,7 +20,6 @@ pub async fn tx_new(args: &NewArgs) { async fn tx_new_inner(args: &NewArgs) -> Result<()> { let sender_wallet = load_wallet(&args.sender)?; - let relayer_wallet = load_relayer_wallet(&args.relayer)?; let receiver = Bech32Address::try_from_bech32_string(args.receiver.clone())?; // Create the interactor – this fetches the network config in the process. @@ -32,7 +27,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { let sender_address = interactor .register_wallet_bech32(sender_wallet.clone()) .await; - let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; + let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; // Determine nonce (explicit override or recalled from network). let nonce = if let Some(n) = args.tx.nonce { @@ -61,38 +56,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { tx.data = Some(base64_encode(&data_raw)); } - // Decode the data field for the human-readable output. - let decoded_data = match &tx.data { - None => String::new(), - Some(d) => { - let bytes = base64_decode(d)?; - String::from_utf8_lossy(&bytes).into_owned() - } - }; - tx.nonce = nonce; - - interactor.sign_tx(&mut tx); - - let output = TxOutputFile { - emitted_transaction: tx, - emitted_transaction_data: decoded_data, - emitted_transaction_hash: String::new(), - contract_address: None, - transaction_on_network: None, - }; - - if args.tx.send { - broadcast_and_save( - output, - &args.gateway.proxy, - args.tx.outfile.as_deref(), - args.tx.wait_result, - ) - .await?; - } else { - save_output(&output, args.tx.outfile.as_deref())?; - } - Ok(()) + sign_and_dispatch(&interactor, tx, nonce, &args.tx, &args.gateway, None).await } // --------------------------------------------------------------------------- diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 5695ee05c1..db6a6ced36 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -1,5 +1,5 @@ use anyhow::{Result, anyhow}; -use multiversx_chain_core::std::base64_decode; +use multiversx_chain_core::std::{Bech32Address, base64_decode}; use super::{ output::TxOutputFile, @@ -35,6 +35,12 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { tx.chain_id = chain_id.clone(); } + if let Some(relayer_str) = &args.relayer.relayer { + let relayer_addr = Bech32Address::try_from_bech32_string(relayer_str.clone()) + .map_err(|e| anyhow!("invalid --relayer address: {e}"))?; + tx.relayer = Some(relayer_addr); + } + let sig = wallet.sign_tx(&tx)?; tx.signature = Some(sig); diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 3bae4ad8d0..55c099efbc 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -8,7 +8,7 @@ use multiversx_sc_snippets::imports::{ use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ - apply_gas_price, build_arg_buffer, load_relayer_wallet, load_wallet, sign_and_dispatch, + apply_gas_price, build_arg_buffer, load_relayer_for_interactor, load_wallet, sign_and_dispatch, validate_chain_id, }; use crate::cli::cli_args_tx::UpgradeArgs; @@ -22,13 +22,12 @@ pub async fn tx_upgrade(args: &UpgradeArgs) { async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { let wallet = load_wallet(&args.sender)?; - let relayer_wallet = load_relayer_wallet(&args.relayer)?; // Create the interactor – this fetches the network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; let sender_address = interactor.register_wallet(wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); - let relayer_address_opt = interactor.register_wallet_bech32_opt(relayer_wallet).await; + let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; // Determine nonce. let nonce = if let Some(n) = args.tx.nonce { diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 96f3ced2ec..e2cc536454 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -104,13 +104,12 @@ where /// Signs the transaction with the sender's wallet and, if present, the relayer's wallet. /// /// Updates the transaction's `signature` field with the sender's signature and the - /// `relayer_signature` field if a relayer address is set. Both wallets must be - /// registered with this interactor. + /// `relayer_signature` field if its wallet is registered. If only a relayer + /// address is present, the transaction is left without a relayer signature. /// /// # Panics /// - /// Panics if the sender's wallet is not registered or if a relayer is specified - /// but not registered. + /// Panics if the sender's wallet is not registered. pub fn sign_tx(&self, transaction: &mut Transaction) { self.sign_tx_for_sender(transaction); self.sign_tx_for_relayer(transaction); @@ -136,10 +135,9 @@ where let Some(relayer_address) = &transaction.relayer else { return; }; - let relayer = self - .sender_map - .get(relayer_address.as_address()) - .expect("relayer wallet not registered"); + let Some(relayer) = self.sender_map.get(relayer_address.as_address()) else { + return; + }; let sig = relayer .wallet .sign_tx(transaction) @@ -147,3 +145,36 @@ where transaction.relayer_signature = Some(sig); } } + +#[cfg(all(test, feature = "http"))] +mod tests { + use multiversx_sc_scenario::imports::Bech32Address; + use multiversx_sdk::data::transaction::{Transaction, TransactionVersion}; + + use crate::Interactor; + + #[test] + fn leaves_relayer_signature_empty_when_wallet_is_not_registered() { + let interactor = Interactor::empty(); + let address = Bech32Address::zero_default_hrp(); + let mut transaction = Transaction { + nonce: 0, + value: "0".to_owned(), + receiver: address.clone(), + sender: address.clone(), + gas_price: 1_000_000_000, + gas_limit: 100_000, + data: None, + signature: None, + chain_id: "D".to_owned(), + version: TransactionVersion::V2, + options: None, + relayer: Some(address), + relayer_signature: None, + }; + + interactor.sign_tx_for_relayer(&mut transaction); + + assert!(transaction.relayer_signature.is_none()); + } +} From 60fad4c30ecf1d64bb9f8d12fd7cb42325bc57c0 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 12 Aug 2026 02:27:47 +0300 Subject: [PATCH 172/195] interactor - refactor set_tx_nonce_update_sender --- .../interactor_scenario/interactor_sc_call.rs | 4 +--- .../interactor_scenario/interactor_sc_deploy.rs | 4 +--- .../interactor_scenario/interactor_transfer.rs | 5 +---- .../snippets/src/interactor/interactor_sender.rs | 16 +++++----------- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index 896c30cf1c..f6491db156 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -62,11 +62,9 @@ where } async fn tx_call_to_blockchain_signed_tx(&mut self, sc_call_step: &ScCallStep) -> Transaction { - let sender_address = &sc_call_step.tx.from.value; let mut transaction = self.tx_call_to_blockchain_tx(&sc_call_step.tx); // Set relayer field before signing — it is included in the signing bytes. - self.set_tx_nonce_update_sender(sender_address, &mut transaction) - .await; + self.set_tx_nonce_update_sender(&mut transaction).await; self.sign_tx(&mut transaction); transaction diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index 98189e96d5..2517a4e83c 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -47,11 +47,9 @@ where &mut self, sc_deploy_step: &ScDeployStep, ) -> Transaction { - let sender_address = &sc_deploy_step.tx.from.value; let mut transaction = self.sc_deploy_to_blockchain_tx(sc_deploy_step); // Set relayer field before signing — it is included in the signing bytes. - self.set_tx_nonce_update_sender(sender_address, &mut transaction) - .await; + self.set_tx_nonce_update_sender(&mut transaction).await; self.sign_tx(&mut transaction); transaction diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs index 0a209b7c1e..02514f2d9c 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs @@ -62,11 +62,8 @@ where async fn launch_transfer(&mut self, transfer_step: &TransferStep) -> Transaction { self.pre_runners.run_transfer_step(transfer_step); - - let sender_address = &transfer_step.tx.from.value; let mut transaction = self.tx_call_to_blockchain_tx(&transfer_step.tx.to_tx_call()); - self.set_tx_nonce_update_sender(sender_address, &mut transaction) - .await; + self.set_tx_nonce_update_sender(&mut transaction).await; self.sign_tx(&mut transaction); transaction diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index e2cc536454..cb38571aab 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -74,19 +74,13 @@ where /// Updates: /// - the transaction with the nonce read from the network /// - the sender's current_nonce - pub(crate) async fn set_tx_nonce_update_sender( - &mut self, - sender_address: &Address, - transaction: &mut Transaction, - ) { - // read - let sender = self - .sender_map - .get(sender_address) - .expect("the wallet that was supposed to sign is not registered"); + /// + /// Assumes that the transaction sender has already been set. + pub(crate) async fn set_tx_nonce_update_sender(&mut self, transaction: &mut Transaction) { + let sender_address = transaction.sender.as_address(); // recall - let nonce = self.recall_nonce(&sender.address).await; + let nonce = self.recall_nonce(sender_address).await; println!("sender's recalled nonce: {nonce}"); // set tx nonce From d6bc76b5a81ce366f89527524bd132e21573af65 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 12 Aug 2026 03:00:58 +0300 Subject: [PATCH 173/195] interactor - override tx nonce mechanism --- framework/meta/src/cmd/tx/tx_cli_call.rs | 13 +++------ framework/meta/src/cmd/tx/tx_cli_common.rs | 7 ++--- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 14 +++------- framework/meta/src/cmd/tx/tx_cli_new.rs | 11 ++------ framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 13 +++------ .../src/interactor/interactor_base.rs | 10 +++++++ .../src/interactor/interactor_sender.rs | 28 +++++++++++++++---- .../src/multi/interactor_multi_sc_process.rs | 3 +- 8 files changed, 51 insertions(+), 48 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 01af8dd7c4..21075050c1 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -20,17 +20,11 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { // Create the interactor – fetches network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; + interactor.override_next_tx_nonce = args.tx.nonce; let sender_address = interactor.register_wallet(sender_wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - // Determine nonce. - let nonce = if let Some(n) = args.tx.nonce { - n - } else { - interactor.recall_nonce(&sender_address).await - }; - apply_gas_price(&mut interactor, &args.tx); validate_chain_id(&interactor, &args.gateway)?; @@ -40,7 +34,7 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { let arg_buffer = build_arg_buffer(&args.arguments)?; let payments = parse_all_payment_args(&args.payment)?; - let tx = interactor + let mut tx = interactor .tx() .from(&sender_bech32) .to(&contract) @@ -50,6 +44,7 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { .arguments_raw(arg_buffer) .opt_relayer(relayer_address_opt) .into_sdk_transaction(); + interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, nonce, &args.tx, &args.gateway, None).await + sign_and_dispatch(&interactor, tx, &args.tx, &args.gateway, None).await } diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 8a73f4d7f4..a0b4aa746f 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -188,21 +188,18 @@ pub async fn load_relayer_for_interactor( } } -/// Apply the nonce, sign the transaction, then -/// write / print / broadcast it according to the `TxArgs` flags. +/// Sign the nonce-prepared transaction, then write / print / broadcast it +/// according to the `TxArgs` flags. /// `contract_address` should be `Some(bech32)` for deploy transactions. /// The sender wallet and any relayer wallet must already be registered with `interactor`. /// A relayer address without a registered wallet is preserved without a relayer signature. pub async fn sign_and_dispatch( interactor: &Interactor, mut tx: Transaction, - nonce: u64, tx_args: &TxArgs, gateway_args: &GatewayArgs, contract_address: Option, ) -> Result<()> { - tx.nonce = nonce; - let decoded_data = match &tx.data { None => String::new(), Some(d) => { diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index 5fd852cee5..9e79952bab 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -23,18 +23,12 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; + interactor.override_next_tx_nonce = args.tx.nonce; let sender_address = interactor .register_wallet_bech32(sender_wallet.clone()) .await; let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - // Determine nonce. - let nonce = if let Some(n) = args.tx.nonce { - n - } else { - interactor.recall_nonce(&sender_address.address).await - }; - apply_gas_price(&mut interactor, &args.tx); validate_chain_id(&interactor, &args.gateway)?; @@ -59,9 +53,10 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { .arguments_raw(arg_buffer) .opt_relayer(relayer_address_opt); - let tx = tx_builder.into_sdk_transaction(); + let mut tx = tx_builder.into_sdk_transaction(); + interactor.set_tx_nonce_update_sender(&mut tx).await; - let contract_address = compute_new_address_bech32(&tx.sender, nonce); + let contract_address = compute_new_address_bech32(&tx.sender, tx.nonce); if let Some(ex) = &interactor.explorer_url { println!("new contract: {}", ex.address_url(&contract_address)); @@ -72,7 +67,6 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { sign_and_dispatch( &interactor, tx, - nonce, &args.tx, &args.gateway, Some(contract_address.to_bech32_string()), diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index abc8dd6c8a..a0ff80a0b7 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -24,18 +24,12 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { // Create the interactor – this fetches the network config in the process. let mut interactor = Interactor::new(&args.gateway.proxy).await; + interactor.override_next_tx_nonce = args.tx.nonce; let sender_address = interactor .register_wallet_bech32(sender_wallet.clone()) .await; let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - // Determine nonce (explicit override or recalled from network). - let nonce = if let Some(n) = args.tx.nonce { - n - } else { - interactor.recall_nonce(&sender_address.address).await - }; - apply_gas_price(&mut interactor, &args.tx); validate_chain_id(&interactor, &args.gateway)?; @@ -55,8 +49,9 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { if !data_raw.is_empty() { tx.data = Some(base64_encode(&data_raw)); } + interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, nonce, &args.tx, &args.gateway, None).await + sign_and_dispatch(&interactor, tx, &args.tx, &args.gateway, None).await } // --------------------------------------------------------------------------- diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 55c099efbc..98079b743a 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -25,17 +25,11 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { // Create the interactor – this fetches the network config. let mut interactor = Interactor::new(&args.gateway.proxy).await; + interactor.override_next_tx_nonce = args.tx.nonce; let sender_address = interactor.register_wallet(wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - // Determine nonce. - let nonce = if let Some(n) = args.tx.nonce { - n - } else { - interactor.recall_nonce(&sender_address).await - }; - apply_gas_price(&mut interactor, &args.tx); validate_chain_id(&interactor, &args.gateway)?; @@ -50,7 +44,7 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { // Build upgrade transaction — same layout as deploy but sent to the existing contract address. let arg_buffer = build_arg_buffer(&args.arguments)?; - let tx = interactor + let mut tx = interactor .tx() .from(&sender_bech32) .to(&contract) @@ -62,6 +56,7 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { .arguments_raw(arg_buffer) .opt_relayer(relayer_address_opt) .into_sdk_transaction(); + interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, nonce, &args.tx, &args.gateway, None).await + sign_and_dispatch(&interactor, tx, &args.tx, &args.gateway, None).await } diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 44d34d5fde..57101dd899 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -81,6 +81,15 @@ where /// `connection.network_config.min_gas_price` at connection time. pub gas_price: u64, + /// Explicit transaction nonce. When set, transaction preparation uses this + /// value instead of fetching the sender nonce from the gateway. + /// + /// Only applies for the next transaction; it is cleared after use. + /// + /// This is used to implement the `--nonce` CLI flag, + /// which overrides the nonce for a single transaction. + pub override_next_tx_nonce: Option, + /// All wallets registered as transaction signers, keyed by their on-chain /// address. Populated by [`Self::register_wallet`]. pub sender_map: HashMap, @@ -118,6 +127,7 @@ where post_runners: ScenarioRunnerList::empty(), current_dir: PathBuf::default(), gas_price: 0, + override_next_tx_nonce: None, explorer_url: None, } } diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index cb38571aab..4126449f21 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -76,12 +76,10 @@ where /// - the sender's current_nonce /// /// Assumes that the transaction sender has already been set. - pub(crate) async fn set_tx_nonce_update_sender(&mut self, transaction: &mut Transaction) { + pub async fn set_tx_nonce_update_sender(&mut self, transaction: &mut Transaction) { let sender_address = transaction.sender.as_address(); - // recall - let nonce = self.recall_nonce(sender_address).await; - println!("sender's recalled nonce: {nonce}"); + let nonce = self.resolve_tx_nonce(sender_address).await; // set tx nonce transaction.nonce = nonce; @@ -95,6 +93,17 @@ where sender.current_nonce = Some(nonce + 1); } + pub(crate) async fn resolve_tx_nonce(&mut self, sender_address: &Address) -> u64 { + if let Some(nonce) = self.override_next_tx_nonce.take() { + println!("sender's overridden nonce: {nonce}"); + nonce + } else { + let nonce = self.recall_nonce(sender_address).await; + println!("sender's recalled nonce: {nonce}"); + nonce + } + } + /// Signs the transaction with the sender's wallet and, if present, the relayer's wallet. /// /// Updates the transaction's `signature` field with the sender's signature and the @@ -142,11 +151,20 @@ where #[cfg(all(test, feature = "http"))] mod tests { - use multiversx_sc_scenario::imports::Bech32Address; + use multiversx_sc_scenario::{imports::Bech32Address, multiversx_sc::types::Address}; use multiversx_sdk::data::transaction::{Transaction, TransactionVersion}; use crate::Interactor; + #[tokio::test] + async fn nonce_override_does_not_require_a_gateway_connection() { + let mut interactor = Interactor::empty(); + interactor.override_next_tx_nonce = Some(42); + + assert_eq!(interactor.resolve_tx_nonce(&Address::zero()).await, 42); + assert_eq!(interactor.override_next_tx_nonce, None); + } + #[test] fn leaves_relayer_signature_empty_when_wallet_is_not_registered() { let interactor = Interactor::empty(); diff --git a/framework/snippets/src/multi/interactor_multi_sc_process.rs b/framework/snippets/src/multi/interactor_multi_sc_process.rs index 46f086b6ed..a0bddac7f2 100644 --- a/framework/snippets/src/multi/interactor_multi_sc_process.rs +++ b/framework/snippets/src/multi/interactor_multi_sc_process.rs @@ -15,14 +15,13 @@ where { pub(crate) async fn recall_senders_nonce(&mut self, senders: HashSet
) { for sender_address in &senders { - let nonce = self.recall_nonce(sender_address).await; + let nonce = self.resolve_tx_nonce(sender_address).await; let sender = self .sender_map .get_mut(sender_address) .expect("sender not registered"); sender.current_nonce = Some(nonce); - println!("sender's recalled nonce: {nonce}"); } } From b92abd1bc03f4460b93d7dcbaf8739701d52a7ad Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 12 Aug 2026 03:09:13 +0300 Subject: [PATCH 174/195] interactor - gateway uri refactor --- framework/meta/src/cmd/tx/tx_cli_call.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_common.rs | 3 +- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 1 - framework/meta/src/cmd/tx/tx_cli_new.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 2 +- .../src/interactor/interactor_base.rs | 34 +++++++++++-------- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 21075050c1..473879c2de 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -46,5 +46,5 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { .into_sdk_transaction(); interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, &args.tx, &args.gateway, None).await + sign_and_dispatch(&interactor, tx, &args.tx, None).await } diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index a0b4aa746f..25178a2236 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -197,7 +197,6 @@ pub async fn sign_and_dispatch( interactor: &Interactor, mut tx: Transaction, tx_args: &TxArgs, - gateway_args: &GatewayArgs, contract_address: Option, ) -> Result<()> { let decoded_data = match &tx.data { @@ -221,7 +220,7 @@ pub async fn sign_and_dispatch( if tx_args.send { broadcast_and_save( output, - &gateway_args.proxy, + interactor.gateway_uri(), tx_args.outfile.as_deref(), tx_args.wait_result, ) diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index 9e79952bab..a868c90a54 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -68,7 +68,6 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { &interactor, tx, &args.tx, - &args.gateway, Some(contract_address.to_bech32_string()), ) .await diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index a0ff80a0b7..d3a5af8a16 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -51,7 +51,7 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { } interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, &args.tx, &args.gateway, None).await + sign_and_dispatch(&interactor, tx, &args.tx, None).await } // --------------------------------------------------------------------------- diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 98079b743a..c69c5ad554 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -58,5 +58,5 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { .into_sdk_transaction(); interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, &args.tx, &args.gateway, None).await + sign_and_dispatch(&interactor, tx, &args.tx, None).await } diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 57101dd899..945e68698c 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -38,6 +38,8 @@ pub struct InteractorConnection where GatewayProxy: GatewayAsyncService, { + /// Gateway URI used to construct `proxy`. + pub gateway_uri: String, /// The async gateway used to communicate with the blockchain network. pub proxy: GatewayProxy, /// Chain-level parameters (chain ID, gas price, HRP, …) fetched from the gateway. @@ -159,6 +161,7 @@ where self.gas_price = network_config.min_gas_price; self.explorer_url = ExplorerUrl::from_chain_id(&network_config.chain_id); self.connection = Some(InteractorConnection { + gateway_uri: gateway_uri.to_owned(), proxy, network_config, }); @@ -248,19 +251,28 @@ where config } + fn connection(&self) -> &InteractorConnection { + self.connection.as_ref().expect( + "interactor connection is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", + ) + } + /// Returns the initialized gateway proxy reference. /// /// # Panics /// /// Panics if connection has not been initialized. pub fn proxy(&self) -> &GatewayProxy { - &self - .connection - .as_ref() - .expect( - "interactor proxy is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", - ) - .proxy + &self.connection().proxy + } + + /// Returns the gateway URI used to initialize the connection. + /// + /// # Panics + /// + /// Panics if connection has not been initialized. + pub fn gateway_uri(&self) -> &str { + &self.connection().gateway_uri } /// Returns the initialized network configuration. @@ -269,13 +281,7 @@ where /// /// Panics if connection has not been initialized. pub fn network_config(&self) -> &NetworkConfig { - &self - .connection - .as_ref() - .expect( - "interactor network_config is uninitialized; call InteractorBase::with_connection(...) or InteractorBase::new(...) first", - ) - .network_config + &self.connection().network_config } /// Validates that `chain_id` matches the chain ID reported by the gateway. From 6c9d6cfbd91969237c2bb22db4720b84c2a6174f Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 13 Aug 2026 00:50:55 +0300 Subject: [PATCH 175/195] sc-meta CLI cs tests - more blocks --- framework/meta/tests/cs_tx_cli_test.rs | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 5165ae8ea4..c42b626439 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -14,6 +14,8 @@ const TRANSFER_AMOUNT: u128 = 100_000_000_000_000_000; /// Minimum gas for a plain EGLD transfer. const GAS_LIMIT: u64 = 50_000; +const CLI_TX_PROCESSING_BLOCKS: u64 = 40; + /// Deploys the adder contract, calls `add`, and verifies `getSum` returns the expected value. /// Mirrors the deploy / add / getSum flow from the adder snippets.sh. #[tokio::test] @@ -76,7 +78,10 @@ async fn test_adder_deploy_add_get_sum() { ); assert!(deploy_output.status.success(), "deploy failed"); - interactor.generate_blocks(10).await.unwrap(); + interactor + .generate_blocks(CLI_TX_PROCESSING_BLOCKS) + .await + .unwrap(); // Read the deployed contract address from the interaction output file. let outfile_content = @@ -140,7 +145,10 @@ async fn test_adder_deploy_add_get_sum() { assert!(status.success(), "add call failed"); - interactor.generate_blocks(10).await.unwrap(); + interactor + .generate_blocks(CLI_TX_PROCESSING_BLOCKS) + .await + .unwrap(); // Read and verify deterministic call fields. let call_content = std::fs::read_to_string(&outfile_call).expect("failed to read call outfile"); @@ -223,7 +231,10 @@ async fn test_adder_deploy_add_get_sum() { ); assert!(upgrade_output.status.success(), "upgrade failed"); - interactor.generate_blocks(10).await.unwrap(); + interactor + .generate_blocks(CLI_TX_PROCESSING_BLOCKS) + .await + .unwrap(); // Verify the upgrade outfile references the same contract address. let upgrade_content = @@ -307,7 +318,10 @@ async fn test_adder_deploy_add_get_sum() { assert!(status.success(), "relayed add call failed"); - interactor.generate_blocks(10).await.unwrap(); + interactor + .generate_blocks(CLI_TX_PROCESSING_BLOCKS) + .await + .unwrap(); // getSum must return 5 + 3 = 8 after the relayed add. let query_after_relayed = Command::new(sc_meta_bin) @@ -416,7 +430,10 @@ async fn test_egld_transfer_alice_to_bob() { assert!(status.success(), "sc-meta tx new command failed"); // Allow the transfer transaction to settle. - interactor.generate_blocks(20).await.unwrap(); + interactor + .generate_blocks(CLI_TX_PROCESSING_BLOCKS) + .await + .unwrap(); // ── balances after transfer ─────────────────────────────────────────────── let alice_balance_after: u128 = interactor From c4bb79f27b8d03b69335942d7bd7ddd4c3bcbea8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 13 Aug 2026 19:14:02 +0300 Subject: [PATCH 176/195] cleanup --- framework/snippets/src/interactor/interactor_sender.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index 4126449f21..bc8788fa30 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -149,7 +149,7 @@ where } } -#[cfg(all(test, feature = "http"))] +#[cfg(test)] mod tests { use multiversx_sc_scenario::{imports::Bech32Address, multiversx_sc::types::Address}; use multiversx_sdk::data::transaction::{Transaction, TransactionVersion}; From fc9b1dc8dbd051142fdfd5a2136898d3de0f0674 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 13 Aug 2026 19:26:35 +0300 Subject: [PATCH 177/195] plan cleanup --- framework/meta/relayer.md | 343 -------------------------------------- 1 file changed, 343 deletions(-) delete mode 100644 framework/meta/relayer.md diff --git a/framework/meta/relayer.md b/framework/meta/relayer.md deleted file mode 100644 index 343b040df2..0000000000 --- a/framework/meta/relayer.md +++ /dev/null @@ -1,343 +0,0 @@ -# Relayed Transactions v3 — Implementation Plan - -## Overview - -Relayed transactions v3 add two optional fields to an existing transaction: -- `relayer` — bech32 address of the account that pays the gas fees -- `relayerSignature` — hex signature proving the relayer's agreement - -No inner transaction wrapping is needed (unlike v1/v2). The sender signs first, then the relayer signs. - -Gas: `gas_limit = base_cost + base_cost + cost_per_byte * len(data)` - -Signing order: -1. Sender signs over the full transaction bytes (which already include the `relayer` field). -2. Relayer signs over the same transaction bytes (with `relayer` set but `relayerSignature` still empty/absent). - ---- - -## Phase 1 — SDK: Extend the `Transaction` struct - -**File:** `sdk/core/src/data/transaction/transaction_request.rs` - -Add two new optional fields, skipped during serialization when absent: - -```rust -#[serde(skip_serializing_if = "Option::is_none")] -pub relayer: Option, - -#[serde(skip_serializing_if = "Option::is_none")] -pub relayer_signature: Option, -``` - -The existing `Wallet::sign_tx` method already signs over the serialized transaction bytes via protobuf. The protobuf schema already supports `Relayer` and `RelayerSignature` fields (confirmed in `chain/vm`). No new signing method is needed — the same `sign_tx` is called by both sender and relayer (at different stages, with `relayer_signature` absent during both signing calls). - -Verify that the protobuf serializer (`sdk/core/src/data/transaction/`) includes `relayer` in the bytes-to-sign and excludes `relayer_signature` from both signing payloads. - ---- - -## Phase 2 — sc-meta CLI - -### 2a. `RelayerArgs` struct - -**File:** `framework/meta/src/cli/cli_args_sender.rs` -*(mirrors the existing `SenderArgs`)* - -```rust -/// Wallet / relayer arguments for commands that add a relayer signature. -#[derive(Clone, PartialEq, Eq, Debug, Args)] -pub struct RelayerArgs { - /// Path to a PEM wallet file for the relayer. - #[arg(long = "relayer-pem", group = "relayer_source")] - pub pem: Option, - - /// Path to a JSON keystore wallet file for the relayer. - #[arg(long = "relayer-keyfile", group = "relayer_source")] - pub keyfile: Option, - - /// Relayer keystore password (plain text). If omitted, prompted interactively. - #[arg(long = "relayer-keystore-password")] - pub keystore_password: Option, -} -``` - -Add a companion `load_relayer_wallet(args: &RelayerArgs) -> Result>` function alongside the existing `load_wallet`. Returns `None` when no relayer args are set (relayer wallet is optional in most commands). - -### 2b. `--relayer` address field - -**File:** `framework/meta/src/cli/cli_args_tx.rs` - -Add to `TxArgs`: -```rust -/// Bech32 address of the relayer. If set, the transaction gas limit must -/// include the extra base cost for the relayed operation. -#[arg(long)] -pub relayer: Option, -``` - -This field is used by `tx new`, `tx deploy`, `tx call`, `tx upgrade` to set `tx.relayer` before the sender signs. - -### 2c. Extend existing commands to accept a relayer wallet - -**Files:** `cli_args_tx.rs`, `tx_cli_new.rs`, `tx_cli_deploy.rs`, `tx_cli_call.rs`, `tx_cli_upgrade.rs` - -For each of `DeployArgs`, `CallArgs`, `UpgradeArgs`, `NewArgs`: - -```rust -#[command(flatten)] -pub relayer: RelayerArgs, -``` - -In their respective `tx_cli_*.rs` implementations: -1. If `args.tx.relayer` is set, write it to `tx.relayer` before the sender signs. -2. After the sender signs, call `load_relayer_wallet(&args.relayer)?`. -3. If a relayer wallet is present, validate its address matches `tx.relayer`, then call `wallet.sign_tx(&tx)` and write the result to `tx.relayer_signature`. - -**File:** `framework/meta/src/cli/cli_args_tx.rs` — extend `SignArgs`: - -```rust -#[derive(Clone, PartialEq, Eq, Debug, Args)] -pub struct SignArgs { - #[arg(long)] - pub infile: PathBuf, - - #[command(flatten)] - pub gateway: GatewayArgs, - - #[command(flatten)] - pub sender: SenderArgs, // existing - - #[command(flatten)] - pub relayer: RelayerArgs, // NEW - - // ... existing send/wait_result/outfile -} -``` - -In `tx_cli_sign.rs`: -- After the sender signs (existing behaviour), optionally load the relayer wallet and add `relayer_signature`. - -### 2d. New `tx relay` subcommand - -This is a dedicated command for the relayer party: they receive a pre-built, sender-signed transaction file and add their signature. - -**`cli_args_tx.rs`** — add variant to `TxCliAction`: - -```rust -#[command(about = "Adds the relayer signature to a previously signed transaction.")] -Relay(RelayArgs), -``` - -**New `RelayArgs` struct:** - -```rust -#[derive(Clone, PartialEq, Eq, Debug, Args)] -pub struct RelayArgs { - /// Path to the input transaction file (must already have sender signature and relayer field set). - #[arg(long)] - pub infile: PathBuf, - - #[command(flatten)] - pub relayer: RelayerArgs, - - #[command(flatten)] - pub gateway: GatewayArgs, - - /// If set, the transaction is broadcast after signing. - #[arg(long, default_value = "false")] - pub send: bool, - - /// Wait for the transaction result. Requires --send. - #[arg(long, default_value = "false", requires = "send")] - pub wait_result: bool, - - /// Path to write the output to. Defaults to stdout. - #[arg(long)] - pub outfile: Option, -} -``` - -**New file:** `framework/meta/src/cmd/tx/tx_cli_relay.rs` - -Logic (`tx_relay_inner`): -1. Load `RelayerArgs` wallet — error if no wallet provided. -2. Load transaction from `--infile`. -3. Validate `tx.relayer` is set. -4. Validate relayer wallet address == `tx.relayer`. -5. Validate `tx.signature` is present (sender must have already signed). -6. Call `relayer_wallet.sign_tx(&tx)` and set `tx.relayer_signature`. -7. Save / broadcast via existing `save_output` / `broadcast_and_save` helpers. - -**`tx.rs`** — wire up: - -```rust -TxCliAction::Relay(relay_args) => tx_relay(relay_args).await, -``` - ---- - -## Phase 3 — Tx environment trait for relayer - -**File:** `framework/base/src/types/interaction/tx_env.rs` - -Add a new trait analogous to `TxEnvWithTxHash`, to carry the relayer address through the tx building pipeline: - -```rust -pub trait TxEnvWithRelayer: TxEnv { - fn set_relayer_address(&mut self, relayer: ManagedAddress); - - /// Retrieves the relayer address, while resetting it in self. - fn take_relayer_address(&mut self) -> Option>; -} -``` - -This trait is implemented wherever `TxEnvWithTxHash` is already implemented: - -- **`ScenarioTxEnvData`** (`framework/scenario/src/facade/world_tx/scenario_tx_env.rs`) - Add `relayer: Option>` field; implement the two methods. - -- **`ScenarioEnvExec`** (`framework/scenario/src/facade/world_tx/scenario_exec_call.rs`) - Forward to `self.data`. - -- **`InteractorEnvExec`** (`framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs`) - Forward to `self.data`. - -**`Tx` builder method** (`framework/base/src/types/interaction/tx.rs`) - -Mirroring `.id()` and `.tx_hash()` (which require `Env: TxEnvWithTxHash`), add a `.relayer()` method that requires `Env: TxEnvWithRelayer`: - -```rust -impl Tx -where - Env: TxEnvWithRelayer, - // ... other bounds -{ - /// Sets the relayer address for a relayed v3 transaction. - /// - /// The relayer pays the gas fees. After the sender signs the transaction, - /// the relayer must also sign it (adding `relayerSignature`) before broadcasting. - pub fn relayer(mut self, relayer: A) -> Self - where - A: AnnotatedValue>, - { - let relayer_addr = relayer.into_value(&self.env); - self.env.set_relayer_address(relayer_addr); - self - } -} -``` - -The relayer address stored in the env is read by the interactor execution pipeline (just before signing) to populate `tx.relayer` on the SDK `Transaction`. - ---- - -## Phase 4 — Interactor Framework - -### 4a. `Transaction` struct already extended (Phase 1) - -No further struct changes needed here. - -### 4b. Relayer registration — reuse `sender_map` - -No new map or registration method is needed. The relayer is registered via the same `register_wallet` / `register_wallets` path as any other sender. The `InteractorBase` simply stores the chosen relayer address: - -```rust -pub relayer_address: Option
, // added to InteractorBase -``` - -A dedicated setter: -```rust -pub fn set_relayer(&mut self, address: Address) { - // address must already be registered in sender_map - assert!(self.sender_map.contains_key(&address), "relayer wallet not registered"); - self.relayer_address = Some(address); -} -``` - -Users register the wallet normally and then call `set_relayer`: -```rust -let relayer_addr = interactor.register_wallet(relayer_wallet).await; -interactor.set_relayer(relayer_addr); -``` - -### 4c. `InteractorConfig` trait — no change - -**File:** `framework/snippets/src/config/interactor_config.rs` - -The `InteractorConfig` trait is **not** extended. There is no `relayer_wallet()` method. Contract-specific `Config` structs include a relayer wallet as a regular `WalletConfig` field (e.g. `pub relayer: WalletConfig`) and return it inside `register_wallets()`. The interactor setup code then calls `set_relayer` explicitly after loading the config. - -Example in contract interactor setup code: -```rust -let relayer_addr = interactor.register_wallet(config.relayer.wallet().clone()).await; -interactor.set_relayer(relayer_addr); -``` - -### 4d. Transaction dispatch - -**File:** `framework/snippets/src/interactor/interactor_sender.rs` - -After the sender signs, if `relayer_address` is set on the interactor: -1. `tx.relayer` is populated from the env (via `take_relayer_address`, or from `interactor.relayer_address`). -2. `sign_tx_as_relayer` signs using the relayer's entry in `sender_map`: - -```rust -pub(crate) fn sign_tx_as_relayer(&self, tx: &mut Transaction) { - let Some(relayer_address) = &self.relayer_address else { return }; - let relayer = self.sender_map - .get(relayer_address) - .expect("relayer wallet not registered"); - let sig = relayer.wallet.sign_tx(tx); - tx.relayer_signature = Some(sig); -} -``` - ---- - -## Summary of new files - -| File | Purpose | -|---|---| -| `framework/meta/src/cmd/tx/tx_cli_relay.rs` | Implementation of `tx relay` command | - -## Summary of modified files - -| File | Change | -|---|---| -| `sdk/core/src/data/transaction/transaction_request.rs` | Add `relayer`, `relayer_signature` fields | -| `framework/base/src/types/interaction/tx_env.rs` | Add `TxEnvWithRelayer` trait | -| `framework/base/src/types/interaction/tx.rs` | Add `.relayer()` method on `Tx` (requires `Env: TxEnvWithRelayer`) | -| `framework/scenario/src/facade/world_tx/scenario_tx_env.rs` | Add `relayer` field to `ScenarioTxEnvData`; implement `TxEnvWithRelayer` | -| `framework/scenario/src/facade/world_tx/scenario_exec_call.rs` | Implement `TxEnvWithRelayer` for `ScenarioEnvExec` | -| `framework/snippets/src/interactor/interactor_tx/interactor_exec_env.rs` | Implement `TxEnvWithRelayer` for `InteractorEnvExec` | -| `framework/meta/src/cli/cli_args_sender.rs` | Add `RelayerArgs`, `load_relayer_wallet` | -| `framework/meta/src/cli/cli_args_tx.rs` | Add `--relayer` to `TxArgs`; add `RelayerArgs` flatten to `DeployArgs`/`CallArgs`/`UpgradeArgs`/`NewArgs`/`SignArgs`; add `RelayArgs` and `TxCliAction::Relay` | -| `framework/meta/src/cmd/tx/tx.rs` | Wire `TxCliAction::Relay` | -| `framework/meta/src/cmd/tx/tx_cli_new.rs` | Set `tx.relayer`, sign as relayer if wallet provided | -| `framework/meta/src/cmd/tx/tx_cli_deploy.rs` | Same | -| `framework/meta/src/cmd/tx/tx_cli_call.rs` | Same | -| `framework/meta/src/cmd/tx/tx_cli_upgrade.rs` | Same | -| `framework/meta/src/cmd/tx/tx_cli_sign.rs` | Optionally sign as relayer if `RelayerArgs` provided | -| `framework/snippets/src/interactor/interactor_base.rs` | Add `relayer_address: Option
` field | -| `framework/snippets/src/interactor/interactor_sender.rs` | Add `set_relayer`, `sign_tx_as_relayer` | - ---- - -## Key design decisions - -1. **`RelayerArgs` mirrors `SenderArgs`** — same fields, same loading logic, different prefix (`--relayer-pem` etc.). This follows the Python CLI pattern exactly. - -2. **Reuse `sender_map` for relayers** — no new map, no new registration path. A relayer is just another registered wallet identified by its address. - -3. **No `relayer_wallet()` in `InteractorConfig`** — the trait is not extended. Relayer wallets are returned by `register_wallets()` alongside senders. The interactor setup code calls `set_relayer(address)` explicitly. - -4. **`TxEnvWithRelayer` as a first-class trait** — mirrors the `TxEnvWithTxHash` pattern. The relayer address is carried in the environment, not as a separate parameter, so the tx builder `.relayer()` method integrates cleanly into the existing `Tx` chain. - -5. **`--relayer` address vs `--relayer-pem`** — the address flag lets the sender declare the intended relayer without providing the key (two-step workflow). The wallet flags are only needed when both signatures happen in one command or via `tx relay`. - -6. **Two-step vs one-step CLI workflow:** - - *One-step:* `sc-meta tx new --relayer erd1... --relayer-pem relayer.pem ...` — both signatures in one go. - - *Two-step:* `sc-meta tx new --relayer erd1... ...` (sender signs, saves to file) → `sc-meta tx relay --infile tx.json --relayer-pem relayer.pem` (relayer signs, broadcasts). - -7. **Gas limit** — the caller is responsible for providing the correct gas limit including the extra base cost. No automatic adjustment is added at this stage; documentation/comments will explain the formula. - -8. **Broadcast validation** — `broadcast_and_save` should additionally check that if `tx.relayer` is set, `tx.relayer_signature` is also present before broadcasting. From bcf82c8653970838e8c4c7cf0f0e9e610c8e5e42 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 13 Aug 2026 19:07:38 +0300 Subject: [PATCH 178/195] sc-meta CLI chain simulator mode auto-config, refactor --- framework/meta/src/cmd/tx/tx_cli_call.rs | 12 ++++-------- framework/meta/src/cmd/tx/tx_cli_common.rs | 18 ++++++++++++++++-- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 12 ++++-------- framework/meta/src/cmd/tx/tx_cli_new.rs | 10 +++------- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 14 ++++---------- .../snippets/src/interactor/interactor_base.rs | 12 ++++++++++++ 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 473879c2de..69fbe6f745 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -1,10 +1,10 @@ use anyhow::Result; -use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; +use multiversx_sc_snippets::imports::{Bech32Address, InteractorIntoSdkTransaction}; use super::parse_payments::parse_all_payment_args; use super::tx_cli_common::{ - apply_gas_price, build_arg_buffer, load_relayer_for_interactor, load_wallet, sign_and_dispatch, - validate_chain_id, + build_arg_buffer, create_interactor, load_relayer_for_interactor, load_wallet, + sign_and_dispatch, }; use crate::cli::cli_args_tx::CallArgs; @@ -19,15 +19,11 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { let sender_wallet = load_wallet(&args.sender)?; // Create the interactor – fetches network config. - let mut interactor = Interactor::new(&args.gateway.proxy).await; - interactor.override_next_tx_nonce = args.tx.nonce; + let mut interactor = create_interactor(&args.gateway, &args.tx).await?; let sender_address = interactor.register_wallet(sender_wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - apply_gas_price(&mut interactor, &args.tx); - validate_chain_id(&interactor, &args.gateway)?; - let contract = Bech32Address::try_from_bech32_string(args.contract.clone())?; // Build call transaction. diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 25178a2236..8ab651c0d8 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -143,13 +143,27 @@ pub fn build_arg_buffer(arguments: &[String]) -> Result Result { + let mut interactor = Interactor::empty() + .with_connection(&gateway.proxy) + .await + .use_chain_simulator_auto(); + interactor.override_next_tx_nonce = tx.nonce; + apply_gas_price(&mut interactor, tx); + validate_chain_id(&interactor, gateway)?; + Ok(interactor) +} + +fn apply_gas_price(interactor: &mut Interactor, tx_args: &TxArgs) { if let Some(gas_price) = tx_args.gas_price { interactor.gas_price = gas_price; } } -pub fn validate_chain_id(interactor: &Interactor, gateway_args: &GatewayArgs) -> Result<()> { +fn validate_chain_id(interactor: &Interactor, gateway_args: &GatewayArgs) -> Result<()> { if let Some(chain_id) = &gateway_args.chain { interactor.validate_chain_id(chain_id)?; } diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index a868c90a54..a1f90818e4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -2,12 +2,12 @@ use std::fs; use anyhow::{Context, Result}; use multiversx_sc::chain_core::std::new_address::compute_new_address_bech32; -use multiversx_sc_snippets::imports::{BytesValue, Interactor, InteractorIntoSdkTransaction}; +use multiversx_sc_snippets::imports::{BytesValue, InteractorIntoSdkTransaction}; use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ - apply_gas_price, build_arg_buffer, load_relayer_for_interactor, load_wallet, sign_and_dispatch, - validate_chain_id, + build_arg_buffer, create_interactor, load_relayer_for_interactor, load_wallet, + sign_and_dispatch, }; use crate::cli::cli_args_tx::DeployArgs; @@ -22,16 +22,12 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { let sender_wallet = load_wallet(&args.sender)?; // Create the interactor – this fetches the network config in the process. - let mut interactor = Interactor::new(&args.gateway.proxy).await; - interactor.override_next_tx_nonce = args.tx.nonce; + let mut interactor = create_interactor(&args.gateway, &args.tx).await?; let sender_address = interactor .register_wallet_bech32(sender_wallet.clone()) .await; let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - apply_gas_price(&mut interactor, &args.tx); - validate_chain_id(&interactor, &args.gateway)?; - // Read bytecode file and wrap in BytesValue so it implements TxCodeValue. let bytecode = fs::read(&args.bytecode) .with_context(|| format!("failed to read bytecode from {}", args.bytecode.display()))?; diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index d3a5af8a16..d091523740 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -2,11 +2,11 @@ use std::fs; use anyhow::{Context, Result}; use multiversx_chain_core::std::base64_encode; -use multiversx_sc_snippets::imports::{Bech32Address, Interactor, InteractorIntoSdkTransaction}; +use multiversx_sc_snippets::imports::{Bech32Address, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; use crate::cmd::tx::tx_cli_common::{ - apply_gas_price, load_relayer_for_interactor, load_wallet, sign_and_dispatch, validate_chain_id, + create_interactor, load_relayer_for_interactor, load_wallet, sign_and_dispatch, }; use super::parse_payments::parse_all_payment_args; @@ -23,16 +23,12 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { let receiver = Bech32Address::try_from_bech32_string(args.receiver.clone())?; // Create the interactor – this fetches the network config in the process. - let mut interactor = Interactor::new(&args.gateway.proxy).await; - interactor.override_next_tx_nonce = args.tx.nonce; + let mut interactor = create_interactor(&args.gateway, &args.tx).await?; let sender_address = interactor .register_wallet_bech32(sender_wallet.clone()) .await; let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - apply_gas_price(&mut interactor, &args.tx); - validate_chain_id(&interactor, &args.gateway)?; - // Build Transaction via unified Tx syntax (resembles interactor code). let payments = parse_all_payment_args(&args.payment)?; let mut tx = interactor diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index c69c5ad554..ef884ba8fb 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -2,14 +2,12 @@ use std::fs; use anyhow::{Context, Result}; use multiversx_sc_scenario::imports::NotPayable; -use multiversx_sc_snippets::imports::{ - Bech32Address, BytesValue, Interactor, InteractorIntoSdkTransaction, -}; +use multiversx_sc_snippets::imports::{Bech32Address, BytesValue, InteractorIntoSdkTransaction}; use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ - apply_gas_price, build_arg_buffer, load_relayer_for_interactor, load_wallet, sign_and_dispatch, - validate_chain_id, + build_arg_buffer, create_interactor, load_relayer_for_interactor, load_wallet, + sign_and_dispatch, }; use crate::cli::cli_args_tx::UpgradeArgs; @@ -24,15 +22,11 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { let wallet = load_wallet(&args.sender)?; // Create the interactor – this fetches the network config. - let mut interactor = Interactor::new(&args.gateway.proxy).await; - interactor.override_next_tx_nonce = args.tx.nonce; + let mut interactor = create_interactor(&args.gateway, &args.tx).await?; let sender_address = interactor.register_wallet(wallet).await; let sender_bech32 = sender_address.to_bech32(interactor.get_hrp()); let relayer_address_opt = load_relayer_for_interactor(&mut interactor, &args.relayer).await?; - apply_gas_price(&mut interactor, &args.tx); - validate_chain_id(&interactor, &args.gateway)?; - let contract = Bech32Address::try_from_bech32_string(args.contract.clone())?; // Read bytecode. diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 945e68698c..1225259d5a 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -28,6 +28,7 @@ use crate::{ const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; pub const INTERACTOR_SCENARIO_TRACE_PATH: &str = "interactor_trace.scen.json"; pub const INTERACTOR_SET_STATE_PATH: &str = "set_state.json"; +pub const CHAIN_SIMULATOR_CHAIN_ID: &str = "chain"; /// Holds the gateway connection and chain network configuration together. /// @@ -301,6 +302,17 @@ where self } + /// Enables or disables chain-simulator mode automatically, based on the chain ID + /// reported by the gateway: the chain simulator reports chain ID [`CHAIN_SIMULATOR_CHAIN_ID`]. + /// + /// # Panics + /// + /// Panics if connection has not been initialized. + pub fn use_chain_simulator_auto(mut self) -> Self { + self.use_chain_simulator = self.network_config().chain_id == CHAIN_SIMULATOR_CHAIN_ID; + self + } + /// Registers a wallet as a transaction sender and funds it in simulator mode. /// /// Returns the wallet address. From 050cc0e42de7973249628e038eb012f28b921a0c Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 01:32:57 +0300 Subject: [PATCH 179/195] moved sign & dispatch functions from sc-meta CLI to interactor --- framework/meta/src/cmd/tx.rs | 1 - framework/meta/src/cmd/tx/output.rs | 31 ---- framework/meta/src/cmd/tx/tx_cli_call.rs | 11 +- framework/meta/src/cmd/tx/tx_cli_common.rs | 154 +----------------- framework/meta/src/cmd/tx/tx_cli_deploy.rs | 17 +- framework/meta/src/cmd/tx/tx_cli_new.rs | 14 +- framework/meta/src/cmd/tx/tx_cli_query.rs | 6 +- framework/meta/src/cmd/tx/tx_cli_relay.rs | 40 ++--- framework/meta/src/cmd/tx/tx_cli_send.rs | 35 +--- framework/meta/src/cmd/tx/tx_cli_sign.rs | 42 ++--- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 11 +- framework/snippets/src/interactor.rs | 3 + .../src/interactor/interactor_dispatch.rs | 108 ++++++++++++ .../snippets/src/interactor/tx_output_file.rs | 80 +++++++++ 14 files changed, 262 insertions(+), 291 deletions(-) delete mode 100644 framework/meta/src/cmd/tx/output.rs create mode 100644 framework/snippets/src/interactor/interactor_dispatch.rs create mode 100644 framework/snippets/src/interactor/tx_output_file.rs diff --git a/framework/meta/src/cmd/tx.rs b/framework/meta/src/cmd/tx.rs index 907e771a0a..66f4db35c0 100644 --- a/framework/meta/src/cmd/tx.rs +++ b/framework/meta/src/cmd/tx.rs @@ -1,4 +1,3 @@ -mod output; mod parse_code_metadata; mod parse_payments; mod tx_cli_call; diff --git a/framework/meta/src/cmd/tx/output.rs b/framework/meta/src/cmd/tx/output.rs deleted file mode 100644 index 7476f70383..0000000000 --- a/framework/meta/src/cmd/tx/output.rs +++ /dev/null @@ -1,31 +0,0 @@ -use multiversx_sc_snippets::sdk::data::transaction::{ApiTransactionResult, Transaction}; -use serde::{Deserialize, Serialize}; - -/// mxpy-compatible output format for a signed transaction. -/// -/// When saved to a file this can be re-loaded by `sc-meta tx send --infile ` -/// or by `mxpy tx send --infile `. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TxOutputFile { - #[serde(rename = "emittedTransaction")] - pub emitted_transaction: Transaction, - - /// Decoded (plain-text / UTF-8) representation of the transaction data field. - #[serde(rename = "emittedTransactionData")] - pub emitted_transaction_data: String, - - /// Populated after a successful broadcast; empty string when just serializing. - #[serde(rename = "emittedTransactionHash")] - pub emitted_transaction_hash: String, - - /// Populated for deploy transactions: the deterministically-computed contract address. - #[serde(rename = "contractAddress", skip_serializing_if = "Option::is_none")] - pub contract_address: Option, - - /// Populated after waiting for the transaction result on-network. - #[serde( - rename = "transactionOnNetwork", - skip_serializing_if = "Option::is_none" - )] - pub transaction_on_network: Option, -} diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 69fbe6f745..9c2630c8ae 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -4,7 +4,6 @@ use multiversx_sc_snippets::imports::{Bech32Address, InteractorIntoSdkTransactio use super::parse_payments::parse_all_payment_args; use super::tx_cli_common::{ build_arg_buffer, create_interactor, load_relayer_for_interactor, load_wallet, - sign_and_dispatch, }; use crate::cli::cli_args_tx::CallArgs; @@ -42,5 +41,13 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { .into_sdk_transaction(); interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, &args.tx, None).await + interactor + .sign_and_dispatch( + tx, + None, + args.tx.send, + args.tx.wait_result, + args.tx.outfile.as_deref(), + ) + .await } diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx/tx_cli_common.rs index 8ab651c0d8..d097822de6 100644 --- a/framework/meta/src/cmd/tx/tx_cli_common.rs +++ b/framework/meta/src/cmd/tx/tx_cli_common.rs @@ -1,24 +1,17 @@ use std::fs; use anyhow::{Context, Result, anyhow}; -use multiversx_chain_core::std::base64_decode; use multiversx_sc::imports::Bech32Address; -use multiversx_sc_snippets::ExplorerUrl; use multiversx_sc_snippets::{ - hex, imports::{ - BytesValue, GatewayHttpProxy, Interactor, InterpretableFrom, InterpreterContext, - ManagedArgBuffer, ManagedBuffer, StaticApi, + BytesValue, Interactor, InterpretableFrom, InterpreterContext, ManagedArgBuffer, + ManagedBuffer, StaticApi, }, - sdk::data::transaction::{ApiTransactionResult, Transaction}, + sdk::data::transaction::Transaction, }; -use serde::Serialize; -use multiversx_sc_scenario::imports::ReturnCode; -use multiversx_sc_snippets::network_response; use serde_json::Value; -use super::output::TxOutputFile; pub use crate::cli::cli_args_sender::{load_relayer_wallet, load_wallet}; use crate::cli::cli_args_tx::{GatewayArgs, RelayerArgs, TxArgs}; @@ -44,92 +37,6 @@ pub(super) fn load_transaction_from_file(path: &std::path::Path) -> Result Result<(ApiTransactionResult, ReturnCode)> { - let proxy = GatewayHttpProxy::new(gateway.to_string()); - multiversx_sdk::retrieve_tx_on_network(&proxy, tx_hash.to_string()).await -} - -/// Write `output` to `outfile`, or print to stdout when no outfile is given. -pub(super) fn save_output(output: &TxOutputFile, outfile: Option<&std::path::Path>) -> Result<()> { - let json = to_json_pretty(output)?; - if let Some(path) = outfile { - fs::write(path, &json).with_context(|| format!("failed to write to {}", path.display()))?; - println!("Transaction saved to {}", path.display()); - } else { - println!("{json}"); - } - Ok(()) -} - -/// Broadcast the transaction inside `output`, update the hash (and optionally -/// the on-network result), then write/print the updated output. -pub(super) async fn broadcast_and_save( - output: TxOutputFile, - proxy_url: &str, - outfile: Option<&std::path::Path>, - wait_result: bool, -) -> Result<()> { - if output.emitted_transaction.signature.is_none() { - return Err(anyhow!( - "transaction is not signed; sign it before broadcasting" - )); - } - if output.emitted_transaction.relayer.is_some() - && output.emitted_transaction.relayer_signature.is_none() - { - return Err(anyhow!( - "relayed transaction is missing relayer signature; use `tx relay` to add it" - )); - } - - let proxy = GatewayHttpProxy::new(proxy_url.to_string()); - let tx_hash = proxy - .send_transaction(&output.emitted_transaction) - .await - .context("failed to broadcast transaction")?; - if let Some(ex) = ExplorerUrl::from_chain_id(&output.emitted_transaction.chain_id) { - println!("transaction: {}", ex.tx_url(&tx_hash)); - } else { - println!("transaction hash: {tx_hash}"); - } - - let mut output_with_hash = TxOutputFile { - emitted_transaction_hash: tx_hash.clone(), - ..output - }; - - if wait_result { - println!("Waiting for transaction result..."); - let (tx_on_network, return_code) = fetch_tx_on_network(proxy_url, &tx_hash).await?; - let tx_response = network_response::parse_tx_response(tx_on_network.clone(), return_code); - print_tx_results(&tx_response); - output_with_hash.transaction_on_network = Some(tx_on_network); - } - - let json = to_json_pretty(&output_with_hash)?; - if let Some(path) = outfile { - fs::write(path, &json).with_context(|| format!("failed to write to {}", path.display()))?; - } else { - println!("{json}"); - } - Ok(()) -} - -/// Serialize a value to a JSON string with 4-space indentation (matches mxpy output). -pub(super) fn to_json_pretty(value: &T) -> Result { - let mut buf = Vec::new(); - let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter); - value - .serialize(&mut ser) - .context("failed to serialize transaction")?; - String::from_utf8(buf).context("non-UTF8 in serialized JSON") -} - /// Interpret a list of mandos-format argument strings (e.g. `0x1a`, `str:hello`, `42`) /// into a `ManagedArgBuffer` ready to be passed to `.arguments_raw()`. pub fn build_arg_buffer(arguments: &[String]) -> Result> { @@ -202,61 +109,6 @@ pub async fn load_relayer_for_interactor( } } -/// Sign the nonce-prepared transaction, then write / print / broadcast it -/// according to the `TxArgs` flags. -/// `contract_address` should be `Some(bech32)` for deploy transactions. -/// The sender wallet and any relayer wallet must already be registered with `interactor`. -/// A relayer address without a registered wallet is preserved without a relayer signature. -pub async fn sign_and_dispatch( - interactor: &Interactor, - mut tx: Transaction, - tx_args: &TxArgs, - contract_address: Option, -) -> Result<()> { - let decoded_data = match &tx.data { - None => String::new(), - Some(d) => { - let bytes = base64_decode(d)?; - String::from_utf8_lossy(&bytes).into_owned() - } - }; - - interactor.sign_tx(&mut tx); - - let output = TxOutputFile { - emitted_transaction: tx, - emitted_transaction_data: decoded_data, - emitted_transaction_hash: String::new(), - contract_address, - transaction_on_network: None, - }; - - if tx_args.send { - broadcast_and_save( - output, - interactor.gateway_uri(), - tx_args.outfile.as_deref(), - tx_args.wait_result, - ) - .await?; - } else { - save_output(&output, tx_args.outfile.as_deref())?; - } - Ok(()) -} - -/// Print the status and hex-encoded return values of a completed transaction. -pub(super) fn print_tx_results(tx_response: &multiversx_sc_scenario::scenario_model::TxResponse) { - if tx_response.tx_error.is_success() { - println!("Transaction successful."); - } else { - println!("Transaction failed: {}", tx_response.tx_error); - } - for (i, result) in tx_response.out.iter().enumerate() { - println!("Result[{i}]: 0x{}", hex::encode(result)); - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index a1f90818e4..9fef440bb4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -7,7 +7,6 @@ use multiversx_sc_snippets::imports::{BytesValue, InteractorIntoSdkTransaction}; use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ build_arg_buffer, create_interactor, load_relayer_for_interactor, load_wallet, - sign_and_dispatch, }; use crate::cli::cli_args_tx::DeployArgs; @@ -60,11 +59,13 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { println!("new contract address: {contract_address}"); } - sign_and_dispatch( - &interactor, - tx, - &args.tx, - Some(contract_address.to_bech32_string()), - ) - .await + interactor + .sign_and_dispatch( + tx, + Some(contract_address.to_bech32_string()), + args.tx.send, + args.tx.wait_result, + args.tx.outfile.as_deref(), + ) + .await } diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index d091523740..9ce61471de 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -5,9 +5,7 @@ use multiversx_chain_core::std::base64_encode; use multiversx_sc_snippets::imports::{Bech32Address, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; -use crate::cmd::tx::tx_cli_common::{ - create_interactor, load_relayer_for_interactor, load_wallet, sign_and_dispatch, -}; +use crate::cmd::tx::tx_cli_common::{create_interactor, load_relayer_for_interactor, load_wallet}; use super::parse_payments::parse_all_payment_args; @@ -47,7 +45,15 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { } interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, &args.tx, None).await + interactor + .sign_and_dispatch( + tx, + None, + args.tx.send, + args.tx.wait_result, + args.tx.outfile.as_deref(), + ) + .await } // --------------------------------------------------------------------------- diff --git a/framework/meta/src/cmd/tx/tx_cli_query.rs b/framework/meta/src/cmd/tx/tx_cli_query.rs index 84bba108cf..916e549dbf 100644 --- a/framework/meta/src/cmd/tx/tx_cli_query.rs +++ b/framework/meta/src/cmd/tx/tx_cli_query.rs @@ -1,11 +1,11 @@ use anyhow::{Context, Result, anyhow}; use multiversx_sc_snippets::{ hex, - imports::{Bech32Address, GatewayHttpProxy}, + imports::{Bech32Address, GatewayHttpProxy, Interactor}, sdk::data::vm::VMQueryInput, }; -use super::tx_cli_common::{build_arg_buffer, to_json_pretty}; +use super::tx_cli_common::build_arg_buffer; use crate::cli::cli_args_tx::QueryArgs; pub async fn tx_query(args: &QueryArgs) { @@ -53,7 +53,7 @@ async fn tx_query_inner(args: &QueryArgs) -> Result<()> { .map(|bytes| hex::encode(&bytes)) .collect(); - let json = to_json_pretty(&return_data)?; + let json = Interactor::to_json_pretty(&return_data)?; println!("{json}"); Ok(()) } diff --git a/framework/meta/src/cmd/tx/tx_cli_relay.rs b/framework/meta/src/cmd/tx/tx_cli_relay.rs index 0c6342faf5..cd3f0ebe4f 100644 --- a/framework/meta/src/cmd/tx/tx_cli_relay.rs +++ b/framework/meta/src/cmd/tx/tx_cli_relay.rs @@ -1,12 +1,7 @@ use anyhow::{Result, anyhow}; -use multiversx_chain_core::std::base64_decode; +use multiversx_sc_snippets::{Interactor, TxOutputFile}; -use super::{ - output::TxOutputFile, - tx_cli_common::{ - broadcast_and_save, load_relayer_wallet, load_transaction_from_file, save_output, - }, -}; +use super::tx_cli_common::{load_relayer_wallet, load_transaction_from_file}; use crate::cli::cli_args_tx::RelayArgs; pub async fn tx_relay(args: &RelayArgs) { @@ -53,32 +48,17 @@ async fn tx_relay_inner(args: &RelayArgs) -> Result<()> { let relayer_sig = relayer_wallet.sign_tx(&tx)?; tx.relayer_signature = Some(relayer_sig); - let decoded_data = match &tx.data { - None => String::new(), - Some(d) => { - let bytes = base64_decode(d)?; - String::from_utf8_lossy(&bytes).into_owned() - } - }; - - let output = TxOutputFile { - emitted_transaction: tx, - emitted_transaction_data: decoded_data, - emitted_transaction_hash: String::new(), - contract_address: None, - transaction_on_network: None, - }; + let output = TxOutputFile::from_transaction(tx, None)?; if args.send { - broadcast_and_save( - output, - &args.gateway.proxy, - args.outfile.as_deref(), - args.wait_result, - ) - .await?; + let interactor = Interactor::empty() + .with_connection(&args.gateway.proxy) + .await; + interactor + .broadcast_and_save(output, args.outfile.as_deref(), args.wait_result) + .await?; } else { - save_output(&output, args.outfile.as_deref())?; + output.save_output(args.outfile.as_deref())?; } Ok(()) } diff --git a/framework/meta/src/cmd/tx/tx_cli_send.rs b/framework/meta/src/cmd/tx/tx_cli_send.rs index d21226c870..60f5561e4b 100644 --- a/framework/meta/src/cmd/tx/tx_cli_send.rs +++ b/framework/meta/src/cmd/tx/tx_cli_send.rs @@ -1,10 +1,7 @@ use anyhow::Result; -use multiversx_chain_core::std::base64_decode; +use multiversx_sc_snippets::{Interactor, TxOutputFile}; -use super::{ - output::TxOutputFile, - tx_cli_common::{broadcast_and_save, load_transaction_from_file}, -}; +use super::tx_cli_common::load_transaction_from_file; use crate::cli::cli_args_tx::SendArgs; pub async fn tx_send(args: &SendArgs) { @@ -17,27 +14,9 @@ pub async fn tx_send(args: &SendArgs) { async fn tx_send_inner(args: &SendArgs) -> Result<()> { let tx = load_transaction_from_file(&args.infile)?; - let decoded_data = match &tx.data { - None => String::new(), - Some(d) => { - let bytes = base64_decode(d)?; - String::from_utf8_lossy(&bytes).into_owned() - } - }; - - let output = TxOutputFile { - emitted_transaction: tx, - emitted_transaction_data: decoded_data, - emitted_transaction_hash: String::new(), - contract_address: None, - transaction_on_network: None, - }; - - broadcast_and_save( - output, - &args.proxy, - args.outfile.as_deref(), - args.wait_result, - ) - .await + let output = TxOutputFile::from_transaction(tx, None)?; + let interactor = Interactor::empty().with_connection(&args.proxy).await; + interactor + .broadcast_and_save(output, args.outfile.as_deref(), args.wait_result) + .await } diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index db6a6ced36..1ad4ab6e29 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -1,13 +1,8 @@ use anyhow::{Result, anyhow}; -use multiversx_chain_core::std::{Bech32Address, base64_decode}; +use multiversx_chain_core::std::Bech32Address; +use multiversx_sc_snippets::{Interactor, TxOutputFile}; -use super::{ - output::TxOutputFile, - tx_cli_common::{ - broadcast_and_save, load_relayer_wallet, load_transaction_from_file, load_wallet, - save_output, - }, -}; +use super::tx_cli_common::{load_relayer_wallet, load_transaction_from_file, load_wallet}; use crate::cli::cli_args_tx::SignArgs; pub async fn tx_sign(args: &SignArgs) { @@ -63,32 +58,17 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { tx.relayer_signature = Some(relayer_sig); } - let decoded_data = match &tx.data { - None => String::new(), - Some(d) => { - let bytes = base64_decode(d)?; - String::from_utf8_lossy(&bytes).into_owned() - } - }; - - let output = TxOutputFile { - emitted_transaction: tx, - emitted_transaction_data: decoded_data, - emitted_transaction_hash: String::new(), - contract_address: None, - transaction_on_network: None, - }; + let output = TxOutputFile::from_transaction(tx, None)?; if args.send { - broadcast_and_save( - output, - &args.gateway.proxy, - args.outfile.as_deref(), - args.wait_result, - ) - .await?; + let interactor = Interactor::empty() + .with_connection(&args.gateway.proxy) + .await; + interactor + .broadcast_and_save(output, args.outfile.as_deref(), args.wait_result) + .await?; } else { - save_output(&output, args.outfile.as_deref())?; + output.save_output(args.outfile.as_deref())?; } Ok(()) } diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index ef884ba8fb..3480b4e4d8 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -7,7 +7,6 @@ use multiversx_sc_snippets::imports::{Bech32Address, BytesValue, InteractorIntoS use super::parse_code_metadata::parse_code_metadata; use super::tx_cli_common::{ build_arg_buffer, create_interactor, load_relayer_for_interactor, load_wallet, - sign_and_dispatch, }; use crate::cli::cli_args_tx::UpgradeArgs; @@ -52,5 +51,13 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { .into_sdk_transaction(); interactor.set_tx_nonce_update_sender(&mut tx).await; - sign_and_dispatch(&interactor, tx, &args.tx, None).await + interactor + .sign_and_dispatch( + tx, + None, + args.tx.send, + args.tx.wait_result, + args.tx.outfile.as_deref(), + ) + .await } diff --git a/framework/snippets/src/interactor.rs b/framework/snippets/src/interactor.rs index 3c58e13b17..424d0f0a4e 100644 --- a/framework/snippets/src/interactor.rs +++ b/framework/snippets/src/interactor.rs @@ -1,13 +1,16 @@ mod explorer_url; mod interactor_base; mod interactor_chain_simulator; +mod interactor_dispatch; mod interactor_dns; mod interactor_scenario; mod interactor_sender; mod interactor_tx; +mod tx_output_file; pub use explorer_url::ExplorerUrl; pub use interactor_base::*; pub use interactor_dns::*; pub use interactor_sender::*; pub use interactor_tx::*; +pub use tx_output_file::TxOutputFile; diff --git a/framework/snippets/src/interactor/interactor_dispatch.rs b/framework/snippets/src/interactor/interactor_dispatch.rs new file mode 100644 index 0000000000..29ebacf90f --- /dev/null +++ b/framework/snippets/src/interactor/interactor_dispatch.rs @@ -0,0 +1,108 @@ +use crate::{InteractorBase, sdk::data::transaction::Transaction}; +use anyhow::Context; +use multiversx_sdk::gateway::GatewayAsyncService; +use serde::Serialize; + +use super::{ExplorerUrl, TxOutputFile}; +use std::path::Path; + +impl InteractorBase +where + GatewayProxy: GatewayAsyncService, +{ + /// Serialize a value to JSON with 4-space indentation (matching mxpy output). + pub fn to_json_pretty(value: &T) -> anyhow::Result { + to_json_pretty(value) + } + + /// Broadcast the transaction inside `output`, update the hash (and optionally + /// the on-network result), then write/print the updated output. + pub async fn broadcast_and_save( + &self, + output: TxOutputFile, + outfile: Option<&Path>, + wait_result: bool, + ) -> anyhow::Result<()> { + if output.emitted_transaction.signature.is_none() { + anyhow::bail!("transaction is not signed; sign it before broadcasting"); + } + if output.emitted_transaction.relayer.is_some() + && output.emitted_transaction.relayer_signature.is_none() + { + anyhow::bail!( + "relayed transaction is missing relayer signature; use `tx relay` to add it" + ); + } + + let tx_hash = self + .proxy() + .request(multiversx_sdk::gateway::SendTxRequest( + &output.emitted_transaction, + )) + .await + .context("failed to broadcast transaction")?; + if let Some(explorer_url) = ExplorerUrl::from_chain_id(&output.emitted_transaction.chain_id) + { + println!("transaction: {}", explorer_url.tx_url(&tx_hash)); + } else { + println!("transaction hash: {tx_hash}"); + } + + let mut output_with_hash = TxOutputFile { + emitted_transaction_hash: tx_hash.clone(), + ..output + }; + + if wait_result { + println!("Waiting for transaction result..."); + let (tx_on_network, return_code) = + multiversx_sdk::retrieve_tx_on_network(self.proxy(), tx_hash).await?; + let tx_response = + crate::network_response::parse_tx_response(tx_on_network.clone(), return_code); + print_tx_results(&tx_response); + output_with_hash.transaction_on_network = Some(tx_on_network); + } + + output_with_hash.save_output(outfile) + } + + /// Sign `transaction`, then write / print / broadcast it according to the supplied flags. + pub async fn sign_and_dispatch( + &self, + mut transaction: Transaction, + contract_address: Option, + send: bool, + wait_result: bool, + outfile: Option<&Path>, + ) -> anyhow::Result<()> { + self.sign_tx(&mut transaction); + let output = TxOutputFile::from_transaction(transaction, contract_address)?; + + if send { + self.broadcast_and_save(output, outfile, wait_result).await + } else { + output.save_output(outfile) + } + } +} + +fn to_json_pretty(value: &T) -> anyhow::Result { + let mut buf = Vec::new(); + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut serializer = serde_json::Serializer::with_formatter(&mut buf, formatter); + value + .serialize(&mut serializer) + .context("failed to serialize transaction")?; + String::from_utf8(buf).context("non-UTF8 in serialized JSON") +} + +fn print_tx_results(tx_response: &multiversx_sc_scenario::scenario_model::TxResponse) { + if tx_response.tx_error.is_success() { + println!("Transaction successful."); + } else { + println!("Transaction failed: {}", tx_response.tx_error); + } + for (index, result) in tx_response.out.iter().enumerate() { + println!("Result[{index}]: 0x{}", hex::encode(result)); + } +} diff --git a/framework/snippets/src/interactor/tx_output_file.rs b/framework/snippets/src/interactor/tx_output_file.rs new file mode 100644 index 0000000000..17843cbec9 --- /dev/null +++ b/framework/snippets/src/interactor/tx_output_file.rs @@ -0,0 +1,80 @@ +use anyhow::{Context, Result}; +use multiversx_sdk::{ + chain_core::std::base64_decode, + data::transaction::{ApiTransactionResult, Transaction}, +}; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +/// mxpy-compatible output format for a signed transaction. +/// +/// When saved to a file this can be re-loaded by `sc-meta tx send --infile ` +/// or by `mxpy tx send --infile `. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxOutputFile { + #[serde(rename = "emittedTransaction")] + pub emitted_transaction: Transaction, + + /// Decoded (plain-text / UTF-8) representation of the transaction data field. + #[serde(rename = "emittedTransactionData")] + pub emitted_transaction_data: String, + + /// Populated after a successful broadcast; empty string when just serializing. + #[serde(rename = "emittedTransactionHash")] + pub emitted_transaction_hash: String, + + /// Populated for deploy transactions: the deterministically-computed contract address. + #[serde(rename = "contractAddress", skip_serializing_if = "Option::is_none")] + pub contract_address: Option, + + /// Populated after waiting for the transaction result on-network. + #[serde( + rename = "transactionOnNetwork", + skip_serializing_if = "Option::is_none" + )] + pub transaction_on_network: Option, +} + +impl TxOutputFile { + /// Creates an mxpy-compatible output value from a transaction. + pub fn from_transaction( + emitted_transaction: Transaction, + contract_address: Option, + ) -> Result { + let emitted_transaction_data = match &emitted_transaction.data { + None => String::new(), + Some(data) => String::from_utf8_lossy(&base64_decode(data)?).into_owned(), + }; + + Ok(Self { + emitted_transaction, + emitted_transaction_data, + emitted_transaction_hash: String::new(), + contract_address, + transaction_on_network: None, + }) + } + + /// Write this output to `outfile`, or print to stdout when no outfile is given. + pub fn save_output(&self, outfile: Option<&Path>) -> Result<()> { + let json = to_json_pretty(self)?; + if let Some(path) = outfile { + std::fs::write(path, &json) + .with_context(|| format!("failed to write to {}", path.display()))?; + println!("Transaction saved to {}", path.display()); + } else { + println!("{json}"); + } + Ok(()) + } +} + +fn to_json_pretty(value: &T) -> Result { + let mut buf = Vec::new(); + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut serializer = serde_json::Serializer::with_formatter(&mut buf, formatter); + value + .serialize(&mut serializer) + .context("failed to serialize transaction")?; + String::from_utf8(buf).context("non-UTF8 in serialized JSON") +} From c6ba2f9942f92570ce0152f61b5a89bbbcc54a24 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 01:58:42 +0300 Subject: [PATCH 180/195] interactor - broadcast transaction refactor and dedup --- .../src/interactor/interactor_dispatch.rs | 43 +++++++++++++------ .../interactor_scenario/error_message.rs | 24 ----------- .../interactor_scenario/interactor_sc_call.rs | 33 +++----------- .../interactor_sc_deploy.rs | 33 +++----------- .../interactor_transfer.rs | 20 ++------- 5 files changed, 45 insertions(+), 108 deletions(-) diff --git a/framework/snippets/src/interactor/interactor_dispatch.rs b/framework/snippets/src/interactor/interactor_dispatch.rs index 29ebacf90f..d6ab468876 100644 --- a/framework/snippets/src/interactor/interactor_dispatch.rs +++ b/framework/snippets/src/interactor/interactor_dispatch.rs @@ -15,6 +15,35 @@ where to_json_pretty(value) } + pub async fn broadcast_transaction( + &self, + transaction: &Transaction, + info: &str, + ) -> anyhow::Result { + let result = self + .proxy() + .request(multiversx_sdk::gateway::SendTxRequest(transaction)) + .await; + let Ok(tx_hash) = result else { + let Err(err) = result else { + unreachable!(); + }; + let error = anyhow::anyhow!("{info} error: {err}"); + eprintln!("{error}"); + log::error!("{error}"); + return Err(error); + }; + + log::info!("{info} tx hash: {tx_hash}"); + if let Some(explorer_url) = ExplorerUrl::from_chain_id(&transaction.chain_id) { + println!("{info}: {}", explorer_url.tx_url(&tx_hash)); + } else { + println!("{info} tx hash: {tx_hash}"); + } + + Ok(tx_hash) + } + /// Broadcast the transaction inside `output`, update the hash (and optionally /// the on-network result), then write/print the updated output. pub async fn broadcast_and_save( @@ -35,18 +64,8 @@ where } let tx_hash = self - .proxy() - .request(multiversx_sdk::gateway::SendTxRequest( - &output.emitted_transaction, - )) - .await - .context("failed to broadcast transaction")?; - if let Some(explorer_url) = ExplorerUrl::from_chain_id(&output.emitted_transaction.chain_id) - { - println!("transaction: {}", explorer_url.tx_url(&tx_hash)); - } else { - println!("transaction hash: {tx_hash}"); - } + .broadcast_transaction(&output.emitted_transaction, "transaction") + .await?; let mut output_with_hash = TxOutputFile { emitted_transaction_hash: tx_hash.clone(), diff --git a/framework/snippets/src/interactor/interactor_scenario/error_message.rs b/framework/snippets/src/interactor/interactor_scenario/error_message.rs index 50211b29b1..4eb3c73d1f 100644 --- a/framework/snippets/src/interactor/interactor_scenario/error_message.rs +++ b/framework/snippets/src/interactor/interactor_scenario/error_message.rs @@ -8,14 +8,6 @@ pub(super) fn query_err_message(err: &anyhow::Error) { ); } -pub(super) fn transfer_err_message(err: &anyhow::Error) { - eprintln!( - "{}{}", - "Transfer failed: ".to_string().red().bold(), - err.to_string().red().bold() - ); -} - pub(super) fn simulate_gas_transfer_err_message(err: &anyhow::Error) { eprintln!( "{}{}", @@ -27,14 +19,6 @@ pub(super) fn simulate_gas_transfer_err_message(err: &anyhow::Error) { ); } -pub(super) fn deploy_err_message(err: &anyhow::Error) { - eprintln!( - "{}{}", - "Deploy failed: ".to_string().red().bold(), - err.to_string().red().bold() - ); -} - pub(super) fn simulate_gas_deploy_err_message(err: &anyhow::Error) { eprintln!( "{}{}", @@ -46,14 +30,6 @@ pub(super) fn simulate_gas_deploy_err_message(err: &anyhow::Error) { ); } -pub(crate) fn sc_call_err_message(err: &anyhow::Error) { - eprintln!( - "{}{}", - "Call failed: ".to_string().red().bold(), - err.to_string().red().bold() - ); -} - pub(crate) fn simulate_gas_sc_call_err_message(err: &anyhow::Error) { eprintln!( "{}{}", diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs index f6491db156..0ebdcd4603 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_call.rs @@ -1,6 +1,6 @@ use std::process; -use super::error_message::{sc_call_err_message, simulate_gas_sc_call_err_message}; +use super::error_message::simulate_gas_sc_call_err_message; use crate::{InteractorBase, SimulateGas, network_response}; use multiversx_sc_scenario::{ scenario::ScenarioRunner, @@ -9,10 +9,7 @@ use multiversx_sc_scenario::{ use multiversx_sdk::chain_core::std::base64_encode; use multiversx_sdk::data::transaction::{TransactionOptions, TransactionVersion}; use multiversx_sdk::{data::transaction::Transaction, gateway::SimulateTxRequest}; -use multiversx_sdk::{ - gateway::{GatewayAsyncService, SendTxRequest}, - retrieve_tx_on_network, -}; +use multiversx_sdk::{gateway::GatewayAsyncService, retrieve_tx_on_network}; impl InteractorBase where @@ -33,7 +30,9 @@ where self.pre_runners.run_sc_call_step(sc_call_step); - let tx_hash = self.launch_sc_call(&transaction).await; + let Ok(tx_hash) = self.broadcast_transaction(&transaction, "sc call").await else { + process::exit(1) + }; self.generate_blocks_until_tx_processed(&tx_hash) .await @@ -70,28 +69,6 @@ where transaction } - async fn launch_sc_call(&mut self, transaction: &Transaction) -> String { - let tx_hash = self.proxy().request(SendTxRequest(transaction)).await; - - match tx_hash { - Ok(tx_hash) => { - log::info!("sc call tx hash: {tx_hash}"); - if let Some(ex) = &self.explorer_url { - println!("sc call: {}", ex.tx_url(&tx_hash)); - } else { - println!("sc call tx hash: {tx_hash}"); - } - tx_hash - } - Err(err) => { - println!("sc call error: {err}"); - log::error!("sc call error: {err}"); - sc_call_err_message(&err); - process::exit(1) - } - } - } - async fn sc_call_simulate_transaction(&mut self, transaction: &Transaction) -> u64 { let result = self.proxy().request(SimulateTxRequest(transaction)).await; diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs index 2517a4e83c..7824c1db36 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_sc_deploy.rs @@ -1,6 +1,6 @@ use std::process; -use super::error_message::{deploy_err_message, simulate_gas_deploy_err_message}; +use super::error_message::simulate_gas_deploy_err_message; use crate::{InteractorBase, SimulateGas, network_response}; use multiversx_sc_scenario::{ imports::Bech32Address, @@ -10,10 +10,7 @@ use multiversx_sc_scenario::{ use multiversx_sdk::chain_core::std::base64_encode; use multiversx_sdk::data::transaction::{TransactionOptions, TransactionVersion}; use multiversx_sdk::{data::transaction::Transaction, gateway::SimulateTxRequest}; -use multiversx_sdk::{ - gateway::{GatewayAsyncService, SendTxRequest}, - retrieve_tx_on_network, -}; +use multiversx_sdk::{gateway::GatewayAsyncService, retrieve_tx_on_network}; impl InteractorBase where @@ -55,28 +52,6 @@ where transaction } - async fn launch_sc_deploy(&mut self, transaction: &Transaction) -> String { - let tx_hash_result = self.proxy().request(SendTxRequest(transaction)).await; - - match tx_hash_result { - Ok(tx_hash) => { - log::info!("sc deploy tx hash: {tx_hash}"); - if let Some(ex) = &self.explorer_url { - println!("sc deploy: {}", ex.tx_url(&tx_hash)); - } else { - println!("sc deploy tx hash: {tx_hash}"); - } - tx_hash - } - Err(err) => { - println!("sc deploy error: {err}"); - log::error!("sc deploy error: {err}"); - deploy_err_message(&err); - process::exit(1) - } - } - } - pub async fn sc_deploy(&mut self, sc_deploy_step: &mut ScDeployStep) { let mut transaction = self.sc_deploy_to_blockchain_signed_tx(sc_deploy_step).await; @@ -92,7 +67,9 @@ where self.pre_runners.run_sc_deploy_step(sc_deploy_step); - let tx_hash = self.launch_sc_deploy(&transaction).await; + let Ok(tx_hash) = self.broadcast_transaction(&transaction, "sc deploy").await else { + process::exit(1) + }; self.generate_blocks_until_tx_processed(&tx_hash) .await diff --git a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs index 02514f2d9c..994c06211a 100644 --- a/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs +++ b/framework/snippets/src/interactor/interactor_scenario/interactor_transfer.rs @@ -1,12 +1,11 @@ use std::process; -use super::error_message::{simulate_gas_transfer_err_message, transfer_err_message}; +use super::error_message::simulate_gas_transfer_err_message; use crate::InteractorBase; -use log::info; use multiversx_sc_scenario::{scenario::ScenarioRunner, scenario_model::TransferStep}; use multiversx_sdk::{ data::transaction::Transaction, - gateway::{GatewayAsyncService, SendTxRequest, SimulateTxRequest}, + gateway::{GatewayAsyncService, SimulateTxRequest}, retrieve_tx_on_network, }; @@ -17,24 +16,13 @@ where pub async fn transfer(&mut self, transfer_step: TransferStep) -> String { let transaction = self.launch_transfer(&transfer_step).await; - let tx_hash = match self.proxy().request(SendTxRequest(&transaction)).await { - Ok(hash) => hash, - Err(err) => { - transfer_err_message(&err); - process::exit(1); - } + let Ok(tx_hash) = self.broadcast_transaction(&transaction, "transfer").await else { + process::exit(1) }; self.generate_blocks_until_tx_processed(&tx_hash) .await .unwrap(); - if let Some(ex) = &self.explorer_url { - println!("transfer: {}", ex.tx_url(&tx_hash)); - } else { - println!("transfer tx hash: {tx_hash}"); - } - info!("transfer tx hash: {}", tx_hash); - retrieve_tx_on_network(self.proxy(), tx_hash.clone()) .await .expect("failed to fetch transfer tx result"); From d120c0f066cf34ebdde721bf8190ac0c50c4cee9 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 02:03:43 +0300 Subject: [PATCH 181/195] sc-meta CLI --wait-result inserts blocks on chain simulator, until processed --- framework/meta/tests/cs_tx_cli_test.rs | 5 +++++ framework/snippets/src/interactor/interactor_dispatch.rs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index c42b626439..5c3f83fc3e 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -62,6 +62,7 @@ async fn test_adder_deploy_add_get_sum() { "--arguments", "0", "--send", + "--wait-result", "--outfile", outfile_deploy.to_str().unwrap(), ]) @@ -137,6 +138,7 @@ async fn test_adder_deploy_add_get_sum() { "--arguments", "5", "--send", + "--wait-result", "--outfile", outfile_call.to_str().unwrap(), ]) @@ -215,6 +217,7 @@ async fn test_adder_deploy_add_get_sum() { "--gas-limit", "50000000", "--send", + "--wait-result", "--outfile", outfile_upgrade.to_str().unwrap(), ]) @@ -310,6 +313,7 @@ async fn test_adder_deploy_add_get_sum() { "--arguments", "3", "--send", + "--wait-result", "--outfile", outfile_call_relayed.to_str().unwrap(), ]) @@ -423,6 +427,7 @@ async fn test_egld_transfer_alice_to_bob() { "--value", &TRANSFER_AMOUNT.to_string(), "--send", + "--wait-result", ]) .status() .expect("failed to execute sc-meta tx new"); diff --git a/framework/snippets/src/interactor/interactor_dispatch.rs b/framework/snippets/src/interactor/interactor_dispatch.rs index d6ab468876..94c082eef9 100644 --- a/framework/snippets/src/interactor/interactor_dispatch.rs +++ b/framework/snippets/src/interactor/interactor_dispatch.rs @@ -74,6 +74,9 @@ where if wait_result { println!("Waiting for transaction result..."); + + self.generate_blocks_until_tx_processed(&tx_hash).await?; + let (tx_on_network, return_code) = multiversx_sdk::retrieve_tx_on_network(self.proxy(), tx_hash).await?; let tx_response = From ce29278ba0872f3f5abb2c3f384cf287b89c7e37 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 02:25:07 +0300 Subject: [PATCH 182/195] sc-meta CLI test fix --- framework/meta/tests/cs_tx_cli_test.rs | 47 ++++---------------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 5c3f83fc3e..39cbd356bc 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -11,11 +11,13 @@ const CHAIN_SIMULATOR_CHAIN_ID: &str = "chain"; /// 0.1 EGLD in the smallest denomination (10^17). const TRANSFER_AMOUNT: u128 = 100_000_000_000_000_000; +/// 100 EGLD, the amount senders get funded automatically +/// on interactor/sc-meta CLI + chain simulator. +const FUND_AMOUNT: u128 = 100_00_000_000_000_000_000; + /// Minimum gas for a plain EGLD transfer. const GAS_LIMIT: u64 = 50_000; -const CLI_TX_PROCESSING_BLOCKS: u64 = 40; - /// Deploys the adder contract, calls `add`, and verifies `getSum` returns the expected value. /// Mirrors the deploy / add / getSum flow from the adder snippets.sh. #[tokio::test] @@ -42,7 +44,7 @@ async fn test_adder_deploy_add_get_sum() { let wallet_address = wallet.to_address().to_bech32_default(); interactor.send_user_funds(&wallet_address).await.unwrap(); - interactor.generate_blocks(10).await.unwrap(); + interactor.generate_blocks(20).await.unwrap(); // ── deploy ──────────────────────────────────────────────────────────────── let deploy_output = Command::new(sc_meta_bin) @@ -79,11 +81,6 @@ async fn test_adder_deploy_add_get_sum() { ); assert!(deploy_output.status.success(), "deploy failed"); - interactor - .generate_blocks(CLI_TX_PROCESSING_BLOCKS) - .await - .unwrap(); - // Read the deployed contract address from the interaction output file. let outfile_content = std::fs::read_to_string(&outfile_deploy).expect("failed to read deploy outfile"); @@ -147,11 +144,6 @@ async fn test_adder_deploy_add_get_sum() { assert!(status.success(), "add call failed"); - interactor - .generate_blocks(CLI_TX_PROCESSING_BLOCKS) - .await - .unwrap(); - // Read and verify deterministic call fields. let call_content = std::fs::read_to_string(&outfile_call).expect("failed to read call outfile"); let call_json: serde_json::Value = @@ -234,11 +226,6 @@ async fn test_adder_deploy_add_get_sum() { ); assert!(upgrade_output.status.success(), "upgrade failed"); - interactor - .generate_blocks(CLI_TX_PROCESSING_BLOCKS) - .await - .unwrap(); - // Verify the upgrade outfile references the same contract address. let upgrade_content = std::fs::read_to_string(&outfile_upgrade).expect("failed to read upgrade outfile"); @@ -291,7 +278,7 @@ async fn test_adder_deploy_add_get_sum() { let relayer_address = relayer_wallet.to_address().to_bech32_default(); interactor.send_user_funds(&relayer_address).await.unwrap(); - interactor.generate_blocks(10).await.unwrap(); + interactor.generate_blocks(20).await.unwrap(); let status = Command::new(sc_meta_bin) .args([ @@ -322,11 +309,6 @@ async fn test_adder_deploy_add_get_sum() { assert!(status.success(), "relayed add call failed"); - interactor - .generate_blocks(CLI_TX_PROCESSING_BLOCKS) - .await - .unwrap(); - // getSum must return 5 + 3 = 8 after the relayed add. let query_after_relayed = Command::new(sc_meta_bin) .args([ @@ -381,15 +363,6 @@ async fn test_egld_transfer_alice_to_bob() { let alice_address = test_wallets::alice().to_address(); let bob_address = test_wallets::bob().to_address(); - // fund alice - interactor - .send_user_funds(&alice_address.to_bech32_default()) - .await - .unwrap(); - - // Allow the funding transactions to settle. - interactor.generate_blocks(10).await.unwrap(); - // ── balances before transfer ────────────────────────────────────────────── let alice_balance_before: u128 = interactor .get_account(&alice_address) @@ -434,12 +407,6 @@ async fn test_egld_transfer_alice_to_bob() { assert!(status.success(), "sc-meta tx new command failed"); - // Allow the transfer transaction to settle. - interactor - .generate_blocks(CLI_TX_PROCESSING_BLOCKS) - .await - .unwrap(); - // ── balances after transfer ─────────────────────────────────────────────── let alice_balance_after: u128 = interactor .get_account(&alice_address) @@ -467,7 +434,7 @@ async fn test_egld_transfer_alice_to_bob() { // Alice must have spent at least the transfer amount (gas fees are on top). assert!( - alice_balance_before - alice_balance_after >= TRANSFER_AMOUNT, + alice_balance_before + FUND_AMOUNT - alice_balance_after >= TRANSFER_AMOUNT, "Alice's balance did not decrease by at least the transfer amount" ); } From d5daf9686b9f4e5d9e070460f33e520b90291f08 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 02:31:34 +0300 Subject: [PATCH 183/195] sc-meta test cleanup --- framework/meta/tests/cs_tx_cli_test.rs | 13 ------------- .../src/interactor/interactor_chain_simulator.rs | 1 + 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 39cbd356bc..aa41241a64 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -36,15 +36,8 @@ async fn test_adder_deploy_add_get_sum() { let sc_meta_bin = env!("CARGO_BIN_EXE_sc-meta"); - let interactor = Interactor::new(CHAIN_SIMULATOR_URL) - .await - .use_chain_simulator(true); - let wallet = Wallet::from_pem_file(&wallet_pem_path).unwrap(); let wallet_address = wallet.to_address().to_bech32_default(); - interactor.send_user_funds(&wallet_address).await.unwrap(); - - interactor.generate_blocks(20).await.unwrap(); // ── deploy ──────────────────────────────────────────────────────────────── let deploy_output = Command::new(sc_meta_bin) @@ -274,12 +267,6 @@ async fn test_adder_deploy_add_get_sum() { generate_test_wallet(sc_meta_bin, "s1mon", &relayer_pem_path); - let relayer_wallet = Wallet::from_pem_file(&relayer_pem_path).unwrap(); - let relayer_address = relayer_wallet.to_address().to_bech32_default(); - interactor.send_user_funds(&relayer_address).await.unwrap(); - - interactor.generate_blocks(20).await.unwrap(); - let status = Command::new(sc_meta_bin) .args([ "tx", diff --git a/framework/snippets/src/interactor/interactor_chain_simulator.rs b/framework/snippets/src/interactor/interactor_chain_simulator.rs index b5409d6a96..c785cc6158 100644 --- a/framework/snippets/src/interactor/interactor_chain_simulator.rs +++ b/framework/snippets/src/interactor/interactor_chain_simulator.rs @@ -14,6 +14,7 @@ impl InteractorBase where GatewayProxy: GatewayAsyncService, { + /// Send funds to a user account on the chain simulator. The amount is fixed to 100 EGLD. pub async fn send_user_funds(&self, receiver: &Bech32Address) -> Result { if !self.use_chain_simulator { return Ok(String::from("no-simulator")); From 414006094baa4af3d8f3798978965da627bdecbf Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 02:42:09 +0300 Subject: [PATCH 184/195] sc-meta test cleanup --- framework/meta/tests/cs_tx_cli_test.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index aa41241a64..57eeea0283 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -419,7 +419,10 @@ async fn test_egld_transfer_alice_to_bob() { "Bob's balance did not increase by the expected transfer amount" ); - // Alice must have spent at least the transfer amount (gas fees are on top). + // Alice was funded automatically by the CLI, + // since the chain-simulator config was auto-detected, + // hence the additional FUND_AMOUNT. + // She must have spent at least the transfer amount (gas fees are on top). assert!( alice_balance_before + FUND_AMOUNT - alice_balance_after >= TRANSFER_AMOUNT, "Alice's balance did not decrease by at least the transfer amount" From 93ad8747e0b3d77d566b1b0e074bef42100240c9 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 03:20:30 +0300 Subject: [PATCH 185/195] interactor - generate block after fund tweak --- framework/snippets/src/interactor/interactor_base.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 1225259d5a..333ae39f9a 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -322,7 +322,7 @@ where self.send_user_funds(&address.to_bech32(self.get_hrp())) .await .unwrap(); - self.generate_blocks(1).await.unwrap(); + self.generate_blocks(10).await.unwrap(); self.sender_map.insert( address.clone(), Sender { From 394643462db0d7f17fbf0b0eb295c5d1fb94c41a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Fri, 14 Aug 2026 12:41:20 +0300 Subject: [PATCH 186/195] sc-meta CLI test - generate some blocks before starting --- framework/meta/tests/cs_tx_cli_test.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 57eeea0283..7be9af7c54 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -39,6 +39,12 @@ async fn test_adder_deploy_add_get_sum() { let wallet = Wallet::from_pem_file(&wallet_pem_path).unwrap(); let wallet_address = wallet.to_address().to_bech32_default(); + // Connect to the chain simulator, generate some initial blocks. + let interactor = Interactor::new(CHAIN_SIMULATOR_URL) + .await + .use_chain_simulator(true); + interactor.generate_blocks(10).await.unwrap(); + // ── deploy ──────────────────────────────────────────────────────────────── let deploy_output = Command::new(sc_meta_bin) .args([ @@ -344,6 +350,7 @@ async fn test_egld_transfer_alice_to_bob() { let interactor = Interactor::new(CHAIN_SIMULATOR_URL) .await .use_chain_simulator(true); + interactor.generate_blocks(10).await.unwrap(); // Register wallets – `register_wallet` automatically funds each account via // the chain simulator's `send_user_funds` endpoint. From c1d30dbe6143be2bf6deddb8e825bb501a420f1a Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 1 Sep 2026 11:18:57 +0300 Subject: [PATCH 187/195] cleanup --- .../src/interactor/interactor_dispatch.rs | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/framework/snippets/src/interactor/interactor_dispatch.rs b/framework/snippets/src/interactor/interactor_dispatch.rs index 94c082eef9..9c432b4d03 100644 --- a/framework/snippets/src/interactor/interactor_dispatch.rs +++ b/framework/snippets/src/interactor/interactor_dispatch.rs @@ -15,6 +15,14 @@ where to_json_pretty(value) } + fn print_tx_url(tx_hash: &str, chain_id: &str, info: &str) { + if let Some(explorer_url) = ExplorerUrl::from_chain_id(chain_id) { + println!("{info}: {}", explorer_url.tx_url(tx_hash)); + } else { + println!("{info} tx hash: {tx_hash}"); + } + } + pub async fn broadcast_transaction( &self, transaction: &Transaction, @@ -24,24 +32,19 @@ where .proxy() .request(multiversx_sdk::gateway::SendTxRequest(transaction)) .await; - let Ok(tx_hash) = result else { - let Err(err) = result else { - unreachable!(); - }; - let error = anyhow::anyhow!("{info} error: {err}"); - eprintln!("{error}"); - log::error!("{error}"); - return Err(error); - }; - - log::info!("{info} tx hash: {tx_hash}"); - if let Some(explorer_url) = ExplorerUrl::from_chain_id(&transaction.chain_id) { - println!("{info}: {}", explorer_url.tx_url(&tx_hash)); - } else { - println!("{info} tx hash: {tx_hash}"); + match result { + Ok(tx_hash) => { + log::info!("{info} tx hash: {tx_hash}"); + Self::print_tx_url(&tx_hash, &transaction.chain_id, info); + Ok(tx_hash) + } + Err(err) => { + let error = anyhow::anyhow!("{info} error: {err}"); + eprintln!("{error}"); + log::error!("{error}"); + Err(error) + } } - - Ok(tx_hash) } /// Broadcast the transaction inside `output`, update the hash (and optionally From f1c068ce0aa0e162872e287aa3750251af38a537 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 1 Sep 2026 11:19:47 +0300 Subject: [PATCH 188/195] sc-meta - use_chain_simulator_auto where missing --- framework/meta/src/cmd/tx/tx_cli_relay.rs | 3 ++- framework/meta/src/cmd/tx/tx_cli_send.rs | 5 ++++- framework/meta/src/cmd/tx/tx_cli_sign.rs | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_relay.rs b/framework/meta/src/cmd/tx/tx_cli_relay.rs index cd3f0ebe4f..d2274c5826 100644 --- a/framework/meta/src/cmd/tx/tx_cli_relay.rs +++ b/framework/meta/src/cmd/tx/tx_cli_relay.rs @@ -53,7 +53,8 @@ async fn tx_relay_inner(args: &RelayArgs) -> Result<()> { if args.send { let interactor = Interactor::empty() .with_connection(&args.gateway.proxy) - .await; + .await + .use_chain_simulator_auto(); interactor .broadcast_and_save(output, args.outfile.as_deref(), args.wait_result) .await?; diff --git a/framework/meta/src/cmd/tx/tx_cli_send.rs b/framework/meta/src/cmd/tx/tx_cli_send.rs index 60f5561e4b..9c298337d4 100644 --- a/framework/meta/src/cmd/tx/tx_cli_send.rs +++ b/framework/meta/src/cmd/tx/tx_cli_send.rs @@ -15,7 +15,10 @@ async fn tx_send_inner(args: &SendArgs) -> Result<()> { let tx = load_transaction_from_file(&args.infile)?; let output = TxOutputFile::from_transaction(tx, None)?; - let interactor = Interactor::empty().with_connection(&args.proxy).await; + let interactor = Interactor::empty() + .with_connection(&args.proxy) + .await + .use_chain_simulator_auto(); interactor .broadcast_and_save(output, args.outfile.as_deref(), args.wait_result) .await diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 1ad4ab6e29..011f1d8bcc 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -63,7 +63,8 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { if args.send { let interactor = Interactor::empty() .with_connection(&args.gateway.proxy) - .await; + .await + .use_chain_simulator_auto(); interactor .broadcast_and_save(output, args.outfile.as_deref(), args.wait_result) .await?; From e7f74afb1586bad0ed8c7f5d6c258edc5449f5d2 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 1 Sep 2026 11:26:34 +0300 Subject: [PATCH 189/195] test fix --- framework/meta/tests/cs_tx_cli_test.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 7be9af7c54..388f382eee 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -13,7 +13,7 @@ const TRANSFER_AMOUNT: u128 = 100_000_000_000_000_000; /// 100 EGLD, the amount senders get funded automatically /// on interactor/sc-meta CLI + chain simulator. -const FUND_AMOUNT: u128 = 100_00_000_000_000_000_000; +const FUND_AMOUNT: u128 = 100_000_000_000_000_000_000; /// Minimum gas for a plain EGLD transfer. const GAS_LIMIT: u64 = 50_000; @@ -207,6 +207,8 @@ async fn test_adder_deploy_add_get_sum() { CHAIN_SIMULATOR_CHAIN_ID, "--gas-limit", "50000000", + "--arguments", + "10", "--send", "--wait-result", "--outfile", @@ -239,7 +241,7 @@ async fn test_adder_deploy_add_get_sum() { "upgrade receiver mismatch" ); - // getSum must still return 5 after the upgrade. + // getSum must return 10 (0a) after the upgrade. let query_after_upgrade = Command::new(sc_meta_bin) .args([ "tx", @@ -263,7 +265,7 @@ async fn test_adder_deploy_add_get_sum() { serde_json::from_str(stdout_after.trim()).expect("failed to parse query output as JSON"); assert_eq!( result_after, - vec!["05"], + vec!["0a"], "getSum returned unexpected value after upgrade" ); @@ -302,7 +304,7 @@ async fn test_adder_deploy_add_get_sum() { assert!(status.success(), "relayed add call failed"); - // getSum must return 5 + 3 = 8 after the relayed add. + // getSum must return 0a + 3 = 0d after the relayed add. let query_after_relayed = Command::new(sc_meta_bin) .args([ "tx", @@ -326,7 +328,7 @@ async fn test_adder_deploy_add_get_sum() { serde_json::from_str(stdout_relayed.trim()).expect("failed to parse query output as JSON"); assert_eq!( result_relayed, - vec!["08"], + vec!["0d"], "getSum returned unexpected value after relayed add" ); } From 769f737e3fee1ce38a3e4c42f3a90cad98323a10 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 1 Sep 2026 11:47:33 +0300 Subject: [PATCH 190/195] doc --- framework/snippets/src/interactor/interactor_dispatch.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/framework/snippets/src/interactor/interactor_dispatch.rs b/framework/snippets/src/interactor/interactor_dispatch.rs index 9c432b4d03..263e5fa361 100644 --- a/framework/snippets/src/interactor/interactor_dispatch.rs +++ b/framework/snippets/src/interactor/interactor_dispatch.rs @@ -23,6 +23,12 @@ where } } + /// Broadcast `transaction` through the configured gateway. + /// + /// Returns the transaction hash reported by the gateway. On success, logs the hash and + /// prints its explorer URL when `transaction.chain_id` is supported; otherwise prints the + /// hash. On failure, logs and prints the gateway error before returning it with `info` as + /// context. pub async fn broadcast_transaction( &self, transaction: &Transaction, From 0bcca3cfebf8038fe89df15f2f09751afbc0b288 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 1 Sep 2026 12:08:28 +0300 Subject: [PATCH 191/195] interactor - simplified output file contractAddress generation --- framework/meta/src/cmd/tx/tx_cli_call.rs | 1 - framework/meta/src/cmd/tx/tx_cli_deploy.rs | 1 - framework/meta/src/cmd/tx/tx_cli_new.rs | 1 - framework/meta/src/cmd/tx/tx_cli_relay.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_send.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_sign.rs | 2 +- framework/meta/src/cmd/tx/tx_cli_upgrade.rs | 1 - .../src/interactor/interactor_dispatch.rs | 3 +- .../snippets/src/interactor/tx_output_file.rs | 66 +++++++++++++++++-- 9 files changed, 64 insertions(+), 15 deletions(-) diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx/tx_cli_call.rs index 9c2630c8ae..5d0e2044bb 100644 --- a/framework/meta/src/cmd/tx/tx_cli_call.rs +++ b/framework/meta/src/cmd/tx/tx_cli_call.rs @@ -44,7 +44,6 @@ async fn tx_call_inner(args: &CallArgs) -> Result<()> { interactor .sign_and_dispatch( tx, - None, args.tx.send, args.tx.wait_result, args.tx.outfile.as_deref(), diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx/tx_cli_deploy.rs index 9fef440bb4..a7506c1c3d 100644 --- a/framework/meta/src/cmd/tx/tx_cli_deploy.rs +++ b/framework/meta/src/cmd/tx/tx_cli_deploy.rs @@ -62,7 +62,6 @@ async fn tx_deploy_inner(args: &DeployArgs) -> Result<()> { interactor .sign_and_dispatch( tx, - Some(contract_address.to_bech32_string()), args.tx.send, args.tx.wait_result, args.tx.outfile.as_deref(), diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx/tx_cli_new.rs index 9ce61471de..c4c7cf61d8 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx/tx_cli_new.rs @@ -48,7 +48,6 @@ async fn tx_new_inner(args: &NewArgs) -> Result<()> { interactor .sign_and_dispatch( tx, - None, args.tx.send, args.tx.wait_result, args.tx.outfile.as_deref(), diff --git a/framework/meta/src/cmd/tx/tx_cli_relay.rs b/framework/meta/src/cmd/tx/tx_cli_relay.rs index d2274c5826..2590d71b0f 100644 --- a/framework/meta/src/cmd/tx/tx_cli_relay.rs +++ b/framework/meta/src/cmd/tx/tx_cli_relay.rs @@ -48,7 +48,7 @@ async fn tx_relay_inner(args: &RelayArgs) -> Result<()> { let relayer_sig = relayer_wallet.sign_tx(&tx)?; tx.relayer_signature = Some(relayer_sig); - let output = TxOutputFile::from_transaction(tx, None)?; + let output = TxOutputFile::from_transaction(tx)?; if args.send { let interactor = Interactor::empty() diff --git a/framework/meta/src/cmd/tx/tx_cli_send.rs b/framework/meta/src/cmd/tx/tx_cli_send.rs index 9c298337d4..9c8c6ef082 100644 --- a/framework/meta/src/cmd/tx/tx_cli_send.rs +++ b/framework/meta/src/cmd/tx/tx_cli_send.rs @@ -14,7 +14,7 @@ pub async fn tx_send(args: &SendArgs) { async fn tx_send_inner(args: &SendArgs) -> Result<()> { let tx = load_transaction_from_file(&args.infile)?; - let output = TxOutputFile::from_transaction(tx, None)?; + let output = TxOutputFile::from_transaction(tx)?; let interactor = Interactor::empty() .with_connection(&args.proxy) .await diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx/tx_cli_sign.rs index 011f1d8bcc..77569eb059 100644 --- a/framework/meta/src/cmd/tx/tx_cli_sign.rs +++ b/framework/meta/src/cmd/tx/tx_cli_sign.rs @@ -58,7 +58,7 @@ async fn tx_sign_inner(args: &SignArgs) -> Result<()> { tx.relayer_signature = Some(relayer_sig); } - let output = TxOutputFile::from_transaction(tx, None)?; + let output = TxOutputFile::from_transaction(tx)?; if args.send { let interactor = Interactor::empty() diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs index 3480b4e4d8..25ccc41949 100644 --- a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs +++ b/framework/meta/src/cmd/tx/tx_cli_upgrade.rs @@ -54,7 +54,6 @@ async fn tx_upgrade_inner(args: &UpgradeArgs) -> Result<()> { interactor .sign_and_dispatch( tx, - None, args.tx.send, args.tx.wait_result, args.tx.outfile.as_deref(), diff --git a/framework/snippets/src/interactor/interactor_dispatch.rs b/framework/snippets/src/interactor/interactor_dispatch.rs index 263e5fa361..685bf33cba 100644 --- a/framework/snippets/src/interactor/interactor_dispatch.rs +++ b/framework/snippets/src/interactor/interactor_dispatch.rs @@ -101,13 +101,12 @@ where pub async fn sign_and_dispatch( &self, mut transaction: Transaction, - contract_address: Option, send: bool, wait_result: bool, outfile: Option<&Path>, ) -> anyhow::Result<()> { self.sign_tx(&mut transaction); - let output = TxOutputFile::from_transaction(transaction, contract_address)?; + let output = TxOutputFile::from_transaction(transaction)?; if send { self.broadcast_and_save(output, outfile, wait_result).await diff --git a/framework/snippets/src/interactor/tx_output_file.rs b/framework/snippets/src/interactor/tx_output_file.rs index 17843cbec9..ecff101fbe 100644 --- a/framework/snippets/src/interactor/tx_output_file.rs +++ b/framework/snippets/src/interactor/tx_output_file.rs @@ -1,6 +1,9 @@ use anyhow::{Context, Result}; use multiversx_sdk::{ - chain_core::std::base64_decode, + chain_core::{ + std::{base64_decode, new_address::compute_new_address_bech32}, + types::Address, + }, data::transaction::{ApiTransactionResult, Transaction}, }; use serde::{Deserialize, Serialize}; @@ -37,20 +40,18 @@ pub struct TxOutputFile { impl TxOutputFile { /// Creates an mxpy-compatible output value from a transaction. - pub fn from_transaction( - emitted_transaction: Transaction, - contract_address: Option, - ) -> Result { + pub fn from_transaction(emitted_transaction: Transaction) -> Result { let emitted_transaction_data = match &emitted_transaction.data { None => String::new(), Some(data) => String::from_utf8_lossy(&base64_decode(data)?).into_owned(), }; + let contract_address = tx_opt_deploy_new_address(&emitted_transaction); Ok(Self { emitted_transaction, emitted_transaction_data, emitted_transaction_hash: String::new(), - contract_address, + contract_address: contract_address, transaction_on_network: None, }) } @@ -69,6 +70,15 @@ impl TxOutputFile { } } +/// Returns the new contract address for a deploy transaction, otherwise returns `None`. +fn tx_opt_deploy_new_address(tx: &Transaction) -> Option { + if tx.receiver.address == Address::zero() { + Some(compute_new_address_bech32(&tx.sender, tx.nonce).to_bech32_string()) + } else { + None + } +} + fn to_json_pretty(value: &T) -> Result { let mut buf = Vec::new(); let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); @@ -78,3 +88,47 @@ fn to_json_pretty(value: &T) -> Result { .context("failed to serialize transaction")?; String::from_utf8(buf).context("non-UTF8 in serialized JSON") } + +#[cfg(test)] +mod tests { + use super::*; + use multiversx_sdk::{chain_core::std::Bech32Address, data::transaction::TransactionVersion}; + + fn transaction(receiver: Bech32Address) -> Transaction { + Transaction { + nonce: 42, + value: "0".to_owned(), + receiver, + sender: Address::from([7; 32]).to_bech32_default(), + gas_price: 1_000_000_000, + gas_limit: 100_000, + data: None, + signature: None, + chain_id: "D".to_owned(), + version: TransactionVersion::V2, + options: None, + relayer: None, + relayer_signature: None, + } + } + + #[test] + fn derives_contract_address_for_zero_recipient() { + let transaction = transaction(Bech32Address::zero_default_hrp()); + let expected = + compute_new_address_bech32(&transaction.sender, transaction.nonce).to_bech32_string(); + + let output = TxOutputFile::from_transaction(transaction).unwrap(); + + assert_eq!(output.contract_address, Some(expected)); + } + + #[test] + fn does_not_set_contract_address_for_nonzero_recipient() { + let output = + TxOutputFile::from_transaction(transaction(Address::from([8; 32]).to_bech32_default())) + .unwrap(); + + assert_eq!(output.contract_address, None); + } +} From 5cf60626146981be8ac2367e0e837dc35f408ac8 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Tue, 1 Sep 2026 13:37:21 +0300 Subject: [PATCH 192/195] sc-meta CLI test refactor --- framework/meta/tests/cs_tx_cli_test.rs | 267 +++++++++++-------------- 1 file changed, 117 insertions(+), 150 deletions(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index 388f382eee..b3f73fa5b1 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -18,6 +18,13 @@ const FUND_AMOUNT: u128 = 100_000_000_000_000_000_000; /// Minimum gas for a plain EGLD transfer. const GAS_LIMIT: u64 = 50_000; +struct AdderTestContext<'a> { + sc_meta_bin: &'a str, + wasm_path: &'a std::path::Path, + wallet_pem_path: &'a std::path::Path, + wallet_address: &'a str, +} + /// Deploys the adder contract, calls `add`, and verifies `getSum` returns the expected value. /// Mirrors the deploy / add / getSum flow from the adder snippets.sh. #[tokio::test] @@ -37,7 +44,17 @@ async fn test_adder_deploy_add_get_sum() { let sc_meta_bin = env!("CARGO_BIN_EXE_sc-meta"); let wallet = Wallet::from_pem_file(&wallet_pem_path).unwrap(); - let wallet_address = wallet.to_address().to_bech32_default(); + let wallet_address = wallet + .to_address() + .to_bech32_default() + .to_bech32_str() + .to_owned(); + let context = AdderTestContext { + sc_meta_bin, + wasm_path: &wasm_path, + wallet_pem_path: &wallet_pem_path, + wallet_address: &wallet_address, + }; // Connect to the chain simulator, generate some initial blocks. let interactor = Interactor::new(CHAIN_SIMULATOR_URL) @@ -45,15 +62,36 @@ async fn test_adder_deploy_add_get_sum() { .use_chain_simulator(true); interactor.generate_blocks(10).await.unwrap(); - // ── deploy ──────────────────────────────────────────────────────────────── - let deploy_output = Command::new(sc_meta_bin) + let contract_address = deploy_adder(&context, &outfile_deploy); + call_adder(&context, &contract_address, &outfile_call, "5"); + assert_eq!(query_sum(&context, &contract_address), vec!["05"]); + + upgrade_adder(&context, &contract_address, &outfile_upgrade); + assert_eq!(query_sum(&context, &contract_address), vec!["0a"]); + + let relayer_pem_path = test_artefacts_dir.join("s1mon.pem"); + let outfile_call_relayed = outfiles_dir.join("adder-call-relayed-cs.interaction.json"); + + generate_test_wallet(context.sc_meta_bin, "s1mon", &relayer_pem_path); + call_adder_relayed( + &context, + &contract_address, + &relayer_pem_path, + &outfile_call_relayed, + "3", + ); + assert_eq!(query_sum(&context, &contract_address), vec!["0d"]); +} + +fn deploy_adder(context: &AdderTestContext<'_>, outfile: &std::path::Path) -> String { + let output = Command::new(context.sc_meta_bin) .args([ "tx", "deploy", "--bytecode", - wasm_path.to_str().unwrap(), + context.wasm_path.to_str().unwrap(), "--pem", - wallet_pem_path.to_str().unwrap(), + context.wallet_pem_path.to_str().unwrap(), "--proxy", CHAIN_SIMULATOR_URL, "--chain", @@ -65,64 +103,58 @@ async fn test_adder_deploy_add_get_sum() { "--send", "--wait-result", "--outfile", - outfile_deploy.to_str().unwrap(), + outfile.to_str().unwrap(), ]) .output() .expect("failed to execute sc-meta tx deploy"); - println!( "deploy stdout:\n{}", - String::from_utf8_lossy(&deploy_output.stdout) - ); - println!( - "deploy stderr:\n{}", - String::from_utf8_lossy(&deploy_output.stderr) + String::from_utf8_lossy(&output.stdout) ); - assert!(deploy_output.status.success(), "deploy failed"); + assert!(output.status.success(), "deploy failed"); - // Read the deployed contract address from the interaction output file. - let outfile_content = - std::fs::read_to_string(&outfile_deploy).expect("failed to read deploy outfile"); - let deploy_json: serde_json::Value = - serde_json::from_str(&outfile_content).expect("failed to parse deploy outfile JSON"); - - // Verify deterministic deploy fields. + let content = std::fs::read_to_string(outfile).expect("failed to read deploy outfile"); + let json: serde_json::Value = + serde_json::from_str(&content).expect("failed to parse deploy outfile JSON"); assert_eq!( - deploy_json["emittedTransaction"]["sender"] - .as_str() - .unwrap(), - wallet_address.to_bech32_str(), + json["emittedTransaction"]["sender"].as_str().unwrap(), + context.wallet_address, "deploy sender mismatch" ); assert_eq!( - deploy_json["emittedTransaction"]["receiver"] - .as_str() - .unwrap(), + json["emittedTransaction"]["receiver"].as_str().unwrap(), Bech32Address::zero_default_hrp().to_bech32_str(), "deploy receiver mismatch" ); assert!( - deploy_json["emittedTransactionData"] + json["emittedTransactionData"] .as_str() .unwrap() .ends_with("@0500@0500@"), "deploy emittedTransactionData does not end with @0500@0500@" ); - let contract_address = deploy_json["contractAddress"] + let contract_address = json["contractAddress"] .as_str() - .expect("contractAddress not found in deploy outfile"); - + .expect("contractAddress not found in deploy outfile") + .to_owned(); println!("Deployed adder at: {contract_address}"); + contract_address +} - // ── add(5) ──────────────────────────────────────────────────────────────── - let status = Command::new(sc_meta_bin) +fn call_adder( + context: &AdderTestContext<'_>, + contract_address: &str, + outfile: &std::path::Path, + argument: &str, +) { + let output = Command::new(context.sc_meta_bin) .args([ "tx", "call", contract_address, "--pem", - wallet_pem_path.to_str().unwrap(), + context.wallet_pem_path.to_str().unwrap(), "--proxy", CHAIN_SIMULATOR_URL, "--chain", @@ -132,42 +164,39 @@ async fn test_adder_deploy_add_get_sum() { "--function", "add", "--arguments", - "5", + argument, "--send", "--wait-result", "--outfile", - outfile_call.to_str().unwrap(), + outfile.to_str().unwrap(), ]) - .status() + .output() .expect("failed to execute sc-meta tx call"); + println!("add stdout:\n{}", String::from_utf8_lossy(&output.stdout)); + assert!(output.status.success(), "add call failed"); - assert!(status.success(), "add call failed"); - - // Read and verify deterministic call fields. - let call_content = std::fs::read_to_string(&outfile_call).expect("failed to read call outfile"); - let call_json: serde_json::Value = - serde_json::from_str(&call_content).expect("failed to parse call outfile JSON"); - + let content = std::fs::read_to_string(outfile).expect("failed to read call outfile"); + let json: serde_json::Value = + serde_json::from_str(&content).expect("failed to parse call outfile JSON"); assert_eq!( - call_json["emittedTransaction"]["sender"].as_str().unwrap(), - wallet_address.to_bech32_str(), + json["emittedTransaction"]["sender"].as_str().unwrap(), + context.wallet_address, "call sender mismatch" ); assert_eq!( - call_json["emittedTransaction"]["receiver"] - .as_str() - .unwrap(), + json["emittedTransaction"]["receiver"].as_str().unwrap(), contract_address, "call receiver mismatch" ); assert_eq!( - call_json["emittedTransactionData"].as_str().unwrap(), + json["emittedTransactionData"].as_str().unwrap(), "add@05", "call emittedTransactionData mismatch" ); +} - // ── getSum ──────────────────────────────────────────────────────────────── - let query_output = Command::new(sc_meta_bin) +fn query_sum(context: &AdderTestContext<'_>, contract_address: &str) -> Vec { + let output = Command::new(context.sc_meta_bin) .args([ "tx", "query", @@ -179,28 +208,27 @@ async fn test_adder_deploy_add_get_sum() { ]) .output() .expect("failed to execute sc-meta tx query"); + assert!(output.status.success(), "getSum query failed"); - assert!(query_output.status.success(), "getSum query failed"); - - let stdout = String::from_utf8_lossy(&query_output.stdout); + let stdout = String::from_utf8_lossy(&output.stdout); println!("getSum result: {stdout}"); + serde_json::from_str(stdout.trim()).expect("failed to parse query output as JSON") +} - // The result is a JSON array of hex-encoded values, e.g. ["05"]. - // 5 decimal = 0x05. - let result: Vec = - serde_json::from_str(stdout.trim()).expect("failed to parse query output as JSON"); - assert_eq!(result, vec!["05"], "getSum returned unexpected value"); - - // ── upgrade ─────────────────────────────────────────────────────────────── - let upgrade_output = Command::new(sc_meta_bin) +fn upgrade_adder( + context: &AdderTestContext<'_>, + contract_address: &str, + outfile: &std::path::Path, +) { + let output = Command::new(context.sc_meta_bin) .args([ "tx", "upgrade", contract_address, "--bytecode", - wasm_path.to_str().unwrap(), + context.wasm_path.to_str().unwrap(), "--pem", - wallet_pem_path.to_str().unwrap(), + context.wallet_pem_path.to_str().unwrap(), "--proxy", CHAIN_SIMULATOR_URL, "--chain", @@ -212,76 +240,40 @@ async fn test_adder_deploy_add_get_sum() { "--send", "--wait-result", "--outfile", - outfile_upgrade.to_str().unwrap(), + outfile.to_str().unwrap(), ]) .output() .expect("failed to execute sc-meta tx upgrade"); - println!( "upgrade stdout:\n{}", - String::from_utf8_lossy(&upgrade_output.stdout) + String::from_utf8_lossy(&output.stdout) ); - println!( - "upgrade stderr:\n{}", - String::from_utf8_lossy(&upgrade_output.stderr) - ); - assert!(upgrade_output.status.success(), "upgrade failed"); - - // Verify the upgrade outfile references the same contract address. - let upgrade_content = - std::fs::read_to_string(&outfile_upgrade).expect("failed to read upgrade outfile"); - let upgrade_json: serde_json::Value = - serde_json::from_str(&upgrade_content).expect("failed to parse upgrade outfile JSON"); + assert!(output.status.success(), "upgrade failed"); + let content = std::fs::read_to_string(outfile).expect("failed to read upgrade outfile"); + let json: serde_json::Value = + serde_json::from_str(&content).expect("failed to parse upgrade outfile JSON"); assert_eq!( - upgrade_json["emittedTransaction"]["receiver"] - .as_str() - .unwrap(), + json["emittedTransaction"]["receiver"].as_str().unwrap(), contract_address, "upgrade receiver mismatch" ); +} - // getSum must return 10 (0a) after the upgrade. - let query_after_upgrade = Command::new(sc_meta_bin) - .args([ - "tx", - "query", - contract_address, - "--proxy", - CHAIN_SIMULATOR_URL, - "--function", - "getSum", - ]) - .output() - .expect("failed to execute sc-meta tx query after upgrade"); - - assert!( - query_after_upgrade.status.success(), - "getSum query after upgrade failed" - ); - - let stdout_after = String::from_utf8_lossy(&query_after_upgrade.stdout); - let result_after: Vec = - serde_json::from_str(stdout_after.trim()).expect("failed to parse query output as JSON"); - assert_eq!( - result_after, - vec!["0a"], - "getSum returned unexpected value after upgrade" - ); - - // ── add(3) via relayed transaction (one-step) ───────────────────────────── - let relayer_pem_path = test_artefacts_dir.join("s1mon.pem"); - let outfile_call_relayed = outfiles_dir.join("adder-call-relayed-cs.interaction.json"); - - generate_test_wallet(sc_meta_bin, "s1mon", &relayer_pem_path); - - let status = Command::new(sc_meta_bin) +fn call_adder_relayed( + context: &AdderTestContext<'_>, + contract_address: &str, + relayer_pem_path: &std::path::Path, + outfile: &std::path::Path, + argument: &str, +) { + let output = Command::new(context.sc_meta_bin) .args([ "tx", "call", contract_address, "--pem", - wallet_pem_path.to_str().unwrap(), + context.wallet_pem_path.to_str().unwrap(), "--relayer-pem", relayer_pem_path.to_str().unwrap(), "--proxy", @@ -293,44 +285,19 @@ async fn test_adder_deploy_add_get_sum() { "--function", "add", "--arguments", - "3", + argument, "--send", "--wait-result", "--outfile", - outfile_call_relayed.to_str().unwrap(), - ]) - .status() - .expect("failed to execute sc-meta tx call (relayed)"); - - assert!(status.success(), "relayed add call failed"); - - // getSum must return 0a + 3 = 0d after the relayed add. - let query_after_relayed = Command::new(sc_meta_bin) - .args([ - "tx", - "query", - contract_address, - "--proxy", - CHAIN_SIMULATOR_URL, - "--function", - "getSum", + outfile.to_str().unwrap(), ]) .output() - .expect("failed to execute sc-meta tx query after relayed add"); - - assert!( - query_after_relayed.status.success(), - "getSum query after relayed add failed" - ); - - let stdout_relayed = String::from_utf8_lossy(&query_after_relayed.stdout); - let result_relayed: Vec = - serde_json::from_str(stdout_relayed.trim()).expect("failed to parse query output as JSON"); - assert_eq!( - result_relayed, - vec!["0d"], - "getSum returned unexpected value after relayed add" + .expect("failed to execute sc-meta tx call (relayed)"); + println!( + "relayed add stdout:\n{}", + String::from_utf8_lossy(&output.stdout) ); + assert!(output.status.success(), "relayed add call failed"); } /// Sends a small amount of EGLD from Alice to Bob via the `sc-meta tx new` CLI command From 041c4c0b5be81ffa003cc12840dcc6cf9ee22628 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Wed, 2 Sep 2026 00:06:43 +0300 Subject: [PATCH 193/195] fixes after review --- framework/meta/tests/cs_tx_cli_test.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/framework/meta/tests/cs_tx_cli_test.rs b/framework/meta/tests/cs_tx_cli_test.rs index b3f73fa5b1..9294d937ea 100644 --- a/framework/meta/tests/cs_tx_cli_test.rs +++ b/framework/meta/tests/cs_tx_cli_test.rs @@ -63,7 +63,7 @@ async fn test_adder_deploy_add_get_sum() { interactor.generate_blocks(10).await.unwrap(); let contract_address = deploy_adder(&context, &outfile_deploy); - call_adder(&context, &contract_address, &outfile_call, "5"); + call_adder(&context, &contract_address, &outfile_call, 5); assert_eq!(query_sum(&context, &contract_address), vec!["05"]); upgrade_adder(&context, &contract_address, &outfile_upgrade); @@ -111,7 +111,11 @@ fn deploy_adder(context: &AdderTestContext<'_>, outfile: &std::path::Path) -> St "deploy stdout:\n{}", String::from_utf8_lossy(&output.stdout) ); - assert!(output.status.success(), "deploy failed"); + assert!( + output.status.success(), + "deploy failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); let content = std::fs::read_to_string(outfile).expect("failed to read deploy outfile"); let json: serde_json::Value = @@ -146,7 +150,7 @@ fn call_adder( context: &AdderTestContext<'_>, contract_address: &str, outfile: &std::path::Path, - argument: &str, + argument: usize, ) { let output = Command::new(context.sc_meta_bin) .args([ @@ -164,7 +168,7 @@ fn call_adder( "--function", "add", "--arguments", - argument, + argument.to_string().as_str(), "--send", "--wait-result", "--outfile", @@ -188,9 +192,10 @@ fn call_adder( contract_address, "call receiver mismatch" ); + let expected_transaction_data = format!("add@{:02x}", argument); assert_eq!( json["emittedTransactionData"].as_str().unwrap(), - "add@05", + expected_transaction_data, "call emittedTransactionData mismatch" ); } From 40b2a7d1cde5215eb8b2be51ff0bceb9e382421b Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 3 Sep 2026 14:27:01 +0300 Subject: [PATCH 194/195] renamed tx.rs -> tx_cli.rs --- framework/meta/src/cli/cli_standalone_main.rs | 2 +- framework/meta/src/cmd.rs | 2 +- framework/meta/src/cmd/{tx.rs => tx_cli.rs} | 0 framework/meta/src/cmd/{tx => tx_cli}/TODO.md | 0 framework/meta/src/cmd/{tx => tx_cli}/parse_code_metadata.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/parse_payments.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_call.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_common.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_deploy.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_new.rs | 4 +++- framework/meta/src/cmd/{tx => tx_cli}/tx_cli_query.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_relay.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_send.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_sign.rs | 0 framework/meta/src/cmd/{tx => tx_cli}/tx_cli_upgrade.rs | 0 15 files changed, 5 insertions(+), 3 deletions(-) rename framework/meta/src/cmd/{tx.rs => tx_cli.rs} (100%) rename framework/meta/src/cmd/{tx => tx_cli}/TODO.md (100%) rename framework/meta/src/cmd/{tx => tx_cli}/parse_code_metadata.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/parse_payments.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_call.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_common.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_deploy.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_new.rs (95%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_query.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_relay.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_send.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_sign.rs (100%) rename framework/meta/src/cmd/{tx => tx_cli}/tx_cli_upgrade.rs (100%) diff --git a/framework/meta/src/cli/cli_standalone_main.rs b/framework/meta/src/cli/cli_standalone_main.rs index bed5c02928..5e19b7a6a2 100644 --- a/framework/meta/src/cli/cli_standalone_main.rs +++ b/framework/meta/src/cli/cli_standalone_main.rs @@ -4,7 +4,7 @@ use crate::cmd::data::data_cli; use crate::cmd::ledger_cmd::ledger_cmd; use crate::cmd::retrieve_address::retrieve_address; use crate::cmd::scen_blackbox::scen_blackbox_tool; -use crate::cmd::tx::tx_cli; +use crate::cmd::tx_cli::tx_cli; use crate::cmd::wallet_cmd::wallet; use clap::Parser; diff --git a/framework/meta/src/cmd.rs b/framework/meta/src/cmd.rs index 7e97b1cd07..81651c4712 100644 --- a/framework/meta/src/cmd.rs +++ b/framework/meta/src/cmd.rs @@ -14,6 +14,6 @@ pub mod scen_test_gen; pub mod template; pub mod test; pub mod test_coverage; -pub mod tx; +pub mod tx_cli; pub mod upgrade; pub mod wallet_cmd; diff --git a/framework/meta/src/cmd/tx.rs b/framework/meta/src/cmd/tx_cli.rs similarity index 100% rename from framework/meta/src/cmd/tx.rs rename to framework/meta/src/cmd/tx_cli.rs diff --git a/framework/meta/src/cmd/tx/TODO.md b/framework/meta/src/cmd/tx_cli/TODO.md similarity index 100% rename from framework/meta/src/cmd/tx/TODO.md rename to framework/meta/src/cmd/tx_cli/TODO.md diff --git a/framework/meta/src/cmd/tx/parse_code_metadata.rs b/framework/meta/src/cmd/tx_cli/parse_code_metadata.rs similarity index 100% rename from framework/meta/src/cmd/tx/parse_code_metadata.rs rename to framework/meta/src/cmd/tx_cli/parse_code_metadata.rs diff --git a/framework/meta/src/cmd/tx/parse_payments.rs b/framework/meta/src/cmd/tx_cli/parse_payments.rs similarity index 100% rename from framework/meta/src/cmd/tx/parse_payments.rs rename to framework/meta/src/cmd/tx_cli/parse_payments.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_call.rs b/framework/meta/src/cmd/tx_cli/tx_cli_call.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_call.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_call.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_common.rs b/framework/meta/src/cmd/tx_cli/tx_cli_common.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_common.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_common.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_deploy.rs b/framework/meta/src/cmd/tx_cli/tx_cli_deploy.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_deploy.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_deploy.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_new.rs b/framework/meta/src/cmd/tx_cli/tx_cli_new.rs similarity index 95% rename from framework/meta/src/cmd/tx/tx_cli_new.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_new.rs index c4c7cf61d8..9dd4ab4755 100644 --- a/framework/meta/src/cmd/tx/tx_cli_new.rs +++ b/framework/meta/src/cmd/tx_cli/tx_cli_new.rs @@ -5,7 +5,9 @@ use multiversx_chain_core::std::base64_encode; use multiversx_sc_snippets::imports::{Bech32Address, InteractorIntoSdkTransaction}; use crate::cli::cli_args_tx::NewArgs; -use crate::cmd::tx::tx_cli_common::{create_interactor, load_relayer_for_interactor, load_wallet}; +use crate::cmd::tx_cli::tx_cli_common::{ + create_interactor, load_relayer_for_interactor, load_wallet, +}; use super::parse_payments::parse_all_payment_args; diff --git a/framework/meta/src/cmd/tx/tx_cli_query.rs b/framework/meta/src/cmd/tx_cli/tx_cli_query.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_query.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_query.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_relay.rs b/framework/meta/src/cmd/tx_cli/tx_cli_relay.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_relay.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_relay.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_send.rs b/framework/meta/src/cmd/tx_cli/tx_cli_send.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_send.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_send.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_sign.rs b/framework/meta/src/cmd/tx_cli/tx_cli_sign.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_sign.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_sign.rs diff --git a/framework/meta/src/cmd/tx/tx_cli_upgrade.rs b/framework/meta/src/cmd/tx_cli/tx_cli_upgrade.rs similarity index 100% rename from framework/meta/src/cmd/tx/tx_cli_upgrade.rs rename to framework/meta/src/cmd/tx_cli/tx_cli_upgrade.rs From 8263781a211ae21dee248258791a90c0bb062718 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 3 Sep 2026 14:27:27 +0300 Subject: [PATCH 195/195] clippy fix --- framework/snippets/src/interactor/tx_output_file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/snippets/src/interactor/tx_output_file.rs b/framework/snippets/src/interactor/tx_output_file.rs index ecff101fbe..fc964d4172 100644 --- a/framework/snippets/src/interactor/tx_output_file.rs +++ b/framework/snippets/src/interactor/tx_output_file.rs @@ -51,7 +51,7 @@ impl TxOutputFile { emitted_transaction, emitted_transaction_data, emitted_transaction_hash: String::new(), - contract_address: contract_address, + contract_address, transaction_on_network: None, }) }