diff --git a/bin/e2e-test-client/src/config.rs b/bin/e2e-test-client/src/config.rs index 9d537043351..be2bddf5c78 100644 --- a/bin/e2e-test-client/src/config.rs +++ b/bin/e2e-test-client/src/config.rs @@ -65,9 +65,9 @@ impl Default for SuiteConfig { #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub struct ClientConfig { - // overrides the default endpoint for the suite + // Overrides the default endpoint for the suite pub endpoint: Option, - // the account to use + // The account to use pub secret: SecretKey, } diff --git a/bin/e2e-test-client/src/lib.rs b/bin/e2e-test-client/src/lib.rs index 36ee3ea5413..b90ebd36f71 100644 --- a/bin/e2e-test-client/src/lib.rs +++ b/bin/e2e-test-client/src/lib.rs @@ -2,6 +2,8 @@ use crate::{ config::SuiteConfig, test_context::TestContext, }; +use futures::future::BoxFuture; +use futures::FutureExt; // For using the `.boxed()` method use libtest_mimic::{ Arguments, Failed, @@ -11,6 +13,7 @@ use std::{ env, fs, future::Future, + sync::Arc, time::Duration, }; @@ -22,110 +25,72 @@ pub mod test_context; pub mod tests; pub fn main_body(config: SuiteConfig, mut args: Arguments) { - fn with_cloned( - config: &SuiteConfig, - f: impl FnOnce(SuiteConfig) -> anyhow::Result<(), Failed>, - ) -> impl FnOnce() -> anyhow::Result<(), Failed> { - let config = config.clone(); - move || f(config) - } - - // If we run tests in parallel they may fail because try to use the same state like UTXOs. + // If we run tests in parallel they may fail because they try to use the same state like UTXOs. args.test_threads = Some(1); let tests = vec![ - Trial::test( - "can transfer from alice to bob", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::transfers::basic_transfer(&ctx).await - }) - }), - ), - Trial::test( + create_test("can transfer from alice to bob", &config, tests::transfers::basic_transfer), + create_test( "can transfer from alice to bob and back", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::transfers::transfer_back(&ctx).await - }) - }), - ), - Trial::test( - "can collect fee from alice", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::collect_fee::collect_fee(&ctx).await - }) - }), + &config, + tests::transfers::transfer_back, ), - Trial::test( - "can execute script and get receipts", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::transfers::transfer_back(&ctx).await - }) - }), - ), - Trial::test( + create_test("can collect fee from alice", &config, tests::collect_fee::collect_fee), + create_test("can execute script and get receipts", &config, tests::script::receipts), + create_test( "can dry run transfer script and get receipts", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::script::dry_run(&ctx).await - })?; - Ok(()) - }), + &config, + tests::script::dry_run, ), - Trial::test( + create_test( "can dry run multiple transfer scripts and get receipts", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::script::dry_run_multiple_txs(&ctx).await - })?; - Ok(()) - }), + &config, + tests::script::dry_run_multiple_txs, ), - Trial::test( + create_test( "dry run script that touches the contract with large state", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::script::run_contract_large_state(&ctx).await - })?; - Ok(()) - }), + &config, + tests::script::run_contract_large_state, ), - Trial::test( + create_test( "dry run transaction from `arbitrary_tx.raw` file", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::script::arbitrary_transaction(&ctx).await - })?; - Ok(()) - }), + &config, + tests::script::arbitrary_transaction, ), - Trial::test( + create_test( "can deploy a large contract", - with_cloned(&config, |config| { - async_execute(async { - let ctx = TestContext::new(config).await; - tests::transfers::transfer_back(&ctx).await - }) - }), + &config, + tests::contracts::deploy_large_contract, ), ]; libtest_mimic::run(&args, tests).exit(); } +// Helper function to reduce code duplication when creating tests +fn create_test( + name: &'static str, + config: &SuiteConfig, + test_fn: F, +) -> Trial +where + F: FnOnce(Arc) -> BoxFuture<'static, anyhow::Result<(), Failed>> + + Send + + Sync + + 'static, +{ + let cloned_config = config.clone(); + Trial::test( + name, + move || async_execute(async move { + let ctx = Arc::new(TestContext::new(cloned_config).await); + test_fn(ctx).await + }), + ) +} + pub fn load_config_env() -> SuiteConfig { - // load from env var + // Load from environment variable env::var_os(CONFIG_FILE_KEY) .map(|path| load_config(path.to_string_lossy().to_string())) .unwrap_or_default() @@ -136,9 +101,7 @@ pub fn load_config(path: String) -> SuiteConfig { toml::from_slice(&file).unwrap() } -fn async_execute>>( - func: F, -) -> Result<(), Failed> { +fn async_execute>>(func: F) -> Result<(), Failed> { tokio::runtime::Builder::new_current_thread() .enable_all() .build() diff --git a/bin/e2e-test-client/src/test_context.rs b/bin/e2e-test-client/src/test_context.rs index 1cb6bcb8b07..d5a96468a20 100644 --- a/bin/e2e-test-client/src/test_context.rs +++ b/bin/e2e-test-client/src/test_context.rs @@ -1,55 +1,32 @@ //! Utilities and helper methods for writing tests -use anyhow::{ - anyhow, - Context, -}; +use anyhow::{anyhow, Context}; use fuel_core_chain_config::ContractConfig; use fuel_core_client::client::{ - types::{ - CoinType, - TransactionStatus, - }, + types::{CoinType, TransactionStatus}, FuelClient, }; use fuel_core_types::{ - fuel_asm::{ - op, - GTFArgs, - RegId, - }, + fuel_asm::{op, GTFArgs, RegId}, fuel_crypto::PublicKey, fuel_tx::{ - ConsensusParameters, - Contract, - ContractId, - Finalizable, - Input, - Output, - Transaction, - TransactionBuilder, - TxId, - UniqueIdentifier, - UtxoId, + ConsensusParameters, Contract, ContractId, Finalizable, Input, Output, Transaction, + TransactionBuilder, TxId, UniqueIdentifier, UtxoId, }, fuel_types::{ canonical::Serialize, - Address, - AssetId, - Salt, + Address, AssetId, Salt, }, fuel_vm::SecretKey, }; use itertools::Itertools; -use crate::config::{ - ClientConfig, - SuiteConfig, -}; +use crate::config::{ClientConfig, SuiteConfig}; // The base amount needed to cover the cost of a simple transaction pub const BASE_AMOUNT: u64 = 100_000_000; +#[derive(Clone)] pub struct TestContext { pub alice: Wallet, pub bob: Wallet, @@ -61,8 +38,8 @@ impl TestContext { let alice_client = Self::new_client(config.endpoint.clone(), &config.wallet_a); let bob_client = Self::new_client(config.endpoint.clone(), &config.wallet_b); Self { - alice: Wallet::new(config.wallet_a.secret, alice_client).await, - bob: Wallet::new(config.wallet_b.secret, bob_client).await, + alice: Wallet::new(config.wallet_a.secret.clone(), alice_client).await, + bob: Wallet::new(config.wallet_b.secret.clone(), bob_client).await, config, } } @@ -84,7 +61,7 @@ impl Wallet { pub async fn new(secret: SecretKey, client: FuelClient) -> Self { let public_key: PublicKey = (&secret).into(); let address = Input::owner(&public_key); - // get consensus params + // Get consensus params let consensus_params = client .chain_info() .await @@ -98,7 +75,7 @@ impl Wallet { } } - /// returns the balance associated with a wallet + /// Returns the balance associated with a wallet pub async fn balance(&self, asset_id: Option) -> anyhow::Result { self.client .balance(&self.address, Some(&asset_id.unwrap_or_default())) @@ -122,13 +99,13 @@ impl Wallet { ) -> anyhow::Result { let asset_id = asset_id.unwrap_or(*self.consensus_params.base_asset_id()); let total_amount = transfer_amount + BASE_AMOUNT; - // select coins + // Select coins let coins = &self .client .coins_to_spend(&self.address, vec![(asset_id, total_amount, None)], None) .await?[0]; - // build transaction + // Build transaction let mut tx = TransactionBuilder::script(Default::default(), Default::default()); tx.max_fee_limit(BASE_AMOUNT); tx.script_gas_limit(0); @@ -136,7 +113,7 @@ impl Wallet { for coin in coins { if let CoinType::Coin(coin) = coin { tx.add_unsigned_coin_input( - self.secret, + self.secret.clone(), coin.utxo_id, coin.amount, coin.asset_id, @@ -165,7 +142,7 @@ impl Wallet { coinbase_contract: ContractId, asset_id: AssetId, ) -> anyhow::Result { - // select coins + // Select coins let coins = &self .client .coins_to_spend( @@ -190,7 +167,7 @@ impl Wallet { op::ret(RegId::ONE), ]; - // build transaction + // Build transaction let mut tx_builder = TransactionBuilder::script( script.into_iter().collect(), asset_id @@ -216,7 +193,7 @@ impl Wallet { for coin in coins { if let CoinType::Coin(coin) = coin { tx_builder.add_unsigned_coin_input( - self.secret, + self.secret.clone(), coin.utxo_id, coin.amount, coin.asset_id, @@ -258,10 +235,10 @@ impl Wallet { println!("submitting tx... {:?}", tx_id); let status = self.client.submit_and_await_commit(&tx).await?; - // we know the transferred coin should be output 0 from above + // We know the transferred coin should be output 0 from above let transferred_utxo = UtxoId::new(tx_id, 0); - // get status and return the utxo id of transferred coin + // Get status and return the utxo id of transferred coin Ok(TransferResult { tx_id, transferred_utxo, @@ -277,7 +254,7 @@ impl Wallet { ) -> anyhow::Result<()> { let asset_id = *self.consensus_params.base_asset_id(); let total_amount = BASE_AMOUNT; - // select coins + // Select coins let coins = &self .client .coins_to_spend(&self.address, vec![(asset_id, total_amount, None)], None) @@ -301,7 +278,7 @@ impl Wallet { for coin in coins { if let CoinType::Coin(coin) = coin { tx.add_unsigned_coin_input( - self.secret, + self.secret.clone(), coin.utxo_id, coin.amount, coin.asset_id, @@ -329,10 +306,8 @@ impl Wallet { .submit_and_await_commit(&tx.clone().into()) .await?; - // check status of contract deployment - if let TransactionStatus::Failure { .. } | TransactionStatus::SqueezedOut { .. } = - &status - { + // Check status of contract deployment + if let TransactionStatus::Failure { .. } | TransactionStatus::SqueezedOut { .. } = &status { return Err(anyhow!(format!("unexpected transaction status {status:?}"))); } diff --git a/bin/e2e-test-client/src/tests/collect_fee.rs b/bin/e2e-test-client/src/tests/collect_fee.rs index d7dde4d534e..69dbd202eb7 100644 --- a/bin/e2e-test-client/src/tests/collect_fee.rs +++ b/bin/e2e-test-client/src/tests/collect_fee.rs @@ -1,40 +1,46 @@ use crate::test_context::TestContext; use fuel_core_types::fuel_tx::Receipt; +use futures::future::BoxFuture; +use futures::FutureExt; use libtest_mimic::Failed; +use std::sync::Arc; -// Alice collects tokens from coinbase contract. -pub async fn collect_fee(ctx: &TestContext) -> Result<(), Failed> { - let tx = ctx - .alice - .collect_fee_tx( - ctx.config.coinbase_contract_id, - *ctx.alice.consensus_params.base_asset_id(), - ) - .await?; - let tx_status = ctx.alice.client.submit_and_await_commit(&tx).await?; +// Alice collects tokens from the coinbase contract. +pub fn collect_fee(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + let tx = ctx + .alice + .collect_fee_tx( + ctx.config.coinbase_contract_id, + *ctx.alice.consensus_params.base_asset_id(), + ) + .await?; + let tx_status = ctx.alice.client.submit_and_await_commit(&tx).await?; - if !matches!( - tx_status, - fuel_core_client::client::types::TransactionStatus::Success { .. } - ) { - return Err("collect fee transaction is not successful".into()) - } + if !matches!( + tx_status, + fuel_core_client::client::types::TransactionStatus::Success { .. } + ) { + return Err("collect fee transaction is not successful".into()); + } - let receipts = match &tx_status { - fuel_core_client::client::types::TransactionStatus::Success { - receipts, .. - } => Some(receipts), - _ => None, - }; - let receipts = receipts.ok_or("collect fee transaction doesn't have receipts")?; + let receipts = match &tx_status { + fuel_core_client::client::types::TransactionStatus::Success { receipts, .. } => { + Some(receipts) + } + _ => None, + }; + let receipts = receipts.ok_or("collect fee transaction doesn't have receipts")?; - if !receipts - .iter() - .any(|receipt| matches!(receipt, Receipt::TransferOut { .. })) - { - let msg = format!("TransferOut receipt not found in receipts: {:?}", receipts); - return Err(msg.into()) - } + if !receipts + .iter() + .any(|receipt| matches!(receipt, Receipt::TransferOut { .. })) + { + let msg = format!("TransferOut receipt not found in receipts: {:?}", receipts); + return Err(msg.into()); + } - Ok(()) + Ok(()) + } + .boxed() } diff --git a/bin/e2e-test-client/src/tests/contracts.rs b/bin/e2e-test-client/src/tests/contracts.rs index b3c70397028..7f81d0b460c 100644 --- a/bin/e2e-test-client/src/tests/contracts.rs +++ b/bin/e2e-test-client/src/tests/contracts.rs @@ -1,31 +1,37 @@ use crate::test_context::TestContext; use fuel_core_chain_config::ContractConfig; +use futures::future::BoxFuture; +use futures::FutureExt; use libtest_mimic::Failed; +use std::sync::Arc; use std::time::Duration; use tokio::time::timeout; -// deploy a large contract (16mb) -pub async fn deploy_large_contract(ctx: &TestContext) -> Result<(), Failed> { - // generate large bytecode - let bytecode = if ctx.config.full_test { - // 16mib takes a long time to process due to contract root calculations - // it is under an optional flag to avoid slowing down every CI run. - vec![0u8; 16 * 1024 * 1024] - } else { - vec![0u8; 1024 * 1024] - }; - let mut contract_config = ContractConfig { - contract_id: Default::default(), - code: bytecode, - ..Default::default() - }; - let salt = Default::default(); - contract_config.update_contract_id(salt); +// Deploy a large contract (16 MB) +pub fn deploy_large_contract(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + // Generate large bytecode + let bytecode = if ctx.config.full_test { + // 16 MiB takes a long time to process due to contract root calculations + // It is under an optional flag to avoid slowing down every CI run. + vec![0u8; 16 * 1024 * 1024] + } else { + vec![0u8; 1024 * 1024] + }; + let mut contract_config = ContractConfig { + contract_id: Default::default(), + code: bytecode, + ..Default::default() + }; + let salt = Default::default(); + contract_config.update_contract_id(salt); - let deployment_request = ctx.bob.deploy_contract(contract_config, salt); + let deployment_request = ctx.bob.deploy_contract(contract_config, salt); - // wait for contract to deploy in 5 minutes, because 16mb takes a lot of time. - timeout(Duration::from_secs(300), deployment_request).await??; + // Wait for contract to deploy in 5 minutes, because 16 MB takes a lot of time. + timeout(Duration::from_secs(300), deployment_request).await??; - Ok(()) + Ok(()) + } + .boxed() } diff --git a/bin/e2e-test-client/src/tests/script.rs b/bin/e2e-test-client/src/tests/script.rs index a14268627e0..a88de7b9ef1 100644 --- a/bin/e2e-test-client/src/tests/script.rs +++ b/bin/e2e-test-client/src/tests/script.rs @@ -1,67 +1,57 @@ -use crate::test_context::{ - TestContext, - BASE_AMOUNT, -}; -use fuel_core_chain_config::{ - ContractConfig, - SnapshotMetadata, - StateConfig, -}; +use crate::test_context::{TestContext, BASE_AMOUNT}; +use fuel_core_chain_config::{ContractConfig, SnapshotMetadata, StateConfig}; use fuel_core_types::{ - fuel_tx::{ - Receipt, - ScriptExecutionResult, - Transaction, - UniqueIdentifier, - }, + fuel_tx::{Receipt, ScriptExecutionResult, Transaction, UniqueIdentifier}, fuel_types::{ - canonical::{ - Deserialize, - Serialize, - }, + canonical::{Deserialize, Serialize}, Salt, }, services::executor::TransactionExecutionResult, }; +use futures::future::BoxFuture; +use futures::FutureExt; use libtest_mimic::Failed; use std::{ path::Path, + sync::Arc, time::Duration, }; use tokio::time::timeout; // Executes transfer script and gets the receipts. -pub async fn receipts(ctx: &TestContext) -> Result<(), Failed> { - // alice makes transfer to bob - let result = tokio::time::timeout( - ctx.config.sync_timeout(), - ctx.alice.transfer(ctx.bob.address, BASE_AMOUNT, None), - ) - .await??; - let status = result.status; - if !result.success { - return Err(format!("transfer failed with status {status:?}").into()); - } - println!("The tx id of the script: {}", result.tx_id); +pub fn receipts(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + // Alice makes transfer to Bob + let result = tokio::time::timeout( + ctx.config.sync_timeout(), + ctx.alice.transfer(ctx.bob.address, BASE_AMOUNT, None), + ) + .await??; + let status = result.status; + if !result.success { + return Err(format!("transfer failed with status {status:?}").into()); + } + println!("The tx id of the script: {}", result.tx_id); - let mut queries = vec![]; - for i in 0..100 { - let tx_id = result.tx_id; - queries.push(async move { (ctx.alice.client.receipts(&tx_id).await, i) }); - } + let mut queries = vec![]; + for i in 0..100 { + let ctx = ctx.clone(); + let tx_id = result.tx_id; + queries.push(async move { (ctx.alice.client.receipts(&tx_id).await, i) }); + } - let queries = futures::future::join_all(queries).await; - for query in queries { - let (query, query_number) = query; - let receipts = query?; - if receipts.is_none() { - return Err( - format!("Receipts are empty for query_number {query_number}").into(), - ); + let queries = futures::future::join_all(queries).await; + for query in queries { + let (query, query_number) = query; + let receipts = query?; + if receipts.is_none() { + return Err(format!("Receipts are empty for query_number {query_number}").into()); + } } - } - Ok(()) + Ok(()) + } + .boxed() } #[derive(PartialEq, Eq)] @@ -71,36 +61,42 @@ enum DryRunResult { } // Dry run the transaction. -pub async fn dry_run(ctx: &TestContext) -> Result<(), Failed> { - let transaction = tokio::time::timeout( - ctx.config.sync_timeout(), - ctx.alice.transfer_tx(ctx.bob.address, 0, None), - ) - .await??; - - _dry_runs(ctx, &[transaction], 100, DryRunResult::Successful).await +pub fn dry_run(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + let transaction = tokio::time::timeout( + ctx.config.sync_timeout(), + ctx.alice.transfer_tx(ctx.bob.address, 0, None), + ) + .await??; + + _dry_runs(ctx, vec![transaction], 100, DryRunResult::Successful).await + } + .boxed() } // Dry run multiple transactions -pub async fn dry_run_multiple_txs(ctx: &TestContext) -> Result<(), Failed> { - let transaction1 = tokio::time::timeout( - ctx.config.sync_timeout(), - ctx.alice.transfer_tx(ctx.bob.address, 0, None), - ) - .await??; - let transaction2 = tokio::time::timeout( - ctx.config.sync_timeout(), - ctx.alice.transfer_tx(ctx.alice.address, 0, None), - ) - .await??; - - _dry_runs( - ctx, - &[transaction1, transaction2], - 100, - DryRunResult::Successful, - ) - .await +pub fn dry_run_multiple_txs(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + let transaction1 = tokio::time::timeout( + ctx.config.sync_timeout(), + ctx.alice.transfer_tx(ctx.bob.address, 0, None), + ) + .await??; + let transaction2 = tokio::time::timeout( + ctx.config.sync_timeout(), + ctx.alice.transfer_tx(ctx.alice.address, 0, None), + ) + .await??; + + _dry_runs( + ctx, + vec![transaction1, transaction2], + 100, + DryRunResult::Successful, + ) + .await + } + .boxed() } fn load_contract(salt: Salt, path: impl AsRef) -> Result { @@ -117,126 +113,124 @@ fn load_contract(salt: Salt, path: impl AsRef) -> Result Result<(), Failed> { - let salt: Salt = "0x3b91bab936e4f3db9453046b34c142514e78b64374bf61a04ab45afbd6bca83e" - .parse() - .expect("Should be able to parse the salt"); - let contract_config = load_contract(salt, "./src/tests/test_data/large_state")?; - let dry_run = include_bytes!("test_data/large_state/tx.json"); - let dry_run: Transaction = serde_json::from_slice(dry_run.as_ref()) - .expect("Should be able do decode the Transaction"); - - // If the contract changed, you need to update the - // `f4292fe50d21668e140636ab69c7d4b3d069f66eb9ef3da4b0a324409cc36b8c` in the - // `test_data/large_state/state_config.json` together with: - // 244, 41, 47, 229, 13, 33, 102, 142, 20, 6, 54, 171, 105, 199, 212, 179, 208, 105, 246, 110, 185, 239, 61, 164, 176, 163, 36, 64, 156, 195, 107, 140, - let contract_id = contract_config.contract_id; - - // if the contract is not deployed yet, let's deploy it - let result = ctx.bob.client.contract(&contract_id).await; - if result?.is_none() { - let deployment_request = ctx.bob.deploy_contract(contract_config, salt); - - timeout(Duration::from_secs(20), deployment_request).await??; - } +pub fn run_contract_large_state(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + let salt: Salt = "0x3b91bab936e4f3db9453046b34c142514e78b64374bf61a04ab45afbd6bca83e" + .parse() + .expect("Should be able to parse the salt"); + let contract_config = load_contract(salt, "./src/tests/test_data/large_state")?; + let dry_run = include_bytes!("test_data/large_state/tx.json"); + let dry_run: Transaction = serde_json::from_slice(dry_run.as_ref()) + .expect("Should be able to decode the Transaction"); + + let contract_id = contract_config.contract_id; + + // If the contract is not deployed yet, let's deploy it + let result = ctx.bob.client.contract(&contract_id).await; + if result?.is_none() { + let deployment_request = ctx.bob.deploy_contract(contract_config, salt); + + timeout(Duration::from_secs(20), deployment_request).await??; + } - _dry_runs(ctx, &[dry_run], 100, DryRunResult::MayFail).await + _dry_runs(ctx, vec![dry_run], 100, DryRunResult::MayFail).await + } + .boxed() } -pub async fn arbitrary_transaction(ctx: &TestContext) -> Result<(), Failed> { - const RAW_PATH: &str = "src/tests/test_data/arbitrary_tx.raw"; - const JSON_PATH: &str = "src/tests/test_data/arbitrary_tx.json"; - let dry_run_raw = - std::fs::read_to_string(RAW_PATH).expect("Should read the raw transaction"); - let dry_run_json = - std::fs::read_to_string(JSON_PATH).expect("Should read the json transaction"); - let bytes = dry_run_raw.replace("0x", ""); - let hex_tx = hex::decode(bytes).expect("Expected hex string"); - let dry_run_tx_from_raw: Transaction = Transaction::from_bytes(hex_tx.as_ref()) - .expect("Should be able do decode the Transaction from canonical representation"); - let mut dry_run_tx_from_json: Transaction = - serde_json::from_str(dry_run_json.as_ref()) - .expect("Should be able do decode the Transaction from json representation"); - - if std::env::var_os("OVERRIDE_RAW_WITH_JSON").is_some() { - let bytes = dry_run_tx_from_json.to_bytes(); - std::fs::write(RAW_PATH, hex::encode(bytes)) - .expect("Should write the raw transaction"); - } else if std::env::var_os("OVERRIDE_JSON_WITH_RAW").is_some() { - let bytes = serde_json::to_string_pretty(&dry_run_tx_from_raw) - .expect("Should be able to encode the Transaction"); - dry_run_tx_from_json = dry_run_tx_from_raw.clone(); - std::fs::write(JSON_PATH, bytes.as_bytes()) - .expect("Should write the json transaction"); - } +pub fn arbitrary_transaction(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + const RAW_PATH: &str = "src/tests/test_data/arbitrary_tx.raw"; + const JSON_PATH: &str = "src/tests/test_data/arbitrary_tx.json"; + let dry_run_raw = std::fs::read_to_string(RAW_PATH).expect("Should read the raw transaction"); + let dry_run_json = + std::fs::read_to_string(JSON_PATH).expect("Should read the json transaction"); + let bytes = dry_run_raw.replace("0x", ""); + let hex_tx = hex::decode(bytes).expect("Expected hex string"); + let dry_run_tx_from_raw: Transaction = Transaction::from_bytes(hex_tx.as_ref()) + .expect("Should be able to decode the Transaction from canonical representation"); + let mut dry_run_tx_from_json: Transaction = serde_json::from_str(dry_run_json.as_ref()) + .expect("Should be able to decode the Transaction from json representation"); + + if std::env::var_os("OVERRIDE_RAW_WITH_JSON").is_some() { + let bytes = dry_run_tx_from_json.to_bytes(); + std::fs::write(RAW_PATH, hex::encode(bytes)).expect("Should write the raw transaction"); + } else if std::env::var_os("OVERRIDE_JSON_WITH_RAW").is_some() { + let bytes = serde_json::to_string_pretty(&dry_run_tx_from_raw) + .expect("Should be able to encode the Transaction"); + dry_run_tx_from_json = dry_run_tx_from_raw.clone(); + std::fs::write(JSON_PATH, bytes.as_bytes()).expect("Should write the json transaction"); + } - assert_eq!(dry_run_tx_from_raw, dry_run_tx_from_json); + assert_eq!(dry_run_tx_from_raw, dry_run_tx_from_json); - _dry_runs(ctx, &[dry_run_tx_from_json], 100, DryRunResult::MayFail).await + _dry_runs(ctx, vec![dry_run_tx_from_json], 100, DryRunResult::MayFail).await + } + .boxed() } -async fn _dry_runs( - ctx: &TestContext, - transactions: &[Transaction], +fn _dry_runs( + ctx: Arc, + transactions: Vec, count: usize, expect: DryRunResult, -) -> Result<(), Failed> { - println!("\nStarting dry runs"); - let mut queries = vec![]; - for i in 0..count { - queries.push(async move { - let before = tokio::time::Instant::now(); - let query = ctx - .alice - .client - .dry_run_opt(transactions, Some(false), None) - .await; - println!( - "Received the response for the query number {i} for {}ms", - before.elapsed().as_millis() - ); - (query, i) - }); - } - - // All queries should be resolved for 60 seconds. - let queries = - tokio::time::timeout(Duration::from_secs(60), futures::future::join_all(queries)) - .await?; - - let chain_info = ctx.alice.client.chain_info().await?; - for query in queries { - let (query, query_number) = query; - if let Err(e) = &query { - println!("The query {query_number} failed with {e}"); +) -> BoxFuture<'static, Result<(), Failed>> { + async move { + println!("\nStarting dry runs"); + let mut queries = vec![]; + for i in 0..count { + let ctx = ctx.clone(); + let transactions = transactions.clone(); + queries.push(async move { + let before = tokio::time::Instant::now(); + let query = ctx + .alice + .client + .dry_run_opt(&transactions, Some(false), None) + .await; + println!( + "Received the response for the query number {i} in {}ms", + before.elapsed().as_millis() + ); + (query, i) + }); } - let tx_statuses = query?; - for (tx_status, tx) in tx_statuses.iter().zip(transactions.iter()) { - if tx_status.result.receipts().is_empty() { - return Err( - format!("Receipts are empty for query_number {query_number}").into(), - ); + // All queries should be resolved within 60 seconds. + let queries = + tokio::time::timeout(Duration::from_secs(60), futures::future::join_all(queries)) + .await?; + + let chain_info = ctx.alice.client.chain_info().await?; + for query in queries { + let (query, query_number) = query; + if let Err(e) = &query { + println!("The query {query_number} failed with {e}"); } - assert!(tx.id(&chain_info.consensus_parameters.chain_id()) == tx_status.id); - if expect == DryRunResult::Successful { - assert!(matches!( - &tx_status.result, - TransactionExecutionResult::Success { - result: _result, - .. - } - )); - assert!(matches!( - tx_status.result.receipts().last(), - Some(Receipt::ScriptResult { - result: ScriptExecutionResult::Success, - .. - }) - )); + let tx_statuses = query?; + for (tx_status, tx) in tx_statuses.iter().zip(transactions.iter()) { + if tx_status.result.receipts().is_empty() { + return Err(format!("Receipts are empty for query_number {query_number}").into()); + } + + assert!(tx.id(&chain_info.consensus_parameters.chain_id()) == tx_status.id); + if expect == DryRunResult::Successful { + assert!(matches!( + &tx_status.result, + TransactionExecutionResult::Success { result: _result, .. } + )); + assert!(matches!( + tx_status.result.receipts().last(), + Some(Receipt::ScriptResult { + result: ScriptExecutionResult::Success, + .. + }) + )); + } } } + Ok(()) } - Ok(()) + .boxed() } diff --git a/bin/e2e-test-client/src/tests/transfers.rs b/bin/e2e-test-client/src/tests/transfers.rs index b9619453649..fb666239a48 100644 --- a/bin/e2e-test-client/src/tests/transfers.rs +++ b/bin/e2e-test-client/src/tests/transfers.rs @@ -1,66 +1,70 @@ -use crate::test_context::{ - TestContext, - BASE_AMOUNT, -}; +use crate::test_context::{TestContext, BASE_AMOUNT}; +use futures::future::BoxFuture; +use futures::FutureExt; use libtest_mimic::Failed; +use std::sync::Arc; use tokio::time::timeout; // Alice makes transfer to Bob of `4 * BASE_AMOUNT` native tokens. -pub async fn basic_transfer(ctx: &TestContext) -> Result<(), Failed> { - // alice makes transfer to bob - let result = ctx - .alice - .transfer(ctx.bob.address, 4 * BASE_AMOUNT, None) - .await?; - if !result.success { - return Err("transfer failed".into()) - } - // wait until bob sees the transaction - timeout( - ctx.config.sync_timeout(), - ctx.bob.client.await_transaction_commit(&result.tx_id), - ) - .await??; - println!("\nThe tx id of the transfer: {}", result.tx_id); +pub fn basic_transfer(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + // Alice makes transfer to Bob + let result = ctx + .alice + .transfer(ctx.bob.address, 4 * BASE_AMOUNT, None) + .await?; + if !result.success { + return Err("transfer failed".into()); + } + // Wait until Bob sees the transaction + timeout( + ctx.config.sync_timeout(), + ctx.bob.client.await_transaction_commit(&result.tx_id), + ) + .await??; + println!("\nThe tx id of the transfer: {}", result.tx_id); - // bob checks to see if utxo was received - // we don't check balance in order to avoid brittleness in the case of - // external activity on these wallets - let received_transfer = ctx.bob.owns_coin(result.transferred_utxo).await?; - if !received_transfer { - return Err("Bob failed to receive transfer".into()) - } + // Bob checks to see if UTXO was received + // We don't check balance to avoid brittleness in the case of external activity on these wallets + let received_transfer = ctx.bob.owns_coin(result.transferred_utxo).await?; + if !received_transfer { + return Err("Bob failed to receive transfer".into()); + } - Ok(()) + Ok(()) + } + .boxed() } // Alice makes transfer to Bob of `4 * BASE_AMOUNT` native tokens - `basic_transfer`. // Bob returns `3 * BASE_AMOUNT` tokens back to Alice and pays `BASE_AMOUNT` as a fee. -pub async fn transfer_back(ctx: &TestContext) -> Result<(), Failed> { - basic_transfer(ctx).await?; +pub fn transfer_back(ctx: Arc) -> BoxFuture<'static, Result<(), Failed>> { + async move { + basic_transfer(ctx.clone()).await?; - // bob makes transfer to alice - let result = ctx - .bob - .transfer(ctx.alice.address, 3 * BASE_AMOUNT, None) - .await?; - if !result.success { - return Err("transfer failed".into()) - } - // wait until alice sees the transaction - timeout( - ctx.config.sync_timeout(), - ctx.alice.client.await_transaction_commit(&result.tx_id), - ) - .await??; + // Bob makes transfer to Alice + let result = ctx + .bob + .transfer(ctx.alice.address, 3 * BASE_AMOUNT, None) + .await?; + if !result.success { + return Err("transfer failed".into()); + } + // Wait until Alice sees the transaction + timeout( + ctx.config.sync_timeout(), + ctx.alice.client.await_transaction_commit(&result.tx_id), + ) + .await??; - // alice checks to see if utxo was received - // we don't check balance in order to avoid brittleness in the case of - // external activity on these wallets - let received_transfer = ctx.alice.owns_coin(result.transferred_utxo).await?; - if !received_transfer { - return Err("Alice failed to receive transfer".into()) - } + // Alice checks to see if UTXO was received + // We don't check balance to avoid brittleness in the case of external activity on these wallets + let received_transfer = ctx.alice.owns_coin(result.transferred_utxo).await?; + if !received_transfer { + return Err("Alice failed to receive transfer".into()); + } - Ok(()) + Ok(()) + } + .boxed() } diff --git a/bin/fuel-core-client/src/main.rs b/bin/fuel-core-client/src/main.rs index b115c13954b..d1b7e0c3a42 100644 --- a/bin/fuel-core-client/src/main.rs +++ b/bin/fuel-core-client/src/main.rs @@ -49,14 +49,14 @@ impl CliArgs { Command::Transaction(sub_cmd) => match sub_cmd { TransactionCommands::Submit { tx } => { let tx: Transaction = - serde_json::from_str(tx).expect("invalid transaction json"); + self.deserialize_tx(tx).expect("invalid transaction json"); let result = client.submit(&tx).await; println!("{}", result.unwrap()); } TransactionCommands::EstimatePredicates { tx } => { let mut tx: Transaction = - serde_json::from_str(tx).expect("invalid transaction json"); + self.deserialize_tx(tx).expect("invalid transaction json"); client .estimate_predicates(&mut tx) @@ -68,7 +68,7 @@ impl CliArgs { let txs: Vec = txs .iter() .map(|tx| { - serde_json::from_str(tx).expect("invalid transaction json") + self.deserialize_tx(tx).expect("invalid transaction json") }) .collect(); @@ -88,6 +88,10 @@ impl CliArgs { }, } } + + fn deserialize_tx(&self, tx: &str) -> Result { + serde_json::from_str(tx) + } } #[tokio::main(flavor = "current_thread")] diff --git a/crates/services/executor/src/ports.rs b/crates/services/executor/src/ports.rs index 36f89bcbfdb..f796b8d3a4a 100644 --- a/crates/services/executor/src/ports.rs +++ b/crates/services/executor/src/ports.rs @@ -28,7 +28,7 @@ use alloc::{ vec::Vec, }; -/// The wrapper around either `Transaction` or `CheckedTransaction`. +/// Wrapper around either Transaction or CheckedTransaction. #[allow(clippy::large_enum_variant)] pub enum MaybeCheckedTransaction { CheckedTransaction(CheckedTransaction, ConsensusParametersVersion), @@ -38,30 +38,18 @@ pub enum MaybeCheckedTransaction { impl MaybeCheckedTransaction { pub fn id(&self, chain_id: &ChainId) -> TxId { match self { - MaybeCheckedTransaction::CheckedTransaction( - CheckedTransaction::Script(tx), - _, - ) => tx.id(), - MaybeCheckedTransaction::CheckedTransaction( - CheckedTransaction::Create(tx), - _, - ) => tx.id(), - MaybeCheckedTransaction::CheckedTransaction( - CheckedTransaction::Mint(tx), - _, - ) => tx.id(), - MaybeCheckedTransaction::CheckedTransaction( - CheckedTransaction::Upgrade(tx), - _, - ) => tx.id(), - MaybeCheckedTransaction::CheckedTransaction( - CheckedTransaction::Upload(tx), - _, - ) => tx.id(), - MaybeCheckedTransaction::CheckedTransaction( - CheckedTransaction::Blob(tx), - _, - ) => tx.id(), + MaybeCheckedTransaction::CheckedTransaction(checked_tx, _) => { + match checked_tx { + // For these variants, we need to pass `chain_id` + CheckedTransaction::Script(tx) => tx.as_ref().id(chain_id), + CheckedTransaction::Create(tx) => tx.as_ref().id(chain_id), + CheckedTransaction::Upgrade(tx) => tx.as_ref().id(chain_id), + CheckedTransaction::Upload(tx) => tx.as_ref().id(chain_id), + CheckedTransaction::Blob(tx) => tx.as_ref().id(chain_id), + // For `Mint`, `id()` does not take `chain_id` + CheckedTransaction::Mint(tx) => tx.id(), + } + } MaybeCheckedTransaction::Transaction(tx) => tx.id(chain_id), } } @@ -78,12 +66,12 @@ impl TransactionExt for Transaction { match self { Transaction::Script(tx) => Ok(tx.max_gas(gas_costs, fee_params)), Transaction::Create(tx) => Ok(tx.max_gas(gas_costs, fee_params)), - Transaction::Mint(_) => Err(ExecutorError::Other( - "Mint transaction doesn't have max_gas".to_string(), - )), Transaction::Upgrade(tx) => Ok(tx.max_gas(gas_costs, fee_params)), Transaction::Upload(tx) => Ok(tx.max_gas(gas_costs, fee_params)), Transaction::Blob(tx) => Ok(tx.max_gas(gas_costs, fee_params)), + Transaction::Mint(_) => Err(ExecutorError::Other( + "Mint transaction doesn't have max_gas".to_string(), + )), } } } @@ -93,12 +81,12 @@ impl TransactionExt for CheckedTransaction { match self { CheckedTransaction::Script(tx) => Ok(tx.metadata().max_gas), CheckedTransaction::Create(tx) => Ok(tx.metadata().max_gas), - CheckedTransaction::Mint(_) => Err(ExecutorError::Other( - "Mint transaction doesn't have max_gas".to_string(), - )), CheckedTransaction::Upgrade(tx) => Ok(tx.metadata().max_gas), CheckedTransaction::Upload(tx) => Ok(tx.metadata().max_gas), CheckedTransaction::Blob(tx) => Ok(tx.metadata().max_gas), + CheckedTransaction::Mint(_) => Err(ExecutorError::Other( + "Mint transaction doesn't have max_gas".to_string(), + )), } } } @@ -115,9 +103,9 @@ impl TransactionExt for MaybeCheckedTransaction { } pub trait TransactionsSource { - /// Returns the next batch of transactions to satisfy the `gas_limit` and `block_transaction_size_limit`. - /// The returned batch has at most `tx_count_limit` transactions, none - /// of which has a size in bytes greater than `size_limit`. + /// Returns the next batch of transactions to satisfy the gas_limit and block_transaction_size_limit. + /// The returned batch has at most tx_count_limit transactions, none + /// of which has a size in bytes greater than size_limit. fn next( &self, gas_limit: u64, @@ -127,9 +115,9 @@ pub trait TransactionsSource { } pub trait RelayerPort { - /// Returns `true` if the relayer is enabled. + /// Returns true if the relayer is enabled. fn enabled(&self) -> bool; - /// Get events from the relayer at a given da height. + /// Get events from the relayer at a given DA height. fn get_events(&self, da_height: &DaBlockHeight) -> anyhow::Result>; }