diff --git a/chain/core/src/types.rs b/chain/core/src/types.rs index 36750c0bae..58e6d141af 100644 --- a/chain/core/src/types.rs +++ b/chain/core/src/types.rs @@ -7,6 +7,7 @@ mod h256; mod heap_address; mod heap_h256; mod shard_config; +mod shard_id; mod time; pub use address::Address; @@ -17,5 +18,6 @@ pub use flags::*; pub use h256::H256; pub use heap_address::HeapAddress; pub use heap_h256::HeapH256; -pub use shard_config::{ShardConfig, ShardId}; +pub use shard_config::ShardConfig; +pub use shard_id::ShardId; pub use time::*; diff --git a/chain/core/src/types/shard_config.rs b/chain/core/src/types/shard_config.rs index 81057ebece..ffba6ccb10 100644 --- a/chain/core/src/types/shard_config.rs +++ b/chain/core/src/types/shard_config.rs @@ -1,31 +1,4 @@ -use super::Address; - -/// Identifies a shard by its numeric index. -/// -/// Regular shards are numbered from `0` to `number_of_shards - 1`. -/// The special value [`ShardId::METACHAIN_ID`] (`u32::MAX`) identifies the metachain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(transparent) -)] -pub struct ShardId(u32); - -impl ShardId { - /// Shard ID reserved for the metachain. - pub const METACHAIN_ID: ShardId = ShardId(u32::MAX); - - pub fn as_u32(&self) -> u32 { - self.0 - } -} - -impl From for ShardId { - fn from(value: u32) -> Self { - ShardId(value) - } -} +use super::{Address, shard_id::ShardId}; /// Precomputed configuration for shard assignment. /// Mirrors the Go `multiShardCoordinator` struct. @@ -95,7 +68,7 @@ impl ShardConfig { shard = val & self.mask_low; } - ShardId(shard) + ShardId::from(shard) } /// Returns true if both addresses belong to the same shard. @@ -176,9 +149,9 @@ mod tests { let addr = address_from_u32(i); let shard_id = sr.compute_id(&addr); assert!( - shard_id.0 < sr.number_of_shards, + shard_id.as_u32() < sr.number_of_shards, "i={i}: shard {} >= {num_of_shards}", - shard_id.0 + shard_id.as_u32() ); } } @@ -208,7 +181,7 @@ mod tests { let addr = address_from_u32(address); assert_eq!( sr.compute_id(&addr), - ShardId(expected_shard), + ShardId::from(expected_shard), "address={address}" ); } @@ -285,7 +258,11 @@ mod tests { ]; for &(hex, expected_shard) in dataset { let addr = address_from_hex(hex); - assert_eq!(sr.compute_id(&addr), ShardId(expected_shard), "hex={hex}"); + assert_eq!( + sr.compute_id(&addr), + ShardId::from(expected_shard), + "hex={hex}" + ); } } @@ -308,7 +285,7 @@ mod tests { let addr = address_from_u32(address); assert_eq!( sr.compute_id(&addr), - ShardId(expected_shard), + ShardId::from(expected_shard), "address={address}" ); } diff --git a/chain/core/src/types/shard_id.rs b/chain/core/src/types/shard_id.rs new file mode 100644 index 0000000000..92d5a8e536 --- /dev/null +++ b/chain/core/src/types/shard_id.rs @@ -0,0 +1,36 @@ +/// Identifies a shard by its numeric index. +/// +/// Regular shards are numbered from `0` to `number_of_shards - 1`. +/// The special value [`ShardId::METACHAIN_ID`] (`u32::MAX`) identifies the metachain. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent) +)] +pub struct ShardId(u32); + +impl ShardId { + /// Shard ID reserved for the metachain. + pub const METACHAIN_ID: ShardId = ShardId(u32::MAX); + + pub fn as_u32(&self) -> u32 { + self.0 + } +} + +impl From for ShardId { + fn from(value: u32) -> Self { + ShardId(value) + } +} + +impl core::fmt::Display for ShardId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if *self == ShardId::METACHAIN_ID { + write!(f, "metachain") + } else { + write!(f, "{}", self.0) + } + } +} diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/.gitignore b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/.gitignore index 5a64d09a70..169cd25d45 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/.gitignore +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/.gitignore @@ -1,2 +1,5 @@ # Pem files are used for interactions, but shouldn't be committed *.pem + +# Last deployed addresses +deploy.toml diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/README.md b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/README.md index b98bd314bd..820b6d5f13 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/README.md +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/README.md @@ -11,21 +11,47 @@ cargo run -- [OPTIONS] --- +## Typical workflow + +1. Fill in `config.toml` with the gateway, token IDs, contract addresses, and wallet PEM paths. +2. **Deploy** — creates one forwarder-blind contract per wallet and records the addresses in `deploy.toml`: + ```bash + cargo run -- deploy + ``` +3. **Copy addresses** — open `deploy.toml` and paste the `contract_addresses` list into `config.toml`. +4. **Wrap** — give each wallet WEGLD to spend: + ```bash + cargo run -- wrap -a + ``` +5. **Swap** — run any combination of swap commands to exercise the different call types. +6. **Drain** (optional) — recover tokens left in the contracts after `te` swaps: + ```bash + cargo run -- drain + ``` + +--- + ## Commands ### `deploy` -Deploy a new instance of the forwarder-blind contract. +Deploy one forwarder-blind contract instance per configured wallet. ```bash cargo run -- deploy ``` +Each wallet in `wallet_pem_paths` deploys its own contract instance. The resulting addresses +are written to `deploy.toml`. + +> **After deploying**, copy the addresses from `deploy.toml` into the `contract_addresses` +> list in `config.toml` so that subsequent swap and drain commands can target them. + --- ### `wrap` -Wrap EGLD into WEGLD via the WEGLD swap contract. +Wrap EGLD into WEGLD via the WEGLD swap contract. Runs once per wallet in parallel. ```bash cargo run -- wrap -a @@ -47,11 +73,11 @@ cargo run -- swap1 [OPTIONS] | Method | Description | |--------|-------------| -| `direct` | Swap directly on the DEX pair | -| `sync` | Swap via forwarder-blind using `blind_sync` | -| `async1` | Swap via forwarder-blind using `blind_async_v1` | -| `async2` | Swap via forwarder-blind using `blind_async_v2` | -| `te` | Swap via forwarder-blind using `blind_transf_exec` | +| `direct` | Each wallet swaps directly on the DEX pair | +| `sync` | Each wallet × each contract calls `blind_sync` (same-shard only) | +| `async1` | Each wallet × each contract calls `blind_async_v1` | +| `async2` | Each wallet × each contract calls `blind_async_v2` | +| `te` | Each wallet × each contract calls `blind_transf_exec` | #### Options (all methods) @@ -91,11 +117,11 @@ cargo run -- swap2 [OPTIONS] | Method | Description | |--------|-------------| -| `direct` | Swap directly on the DEX pair | -| `sync` | Swap via forwarder-blind using `blind_sync` | -| `async1` | Swap via forwarder-blind using `blind_async_v1` | -| `async2` | Swap via forwarder-blind using `blind_async_v2` | -| `te` | Swap via forwarder-blind using `blind_transf_exec` | +| `direct` | Each wallet swaps directly on the DEX pair | +| `sync` | Each wallet × each contract calls `blind_sync` (same-shard only) | +| `async1` | Each wallet × each contract calls `blind_async_v1` | +| `async2` | Each wallet × each contract calls `blind_async_v2` | +| `te` | Each wallet × each contract calls `blind_transf_exec` | #### Options (all methods) @@ -151,18 +177,22 @@ cargo run -- get-liquidity ### `drain` -Drain all WEGLD and USDC balances held by the deployed forwarder-blind contract back to the owner. -Useful to recover tokens left in the contract after transfer-execute swaps (which have no callback). +Drain all WEGLD and USDC balances held by the forwarder-blind contracts back to their owners. +Useful to recover tokens left in the contracts after transfer-execute swaps (which have no callback). ```bash cargo run -- drain ``` +For each contract address in `config.toml` (`contract_addresses`), the interactor looks up the +on-chain owner. If that owner is one of the registered wallets, it sends a drain transaction. +Contracts whose owner is not a registered wallet are skipped. + --- ## Configuration -The interactor reads `config.toml` from the current directory. Example: +The interactor reads `config.toml` from the current directory: ```toml chain_type = 'real' @@ -171,15 +201,79 @@ wegld_address = 'erd1...' # WEGLD swap contract pair_address = 'erd1...' # WEGLD/USDC DEX pair contract wegld_token_id = 'WEGLD-bd4d79' usdc_token_id = 'USDC-c76f1f' + +wallet_pem_paths = [ + 'path/to/wallet1.pem', + 'path/to/wallet2.pem', +] + +contract_addresses = [ + 'erd1...', + 'erd1...', +] ``` | Field | Description | |-------|-------------| -| `chain_type` | `real` for mainnet/testnet or `simulator` for the chain simulator | +| `chain_type` | `real` for mainnet/testnet, or `simulator` for the chain simulator | | `gateway_uri` | Gateway endpoint URL | | `wegld_address` | Address of the WEGLD swap contract | | `pair_address` | Address of the WEGLD/USDC DEX pair contract | | `wegld_token_id` | ESDT identifier for WEGLD | | `usdc_token_id` | ESDT identifier for USDC | +| `wallet_pem_paths` | List of PEM file paths, one per wallet. Paths are relative to the workspace root. If absent or empty, all operations are skipped with a warning. | +| `contract_addresses` | List of forwarder-blind contract addresses to target for swap-via-forwarder commands. | + +--- + +## Deploy output + +`deploy.toml` is written automatically after every `deploy` run. Example contents: + +```toml +# These are the last deployed addresses. Copy them to config.toml contract_addresses to use them. +contract_addresses = [ + "erd1...", + "erd1...", +] +``` -The deployed contract address is persisted automatically in `state.toml` after a successful `deploy`. +This file is **output-only** — no command reads it back. All commands (swap, drain, etc.) read +contract addresses exclusively from `contract_addresses` in `config.toml`. + +> **After deploying**, copy the addresses from `deploy.toml` into `config.toml` +> (`contract_addresses`) before running any other command. + +--- + +## Multiple wallets and contracts + +The interactor is designed to exercise the same call paths from many wallets simultaneously, +each going through its own contract instance. The relationship between wallets and contracts +is many-to-many. + +### `deploy` +One deploy transaction is submitted per wallet. The N resulting contract addresses are stored in +`deploy.toml` in the same order as the wallets in `wallet_pem_paths`. Copy those addresses into +`config.toml` (`contract_addresses`) before running any other command. + +### `wrap` +One wrap transaction is submitted per wallet. All transactions are batched and sent in parallel. + +### `swap1` / `swap2` — `direct` +One swap transaction is submitted per wallet, sent directly to the DEX pair. + +### `swap1` / `swap2` — `sync`, `async1`, `async2`, `te` +One transaction is submitted for every **(wallet, contract)** pair — the full Cartesian product. +For example, 3 wallets × 3 contracts = 9 transactions per command, all sent in a single batch. + +**Shard constraint for `sync`:** `blind_sync` uses a synchronous call, which requires the +forwarder contract and the DEX pair to be on the same shard. Any (wallet, contract) pair where +the contract's shard differs from the DEX pair's shard is skipped with a warning. + +### `drain` +Reads contract addresses from `config.toml` (`contract_addresses`), exactly like swap commands. +For each contract it fetches the on-chain owner and checks whether that address corresponds to +one of the registered wallets. Only contracts whose owner is a registered wallet receive a drain +transaction, so it is safe to run even when `config.toml` contains contracts deployed by other +parties. 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 a9f5bdd0a4..6d2096c439 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/config.toml +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/config.toml @@ -4,3 +4,18 @@ 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. +# 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', +] + +contract_addresses = [ + "erd1qqqqqqqqqqqqqpgqwlqhw27xz6j2cqh8w8v53l5fdulfu4rqqqqqdt5qat", + "erd1qqqqqqqqqqqqqpgq62w4nut0gedtfysdgpajryeap0jptvthqqqs27tpsu", + "erd1qqqqqqqqqqqqqpgqmsxxxae3df5eylg23rapr4r4j8kevqjdqqpqq4uamr", +] 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 ae0ec8d305..3d35813207 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 @@ -21,6 +21,13 @@ pub struct Config { 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, + /// Forwarder contract addresses to target for all swap transactions. + #[serde(default)] + pub contract_addresses: Vec, } impl Config { diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact.rs index d876a52d91..d34f11f1c5 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact.rs @@ -88,13 +88,24 @@ pub async fn forwarder_blind_cli() { Some(interact_cli::InteractCliCommand::Drain) => { interact.drain().await; } + Some(interact_cli::InteractCliCommand::Balances) => { + interact.balances().await; + } None => {} } } +#[derive(Copy, Clone)] +enum ForwarderMethod { + Sync, + AsyncV1, + AsyncV2, + TransfExec, +} + pub struct ContractInteract { pub interactor: Interactor, - pub wallet_address: Bech32Address, + pub wallet_addresses: Vec, pub config: Config, pub state: State, } @@ -108,51 +119,72 @@ impl ContractInteract { "contracts/feature-tests/composability/forwarder-blind/dex-interactor", ); - let wallet_address = interactor.register_wallet(test_wallets::simon()).await; + let wallet_addresses: Vec = if config.wallet_pem_paths.is_empty() { + println!("WARNING: no wallet_pem_paths 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 + }; interactor.generate_blocks_until_all_activations().await; ContractInteract { interactor, - wallet_address: wallet_address.into(), + wallet_addresses, config, state: State::load_state(), } } pub async fn deploy(&mut self) { - let new_address = self - .interactor - .tx() - .from(&self.wallet_address) - .gas(SimulateGas) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .init() - .code(FORWARDER_BLIND_CODE_PATH) - .returns(ReturnsNewBech32Address) - .run() - .await; - - println!("new address: {new_address}"); - self.state.set_contract_address(new_address); + let wallet_addresses = self.wallet_addresses.clone(); + let mut buffer = self.interactor.homogenous_call_buffer(); + for wallet in &wallet_addresses { + buffer.push_tx(|tx| { + tx.from(wallet) + .gas(80_000_000u64) + .typed(forwarder_blind_proxy::ForwarderBlindProxy) + .init() + .code(FORWARDER_BLIND_CODE_PATH) + .code_metadata(CodeMetadata::PAYABLE) + .returns(ReturnsNewBech32Address) + }); + } + let new_addresses: Vec = buffer.run().await; + for (i, addr) in new_addresses.iter().enumerate() { + println!("new address (wallet {i}): {addr}"); + } + self.state.set_contract_addresses(new_addresses); } pub async fn wrap_egld(&mut self, amount: u64) { - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(&self.config.wegld_address) - .gas(5_000_000) - .typed(wegld_proxy::EgldEsdtSwapProxy) - .wrap_egld() - .egld(amount) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; - - println!("Wrapping complete: status={status:?}, gas_used={gas_used:?}"); + let wallet_addresses = self.wallet_addresses.clone(); + let wegld_address = self.config.wegld_address.clone(); + let mut buffer = self.interactor.homogenous_call_buffer(); + for wallet in &wallet_addresses { + buffer.push_tx(|tx| { + tx.from(wallet) + .to(&wegld_address) + .gas(5_000_000u64) + .typed(wegld_proxy::EgldEsdtSwapProxy) + .wrap_egld() + .egld(amount) + .returns(ReturnsStatus) + .returns(ReturnsGasUsed) + }); + } + let results: Vec<(u64, u64)> = buffer.run().await; + for (wallet, (status, gas_used)) in wallet_addresses.iter().zip(results.iter()) { + println!( + "Wrapping complete (wallet: {wallet}): status={status:?}, gas_used={gas_used:?}" + ); + } } fn build_swap_function_call( @@ -167,281 +199,271 @@ impl ContractInteract { .into_function_call() } - pub async fn swap1_direct(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.usdc_token_id.clone(), usdc_amount_min); - - let response = self - .interactor - .tx() - .from(&self.wallet_address) - .to(&self.config.pair_address) - .gas(50_000_000u64) - .raw_data(swap_function_call) - .payment( - Payment::try_new(&self.config.wegld_token_id, 0, wegld_amount) - .expect("Amount must be > 0"), - ) - .original_result::>() - .returns(ReturnsResult) - .run() - .await; + async fn swap_direct_impl( + &mut self, + send_token_id: String, + send_amount: u64, + want_token_id: String, + want_amount_min: u64, + ) { + for wallet in self.wallet_addresses.clone() { + let fc = self.build_swap_function_call(&want_token_id, want_amount_min); + let response = self + .interactor + .tx() + .from(&wallet) + .to(&self.config.pair_address) + .gas(50_000_000u64) + .raw_data(fc) + .payment( + Payment::try_new(&send_token_id, 0, send_amount).expect("Amount must be > 0"), + ) + .original_result::>() + .returns(ReturnsResult) + .run() + .await; - println!("USDC received: {response}"); + println!("{want_token_id} received (wallet: {wallet}): {response}"); + } } - pub async fn swap2_direct(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.wegld_token_id.clone(), wegld_amount_min); - - let response = self - .interactor - .tx() - .from(&self.wallet_address) - .to(&self.config.pair_address) - .gas(50_000_000u64) - .raw_data(swap_function_call) - .payment( - Payment::try_new(&self.config.usdc_token_id, 0, usdc_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsRawResult) - .run() - .await; - - let first = response.get(0).clone(); - let payment = Payment::::top_decode(first).unwrap(); - println!("WEGLD received: {payment:?}"); + async fn swap_via_forwarder_impl( + &mut self, + send_token_id: String, + send_amount: u64, + want_token_id: String, + want_amount_min: u64, + method: ForwarderMethod, + ) { + let wallet_addresses = self.wallet_addresses.clone(); + let contract_addresses = self.config.contract_addresses.clone(); + let pair_address = self.config.pair_address.clone(); + let fc = self.build_swap_function_call(&want_token_id, want_amount_min); + + let mut buffer = self.interactor.homogenous_call_buffer(); + for wallet in &wallet_addresses { + for contract in &contract_addresses { + if matches!(method, ForwarderMethod::Sync) { + let contract_shard = contract.as_address().shard_of_3(); + let pair_shard = pair_address.as_address().shard_of_3(); + if contract_shard != pair_shard { + println!( + "WARNING: skipping swap from {wallet} to {contract} due to incompatible shard \ + (contract shard: {contract_shard}, pair shard: {pair_shard})" + ); + continue; + } + } + buffer.push_tx(|tx| { + let typed = tx + .from(wallet) + .to(contract) + .gas(70_000_000u64) + .typed(forwarder_blind_proxy::ForwarderBlindProxy); + match method { + ForwarderMethod::Sync => typed.blind_sync(&pair_address, fc.clone()), + ForwarderMethod::AsyncV1 => typed.blind_async_v1(&pair_address, fc.clone()), + ForwarderMethod::AsyncV2 => typed.blind_async_v2(&pair_address, fc.clone()), + ForwarderMethod::TransfExec => { + typed.blind_transf_exec(&pair_address, fc.clone()) + } + } + .payment( + Payment::try_new(&send_token_id, 0, send_amount) + .expect("Amount must be > 0"), + ) + .returns(PassValue(wallet.clone())) + .returns(PassValue(contract.clone())) + .returns(ReturnsStatus) + .returns(ReturnsGasUsed) + }); + } + } + for (wallet, contract, status, gas_used) in buffer.run().await { + println!( + "swap via forwarder (wallet: {wallet}, contract: {contract}): status={status:?}, gas_used={gas_used:?}" + ); + } } - pub async fn swap1_sync(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.usdc_token_id.clone(), usdc_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_sync(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.wegld_token_id, 0, wegld_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() + 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(); + self.swap_direct_impl(send_token, wegld_amount, want_token, usdc_amount_min) .await; - - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); } - pub async fn swap2_sync(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.wegld_token_id.clone(), wegld_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_sync(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.usdc_token_id, 0, usdc_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() + 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(); + self.swap_direct_impl(send_token, usdc_amount, want_token, wegld_amount_min) .await; - - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); } - pub async fn swap1_async1(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.usdc_token_id.clone(), usdc_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_async_v1(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.wegld_token_id, 0, wegld_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; - - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); + 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(); + self.swap_via_forwarder_impl( + send_token, + wegld_amount, + want_token, + usdc_amount_min, + ForwarderMethod::Sync, + ) + .await; } - pub async fn swap1_async2(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.usdc_token_id.clone(), usdc_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_async_v2(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.wegld_token_id, 0, wegld_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; - - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); + 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(); + self.swap_via_forwarder_impl( + send_token, + usdc_amount, + want_token, + wegld_amount_min, + ForwarderMethod::Sync, + ) + .await; } - pub async fn swap1_te(&mut self, wegld_amount: u64, usdc_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.usdc_token_id.clone(), usdc_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_transf_exec(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.wegld_token_id, 0, wegld_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; - - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); + 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(); + self.swap_via_forwarder_impl( + send_token, + wegld_amount, + want_token, + usdc_amount_min, + ForwarderMethod::AsyncV1, + ) + .await; } pub async fn swap2_async1(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.wegld_token_id.clone(), wegld_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_async_v1(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.usdc_token_id, 0, usdc_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; + let send_token = self.config.usdc_token_id.clone(); + let want_token = self.config.wegld_token_id.clone(); + self.swap_via_forwarder_impl( + send_token, + usdc_amount, + want_token, + wegld_amount_min, + ForwarderMethod::AsyncV1, + ) + .await; + } - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); + 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(); + self.swap_via_forwarder_impl( + send_token, + wegld_amount, + want_token, + usdc_amount_min, + ForwarderMethod::AsyncV2, + ) + .await; } pub async fn swap2_async2(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.wegld_token_id.clone(), wegld_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_async_v2(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.usdc_token_id, 0, usdc_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; + let send_token = self.config.usdc_token_id.clone(); + let want_token = self.config.wegld_token_id.clone(); + self.swap_via_forwarder_impl( + send_token, + usdc_amount, + want_token, + wegld_amount_min, + ForwarderMethod::AsyncV2, + ) + .await; + } - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); + 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(); + self.swap_via_forwarder_impl( + send_token, + wegld_amount, + want_token, + usdc_amount_min, + ForwarderMethod::TransfExec, + ) + .await; } pub async fn swap2_te(&mut self, usdc_amount: u64, wegld_amount_min: u64) { - let swap_function_call = - self.build_swap_function_call(&self.config.wegld_token_id.clone(), wegld_amount_min); - - let (status, gas_used) = self - .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(70_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .blind_transf_exec(&self.config.pair_address, swap_function_call) - .payment( - Payment::try_new(&self.config.usdc_token_id, 0, usdc_amount) - .expect("Amount must be > 0"), - ) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() - .await; - - println!("swap via forwarder: status={status:?}, gas_used={gas_used:?}"); + let send_token = self.config.usdc_token_id.clone(); + let want_token = self.config.wegld_token_id.clone(); + self.swap_via_forwarder_impl( + send_token, + usdc_amount, + want_token, + wegld_amount_min, + ForwarderMethod::TransfExec, + ) + .await; } pub async fn drain(&mut self) { - let contract_esdt = self - .interactor - .get_account_esdt(&self.state.current_address().to_address()) - .await; - - for token_id in [ - self.config.wegld_token_id.clone(), - self.config.usdc_token_id.clone(), - ] { - let balance = contract_esdt - .get(&token_id) - .map(|b| b.balance.parse::().unwrap_or(0)) - .unwrap_or(0); - - if balance == 0 { - println!("Drain {token_id}: no balance, skipping"); - continue; + 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(); + + // 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(); + for contract in &contract_addresses { + match self + .interactor + .get_registered_owner(contract.as_address()) + .await + { + Some(owner) => owner_per_contract.push((contract.clone(), owner)), + None => { + println!("Drain: no registered owner found for contract {contract}, skipping") + } } + } - println!("Drain {token_id}: balance={balance}"); - - let (status, gas_used) = self + // Fetch ESDT balances for each contract before opening the call buffer. + let mut esdt_per_contract = Vec::new(); + for (contract, owner) in &owner_per_contract { + let contract_esdt = self .interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_address()) - .gas(10_000_000u64) - .typed(forwarder_blind_proxy::ForwarderBlindProxy) - .drain(token_id.as_str(), 0u64) - .returns(ReturnsStatus) - .returns(ReturnsGasUsed) - .run() + .get_account_esdt(contract.as_address()) .await; + esdt_per_contract.push((contract.clone(), owner.clone(), contract_esdt)); + } + + let mut buffer = self.interactor.homogenous_call_buffer(); + for (contract, owner, contract_esdt) in &esdt_per_contract { + for token_id in [wegld_token_id.clone(), usdc_token_id.clone()] { + let balance = contract_esdt + .get(&token_id) + .and_then(|b| b.balance.parse::().ok()) + .unwrap_or(0); + if balance == 0 { + println!("Drain {token_id} (contract: {contract}): no balance, skipping"); + continue; + } + buffer.push_tx(|tx| { + tx.from(owner) + .to(contract) + .gas(10_000_000u64) + .typed(forwarder_blind_proxy::ForwarderBlindProxy) + .drain(EsdtTokenIdentifier::from(token_id.as_str()), 0u64) + .returns(PassValue(owner.clone())) + .returns(PassValue(contract.clone())) + .returns(PassValue(token_id.clone())) + .returns(ReturnsStatus) + .returns(ReturnsGasUsed) + }); + } + } - println!("Drain {token_id}: status={status:?}, gas_used={gas_used:?}"); + for (owner, contract, token_id, status, gas_used) in buffer.run().await { + println!( + "Drain {token_id} (owner: {owner}, contract: {contract}): status={status:?}, gas_used={gas_used:?}" + ); } } @@ -481,4 +503,49 @@ impl ContractInteract { println!("{} reserve: {usdc_reserve}", self.config.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(); + + println!("=== Wallet Balances ==="); + for wallet in &self.wallet_addresses.clone() { + let account = self.interactor.get_account(wallet.as_address()).await; + let esdt = self.interactor.get_account_esdt(wallet.as_address()).await; + let wegld = esdt + .get(&wegld_token_id) + .map(|b| b.balance.as_str()) + .unwrap_or("0"); + let usdc = esdt + .get(&usdc_token_id) + .map(|b| b.balance.as_str()) + .unwrap_or("0"); + println!(" wallet: {wallet}"); + println!(" EGLD: {}", account.balance); + println!(" {wegld_token_id}: {wegld}"); + println!(" {usdc_token_id}: {usdc}"); + } + + let contract_addresses = self.config.contract_addresses.clone(); + if !contract_addresses.is_empty() { + println!("=== Contract Balances ==="); + for contract in &contract_addresses { + let esdt = self + .interactor + .get_account_esdt(contract.as_address()) + .await; + let wegld = esdt + .get(&wegld_token_id) + .map(|b| b.balance.as_str()) + .unwrap_or("0"); + let usdc = esdt + .get(&usdc_token_id) + .map(|b| b.balance.as_str()) + .unwrap_or("0"); + println!(" contract: {contract}"); + println!(" {wegld_token_id}: {wegld}"); + println!(" {usdc_token_id}: {usdc}"); + } + } + } } diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact_cli.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact_cli.rs index c1a0e2434e..1870047140 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact_cli.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/interact_cli.rs @@ -35,6 +35,11 @@ pub enum InteractCliCommand { about = "Drain WEGLD and USDC balances from the forwarder contract back to the owner" )] Drain, + #[command( + name = "balances", + about = "Display EGLD (wallets only), WEGLD, and USDC balances for all wallets and contracts" + )] + Balances, } #[derive(Clone, PartialEq, Eq, Debug, Args)] diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/state.rs b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/state.rs index 4ae4099110..300f6699bd 100644 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/state.rs +++ b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/src/state.rs @@ -1,17 +1,15 @@ use multiversx_sc_snippets::imports::*; use serde::{Deserialize, Serialize}; -use std::{ - io::{Read, Write}, - path::Path, -}; +use std::{io::Read, path::Path}; /// State file -const STATE_FILE: &str = "state.toml"; +const STATE_FILE: &str = "deploy.toml"; /// ForwarderBlind Interact state #[derive(Debug, Default, Serialize, Deserialize)] pub struct State { - contract_address: Option, + #[serde(default)] + last_deployed: Vec, } impl State { @@ -27,24 +25,22 @@ impl State { } } - /// Sets the contract address - pub fn set_contract_address(&mut self, address: Bech32Address) { - self.contract_address = Some(address); + pub fn set_contract_addresses(&mut self, addresses: Vec) { + self.last_deployed = addresses; } - /// Returns the contract address - pub fn current_address(&self) -> &Bech32Address { - self.contract_address - .as_ref() - .expect("no known contract, deploy first") + pub fn contract_addresses(&self) -> &[Bech32Address] { + &self.last_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(); + let content = format!( + "# These are the last deployed addresses. Copy them to config.toml contract_addresses to use them.\n{}", + toml::to_string_pretty(self).unwrap() + ); + std::fs::write(STATE_FILE, content).unwrap(); } } diff --git a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/state.toml b/contracts/feature-tests/composability/forwarder-blind/dex-interactor/state.toml deleted file mode 100644 index f77b716ede..0000000000 --- a/contracts/feature-tests/composability/forwarder-blind/dex-interactor/state.toml +++ /dev/null @@ -1 +0,0 @@ -contract_address = "erd1qqqqqqqqqqqqqpgqsj4af750fae34tenvlsuz647k5dq2355qqqs9un9k8" diff --git a/framework/snippets/src/interactor/interactor_base.rs b/framework/snippets/src/interactor/interactor_base.rs index 3955b21460..bf8b455901 100644 --- a/framework/snippets/src/interactor/interactor_base.rs +++ b/framework/snippets/src/interactor/interactor_base.rs @@ -108,6 +108,10 @@ where &self.network_config.address_hrp } + pub fn is_registered_wallet(&self, address: &Address) -> bool { + self.sender_map.contains_key(address) + } + pub fn get_accounts_from_file(&self) -> Vec { let file_path = self.get_state_file_path(); diff --git a/framework/snippets/src/interactor/interactor_sender.rs b/framework/snippets/src/interactor/interactor_sender.rs index a4f90b9f37..7f6265a0e8 100644 --- a/framework/snippets/src/interactor/interactor_sender.rs +++ b/framework/snippets/src/interactor/interactor_sender.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::sdk::{data::transaction::Transaction, wallet::Wallet}; -use multiversx_sc_scenario::multiversx_sc::types::Address; +use multiversx_sc_scenario::{imports::Bech32Address, multiversx_sc::types::Address}; use multiversx_sdk::data::account::Account; use multiversx_sdk::data::esdt::EsdtBalance; use multiversx_sdk::gateway::{ @@ -48,6 +48,19 @@ where .expect("failed to retrieve account") } + /// Fetches the on-chain owner of `contract` and returns it as a [`Bech32Address`] + /// if that owner is a registered wallet, or `None` otherwise. + pub async fn get_registered_owner(&self, contract: &Address) -> Option { + let account = self.get_account(contract).await; + let owner_str = account.owner_address.filter(|s| !s.is_empty())?; + let owner = Bech32Address::from_bech32_string(owner_str); + if self.is_registered_wallet(owner.as_address()) { + Some(owner) + } else { + None + } + } + pub async fn get_account_esdt(&self, address: &Address) -> HashMap { self.proxy .request(GetAccountEsdtTokensRequest::new( diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 7a8c6cea70..2ffba5d210 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -119,7 +119,7 @@ impl Wallet { } pub fn from_pem_file(file_path: &str) -> Result { - let contents = std::fs::read_to_string(file_path).unwrap(); + let contents = std::fs::read_to_string(file_path)?; Self::from_pem_file_contents(contents) }