From 9867c8fff002767565754569221adcfa0f0df9a4 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Thu, 5 Dec 2024 15:17:57 +1100 Subject: [PATCH 1/2] fix Cargo.lock files --- e2e-tests-rust/Cargo.lock | 24 ++++++++++++---------- spec-tests/Cargo.lock | 42 +++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/e2e-tests-rust/Cargo.lock b/e2e-tests-rust/Cargo.lock index a1f8cf3e..22bad46b 100644 --- a/e2e-tests-rust/Cargo.lock +++ b/e2e-tests-rust/Cargo.lock @@ -671,6 +671,19 @@ dependencies = [ "libc", ] +[[package]] +name = "anvil-zksync_e2e_tests" +version = "0.0.0" +dependencies = [ + "alloy", + "alloy-zksync", + "anyhow", + "fs2", + "futures", + "itertools 0.13.0", + "tokio", +] + [[package]] name = "anyhow" version = "1.0.93" @@ -1277,17 +1290,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "era_test_node_e2e_tests" -version = "0.0.0" -dependencies = [ - "alloy", - "alloy-zksync", - "anyhow", - "fs2", - "tokio", -] - [[package]] name = "errno" version = "0.3.9" diff --git a/spec-tests/Cargo.lock b/spec-tests/Cargo.lock index c4ad9b70..a6ed969e 100644 --- a/spec-tests/Cargo.lock +++ b/spec-tests/Cargo.lock @@ -695,6 +695,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anvil-zksync_spec_tests" +version = "0.0.0" +dependencies = [ + "alloy", + "anyhow", + "chrono", + "fs2", + "itertools 0.13.0", + "jsonschema", + "openrpc-types", + "reqwest", + "schemars", + "serde", + "serde_json", + "test-log", + "tokio", + "tracing", + "zksync_basic_types", +] + [[package]] name = "anyhow" version = "1.0.91" @@ -1404,27 +1425,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "era_test_node_spec_tests" -version = "0.0.0" -dependencies = [ - "alloy", - "anyhow", - "chrono", - "fs2", - "itertools 0.13.0", - "jsonschema", - "openrpc-types", - "reqwest", - "schemars", - "serde", - "serde_json", - "test-log", - "tokio", - "tracing", - "zksync_basic_types", -] - [[package]] name = "errno" version = "0.3.9" From a258c7f4e5c90cf3bce8041b0a679dcbfd189d4e Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Thu, 5 Dec 2024 15:15:16 +1100 Subject: [PATCH 2/2] create utilities for common test patterns --- e2e-tests-rust/Cargo.lock | 2 + e2e-tests-rust/Cargo.toml | 5 +- e2e-tests-rust/src/ext.rs | 73 ++++ e2e-tests-rust/src/lib.rs | 39 +-- e2e-tests-rust/src/provider/anvil_zksync.rs | 34 ++ e2e-tests-rust/src/provider/mod.rs | 5 + e2e-tests-rust/src/provider/testing.rs | 360 ++++++++++++++++++++ e2e-tests-rust/tests/lib.rs | 344 +++++-------------- 8 files changed, 574 insertions(+), 288 deletions(-) create mode 100644 e2e-tests-rust/src/ext.rs create mode 100644 e2e-tests-rust/src/provider/anvil_zksync.rs create mode 100644 e2e-tests-rust/src/provider/mod.rs create mode 100644 e2e-tests-rust/src/provider/testing.rs diff --git a/e2e-tests-rust/Cargo.lock b/e2e-tests-rust/Cargo.lock index 22bad46b..a3437af1 100644 --- a/e2e-tests-rust/Cargo.lock +++ b/e2e-tests-rust/Cargo.lock @@ -267,6 +267,7 @@ dependencies = [ "const-hex", "derive_more", "foldhash", + "getrandom", "hashbrown 0.15.2", "hex-literal", "indexmap", @@ -678,6 +679,7 @@ dependencies = [ "alloy", "alloy-zksync", "anyhow", + "async-trait", "fs2", "futures", "itertools 0.13.0", diff --git a/e2e-tests-rust/Cargo.toml b/e2e-tests-rust/Cargo.toml index 3bda75f8..444d4c8d 100644 --- a/e2e-tests-rust/Cargo.toml +++ b/e2e-tests-rust/Cargo.toml @@ -11,10 +11,13 @@ publish = false [dependencies] alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "c43bba1a6c5e744afb975b261cba6e964d6a58c6" } -alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types", "provider-anvil-api"] } +alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types", "getrandom", "provider-anvil-api"] } anyhow = "1.0" fs2 = "0.4.3" tokio = { version = "1", features = ["time", "rt", "process"] } +futures = "0.3.31" +itertools = "0.13.0" +async-trait = "0.1.83" [dev-dependencies] diff --git a/e2e-tests-rust/src/ext.rs b/e2e-tests-rust/src/ext.rs new file mode 100644 index 00000000..8b08f7e9 --- /dev/null +++ b/e2e-tests-rust/src/ext.rs @@ -0,0 +1,73 @@ +//! Module containing extensions of existing alloy abstractions. + +use alloy::network::ReceiptResponse; +use alloy::primitives::{Address, BlockHash}; +use alloy::providers::WalletProvider; +use alloy::signers::local::PrivateKeySigner; +use alloy_zksync::network::Zksync; +use alloy_zksync::wallet::ZksyncWallet; + +pub trait ReceiptExt: ReceiptResponse { + fn block_number_ext(&self) -> anyhow::Result { + self.block_number().ok_or_else(|| { + anyhow::anyhow!( + "receipt (hash={}) does not have block number", + self.transaction_hash() + ) + }) + } + + fn block_hash_ext(&self) -> anyhow::Result { + self.block_hash().ok_or_else(|| { + anyhow::anyhow!( + "receipt (hash={}) does not have block hash", + self.transaction_hash() + ) + }) + } + + /// Asserts that receipts belong to a block and that block is the same for both of them. + fn assert_same_block(&self, other: &Self) -> anyhow::Result<()> { + let lhs_number = self.block_number_ext()?; + let rhs_number = other.block_number_ext()?; + let lhs_hash = self.block_hash_ext()?; + let rhs_hash = other.block_hash_ext()?; + + if lhs_number == rhs_number && lhs_hash == rhs_hash { + Ok(()) + } else { + anyhow::bail!( + "receipt (hash={}, block={}) is not from the same block as receipt (hash={}, block={})", + self.transaction_hash(), + lhs_number, + other.transaction_hash(), + rhs_number + ) + } + } + /// Asserts that receipt is successful. + fn assert_successful(&self) -> anyhow::Result<()> { + if !self.status() { + anyhow::bail!( + "receipt (hash={}, block={:?}) is not successful", + self.transaction_hash(), + self.block_number(), + ); + } + Ok(()) + } +} + +impl ReceiptExt for T {} + +pub trait ZksyncWalletProviderExt: WalletProvider { + /// Creates and registers a random signer. Returns new signer's address. + fn register_random_signer(&mut self) -> Address { + let signer = PrivateKeySigner::random(); + let address = signer.address(); + self.wallet_mut().register_signer(signer); + address + } +} + +impl> ZksyncWalletProviderExt for T {} diff --git a/e2e-tests-rust/src/lib.rs b/e2e-tests-rust/src/lib.rs index 90ed7d60..072c3036 100644 --- a/e2e-tests-rust/src/lib.rs +++ b/e2e-tests-rust/src/lib.rs @@ -1,35 +1,8 @@ -use alloy::network::Network; -use alloy::providers::{Provider, ProviderCall}; -use alloy::rpc::client::NoParams; -use alloy::serde::WithOtherFields; -use alloy::transports::Transport; -use alloy_zksync::network::Zksync; +#![allow(async_fn_in_trait)] -pub mod utils; +mod ext; +mod provider; +mod utils; -pub trait AnvilZKsyncApiProvider: Provider -where - T: Transport + Clone, -{ - /// Custom version of [`alloy::providers::ext::AnvilApi::anvil_mine_detailed`] that returns - /// block representation with transactions that contain extra custom fields. - fn mine_detailed( - &self, - ) -> ProviderCall< - T, - NoParams, - alloy::rpc::types::Block< - WithOtherFields<::TransactionResponse>, - ::HeaderResponse, - >, - > { - self.client().request_noparams("anvil_mine_detailed").into() - } -} - -impl AnvilZKsyncApiProvider for P -where - T: Transport + Clone, - P: Provider, -{ -} +pub use ext::{ReceiptExt, ZksyncWalletProviderExt}; +pub use provider::{init_testing_provider, AnvilZKsyncApi, TestingProvider}; diff --git a/e2e-tests-rust/src/provider/anvil_zksync.rs b/e2e-tests-rust/src/provider/anvil_zksync.rs new file mode 100644 index 00000000..b38e9836 --- /dev/null +++ b/e2e-tests-rust/src/provider/anvil_zksync.rs @@ -0,0 +1,34 @@ +use alloy::network::Network; +use alloy::providers::{Provider, ProviderCall}; +use alloy::rpc::client::NoParams; +use alloy::serde::WithOtherFields; +use alloy::transports::Transport; +use alloy_zksync::network::Zksync; + +/// RPC interface that gives access to methods specific to anvil-zksync. +pub trait AnvilZKsyncApi: Provider +where + T: Transport + Clone, +{ + /// Custom version of [`alloy::providers::ext::AnvilApi::anvil_mine_detailed`] that returns + /// block representation with transactions that contain extra custom fields. + fn anvil_zksync_mine_detailed( + &self, + ) -> ProviderCall< + T, + NoParams, + alloy::rpc::types::Block< + WithOtherFields<::TransactionResponse>, + ::HeaderResponse, + >, + > { + self.client().request_noparams("anvil_mine_detailed").into() + } +} + +impl AnvilZKsyncApi for P +where + T: Transport + Clone, + P: Provider, +{ +} diff --git a/e2e-tests-rust/src/provider/mod.rs b/e2e-tests-rust/src/provider/mod.rs new file mode 100644 index 00000000..e13184f1 --- /dev/null +++ b/e2e-tests-rust/src/provider/mod.rs @@ -0,0 +1,5 @@ +mod anvil_zksync; +mod testing; + +pub use anvil_zksync::AnvilZKsyncApi; +pub use testing::{init_testing_provider, TestingProvider}; diff --git a/e2e-tests-rust/src/provider/testing.rs b/e2e-tests-rust/src/provider/testing.rs new file mode 100644 index 00000000..1b83f64f --- /dev/null +++ b/e2e-tests-rust/src/provider/testing.rs @@ -0,0 +1,360 @@ +use crate::utils::LockedPort; +use crate::ReceiptExt; +use alloy::network::{Network, TransactionBuilder}; +use alloy::primitives::{Address, U256}; +use alloy::providers::{ + PendingTransaction, PendingTransactionBuilder, PendingTransactionError, Provider, RootProvider, + SendableTx, WalletProvider, +}; +use alloy::rpc::types::TransactionRequest; +use alloy::transports::http::{reqwest, Http}; +use alloy::transports::{RpcError, Transport, TransportErrorKind, TransportResult}; +use alloy_zksync::network::receipt_response::ReceiptResponse; +use alloy_zksync::network::Zksync; +use alloy_zksync::node_bindings::EraTestNode; +use alloy_zksync::provider::{zksync_provider, ProviderBuilderExt}; +use alloy_zksync::wallet::ZksyncWallet; +use itertools::Itertools; +use std::future::Future; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; +use tokio::task::JoinHandle; + +/// Full requirements for the underlying Zksync provider. +pub trait FullZksyncProvider: + Provider + WalletProvider + Clone +where + T: Transport + Clone, +{ +} +impl FullZksyncProvider for P +where + P: Provider + WalletProvider + Clone, + T: Transport + Clone, +{ +} + +/// Testing provider that redirects all alloy functionality to the underlying provider but also provides +/// extra functionality for testing. +/// +/// It is also aware of rich accounts. It is a bit different from [`WalletProvider::signer_addresses`] +/// as signer set can change dynamically over time if, for example, user registers a new signer on +/// their side. +#[derive(Debug, Clone)] +pub struct TestingProvider +where + P: FullZksyncProvider, + T: Transport + Clone, +{ + inner: P, + rich_accounts: Vec
, + _pd: PhantomData, +} + +// Outside of `TestingProvider` to avoid specifying `P` +pub async fn init_testing_provider( + f: impl FnOnce(EraTestNode) -> EraTestNode, +) -> anyhow::Result< + TestingProvider>, Http>, +> { + let locked_port = LockedPort::acquire_unused().await?; + let provider = zksync_provider() + .with_recommended_fillers() + .on_era_test_node_with_wallet_and_config(|node| { + f(node + .path( + std::env::var("ANVIL_ZKSYNC_BINARY_PATH") + .unwrap_or("../target/release/anvil-zksync".to_string()), + ) + .port(locked_port.port)) + }); + + // Grab default rich accounts right after init. Note that subsequent calls to this method + // might return different value as wallet's signers are dynamic and can be changed by the user. + let rich_accounts = provider.signer_addresses().collect::>(); + // Wait for anvil-zksync to get up and be able to respond + provider.get_chain_id().await?; + // Explicitly unlock the port to showcase why we waited above + drop(locked_port); + + Ok(TestingProvider { + inner: provider, + rich_accounts, + _pd: Default::default(), + }) +} + +impl TestingProvider +where + P: FullZksyncProvider, + T: Transport + Clone, +{ + /// Returns a rich account under the requested index. Rich accounts returned from this method + /// are guaranteed to not change over the node's lifetime. + pub fn rich_account(&self, index: usize) -> Address { + *self + .rich_accounts + .get(index) + .unwrap_or_else(|| panic!("not enough rich accounts (#{} was requested)", index,)) + } +} + +impl TestingProvider +where + P: FullZksyncProvider, + T: Transport + Clone, + Self: 'static, +{ + /// Creates a default transaction (transfers 100 wei to a random account from the default signer) + /// and returns it as a builder. The builder can then be used to populate transaction with custom + /// data and then to register it or wait until it is finalized. + pub fn tx(&self) -> TestTxBuilder { + let tx = TransactionRequest::default() + .with_to(Address::random()) + .with_value(U256::from(100)); + TestTxBuilder { + inner: tx, + provider: (*self).clone(), + _pd: Default::default(), + } + } + + /// Submit `N` concurrent transactions and wait for all of them to finalize. Returns an array of + /// receipts packed as [`RacedReceipts`] (helper structure for asserting conditions on all receipts + /// at the same time). + pub async fn race_n_txs( + &self, + f: impl Fn(usize, TestTxBuilder) -> TestTxBuilder, + ) -> Result, PendingTransactionError> { + let pending_txs: [JoinHandle< + Result, PendingTransactionError>, + >; N] = std::array::from_fn(|i| { + let tx = f(i, self.tx()); + tokio::spawn(tx.register()) + }); + + let receipt_futures = futures::future::try_join_all(pending_txs) + .await + .expect("failed to join a handle") + .into_iter() + .map_ok(|pending_tx| pending_tx.wait_until_finalized()) + .collect::, _>>()?; + + let receipts = futures::future::join_all(receipt_futures) + .await + .into_iter() + .collect::, _>>()?; + + // Unwrap is safe as we are sure `receipts` contains exactly `N` elements + Ok(RacedReceipts { + receipts: receipts.try_into().unwrap(), + }) + } + + /// Convenience method over [`Self::race_n_txs`] that builds `N` default transactions but uses + /// a different rich signer for each of them. Panics if there is not enough rich accounts. + pub async fn race_n_txs_rich( + &self, + ) -> Result, PendingTransactionError> { + self.race_n_txs(|i, tx| tx.with_rich_from(i)).await + } +} + +#[async_trait::async_trait] +impl Provider for TestingProvider +where + P: FullZksyncProvider, + T: Transport + Clone, +{ + fn root(&self) -> &RootProvider { + self.inner.root() + } + + async fn send_transaction_internal( + &self, + tx: SendableTx, + ) -> TransportResult> { + self.inner.send_transaction_internal(tx).await + } +} + +impl, T: Transport + Clone> WalletProvider + for TestingProvider +{ + type Wallet = ZksyncWallet; + + fn wallet(&self) -> &Self::Wallet { + self.inner.wallet() + } + + fn wallet_mut(&mut self) -> &mut Self::Wallet { + self.inner.wallet_mut() + } +} + +/// Helper struct for building and submitting transactions. Main idea here is to reduce the amount +/// of boilerplate for users who just want to submit default transactions (see [`TestingProvider::tx`]) +/// most of the time. Also returns wrapped pending transaction in the form of [`PendingTransactionFinalizable`], +/// which can be finalized without a user-supplied provider instance. +pub struct TestTxBuilder +where + P: FullZksyncProvider, + T: Transport + Clone, +{ + inner: TransactionRequest, + provider: TestingProvider, + _pd: PhantomData, +} + +impl TestTxBuilder +where + T: Transport + Clone, + P: FullZksyncProvider, +{ + /// Builder-pattern method for setting the sender. + pub fn with_from(mut self, from: Address) -> Self { + self.inner = self.inner.with_from(from); + self + } + + /// Sets the sender to an indexed rich account (see [`TestingProvider::rich_account`]). + pub fn with_rich_from(mut self, index: usize) -> Self { + let from = self.provider.rich_account(index); + self.inner = self.inner.with_from(from); + self + } + + /// Submits transaction to the node. + /// + /// This does not wait for the transaction to be confirmed, but returns a [`PendingTransactionFinalizable`] + /// that can be awaited at a later moment. + pub async fn register( + self, + ) -> Result, PendingTransactionError> { + let pending_tx = self + .provider + .send_transaction(self.inner.into()) + .await? + .register() + .await?; + Ok(PendingTransactionFinalizable { + inner: pending_tx, + provider: self.provider.root().clone(), + }) + } + + /// Waits for the transaction to finalize with the given number of confirmations and then fetches + /// its receipt. + pub async fn finalize(self) -> Result { + self.provider + .send_transaction(self.inner.into()) + .await? + .get_receipt() + .await + } +} + +/// A wrapper around [`PendingTransaction`] that holds a provider instance which can be used to check +/// if the transaction is finalized or not without user supplying it again. Also contains helper +/// methods to assert different finalization scenarios. +pub struct PendingTransactionFinalizable { + inner: PendingTransaction, + provider: RootProvider, +} + +impl AsRef for PendingTransactionFinalizable { + fn as_ref(&self) -> &PendingTransaction { + &self.inner + } +} + +impl AsMut for PendingTransactionFinalizable { + fn as_mut(&mut self) -> &mut PendingTransaction { + &mut self.inner + } +} + +impl Deref for PendingTransactionFinalizable { + type Target = PendingTransaction; + + fn deref(&self) -> &PendingTransaction { + &self.inner + } +} + +impl DerefMut for PendingTransactionFinalizable { + fn deref_mut(&mut self) -> &mut PendingTransaction { + &mut self.inner + } +} + +impl Future for PendingTransactionFinalizable { + type Output = ::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.inner).poll(cx) + } +} + +impl PendingTransactionFinalizable { + /// Asserts that transaction is finalizable by waiting until its receipt gets resolved. + pub async fn wait_until_finalized(self) -> Result { + let tx_hash = self.inner.await?; + let receipt = self.provider.get_transaction_receipt(tx_hash).await?; + if let Some(receipt) = receipt { + Ok(receipt) + } else { + Err(RpcError::::NullResp.into()) + } + } + + /// Asserts that transaction is not finalizable by expecting to timeout in the given duration + /// while trying to resolve its receipt. + pub async fn assert_not_finalizable(mut self, duration: Duration) -> anyhow::Result { + let timeout = tokio::time::timeout(duration, &mut self); + match timeout.await { + Ok(Ok(tx_hash)) => { + anyhow::bail!( + "expected transaction (hash={}) to not be finalizable, but it was", + tx_hash + ); + } + Ok(Err(e)) => { + anyhow::bail!("failed to wait for a pending transaction: {}", e); + } + Err(_) => Ok(self), + } + } +} + +/// Helper wrapper of `N` receipts of transactions that were raced together (see +/// [`TestingProvider::race_n_txs`]). Contains method for asserting different conditions for all receipts. +pub struct RacedReceipts { + pub receipts: [ReceiptResponse; N], +} + +impl RacedReceipts { + /// Asserts that all transactions were successful. + pub fn assert_successful(self) -> anyhow::Result { + for receipt in &self.receipts { + receipt.assert_successful()?; + } + Ok(self) + } + + /// Asserts that all transactions were sealed in the same block. + pub fn assert_same_block(self) -> anyhow::Result { + if N == 0 { + return Ok(self); + } + let first = &self.receipts[0]; + for receipt in &self.receipts[1..] { + receipt.assert_same_block(first)?; + } + + Ok(self) + } +} diff --git a/e2e-tests-rust/tests/lib.rs b/e2e-tests-rust/tests/lib.rs index 60eaf716..e397a523 100644 --- a/e2e-tests-rust/tests/lib.rs +++ b/e2e-tests-rust/tests/lib.rs @@ -1,108 +1,17 @@ -use alloy::network::{ReceiptResponse, TransactionBuilder}; -use alloy::primitives::{address, Address, U256}; +use alloy::network::ReceiptResponse; use alloy::providers::ext::AnvilApi; -use alloy::providers::{PendingTransaction, PendingTransactionError, Provider, WalletProvider}; -use alloy::signers::local::PrivateKeySigner; -use alloy::transports::http::{reqwest, Http}; -use alloy_zksync::network::transaction_request::TransactionRequest; -use alloy_zksync::network::Zksync; -use alloy_zksync::node_bindings::EraTestNode; -use alloy_zksync::provider::{zksync_provider, ProviderBuilderExt}; -use alloy_zksync::wallet::ZksyncWallet; -use anvil_zksync_e2e_tests::utils::LockedPort; -use anvil_zksync_e2e_tests::AnvilZKsyncApiProvider; +use anvil_zksync_e2e_tests::{ + init_testing_provider, AnvilZKsyncApi, ReceiptExt, ZksyncWalletProviderExt, +}; use std::time::Duration; -async fn init( - f: impl FnOnce(EraTestNode) -> EraTestNode, -) -> anyhow::Result< - impl Provider, Zksync> + WalletProvider + Clone, -> { - let locked_port = LockedPort::acquire_unused().await?; - let provider = zksync_provider() - .with_recommended_fillers() - .on_era_test_node_with_wallet_and_config(|node| { - f(node - .path( - std::env::var("ANVIL_ZKSYNC_BINARY_PATH") - .unwrap_or("../target/release/anvil-zksync".to_string()), - ) - .port(locked_port.port)) - }); - - // Wait for anvil-zksync to get up and be able to respond - provider.get_accounts().await?; - // Explicitly unlock the port to showcase why we waited above - drop(locked_port); - - Ok(provider) -} - -const RICH_WALLET0: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); -const RICH_WALLET1: Address = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"); - -async fn test_finalize_two_txs_in_the_same_block( - provider: impl Provider, Zksync> + WalletProvider + Clone + 'static, - target: Address, -) -> anyhow::Result<()> { - async fn submit_tx( - provider: impl Provider, Zksync> + WalletProvider + Clone, - rich_wallet: Address, - target: Address, - ) -> Result { - let tx = TransactionRequest::default() - .with_from(rich_wallet) - .with_to(target) - .with_value(U256::from(100)); - provider.send_transaction(tx).await?.register().await - } - - // Submit two txs at the same time - let handle0 = tokio::spawn(submit_tx(provider.clone(), RICH_WALLET0, target)); - let handle1 = tokio::spawn(submit_tx(provider.clone(), RICH_WALLET1, target)); - - // Wait until both are finalized - let (pending_tx0, pending_tx1) = tokio::join!(handle0, handle1); - let pending_tx0 = pending_tx0??; - let pending_tx1 = pending_tx1??; - - // Fetch their receipts - let receipt0 = provider - .get_transaction_receipt(pending_tx0.await?) - .await? - .unwrap(); - assert!(receipt0.status()); - let receipt1 = provider - .get_transaction_receipt(pending_tx1.await?) - .await? - .unwrap(); - assert!(receipt1.status()); - - // Assert that they are different txs but executed in the same block - assert_eq!(receipt0.from(), RICH_WALLET0); - assert_eq!(receipt1.from(), RICH_WALLET1); - assert_ne!(receipt0.transaction_hash(), receipt1.transaction_hash()); - - // But executed in the same block - assert!(receipt0.block_number().is_some()); - assert_eq!(receipt0.block_number(), receipt1.block_number()); - assert!(receipt0.block_hash().is_some()); - assert_eq!(receipt0.block_hash(), receipt1.block_hash()); - - Ok(()) -} - #[tokio::test] async fn interval_sealing_finalization() -> anyhow::Result<()> { // Test that we can submit a transaction and wait for it to finalize when anvil-zksync is // operating in interval sealing mode. - let provider = init(|node| node.block_time(1)).await?; + let provider = init_testing_provider(|node| node.block_time(1)).await?; - let tx = TransactionRequest::default() - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let receipt = provider.send_transaction(tx).await?.get_receipt().await?; - assert!(receipt.status()); + provider.tx().finalize().await?.assert_successful()?; Ok(()) } @@ -112,13 +21,13 @@ async fn interval_sealing_multiple_txs() -> anyhow::Result<()> { // Test that we can submit two transactions and wait for them to finalize in the same block when // anvil-zksync is operating in interval sealing mode. 3 seconds should be long enough for // the entire flow to execute before the first block is produced. - let provider = init(|node| node.block_time(3)).await?; + let provider = init_testing_provider(|node| node.block_time(3)).await?; - test_finalize_two_txs_in_the_same_block( - provider, - address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"), - ) - .await?; + provider + .race_n_txs_rich::<2>() + .await? + .assert_successful()? + .assert_same_block()?; Ok(()) } @@ -127,20 +36,19 @@ async fn interval_sealing_multiple_txs() -> anyhow::Result<()> { async fn no_sealing_timeout() -> anyhow::Result<()> { // Test that we can submit a transaction and timeout while waiting for it to finalize when // anvil-zksync is operating in no sealing mode. - let provider = init(|node| node.no_mine()).await?; + let provider = init_testing_provider(|node| node.no_mine()).await?; - let tx = TransactionRequest::default() - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx = provider.send_transaction(tx).await?.register().await?; - let tx_hash = pending_tx.tx_hash().clone(); - let finalization_result = tokio::time::timeout(Duration::from_secs(3), pending_tx).await; - assert!(finalization_result.is_err()); + let pending_tx = provider.tx().register().await?; + let pending_tx = pending_tx + .assert_not_finalizable(Duration::from_secs(3)) + .await?; // Mine a block manually and assert that the transaction is finalized now provider.anvil_mine(None, None).await?; - let receipt = provider.get_transaction_receipt(tx_hash).await?.unwrap(); - assert!(receipt.status()); + pending_tx + .wait_until_finalized() + .await? + .assert_successful()?; Ok(()) } @@ -148,7 +56,7 @@ async fn no_sealing_timeout() -> anyhow::Result<()> { #[tokio::test] async fn dynamic_sealing_mode() -> anyhow::Result<()> { // Test that we can successfully switch between different sealing modes - let provider = init(|node| node.no_mine()).await?; + let provider = init_testing_provider(|node| node.no_mine()).await?; assert_eq!(provider.anvil_get_auto_mine().await?, false); // Enable immediate block sealing @@ -156,10 +64,7 @@ async fn dynamic_sealing_mode() -> anyhow::Result<()> { assert_eq!(provider.anvil_get_auto_mine().await?, true); // Check that we can finalize transactions now - let tx = TransactionRequest::default() - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let receipt = provider.send_transaction(tx).await?.get_receipt().await?; + let receipt = provider.tx().finalize().await?; assert!(receipt.status()); // Enable interval block sealing @@ -167,23 +72,23 @@ async fn dynamic_sealing_mode() -> anyhow::Result<()> { assert_eq!(provider.anvil_get_auto_mine().await?, false); // Check that we can finalize two txs in the same block now - test_finalize_two_txs_in_the_same_block( - provider.clone(), - address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"), - ) - .await?; + provider + .race_n_txs_rich::<2>() + .await? + .assert_successful()? + .assert_same_block()?; // Disable block sealing entirely provider.anvil_set_auto_mine(false).await?; assert_eq!(provider.anvil_get_auto_mine().await?, false); // Check that transactions do not get finalized now - let tx = TransactionRequest::default() - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx = provider.send_transaction(tx).await?.register().await?; - let finalization_result = tokio::time::timeout(Duration::from_secs(3), pending_tx).await; - assert!(finalization_result.is_err()); + provider + .tx() + .register() + .await? + .assert_not_finalizable(Duration::from_secs(3)) + .await?; Ok(()) } @@ -193,19 +98,10 @@ async fn drop_transaction() -> anyhow::Result<()> { // Test that we can submit two transactions and then remove one from the pool before it gets // finalized. 3 seconds should be long enough for the entire flow to execute before the first // block is produced. - let provider = init(|node| node.block_time(3)).await?; + let provider = init_testing_provider(|node| node.block_time(3)).await?; - // Submit two transactions - let tx0 = TransactionRequest::default() - .with_from(RICH_WALLET0) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx0 = provider.send_transaction(tx0).await?.register().await?; - let tx1 = TransactionRequest::default() - .with_from(RICH_WALLET1) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx1 = provider.send_transaction(tx1).await?.register().await?; + let pending_tx0 = provider.tx().with_rich_from(0).register().await?; + let pending_tx1 = provider.tx().with_rich_from(1).register().await?; // Drop first provider @@ -213,44 +109,37 @@ async fn drop_transaction() -> anyhow::Result<()> { .await?; // Assert first never gets finalized but the second one does - let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await; - assert!(finalization_result.is_err()); - let receipt = provider - .get_transaction_receipt(pending_tx1.await?) + pending_tx0 + .assert_not_finalizable(Duration::from_secs(4)) + .await?; + pending_tx1 + .wait_until_finalized() .await? - .unwrap(); - assert!(receipt.status()); + .assert_successful()?; Ok(()) } #[tokio::test] async fn drop_all_transactions() -> anyhow::Result<()> { - // Test that we can submit two transactions and then remove them from the pool before the get + // Test that we can submit two transactions and then remove them from the pool before they get // finalized. 3 seconds should be long enough for the entire flow to execute before the first // block is produced. - let provider = init(|node| node.block_time(3)).await?; + let provider = init_testing_provider(|node| node.block_time(3)).await?; - // Submit two transactions - let tx0 = TransactionRequest::default() - .with_from(RICH_WALLET0) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx0 = provider.send_transaction(tx0).await?.register().await?; - let tx1 = TransactionRequest::default() - .with_from(RICH_WALLET1) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx1 = provider.send_transaction(tx1).await?.register().await?; + let pending_tx0 = provider.tx().with_rich_from(0).register().await?; + let pending_tx1 = provider.tx().with_rich_from(1).register().await?; // Drop all transactions provider.anvil_drop_all_transactions().await?; // Neither transaction gets finalized - let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await; - assert!(finalization_result.is_err()); - let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx1).await; - assert!(finalization_result.is_err()); + pending_tx0 + .assert_not_finalizable(Duration::from_secs(4)) + .await?; + pending_tx1 + .assert_not_finalizable(Duration::from_secs(4)) + .await?; Ok(()) } @@ -260,33 +149,25 @@ async fn remove_pool_transactions() -> anyhow::Result<()> { // Test that we can submit two transactions from two senders and then remove first sender's // transaction from the pool before it gets finalized. 3 seconds should be long enough for the // entire flow to execute before the first block is produced. - let provider = init(|node| node.block_time(3)).await?; + let provider = init_testing_provider(|node| node.block_time(3)).await?; // Submit two transactions - let tx0 = TransactionRequest::default() - .with_from(RICH_WALLET0) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx0 = provider.send_transaction(tx0).await?.register().await?; - let tx1 = TransactionRequest::default() - .with_from(RICH_WALLET1) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx1 = provider.send_transaction(tx1).await?.register().await?; + let pending_tx0 = provider.tx().with_rich_from(0).register().await?; + let pending_tx1 = provider.tx().with_rich_from(1).register().await?; // Drop first provider - .anvil_remove_pool_transactions(RICH_WALLET0) + .anvil_remove_pool_transactions(provider.rich_account(0)) .await?; // Assert first never gets finalized but the second one does - let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await; - assert!(finalization_result.is_err()); - let receipt = provider - .get_transaction_receipt(pending_tx1.await?) + pending_tx0 + .assert_not_finalizable(Duration::from_secs(4)) + .await?; + pending_tx1 + .wait_until_finalized() .await? - .unwrap(); - assert!(receipt.status()); + .assert_successful()?; Ok(()) } @@ -295,51 +176,32 @@ async fn remove_pool_transactions() -> anyhow::Result<()> { async fn manual_mining_two_txs_in_one_block() -> anyhow::Result<()> { // Test that we can submit two transaction and then manually mine one block that contains both // transactions in it. - let provider = init(|node| node.no_mine()).await?; - - let tx0 = TransactionRequest::default() - .with_from(RICH_WALLET0) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx0 = provider.send_transaction(tx0).await?.register().await?; - let tx1 = TransactionRequest::default() - .with_from(RICH_WALLET1) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx1 = provider.send_transaction(tx1).await?.register().await?; + let provider = init_testing_provider(|node| node.no_mine()).await?; + + let pending_tx0 = provider.tx().with_rich_from(0).register().await?; + let pending_tx1 = provider.tx().with_rich_from(1).register().await?; // Mine a block manually and assert that both transactions are finalized now provider.anvil_mine(None, None).await?; - let receipt0 = provider - .get_transaction_receipt(pending_tx0.await?) - .await? - .unwrap(); - assert!(receipt0.status()); - let receipt1 = provider - .get_transaction_receipt(pending_tx1.await?) - .await? - .unwrap(); - assert!(receipt1.status()); - assert_eq!(receipt0.block_hash(), receipt1.block_hash()); - assert_eq!(receipt0.block_number(), receipt1.block_number()); + let receipt0 = pending_tx0.wait_until_finalized().await?; + receipt0.assert_successful()?; + let receipt1 = pending_tx1.wait_until_finalized().await?; + receipt1.assert_successful()?; + receipt0.assert_same_block(&receipt1)?; Ok(()) } #[tokio::test] async fn detailed_mining_success() -> anyhow::Result<()> { - // Test that we can detailed mining on a successful transaction and get output from it. - let provider = init(|node| node.no_mine()).await?; + // Test that we can call detailed mining after a successful transaction and match output from it. + let provider = init_testing_provider(|node| node.no_mine()).await?; - let tx = TransactionRequest::default() - .with_from(RICH_WALLET0) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - provider.send_transaction(tx).await?.register().await?; + provider.tx().register().await?; // Mine a block manually and assert that it has our transaction with extra fields - let block = provider.mine_detailed().await?; + let block = provider.anvil_zksync_mine_detailed().await?; assert_eq!(block.transactions.len(), 1); let actual_tx = block .transactions @@ -361,58 +223,32 @@ async fn detailed_mining_success() -> anyhow::Result<()> { async fn seal_block_ignoring_halted_transaction() -> anyhow::Result<()> { // Test that we can submit three transactions (1 and 3 are successful, 2 is halting). And then // observe a block that finalizes 1 and 3 while ignoring 2. - let mut provider = init(|node| node.block_time(3)).await?; - let signer = PrivateKeySigner::random(); - let random_account = signer.address(); - provider.wallet_mut().register_signer(signer); + let mut provider = init_testing_provider(|node| node.block_time(3)).await?; + let random_account = provider.register_random_signer(); // Impersonate random account for now so that gas estimation works as expected provider.anvil_impersonate_account(random_account).await?; - // Submit three transactions - let tx0 = TransactionRequest::default() - .with_from(RICH_WALLET0) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx0 = provider.send_transaction(tx0).await?.register().await?; - let tx1 = TransactionRequest::default() - .with_from(random_account) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx1 = provider.send_transaction(tx1).await?.register().await?; - let tx2 = TransactionRequest::default() - .with_from(RICH_WALLET1) - .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) - .with_value(U256::from(100)); - let pending_tx2 = provider.send_transaction(tx2).await?.register().await?; + let pending_tx0 = provider.tx().with_rich_from(0).register().await?; + let pending_tx1 = provider.tx().with_from(random_account).register().await?; + let pending_tx2 = provider.tx().with_rich_from(1).register().await?; // Stop impersonating random account so that tx is going to halt provider .anvil_stop_impersonating_account(random_account) .await?; - // Fetch their receipts - let receipt0 = provider - .get_transaction_receipt(pending_tx0.await?) - .await? - .unwrap(); - assert!(receipt0.status()); - let receipt2 = provider - .get_transaction_receipt(pending_tx2.await?) - .await? - .unwrap(); - assert!(receipt2.status()); - - // Assert that they are different txs but executed in the same block - assert_eq!(receipt0.from(), RICH_WALLET0); - assert_eq!(receipt2.from(), RICH_WALLET1); - assert_ne!(receipt0.transaction_hash(), receipt2.transaction_hash()); - assert_eq!(receipt0.block_hash(), receipt2.block_hash()); - assert_eq!(receipt0.block_number(), receipt2.block_number()); + // Fetch their receipts and assert they are executed in the same block + let receipt0 = pending_tx0.wait_until_finalized().await?; + receipt0.assert_successful()?; + let receipt2 = pending_tx2.wait_until_finalized().await?; + receipt2.assert_successful()?; + receipt0.assert_same_block(&receipt2)?; // Halted transaction never gets finalized - let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx1).await; - assert!(finalization_result.is_err()); + pending_tx1 + .assert_not_finalizable(Duration::from_secs(4)) + .await?; Ok(()) }