diff --git a/.github/scripts/check-wasm.sh b/.github/scripts/check-wasm.sh new file mode 100755 index 00000000..b33b0f79 --- /dev/null +++ b/.github/scripts/check-wasm.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +mydir=$(dirname "$0") +cd "$mydir" || exit +cd ../.. + +# Check contract wasm binary by crate name +check_wasm () { + local CONTRACT_CRATE_NAME=$1 + local CONTRACT_BIN_NAME="${CONTRACT_CRATE_NAME//-/_}.wasm" + + echo + echo "Checking contract $CONTRACT_CRATE_NAME" + cargo stylus check --wasm-file-path ./target/wasm32-unknown-unknown/release/"$CONTRACT_BIN_NAME" +} + +# Retrieve all alphanumeric contract's crate names in `./examples` directory. +get_example_crate_names () { + # shellcheck disable=SC2038 + # NOTE: optimistically relying on the 'name = ' string at Cargo.toml file + find ./examples -type f -name "Cargo.toml" | xargs grep 'name = ' | grep -oE '".*"' | tr -d "'\"" +} + +NIGHTLY_TOOLCHAIN=${NIGHTLY_TOOLCHAIN:-nightly} + +cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort + +for CRATE_NAME in $(get_example_crate_names) +do + check_wasm "$CRATE_NAME" +done diff --git a/.github/workflows/check-wasm.yml b/.github/workflows/check-wasm.yml new file mode 100644 index 00000000..b1179451 --- /dev/null +++ b/.github/workflows/check-wasm.yml @@ -0,0 +1,56 @@ +name: check-wasm +# This workflow checks that the compiled wasm binary of every example contract +# can be deployed to Arbitrum Stylus. +# +# NOTE: This is using version `0.2.1` of `cargo-stylus`, which we will need +# to update once `nitro-testnode` gets updated. +permissions: + contents: read +on: + push: + branches: [ main ] + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + CARGO_TERM_COLOR: always +jobs: + check-wasm: + name: check WASM binary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} + + - name: cache cargo-stylus + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/.crates.toml + key: ${{ runner.os }}-cargo-bin-cargo-stylus@0.2.1 + save-always: true + + - name: set up rust + uses: dtolnay/rust-toolchain@master + id: toolchain + with: + target: wasm32-unknown-unknown + components: rust-src + toolchain: nightly-2024-01-01 + + - name: install cargo-stylus + run: RUSTFLAGS="-C link-args=-rdynamic" cargo install cargo-stylus@0.2.1 + + - name: run wasm check + run: | + export NIGHTLY_TOOLCHAIN=${{steps.toolchain.outputs.name}} + ./.github/scripts/check-wasm.sh \ No newline at end of file diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 366bf541..ad1c4d57 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -3,9 +3,8 @@ name: e2e-tests # # It roughly follows these steps: # - A local `nitro-testnode` gets spun up. -# - Contracts get deployed to the local node. -# - A few addresses get funded. -# - The test suite runs. +# - The test suite runs +# Test contract deployments and test user funding happen per test. permissions: contents: read on: @@ -53,20 +52,7 @@ jobs: run: RUSTFLAGS="-C link-args=-rdynamic" cargo install cargo-stylus@0.2.1 - name: setup nitro node - run: | - # clone nitro test node repo - git clone -b stylus --recurse-submodules https://github.com/OffchainLabs/nitro-testnode.git && cd nitro-testnode - git checkout 1886f4b89f5c20fd5b0c2cf3d08a009ee73e45ca - - # setup nitro test node - ./test-node.bash --no-run --init --no-tokenbridge - ./test-node.bash --detach - - # TODO: remove hard coded wallets when user creation will be per test case - # fund Alice's wallet - ./test-node.bash script send-l2 --to address_0x01fA6bf4Ee48B6C95900BCcf9BEA172EF5DBd478 --ethamount 10000 - # fund Bob's wallet - ./test-node.bash script send-l2 --to address_0xF4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526 --ethamount 10000 + run: ./e2e-tests/nitro-testnode.sh -d -i - name: run integration tests run: | diff --git a/.gitignore b/.gitignore index c6170f88..ccb1d223 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ docs/build/ **/.DS_Store + +/e2e-tests/nitro-testnode diff --git a/Cargo.lock b/Cargo.lock index 30e17c4f..4963e99d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,10 +702,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] -name = "e2e-tests" +name = "e2e" version = "0.1.0" dependencies = [ "async-trait", + "e2e-proc", + "ethers", + "eyre", + "once_cell", + "regex", + "tokio", +] + +[[package]] +name = "e2e-proc" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "e2e-tests" +version = "0.1.0" +dependencies = [ + "e2e", "ethers", "eyre", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 45a687f5..933d3bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ members = [ "examples/ownable", "examples/access-control", "e2e-tests", + "lib/e2e", + "lib/e2e-proc", ] default-members = [ "contracts", @@ -21,6 +23,7 @@ default-members = [ "examples/merkle-proofs", "examples/ownable", "examples/access-control", + "lib/e2e-proc", ] # Explicitly set the resolver to version 2, which is the default for packages diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index f0f40dbc..11469a37 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -11,7 +11,7 @@ repository.workspace = true tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" -async-trait = "0.1.80" +e2e = { path = "../lib/e2e" } [lib] doctest = false diff --git a/e2e-tests/README.md b/e2e-tests/README.md index 45a3aa18..1f42fa64 100644 --- a/e2e-tests/README.md +++ b/e2e-tests/README.md @@ -2,49 +2,31 @@ ## Run tests -Deploy every contract from `./examples` directory and running integration tests. +### Setup local nitro node -### Against local nitro node - -Set up first a local nitro node according to -this [guide](https://github.com/OffchainLabs/nitro-testnode/blob/release/README.md) +Run in detached mode: ```terminal -# setup nitro test node in detached mode -# docker images should be shutdown manually later -./test-node.bash --no-run --init --no-tokenbridge -./test-node.bash --detach - -# fund Alice's wallet -./test-node.bash script send-l2 --to address_0x01fA6bf4Ee48B6C95900BCcf9BEA172EF5DBd478 --ethamount 10000 -# fund Bob's wallet -./test-node.bash script send-l2 --to address_0xF4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526 --ethamount 10000 +./nitro-testnode -d ``` -Run integration testing command: +Clean local nitro node to free system resources: ```terminal - ./e2e-tests/test.sh +./nitro-testnode -down ``` -### Against stylus dev net +### Run end-to-end tests -`ALICE_PRIV_KEY` and `BOB_PRIV_KEY` should be valid funded wallets. -`RPC_URL` should contain url of the stylus testnet. -Run this command: +Builds every contract with entrypoint and run tests against locally deployed nitro node: ```terminal - ALICE_PRIV_KEY=0x... \ - BOB_PRIV_KEY=0x... \ - RPC_URL=https://stylus-testnet.arbitrum.io/rpc \ - ./e2e-tests//test.sh +./test.sh ``` ## Add test for the new contract -Assuming that contract associated crate exists at `./examples` directory -with the crate name `erc20-example`. -Add ethereum contracts to `./e2e-tests/src/context` directory like: +Add solidity abi: ```rust ethers::contract::abigen!( @@ -58,15 +40,15 @@ ethers::contract::abigen!( ); ``` -Then add wrapper type for the contract: +Then add wrapper type for the contract and link to an example crate name: ```rust pub type Erc20 = Erc20Token; link_to_crate!(Erc20, "erc20-example"); ``` -Tests should create new infrastructure instance like this: +Tests should instantiate new contract like this: ```rust -let context = E2EContext::::new().await?; +let erc20 = &alice.deploys::().await?; ``` diff --git a/e2e-tests/build.rs b/e2e-tests/build.rs new file mode 100644 index 00000000..417dc47b --- /dev/null +++ b/e2e-tests/build.rs @@ -0,0 +1,30 @@ +use std::path::{Path, PathBuf}; + +fn main() { + set_env("TARGET_DIR", &get_target_dir()); + set_env("RPC_URL", "http://localhost:8547"); + set_env( + "TEST_NITRO_NODE_PATH", + Path::new(&load_env_var("CARGO_MANIFEST_DIR")) + .join("nitro-testnode") + .to_str() + .expect("set env var TEST_NITRO_NODE_PATH"), + ); +} + +fn set_env(var_name: &str, value: &str) { + println!("cargo:rustc-env={}={}", var_name, value); +} + +fn load_env_var(var_name: &str) -> String { + std::env::var(var_name) + .unwrap_or_else(|_| panic!("failed to load {} env var", var_name)) +} + +fn get_target_dir() -> String { + // should be smth like + // ./rust-contracts-stylus/target/debug/build/e2e-tests-b008947425bb8267/out + let out_dir = load_env_var("OUT_DIR"); + let target_dir = Path::new(&out_dir).join("../../../../"); + target_dir.to_str().expect("target dir").to_string() +} diff --git a/e2e-tests/nitro-testnode.sh b/e2e-tests/nitro-testnode.sh new file mode 100755 index 00000000..6ed20fb2 --- /dev/null +++ b/e2e-tests/nitro-testnode.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +MYDIR=$(realpath "$(dirname "$0")") +cd "$MYDIR" || exit + +HAS_INIT=false +HAS_DETACH=false + +while [[ $# -gt 0 ]] +do + case "$1" in + -i|--init) + HAS_INIT=true + shift + ;; + -d|--detach) + HAS_DETACH=true + shift + ;; + -down|--shutdown) + docker container stop "$(docker container ls -q --filter name=nitro-testnode)" + exit 0 + ;; + *) + echo "OPTIONS:" + echo "-i|--init: clone repo and init nitro test node" + echo "-d|--detach: setup nitro test node in detached mode" + echo "-down|--shutdown: shutdown nitro test node docker containers" + exit 0 + ;; + esac +done + +TEST_NODE_DIR="$MYDIR/nitro-testnode" +if [ ! -d "$TEST_NODE_DIR" ]; then + HAS_INIT=true +fi + +if $HAS_INIT +then + cd "$MYDIR" || exit + # clone nitro test node repo + git clone -b stylus --recurse-submodules https://github.com/OffchainLabs/nitro-testnode.git + cd ./nitro-testnode || exit + git checkout 1886f4b89f5c20fd5b0c2cf3d08a009ee73e45ca || exit + + # setup nitro test node + ./test-node.bash --no-run --init --no-tokenbridge || exit +fi + + +cd "$TEST_NODE_DIR" || exit +if $HAS_DETACH +then + ./test-node.bash --detach +else + ./test-node.bash +fi \ No newline at end of file diff --git a/e2e-tests/src/context/erc20.rs b/e2e-tests/src/abi/erc20.rs similarity index 96% rename from e2e-tests/src/context/erc20.rs rename to e2e-tests/src/abi/erc20.rs index 31e9b629..b1afdc00 100644 --- a/e2e-tests/src/context/erc20.rs +++ b/e2e-tests/src/abi/erc20.rs @@ -1,6 +1,4 @@ -use ethers::prelude::*; - -use crate::context::*; +use e2e::prelude::*; abigen!( Erc20Token, diff --git a/e2e-tests/src/context/erc721.rs b/e2e-tests/src/abi/erc721.rs similarity index 97% rename from e2e-tests/src/context/erc721.rs rename to e2e-tests/src/abi/erc721.rs index 02e0afdd..f3bff0f3 100644 --- a/e2e-tests/src/context/erc721.rs +++ b/e2e-tests/src/abi/erc721.rs @@ -1,6 +1,4 @@ -use ethers::prelude::*; - -use crate::context::*; +use e2e::prelude::*; abigen!( Erc721Token, diff --git a/e2e-tests/src/abi/mod.rs b/e2e-tests/src/abi/mod.rs new file mode 100644 index 00000000..7f6176ba --- /dev/null +++ b/e2e-tests/src/abi/mod.rs @@ -0,0 +1,2 @@ +pub mod erc20; +pub mod erc721; diff --git a/e2e-tests/src/context/mod.rs b/e2e-tests/src/context/mod.rs deleted file mode 100644 index fb14eaca..00000000 --- a/e2e-tests/src/context/mod.rs +++ /dev/null @@ -1,225 +0,0 @@ -pub mod erc20; -pub mod erc721; - -use std::{ - ops::{Add, Deref}, - str::FromStr, - sync::Arc, -}; - -use async_trait::async_trait; -use ethers::{ - abi::{AbiEncode, Detokenize}, - contract::ContractCall, - middleware::{Middleware, SignerMiddleware}, - providers::{Http, Provider}, - signers::{LocalWallet, Signer}, - types::{Address, TransactionReceipt, U256}, -}; -use eyre::{bail, Context, ContextCompat, Report, Result}; - -const ALICE_PRIV_KEY: &str = "ALICE_PRIV_KEY"; -const BOB_PRIV_KEY: &str = "BOB_PRIV_KEY"; -const RPC_URL: &str = "RPC_URL"; - -/// End-to-end testing context that allows to act on behalf of `Alice` -/// and `Bob` accounts. -pub struct E2EContext { - pub alice: Client, - pub bob: Client, -} - -impl E2EContext { - /// Constructs new instance of an integration testing context. - /// - /// Requires env variables `ALICE_PRIV_KEY`, `BOB_PRIV_KEY`, `RPC_URL` - /// and _DEPLOYMENT_ADDRESS - /// where is the "SCREAMING_SNAKE_CASE" conversion of the crate - /// name from the `./examples` directory. - pub async fn new() -> Result { - let alice_priv_key = - std::env::var(ALICE_PRIV_KEY).with_context(|| { - format!("failed to load {} env var", ALICE_PRIV_KEY) - })?; - let bob_priv_key = std::env::var(BOB_PRIV_KEY).with_context(|| { - format!("failed to load {} env var", BOB_PRIV_KEY) - })?; - let rpc_url = std::env::var(RPC_URL) - .with_context(|| format!("failed to load {} env var", RPC_URL))?; - - let program_address_env_name = T::CRATE_NAME - .replace('-', "_") - .to_ascii_uppercase() - .add("_DEPLOYMENT_ADDRESS"); - let program_address: Address = std::env::var(&program_address_env_name) - .with_context(|| { - format!("failed to load {} env var", program_address_env_name) - })? - .parse()?; - - let provider = Provider::::try_from(rpc_url)?; - - Ok(E2EContext { - alice: Client::new( - provider.clone(), - program_address, - alice_priv_key, - ) - .await?, - bob: Client::new(provider, program_address, bob_priv_key).await?, - }) - } -} - -/// Client of participant that allows to check wallet address and call contract -/// functions. -pub struct Client { - pub wallet: LocalWallet, - pub contract: T, -} - -impl Deref for Client { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.contract - } -} - -/// Abstraction for the deployed contract. -pub trait Contract { - /// Crate name of the contract. - /// - /// e.g can be `erc721-example`. - const CRATE_NAME: &'static str; - - /// Abstracts token creation function. - /// - /// e.g. `Self::new(address, client)`. - fn new(address: Address, client: Arc) -> Self; -} - -/// Link `abigen!` contract to the crate name in `./examples` directory. -/// -/// # Example -/// ``` -/// use e2e-tests::{link_to_crate, context::HttpMiddleware}; -/// use ethers::contract::abigen; -/// -/// abigen!( -/// Erc20Token, -/// r#"[ -/// function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) -/// function mint(address account, uint256 amount) external -/// -/// error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed) -/// ]"# -/// ); -/// -/// pub type Erc20 = Erc20Token; -/// link_to_crate!(Erc20, "erc20-example"); -/// ``` -macro_rules! link_to_crate { - ($token_type:ty, $program_address:literal) => { - impl $crate::context::Contract for $token_type { - const CRATE_NAME: &'static str = $program_address; - - fn new( - address: ethers::types::Address, - client: std::sync::Arc, - ) -> Self { - Self::new(address, client) - } - } - }; -} - -pub(crate) use link_to_crate; - -pub type HttpMiddleware = SignerMiddleware, LocalWallet>; - -impl Client { - pub async fn new( - provider: Provider, - program_address: Address, - priv_key: String, - ) -> Result { - let wallet = LocalWallet::from_str(&priv_key)?; - let chain_id = provider.get_chainid().await?.as_u64(); - let signer = Arc::new(SignerMiddleware::new( - provider, - wallet.clone().with_chain_id(chain_id), - )); - let caller = T::new(program_address, signer); - Ok(Self { wallet, contract: caller }) - } -} - -pub fn random_token_id() -> U256 { - let num: u32 = ethers::core::rand::random(); - num.into() -} - -pub trait Assert { - /// Asserts that current error result corresponds to the typed abi encoded - /// error `expected_err`. - fn assert(&self, expected_err: E) -> Result<()>; -} - -impl Assert for Report { - fn assert(&self, expected_err: E) -> Result<()> { - let received_err = format!("{:#}", self); - let expected_err = expected_err.encode_hex(); - if received_err.contains(&expected_err) { - Ok(()) - } else { - bail!("Different error expected: Expected error is {expected_err}: Received error is {received_err}") - } - } -} - -#[async_trait] -pub trait ContextCall { - /// Queries the blockchain via an `eth_call` for the provided transaction. - /// - /// Wraps error with function info context. - /// - /// If executed on a non-state mutating smart contract function (i.e. - /// `view`, `pure`) then it will return the raw data from the chain. - /// - /// If executed on a mutating smart contract function, it will do a "dry - /// run" of the call and return the return type of the transaction - /// without mutating the state - async fn ctx_call(self) -> Result; -} - -#[async_trait] -impl ContextCall - for ContractCall -{ - async fn ctx_call(self) -> Result { - let function_name = &self.function.name; - self.call().await.context(format!("call {function_name}")) - } -} - -#[async_trait] -pub trait ContextSend { - /// Signs and broadcasts the provided transaction. - /// - /// Wraps error with function info context. - async fn ctx_send(self) -> Result; -} - -#[async_trait] -impl ContextSend for ContractCall { - async fn ctx_send(self) -> Result { - let function_name = &self.function.name; - self.send() - .await - .context(format!("send {function_name}"))? - .await - .context(format!("send {function_name}"))? - .context(format!("send {function_name}")) - } -} diff --git a/e2e-tests/src/erc20.rs b/e2e-tests/src/erc20.rs index ed87c43d..d586196a 100644 --- a/e2e-tests/src/erc20.rs +++ b/e2e-tests/src/erc20.rs @@ -1,13 +1,13 @@ -use ethers::prelude::*; -use eyre::Result; +use e2e::prelude::*; -use crate::context::{erc20::*, *}; +use crate::abi::erc20::*; -#[tokio::test] -async fn mint() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn mint(alice: User) -> Result<()> { + let erc20 = &alice.deploys::().await?; // TODO: have a nicer support for custom constructors let _ = alice + .uses(erc20) .constructor( "MyErc20".to_string(), "MRC".to_string(), @@ -19,14 +19,14 @@ async fn mint() -> Result<()> { let one = U256::from(1); let initial_balance = - alice.balance_of(alice.wallet.address()).ctx_call().await?; - let initial_supply = alice.total_supply().ctx_call().await?; + alice.uses(erc20).balance_of(alice.address()).ctx_call().await?; + let initial_supply = alice.uses(erc20).total_supply().ctx_call().await?; - let _ = alice.mint(alice.wallet.address(), one).ctx_send().await?; + let _ = alice.uses(erc20).mint(alice.address(), one).ctx_send().await?; let new_balance = - alice.balance_of(alice.wallet.address()).ctx_call().await?; - let new_supply = alice.total_supply().ctx_call().await?; + alice.uses(erc20).balance_of(alice.address()).ctx_call().await?; + let new_supply = alice.uses(erc20).total_supply().ctx_call().await?; assert_eq!(initial_balance + one, new_balance); assert_eq!(initial_supply + one, new_supply); diff --git a/e2e-tests/src/erc721.rs b/e2e-tests/src/erc721.rs index 674c8828..95f6d6d5 100644 --- a/e2e-tests/src/erc721.rs +++ b/e2e-tests/src/erc721.rs @@ -1,89 +1,103 @@ -use ethers::prelude::*; -use eyre::Result; +use e2e::prelude::*; -use crate::context::{erc721::*, *}; +use crate::abi::erc721::*; -#[tokio::test] -async fn mint() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn mint(alice: User) -> Result<()> { + let erc721 = &alice.deploys::().await?; let token_id = random_token_id(); - let _ = alice.mint(alice.wallet.address(), token_id).ctx_send().await?; - let owner = alice.owner_of(token_id).ctx_call().await?; - assert_eq!(owner, alice.wallet.address()); + let _ = + alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; + let owner = alice.uses(erc721).owner_of(token_id).ctx_call().await?; + assert_eq!(owner, alice.address()); - let balance = alice.balance_of(alice.wallet.address()).ctx_call().await?; + let balance = + alice.uses(erc721).balance_of(alice.address()).ctx_call().await?; assert!(balance >= U256::one()); Ok(()) } -#[tokio::test] -async fn error_when_reusing_token_id() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn error_when_reusing_token_id(alice: User) -> Result<()> { + let erc721 = &alice.deploys::().await?; let token_id = random_token_id(); - let _ = alice.mint(alice.wallet.address(), token_id).ctx_send().await?; + let _ = + alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; let err = alice - .mint(alice.wallet.address(), token_id) + .uses(erc721) + .mint(alice.address(), token_id) .ctx_send() .await .expect_err("should not mint a token id twice"); err.assert(ERC721InvalidSender { sender: Address::zero() }) } -#[tokio::test] -async fn transfer() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn transfer(alice: User, bob: User) -> Result<()> { + let erc721 = &alice.deploys::().await?; let token_id = random_token_id(); - let _ = alice.mint(alice.wallet.address(), token_id).ctx_send().await?; + let _ = + alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; let _ = alice - .transfer_from(alice.wallet.address(), bob.wallet.address(), token_id) + .uses(erc721) + .transfer_from(alice.address(), bob.address(), token_id) .ctx_send() .await?; - let owner = bob.owner_of(token_id).ctx_call().await?; - assert_eq!(owner, bob.wallet.address()); + let owner = bob.uses(erc721).owner_of(token_id).ctx_call().await?; + assert_eq!(owner, bob.address()); Ok(()) } -#[tokio::test] -async fn error_when_transfer_nonexistent_token() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn error_when_transfer_nonexistent_token( + alice: User, + bob: User, +) -> Result<()> { + let erc721 = &alice.deploys::().await?; let token_id = random_token_id(); let err = alice - .transfer_from(alice.wallet.address(), bob.wallet.address(), token_id) + .uses(erc721) + .transfer_from(alice.address(), bob.address(), token_id) .ctx_send() .await .expect_err("should not transfer a non existent token"); err.assert(ERC721NonexistentToken { token_id }) } -#[tokio::test] -async fn approve_token_transfer() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn approve_token_transfer(alice: User, bob: User) -> Result<()> { + let erc721 = &alice.deploys::().await?; let token_id = random_token_id(); - let _ = alice.mint(alice.wallet.address(), token_id).ctx_send().await?; - let _ = alice.approve(bob.wallet.address(), token_id).ctx_send().await?; + let _ = + alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; + let _ = + alice.uses(erc721).approve(bob.address(), token_id).ctx_send().await?; let _ = bob - .transfer_from(alice.wallet.address(), bob.wallet.address(), token_id) + .uses(erc721) + .transfer_from(alice.address(), bob.address(), token_id) .ctx_send() .await?; - let owner = bob.owner_of(token_id).ctx_call().await?; - assert_eq!(owner, bob.wallet.address()); + let owner = bob.uses(erc721).owner_of(token_id).ctx_call().await?; + assert_eq!(owner, bob.address()); Ok(()) } -#[tokio::test] -async fn error_when_transfer_unapproved_token() -> Result<()> { - let E2EContext { alice, bob } = E2EContext::::new().await?; +#[e2e::test] +async fn error_when_transfer_unapproved_token( + alice: User, + bob: User, +) -> Result<()> { + let erc721 = &alice.deploys::().await?; let token_id = random_token_id(); - let _ = alice.mint(alice.wallet.address(), token_id).ctx_send().await?; + let _ = + alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; let err = bob - .transfer_from(alice.wallet.address(), bob.wallet.address(), token_id) + .uses(erc721) + .transfer_from(alice.address(), bob.address(), token_id) .ctx_send() .await .expect_err("should not transfer unapproved token"); - err.assert(ERC721InsufficientApproval { - operator: bob.wallet.address(), - token_id, - }) + err.assert(ERC721InsufficientApproval { operator: bob.address(), token_id }) } // TODO: add more tests for erc721 diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 0be245e6..3b04cbc0 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -1,3 +1,3 @@ -pub mod context; +pub mod abi; mod erc20; mod erc721; diff --git a/e2e-tests/test.sh b/e2e-tests/test.sh index e8fc3da0..8aae3cd0 100755 --- a/e2e-tests/test.sh +++ b/e2e-tests/test.sh @@ -1,71 +1,12 @@ #!/bin/bash set -e -# make sure we will be running script from the project root. -mydir=$(dirname "$0") -cd "$mydir" || exit +MYDIR=$(realpath "$(dirname "$0")") +cd "$MYDIR" cd .. -# Deploy contract by rust crate name. -# Sets $DEPLOYMENT_ADDRESS environment variable after successful deployment. -deploy_contract () { - local CONTRACT_CRATE_NAME=$1 - local CONTRACT_BIN_NAME="${CONTRACT_CRATE_NAME//-/_}.wasm" - local PRIVATE_KEY=$ALICE_PRIV_KEY - local RPC_URL=$RPC_URL - - echo "Deploying contract $CONTRACT_CRATE_NAME." - - DEPLOY_OUTPUT=$(cargo stylus deploy --wasm-file-path ./target/wasm32-unknown-unknown/release/"$CONTRACT_BIN_NAME" -e "$RPC_URL" --private-key "$PRIVATE_KEY" --nightly) || exit $? - - # extract compressed wasm binary size - # NOTE: optimistically relying on the 'Compressed WASM size to be deployed' string in output - WASM_BIN_SIZE="$(echo "$DEPLOY_OUTPUT" | grep 'Compressed WASM size to be deployed' | grep -oE "[0-9]*\.[0-9]* KB")" - - if [[ -z "$WASM_BIN_SIZE" ]] - then - echo "Contract $CONTRACT_CRATE_NAME successfully deployed to the stylus environment ($RPC_URL)." - else - echo "Contract $CONTRACT_CRATE_NAME successfully deployed to the stylus environment ($RPC_URL). Wasm binary size is $WASM_BIN_SIZE" - fi - - # extract randomly created contract deployment address - # NOTE: optimistically relying on the 'Deploying program to address' string in output - DEPLOYMENT_ADDRESS="$(echo "$DEPLOY_OUTPUT" | grep 'Deploying program to address' | grep -oE "(0x)?[0-9a-fA-F]{40}")" - - if [[ -z "$DEPLOYMENT_ADDRESS" ]] - then - echo "Error: Couldn't retrieve deployment address for a contract $CONTRACT_CRATE_NAME." - exit 1 - fi - - DEPLOYMENT_ADDRESS_ENV_VAR_NAME="$(echo "$CRATE_NAME" | tr '-' '_' | tr '[:lower:]' '[:upper:]')_DEPLOYMENT_ADDRESS" - - # export dynamically created variable - set -a - printf -v "$DEPLOYMENT_ADDRESS_ENV_VAR_NAME" "%s" "$DEPLOYMENT_ADDRESS" - set +a -} - -# Retrieve all alphanumeric contract's crate names in `./examples` directory. -get_example_crate_names () { - # shellcheck disable=SC2038 - # NOTE: optimistically relying on the 'name = ' string at Cargo.toml file - find ./examples -type f -name "Cargo.toml" | xargs grep 'name = ' | grep -oE '".*"' | tr -d "'\"" -} - -export ALICE_PRIV_KEY=${ALICE_PRIV_KEY:-0x5744b91fe94e38f7cde31b0cc83e7fa1f45e31c053d015b9fb8c9ab3298f8a2d} -export BOB_PRIV_KEY=${BOB_PRIV_KEY:-0xa038232e463efa8ad57de6f88cd3c68ed64d1981daff2dcc015bce7eaf53db9d} -export RPC_URL=${RPC_URL:-http://localhost:8547} NIGHTLY_TOOLCHAIN=${NIGHTLY_TOOLCHAIN:-nightly} cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort -# TODO: deploy contracts asynchronously -for CRATE_NAME in $(get_example_crate_names) -do - deploy_contract "$CRATE_NAME" -done - -# TODO: run tests in parallel when concurrency scope will be per test/contract -RUST_TEST_THREADS=1 cargo +stable test -p e2e-tests +cargo +stable test -p e2e-tests diff --git a/lib/e2e-proc/Cargo.toml b/lib/e2e-proc/Cargo.toml new file mode 100644 index 00000000..f58949b0 --- /dev/null +++ b/lib/e2e-proc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "e2e-proc" +description = "E2E Testing Procedural Macros" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +keywords.workspace = true +repository.workspace = true + +[dependencies] +proc-macro2 = "1.0.79" +quote = "1.0.35" +syn = { version = "2.0.58", features = ["full"] } + +[lib] +proc-macro = true diff --git a/lib/e2e-proc/README.md b/lib/e2e-proc/README.md new file mode 100644 index 00000000..503d447c --- /dev/null +++ b/lib/e2e-proc/README.md @@ -0,0 +1,5 @@ +# E2E Procedural Macros + +This crate contains procedural macros used in [e2e]. + +[e2e]: ../e2e/README.md diff --git a/lib/e2e-proc/src/lib.rs b/lib/e2e-proc/src/lib.rs new file mode 100644 index 00000000..fe8ba7c9 --- /dev/null +++ b/lib/e2e-proc/src/lib.rs @@ -0,0 +1,29 @@ +use proc_macro::TokenStream; + +mod test; + +/// Defines an end-to-end stylus contract test that provides test user's +/// injection from arguments. +/// +/// # Examples +/// +/// ```rust,ignore +/// #[e2e::test] +/// async fn mint(alice: User) -> Result<()> { +/// let erc721 = &alice.deploys::().await?; +/// let token_id = random_token_id(); +/// let _ = +/// alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; +/// let owner = alice.uses(erc721).owner_of(token_id).ctx_call().await?; +/// assert_eq!(owner, alice.address()); +/// +/// let balance = +/// alice.uses(erc721).balance_of(alice.address()).ctx_call().await?; +/// assert!(balance >= U256::one()); +/// Ok(()) +/// } +/// ``` +#[proc_macro_attribute] +pub fn test(attr: TokenStream, input: TokenStream) -> TokenStream { + test::test(attr, input) +} diff --git a/lib/e2e-proc/src/test.rs b/lib/e2e-proc/src/test.rs new file mode 100644 index 00000000..ba0043d1 --- /dev/null +++ b/lib/e2e-proc/src/test.rs @@ -0,0 +1,47 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, FnArg}; + +/// Shorthand to print nice errors. +macro_rules! error { + ($tokens:expr, $($msg:expr),+ $(,)?) => {{ + let error = syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+)); + return error.to_compile_error().into(); + }}; + (@ $tokens:expr, $($msg:expr),+ $(,)?) => {{ + return Err(syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+))) + }}; +} + +/// Defines an end-to-end test that injects test users through parameters. +/// +/// For more information see [`crate::test`]. +pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream { + let item_fn = parse_macro_input!(input as syn::ItemFn); + let attrs = &item_fn.attrs; + let sig = &item_fn.sig; + let fn_name = &sig.ident; + let fn_return_type = &sig.output; + let fn_stmts = &item_fn.block.stmts; + let fn_args = &sig.inputs; + + let user_declarations = fn_args.into_iter().map(|arg| { + let FnArg::Typed(arg) = arg else { + error!(arg, "unexpected receiver argument in test signature"); + }; + let user_arg_binding = &arg.pat; + let user_ty = &arg.ty; + quote! { + let #user_arg_binding = #user_ty::new().await?; + } + }); + quote! { + #( #attrs )* + #[tokio::test] + async fn #fn_name() #fn_return_type { + #( #user_declarations )* + #( #fn_stmts )* + } + } + .into() +} diff --git a/lib/e2e/Cargo.toml b/lib/e2e/Cargo.toml new file mode 100644 index 00000000..de4063a9 --- /dev/null +++ b/lib/e2e/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "e2e" +description = "End-to-end Testing for Stylus" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +keywords.workspace = true +repository.workspace = true + +[dependencies] +tokio = { version = "1.12.0", features = ["full", "process"] } +ethers = "2.0" +eyre = "0.6.8" +async-trait = "0.1.80" +regex = "1.10.4" +once_cell = "1.19.0" +e2e-proc = { path = "../e2e-proc" } diff --git a/lib/e2e/README.md b/lib/e2e/README.md new file mode 100644 index 00000000..47f22397 --- /dev/null +++ b/lib/e2e/README.md @@ -0,0 +1,65 @@ +# End-to-end Testing for Stylus + +This end-to-end testing crate allows to create users, deploy contracts and +test all necessary scenarios you will probably need. This crate coupled with +[nitro test node](https://github.com/OffchainLabs/nitro-testnode) developed +by Offchain Labs and requires it to be installed to perform integration testing. + +## Usage + +Abi declaration: + +```rust +use e2e::prelude::*; + +abigen!( + Erc721Token, + r#"[ + function ownerOf(uint256 token_id) external view returns (address) + function transferFrom(address from, address to, uint256 token_id) external + function mint(address to, uint256 token_id) external + + error ERC721InvalidOwner(address owner) + error ERC721NonexistentToken(uint256 tokenId) + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner) + ]"# +); + +pub type Erc721 = Erc721Token; +link_to_crate!(Erc721, "erc721-example"); +``` + +Test case example: + +```rust +#[e2e::test] +async fn transfer(alice: User, bob: User) -> Result<()> { + let erc721 = &alice.deploys::().await?; + let token_id = random_token_id(); + let _ = + alice.uses(erc721).mint(alice.address(), token_id).ctx_send().await?; + let _ = alice + .uses(erc721) + .transfer_from(alice.address(), bob.address(), token_id) + .ctx_send() + .await?; + let owner = bob.uses(erc721).owner_of(token_id).ctx_call().await?; + assert_eq!(owner, bob.address()); + Ok(()) +} +``` + +### Notice + +[code of conduct]: ../../CODE_OF_CONDUCT.md + +[contribution guidelines]: ../../CONTRIBUTING.md + +## Security + +> [!WARNING] +> This project is still in a very early and experimental phase. It has never +> been audited nor thoroughly reviewed for security vulnerabilities. Do not use +> in production. + +Refer to our [Security Policy](../../SECURITY.md) for more details. diff --git a/lib/e2e/src/assert.rs b/lib/e2e/src/assert.rs new file mode 100644 index 00000000..a83a3957 --- /dev/null +++ b/lib/e2e/src/assert.rs @@ -0,0 +1,21 @@ +use eyre::{bail, Report}; + +use crate::prelude::abi::AbiEncode; + +pub trait Assert { + /// Asserts that current error result corresponds to the typed abi encoded + /// error `expected_err`. + fn assert(&self, expected_err: E) -> eyre::Result<()>; +} + +impl Assert for Report { + fn assert(&self, expected_err: E) -> eyre::Result<()> { + let received_err = format!("{:#}", self); + let expected_err = expected_err.encode_hex(); + if received_err.contains(&expected_err) { + Ok(()) + } else { + bail!("Different error expected: Expected error is {expected_err}: Received error is {received_err}") + } + } +} diff --git a/lib/e2e/src/context.rs b/lib/e2e/src/context.rs new file mode 100644 index 00000000..73a4edda --- /dev/null +++ b/lib/e2e/src/context.rs @@ -0,0 +1,23 @@ +use std::marker::PhantomData; + +use ethers::addressbook::Address; + +use crate::contract::Contract; + +/// End-to-end testing context that allows to act on behalf of any user that +/// [`crate::user::User::uses`] it. +pub struct E2EContext { + address: Address, + phantom_data: PhantomData, +} + +impl E2EContext { + pub(crate) fn new(address: Address) -> E2EContext { + Self { address, phantom_data: PhantomData } + } + + /// Retrieve address of the contract deployed + pub fn address(&self) -> Address { + self.address + } +} diff --git a/lib/e2e/src/context_decorator.rs b/lib/e2e/src/context_decorator.rs new file mode 100644 index 00000000..c149e42b --- /dev/null +++ b/lib/e2e/src/context_decorator.rs @@ -0,0 +1,60 @@ +use async_trait::async_trait; +use eyre::{bail, ContextCompat, WrapErr}; + +use crate::{ + prelude::{abi::Detokenize, ContractCall, TransactionReceipt, U64}, + HttpMiddleware, +}; + +#[async_trait] +pub trait ContextCall { + /// Queries the blockchain via an `eth_call` for the provided transaction. + /// + /// Wraps error with function info context. + /// + /// If executed on a non-state mutating smart contract function (i.e. + /// `view`, `pure`) then it will return the raw data from the chain. + /// + /// If executed on a mutating smart contract function, it will do a "dry + /// run" of the call and return the return type of the transaction + /// without mutating the state + async fn ctx_call(self) -> eyre::Result; +} + +#[async_trait] +impl ContextCall + for ContractCall +{ + async fn ctx_call(self) -> eyre::Result { + let function_name = &self.function.name; + self.call().await.context(format!("call {function_name}")) + } +} + +#[async_trait] +pub trait ContextSend { + /// Signs and broadcasts the provided transaction. + /// + /// Wraps error with function info context. + async fn ctx_send(self) -> eyre::Result; +} + +#[async_trait] +impl ContextSend for ContractCall { + async fn ctx_send(self) -> eyre::Result { + let function_name = &self.function.name; + let tx = self + .send() + .await + .context(format!("send {function_name}"))? + .await + .context(format!("send {function_name}"))? + .context(format!("send {function_name}"))?; + match tx.status { + Some(status) if status == U64::zero() => { + bail!("send {function_name}: transaction status is not success") + } + _ => Ok(tx), + } + } +} diff --git a/lib/e2e/src/contract.rs b/lib/e2e/src/contract.rs new file mode 100644 index 00000000..98ce75d1 --- /dev/null +++ b/lib/e2e/src/contract.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use ethers::prelude::*; + +use crate::HttpMiddleware; + +/// Abstraction for the deployed contract. +pub trait Contract { + /// Crate name of the contract. + /// + /// e.g can be `erc721-example`. + const CRATE_NAME: &'static str; + + /// Abstracts token creation function. + /// + /// e.g. `Self::new(address, client)`. + fn new(address: Address, client: Arc) -> Self; +} + +#[macro_export] +/// Link `abigen!` contract to the crate name. +/// +/// # Example +/// ``` +/// use e2e::prelude::*; +/// +/// abigen!( +/// Erc20Token, +/// r#"[ +/// function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) +/// function mint(address account, uint256 amount) external +/// +/// error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed) +/// ]"# +/// ); +/// +/// pub type Erc20 = Erc20Token; +/// link_to_crate!(Erc20, "erc20-example"); +/// ``` +macro_rules! link_to_crate { + ($token_type:ty, $program_address:literal) => { + impl $crate::prelude::Contract for $token_type { + const CRATE_NAME: &'static str = $program_address; + + fn new( + address: ethers::types::Address, + client: std::sync::Arc, + ) -> Self { + Self::new(address, client) + } + } + }; +} diff --git a/lib/e2e/src/lib.rs b/lib/e2e/src/lib.rs new file mode 100644 index 00000000..914f1660 --- /dev/null +++ b/lib/e2e/src/lib.rs @@ -0,0 +1,16 @@ +use ethers::prelude::*; +mod assert; +pub mod context; +mod context_decorator; +mod contract; +pub mod prelude; +pub mod user; + +pub use e2e_proc::test; + +pub fn random_token_id() -> U256 { + let num: u32 = rand::random(); + num.into() +} + +pub type HttpMiddleware = SignerMiddleware, LocalWallet>; diff --git a/lib/e2e/src/prelude.rs b/lib/e2e/src/prelude.rs new file mode 100644 index 00000000..126b7f91 --- /dev/null +++ b/lib/e2e/src/prelude.rs @@ -0,0 +1,13 @@ +//! Common imports for `grip` tests. +pub use ethers::prelude::*; +pub use eyre::Result; + +pub use crate::{ + assert::Assert, + context::E2EContext, + context_decorator::{ContextCall, ContextSend}, + contract::Contract, + link_to_crate, random_token_id, + user::User, + HttpMiddleware, +}; diff --git a/lib/e2e/src/user.rs b/lib/e2e/src/user.rs new file mode 100644 index 00000000..9fa5db68 --- /dev/null +++ b/lib/e2e/src/user.rs @@ -0,0 +1,185 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, +}; + +use ethers::{ + core::{k256::ecdsa::SigningKey, rand::thread_rng}, + middleware::SignerMiddleware, + prelude::*, + utils::hex::hex, +}; +use eyre::{bail, eyre, Context, ContextCompat, Report, Result}; +use once_cell::sync::Lazy; +use regex::Regex; +use tokio::sync::Mutex; + +use crate::{context::E2EContext, contract::Contract}; + +const RPC_URL: &str = "RPC_URL"; +const TEST_NITRO_NODE_PATH: &str = "TEST_NITRO_NODE_PATH"; + +fn load_env_var(var_name: &str) -> eyre::Result { + std::env::var(var_name) + .with_context(|| format!("failed to load {} env var", var_name)) +} + +/// Type that corresponds to a test user. +pub struct User { + wallet: LocalWallet, + provider: Provider, + private_key: String, +} + +/// Singleton user factory. +/// Since after wallet generation user get funded inside nitro test node from a +/// single "god" wallet. We should have synchronized user creation (otherwise +/// nonce will be too low) +static SYNC_USER_FACTORY: Lazy> = + Lazy::new(|| Mutex::new(UserFactory)); + +impl User { + /// Create new instance of user. + pub async fn new() -> Result { + SYNC_USER_FACTORY.lock().await.create().await + } + + /// Accept deployed [`Self::deploys`] contract as an argument. + /// Simulates user making call to a specific contract function. + /// + /// # Arguments + /// * `contract_ctx` - Context of the contract deployed + pub fn uses(&self, contract_ctx: &E2EContext) -> T { + let signer = Arc::new(SignerMiddleware::new( + self.provider.clone(), + self.wallet.clone(), + )); + T::new(contract_ctx.address(), signer) + } + + /// Allows to user deploy specific contract. + /// [`T`] is an abi association with crate. + /// + /// # Examples + /// ```rust, ignore + /// let erc20 = &alice.deploys::().await?; + /// ``` + pub async fn deploys(&self) -> Result> { + let rpc_url = load_env_var(RPC_URL)?; + let wasm_bin_name = T::CRATE_NAME.replace('-', "_") + ".wasm"; + + let abs_wasm_bin_path = get_target_dir()? + .join("wasm32-unknown-unknown/release") + .join(wasm_bin_name); + let output = tokio::process::Command::new("cargo") + .arg("stylus") + .arg("deploy") + .arg("--wasm-file-path") + .arg(abs_wasm_bin_path) + .arg("-e") + .arg(rpc_url) + .arg("--private-key") + .arg(&self.private_key) + .arg("--nightly") + .output() + .await?; + + // NOTE: impossible to use `output.status` since it will be error + // returned for a duplicated deployment of the contract + let address = + extract_deployment_address(&output.stdout).map_err(|err| { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + eyre!( + "deploy of the contract wasn't successful - crate name is {}:\n\ + parsing error is {err}:\n\ + stdout is {stdout}:\n\ + stderr is {stderr}", + T::CRATE_NAME, + ) + })?; + Ok(E2EContext::new(address)) + } + + /// Retrieve wallet address of the user. + pub fn address(&self) -> Address { + self.wallet.address() + } +} + +fn extract_deployment_address(output: &[u8]) -> Result
{ + let deployment_address_line = std::str::from_utf8(output)? + .lines() + .find(|l| l.contains("Deploying program to address")) + .context("find deployment address line in the cargo-stylus output")?; + + let re = Regex::new(r"(0x)?[0-9a-fA-F]{40}").unwrap(); + let address = re + .find(deployment_address_line) + .context("extract deployment address from the cargo-stylus output")?; + Ok(address.as_str().parse()?) +} + +struct UserFactory; + +impl UserFactory { + /// Create new user and fund his wallet via test nitro node access. + async fn create(&self) -> Result { + let rpc_url = load_env_var(RPC_URL)?; + let test_nitro_node_path = load_env_var(TEST_NITRO_NODE_PATH)?; + let test_nitro_node_script = + Path::new(&test_nitro_node_path).join("test-node.bash"); + + let provider = Provider::::try_from(rpc_url)?; + + let private_key = + hex::encode(SigningKey::random(&mut thread_rng()).to_bytes()); + let chain_id = get_chain_id(&provider).await?; + let wallet = + LocalWallet::from_str(&private_key)?.with_chain_id(chain_id); + let hex_address = hex::encode(wallet.address().as_bytes()); + + // ./test-node.bash script send-l2 --to + // address_0x01fA6bf4Ee48B6C95900BCcf9BEA172EF5DBd478 --ethamount 10000 + let output = tokio::process::Command::new(test_nitro_node_script) + .arg("script") + .arg("send-l2") + .arg("--to") + .arg(format!("address_0x{}", hex_address)) + .arg("--ethamount") + .arg("10") + .output() + .await?; + if output.status.success() { + let user = User { wallet, provider, private_key }; + Ok(user) + } else { + let err = String::from_utf8_lossy(&output.stderr); + bail!( + "user wallet wasn't filled - address is {hex_address}:\n{err}" + ) + } + } +} + +fn get_target_dir() -> Result { + let target_dir = load_env_var("TARGET_DIR")?; + Ok(Path::new(&target_dir).to_path_buf()) +} + +async fn get_chain_id(provider: &Provider) -> Result { + static CHAIN_ID: tokio::sync::OnceCell = + tokio::sync::OnceCell::const_new(); + + CHAIN_ID + .get_or_try_init(|| async { + Ok(provider + .get_chainid() + .await + .context("Trying to get configured chain id. Try to setup nitro test node first")? + .as_u64()) + }) + .await + .cloned() +}