diff --git a/networks/movement/movement-client/Cargo.toml b/networks/movement/movement-client/Cargo.toml index 5680c517d..6ce7225bd 100644 --- a/networks/movement/movement-client/Cargo.toml +++ b/networks/movement/movement-client/Cargo.toml @@ -40,6 +40,14 @@ name = "movement-tests-e2e-ggp-gas-fee" path = "src/bin/e2e/ggp_gas_fee.rs" +[[bin]] +name = "movement-tests-e2e-ggp-fund-fee" +path = "src/bin/e2e/ggp_fund.rs" + +[[bin]] +name = "movement-tests-e2e-complex-ggp-gas" +path = "src/bin/e2e/complex_ggp_e2e.rs" + [dependencies] aptos-sdk = { workspace = true } aptos-types = { workspace = true } diff --git a/networks/movement/movement-client/src/bin/e2e/complex_ggp_e2e.rs b/networks/movement/movement-client/src/bin/e2e/complex_ggp_e2e.rs new file mode 100644 index 000000000..87e611c6c --- /dev/null +++ b/networks/movement/movement-client/src/bin/e2e/complex_ggp_e2e.rs @@ -0,0 +1,223 @@ +use anyhow::Context; +use aptos_sdk::rest_client::{ + aptos_api_types::{Address, EntryFunctionId, IdentifierWrapper, MoveModuleId, ViewRequest}, + Response, +}; +use aptos_sdk::types::account_address::AccountAddress; +use movement_client::{ + coin_client::CoinClient, + rest_client::{Client, FaucetClient}, + types::LocalAccount, +}; +use once_cell::sync::Lazy; +use std::str::FromStr; +use tracing; +use url::Url; + +static SUZUKA_CONFIG: Lazy = Lazy::new(|| { + let dot_movement = dot_movement::DotMovement::try_from_env().unwrap(); + let config = dot_movement.try_get_config_from_json::().unwrap(); + config +}); + +static NODE_URL: Lazy = Lazy::new(|| { + let node_connection_address = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_hostname + .clone(); + let node_connection_port = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_port + .clone(); + let node_connection_url = + format!("http://{}:{}", node_connection_address, node_connection_port); + Url::from_str(node_connection_url.as_str()).unwrap() +}); + +static FAUCET_URL: Lazy = Lazy::new(|| { + let faucet_listen_address = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_hostname + .clone(); + let faucet_listen_port = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_port + .clone(); + let faucet_listen_url = format!("http://{}:{}", faucet_listen_address, faucet_listen_port); + Url::from_str(faucet_listen_url.as_str()).unwrap() +}); + +const NUM_ACCOUNTS: usize = 5; +const TRANSACTIONS_PER_ACCOUNT: usize = 100; +const INITIAL_FUNDING: u64 = 10_000_000; +const TRANSFER_AMOUNT: u64 = 100; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let rest_client = Client::new(NODE_URL.clone()); + let faucet_client = FaucetClient::new(FAUCET_URL.clone(), NODE_URL.clone()); + let coin_client = CoinClient::new(&rest_client); + + let ggp_address = get_governed_gas_pool_address(&rest_client).await?; + + let mut accounts = create_and_fund_accounts(&faucet_client, NUM_ACCOUNTS).await?; + + // Get initial gas pool balance + let initial_pool_balance = coin_client + .get_account_balance(&ggp_address) + .await + .context("Failed to get initial gas pool balance")?; + tracing::info!("Initial gas pool balance: {}", initial_pool_balance); + + // Execute multiple rounds of transactions between accounts + execute_transaction_rounds(&mut accounts, &coin_client, &rest_client).await?; + + // Get final gas pool balance + let final_pool_balance = coin_client + .get_account_balance(&ggp_address) + .await + .context("Failed to get final gas pool balance")?; + tracing::info!("Final gas pool balance: {}", final_pool_balance); + + // Verify gas fees were collected + assert!( + final_pool_balance > initial_pool_balance, + "Gas pool balance did not increase after {} transactions", + NUM_ACCOUNTS * TRANSACTIONS_PER_ACCOUNT + ); + + tracing::info!("Total gas fees collected: {}", final_pool_balance - initial_pool_balance); + + Ok(()) +} + +async fn get_governed_gas_pool_address( + rest_client: &Client, +) -> Result { + let view_req = ViewRequest { + function: EntryFunctionId { + module: MoveModuleId { + address: Address::from_str("0x1").unwrap(), + name: IdentifierWrapper::from_str("governed_gas_pool").unwrap(), + }, + name: IdentifierWrapper::from_str("governed_gas_pool_address").unwrap(), + }, + type_arguments: vec![], + arguments: vec![], + }; + + let view_res: Response> = rest_client + .view(&view_req, None) + .await + .context("Failed to get governed gas pool address")?; + + let inner_value = serde_json::to_value(view_res.inner()) + .context("Failed to convert response inner to serde_json::Value")?; + + let ggp_address: Vec = + serde_json::from_value(inner_value).context("Failed to deserialize AddressResponse")?; + + Ok(AccountAddress::from_str(&ggp_address[0]).expect("Failed to parse address")) +} + +async fn create_and_fund_accounts( + faucet_client: &FaucetClient, + num_accounts: usize, +) -> Result, anyhow::Error> { + let mut accounts = Vec::with_capacity(num_accounts); + + for i in 0..num_accounts { + let account = LocalAccount::generate(&mut rand::rngs::OsRng); + tracing::info!("Creating account {}: {}", i, account.address()); + + faucet_client + .fund(account.address(), INITIAL_FUNDING) + .await + .context(format!("Failed to fund account {}", i))?; + + accounts.push(account); + } + + Ok(accounts) +} + +async fn execute_transaction_rounds<'a>( + accounts: &mut [LocalAccount], + coin_client: &'a CoinClient<'a>, + rest_client: &'a Client, +) -> Result<(), anyhow::Error> { + for round in 0..TRANSACTIONS_PER_ACCOUNT { + tracing::info!("Starting transaction round {}", round); + + for i in 0..accounts.len() { + let receiver_idx = (i + 1) % accounts.len(); + + tracing::info!( + "Attempting transfer: Account {} -> Account {}, Round {}", + i, + receiver_idx, + round + ); + + let receiver_address = accounts[receiver_idx].address(); + tracing::info!("Receiver address retrieved: {}", receiver_address); + + let txn_hash = if receiver_idx <= i { + tracing::info!("Using first branch of split_at_mut"); + let (left, right) = accounts.split_at_mut(i + 1); + let sender = &mut left[i]; + + tracing::info!("Initiating transfer..."); + coin_client + .transfer(sender, receiver_address, TRANSFER_AMOUNT, None) + .await + .context(format!( + "Failed to submit transfer from account {} to {}", + i, receiver_idx + ))? + } else { + tracing::info!("Using second branch of split_at_mut"); + let (left, right) = accounts.split_at_mut(i + 1); + let sender = &mut left[i]; + + tracing::info!("Initiating transfer..."); + coin_client + .transfer(sender, receiver_address, TRANSFER_AMOUNT, None) + .await + .context(format!( + "Failed to submit transfer from account {} to {}", + i, receiver_idx + ))? + }; + + tracing::info!("Transfer submitted. Transaction hash: {:?}", txn_hash); + + tracing::info!("Waiting for transaction confirmation..."); + rest_client + .wait_for_transaction(&txn_hash) + .await + .context("Failed when waiting for transfer transaction")?; + + tracing::info!( + "Transaction confirmed: Account {} -> Account {}, Round {}", + i, + receiver_idx, + round + ); + + if round % 10 == 0 && i == 0 { + tracing::info!("Completed {} transactions per account", round + 1); + } + } + } + + Ok(()) +} diff --git a/networks/movement/movement-client/src/bin/e2e/ggp_fund.rs b/networks/movement/movement-client/src/bin/e2e/ggp_fund.rs new file mode 100644 index 000000000..70418c8ac --- /dev/null +++ b/networks/movement/movement-client/src/bin/e2e/ggp_fund.rs @@ -0,0 +1,181 @@ +use anyhow::Context; +use aptos_sdk::rest_client::{ + aptos_api_types::{Address, EntryFunctionId, IdentifierWrapper, MoveModuleId, ViewRequest}, + Response, +}; +use aptos_sdk::types::account_address::AccountAddress; +use movement_client::{ + coin_client::CoinClient, + rest_client::{Client, FaucetClient}, + types::LocalAccount, +}; +use once_cell::sync::Lazy; +use std::str::FromStr; +use tracing; +use url::Url; + +static SUZUKA_CONFIG: Lazy = Lazy::new(|| { + let dot_movement = dot_movement::DotMovement::try_from_env().unwrap(); + let config = dot_movement.try_get_config_from_json::().unwrap(); + config +}); + +static NODE_URL: Lazy = Lazy::new(|| { + let node_connection_address = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_hostname + .clone(); + let node_connection_port = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_port + .clone(); + let node_connection_url = + format!("http://{}:{}", node_connection_address, node_connection_port); + Url::from_str(node_connection_url.as_str()).unwrap() +}); + +static FAUCET_URL: Lazy = Lazy::new(|| { + let faucet_listen_address = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_hostname + .clone(); + let faucet_listen_port = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_port + .clone(); + let faucet_listen_url = format!("http://{}:{}", faucet_listen_address, faucet_listen_port); + Url::from_str(faucet_listen_url.as_str()).unwrap() +}); + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let rest_client = Client::new(NODE_URL.clone()); + let faucet_client = FaucetClient::new(FAUCET_URL.clone(), NODE_URL.clone()); + let coin_client = CoinClient::new(&rest_client); + + // Create accounts + let mut gas_payer = LocalAccount::generate(&mut rand::rngs::OsRng); + let beneficiary = LocalAccount::generate(&mut rand::rngs::OsRng); + + tracing::info!("Created test accounts"); + tracing::debug!( + "Gas payer address: {}, Beneficiary address: {}", + gas_payer.address(), + beneficiary.address() + ); + + // Fund gas payer and get initial balance + faucet_client + .fund(gas_payer.address(), 1_000_000) + .await + .context("Failed to fund gas payer account")?; + + let initial_gas_payer_balance = coin_client + .get_account_balance(&gas_payer.address()) + .await + .context("Failed to get initial gas payer balance")?; + tracing::info!("Initial gas payer balance: {}", initial_gas_payer_balance); + + // Fund beneficiary account + faucet_client + .create_account(beneficiary.address()) + .await + .context("Failed to create beneficiary account")?; + + // Get initial beneficiary balance + let initial_beneficiary_balance = coin_client + .get_account_balance(&beneficiary.address()) + .await + .context("Failed to get initial beneficiary balance")?; + tracing::info!("Initial beneficiary balance: {}", initial_beneficiary_balance); + + // Get the governed gas pool address + let view_req = ViewRequest { + function: EntryFunctionId { + module: MoveModuleId { + address: Address::from_str("0x1").unwrap(), + name: IdentifierWrapper::from_str("governed_gas_pool").unwrap(), + }, + name: IdentifierWrapper::from_str("governed_gas_pool_address").unwrap(), + }, + type_arguments: vec![], + arguments: vec![], + }; + + let view_res: Response> = rest_client + .view(&view_req, None) + .await + .context("Failed to get governed gas pool address")?; + + let inner_value = serde_json::to_value(view_res.inner()) + .context("Failed to convert response inner to serde_json::Value")?; + + let ggp_address: Vec = + serde_json::from_value(inner_value).context("Failed to deserialize AddressResponse")?; + + let ggp_account_address = + AccountAddress::from_str(&ggp_address[0]).expect("Failed to parse address"); + + // Get initial gas pool balance + let initial_pool_balance = coin_client + .get_account_balance(&ggp_account_address) + .await + .context("Failed to get initial gas pool balance")?; + tracing::info!("Initial gas pool balance: {}", initial_pool_balance); + + // Get gas payer balance before transfer + let pre_transfer_gas_payer_balance = coin_client + .get_account_balance(&gas_payer.address()) + .await + .context("Failed to get pre-transfer gas payer balance")?; + tracing::info!("Gas payer balance before transfer: {}", pre_transfer_gas_payer_balance); + + // Make the transfer + let txn_hash = coin_client + .transfer(&mut gas_payer, beneficiary.address(), 1_000, None) + .await + .context("Failed to submit transfer transaction")?; + + // Wait for transaction and get detailed info + rest_client + .wait_for_transaction(&txn_hash) + .await + .context("Failed when waiting for transfer transaction")?; + + // Get all final balances + let final_gas_payer_balance = coin_client + .get_account_balance(&gas_payer.address()) + .await + .context("Failed to get final gas payer balance")?; + + let final_beneficiary_balance = coin_client + .get_account_balance(&beneficiary.address()) + .await + .context("Failed to get final beneficiary balance")?; + + let final_pool_balance = coin_client + .get_account_balance(&ggp_account_address) + .await + .context("Failed to get final gas pool balance")?; + + tracing::info!("Final gas pool balance: {}", final_pool_balance); + tracing::info!("Final beneficiary balance: {}", final_beneficiary_balance); + tracing::info!("Final gas payer balance: {}", final_gas_payer_balance); + + // Verify beneficiary received full amount + assert_eq!( + final_beneficiary_balance - initial_beneficiary_balance, + 1000, + "Beneficiary did not receive the full amount" + ); + + Ok(()) +} diff --git a/process-compose/movement-full-node/process-compose.test-complex-ggp-gas.yml b/process-compose/movement-full-node/process-compose.test-complex-ggp-gas.yml new file mode 100644 index 000000000..71e9d3dc8 --- /dev/null +++ b/process-compose/movement-full-node/process-compose.test-complex-ggp-gas.yml @@ -0,0 +1,13 @@ +version: "3" + +environment: + +processes: + test-ggp-gas-fee: + command: | + cargo run --bin movement-tests-e2e-complex-ggp-gas + depends_on: + movement-full-node: + condition: process_healthy + movement-faucet: + condition: process_healthy diff --git a/process-compose/movement-full-node/process-compose.test-ggp-gas-fund-fee.yml b/process-compose/movement-full-node/process-compose.test-ggp-gas-fund-fee.yml new file mode 100644 index 000000000..ce33d4382 --- /dev/null +++ b/process-compose/movement-full-node/process-compose.test-ggp-gas-fund-fee.yml @@ -0,0 +1,13 @@ +version: "3" + +environment: + +processes: + test-ggp-gas-fund-fee: + command: | + cargo run --bin movement-tests-e2e-ggp-fund-fee + depends_on: + movement-full-node: + condition: process_healthy + movement-faucet: + condition: process_healthy