diff --git a/crates/anvil-polkadot/Cargo.toml b/crates/anvil-polkadot/Cargo.toml index 1ca8156489d49..b570ae43ae072 100644 --- a/crates/anvil-polkadot/Cargo.toml +++ b/crates/anvil-polkadot/Cargo.toml @@ -160,5 +160,4 @@ op-alloy-rpc-types.workspace = true [features] default = [] asm-keccak = ["alloy-primitives/asm-keccak"] -forking-support = [] -forking-tests = [] \ No newline at end of file +forking-support = [] \ No newline at end of file diff --git a/crates/anvil-polkadot/src/api_server/error.rs b/crates/anvil-polkadot/src/api_server/error.rs index 2b0c51043b4ff..7ba7838b92ef4 100644 --- a/crates/anvil-polkadot/src/api_server/error.rs +++ b/crates/anvil-polkadot/src/api_server/error.rs @@ -47,6 +47,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: subxt::ext::subxt_rpcs::Error) -> Self { + Self::from(subxt::Error::from(err)) + } +} + impl From for Error { fn from(err: ClientError) -> Self { match err { diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index 265f959e22185..0bb3ad12c7832 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -87,14 +87,23 @@ use sqlx::sqlite::SqlitePoolOptions; use std::{collections::BTreeSet, sync::Arc, time::Duration}; use substrate_runtime::{Balance, constants::NATIVE_TO_ETH_RATIO}; use subxt::{ - Metadata as SubxtMetadata, OnlineClient, backend::rpc::RpcClient, - client::RuntimeVersion as SubxtRuntimeVersion, config::substrate::H256, - ext::subxt_rpcs::LegacyRpcMethods, utils::H160, + Metadata as SubxtMetadata, OnlineClient, + backend::rpc::RpcClient, + client::RuntimeVersion as SubxtRuntimeVersion, + config::substrate::H256, + dynamic::{Value as DynamicValue, tx as dynamic_tx}, + ext::subxt_rpcs::LegacyRpcMethods, + utils::H160, }; use subxt_signer::eth::Keypair; use tokio::try_join; pub const CLIENT_VERSION: &str = concat!("anvil-polkadot/v", env!("CARGO_PKG_VERSION")); +// When forking, operations can be slower due to fetching state from the remote chain, +// so we use a higher timeout to avoid spurious failures. +#[cfg(feature = "forking-support")] +const TIMEOUT_DURATION: Duration = Duration::from_secs(120); +#[cfg(not(feature = "forking-support"))] const TIMEOUT_DURATION: Duration = Duration::from_secs(30); pub struct ApiServer { @@ -113,6 +122,12 @@ pub struct ApiServer { /// Tracks all active filters filters: Filters, hardcoded_chain_id: u64, + /// RPC methods for submitting transactions + rpc: LegacyRpcMethods, + /// Subxt OnlineClient for dynamic transaction building. + /// When forking, the metadata comes from the forked chain's WASM (loaded via lazy loading), + /// so pallet indices will be correct for the forked runtime. + api: OnlineClient, } /// Fetch the chain ID from the substrate chain. @@ -140,7 +155,7 @@ impl ApiServer { let eth_rpc_client = create_revive_rpc_client( api.clone(), rpc_client.clone(), - rpc, + rpc.clone(), block_provider.clone(), substrate_service.spawn_handle.clone(), revive_rpc_block_limit, @@ -176,6 +191,8 @@ impl ApiServer { instance_id: B256::random(), filters, hardcoded_chain_id: chain_id, + rpc, + api, }) } @@ -807,8 +824,18 @@ impl ApiServer { async fn send_raw_transaction(&self, transaction: Bytes) -> Result { let hash = H256(keccak_256(&transaction.0)); - let call = subxt_client::tx().revive().eth_transact(transaction.0); - self.eth_rpc_client.submit(call).await?; + + // Use dynamic transaction building to ensure the correct pallet index is used. + // The metadata in self.api comes from the runtime's WASM (via runtime API call), + // which is the forked chain's WASM when forking. This ensures correct pallet indices. + let payload_value = DynamicValue::from_bytes(transaction.0); + let tx_payload = dynamic_tx("Revive", "eth_transact", vec![payload_value]); + + let ext = self.api.tx().create_unsigned(&tx_payload)?; + + // Submit the extrinsic to the transaction pool + self.rpc.author_submit_extrinsic(ext.encoded()).await?; + Ok(hash) } @@ -1910,7 +1937,14 @@ impl ApiServer { awaited_hash: H256, ) -> Result<()> { if let Some(mut receiver) = receiver { - tokio::time::timeout(Duration::from_secs(3), async { + // When forking, block production can be slower due to fetching state from the + // remote chain, so we use a higher timeout to avoid spurious failures. + #[cfg(feature = "forking-support")] + let timeout = TIMEOUT_DURATION; + #[cfg(not(feature = "forking-support"))] + let timeout = Duration::from_secs(3); + + tokio::time::timeout(timeout, async { loop { if let Ok(block_hash) = receiver.recv().await { if let Err(e) = self.log_mined_block(block_hash).await { @@ -1948,38 +1982,38 @@ impl ApiServer { if let Ok(Some(substrate_block)) = self.eth_rpc_client.block_by_hash(&block_hash).await && let Some(evm_block) = self.eth_rpc_client.evm_block(substrate_block, false).await { - // Extract transaction hashes - let tx_hashes: Vec = match &evm_block.transactions { - HashesOrTransactionInfos::Hashes(hashes) => hashes.clone(), - // Considering that we called evm_block with hydrated set to false, we will - // never receive TransactionInfos, but we handle it anyways. - HashesOrTransactionInfos::TransactionInfos(infos) => { - infos.iter().map(|i| i.hash).collect() - } - }; - - if !tx_hashes.is_empty() { - node_info!(""); + // Extract transaction hashes + let tx_hashes: Vec = match &evm_block.transactions { + HashesOrTransactionInfos::Hashes(hashes) => hashes.clone(), + // Considering that we called evm_block with hydrated set to false, we will + // never receive TransactionInfos, but we handle it anyways. + HashesOrTransactionInfos::TransactionInfos(infos) => { + infos.iter().map(|i| i.hash).collect() + } + }; - // Log each transaction - for tx_hash in tx_hashes { - if let Some(receipt) = self.eth_rpc_client.receipt(&tx_hash).await { - node_info!(" Transaction: {:?}", receipt.transaction_hash); + if !tx_hashes.is_empty() { + node_info!(""); - if let Some(contract) = &receipt.contract_address { - node_info!(" Contract created: {contract}"); - } + // Log each transaction + for tx_hash in tx_hashes { + if let Some(receipt) = self.eth_rpc_client.receipt(&tx_hash).await { + node_info!(" Transaction: {:?}", receipt.transaction_hash); - node_info!(" Gas used: {}", receipt.cumulative_gas_used); + if let Some(contract) = &receipt.contract_address { + node_info!(" Contract created: {contract}"); + } - if receipt.status == Some(evm::U256::zero()) { - node_info!(" Error: reverted"); - } + node_info!(" Gas used: {}", receipt.cumulative_gas_used); - node_info!(""); + if receipt.status == Some(evm::U256::zero()) { + node_info!(" Error: reverted"); } + + node_info!(""); } } + } } node_info!(" Block Number: {}", block_number); diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs index fd11c8b27e413..2e80198fa853c 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs @@ -156,10 +156,11 @@ impl Blockchain { storage.genesis_hash = hash; } - // Update leaves for non-genesis blocks - if storage.blocks.len() > 1 { - storage.leaves.import(hash, number, *header.parent_hash()); - } + // Update leaves for all blocks including genesis. + // For genesis when forking, the parent_hash points to the previous block on the remote + // chain. That parent won't be in our leaf set, so this effectively adds genesis as + // a new leaf. + storage.leaves.import(hash, number, *header.parent_hash()); // Finalize block only if explicitly requested via new_state if let NewBlockState::Final = new_state { @@ -266,25 +267,43 @@ impl HeaderBackend for Blockchain { + let block = full.block.clone(); + self.storage + .write() + .blocks + .insert(hash, StoredBlock::Full(block.clone(), full.justifications)); + Some(block.header().clone()) + } + Ok(None) => { + // Block not found on remote chain - this is expected for locally-built blocks + tracing::debug!( + target: LAZY_LOADING_LOG_TARGET, + "Block {:?} not found in local storage or remote RPC", + hash + ); + None + } + Err(e) => { + tracing::warn!( + target: LAZY_LOADING_LOG_TARGET, + "Failed to fetch block {:?} from RPC: {}", + hash, + e + ); + None + } + } } else { - None - }; - - if header.is_none() { - tracing::warn!( + // No RPC configured - block simply doesn't exist locally + tracing::debug!( target: LAZY_LOADING_LOG_TARGET, - "Expected block {:x?} to exist.", - &hash + "Block {:?} not found in local storage (no RPC configured)", + hash ); - } + None + }; Ok(header) } @@ -418,19 +437,30 @@ impl sp_blockchain::Backend for Blockch Ok(leaves) } - fn children(&self, _parent_hash: Block::Hash) -> sp_blockchain::Result> { - unimplemented!("Not supported by the `lazy-loading` backend.") + fn children(&self, parent_hash: Block::Hash) -> sp_blockchain::Result> { + // Find all blocks whose parent_hash matches the given hash + let storage = self.storage.read(); + let children: Vec = storage + .blocks + .iter() + .filter_map(|(hash, block)| { + if *block.header().parent_hash() == parent_hash { Some(*hash) } else { None } + }) + .collect(); + Ok(children) } fn indexed_transaction(&self, _hash: Block::Hash) -> sp_blockchain::Result>> { - unimplemented!("Not supported by the `lazy-loading` backend.") + // Indexed transactions are not supported in the lazy-loading backend + Ok(None) } fn block_indexed_body( &self, _hash: Block::Hash, ) -> sp_blockchain::Result>>> { - unimplemented!("Not supported by the `lazy-loading` backend.") + // Indexed block bodies are not supported in the lazy-loading backend + Ok(None) } } diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index d8bd824fa325a..0e5c286bebef4 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -14,11 +14,20 @@ use anvil_polkadot::{ }; use polkadot_sdk::pallet_revive::evm::Account; -/// Westend Asset Hub zombienet local URL for forking tests -/// This URL should point to a running zombienet instance, -/// so running the tests depending on it should be preceded -/// by setting up a zombienet network with a running RPC. -const WESTEND_ASSET_HUB_URL: &str = "http://127.0.0.1:63982"; +/// Westend Asset Hub real RPC URL for forking tests +/// This endpoint provides both Substrate RPC and ETH RPC methods +const WESTEND_ASSET_HUB_URL: &str = "https://westend-asset-hub-rpc.polkadot.io"; + +/// Helper to create a fork config pointing to Westend Asset Hub +/// We have verbose forking tests to debug this new experimental workflow. +fn westend_fork_config() -> AnvilNodeConfig { + AnvilNodeConfig::test_config() + .with_port(0) + .with_eth_rpc_url(Some(WESTEND_ASSET_HUB_URL.to_string())) + .with_auto_impersonate(true) + .with_tracing(true) + .set_silent(false) +} /// Tests that forking preserves state from the source chain and allows local modifications #[tokio::test(flavor = "multi_thread")] @@ -603,13 +612,6 @@ async fn test_fork_with_contract_deployment() { // These tests require a running zombienet instance at WESTEND_ASSET_HUB_URL // ============================================================================= -/// Helper to create a fork config pointing to Westend Asset Hub zombienet -fn westend_fork_config() -> AnvilNodeConfig { - AnvilNodeConfig::test_config() - .with_port(0) - .with_eth_rpc_url(Some(WESTEND_ASSET_HUB_URL.to_string())) -} - /// Tests that we can fork from Westend Asset Hub and get balance of addresses #[tokio::test(flavor = "multi_thread")] async fn test_fork_eth_get_balance_from_westend() { @@ -688,7 +690,11 @@ async fn test_fork_eth_get_code_from_westend() { .unwrap(), ) .unwrap(); - assert!(!deployed_code.is_empty(), "Deployed contract should have code"); + assert_eq!( + deployed_code.as_ref(), + contract_code.runtime.as_deref().expect("missing runtime bytecode"), + "Deployed code should match contract runtime bytecode" + ); // Deploy another contract to verify chain continues working let tx_hash2 = fork_node.deploy_contract(&contract_code.init, alith.address()).await; @@ -732,8 +738,11 @@ async fn test_fork_eth_get_nonce_from_westend() { .from(alith_address) .to(Address::from(baltathar_address)); - fork_node.send_transaction(transaction).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + // Send and wait for transaction to be included + fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); // Nonce should have increased by 1 let nonce_after_tx = fork_node.get_nonce(alith_address).await; @@ -798,8 +807,16 @@ async fn test_fork_state_snapshotting_from_westend() { .from(alith_address) .to(baltathar_address); - fork_node.send_transaction(transaction).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + // Send and wait for transaction to be included + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); + assert_eq!( + receipt.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Transaction should succeed" + ); // Verify state changed let alith_balance_after = fork_node.get_balance(alith.address(), None).await; @@ -846,7 +863,6 @@ async fn test_fork_can_send_tx_from_westend() { let fork_config = westend_fork_config(); let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); - let initial_block = fork_node.best_block_number().await; let alith = Account::from(subxt_signer::eth::dev::alith()); @@ -882,13 +898,11 @@ async fn test_fork_can_send_tx_from_westend() { .from(alith_address) .to(baltathar_address); - let tx_hash = fork_node.send_transaction(transaction).await.unwrap(); - - // Mine the transaction - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); - - // Get receipt and verify transaction succeeded - let receipt = fork_node.get_transaction_receipt(tx_hash).await; + // Send and wait for transaction to be included + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); assert_eq!( receipt.status, Some(polkadot_sdk::pallet_revive::U256::from(1)), @@ -916,25 +930,158 @@ async fn test_fork_can_send_tx_from_westend() { .from(baltathar_address) .to(alith_address); - let tx_hash2 = fork_node.send_transaction(transaction2).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); - - let receipt2 = fork_node.get_transaction_receipt(tx_hash2).await; + // Send and wait for second transaction + let receipt2 = fork_node + .send_transaction_and_wait(transaction2, 120) + .await + .expect("Second transaction receipt not found within timeout"); assert_eq!( receipt2.status, Some(polkadot_sdk::pallet_revive::U256::from(1)), "Second transaction should succeed" ); - // Verify block number increased (2 transactions = 2 blocks mined) + // Verify block number increased let final_block = fork_node.best_block_number().await; + assert!(final_block > initial_block, "Block number should increase after transactions"); +} + +/// Tests sending a transaction on a forked chain with automine enabled. +/// This exercises the EthSendTransactionSync path in send_transaction_and_wait. +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_can_send_tx_from_westend_with_automine() { + // Start with automine disabled (default for test_config) so we can warm up + // the forking backend with a manual mine before enabling automine. + let fork_config = westend_fork_config(); + let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); + let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); + + let alith = Account::from(subxt_signer::eth::dev::alith()); + let baltathar = Account::from(subxt_signer::eth::dev::baltathar()); + let alith_address = Address::from(ReviveAddress::new(alith.address())); + let baltathar_address = Address::from(ReviveAddress::new(baltathar.address())); + + // Set initial balances for dev accounts (they may not have balance in the forked chain) + let initial_balance = U256::from(1e20 as u128); // 100 ether + unwrap_response::<()>( + fork_node.eth_rpc(EthRequest::SetBalance(alith_address, initial_balance)).await.unwrap(), + ) + .unwrap(); + + // Warm up the forking backend by mining an empty block. The first block in forking + // mode is slow because it imports state from the remote chain. Without this, the + // 30s internal timeout of EthSendTransactionSync would be exceeded. + unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + + // Enable automine so send_transaction_and_wait uses EthSendTransactionSync + unwrap_response::<()>(fork_node.eth_rpc(EthRequest::SetAutomine(true)).await.unwrap()).unwrap(); + + // Send a transaction using the automine path (EthSendTransactionSync) + let baltathar_balance_before = fork_node.get_balance(baltathar.address(), None).await; + let transfer_amount = U256::from(1e18 as u128); // 1 ether + let transaction = TransactionRequest::default() + .value(transfer_amount) + .from(alith_address) + .to(baltathar_address); + + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); assert_eq!( - final_block, - initial_block + 2, - "Block number should increase by 2 after two mined transactions" + receipt.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Transaction should succeed" + ); + + // Verify balances changed + let baltathar_balance_after = fork_node.get_balance(baltathar.address(), None).await; + assert_eq!( + baltathar_balance_after, + baltathar_balance_before + transfer_amount, + "Baltathar should receive the transfer amount" ); } +/// Tests deploying a contract on a forked chain +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_can_deploy_contract_from_westend() { + let fork_config = westend_fork_config(); + let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); + let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); + + let alith = Account::from(subxt_signer::eth::dev::alith()); + let alith_address = Address::from(ReviveAddress::new(alith.address())); + + // Set balance for deployment + let initial_balance = U256::from(100_000_000_000_000_000_000u128); // 100 ether + unwrap_response::<()>( + fork_node.eth_rpc(EthRequest::SetBalance(alith_address, initial_balance)).await.unwrap(), + ) + .unwrap(); + + // Deploy SimpleStorage contract and wait for receipt + let contract_code = get_contract_code("SimpleStorage"); + let receipt = fork_node + .deploy_contract_and_wait(&contract_code.init, alith.address(), 120) + .await + .expect("Contract deployment receipt not found within timeout"); + assert_eq!( + receipt.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Contract deployment should succeed" + ); + + let contract_address = receipt.contract_address.expect("Contract address should exist"); + + // Verify contract has code + let code = unwrap_response::( + fork_node + .eth_rpc(EthRequest::EthGetCodeAt( + Address::from(ReviveAddress::new(contract_address)), + None, + )) + .await + .unwrap(), + ) + .unwrap(); + assert_eq!( + code.as_ref(), + contract_code.runtime.as_deref().expect("missing runtime bytecode"), + "Deployed code should match contract runtime bytecode" + ); + + // Deploy another contract to verify chain continues working + let receipt2 = fork_node + .deploy_contract_and_wait(&contract_code.init, alith.address(), 120) + .await + .expect("Second contract deployment receipt not found within timeout"); + assert_eq!( + receipt2.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Second contract deployment should succeed" + ); + + let contract_address2 = + receipt2.contract_address.expect("Second contract address should exist"); + assert_ne!(contract_address, contract_address2, "Contract addresses should be different"); + + // Verify contract methods work: set a value and read it back + let set_value = SimpleStorage::setValueCall::new((U256::from(42),)).abi_encode(); + let set_tx = TransactionRequest::default() + .from(alith_address) + .to(Address::from(ReviveAddress::new(contract_address))) + .input(TransactionInput::both(Bytes::from(set_value))); + fork_node + .send_transaction_and_wait(set_tx, 120) + .await + .expect("setValue transaction should succeed"); + + let stored_value = + simplestorage_get_value(&mut fork_node, contract_address, alith_address).await; + assert_eq!(stored_value, U256::from(42), "getValue should return the value we set"); +} + /// Tests impersonating an account on a forked chain #[tokio::test(flavor = "multi_thread")] async fn test_fork_impersonate_account_from_westend() { @@ -966,10 +1113,11 @@ async fn test_fork_impersonate_account_from_westend() { .from(impersonated_addr) .to(recipient_addr); - let tx_hash = fork_node.send_transaction(transaction).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); - - let receipt = fork_node.get_transaction_receipt(tx_hash).await; + // Send and wait for transaction to be included + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Impersonated transaction receipt not found within timeout"); assert_eq!( receipt.status, Some(polkadot_sdk::pallet_revive::U256::from(1)), diff --git a/crates/anvil-polkadot/tests/it/main.rs b/crates/anvil-polkadot/tests/it/main.rs index dbc0fbf290c9e..39934a1aae174 100644 --- a/crates/anvil-polkadot/tests/it/main.rs +++ b/crates/anvil-polkadot/tests/it/main.rs @@ -1,6 +1,6 @@ mod abi; mod filters; -#[cfg(feature = "forking-tests")] +#[cfg(feature = "forking-support")] mod forking; mod gas; mod genesis; diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index c3507e141af3b..f514e954446af 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -48,7 +48,7 @@ use subxt::utils::H160; use tempfile::TempDir; use crate::abi::Multicall; -#[cfg(feature = "forking-tests")] +#[cfg(feature = "forking-support")] use crate::abi::SimpleStorage; pub struct BlockWaitTimeout { @@ -149,6 +149,64 @@ impl TestNode { self.send_transaction_inner(transaction, None, false).await } + /// Execute an ethereum transaction and wait for its receipt. + /// When automine is enabled, uses `EthSendTransactionSync` to get the receipt directly. + /// Otherwise, sends the transaction and polls for the receipt via block import notifications. + #[cfg(feature = "forking-support")] + pub async fn send_transaction_and_wait( + &mut self, + transaction: TransactionRequest, + timeout_secs: u64, + ) -> Result { + let is_automine = + unwrap_response::(self.eth_rpc(EthRequest::GetAutoMine(())).await.unwrap()) + .unwrap(); + + if is_automine { + return unwrap_response::( + self.eth_rpc(EthRequest::EthSendTransactionSync(Box::new(WithOtherFields::new( + transaction, + )))) + .await + .unwrap(), + ); + } + + let tx_hash = self.send_transaction(transaction).await?; + let mut import_stream = self.service.client.import_notification_stream(); + let timeout = Duration::from_secs(timeout_secs); + + let result = tokio::time::timeout(timeout, async { + loop { + // Check if receipt is available + let receipt_result = self + .eth_rpc(EthRequest::EthGetTransactionReceipt(B256::from( + tx_hash.to_fixed_bytes(), + ))) + .await; + + if let Ok(ResponseResult::Success(val)) = receipt_result + && !val.is_null() + { + return serde_json::from_value(val) + .map_err(|_| RpcError::new(ErrorCode::InternalError)); + } + + // Mine a block and wait for the next block import notification + let _ = self.eth_rpc(EthRequest::Mine(None, None)).await; + let _ = import_stream.next().await; + } + }) + .await; + + match result { + Ok(receipt) => receipt, + Err(_) => Err(RpcError::internal_error_with(format!( + "Transaction {tx_hash:?} was not confirmed within {timeout:?}" + ))), + } + } + /// Execute an impersonated ethereum transaction. pub async fn send_unsigned_transaction( &mut self, @@ -228,7 +286,6 @@ impl TestNode { self.eth_best_block().await.number.as_u32() } - #[cfg(feature = "forking-tests")] pub fn substrate_rpc_port(&self) -> u16 { self.service .rpc_handlers @@ -360,6 +417,20 @@ impl TestNode { self.send_transaction(deploy_contract_tx).await.unwrap() } + /// Deploy a contract and wait for its receipt. + #[cfg(feature = "forking-support")] + pub async fn deploy_contract_and_wait( + &mut self, + code: &[u8], + deployer: H160, + timeout_secs: u64, + ) -> Result { + let deploy_contract_tx = TransactionRequest::default() + .from(Address::from(ReviveAddress::new(deployer))) + .input(TransactionInput::both(Bytes::copy_from_slice(code))); + self.send_transaction_and_wait(deploy_contract_tx, timeout_secs).await + } + pub async fn get_storage_at(&mut self, storage_key: U256, contract_address: H160) -> U256 { let result = self .eth_rpc(EthRequest::EthGetStorageAt( @@ -550,7 +621,7 @@ pub fn to_hex_string(value: u64) -> String { } /// Helper function to call getValue() on a SimpleStorage contract -#[cfg(feature = "forking-tests")] +#[cfg(feature = "forking-support")] pub async fn simplestorage_get_value( node: &mut TestNode, contract_address: polkadot_sdk::pallet_revive::H160,