diff --git a/.cargo/audit.toml b/.cargo/audit.toml deleted file mode 100644 index c73dfd47..00000000 --- a/.cargo/audit.toml +++ /dev/null @@ -1,4 +0,0 @@ -[advisories] -ignore = [ - "RUSTSEC-2021-0127", # serde_cbor dependency through the criterion dev-dependency -] diff --git a/.github/scripts/install_test_binaries.sh b/.github/scripts/install_test_binaries.sh new file mode 100755 index 00000000..392f40e8 --- /dev/null +++ b/.github/scripts/install_test_binaries.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Installs Solc and Geth binaries +# Note: intended for use only with CI (x86_64 Ubuntu, MacOS or Windows) +set -e + +GETH_BUILD=${GETH_BUILD:-"1.11.2-73b01f40"} + +BIN_DIR=${BIN_DIR:-"$HOME/bin"} + +PLATFORM="$(uname -s | awk '{print tolower($0)}')" +if [ "$PLATFORM" != "linux" ] && [ "$PLATFORM" != "darwin" ]; then + EXT=".exe" +fi + +main() { + mkdir -p "$BIN_DIR" + cd "$BIN_DIR" + export PATH="$BIN_DIR:$PATH" + if [ "$GITHUB_PATH" ]; then + echo "$BIN_DIR" >> "$GITHUB_PATH" + fi + + install_geth & + g=$! + install_solc & + wait $g $! + + echo "" + echo "Installed Geth:" + geth version + echo "" + echo "Installed Solc:" + solc --version +} + +# Installs geth from https://geth.ethereum.org/downloads +install_geth() { + case "$PLATFORM" in + linux|darwin) + name="geth-$PLATFORM-amd64-$GETH_BUILD" + curl -s "https://gethstore.blob.core.windows.net/builds/$name.tar.gz" | tar -xzf - + mv -f "$name/geth" ./ + rm -rf "$name" + chmod +x geth + ;; + *) + name="geth-windows-amd64-$GETH_BUILD" + zip="$name.zip" + curl -so "$zip" "https://gethstore.blob.core.windows.net/builds/$zip" + unzip "$zip" + mv -f "$name/geth.exe" ./ + rm -rf "$name" "$zip" + ;; + esac +} + +# Installs solc from https://binaries.soliditylang.org (https://github.com/ethereum/solc-bin) +install_solc() { + bins_url="https://binaries.soliditylang.org" + case "$PLATFORM" in + linux) bins_url+="/linux-amd64";; + darwin) bins_url+="/macosx-amd64";; + *) bins_url+="/windows-amd64";; + esac + + list=$(curl -s "$bins_url/list.json") + # use latest version + if [ -z "$SOLC_VERSION" ]; then + SOLC_VERSION="$(echo "$list" | jq -r ".latestRelease")" + fi + bin=$(echo "$list" | jq -r ".releases[\"$SOLC_VERSION\"]") + + if [ "$bin" = "null" ]; then + echo "Invalid Solc version: $SOLC_VERSION" 1>&2 + exit 1 + fi + + # windows versions <= 0.7.1 use .zip + if [[ "$bin" = *.zip ]]; then + echo "Cannot install solc <= 0.7.1" 1>&2 + exit 1 + fi + + curl -so "$bin" "$bins_url/$bin" + mv -f "$bin" "solc$EXT" + chmod +x "solc$EXT" +} + +main diff --git a/.github/workflows/release-tag-from.js b/.github/scripts/release-tag-from.js similarity index 100% rename from .github/workflows/release-tag-from.js rename to .github/scripts/release-tag-from.js diff --git a/.github/workflows/audit-on-push.yml b/.github/workflows/audit-on-push.yml deleted file mode 100644 index 7ecf3535..00000000 --- a/.github/workflows/audit-on-push.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Security audit -on: - push: - paths: - - "**/Cargo.toml" - - "**/Cargo.lock" -jobs: - security_audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index e1b2947d..5a2cec0d 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -1,16 +1,17 @@ name: book + on: push: branches: [master] paths: - - 'book/**' - - 'book.toml' + - "book/**" + - "book.toml" pull_request: branches: [master] paths: - - 'book/**' - - 'book.toml' + - "book/**" + - "book.toml" jobs: build: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d9600bf..0a46c89f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,288 +1,180 @@ +name: CI + on: push: - branches: - - master + branches: [master] pull_request: -name: Tests - # Yeah I know it's bad practice to have API keys, this is a read-only API key # so that we do not get rate limited by Etherscan (and it's free to generate as # many as you want) env: - ETHERSCAN_API_KEY_ETHEREUM: I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB - ETHERSCAN_API_KEY_CELO: B13XSMUT6Q3Q4WZ5DNQR8RXDBA2KNTMT4M + ETHERSCAN_API_KEY: "I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB" GOERLI_PRIVATE_KEY: "fa4a1a79e869a96fcb42727f75e3232d6865a82ea675bb95de967a7fe6a773b2" - GETH_BUILD: 1.10.26-e5eb32ac + GETH_BUILD: "1.11.2-73b01f40" + SOLC_VERSION: "0.8.19" + CARGO_TERM_COLOR: always jobs: - tests: - name: ethereum tests - runs-on: ubuntu-latest + test: + name: test ${{ matrix.os }} ${{ matrix.flags }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + flags: ["--workspace", "--workspace --all-features"] + exclude: + # sha2-asm + - os: windows-latest + flags: --workspace --all-features steps: - - name: Checkout sources - uses: actions/checkout@v3 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable - name: Install Anvil uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - - name: Install Solc + - name: Install test binaries + shell: bash + run: ./.github/scripts/install_test_binaries.sh + - name: Install nextest + uses: taiki-e/install-action@nextest + - uses: Swatinem/rust-cache@v2 + - name: test + shell: bash + # skip `ethers_etherscan::it` and `ethers::live` run: | - mkdir -p "$HOME/bin" - wget -q https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux -O $HOME/bin/solc - chmod u+x "$HOME/bin/solc" - export PATH=$HOME/bin:$PATH - solc --version + cargo nextest run \ + ${{ matrix.flags }} \ + -E "!binary(~live) & !(deps(ethers-etherscan) & kind(test))" - - name: Install geth - run: | - mkdir -p "$HOME/bin" - wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-$GETH_BUILD.tar.gz - tar -xvf geth-linux-amd64-$GETH_BUILD.tar.gz - mv geth-linux-amd64-$GETH_BUILD/geth $HOME/bin/geth - chmod u+x "$HOME/bin/geth" - export PATH=$HOME/bin:$PATH - geth version - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v1 - with: - cache-on-failure: true - - name: cargo test - run: | - export PATH=$HOME/bin:$PATH - export ETHERSCAN_API_KEY=$ETHERSCAN_API_KEY_ETHEREUM - cargo test + etherscan-tests: + name: etherscan tests + runs-on: ubuntu-latest + concurrency: etherscan-tests-${{ github.head_ref || github.run_id }} + # Run after main tests are done to avoid rate limiting, + # regardless of whether they were successful + if: ${{ always() }} + needs: test + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # Don't use cargo-nextest since all the tests have to be ran sequentially + - name: live tests + run: cargo test -p ethers-etherscan --test it - feature-tests: - name: celo tests + live-tests: + name: live tests + runs-on: ubuntu-latest + concurrency: live-tests-${{ github.head_ref || github.run_id }} + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: live tests + run: cargo test -p ethers --test live --all-features + + # TODO: [#2191](https://github.com/gakonst/ethers-rs/issues/2191) + # feature-checks: + # name: feature checks + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: dtolnay/rust-toolchain@nightly + # - uses: taiki-e/install-action@cargo-hack + # - uses: Swatinem/rust-cache@v2 + # - name: cargo hack + # run: + # cargo hack check --all --feature-powerset --depth 2 -Z avoid-dev-deps --keep-going + + clippy: + name: clippy runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v3 - # TODO: can we combine these shared steps in github actions? - - name: Install Anvil - uses: foundry-rs/foundry-toolchain@v1 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly with: - version: nightly - - name: Install Solc - run: | - mkdir -p "$HOME/bin" - wget -q https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux -O $HOME/bin/solc - chmod u+x "$HOME/bin/solc" - export PATH=$HOME/bin:$PATH - solc --version - - name: Install geth - run: | - mkdir -p "$HOME/bin" - wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-$GETH_BUILD.tar.gz - tar -xvf geth-linux-amd64-$GETH_BUILD.tar.gz - mv geth-linux-amd64-$GETH_BUILD/geth $HOME/bin/geth - chmod u+x "$HOME/bin/geth" - export PATH=$HOME/bin:$PATH - geth version - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v1 - with: - cache-on-failure: true - - name: cargo test (Celo) - run: | - export PATH=$HOME/bin:$PATH - export ETHERSCAN_API_KEY=$ETHERSCAN_API_KEY_CELO - cargo test --all-features - - lint: - name: lints - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - profile: minimal - components: rustfmt, clippy - override: true - - uses: Swatinem/rust-cache@v1 - with: - cache-on-failure: true - - name: cargo fmt - run: cargo +nightly fmt --all -- --check - - name: cargo clippy - run: cargo +nightly clippy --all-features -- -D warnings + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: clippy + run: cargo clippy --workspace --tests --all-features + env: + RUSTFLAGS: "-D warnings" docs: name: docs runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v3 - - name: Install toolchain - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly - override: true - - uses: Swatinem/rust-cache@v1 - with: - cache-on-failure: true - - name: cargo doc - run: cargo doc --no-deps --all --all-features + components: rust-docs + - uses: Swatinem/rust-cache@v2 + - name: doc + run: cargo doc --workspace --all-features --no-deps --document-private-items env: - RUSTFLAGS: --cfg docsrs - RUSTDOCFLAGS: --cfg docsrs + RUSTFLAGS: "--cfg docsrs" + RUSTDOCFLAGS: "--cfg docsrs -D warnings" + + fmt: + name: fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - name: fmt --check + run: cargo fmt --all --check wasm: name: WASM runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - name: Install rust - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable target: wasm32-unknown-unknown - profile: minimal - override: true - - name: Install Anvil uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - - uses: Swatinem/rust-cache@v1 - with: - cache-on-failure: true - + - uses: Swatinem/rust-cache@v2 - name: Check - uses: actions-rs/cargo@v1 - with: - command: check - args: --target wasm32-unknown-unknown - - - name: Launch Anvil - run: - anvil --block-time 2 -m "stuff inherit faith park genre spread huge knee ecology - private marble supreme" & + run: cargo check --workspace --target wasm32-unknown-unknown + # TODO: [#2191](https://github.com/gakonst/ethers-rs/issues/2191) + # - name: Check all features + # run: cargo check --workspace --target wasm32-unknown-unknown --all-features - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - - name: Wasm-pack test firefox + - uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Run wasm example + working-directory: examples/ethers-wasm run: | - cd examples/ethers-wasm + yarn + yarn anvil & wasm-pack test --headless --firefox - - - name: Wasm-pack test chrome - run: | - cd examples/ethers-wasm wasm-pack test --headless --chrome examples: name: Examples runs-on: ubuntu-latest + concurrency: examples-${{ github.head_ref || github.run_id }} steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Install geth (for state overrides example) - run: | - mkdir -p "$HOME/bin" - wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-$GETH_BUILD.tar.gz - tar -xvf geth-linux-amd64-$GETH_BUILD.tar.gz - mv geth-linux-amd64-$GETH_BUILD/geth $HOME/bin/geth - chmod u+x "$HOME/bin/geth" - export PATH=$HOME/bin:$PATH - geth version - + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable - name: Install Anvil uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - - - name: Install Solc - run: | - mkdir -p "$HOME/bin" - wget -q https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux -O $HOME/bin/solc - chmod u+x "$HOME/bin/solc" - export PATH=$HOME/bin:$PATH - solc --version - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v1 - with: - cache-on-failure: true + - name: Install test binaries + run: ./.github/scripts/install_test_binaries.sh + - uses: Swatinem/rust-cache@v2 - name: Build and run all examples - run: | - export PATH=$HOME/bin:$PATH - chmod +x ./scripts/examples.sh - ./scripts/examples.sh - - windows-build: - runs-on: windows-latest - name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) - env: - CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }} - strategy: - fail-fast: false - matrix: - target: - [ - i686-pc-windows-gnu, - i686-pc-windows-msvc, - x86_64-pc-windows-gnu, - x86_64-pc-windows-msvc, - ] - cfg_release_channel: [nightly] - - steps: - - name: checkout - uses: actions/checkout@v3 - - # Run build - - name: Update Rustup - run: | - # disable download progress bar - $ProgressPreference = "SilentlyContinue" - rustup update - rustup target add ${{ matrix.target }} - shell: powershell - - - name: Add mingw32 to path for i686-gnu - run: | - echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH - if: matrix.target == 'i686-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - - name: Add mingw64 to path for x86_64-gnu - run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH - if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - - name: build - run: | - rustc -Vv - cargo -V - # we test without --all-features on Windows so that sha2-asm is not activated. - cargo check - shell: cmd + run: ./scripts/examples.sh diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml new file mode 100644 index 00000000..6fbf7883 --- /dev/null +++ b/.github/workflows/deps.yml @@ -0,0 +1,33 @@ +name: deps + +on: + push: + branches: [master] + paths: [Cargo.lock] + pull_request: + branches: [master] + paths: [Cargo.lock] + +env: + RUSTFLAGS: -D warnings + CARGO_TERM_COLOR: always + +concurrency: deps-${{ github.head_ref || github.run_id }} + +jobs: + deny: + name: deny (${{ matrix.checks }}) + runs-on: ubuntu-latest + strategy: + matrix: + checks: + - advisories + - bans licenses sources + + continue-on-error: ${{ matrix.checks == 'advisories' }} + + steps: + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check ${{ matrix.checks }} diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 5ae5a53d..1dbe6779 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -1,4 +1,4 @@ -name: Release +name: release on: schedule: @@ -72,7 +72,7 @@ jobs: id: changelog run: | current_version=$(git tag --contains HEAD -l "v*" | head -1) - from_version=$(node .github/workflows/release-tag-from.js $current_version $RELEASE_TYPE) + from_version=$(node .github/scripts/release-tag-from.js $current_version $RELEASE_TYPE) echo from $from_version to $current_version echo "::set-output name=release_version::$(echo $current_version)" diff --git a/Cargo.lock b/Cargo.lock index 06d397c3..d5f9b472 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,7 +1475,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "serial_test", "thiserror", "tokio", "tracing", @@ -1550,7 +1549,6 @@ dependencies = [ "thiserror", "tokio", "tracing", - "tracing-subscriber", "trezor-client", "yubihsm", ] @@ -2635,16 +2633,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -2810,12 +2798,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.11.1" @@ -3841,9 +3823,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "0.10.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2" +checksum = "538c30747ae860d6fb88330addbbd3e0ddbe46d662d032855596d8a8ca260611" dependencies = [ "dashmap", "futures", @@ -3855,9 +3837,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "0.10.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3" +checksum = "079a83df15f85d89a68d64ae1238f142f172b1fa915d0d76b26a7cba1b659a69" dependencies = [ "proc-macro2", "quote", @@ -4432,7 +4414,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", - "valuable", ] [[package]] @@ -4445,17 +4426,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.16" @@ -4463,15 +4433,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ "matchers", - "nu-ansi-term", "once_cell", "regex", "sharded-slab", - "smallvec", "thread_local", "tracing", "tracing-core", - "tracing-log", ] [[package]] @@ -4616,12 +4583,6 @@ dependencies = [ "serde", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index abfb4738..76b03627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,26 +107,21 @@ ethers-contract = { version = "^1.0.0", default-features = false, path = "./ethe "abigen", "eip712", ] } -ethers-providers = { version = "^1.0.0", default-features = false, path = "./ethers-providers", features = [ - "ws", -] } -tempfile = "3.4.0" - -[target.'cfg(target_family = "unix")'.dev-dependencies] ethers-providers = { version = "^1.0.0", default-features = false, path = "./ethers-providers", features = [ "ws", "ipc", ] } + +bytes = "1.4.0" eyre = "0.6" +hex = "0.4.3" rand = "0.8.5" serde = { version = "1.0.124", features = ["derive"] } serde_json = "1.0.64" +tempfile = "3.3.0" tokio = { version = "1.18", features = ["macros", "rt-multi-thread"] } -hex = "0.4.3" -bytes = "1.4.0" # profile for the wasm example [profile.release.package.ethers-wasm] # Tell `rustc` to optimize for small code size. opt-level = "s" - diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..b81bbbba --- /dev/null +++ b/deny.toml @@ -0,0 +1,89 @@ +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +vulnerability = "deny" +unmaintained = "warn" +unsound = "warn" +yanked = "warn" +notice = "warn" + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +highlight = "all" +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [] + +[licenses] +unlicensed = "deny" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "OpenSSL", + "Unlicense", +] + +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # CC0 is a permissive license but somewhat unclear status for source code + # so we prefer to not have dependencies using it + # https://tldrlegal.com/license/creative-commons-cc0-1.0-universal + { allow = ["CC0-1.0"], name = "secp256k1" }, + { allow = ["CC0-1.0"], name = "secp256k1-sys" }, + { allow = ["CC0-1.0"], name = "tiny-keccak" }, + { allow = ["CC0-1.0"], name = "more-asserts" }, + + # TODO: ethers transitive deps + { allow = ["GPL-3.0"], name = "fastrlp" }, + { allow = ["GPL-3.0"], name = "fastrlp-derive" }, +] +#copyleft = "deny" + +# See note in unicode-ident's readme! +[[licenses.clarify]] +name = "unicode-ident" +version = "*" +expression = "(MIT OR Apache-2.0) AND Unicode-DFS-2016" +license-files = [{ path = "LICENSE-UNICODE", hash = 0x3fb01745 }] +[[licenses.clarify]] +name = "ring" +version = "*" +expression = "OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "allow" diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 97669bc7..d884690d 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -45,9 +45,11 @@ tokio = { version = "1.18", default-features = false, features = ["macros"] } default = ["abigen"] eip712 = ["ethers-derive-eip712", "ethers-core/eip712"] -abigen = ["ethers-contract-abigen/reqwest", "ethers-contract-derive"] + abigen-offline = ["ethers-contract-abigen", "ethers-contract-derive"] -celo = ["legacy", "ethers-core/celo", "ethers-core/celo", "ethers-providers/celo"] +abigen = ["abigen-offline", "ethers-contract-abigen/online"] + +celo = ["legacy", "ethers-core/celo", "ethers-providers/celo"] legacy = [] rustls = ["ethers-contract-abigen/rustls"] diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 5f4af839..4c810979 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -34,10 +34,7 @@ regex = "1.6.0" toml = "0.5.9" reqwest = { version = "0.11.3", default-features = false, features = ["blocking"], optional = true } -tokio = { version = "1.0", default-features = false, features = [ - "rt-multi-thread", - "sync", -], optional = true } +tokio = { version = "1.0", default-features = false, features = ["sync"], optional = true } url = { version = "2.3.1", default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index 7076dd73..94b60053 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -230,19 +230,7 @@ pub(crate) fn event_struct_alias(event_name: &str) -> Ident { mod tests { use super::*; use crate::Abigen; - use ethers_core::abi::{EventParam, Hash, ParamType}; - use proc_macro2::Literal; - - /// Expands a 256-bit `Hash` into a literal representation that can be used with - /// quasi-quoting for code generation. We do this to avoid allocating at runtime - fn expand_hash(hash: Hash) -> TokenStream { - let bytes = hash.as_bytes().iter().copied().map(Literal::u8_unsuffixed); - let ethers_core = ethers_core_crate(); - - quote! { - #ethers_core::types::H256([#( #bytes ),*]) - } - } + use ethers_core::abi::{EventParam, ParamType}; fn test_context() -> Context { Context::from_abigen(Abigen::new("TestToken", "[]").unwrap()).unwrap() @@ -254,44 +242,30 @@ mod tests { } #[test] - #[rustfmt::skip] fn expand_transfer_filter_with_alias() { let event = Event { name: "Transfer".into(), inputs: vec![ - EventParam { - name: "from".into(), - kind: ParamType::Address, - indexed: true, - }, - EventParam { - name: "to".into(), - kind: ParamType::Address, - indexed: true, - }, - EventParam { - name: "amount".into(), - kind: ParamType::Uint(256), - indexed: false, - }, + EventParam { name: "from".into(), kind: ParamType::Address, indexed: true }, + EventParam { name: "to".into(), kind: ParamType::Address, indexed: true }, + EventParam { name: "amount".into(), kind: ParamType::Uint(256), indexed: false }, ], anonymous: false, }; let sig = "Transfer(address,address,uint256)"; let cx = test_context_with_alias(sig, "TransferEvent"); + #[rustfmt::skip] assert_quote!(cx.expand_filter(&event), { #[doc = "Gets the contract's `Transfer` event"] pub fn transfer_event_filter( &self - ) -> ::ethers_contract::builders::Event< - ::std::sync::Arc, - M, - TransferEventFilter, - > { + ) -> ::ethers_contract::builders::Event<::std::sync::Arc, M, TransferEventFilter> + { self.0.event() } }); } + #[test] fn expand_transfer_filter() { let event = Event { @@ -304,10 +278,11 @@ mod tests { anonymous: false, }; let cx = test_context(); + #[rustfmt::skip] assert_quote!(cx.expand_filter(&event), { #[doc = "Gets the contract's `Transfer` event"] pub fn transfer_filter( - &self, + &self ) -> ::ethers_contract::builders::Event<::std::sync::Arc, M, TransferFilter> { self.0.event() @@ -406,20 +381,4 @@ mod tests { struct FooAliasedFilter(pub bool, pub ::ethers_core::types::Address); }); } - - #[test] - #[rustfmt::skip] - fn expand_hash_value() { - assert_quote!( - expand_hash( - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".parse().unwrap() - ), - { - ::ethers_core::types::H256([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 - ]) - }, - ); - } } diff --git a/ethers-contract/ethers-contract-abigen/src/source/mod.rs b/ethers-contract/ethers-contract-abigen/src/source/mod.rs index 778df22c..95f05d4d 100644 --- a/ethers-contract/ethers-contract-abigen/src/source/mod.rs +++ b/ethers-contract/ethers-contract-abigen/src/source/mod.rs @@ -23,16 +23,16 @@ pub enum Source { Local(PathBuf), /// An address of a smart contract address verified at a supported blockchain explorer. - #[cfg(feature = "online")] + #[cfg(all(feature = "online", not(target_arch = "wasm32")))] Explorer(Explorer, ethers_core::types::Address), /// The package identifier of an npm package with a path to a Truffle artifact or ABI to be /// retrieved from `unpkg.io`. - #[cfg(feature = "online")] + #[cfg(all(feature = "online", not(target_arch = "wasm32")))] Npm(String), /// An ABI to be retrieved over HTTP(S). - #[cfg(feature = "online")] + #[cfg(all(feature = "online", not(target_arch = "wasm32")))] Http(url::Url), } @@ -77,12 +77,12 @@ impl Source { match source.chars().next() { Some('[' | '{') => Ok(Self::String(source.to_string())), - #[cfg(not(feature = "online"))] + #[cfg(any(not(feature = "online"), target_arch = "wasm32"))] _ => Ok(Self::local(source)?), - #[cfg(feature = "online")] + #[cfg(all(feature = "online", not(target_arch = "wasm32")))] Some('/') => Self::local(source), - #[cfg(feature = "online")] + #[cfg(all(feature = "online", not(target_arch = "wasm32")))] _ => Self::parse_online(source), } } @@ -120,16 +120,8 @@ impl Source { Self::Local(path) => Ok(fs::read_to_string(path)?), Self::String(abi) => Ok(abi.clone()), - #[cfg(feature = "online")] - _ => { - cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - Err(eyre::eyre!("Online ABI locations are currently unsupported for WASM builds.")) - } else { - self.get_online() - } - } - } + #[cfg(all(feature = "online", not(target_arch = "wasm32")))] + _ => self.get_online(), } } } @@ -148,7 +140,7 @@ mod tests { env!("CARGO_MANIFEST_DIR"), "/../tests/solidity-contracts/console.json" ); - let exp = Source::Local(Path::new(rel).canonicalize().unwrap()); + let exp = Source::Local(dunce::canonicalize(Path::new(rel)).unwrap()); assert_eq!(Source::parse(rel).unwrap(), exp); assert_eq!(Source::parse(abs).unwrap(), exp); assert_eq!(Source::parse(abs_url).unwrap(), exp); diff --git a/ethers-contract/ethers-contract-abigen/src/source/online.rs b/ethers-contract/ethers-contract-abigen/src/source/online.rs index 3c3c70d8..212aff96 100644 --- a/ethers-contract/ethers-contract-abigen/src/source/online.rs +++ b/ethers-contract/ethers-contract-abigen/src/source/online.rs @@ -67,8 +67,7 @@ impl Explorer { let chain = self.chain(); let client = match api_key { Some(api_key) => Client::new(chain, api_key), - None => Client::new_from_env(chain) - .or_else(|_| Client::builder().chain(chain).and_then(|b| b.build())), + None => Client::new_from_opt_env(chain), }?; Ok(client) } @@ -207,6 +206,11 @@ mod tests { #[test] fn get_mainnet_contract() { + // Skip if ETHERSCAN_API_KEY is not set + if std::env::var("ETHERSCAN_API_KEY").is_err() { + return + } + let source = Source::parse("mainnet:0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let abi = source.get().unwrap(); assert!(!abi.is_empty()); diff --git a/ethers-contract/ethers-contract-abigen/src/test/macros.rs b/ethers-contract/ethers-contract-abigen/src/test/macros.rs index 467f89e9..a31008af 100644 --- a/ethers-contract/ethers-contract-abigen/src/test/macros.rs +++ b/ethers-contract/ethers-contract-abigen/src/test/macros.rs @@ -5,6 +5,9 @@ /// If the expanded source does not match the quoted source. macro_rules! assert_quote { ($ex:expr, { $($t:tt)* } $(,)?) => { - assert_eq!($ex.to_string(), quote::quote! { $($t)* }.to_string()) + assert_eq!( + $ex.to_string(), + quote::quote! { $($t)* }.to_string(), + ) }; } diff --git a/ethers-contract/tests/it/common/mod.rs b/ethers-contract/tests/it/common.rs similarity index 83% rename from ethers-contract/tests/it/common/mod.rs rename to ethers-contract/tests/it/common.rs index 1d6ab901..5ef32aaa 100644 --- a/ethers-contract/tests/it/common/mod.rs +++ b/ethers-contract/tests/it/common.rs @@ -1,17 +1,9 @@ -#![cfg(not(target_arch = "wasm32"))] -#![allow(dead_code)] - -#[cfg(feature = "abigen")] -use ethers_core::types::Address; - -#[cfg(feature = "abigen")] -use ethers_contract::EthEvent; - -#[cfg(feature = "abigen")] -mod derive; - -use ethers_contract::{Contract, ContractFactory}; -use ethers_core::{abi::Abi, types::Bytes, utils::AnvilInstance}; +use ethers_contract::{Contract, ContractFactory, EthEvent}; +use ethers_core::{ + abi::Abi, + types::{Address, Bytes}, + utils::AnvilInstance, +}; use ethers_providers::{Http, Middleware, Provider}; use ethers_solc::Solc; use std::{convert::TryFrom, sync::Arc, time::Duration}; diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs index a8cc689c..89dcb926 100644 --- a/ethers-contract/tests/it/contract.rs +++ b/ethers-contract/tests/it/contract.rs @@ -1,951 +1,927 @@ -#![allow(unused)] +use crate::common::*; +use ethers_contract::{ + abigen, ContractFactory, ContractInstance, EthAbiType, EthEvent, LogMeta, Multicall, + MulticallError, MulticallVersion, +}; +use ethers_core::{ + abi::{encode, AbiEncode, Token, Tokenizable}, + types::{ + transaction::eip712::Eip712, Address, BlockId, Bytes, Filter, ValueOrArray, H160, H256, + I256, U256, + }, + utils::{keccak256, Anvil}, +}; +use ethers_derive_eip712::*; +use ethers_providers::{Http, Middleware, MiddlewareError, Provider, StreamExt}; +use ethers_signers::{LocalWallet, Signer}; +use std::{convert::TryFrom, iter::FromIterator, sync::Arc, time::Duration}; -pub use crate::common::*; -use ethers_contract::{abigen, ContractFactory, EthAbiType}; -use ethers_core::types::{Filter, ValueOrArray, H256}; +#[derive(Debug)] +pub struct NonClone { + m: M, +} -#[cfg(not(feature = "celo"))] -mod eth_tests { - use super::*; - use ethers_contract::{ - ContractInstance, EthEvent, LogMeta, Multicall, MulticallError, MulticallVersion, - }; - use ethers_core::{ - abi::{encode, AbiEncode, Detokenize, Token, Tokenizable}, - types::{transaction::eip712::Eip712, Address, BlockId, Bytes, H160, I256, U256}, - utils::{keccak256, Anvil}, - }; - use ethers_derive_eip712::*; - use ethers_providers::{ - Http, Middleware, MiddlewareError, PendingTransaction, Provider, StreamExt, - }; - use ethers_signers::{LocalWallet, Signer}; - use std::{convert::TryFrom, iter::FromIterator, sync::Arc, time::Duration}; +#[derive(Debug)] +pub struct MwErr(M::Error); - #[derive(Debug)] - pub struct NonClone { - m: M, +impl MiddlewareError for MwErr +where + M: Middleware, +{ + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { + Self(src) } - #[derive(Debug)] - pub struct MwErr(M::Error); - - impl MiddlewareError for MwErr - where - M: Middleware, - { - type Inner = M::Error; - - fn from_err(src: M::Error) -> Self { - Self(src) - } - - fn as_inner(&self) -> Option<&Self::Inner> { - Some(&self.0) - } - } - - impl std::fmt::Display for MwErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } - } - impl std::error::Error for MwErr {} - - impl Middleware for NonClone { - type Error = MwErr; - - type Provider = M::Provider; - - type Inner = M; - - fn inner(&self) -> &Self::Inner { - &self.m - } - } - - // this is not a test. It is a compile check. :) - // It exists to ensure that trait bounds on contract internal behave as - // expected. It should not be run - fn it_compiles() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - - // launch anvil - let anvil = Anvil::new().spawn(); - - let client = Provider::::try_from(anvil.endpoint()) - .unwrap() - .interval(Duration::from_millis(10u64)); - - // Works (B == M, M: Clone) - let c: ContractInstance<&Provider, Provider> = - ContractInstance::new(H160::default(), abi.clone(), &client); - - let _ = c.method::<(), ()>("notARealMethod", ()); - - // Works (B == &M, M: Clone) - let c: ContractInstance, Provider> = - ContractInstance::new(H160::default(), abi.clone(), client.clone()); - - let _ = c.method::<(), ()>("notARealMethod", ()); - - let non_clone_mware = NonClone { m: client }; - - // Works (B == &M, M: !Clone) - let c: ContractInstance<&NonClone>, NonClone>> = - ContractInstance::new(H160::default(), abi, &non_clone_mware); - - let _ = c.method::<(), ()>("notARealMethod", ()); - - // // Fails (B == M, M: !Clone) - // let c: ContractInternal>, NonClone>> = - // ContractInternal::new(H160::default(), abi, non_clone_mware); - - // let _ = c.method::<(), ()>("notARealMethod", ()); - } - - #[tokio::test] - async fn deploy_and_call_contract() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - - // launch anvil - let anvil = Anvil::new().spawn(); - - // Instantiate the clients. We assume that clients consume the provider and the wallet - // (which makes sense), so for multi-client tests, you must clone the provider. - let addrs = anvil.addresses().to_vec(); - let addr2 = addrs[1]; - let client = connect(&anvil, 0); - let client2 = connect(&anvil, 1); - - // create a factory which will be used to deploy instances of the contract - let factory = ContractFactory::new(abi, bytecode, client.clone()); - - // `send` consumes the deployer so it must be cloned for later re-use - // (practically it's not expected that you'll need to deploy multiple instances of - // the _same_ deployer, so it's fine to clone here from a dev UX vs perf tradeoff) - let deployer = factory.deploy("initial value".to_string()).unwrap().legacy(); - // dry runs the deployment of the contract. takes the deployer by reference, no need to - // clone. - deployer.call().await.unwrap(); - let (contract, receipt) = deployer.clone().send_with_receipt().await.unwrap(); - assert_eq!(receipt.contract_address.unwrap(), contract.address()); - - let get_value = contract.method::<_, String>("getValue", ()).unwrap(); - let last_sender = contract.method::<_, Address>("lastSender", ()).unwrap(); - - // the initial value must be the one set in the constructor - let value = get_value.clone().call().await.unwrap(); - assert_eq!(value, "initial value"); - - // need to declare the method first, and only then send it - // this is because it internally clones an Arc which would otherwise - // get immediately dropped - let contract_call = contract - .connect(client2.clone()) - .method::<_, H256>("setValue", "hi".to_owned()) - .unwrap(); - let calldata = contract_call.calldata().unwrap(); - let gas_estimate = contract_call.estimate_gas().await.unwrap(); - let contract_call = contract_call.legacy(); - let pending_tx = contract_call.send().await.unwrap(); - let tx = client.get_transaction(*pending_tx).await.unwrap().unwrap(); - let tx_receipt = pending_tx.await.unwrap().unwrap(); - assert_eq!(last_sender.clone().call().await.unwrap(), addr2); - assert_eq!(get_value.clone().call().await.unwrap(), "hi"); - assert_eq!(tx.input, calldata); - - // we can also call contract methods at other addresses with the `at` call - // (useful when interacting with multiple ERC20s for example) - let contract2_addr = deployer.send().await.unwrap().address(); - let contract2 = contract.at(contract2_addr); - let init_value: String = - contract2.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); - let init_address = - contract2.method::<_, Address>("lastSender", ()).unwrap().call().await.unwrap(); - assert_eq!(init_address, Address::zero()); - assert_eq!(init_value, "initial value"); - - // methods with multiple args also work - let _tx_hash = contract - .method::<_, H256>("setValues", ("hi".to_owned(), "bye".to_owned())) - .unwrap() - .legacy() - .send() - .await - .unwrap() - .await - .unwrap(); - } - - #[tokio::test] - #[cfg(feature = "abigen")] - async fn get_past_events() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - let anvil = Anvil::new().spawn(); - let client = connect(&anvil, 0); - let address = client.get_accounts().await.unwrap()[0]; - let contract = deploy(client.clone(), abi, bytecode).await; - - // make a call with `client` - let func = contract.method::<_, H256>("setValue", "hi".to_owned()).unwrap().legacy(); - let tx = func.send().await.unwrap(); - let _receipt = tx.await.unwrap(); - - // and we can fetch the events - let logs: Vec = contract - .event() - .from_block(0u64) - .topic1(address) // Corresponds to the first indexed parameter - .query() - .await - .unwrap(); - assert_eq!(logs[0].new_value, "initial value"); - assert_eq!(logs[1].new_value, "hi"); - assert_eq!(logs.len(), 2); - - // and we can fetch the events at a block hash - let hash = client.get_block(1).await.unwrap().unwrap().hash.unwrap(); - let logs: Vec = contract - .event() - .at_block_hash(hash) - .topic1(address) // Corresponds to the first indexed parameter - .query() - .await - .unwrap(); - assert_eq!(logs[0].new_value, "initial value"); - assert_eq!(logs.len(), 1); - } - - #[tokio::test] - #[cfg(feature = "abigen")] - async fn get_events_with_meta() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - let anvil = Anvil::new().spawn(); - let client = connect(&anvil, 0); - let address = anvil.addresses()[0]; - let contract = deploy(client.clone(), abi, bytecode).await; - - // and we can fetch the events - let logs: Vec<(ValueChanged, LogMeta)> = contract - .event() - .from_block(0u64) - .topic1(address) // Corresponds to the first indexed parameter - .query_with_meta() - .await - .unwrap(); - - assert_eq!(logs.len(), 1); - let (log, meta) = &logs[0]; - assert_eq!(log.new_value, "initial value"); - - assert_eq!(meta.address, contract.address()); - assert_eq!(meta.log_index, 0.into()); - assert_eq!(meta.block_number, 1.into()); - let block = client.get_block(1).await.unwrap().unwrap(); - assert_eq!(meta.block_hash, block.hash.unwrap()); - assert_eq!(block.transactions.len(), 1); - let tx = block.transactions[0]; - assert_eq!(meta.transaction_hash, tx); - assert_eq!(meta.transaction_index, 0.into()); - } - - #[tokio::test] - async fn call_past_state() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - let anvil = Anvil::new().spawn(); - let client = connect(&anvil, 0); - let contract = deploy(client.clone(), abi, bytecode).await; - let deployed_block = client.get_block_number().await.unwrap(); - - // assert initial state - let value = - contract.method::<_, String>("getValue", ()).unwrap().legacy().call().await.unwrap(); - assert_eq!(value, "initial value"); - - // make a call with `client` - let _tx_hash = *contract - .method::<_, H256>("setValue", "hi".to_owned()) - .unwrap() - .legacy() - .send() - .await - .unwrap(); - - // assert new value - let value = - contract.method::<_, String>("getValue", ()).unwrap().legacy().call().await.unwrap(); - assert_eq!(value, "hi"); - - // assert previous value - let value = contract - .method::<_, String>("getValue", ()) - .unwrap() - .legacy() - .block(BlockId::Number(deployed_block.into())) - .call() - .await - .unwrap(); - assert_eq!(value, "initial value"); - - // Here would be the place to test EIP-1898, specifying the `BlockId` of `call` as the - // first block hash. However, Ganache does not implement this :/ - - // let hash = client.get_block(1).await.unwrap().unwrap().hash.unwrap(); - // let value = contract - // .method::<_, String>("getValue", ()) - // .unwrap() - // .block(BlockId::Hash(hash)) - // .call() - // .await - // .unwrap(); - // assert_eq!(value, "initial value"); - } - - #[tokio::test] - #[ignore] - async fn call_past_hash_test() { - // geth --dev --http --http.api eth,web3 - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - let provider = Provider::::try_from("http://localhost:8545").unwrap(); - let deployer = provider.get_accounts().await.unwrap()[0]; - - let client = Arc::new(provider.with_sender(deployer)); - let contract = deploy(client.clone(), abi, bytecode).await; - let deployed_block = client.get_block_number().await.unwrap(); - - // assert initial state - let value = contract.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); - assert_eq!(value, "initial value"); - - // make a call with `client` - let _tx_hash = - *contract.method::<_, H256>("setValue", "hi".to_owned()).unwrap().send().await.unwrap(); - - // assert new value - let value = contract.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); - assert_eq!(value, "hi"); - - // assert previous value using block hash - let hash = client.get_block(deployed_block).await.unwrap().unwrap().hash.unwrap(); - let value = contract - .method::<_, String>("getValue", ()) - .unwrap() - .block(BlockId::Hash(hash)) - .call() - .await - .unwrap(); - assert_eq!(value, "initial value"); - } - - #[tokio::test] - #[cfg(feature = "abigen")] - async fn watch_events() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - let anvil = Anvil::new().spawn(); - let client = connect(&anvil, 0); - let contract = deploy(client.clone(), abi.clone(), bytecode).await; - - // We spawn the event listener: - let event = contract.event::(); - let mut stream = event.stream().await.unwrap(); - - // Also set up a subscription for the same thing - let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); - let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws.into()); - let event2 = contract2.event::(); - let mut subscription = event2.subscribe().await.unwrap(); - - let mut subscription_meta = event2.subscribe().await.unwrap().with_meta(); - - let num_calls = 3u64; - - // and we make a few calls - let num = client.get_block_number().await.unwrap(); - for i in 0..num_calls { - let call = contract.method::<_, H256>("setValue", i.to_string()).unwrap().legacy(); - let pending_tx = call.send().await.unwrap(); - let _receipt = pending_tx.await.unwrap(); - } - - for i in 0..num_calls { - // unwrap the option of the stream, then unwrap the decoding result - let log = stream.next().await.unwrap().unwrap(); - let log2 = subscription.next().await.unwrap().unwrap(); - let (log3, meta) = subscription_meta.next().await.unwrap().unwrap(); - assert_eq!(log.new_value, log3.new_value); - assert_eq!(log.new_value, log2.new_value); - assert_eq!(log.new_value, i.to_string()); - assert_eq!(meta.block_number, num + i + 1); - let hash = client.get_block(num + i + 1).await.unwrap().unwrap().hash.unwrap(); - assert_eq!(meta.block_hash, hash); - } - } - - #[tokio::test] - async fn watch_subscription_events_multiple_addresses() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - let anvil = Anvil::new().spawn(); - let client = connect(&anvil, 0); - let contract_1 = deploy(client.clone(), abi.clone(), bytecode.clone()).await; - let contract_2 = deploy(client.clone(), abi.clone(), bytecode).await; - - let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); - let filter = Filter::new() - .address(ValueOrArray::Array(vec![contract_1.address(), contract_2.address()])); - let mut stream = ws.subscribe_logs(&filter).await.unwrap(); - - // and we make a few calls - let call = contract_1.method::<_, H256>("setValue", "1".to_string()).unwrap().legacy(); - let pending_tx = call.send().await.unwrap(); - let _receipt = pending_tx.await.unwrap(); - - let call = contract_2.method::<_, H256>("setValue", "2".to_string()).unwrap().legacy(); - let pending_tx = call.send().await.unwrap(); - let _receipt = pending_tx.await.unwrap(); - - // unwrap the option of the stream, then unwrap the decoding result - let log_1 = stream.next().await.unwrap(); - let log_2 = stream.next().await.unwrap(); - assert_eq!(log_1.address, contract_1.address()); - assert_eq!(log_2.address, contract_2.address()); - } - - #[tokio::test] - async fn build_event_of_type() { - abigen!( - AggregatorInterface, - r#"[ - event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt) - ]"#, - ); - - let anvil = Anvil::new().spawn(); - let client = connect(&anvil, 0); - let event = ethers_contract::Contract::event_of_type::(client); - assert_eq!(event.filter, Filter::new().event(&AnswerUpdatedFilter::abi_signature())); - } - - #[tokio::test] - async fn signer_on_node() { - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - // spawn anvil - let anvil = Anvil::new().spawn(); - - // connect - let provider = Provider::::try_from(anvil.endpoint()) - .unwrap() - .interval(std::time::Duration::from_millis(50u64)); - - // get the first account - let deployer = provider.get_accounts().await.unwrap()[0]; - let client = Arc::new(provider.with_sender(deployer)); - - let contract = deploy(client, abi, bytecode).await; - - // make a call without the signer - let _receipt = contract - .method::<_, H256>("setValue", "hi".to_owned()) - .unwrap() - .legacy() - .send() - .await - .unwrap() - .await - .unwrap(); - let value: String = - contract.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); - assert_eq!(value, "hi"); - } - - #[tokio::test] - async fn multicall_aggregate() { - // get ABI and bytecode for the Multicall contract - let (multicall_abi, multicall_bytecode) = compile_contract("Multicall3", "Multicall.sol"); - - // get ABI and bytecode for the NotSoSimpleStorage contract - let (not_so_simple_abi, not_so_simple_bytecode) = - compile_contract("NotSoSimpleStorage", "NotSoSimpleStorage.sol"); - - // get ABI and bytecode for the SimpleStorage contract - let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); - - // launch anvil - let anvil = Anvil::new().spawn(); - - // Instantiate the clients. We assume that clients consume the provider and the wallet - // (which makes sense), so for multi-client tests, you must clone the provider. - // `client` is used to deploy the Multicall contract - // `client2` is used to deploy the first SimpleStorage contract - // `client3` is used to deploy the second SimpleStorage contract - // `client4` is used to make the aggregate call - let addrs = anvil.addresses().to_vec(); - let addr2 = addrs[1]; - let addr3 = addrs[2]; - let client = connect(&anvil, 0); - let client2 = connect(&anvil, 1); - let client3 = connect(&anvil, 2); - let client4 = connect(&anvil, 3); - - // create a factory which will be used to deploy instances of the contract - let multicall_factory = - ContractFactory::new(multicall_abi, multicall_bytecode, client.clone()); - let simple_factory = ContractFactory::new(abi.clone(), bytecode.clone(), client2.clone()); - let not_so_simple_factory = - ContractFactory::new(not_so_simple_abi, not_so_simple_bytecode, client3.clone()); - - let multicall_contract = - multicall_factory.deploy(()).unwrap().legacy().send().await.unwrap(); - let addr = multicall_contract.address(); - - let simple_contract = simple_factory - .deploy("the first one".to_string()) - .unwrap() - .legacy() - .send() - .await - .unwrap(); - let not_so_simple_contract = not_so_simple_factory - .deploy("the second one".to_string()) - .unwrap() - .legacy() - .send() - .await - .unwrap(); - - // Client2 and Client3 broadcast txs to set the values for both contracts - simple_contract - .connect(client2.clone()) - .method::<_, H256>("setValue", "reset first".to_owned()) - .unwrap() - .legacy() - .send() - .await - .unwrap(); - - not_so_simple_contract - .connect(client3.clone()) - .method::<_, H256>("setValue", "reset second".to_owned()) - .unwrap() - .legacy() - .send() - .await - .unwrap(); - - // get the calls for `value` and `last_sender` for both SimpleStorage contracts - let value = simple_contract.method::<_, String>("getValue", ()).unwrap(); - let value2 = - not_so_simple_contract.method::<_, (String, Address)>("getValues", ()).unwrap(); - let last_sender = simple_contract.method::<_, Address>("lastSender", ()).unwrap(); - let last_sender2 = not_so_simple_contract.method::<_, Address>("lastSender", ()).unwrap(); - - // initiate the Multicall instance and add calls one by one in builder style - let mut multicall = Multicall::new(client4.clone(), Some(addr)).await.unwrap(); - - // Set version to 1 - multicall = multicall.version(MulticallVersion::Multicall); - - multicall - .add_call(value, false) - .add_call(value2, false) - .add_call(last_sender, false) - .add_call(last_sender2, false); - - let return_data: (String, (String, Address), Address, Address) = - multicall.call().await.unwrap(); - - assert_eq!(return_data.0, "reset first"); - assert_eq!((return_data.1).0, "reset second"); - assert_eq!((return_data.1).1, addr3); - assert_eq!(return_data.2, addr2); - assert_eq!(return_data.3, addr3); - - // construct broadcast transactions that will be batched and broadcast via Multicall - let broadcast = simple_contract - .connect(client4.clone()) - .method::<_, H256>("setValue", "first reset again".to_owned()) - .unwrap(); - let broadcast2 = not_so_simple_contract - .connect(client4.clone()) - .method::<_, H256>("setValue", "second reset again".to_owned()) - .unwrap(); - - // use the already initialised Multicall instance, clearing the previous calls and adding - // new calls. Previously we used the `.call()` functionality to do a batch of calls in one - // go. Now we will use the `.send()` functionality to broadcast a batch of transactions - // in one go - let mut multicall_send = multicall.clone(); - multicall_send.clear_calls().add_call(broadcast, false).add_call(broadcast2, false); - - // broadcast the transaction and wait for it to be mined - let _tx_receipt = multicall_send.legacy().send().await.unwrap().await.unwrap(); - - // Do another multicall to check the updated return values - // The `getValue` calls should return the last value we set in the batched broadcast - // The `lastSender` calls should return the address of the Multicall contract, as it is - // the one acting as proxy and calling our SimpleStorage contracts (msg.sender) - let return_data: (String, (String, Address), Address, Address) = - multicall.call().await.unwrap(); - - assert_eq!(return_data.0, "first reset again"); - assert_eq!((return_data.1).0, "second reset again"); - assert_eq!((return_data.1).1, multicall_contract.address()); - assert_eq!(return_data.2, multicall_contract.address()); - assert_eq!(return_data.3, multicall_contract.address()); - - let addrs = anvil.addresses(); - // query ETH balances of multiple addresses - // these keys haven't been used to do any tx - // so should have 100 ETH - multicall - .clear_calls() - .add_get_eth_balance(addrs[4], false) - .add_get_eth_balance(addrs[5], false) - .add_get_eth_balance(addrs[6], false); - - let valid_balances = [ - U256::from(10_000_000_000_000_000_000_000u128), - U256::from(10_000_000_000_000_000_000_000u128), - U256::from(10_000_000_000_000_000_000_000u128), - ]; - - let balances: (U256, U256, U256) = multicall.call().await.unwrap(); - assert_eq!(balances.0, valid_balances[0]); - assert_eq!(balances.1, valid_balances[1]); - assert_eq!(balances.2, valid_balances[2]); - - // call_array - multicall - .clear_calls() - .add_get_eth_balance(addrs[4], false) - .add_get_eth_balance(addrs[5], false) - .add_get_eth_balance(addrs[6], false); - - let balances: Vec = multicall.call_array().await.unwrap(); - assert_eq!(balances, Vec::from_iter(valid_balances.iter().copied())); - - // clear multicall so we can test `call_raw` w/ >16 calls - multicall.clear_calls(); - - // clear the current value - simple_contract - .connect(client2.clone()) - .method::<_, H256>("setValue", "many".to_owned()) - .unwrap() - .legacy() - .send() - .await - .unwrap(); - - multicall.add_calls( - false, - std::iter::repeat(simple_contract.method::<_, String>("getValue", ()).unwrap()) - .take(17), - ); - - let tokens = multicall.call_raw().await.unwrap(); - let results: Vec = tokens - .into_iter() - .map(|result| { - // decode manually using Tokenizable method - String::from_token(result.unwrap()).unwrap() - }) - .collect(); - assert_eq!(results, ["many"; 17]); - - // test version 2 - multicall = multicall.version(MulticallVersion::Multicall2); - - // deploy contract with reverting methods - let reverting_contract = { - let (abi, bytecode) = - compile_contract("SimpleRevertingStorage", "SimpleRevertingStorage.sol"); - let f = ContractFactory::new(abi, bytecode, client.clone()); - f.deploy("This contract can revert".to_string()).unwrap().send().await.unwrap() - }; - - // reset value - reverting_contract - .connect(client2.clone()) - .method::<_, H256>("setValue", ("reset third".to_owned(), false)) - .unwrap() - .send() - .await - .unwrap(); - - // create calls - let set_value_call = reverting_contract - .connect(client.clone()) - .method::<_, H256>("setValue", ("this didn't revert".to_owned(), false)) - .unwrap(); - let set_value_reverting_call = reverting_contract - .connect(client3.clone()) - .method::<_, H256>("setValue", ("this reverted".to_owned(), true)) - .unwrap(); - let get_value_call = reverting_contract - .connect(client2.clone()) - .method::<_, String>("getValue", false) - .unwrap(); - let get_value_reverting_call = reverting_contract - .connect(client.clone()) - .method::<_, String>("getValue", true) - .unwrap(); - - // .send reverts - // don't allow revert - multicall - .clear_calls() - .add_call(set_value_reverting_call.clone(), false) - .add_call(set_value_call.clone(), false); - multicall.send().await.unwrap_err(); - - // value has not changed - assert_eq!(get_value_call.clone().call().await.unwrap(), "reset third"); - - // allow revert - multicall - .clear_calls() - .add_call(set_value_reverting_call.clone(), true) - .add_call(set_value_call.clone(), false); - multicall.send().await.unwrap(); - - // value has changed - assert_eq!(get_value_call.clone().call().await.unwrap(), "this didn't revert"); - - // reset value again - reverting_contract - .connect(client2.clone()) - .method::<_, H256>("setValue", ("reset third again".to_owned(), false)) - .unwrap() - .send() - .await - .unwrap(); - - // .call reverts - // don't allow revert - multicall - .clear_calls() - .add_call(get_value_reverting_call.clone(), false) - .add_call(get_value_call.clone(), false); - let res = multicall.call::<(String, String)>().await; - let err = res.unwrap_err(); - - assert!(err.is_revert()); - let message = err.decode_revert::().unwrap(); - assert!(message.contains("Multicall3: call failed")); - - // allow revert -> call doesn't revert, but returns Err(_) in raw tokens - let expected = Bytes::from_static(b"getValue revert").encode(); - multicall.clear_calls().add_call(get_value_reverting_call.clone(), true); - assert_eq!(multicall.call_raw().await.unwrap()[0].as_ref().unwrap_err()[4..], expected[..]); - assert_eq!( - multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..], - expected[..] - ); - - // v2 illegal revert - multicall - .clear_calls() - .add_call(get_value_reverting_call.clone(), false) // don't allow revert - .add_call(get_value_call.clone(), true); // true here will result in `tryAggregate(false, ...)` - assert!(matches!( - multicall.call::<(String, String)>().await.unwrap_err(), - MulticallError::IllegalRevert - )); - - // test version 3 - // aggregate3 is the same as try_aggregate except with allowing failure on a per-call basis. - // no need to test that - multicall = multicall.version(MulticallVersion::Multicall3); - - // .send with value - let amount = U256::from(100); - let value_tx = reverting_contract.method::<_, H256>("deposit", ()).unwrap().value(amount); - let rc_addr = reverting_contract.address(); - - let (bal_before,): (U256,) = - multicall.clear_calls().add_get_eth_balance(rc_addr, false).call().await.unwrap(); - - // send 2 value_tx - multicall.clear_calls().add_call(value_tx.clone(), false).add_call(value_tx.clone(), false); - multicall.send().await.unwrap(); - - let (bal_after,): (U256,) = - multicall.clear_calls().add_get_eth_balance(rc_addr, false).call().await.unwrap(); - - assert_eq!(bal_after, bal_before + U256::from(2) * amount); - - // test specific revert cases - // empty revert - let empty_revert = reverting_contract.method::<_, H256>("emptyRevert", ()).unwrap(); - multicall.clear_calls().add_call(empty_revert.clone(), true); - assert!(multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap().is_empty()); - - // string revert - let string_revert = - reverting_contract.method::<_, H256>("stringRevert", ("String".to_string())).unwrap(); - multicall.clear_calls().add_call(string_revert, true); - assert_eq!( - multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..], - Bytes::from_static(b"String").encode()[..] - ); - - // custom error revert - let custom_error = reverting_contract.method::<_, H256>("customError", ()).unwrap(); - multicall.clear_calls().add_call(custom_error, true); - assert_eq!( - multicall.call::<(Bytes,)>().await.unwrap_err().as_revert().unwrap()[..], - keccak256("CustomError()")[..4] - ); - - // custom error with data revert - let custom_error_with_data = reverting_contract - .method::<_, H256>("customErrorWithData", ("Data".to_string())) - .unwrap(); - multicall.clear_calls().add_call(custom_error_with_data, true); - let err = multicall.call::<(Bytes,)>().await.unwrap_err(); - let bytes = err.as_revert().unwrap(); - assert_eq!(bytes[..4], keccak256("CustomErrorWithData(string)")[..4]); - assert_eq!(bytes[4..], encode(&[Token::String("Data".to_string())])); - } - - #[tokio::test] - async fn test_derive_eip712() { - // Generate Contract ABI Bindings - abigen!( - DeriveEip712Test, - "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json", - event_derives(serde::Deserialize, serde::Serialize) - ); - - // Create derived structs - - #[derive(Debug, Clone, Eip712, EthAbiType)] - #[eip712( - name = "Eip712Test", - version = "1", - chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000001", - salt = "eip712-test-75F0CCte" - )] - struct FooBar { - foo: I256, - bar: U256, - fizz: Bytes, - buzz: [u8; 32], - far: String, - out: Address, - } - - // get ABI and bytecode for the DeriveEip712Test contract - let (abi, bytecode) = compile_contract("DeriveEip712Test", "DeriveEip712Test.sol"); - - // launch the network & connect to it - let anvil = Anvil::new().spawn(); - let from = anvil.addresses()[0]; - let provider = Provider::try_from(anvil.endpoint()) - .unwrap() - .with_sender(from) - .interval(std::time::Duration::from_millis(10)); - let client = Arc::new(provider); - - let wallet: LocalWallet = anvil.keys()[0].clone().into(); - - let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone()); - - let contract = factory - .deploy(()) - .expect("failed to deploy DeriveEip712Test contract") - .legacy() - .send() - .await - .expect("failed to instantiate factory for DeriveEip712 contract"); - - let addr = contract.address(); - - let contract = DeriveEip712Test::new(addr, client.clone()); - - let foo_bar = FooBar { - foo: I256::from(10u64), - bar: U256::from(20u64), - fizz: b"fizz".into(), - buzz: keccak256("buzz"), - far: String::from("space"), - out: Address::from([0; 20]), - }; - - let derived_foo_bar = derive_eip_712_test::FooBar { - foo: foo_bar.foo, - bar: foo_bar.bar, - fizz: foo_bar.fizz.clone(), - buzz: foo_bar.buzz, - far: foo_bar.far.clone(), - out: foo_bar.out, - }; - - let sig = wallet.sign_typed_data(&foo_bar).await.expect("failed to sign typed data"); - - let r = <[u8; 32]>::try_from(sig.r) - .expect("failed to parse 'r' value from signature into [u8; 32]"); - let s = <[u8; 32]>::try_from(sig.s) - .expect("failed to parse 's' value from signature into [u8; 32]"); - let v = u8::try_from(sig.v).expect("failed to parse 'v' value from signature into u8"); - - let domain_separator = contract - .domain_separator() - .call() - .await - .expect("failed to retrieve domain_separator from contract"); - let type_hash = - contract.type_hash().call().await.expect("failed to retrieve type_hash from contract"); - let struct_hash = contract - .struct_hash(derived_foo_bar.clone()) - .call() - .await - .expect("failed to retrieve struct_hash from contract"); - let encoded = contract - .encode_eip_712(derived_foo_bar.clone()) - .call() - .await - .expect("failed to retrieve eip712 encoded hash from contract"); - let verify = contract - .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v) - .call() - .await - .expect("failed to verify signed typed data eip712 payload"); - - assert_eq!( - domain_separator, - foo_bar - .domain() - .expect("failed to return domain_separator from Eip712 implemented struct") - .separator(), - "domain separator does not match contract domain separator!" - ); - - assert_eq!( - type_hash, - FooBar::type_hash().expect("failed to return type_hash from Eip712 implemented struct"), - "type hash does not match contract struct type hash!" - ); - - assert_eq!( - struct_hash, - foo_bar - .clone() - .struct_hash() - .expect("failed to return struct_hash from Eip712 implemented struct"), - "struct hash does not match contract struct hash!" - ); - - assert_eq!( - encoded, - foo_bar - .encode_eip712() - .expect("failed to return domain_separator from Eip712 implemented struct"), - "Encoded value does not match!" - ); - - assert!(verify, "typed data signature failed!"); + fn as_inner(&self) -> Option<&Self::Inner> { + Some(&self.0) } } + +impl std::fmt::Display for MwErr { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} +impl std::error::Error for MwErr {} + +impl Middleware for NonClone { + type Error = MwErr; + + type Provider = M::Provider; + + type Inner = M; + + fn inner(&self) -> &Self::Inner { + &self.m + } +} + +// this is not a test. It is a compile check. :) +// It exists to ensure that trait bounds on contract internal behave as +// expected. It should not be run +fn _it_compiles() { + let (abi, _bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + + // launch anvil + let anvil = Anvil::new().spawn(); + + let client = Provider::::try_from(anvil.endpoint()) + .unwrap() + .interval(Duration::from_millis(10u64)); + + // Works (B == M, M: Clone) + let c: ContractInstance<&Provider, Provider> = + ContractInstance::new(H160::default(), abi.clone(), &client); + + let _ = c.method::<(), ()>("notARealMethod", ()); + + // Works (B == &M, M: Clone) + let c: ContractInstance, Provider> = + ContractInstance::new(H160::default(), abi.clone(), client.clone()); + + let _ = c.method::<(), ()>("notARealMethod", ()); + + let non_clone_mware = NonClone { m: client }; + + // Works (B == &M, M: !Clone) + let c: ContractInstance<&NonClone>, NonClone>> = + ContractInstance::new(H160::default(), abi, &non_clone_mware); + + let _ = c.method::<(), ()>("notARealMethod", ()); + + // // Fails (B == M, M: !Clone) + // let c: ContractInternal>, NonClone>> = + // ContractInternal::new(H160::default(), abi, non_clone_mware); + + // let _ = c.method::<(), ()>("notARealMethod", ()); +} + +#[tokio::test] +async fn deploy_and_call_contract() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + + // launch anvil + let anvil = Anvil::new().spawn(); + + // Instantiate the clients. We assume that clients consume the provider and the wallet + // (which makes sense), so for multi-client tests, you must clone the provider. + let addrs = anvil.addresses().to_vec(); + let addr2 = addrs[1]; + let client = connect(&anvil, 0); + let client2 = connect(&anvil, 1); + + // create a factory which will be used to deploy instances of the contract + let factory = ContractFactory::new(abi, bytecode, client.clone()); + + // `send` consumes the deployer so it must be cloned for later re-use + // (practically it's not expected that you'll need to deploy multiple instances of + // the _same_ deployer, so it's fine to clone here from a dev UX vs perf tradeoff) + let deployer = factory.deploy("initial value".to_string()).unwrap().legacy(); + // dry runs the deployment of the contract. takes the deployer by reference, no need to + // clone. + deployer.call().await.unwrap(); + let (contract, receipt) = deployer.clone().send_with_receipt().await.unwrap(); + assert_eq!(receipt.contract_address.unwrap(), contract.address()); + + let get_value = contract.method::<_, String>("getValue", ()).unwrap(); + let last_sender = contract.method::<_, Address>("lastSender", ()).unwrap(); + + // the initial value must be the one set in the constructor + let value = get_value.clone().call().await.unwrap(); + assert_eq!(value, "initial value"); + + // need to declare the method first, and only then send it + // this is because it internally clones an Arc which would otherwise + // get immediately dropped + let contract_call = + contract.connect(client2.clone()).method::<_, H256>("setValue", "hi".to_owned()).unwrap(); + let calldata = contract_call.calldata().unwrap(); + let _gas_estimate = contract_call.estimate_gas().await.unwrap(); + let contract_call = contract_call.legacy(); + let pending_tx = contract_call.send().await.unwrap(); + let tx = client.get_transaction(*pending_tx).await.unwrap().unwrap(); + let _tx_receipt = pending_tx.await.unwrap().unwrap(); + assert_eq!(last_sender.clone().call().await.unwrap(), addr2); + assert_eq!(get_value.clone().call().await.unwrap(), "hi"); + assert_eq!(tx.input, calldata); + + // we can also call contract methods at other addresses with the `at` call + // (useful when interacting with multiple ERC20s for example) + let contract2_addr = deployer.send().await.unwrap().address(); + let contract2 = contract.at(contract2_addr); + let init_value: String = + contract2.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); + let init_address = + contract2.method::<_, Address>("lastSender", ()).unwrap().call().await.unwrap(); + assert_eq!(init_address, Address::zero()); + assert_eq!(init_value, "initial value"); + + // methods with multiple args also work + let _tx_hash = contract + .method::<_, H256>("setValues", ("hi".to_owned(), "bye".to_owned())) + .unwrap() + .legacy() + .send() + .await + .unwrap() + .await + .unwrap(); +} + +#[tokio::test] +#[cfg(feature = "abigen")] +async fn get_past_events() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + let anvil = Anvil::new().spawn(); + let client = connect(&anvil, 0); + let address = client.get_accounts().await.unwrap()[0]; + let contract = deploy(client.clone(), abi, bytecode).await; + + // make a call with `client` + let func = contract.method::<_, H256>("setValue", "hi".to_owned()).unwrap().legacy(); + let tx = func.send().await.unwrap(); + let _receipt = tx.await.unwrap(); + + // and we can fetch the events + let logs: Vec = contract + .event() + .from_block(0u64) + .topic1(address) // Corresponds to the first indexed parameter + .query() + .await + .unwrap(); + assert_eq!(logs[0].new_value, "initial value"); + assert_eq!(logs[1].new_value, "hi"); + assert_eq!(logs.len(), 2); + + // and we can fetch the events at a block hash + let hash = client.get_block(1).await.unwrap().unwrap().hash.unwrap(); + let logs: Vec = contract + .event() + .at_block_hash(hash) + .topic1(address) // Corresponds to the first indexed parameter + .query() + .await + .unwrap(); + assert_eq!(logs[0].new_value, "initial value"); + assert_eq!(logs.len(), 1); +} + +#[tokio::test] +#[cfg(feature = "abigen")] +async fn get_events_with_meta() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + let anvil = Anvil::new().spawn(); + let client = connect(&anvil, 0); + let address = anvil.addresses()[0]; + let contract = deploy(client.clone(), abi, bytecode).await; + + // and we can fetch the events + let logs: Vec<(ValueChanged, LogMeta)> = contract + .event() + .from_block(0u64) + .topic1(address) // Corresponds to the first indexed parameter + .query_with_meta() + .await + .unwrap(); + + assert_eq!(logs.len(), 1); + let (log, meta) = &logs[0]; + assert_eq!(log.new_value, "initial value"); + + assert_eq!(meta.address, contract.address()); + assert_eq!(meta.log_index, 0.into()); + assert_eq!(meta.block_number, 1.into()); + let block = client.get_block(1).await.unwrap().unwrap(); + assert_eq!(meta.block_hash, block.hash.unwrap()); + assert_eq!(block.transactions.len(), 1); + let tx = block.transactions[0]; + assert_eq!(meta.transaction_hash, tx); + assert_eq!(meta.transaction_index, 0.into()); +} + +#[tokio::test] +async fn call_past_state() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + let anvil = Anvil::new().spawn(); + let client = connect(&anvil, 0); + let contract = deploy(client.clone(), abi, bytecode).await; + let deployed_block = client.get_block_number().await.unwrap(); + + // assert initial state + let value = + contract.method::<_, String>("getValue", ()).unwrap().legacy().call().await.unwrap(); + assert_eq!(value, "initial value"); + + // make a call with `client` + let _tx_hash = *contract + .method::<_, H256>("setValue", "hi".to_owned()) + .unwrap() + .legacy() + .send() + .await + .unwrap(); + + // assert new value + let value = + contract.method::<_, String>("getValue", ()).unwrap().legacy().call().await.unwrap(); + assert_eq!(value, "hi"); + + // assert previous value + let value = contract + .method::<_, String>("getValue", ()) + .unwrap() + .legacy() + .block(BlockId::Number(deployed_block.into())) + .call() + .await + .unwrap(); + assert_eq!(value, "initial value"); + + // Here would be the place to test EIP-1898, specifying the `BlockId` of `call` as the + // first block hash. However, Ganache does not implement this :/ + + // let hash = client.get_block(1).await.unwrap().unwrap().hash.unwrap(); + // let value = contract + // .method::<_, String>("getValue", ()) + // .unwrap() + // .block(BlockId::Hash(hash)) + // .call() + // .await + // .unwrap(); + // assert_eq!(value, "initial value"); +} + +#[tokio::test] +#[ignore] +async fn call_past_hash_test() { + // geth --dev --http --http.api eth,web3 + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + let provider = Provider::::try_from("http://localhost:8545").unwrap(); + let deployer = provider.get_accounts().await.unwrap()[0]; + + let client = Arc::new(provider.with_sender(deployer)); + let contract = deploy(client.clone(), abi, bytecode).await; + let deployed_block = client.get_block_number().await.unwrap(); + + // assert initial state + let value = contract.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); + assert_eq!(value, "initial value"); + + // make a call with `client` + let _tx_hash = + *contract.method::<_, H256>("setValue", "hi".to_owned()).unwrap().send().await.unwrap(); + + // assert new value + let value = contract.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); + assert_eq!(value, "hi"); + + // assert previous value using block hash + let hash = client.get_block(deployed_block).await.unwrap().unwrap().hash.unwrap(); + let value = contract + .method::<_, String>("getValue", ()) + .unwrap() + .block(BlockId::Hash(hash)) + .call() + .await + .unwrap(); + assert_eq!(value, "initial value"); +} + +#[tokio::test] +#[cfg(feature = "abigen")] +async fn watch_events() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + let anvil = Anvil::new().spawn(); + let client = connect(&anvil, 0); + let contract = deploy(client.clone(), abi.clone(), bytecode).await; + + // We spawn the event listener: + let event = contract.event::(); + let mut stream = event.stream().await.unwrap(); + + // Also set up a subscription for the same thing + let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); + let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws.into()); + let event2 = contract2.event::(); + let mut subscription = event2.subscribe().await.unwrap(); + + let mut subscription_meta = event2.subscribe().await.unwrap().with_meta(); + + let num_calls = 3u64; + + // and we make a few calls + let num = client.get_block_number().await.unwrap(); + for i in 0..num_calls { + let call = contract.method::<_, H256>("setValue", i.to_string()).unwrap().legacy(); + let pending_tx = call.send().await.unwrap(); + let _receipt = pending_tx.await.unwrap(); + } + + for i in 0..num_calls { + // unwrap the option of the stream, then unwrap the decoding result + let log = stream.next().await.unwrap().unwrap(); + let log2 = subscription.next().await.unwrap().unwrap(); + let (log3, meta) = subscription_meta.next().await.unwrap().unwrap(); + assert_eq!(log.new_value, log3.new_value); + assert_eq!(log.new_value, log2.new_value); + assert_eq!(log.new_value, i.to_string()); + assert_eq!(meta.block_number, num + i + 1); + let hash = client.get_block(num + i + 1).await.unwrap().unwrap().hash.unwrap(); + assert_eq!(meta.block_hash, hash); + } +} + +#[tokio::test] +async fn watch_subscription_events_multiple_addresses() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + let anvil = Anvil::new().spawn(); + let client = connect(&anvil, 0); + let contract_1 = deploy(client.clone(), abi.clone(), bytecode.clone()).await; + let contract_2 = deploy(client.clone(), abi.clone(), bytecode).await; + + let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); + let filter = Filter::new() + .address(ValueOrArray::Array(vec![contract_1.address(), contract_2.address()])); + let mut stream = ws.subscribe_logs(&filter).await.unwrap(); + + // and we make a few calls + let call = contract_1.method::<_, H256>("setValue", "1".to_string()).unwrap().legacy(); + let pending_tx = call.send().await.unwrap(); + let _receipt = pending_tx.await.unwrap(); + + let call = contract_2.method::<_, H256>("setValue", "2".to_string()).unwrap().legacy(); + let pending_tx = call.send().await.unwrap(); + let _receipt = pending_tx.await.unwrap(); + + // unwrap the option of the stream, then unwrap the decoding result + let log_1 = stream.next().await.unwrap(); + let log_2 = stream.next().await.unwrap(); + assert_eq!(log_1.address, contract_1.address()); + assert_eq!(log_2.address, contract_2.address()); +} + +#[tokio::test] +async fn build_event_of_type() { + abigen!( + AggregatorInterface, + r#"[ + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt) + ]"#, + ); + + let anvil = Anvil::new().spawn(); + let client = connect(&anvil, 0); + let event = ethers_contract::Contract::event_of_type::(client); + assert_eq!(event.filter, Filter::new().event(&AnswerUpdatedFilter::abi_signature())); +} + +#[tokio::test] +async fn signer_on_node() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + // spawn anvil + let anvil = Anvil::new().spawn(); + + // connect + let provider = Provider::::try_from(anvil.endpoint()) + .unwrap() + .interval(std::time::Duration::from_millis(50u64)); + + // get the first account + let deployer = provider.get_accounts().await.unwrap()[0]; + let client = Arc::new(provider.with_sender(deployer)); + + let contract = deploy(client, abi, bytecode).await; + + // make a call without the signer + let _receipt = contract + .method::<_, H256>("setValue", "hi".to_owned()) + .unwrap() + .legacy() + .send() + .await + .unwrap() + .await + .unwrap(); + let value: String = contract.method::<_, String>("getValue", ()).unwrap().call().await.unwrap(); + assert_eq!(value, "hi"); +} + +#[tokio::test] +async fn multicall_aggregate() { + // get ABI and bytecode for the Multicall contract + let (multicall_abi, multicall_bytecode) = compile_contract("Multicall3", "Multicall.sol"); + + // get ABI and bytecode for the NotSoSimpleStorage contract + let (not_so_simple_abi, not_so_simple_bytecode) = + compile_contract("NotSoSimpleStorage", "NotSoSimpleStorage.sol"); + + // get ABI and bytecode for the SimpleStorage contract + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + + // launch anvil + let anvil = Anvil::new().spawn(); + + // Instantiate the clients. We assume that clients consume the provider and the wallet + // (which makes sense), so for multi-client tests, you must clone the provider. + // `client` is used to deploy the Multicall contract + // `client2` is used to deploy the first SimpleStorage contract + // `client3` is used to deploy the second SimpleStorage contract + // `client4` is used to make the aggregate call + let addrs = anvil.addresses().to_vec(); + let addr2 = addrs[1]; + let addr3 = addrs[2]; + let client = connect(&anvil, 0); + let client2 = connect(&anvil, 1); + let client3 = connect(&anvil, 2); + let client4 = connect(&anvil, 3); + + // create a factory which will be used to deploy instances of the contract + let multicall_factory = ContractFactory::new(multicall_abi, multicall_bytecode, client.clone()); + let simple_factory = ContractFactory::new(abi.clone(), bytecode.clone(), client2.clone()); + let not_so_simple_factory = + ContractFactory::new(not_so_simple_abi, not_so_simple_bytecode, client3.clone()); + + let multicall_contract = multicall_factory.deploy(()).unwrap().legacy().send().await.unwrap(); + let addr = multicall_contract.address(); + + let simple_contract = + simple_factory.deploy("the first one".to_string()).unwrap().legacy().send().await.unwrap(); + let not_so_simple_contract = not_so_simple_factory + .deploy("the second one".to_string()) + .unwrap() + .legacy() + .send() + .await + .unwrap(); + + // Client2 and Client3 broadcast txs to set the values for both contracts + simple_contract + .connect(client2.clone()) + .method::<_, H256>("setValue", "reset first".to_owned()) + .unwrap() + .legacy() + .send() + .await + .unwrap(); + + not_so_simple_contract + .connect(client3.clone()) + .method::<_, H256>("setValue", "reset second".to_owned()) + .unwrap() + .legacy() + .send() + .await + .unwrap(); + + // get the calls for `value` and `last_sender` for both SimpleStorage contracts + let value = simple_contract.method::<_, String>("getValue", ()).unwrap(); + let value2 = not_so_simple_contract.method::<_, (String, Address)>("getValues", ()).unwrap(); + let last_sender = simple_contract.method::<_, Address>("lastSender", ()).unwrap(); + let last_sender2 = not_so_simple_contract.method::<_, Address>("lastSender", ()).unwrap(); + + // initiate the Multicall instance and add calls one by one in builder style + let mut multicall = Multicall::new(client4.clone(), Some(addr)).await.unwrap(); + + // Set version to 1 + multicall = multicall.version(MulticallVersion::Multicall); + + multicall + .add_call(value, false) + .add_call(value2, false) + .add_call(last_sender, false) + .add_call(last_sender2, false); + + let return_data: (String, (String, Address), Address, Address) = + multicall.call().await.unwrap(); + + assert_eq!(return_data.0, "reset first"); + assert_eq!((return_data.1).0, "reset second"); + assert_eq!((return_data.1).1, addr3); + assert_eq!(return_data.2, addr2); + assert_eq!(return_data.3, addr3); + + // construct broadcast transactions that will be batched and broadcast via Multicall + let broadcast = simple_contract + .connect(client4.clone()) + .method::<_, H256>("setValue", "first reset again".to_owned()) + .unwrap(); + let broadcast2 = not_so_simple_contract + .connect(client4.clone()) + .method::<_, H256>("setValue", "second reset again".to_owned()) + .unwrap(); + + // use the already initialised Multicall instance, clearing the previous calls and adding + // new calls. Previously we used the `.call()` functionality to do a batch of calls in one + // go. Now we will use the `.send()` functionality to broadcast a batch of transactions + // in one go + let mut multicall_send = multicall.clone(); + multicall_send.clear_calls().add_call(broadcast, false).add_call(broadcast2, false); + + // broadcast the transaction and wait for it to be mined + let _tx_receipt = multicall_send.legacy().send().await.unwrap().await.unwrap(); + + // Do another multicall to check the updated return values + // The `getValue` calls should return the last value we set in the batched broadcast + // The `lastSender` calls should return the address of the Multicall contract, as it is + // the one acting as proxy and calling our SimpleStorage contracts (msg.sender) + let return_data: (String, (String, Address), Address, Address) = + multicall.call().await.unwrap(); + + assert_eq!(return_data.0, "first reset again"); + assert_eq!((return_data.1).0, "second reset again"); + assert_eq!((return_data.1).1, multicall_contract.address()); + assert_eq!(return_data.2, multicall_contract.address()); + assert_eq!(return_data.3, multicall_contract.address()); + + let addrs = anvil.addresses(); + // query ETH balances of multiple addresses + // these keys haven't been used to do any tx + // so should have 100 ETH + multicall + .clear_calls() + .add_get_eth_balance(addrs[4], false) + .add_get_eth_balance(addrs[5], false) + .add_get_eth_balance(addrs[6], false); + + let valid_balances = [ + U256::from(10_000_000_000_000_000_000_000u128), + U256::from(10_000_000_000_000_000_000_000u128), + U256::from(10_000_000_000_000_000_000_000u128), + ]; + + let balances: (U256, U256, U256) = multicall.call().await.unwrap(); + assert_eq!(balances.0, valid_balances[0]); + assert_eq!(balances.1, valid_balances[1]); + assert_eq!(balances.2, valid_balances[2]); + + // call_array + multicall + .clear_calls() + .add_get_eth_balance(addrs[4], false) + .add_get_eth_balance(addrs[5], false) + .add_get_eth_balance(addrs[6], false); + + let balances: Vec = multicall.call_array().await.unwrap(); + assert_eq!(balances, Vec::from_iter(valid_balances.iter().copied())); + + // clear multicall so we can test `call_raw` w/ >16 calls + multicall.clear_calls(); + + // clear the current value + simple_contract + .connect(client2.clone()) + .method::<_, H256>("setValue", "many".to_owned()) + .unwrap() + .legacy() + .send() + .await + .unwrap(); + + multicall.add_calls( + false, + std::iter::repeat(simple_contract.method::<_, String>("getValue", ()).unwrap()).take(17), + ); + + let tokens = multicall.call_raw().await.unwrap(); + let results: Vec = tokens + .into_iter() + .map(|result| { + // decode manually using Tokenizable method + String::from_token(result.unwrap()).unwrap() + }) + .collect(); + assert_eq!(results, ["many"; 17]); + + // test version 2 + multicall = multicall.version(MulticallVersion::Multicall2); + + // deploy contract with reverting methods + let reverting_contract = { + let (abi, bytecode) = + compile_contract("SimpleRevertingStorage", "SimpleRevertingStorage.sol"); + let f = ContractFactory::new(abi, bytecode, client.clone()); + f.deploy("This contract can revert".to_string()).unwrap().send().await.unwrap() + }; + + // reset value + reverting_contract + .connect(client2.clone()) + .method::<_, H256>("setValue", ("reset third".to_owned(), false)) + .unwrap() + .send() + .await + .unwrap(); + + // create calls + let set_value_call = reverting_contract + .connect(client.clone()) + .method::<_, H256>("setValue", ("this didn't revert".to_owned(), false)) + .unwrap(); + let set_value_reverting_call = reverting_contract + .connect(client3.clone()) + .method::<_, H256>("setValue", ("this reverted".to_owned(), true)) + .unwrap(); + let get_value_call = + reverting_contract.connect(client2.clone()).method::<_, String>("getValue", false).unwrap(); + let get_value_reverting_call = + reverting_contract.connect(client.clone()).method::<_, String>("getValue", true).unwrap(); + + // .send reverts + // don't allow revert + multicall + .clear_calls() + .add_call(set_value_reverting_call.clone(), false) + .add_call(set_value_call.clone(), false); + multicall.send().await.unwrap_err(); + + // value has not changed + assert_eq!(get_value_call.clone().call().await.unwrap(), "reset third"); + + // allow revert + multicall + .clear_calls() + .add_call(set_value_reverting_call.clone(), true) + .add_call(set_value_call.clone(), false); + multicall.send().await.unwrap(); + + // value has changed + assert_eq!(get_value_call.clone().call().await.unwrap(), "this didn't revert"); + + // reset value again + reverting_contract + .connect(client2.clone()) + .method::<_, H256>("setValue", ("reset third again".to_owned(), false)) + .unwrap() + .send() + .await + .unwrap(); + + // .call reverts + // don't allow revert + multicall + .clear_calls() + .add_call(get_value_reverting_call.clone(), false) + .add_call(get_value_call.clone(), false); + let res = multicall.call::<(String, String)>().await; + let err = res.unwrap_err(); + + assert!(err.is_revert()); + let message = err.decode_revert::().unwrap(); + assert!(message.contains("Multicall3: call failed")); + + // allow revert -> call doesn't revert, but returns Err(_) in raw tokens + let expected = Bytes::from_static(b"getValue revert").encode(); + multicall.clear_calls().add_call(get_value_reverting_call.clone(), true); + assert_eq!(multicall.call_raw().await.unwrap()[0].as_ref().unwrap_err()[4..], expected[..]); + assert_eq!( + multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..], + expected[..] + ); + + // v2 illegal revert + multicall + .clear_calls() + .add_call(get_value_reverting_call.clone(), false) // don't allow revert + .add_call(get_value_call.clone(), true); // true here will result in `tryAggregate(false, ...)` + assert!(matches!( + multicall.call::<(String, String)>().await.unwrap_err(), + MulticallError::IllegalRevert + )); + + // test version 3 + // aggregate3 is the same as try_aggregate except with allowing failure on a per-call basis. + // no need to test that + multicall = multicall.version(MulticallVersion::Multicall3); + + // .send with value + let amount = U256::from(100); + let value_tx = reverting_contract.method::<_, H256>("deposit", ()).unwrap().value(amount); + let rc_addr = reverting_contract.address(); + + let (bal_before,): (U256,) = + multicall.clear_calls().add_get_eth_balance(rc_addr, false).call().await.unwrap(); + + // send 2 value_tx + multicall.clear_calls().add_call(value_tx.clone(), false).add_call(value_tx.clone(), false); + multicall.send().await.unwrap(); + + let (bal_after,): (U256,) = + multicall.clear_calls().add_get_eth_balance(rc_addr, false).call().await.unwrap(); + + assert_eq!(bal_after, bal_before + U256::from(2) * amount); + + // test specific revert cases + // empty revert + let empty_revert = reverting_contract.method::<_, H256>("emptyRevert", ()).unwrap(); + multicall.clear_calls().add_call(empty_revert.clone(), true); + assert!(multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap().is_empty()); + + // string revert + let string_revert = + reverting_contract.method::<_, H256>("stringRevert", "String".to_string()).unwrap(); + multicall.clear_calls().add_call(string_revert, true); + assert_eq!( + multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..], + Bytes::from_static(b"String").encode()[..] + ); + + // custom error revert + let custom_error = reverting_contract.method::<_, H256>("customError", ()).unwrap(); + multicall.clear_calls().add_call(custom_error, true); + assert_eq!( + multicall.call::<(Bytes,)>().await.unwrap_err().as_revert().unwrap()[..], + keccak256("CustomError()")[..4] + ); + + // custom error with data revert + let custom_error_with_data = + reverting_contract.method::<_, H256>("customErrorWithData", "Data".to_string()).unwrap(); + multicall.clear_calls().add_call(custom_error_with_data, true); + let err = multicall.call::<(Bytes,)>().await.unwrap_err(); + let bytes = err.as_revert().unwrap(); + assert_eq!(bytes[..4], keccak256("CustomErrorWithData(string)")[..4]); + assert_eq!(bytes[4..], encode(&[Token::String("Data".to_string())])); +} + +#[tokio::test] +async fn test_derive_eip712() { + // Generate Contract ABI Bindings + abigen!( + DeriveEip712Test, + "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json", + event_derives(serde::Deserialize, serde::Serialize) + ); + + // Create derived structs + + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "Eip712Test", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000001", + salt = "eip712-test-75F0CCte" + )] + struct FooBar { + foo: I256, + bar: U256, + fizz: Bytes, + buzz: [u8; 32], + far: String, + out: Address, + } + + // get ABI and bytecode for the DeriveEip712Test contract + let (abi, bytecode) = compile_contract("DeriveEip712Test", "DeriveEip712Test.sol"); + + // launch the network & connect to it + let anvil = Anvil::new().spawn(); + let from = anvil.addresses()[0]; + let provider = Provider::try_from(anvil.endpoint()) + .unwrap() + .with_sender(from) + .interval(std::time::Duration::from_millis(10)); + let client = Arc::new(provider); + + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + + let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone()); + + let contract = factory + .deploy(()) + .expect("failed to deploy DeriveEip712Test contract") + .legacy() + .send() + .await + .expect("failed to instantiate factory for DeriveEip712 contract"); + + let addr = contract.address(); + + let contract = DeriveEip712Test::new(addr, client.clone()); + + let foo_bar = FooBar { + foo: I256::from(10u64), + bar: U256::from(20u64), + fizz: b"fizz".into(), + buzz: keccak256("buzz"), + far: String::from("space"), + out: Address::from([0; 20]), + }; + + let derived_foo_bar = derive_eip_712_test::FooBar { + foo: foo_bar.foo, + bar: foo_bar.bar, + fizz: foo_bar.fizz.clone(), + buzz: foo_bar.buzz, + far: foo_bar.far.clone(), + out: foo_bar.out, + }; + + let sig = wallet.sign_typed_data(&foo_bar).await.expect("failed to sign typed data"); + + let r = <[u8; 32]>::try_from(sig.r) + .expect("failed to parse 'r' value from signature into [u8; 32]"); + let s = <[u8; 32]>::try_from(sig.s) + .expect("failed to parse 's' value from signature into [u8; 32]"); + let v = u8::try_from(sig.v).expect("failed to parse 'v' value from signature into u8"); + + let domain_separator = contract + .domain_separator() + .call() + .await + .expect("failed to retrieve domain_separator from contract"); + let type_hash = + contract.type_hash().call().await.expect("failed to retrieve type_hash from contract"); + let struct_hash = contract + .struct_hash(derived_foo_bar.clone()) + .call() + .await + .expect("failed to retrieve struct_hash from contract"); + let encoded = contract + .encode_eip_712(derived_foo_bar.clone()) + .call() + .await + .expect("failed to retrieve eip712 encoded hash from contract"); + let verify = contract + .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v) + .call() + .await + .expect("failed to verify signed typed data eip712 payload"); + + assert_eq!( + domain_separator, + foo_bar + .domain() + .expect("failed to return domain_separator from Eip712 implemented struct") + .separator(), + "domain separator does not match contract domain separator!" + ); + + assert_eq!( + type_hash, + FooBar::type_hash().expect("failed to return type_hash from Eip712 implemented struct"), + "type hash does not match contract struct type hash!" + ); + + assert_eq!( + struct_hash, + foo_bar + .clone() + .struct_hash() + .expect("failed to return struct_hash from Eip712 implemented struct"), + "struct hash does not match contract struct hash!" + ); + + assert_eq!( + encoded, + foo_bar + .encode_eip712() + .expect("failed to return domain_separator from Eip712 implemented struct"), + "Encoded value does not match!" + ); + + assert!(verify, "typed data signature failed!"); +} diff --git a/ethers-contract/tests/it/contract_call.rs b/ethers-contract/tests/it/contract_call.rs index 29fbd9d2..878d7064 100644 --- a/ethers-contract/tests/it/contract_call.rs +++ b/ethers-contract/tests/it/contract_call.rs @@ -6,9 +6,8 @@ use std::{ sync::Arc, }; -#[tokio::test] -async fn contract_call_into_future_is_send() { - abigen!(DsProxyFactory, "ethers-middleware/contracts/DsProxyFactory.json"); +fn _contract_call_into_future_is_send() { + abigen!(DsProxyFactory, "./../ethers-middleware/contracts/DSProxyFactory.json"); let (provider, _) = Provider::mocked(); let client = Arc::new(provider); let contract = DsProxyFactory::new(Address::zero(), client); diff --git a/ethers-contract/tests/it/common/derive.rs b/ethers-contract/tests/it/derive.rs similarity index 99% rename from ethers-contract/tests/it/common/derive.rs rename to ethers-contract/tests/it/derive.rs index 50a73adf..aa0bd1d0 100644 --- a/ethers-contract/tests/it/common/derive.rs +++ b/ethers-contract/tests/it/derive.rs @@ -261,8 +261,9 @@ fn can_derive_indexed_and_anonymous_attribute() { #[test] fn can_generate_ethevent_from_json() { - abigen!(DsProxyFactory, - "ethers-middleware/contracts/DsProxyFactory.json", + abigen!( + DsProxyFactory, + "./../ethers-middleware/contracts/DSProxyFactory.json", methods { build(address) as build_with_owner; } @@ -399,7 +400,7 @@ fn eth_display_works() { #[test] fn eth_display_works_for_human_readable() { - ethers_contract::abigen!( + abigen!( HevmConsole, r#"[ event log(string) diff --git a/ethers-contract/tests/it/main.rs b/ethers-contract/tests/it/main.rs index bdf63ef0..135d053a 100644 --- a/ethers-contract/tests/it/main.rs +++ b/ethers-contract/tests/it/main.rs @@ -1,10 +1,14 @@ -// #![allow(clippy::extra_unused_type_parameters)] +#![allow(clippy::extra_unused_type_parameters)] +#![cfg(feature = "abigen")] -#[cfg(feature = "abigen")] mod abigen; -pub(crate) mod common; -#[cfg(feature = "abigen")] -mod contract; +mod derive; mod contract_call; + +#[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] +mod common; + +#[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] +mod contract; diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index 1f821e9f..7018f1dc 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -1,3 +1,5 @@ +#![allow(clippy::extra_unused_type_parameters)] + use ethers_contract_derive::EthAbiType; use ethers_core::{ types::{ diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs index 269d5fdb..e0a8ae53 100644 --- a/ethers-core/src/types/chain.rs +++ b/ethers-core/src/types/chain.rs @@ -252,8 +252,8 @@ impl Chain { Dev | AnvilHardhat => 200, Celo | CeloAlfajores | CeloBaklava => 5_000, FilecoinHyperspaceTestnet | FilecoinMainnet => 30_000, - // Explictly handle all network to make it easier not to forget this match when new - // networks are added. + + // Explicitly exhaustive. See NB above. Morden | Ropsten | Rinkeby | Goerli | Kovan | XDai | Chiado | Sepolia | Moonbase | MoonbeamDev | Optimism | OptimismGoerli | OptimismKovan | Poa | Sokol | Rsk | EmeraldTestnet => return None, diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs index 474e7777..4f430e5a 100644 --- a/ethers-core/src/types/transaction/eip2930.rs +++ b/ethers-core/src/types/transaction/eip2930.rs @@ -95,6 +95,7 @@ pub enum Eip2930RequestError { pub struct Eip2930TransactionRequest { #[serde(flatten)] pub tx: TransactionRequest, + #[serde(rename = "accessList")] pub access_list: AccessList, } diff --git a/ethers-core/src/utils/anvil.rs b/ethers-core/src/utils/anvil.rs index b4d12ee1..b0194275 100644 --- a/ethers-core/src/utils/anvil.rs +++ b/ethers-core/src/utils/anvil.rs @@ -1,6 +1,6 @@ use crate::{ types::{Address, Chain}, - utils::{secret_key_to_address, unused_port}, + utils::{secret_key_to_address, unused_ports}, }; use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; use std::{ @@ -161,7 +161,7 @@ impl Anvil { self } - /// Sets the block-time which will be used when the `anvil` instance is launched. + /// Sets the block-time in seconds which will be used when the `anvil` instance is launched. #[must_use] pub fn block_time>(mut self, block_time: T) -> Self { self.block_time = Some(block_time.into()); @@ -223,7 +223,7 @@ impl Anvil { Command::new("anvil") }; cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit()); - let port = if let Some(port) = self.port { port } else { unused_port() }; + let port = if let Some(port) = self.port { port } else { unused_ports::<1>()[0] }; cmd.arg("-p").arg(port.to_string()); if let Some(mnemonic) = self.mnemonic { diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs index d78691ed..0a4f4e11 100644 --- a/ethers-core/src/utils/ganache.rs +++ b/ethers-core/src/utils/ganache.rs @@ -1,6 +1,6 @@ use crate::{ types::Address, - utils::{secret_key_to_address, unused_port}, + utils::{secret_key_to_address, unused_ports}, }; use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; use std::{ @@ -157,7 +157,7 @@ impl Ganache { pub fn spawn(self) -> GanacheInstance { let mut cmd = Command::new("ganache-cli"); cmd.stdout(std::process::Stdio::piped()); - let port = if let Some(port) = self.port { port } else { unused_port() }; + let port = if let Some(port) = self.port { port } else { unused_ports::<1>()[0] }; cmd.arg("-p").arg(port.to_string()); if let Some(mnemonic) = self.mnemonic { diff --git a/ethers-core/src/utils/geth.rs b/ethers-core/src/utils/geth.rs index a5ce02e9..f16ed008 100644 --- a/ethers-core/src/utils/geth.rs +++ b/ethers-core/src/utils/geth.rs @@ -1,10 +1,9 @@ -use k256::ecdsa::SigningKey; - -use super::{unused_port, Genesis}; +use super::{unused_ports, CliqueConfig, Genesis}; use crate::{ types::{Bytes, H256}, utils::secret_key_to_address, }; +use k256::ecdsa::SigningKey; use std::{ fs::{create_dir, File}, io::{BufRead, BufReader}, @@ -15,10 +14,10 @@ use std::{ use tempfile::tempdir; /// How long we will wait for geth to indicate that it is ready. -const GETH_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; +const GETH_STARTUP_TIMEOUT: Duration = Duration::from_secs(10); /// Timeout for waiting for geth to add a peer. -const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::new(20, 0); +const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::from_secs(20); /// The exposed APIs const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug"; @@ -354,13 +353,21 @@ impl Geth { self } - /// Consumes the builder and spawns `geth` with stdout redirected - /// to /dev/null. + /// Consumes the builder and spawns `geth` with stdout redirected to /dev/null. + #[must_use] + #[track_caller] pub fn spawn(mut self) -> GethInstance { - let mut cmd = - if let Some(ref prg) = self.program { Command::new(prg) } else { Command::new(GETH) }; + let bin_path = match self.program.as_ref() { + Some(bin) => bin.as_os_str(), + None => GETH.as_ref(), + } + .to_os_string(); + let mut cmd = Command::new(&bin_path); // geth uses stderr for its logs cmd.stderr(Stdio::piped()); + + let mut unused_ports = unused_ports::<3>().into_iter(); + let mut unused_port = || unused_ports.next().unwrap(); let port = if let Some(port) = self.port { port } else { unused_port() }; let authrpc_port = if let Some(port) = self.authrpc_port { port } else { unused_port() }; @@ -390,7 +397,6 @@ impl Geth { // use geth init to initialize the datadir if the genesis exists if let Some(ref mut genesis) = self.genesis { if is_clique { - use super::CliqueConfig; // set up a clique config with an instant sealing period and short (8 block) epoch let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) }; genesis.config.clique = Some(clique_config); @@ -440,7 +446,7 @@ impl Geth { serde_json::to_writer_pretty(&mut file, &genesis) .expect("could not write genesis to file"); - let mut init_cmd = Command::new(GETH); + let mut init_cmd = Command::new(bin_path); if let Some(ref data_dir) = self.data_dir { init_cmd.arg("--datadir").arg(data_dir); } @@ -514,11 +520,11 @@ impl Geth { let mut http_started = false; loop { - if start + Duration::from_millis(GETH_STARTUP_TIMEOUT_MILLIS) <= Instant::now() { + if start + GETH_STARTUP_TIMEOUT <= Instant::now() { panic!("Timed out waiting for geth to start. Is geth installed?") } - let mut line = String::new(); + let mut line = String::with_capacity(120); reader.read_line(&mut line).expect("Failed to read line from geth process"); if matches!(self.mode, GethMode::NonDev(_)) && line.contains("Started P2P networking") { diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs index 7f1e724d..cb35b893 100644 --- a/ethers-core/src/utils/mod.rs +++ b/ethers-core/src/utils/mod.rs @@ -624,18 +624,24 @@ fn base_fee_surged(base_fee_per_gas: U256) -> U256 { } } -/// A bit of hack to find an unused TCP port. +/// A bit of hack to find unused TCP ports. /// /// Does not guarantee that the given port is unused after the function exists, just that it was /// unused before the function started (i.e., it does not reserve a port). #[cfg(not(target_arch = "wasm32"))] -pub(crate) fn unused_port() -> u16 { - let listener = std::net::TcpListener::bind("127.0.0.1:0") - .expect("Failed to create TCP listener to find unused port"); +pub(crate) fn unused_ports() -> [u16; N] { + use std::net::{SocketAddr, TcpListener}; - let local_addr = - listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port"); - local_addr.port() + std::array::from_fn(|_| { + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + TcpListener::bind(addr).expect("Failed to create TCP listener to find unused port") + }) + .map(|listener| { + listener + .local_addr() + .expect("Failed to read TCP listener local_addr to find unused port") + .port() + }) } #[cfg(test)] diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml index 5e6c724e..827257b1 100644 --- a/ethers-etherscan/Cargo.toml +++ b/ethers-etherscan/Cargo.toml @@ -33,11 +33,12 @@ semver = "1.0.16" getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] +ethers-solc = { version = "^1.0.0", path = "../ethers-solc", default-features = false } + tempfile = "3.4.0" tokio = { version = "1.18", features = ["macros", "rt-multi-thread", "time"] } -serial_test = "0.10.0" +serial_test = "1.0.0" tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] } -ethers-solc = { version = "^1.0.0", path = "../ethers-solc", default-features = false } [package.metadata.docs.rs] all-features = true diff --git a/ethers-etherscan/src/account.rs b/ethers-etherscan/src/account.rs index 92965031..8a51813c 100644 --- a/ethers-etherscan/src/account.rs +++ b/ethers-etherscan/src/account.rs @@ -733,202 +733,3 @@ impl Client { Ok(response.result) } } - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use serial_test::serial; - - use crate::{tests::run_at_least_duration, Chain}; - - use super::*; - - #[tokio::test] - #[serial] - async fn get_ether_balance_single_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let balance = client - .get_ether_balance_single( - &"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(), - None, - ) - .await; - balance.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_ether_balance_multi_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let balances = client - .get_ether_balance_multi( - &[&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap()], - None, - ) - .await; - assert!(balances.is_ok()); - let balances = balances.unwrap(); - assert_eq!(balances.len(), 1); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_transactions_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let txs = client - .get_transactions( - &"0x4F26FfBe5F04ED43630fdC30A87638d53D0b0876".parse().unwrap(), - None, - ) - .await; - txs.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_internal_transactions_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let txs = client - .get_internal_transactions( - InternalTxQueryOption::ByAddress( - "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse().unwrap(), - ), - None, - ) - .await; - txs.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_internal_transactions_by_tx_hash_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let txs = client - .get_internal_transactions( - InternalTxQueryOption::ByTransactionHash( - "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" - .parse() - .unwrap(), - ), - None, - ) - .await; - txs.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_erc20_transfer_events_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let txs = client - .get_erc20_token_transfer_events( - TokenQueryOption::ByAddress( - "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse().unwrap(), - ), - None, - ) - .await - .unwrap(); - let tx = txs.get(0).unwrap(); - assert_eq!(tx.gas_used, 93657u64.into()); - assert_eq!(tx.nonce, 10u64.into()); - assert_eq!(tx.block_number, 2228258u64.into()); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_erc721_transfer_events_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let txs = client - .get_erc721_token_transfer_events( - TokenQueryOption::ByAddressAndContract( - "0x6975be450864c02b4613023c2152ee0743572325".parse().unwrap(), - "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(), - ), - None, - ) - .await; - txs.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_erc1155_transfer_events_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let txs = client - .get_erc1155_token_transfer_events( - TokenQueryOption::ByAddressAndContract( - "0x216CD350a4044e7016f14936663e2880Dd2A39d7".parse().unwrap(), - "0x495f947276749ce646f68ac8c248420045cb7b5e".parse().unwrap(), - ), - None, - ) - .await; - txs.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_mined_blocks_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let blocks = client - .get_mined_blocks( - &"0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse().unwrap(), - None, - None, - ) - .await; - blocks.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn get_avalanche_transactions() { - if std::env::var("SNOWTRACE_API_KEY").is_err() { - // nothing to do if api key unset - return - } - let client = Client::new_from_env(Chain::Avalanche).unwrap(); - let txs = client - .get_transactions(&"0x1549ea9b546ba9ffb306d78a1e1f304760cc4abf".parse().unwrap(), None) - .await; - txs.unwrap(); - } -} diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs index 4e80bbf8..edbfdb38 100644 --- a/ethers-etherscan/src/contract.rs +++ b/ethers-etherscan/src/contract.rs @@ -406,137 +406,3 @@ impl Client { Ok(result) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::run_at_least_duration; - use ethers_core::types::Chain; - use serial_test::serial; - use std::time::Duration; - - /// Abi of [0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413](https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413). - const DAO_ABI: &str = "[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"proposals\",\"outputs\":[{\"name\":\"recipient\",\"type\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"votingDeadline\",\"type\":\"uint256\"},{\"name\":\"open\",\"type\":\"bool\"},{\"name\":\"proposalPassed\",\"type\":\"bool\"},{\"name\":\"proposalHash\",\"type\":\"bytes32\"},{\"name\":\"proposalDeposit\",\"type\":\"uint256\"},{\"name\":\"newCurator\",\"type\":\"bool\"},{\"name\":\"yea\",\"type\":\"uint256\"},{\"name\":\"nay\",\"type\":\"uint256\"},{\"name\":\"creator\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_spender\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minTokensToCreate\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"rewardAccount\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"daoCreator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"divisor\",\"outputs\":[{\"name\":\"divisor\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"extraBalance\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"}],\"name\":\"executeProposal\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"unblockMe\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalRewardToken\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"actualBalance\",\"outputs\":[{\"name\":\"_actualBalance\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"closingTime\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowedRecipients\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferWithoutReward\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"refund\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"},{\"name\":\"_description\",\"type\":\"string\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"},{\"name\":\"_debatingPeriod\",\"type\":\"uint256\"},{\"name\":\"_newCurator\",\"type\":\"bool\"}],\"name\":\"newProposal\",\"outputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"DAOpaidOut\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minQuorumDivisor\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newContract\",\"type\":\"address\"}],\"name\":\"newContract\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"balance\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_allowed\",\"type\":\"bool\"}],\"name\":\"changeAllowedRecipients\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"halveMinQuorum\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"paidOut\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_newCurator\",\"type\":\"address\"}],\"name\":\"splitDAO\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"DAOrewardAccount\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"proposalDeposit\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"numberOfProposals\",\"outputs\":[{\"name\":\"_numberOfProposals\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"lastTimeMinQuorumMet\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_toMembers\",\"type\":\"bool\"}],\"name\":\"retrieveDAOReward\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"receiveEther\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"isFueled\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_tokenHolder\",\"type\":\"address\"}],\"name\":\"createTokenProxy\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"}],\"name\":\"getNewDAOAddress\",\"outputs\":[{\"name\":\"_newDAO\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_supportsProposal\",\"type\":\"bool\"}],\"name\":\"vote\",\"outputs\":[{\"name\":\"_voteID\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"getMyReward\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"rewardToken\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFromWithoutReward\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"remaining\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalDeposit\",\"type\":\"uint256\"}],\"name\":\"changeProposalDeposit\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"blocked\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"curator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"}],\"name\":\"checkProposalCode\",\"outputs\":[{\"name\":\"_codeChecksOut\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"privateCreation\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"inputs\":[{\"name\":\"_curator\",\"type\":\"address\"},{\"name\":\"_daoCreator\",\"type\":\"address\"},{\"name\":\"_proposalDeposit\",\"type\":\"uint256\"},{\"name\":\"_minTokensToCreate\",\"type\":\"uint256\"},{\"name\":\"_closingTime\",\"type\":\"uint256\"},{\"name\":\"_privateCreation\",\"type\":\"address\"}],\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"FuelingToDate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"CreatedToken\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Refund\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"newCurator\",\"type\":\"bool\"},{\"indexed\":false,\"name\":\"description\",\"type\":\"string\"}],\"name\":\"ProposalAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"position\",\"type\":\"bool\"},{\"indexed\":true,\"name\":\"voter\",\"type\":\"address\"}],\"name\":\"Voted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"result\",\"type\":\"bool\"},{\"indexed\":false,\"name\":\"quorum\",\"type\":\"uint256\"}],\"name\":\"ProposalTallied\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_newCurator\",\"type\":\"address\"}],\"name\":\"NewCurator\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_recipient\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_allowed\",\"type\":\"bool\"}],\"name\":\"AllowedRecipientChanged\",\"type\":\"event\"}]"; - - #[allow(unused)] - fn init_tracing() { - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); - } - - #[tokio::test] - #[serial] - #[ignore] - async fn can_fetch_ftm_contract_abi() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Fantom).unwrap(); - - let _abi = client - .contract_abi("0x80AA7cb0006d5DDD91cce684229Ac6e398864606".parse().unwrap()) - .await - .unwrap(); - }) - .await; - } - - #[tokio::test] - #[serial] - #[ignore] - async fn can_fetch_contract_abi() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let abi = client - .contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) - .await - .unwrap(); - assert_eq!(abi, serde_json::from_str(DAO_ABI).unwrap()); - }) - .await; - } - - #[tokio::test] - #[serial] - #[ignore] - async fn can_fetch_contract_source_code() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let meta = client - .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) - .await - .unwrap(); - - assert_eq!(meta.items.len(), 1); - let item = &meta.items[0]; - assert!(matches!(item.source_code, SourceCodeMetadata::SourceCode(_))); - assert_eq!(item.source_code.sources().len(), 1); - assert_eq!(item.abi().unwrap(), serde_json::from_str(DAO_ABI).unwrap()); - }) - .await - } - - #[tokio::test] - #[serial] - #[ignore] - async fn can_get_error_on_unverified_contract() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - let addr = "0xb5c31a0e22cae98ac08233e512bd627885aa24e5".parse().unwrap(); - let err = client.contract_source_code(addr).await.unwrap_err(); - assert!(matches!(err, EtherscanError::ContractCodeNotVerified(_))); - }) - .await - } - - /// Query a contract that has a single string source entry instead of underlying JSON metadata. - #[tokio::test] - #[serial] - #[ignore] - async fn can_fetch_contract_source_tree_for_singleton_contract() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let meta = client - .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) - .await - .unwrap(); - - assert_eq!(meta.items.len(), 1); - let item = &meta.items[0]; - assert!(matches!(item.source_code, SourceCodeMetadata::SourceCode(_))); - assert_eq!(item.source_code.sources().len(), 1); - assert_eq!(item.abi().unwrap(), serde_json::from_str(DAO_ABI).unwrap()); - }) - .await - } - - /// Query a contract that has many source entries as JSON metadata and ensure they are - /// reflected. - #[tokio::test] - #[serial] - #[ignore] - async fn can_fetch_contract_source_tree_for_multi_entry_contract() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let meta = client - .contract_source_code("0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63".parse().unwrap()) - .await - .unwrap(); - - assert_eq!(meta.items.len(), 1); - assert!(matches!(meta.items[0].source_code, SourceCodeMetadata::Metadata { .. })); - let source_tree = meta.source_tree(); - assert_eq!(source_tree.entries.len(), 15); - }) - .await - } -} diff --git a/ethers-etherscan/src/gas.rs b/ethers-etherscan/src/gas.rs index da573ffc..11fd311d 100644 --- a/ethers-etherscan/src/gas.rs +++ b/ethers-etherscan/src/gas.rs @@ -67,60 +67,3 @@ impl Client { Ok(response.result) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::run_at_least_duration; - use ethers_core::types::Chain; - use serial_test::serial; - use std::time::Duration; - - #[tokio::test] - #[serial] - async fn gas_estimate_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let result = client.gas_estimate(2000000000u32.into()).await; - - result.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn gas_estimate_error() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let err = client.gas_estimate(7123189371829732819379218u128.into()).await.unwrap_err(); - - assert!(matches!(err, EtherscanError::GasEstimationFailed)); - }) - .await - } - - #[tokio::test] - #[serial] - async fn gas_oracle_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let result = client.gas_oracle().await; - - assert!(result.is_ok()); - - let oracle = result.unwrap(); - - assert!(oracle.safe_gas_price > 0); - assert!(oracle.propose_gas_price > 0); - assert!(oracle.fast_gas_price > 0); - assert!(oracle.last_block > 0); - assert!(oracle.suggested_base_fee > 0.0); - assert!(!oracle.gas_used_ratio.is_empty()); - }) - .await - } -} diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs index d6fd450c..99b3397b 100644 --- a/ethers-etherscan/src/lib.rs +++ b/ethers-etherscan/src/lib.rs @@ -108,6 +108,20 @@ impl Client { Self::new(chain, api_key) } + /// Create a new client with the correct endpoints based on the chain and API key + /// from the default environment variable defined in [`Chain`]. + /// + /// If the environment variable is not set, create a new client without it. + pub fn new_from_opt_env(chain: Chain) -> Result { + match Self::new_from_env(chain) { + Ok(client) => Ok(client), + Err(EtherscanError::EnvVarNotFound(_)) => { + Self::builder().chain(chain).and_then(|c| c.build()) + } + Err(e) => Err(e), + } + } + /// Sets the root to the cache dir and the ttl to use pub fn set_cache(&mut self, root: impl Into, ttl: Duration) -> &mut Self { self.cache = Some(Cache { root: root.into(), ttl }); @@ -305,7 +319,8 @@ impl ClientBuilder { /// Returns a Client that uses this ClientBuilder configuration. /// /// # Errors - /// if required fields are missing: + /// + /// If the following required fields are missing: /// - `etherscan_api_url` /// - `etherscan_url` pub fn build(self) -> Result { @@ -448,10 +463,6 @@ fn into_url(url: impl IntoUrl) -> std::result::Result { mod tests { use crate::{Client, EtherscanError}; use ethers_core::types::{Address, Chain, H256}; - use std::{ - future::Future, - time::{Duration, SystemTime}, - }; #[test] fn test_api_paths() { @@ -461,17 +472,9 @@ mod tests { assert_eq!(client.block_url(100), "https://goerli.etherscan.io/block/100"); } - #[test] - fn chain_not_supported() { - let err = Client::new_from_env(Chain::Morden).unwrap_err(); - - assert!(matches!(err, EtherscanError::ChainNotSupported(_))); - assert_eq!(err.to_string(), "Chain morden not supported"); - } - #[test] fn stringifies_block_url() { - let etherscan = Client::new_from_env(Chain::Mainnet).unwrap(); + let etherscan = Client::new(Chain::Mainnet, "").unwrap(); let block: u64 = 1; let block_url: String = etherscan.block_url(block); assert_eq!(block_url, format!("https://etherscan.io/block/{block}")); @@ -479,7 +482,7 @@ mod tests { #[test] fn stringifies_address_url() { - let etherscan = Client::new_from_env(Chain::Mainnet).unwrap(); + let etherscan = Client::new(Chain::Mainnet, "").unwrap(); let addr: Address = Address::zero(); let address_url: String = etherscan.address_url(addr); assert_eq!(address_url, format!("https://etherscan.io/address/{addr:?}")); @@ -487,7 +490,7 @@ mod tests { #[test] fn stringifies_transaction_url() { - let etherscan = Client::new_from_env(Chain::Mainnet).unwrap(); + let etherscan = Client::new(Chain::Mainnet, "").unwrap(); let tx_hash = H256::zero(); let tx_url: String = etherscan.transaction_url(tx_hash); assert_eq!(tx_url, format!("https://etherscan.io/tx/{tx_hash:?}")); @@ -495,7 +498,7 @@ mod tests { #[test] fn stringifies_token_url() { - let etherscan = Client::new_from_env(Chain::Mainnet).unwrap(); + let etherscan = Client::new(Chain::Mainnet, "").unwrap(); let token_hash = Address::zero(); let token_url: String = etherscan.token_url(token_hash); assert_eq!(token_url, format!("https://etherscan.io/token/{token_hash:?}")); @@ -506,23 +509,4 @@ mod tests { let err = Client::new_from_env(Chain::Dev).unwrap_err(); assert!(matches!(err, EtherscanError::LocalNetworksNotSupported)); } - - #[tokio::test] - async fn check_wrong_etherscan_api_key() { - let client = Client::new(Chain::Mainnet, "ABCDEFG").unwrap(); - let resp = client - .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) - .await - .unwrap_err(); - - assert!(matches!(resp, EtherscanError::InvalidApiKey)); - } - - pub async fn run_at_least_duration(duration: Duration, block: impl Future) { - let start = SystemTime::now(); - block.await; - if let Some(sleep) = duration.checked_sub(start.elapsed().unwrap()) { - tokio::time::sleep(sleep).await; - } - } } diff --git a/ethers-etherscan/src/transaction.rs b/ethers-etherscan/src/transaction.rs index c54c71da..1ae76b58 100644 --- a/ethers-etherscan/src/transaction.rs +++ b/ethers-etherscan/src/transaction.rs @@ -49,82 +49,3 @@ impl Client { } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{tests::run_at_least_duration, Chain}; - use serial_test::serial; - use std::time::Duration; - - #[tokio::test] - #[serial] - async fn check_contract_execution_status_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let status = client - .check_contract_execution_status( - "0x16197e2a0eacc44c1ebdfddcfcfcafb3538de557c759a66e0ba95263b23d9007", - ) - .await; - - status.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn check_contract_execution_status_error() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let err = client - .check_contract_execution_status( - "0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a", - ) - .await - .unwrap_err(); - - assert!(matches!(err, EtherscanError::ExecutionFailed(_))); - assert_eq!(err.to_string(), "Contract execution call failed: Bad jump destination"); - }) - .await - } - - #[tokio::test] - #[serial] - async fn check_transaction_receipt_status_success() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let success = client - .check_transaction_receipt_status( - "0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76", - ) - .await; - - success.unwrap(); - }) - .await - } - - #[tokio::test] - #[serial] - async fn check_transaction_receipt_status_failed() { - run_at_least_duration(Duration::from_millis(250), async { - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let err = client - .check_transaction_receipt_status( - "0x21a29a497cb5d4bf514c0cca8d9235844bd0215c8fab8607217546a892fd0758", - ) - .await - .unwrap_err(); - - assert!(matches!(err, EtherscanError::TransactionReceiptFailed)); - }) - .await - } -} diff --git a/ethers-etherscan/src/utils.rs b/ethers-etherscan/src/utils.rs index a587fcd7..b285998b 100644 --- a/ethers-etherscan/src/utils.rs +++ b/ethers-etherscan/src/utils.rs @@ -70,36 +70,7 @@ pub fn deserialize_source_code<'de, D: Deserializer<'de>>( #[cfg(test)] mod tests { use super::*; - use crate::{contract::SourceCodeLanguage, tests::run_at_least_duration}; - use semver::{BuildMetadata, Prerelease}; - use serial_test::serial; - use std::time::Duration; - - #[tokio::test] - #[serial] - async fn can_lookup_compiler_version_build_metadata() { - run_at_least_duration(Duration::from_millis(250), async { - let v = Version::new(0, 8, 13); - let version = lookup_compiler_version(&v).await.unwrap(); - assert_eq!(v.major, version.major); - assert_eq!(v.minor, version.minor); - assert_eq!(v.patch, version.patch); - assert_ne!(version.build, BuildMetadata::EMPTY); - assert_eq!(version.pre, Prerelease::EMPTY); - }) - .await - } - - #[tokio::test] - #[serial] - async fn errors_on_invalid_solc() { - run_at_least_duration(Duration::from_millis(250), async { - let v = Version::new(100, 0, 0); - let err = lookup_compiler_version(&v).await.unwrap_err(); - assert!(matches!(err, EtherscanError::MissingSolcVersion(_))); - }) - .await - } + use crate::contract::SourceCodeLanguage; #[test] fn can_deserialize_address_opt() { diff --git a/ethers-etherscan/src/verify.rs b/ethers-etherscan/src/verify.rs index ebe15255..e76b4eca 100644 --- a/ethers-etherscan/src/verify.rs +++ b/ethers-etherscan/src/verify.rs @@ -153,55 +153,3 @@ impl Client { self.post_form(&body).await } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{tests::run_at_least_duration, Client}; - use ethers_core::types::Chain; - use ethers_solc::{Project, ProjectPathsConfig}; - use serial_test::serial; - use std::{path::PathBuf, time::Duration}; - - #[allow(unused)] - fn init_tracing() { - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); - } - - #[tokio::test] - #[serial] - #[ignore] - async fn can_flatten_and_verify_contract() { - init_tracing(); - run_at_least_duration(Duration::from_millis(250), async { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources"); - let paths = ProjectPathsConfig::builder() - .sources(&root) - .build() - .expect("failed to resolve project paths"); - let project = Project::builder() - .paths(paths) - .build() - .expect("failed to build the project"); - - let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33".parse().unwrap(); - let compiler_version = "v0.5.17+commit.d19bba13"; - let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000"; - let contract = project.flatten(&root.join("UniswapExchange.sol")).expect("failed to flatten contract"); - let contract_name = "UniswapExchange".to_owned(); - - let client = Client::new_from_env(Chain::Mainnet).unwrap(); - - let contract = - VerifyContract::new(address, contract_name, contract, compiler_version.to_string()) - .constructor_arguments(Some(constructor_args)) - .optimization(true) - .runs(200); - let resp = client.submit_contract_verification(&contract).await.expect("failed to send the request"); - assert_ne!(resp.result, "Error!"); // `Error!` result means that request was malformatted - }) - .await - } -} diff --git a/ethers-etherscan/tests/it/account.rs b/ethers-etherscan/tests/it/account.rs new file mode 100644 index 00000000..035ba365 --- /dev/null +++ b/ethers-etherscan/tests/it/account.rs @@ -0,0 +1,168 @@ +use crate::*; +use ethers_etherscan::account::{InternalTxQueryOption, TokenQueryOption}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn get_ether_balance_single_success() { + run_with_client(Chain::Mainnet, |client| async move { + let balance = client + .get_ether_balance_single( + &"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(), + None, + ) + .await; + balance.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_ether_balance_multi_success() { + run_with_client(Chain::Mainnet, |client| async move { + let balances = client + .get_ether_balance_multi( + &[&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap()], + None, + ) + .await; + assert!(balances.is_ok()); + let balances = balances.unwrap(); + assert_eq!(balances.len(), 1); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_transactions_success() { + run_with_client(Chain::Mainnet, |client| async move { + let txs = client + .get_transactions(&"0x4F26FfBe5F04ED43630fdC30A87638d53D0b0876".parse().unwrap(), None) + .await; + txs.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_internal_transactions_success() { + run_with_client(Chain::Mainnet, |client| async move { + let txs = client + .get_internal_transactions( + InternalTxQueryOption::ByAddress( + "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse().unwrap(), + ), + None, + ) + .await; + txs.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_internal_transactions_by_tx_hash_success() { + run_with_client(Chain::Mainnet, |client| async move { + let txs = client + .get_internal_transactions( + InternalTxQueryOption::ByTransactionHash( + "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" + .parse() + .unwrap(), + ), + None, + ) + .await; + txs.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_erc20_transfer_events_success() { + run_with_client(Chain::Mainnet, |client| async move { + let txs = client + .get_erc20_token_transfer_events( + TokenQueryOption::ByAddress( + "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse().unwrap(), + ), + None, + ) + .await + .unwrap(); + let tx = txs.get(0).unwrap(); + assert_eq!(tx.gas_used, 93657u64.into()); + assert_eq!(tx.nonce, 10u64.into()); + assert_eq!(tx.block_number, 2228258u64.into()); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_erc721_transfer_events_success() { + run_with_client(Chain::Mainnet, |client| async move { + let txs = client + .get_erc721_token_transfer_events( + TokenQueryOption::ByAddressAndContract( + "0x6975be450864c02b4613023c2152ee0743572325".parse().unwrap(), + "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(), + ), + None, + ) + .await; + txs.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_erc1155_transfer_events_success() { + run_with_client(Chain::Mainnet, |client| async move { + let txs = client + .get_erc1155_token_transfer_events( + TokenQueryOption::ByAddressAndContract( + "0x216CD350a4044e7016f14936663e2880Dd2A39d7".parse().unwrap(), + "0x495f947276749ce646f68ac8c248420045cb7b5e".parse().unwrap(), + ), + None, + ) + .await; + txs.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_mined_blocks_success() { + run_with_client(Chain::Mainnet, |client| async move { + client + .get_mined_blocks( + &"0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse().unwrap(), + None, + None, + ) + .await + .unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn get_avalanche_transactions() { + run_with_client(Chain::Avalanche, |client| async move { + let txs = client + .get_transactions(&"0x1549ea9b546ba9ffb306d78a1e1f304760cc4abf".parse().unwrap(), None) + .await; + txs.unwrap(); + }) + .await +} diff --git a/ethers-etherscan/tests/it/contract.rs b/ethers-etherscan/tests/it/contract.rs new file mode 100644 index 00000000..3ba30edc --- /dev/null +++ b/ethers-etherscan/tests/it/contract.rs @@ -0,0 +1,99 @@ +use crate::*; +use ethers_core::types::Chain; +use ethers_etherscan::contract::SourceCodeMetadata; +use serial_test::serial; + +/// Abi of [0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413](https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413). +const DAO_ABI: &str = include!("../../../tests/testdata/the_dao_abi.expr"); + +#[tokio::test] +#[serial] +async fn can_fetch_ftm_contract_abi() { + run_with_client(Chain::Fantom, |client| async move { + let _abi = client + .contract_abi("0x80AA7cb0006d5DDD91cce684229Ac6e398864606".parse().unwrap()) + .await + .unwrap(); + }) + .await; +} + +#[tokio::test] +#[serial] +async fn can_fetch_contract_abi() { + run_with_client(Chain::Mainnet, |client| async move { + let abi = client + .contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) + .await + .unwrap(); + assert_eq!(abi, serde_json::from_str(DAO_ABI).unwrap()); + }) + .await; +} + +#[tokio::test] +#[serial] +async fn can_fetch_contract_source_code() { + run_with_client(Chain::Mainnet, |client| async move { + let meta = client + .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) + .await + .unwrap(); + + assert_eq!(meta.items.len(), 1); + let item = &meta.items[0]; + assert!(matches!(item.source_code, SourceCodeMetadata::SourceCode(_))); + assert_eq!(item.source_code.sources().len(), 1); + assert_eq!(item.abi().unwrap(), serde_json::from_str(DAO_ABI).unwrap()); + }) + .await +} + +#[tokio::test] +#[serial] +async fn can_get_error_on_unverified_contract() { + init_tracing(); + run_with_client(Chain::Mainnet, |client| async move { + let addr = "0xb5c31a0e22cae98ac08233e512bd627885aa24e5".parse().unwrap(); + let err = client.contract_source_code(addr).await.unwrap_err(); + assert!(matches!(err, EtherscanError::ContractCodeNotVerified(_))); + }) + .await +} + +/// Query a contract that has a single string source entry instead of underlying JSON metadata. +#[tokio::test] +#[serial] +async fn can_fetch_contract_source_tree_for_singleton_contract() { + run_with_client(Chain::Mainnet, |client| async move { + let meta = client + .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) + .await + .unwrap(); + + assert_eq!(meta.items.len(), 1); + let item = &meta.items[0]; + assert!(matches!(item.source_code, SourceCodeMetadata::SourceCode(_))); + assert_eq!(item.source_code.sources().len(), 1); + assert_eq!(item.abi().unwrap(), serde_json::from_str(DAO_ABI).unwrap()); + }) + .await +} + +/// Query a contract that has many source entries as JSON metadata and ensure they are reflected. +#[tokio::test] +#[serial] +async fn can_fetch_contract_source_tree_for_multi_entry_contract() { + run_with_client(Chain::Mainnet, |client| async move { + let meta = client + .contract_source_code("0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63".parse().unwrap()) + .await + .unwrap(); + + assert_eq!(meta.items.len(), 1); + assert!(matches!(meta.items[0].source_code, SourceCodeMetadata::Metadata { .. })); + let source_tree = meta.source_tree(); + assert_eq!(source_tree.entries.len(), 15); + }) + .await +} diff --git a/ethers-etherscan/tests/it/gas.rs b/ethers-etherscan/tests/it/gas.rs new file mode 100644 index 00000000..a8754ee5 --- /dev/null +++ b/ethers-etherscan/tests/it/gas.rs @@ -0,0 +1,45 @@ +use crate::*; +use ethers_core::types::Chain; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn gas_estimate_success() { + run_with_client(Chain::Mainnet, |client| async move { + let result = client.gas_estimate(2000000000u32.into()).await; + + result.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn gas_estimate_error() { + run_with_client(Chain::Mainnet, |client| async move { + let err = client.gas_estimate(7123189371829732819379218u128.into()).await.unwrap_err(); + + assert!(matches!(err, EtherscanError::GasEstimationFailed)); + }) + .await +} + +#[tokio::test] +#[serial] +async fn gas_oracle_success() { + run_with_client(Chain::Mainnet, |client| async move { + let result = client.gas_oracle().await; + + assert!(result.is_ok()); + + let oracle = result.unwrap(); + + assert!(oracle.safe_gas_price > 0); + assert!(oracle.propose_gas_price > 0); + assert!(oracle.fast_gas_price > 0); + assert!(oracle.last_block > 0); + assert!(oracle.suggested_base_fee > 0.0); + assert!(!oracle.gas_used_ratio.is_empty()); + }) + .await +} diff --git a/ethers-etherscan/tests/it/main.rs b/ethers-etherscan/tests/it/main.rs new file mode 100644 index 00000000..e0548705 --- /dev/null +++ b/ethers-etherscan/tests/it/main.rs @@ -0,0 +1,76 @@ +//! Etherscan integration tests + +#![cfg(not(target_arch = "wasm32"))] + +use ethers_core::types::Chain; +use ethers_etherscan::{errors::EtherscanError, Client}; +use std::{ + future::Future, + time::{Duration, Instant}, +}; + +mod account; +mod contract; +mod gas; +mod transaction; +mod verify; +mod version; + +#[tokio::test] +async fn check_wrong_etherscan_api_key() { + let client = Client::new(Chain::Mainnet, "ABCDEFG").unwrap(); + let resp = client + .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) + .await + .unwrap_err(); + + assert!(matches!(resp, EtherscanError::InvalidApiKey)); +} + +/// Calls the function with a new Etherscan Client. +pub async fn run_with_client(chain: Chain, f: F) -> T +where + F: FnOnce(Client) -> Fut, + Fut: Future, +{ + init_tracing(); + let (client, duration) = match Client::new_from_env(chain) { + Ok(c) => (c, rate_limit(chain, true)), + Err(_) => { + (Client::builder().chain(chain).unwrap().build().unwrap(), rate_limit(chain, false)) + } + }; + run_at_least_duration(duration, f(client)).await +} + +#[track_caller] +fn rate_limit(chain: Chain, key: bool) -> Duration { + match (chain, key) { + // Rate limit with an API key is 5 call per second. + (_, true) => Duration::from_millis(250), + + // Rate limit without an API key is 1 call every 5 seconds. + // (Chain::Mainnet, false) => Duration::from_millis(5100), + (Chain::Mainnet, false) => panic!("ETHERSCAN_API_KEY is not set"), + + // Ignore other chains since we don't have more than 1 test with each. + (_, false) => Duration::ZERO, + } +} + +async fn run_at_least_duration(duration: Duration, block: impl Future) -> T { + let start = Instant::now(); + let output = block.await; + let elapsed = start.elapsed(); + if elapsed < duration { + tokio::time::sleep(duration - elapsed).await; + } + output +} + +#[track_caller] +fn init_tracing() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} diff --git a/ethers-etherscan/tests/it/transaction.rs b/ethers-etherscan/tests/it/transaction.rs new file mode 100644 index 00000000..3720f3cd --- /dev/null +++ b/ethers-etherscan/tests/it/transaction.rs @@ -0,0 +1,65 @@ +use crate::*; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn check_contract_execution_status_success() { + run_with_client(Chain::Mainnet, |client| async move { + let status = client + .check_contract_execution_status( + "0x16197e2a0eacc44c1ebdfddcfcfcafb3538de557c759a66e0ba95263b23d9007", + ) + .await; + + status.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn check_contract_execution_status_error() { + run_with_client(Chain::Mainnet, |client| async move { + let err = client + .check_contract_execution_status( + "0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a", + ) + .await + .unwrap_err(); + + assert!(matches!(err, EtherscanError::ExecutionFailed(_))); + assert_eq!(err.to_string(), "Contract execution call failed: Bad jump destination"); + }) + .await +} + +#[tokio::test] +#[serial] +async fn check_transaction_receipt_status_success() { + run_with_client(Chain::Mainnet, |client| async move { + let success = client + .check_transaction_receipt_status( + "0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76", + ) + .await; + + success.unwrap(); + }) + .await +} + +#[tokio::test] +#[serial] +async fn check_transaction_receipt_status_failed() { + run_with_client(Chain::Mainnet, |client| async move { + let err = client + .check_transaction_receipt_status( + "0x21a29a497cb5d4bf514c0cca8d9235844bd0215c8fab8607217546a892fd0758", + ) + .await + .unwrap_err(); + + assert!(matches!(err, EtherscanError::TransactionReceiptFailed)); + }) + .await +} diff --git a/ethers-etherscan/tests/it/verify.rs b/ethers-etherscan/tests/it/verify.rs new file mode 100644 index 00000000..555a7de0 --- /dev/null +++ b/ethers-etherscan/tests/it/verify.rs @@ -0,0 +1,41 @@ +use crate::*; +use ethers_core::types::Chain; +use ethers_etherscan::verify::VerifyContract; +use ethers_solc::{Project, ProjectPathsConfig}; +use serial_test::serial; +use std::path::Path; + +#[tokio::test] +#[serial] +#[ignore] +async fn can_flatten_and_verify_contract() { + let root = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../../tests/testdata/uniswap")); + let paths = ProjectPathsConfig::builder() + .sources(root) + .build() + .expect("failed to resolve project paths"); + let project = Project::builder().paths(paths).build().expect("failed to build the project"); + + let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33".parse().unwrap(); + let compiler_version = "v0.5.17+commit.d19bba13"; + let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000"; + let contract = + project.flatten(&root.join("UniswapExchange.sol")).expect("failed to flatten contract"); + let contract_name = "UniswapExchange".to_owned(); + let contract = + VerifyContract::new(address, contract_name, contract, compiler_version.to_string()) + .constructor_arguments(Some(constructor_args)) + .optimization(true) + .runs(200); + + run_with_client(Chain::Mainnet, |client| async move { + let resp = client + .submit_contract_verification(&contract) + .await + .expect("failed to send the request"); + // `Error!` result means that request was malformatted + assert_ne!(resp.result, "Error!", "{resp:?}"); + assert_ne!(resp.message, "NOTOK", "{resp:?}"); + }) + .await +} diff --git a/ethers-etherscan/tests/it/version.rs b/ethers-etherscan/tests/it/version.rs new file mode 100644 index 00000000..34f325ef --- /dev/null +++ b/ethers-etherscan/tests/it/version.rs @@ -0,0 +1,21 @@ +use crate::*; +use ethers_etherscan::utils::lookup_compiler_version; +use semver::{BuildMetadata, Prerelease, Version}; + +#[tokio::test] +async fn can_lookup_compiler_version_build_metadata() { + let v = Version::new(0, 8, 13); + let version = lookup_compiler_version(&v).await.unwrap(); + assert_eq!(v.major, version.major); + assert_eq!(v.minor, version.minor); + assert_eq!(v.patch, version.patch); + assert_ne!(version.build, BuildMetadata::EMPTY); + assert_eq!(version.pre, Prerelease::EMPTY); +} + +#[tokio::test] +async fn errors_on_invalid_solc() { + let v = Version::new(100, 0, 0); + let err = lookup_compiler_version(&v).await.unwrap_err(); + assert!(matches!(err, EtherscanError::MissingSolcVersion(_))); +} diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml index 6cb746a5..382ac2dd 100644 --- a/ethers-middleware/Cargo.toml +++ b/ethers-middleware/Cargo.toml @@ -15,7 +15,9 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -ethers-contract = { version = "^1.0.0", path = "../ethers-contract", default-features = false } +ethers-contract = { version = "^1.0.0", path = "../ethers-contract", default-features = false, features = [ + "abigen", +] } ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features = false } ethers-etherscan = { version = "^1.0.0", path = "../ethers-etherscan", default-features = false } ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-features = false } @@ -41,15 +43,15 @@ instant = { version = "0.1.12", features = ["now"] } tokio = { version = "1.18" } [dev-dependencies] -hex = { version = "0.4.3", default-features = false, features = ["std"] } -rand = { version = "0.8.5", default-features = false } ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-features = false, features = [ "ws", "rustls", ] } -once_cell = "1.17.1" ethers-solc = { version = "^1.0.0", path = "../ethers-solc" } -serial_test = "0.10.0" + +hex = { version = "0.4.3", default-features = false, features = ["std"] } +rand = { version = "0.8.5", default-features = false } +once_cell = "1.17.1" reqwest = { version = "0.11.14", default-features = false, features = ["json", "rustls"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/ethers-middleware/contracts/DSProxyFactory.json b/ethers-middleware/contracts/DSProxyFactory.json new file mode 100644 index 00000000..c474a072 --- /dev/null +++ b/ethers-middleware/contracts/DSProxyFactory.json @@ -0,0 +1 @@ +{"abi":[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"address","name":"proxy","type":"address"},{"indexed":false,"internalType":"address","name":"cache","type":"address"}],"name":"Created","type":"event"},{"inputs":[],"name":"build","outputs":[{"internalType":"contract DSProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"build","outputs":[{"internalType":"contract DSProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cache","outputs":[{"internalType":"contract DSProxyCache","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isProxy","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}],"bytecode":"60806040526040516100109061005f565b604051809103906000f08015801561002c573d6000803e3d6000fd5b50600180546001600160a01b0319166001600160a01b039290921691909117905534801561005957600080fd5b5061006c565b61020780610e7683390190565b610dfb8061007b6000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c8063297103881461005157806360c7d295146100895780638e1a55fc146100b4578063f3701da2146100bc575b600080fd5b61007461005f366004610208565b60006020819052908152604090205460ff1681565b60405190151581526020015b60405180910390f35b60015461009c906001600160a01b031681565b6040516001600160a01b039091168152602001610080565b61009c6100cf565b61009c6100ca366004610208565b6100df565b60006100da336100df565b905090565b6001546040516000916001600160a01b0316906100fb906101fb565b6001600160a01b039091168152602001604051809103906000f080158015610127573d6000803e3d6000fd5b50600154604080516001600160a01b03808516825292831660208201529293509084169133917f259b30ca39885c6d801a0b5dbc988640f3c25e2f37531fe138c5c5af8955d41b910160405180910390a36040516313af403560e01b81526001600160a01b0383811660048301528216906313af403590602401600060405180830381600087803b1580156101bb57600080fd5b505af11580156101cf573d6000803e3d6000fd5b5050506001600160a01b0382166000908152602081905260409020805460ff1916600117905550919050565b610b8d8061023983390190565b60006020828403121561021a57600080fd5b81356001600160a01b038116811461023157600080fd5b939250505056fe608060405234801561001057600080fd5b50604051610b8d380380610b8d83398101604081905261002f91610239565b600180546001600160a01b031916339081179091556040517fce241d7ca1f669fee44b6fc00b8eba2df3bb514eed0f6f668f8f89096e81ed9490600090a261007681610085565b61007f57600080fd5b506102c1565b600061009c336001600160e01b031983351661015c565b6100ec5760405162461bcd60e51b815260206004820152601460248201527f64732d617574682d756e617574686f72697a6564000000000000000000000000604482015260640160405180910390fd5b600435602435346001600160a01b03851661010657600080fd5b600280546001600160a01b0387166001600160a01b031990911617905560405160019450829084903390600080356001600160e01b0319169161014c9187913690610269565b60405180910390a4505050919050565b6000306001600160a01b0384160361017657506001610233565b6001546001600160a01b039081169084160361019457506001610233565b6000546001600160a01b03166101ac57506000610233565b60005460405163b700961360e01b81526001600160a01b0385811660048301523060248301526001600160e01b0319851660448301529091169063b700961390606401602060405180830381865afa15801561020c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610230919061029f565b90505b92915050565b60006020828403121561024b57600080fd5b81516001600160a01b038116811461026257600080fd5b9392505050565b83815260406020820152816040820152818360608301376000818301606090810191909152601f909201601f1916010192915050565b6000602082840312156102b157600080fd5b8151801515811461026257600080fd5b6108bd806102d06000396000f3fe6080604052600436106100795760003560e01c80637a9e5e4b1161004b5780637a9e5e4b146101325780638da5cb5b14610152578063948f507614610172578063bf7e214f146101a257005b806313af4035146100825780631cff79cd146100a25780631f6a1eb9146100c857806360c7d295146100fa57005b3661008057005b005b34801561008e57600080fd5b5061008061009d366004610625565b6101c2565b6100b56100b03660046106ec565b610247565b6040519081526020015b60405180910390f35b6100db6100d636600461073c565b610303565b604080516001600160a01b0390931683526020830191909152016100bf565b34801561010657600080fd5b5060025461011a906001600160a01b031681565b6040516001600160a01b0390911681526020016100bf565b34801561013e57600080fd5b5061008061014d366004610625565b610412565b34801561015e57600080fd5b5060015461011a906001600160a01b031681565b34801561017e57600080fd5b5061019261018d366004610625565b61048c565b60405190151581526020016100bf565b3480156101ae57600080fd5b5060005461011a906001600160a01b031681565b6101d8336000356001600160e01b031916610530565b6101fd5760405162461bcd60e51b81526004016101f490610796565b60405180910390fd5b600180546001600160a01b0319166001600160a01b0383169081179091556040517fce241d7ca1f669fee44b6fc00b8eba2df3bb514eed0f6f668f8f89096e81ed9490600090a250565b600061025f336000356001600160e01b031916610530565b61027b5760405162461bcd60e51b81526004016101f490610796565b600435602435346001600160a01b03861661029557600080fd5b60206000865160208801896113885a03f460005194508015600181036102ba57600080fd5b50508183336001600160a01b03166000356001600160e01b0319166001600160e01b031916846000366040516102f2939291906107c4565b60405180910390a450505092915050565b6002546040516322fd145760e21b815260009182916001600160a01b0390911690638bf4515c906103389087906004016107fa565b602060405180830381865afa158015610355573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103799190610848565b91506001600160a01b0382166103ff57600254604051633f6861d960e11b81526001600160a01b0390911690637ed0c3b2906103b99087906004016107fa565b6020604051808303816000875af11580156103d8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103fc9190610848565b91505b6104098284610247565b90509250929050565b610428336000356001600160e01b031916610530565b6104445760405162461bcd60e51b81526004016101f490610796565b600080546001600160a01b0319166001600160a01b038316908117825560405190917f1abebea81bfa2637f28358c371278fb15ede7ea8dd28d2e03b112ff6d936ada491a250565b60006104a4336000356001600160e01b031916610530565b6104c05760405162461bcd60e51b81526004016101f490610796565b600435602435346001600160a01b0385166104da57600080fd5b600280546001600160a01b0387166001600160a01b031990911617905560405160019450829084903390600080356001600160e01b0319169161052091879136906107c4565b60405180910390a4505050919050565b6000306001600160a01b0384160361054a57506001610607565b6001546001600160a01b039081169084160361056857506001610607565b6000546001600160a01b031661058057506000610607565b60005460405163b700961360e01b81526001600160a01b0385811660048301523060248301526001600160e01b0319851660448301529091169063b700961390606401602060405180830381865afa1580156105e0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106049190610865565b90505b92915050565b6001600160a01b038116811461062257600080fd5b50565b60006020828403121561063757600080fd5b81356106428161060d565b9392505050565b634e487b7160e01b600052604160045260246000fd5b600082601f83011261067057600080fd5b813567ffffffffffffffff8082111561068b5761068b610649565b604051601f8301601f19908116603f011681019082821181831017156106b3576106b3610649565b816040528381528660208588010111156106cc57600080fd5b836020870160208301376000602085830101528094505050505092915050565b600080604083850312156106ff57600080fd5b823561070a8161060d565b9150602083013567ffffffffffffffff81111561072657600080fd5b6107328582860161065f565b9150509250929050565b6000806040838503121561074f57600080fd5b823567ffffffffffffffff8082111561076757600080fd5b6107738683870161065f565b9350602085013591508082111561078957600080fd5b506107328582860161065f565b602080825260149082015273191ccb585d5d1a0b5d5b985d5d1a1bdc9a5e995960621b604082015260600190565b83815260406020820152816040820152818360608301376000818301606090810191909152601f909201601f1916010192915050565b600060208083528351808285015260005b818110156108275785810183015185820160400152820161080b565b506000604082860101526040601f19601f8301168501019250505092915050565b60006020828403121561085a57600080fd5b81516106428161060d565b60006020828403121561087757600080fd5b8151801515811461064257600080fdfea26469706673582212205fd82ffa4dee296c48714a3bc029a53d744182ae006fc2de40e95c978e5484d164736f6c63430008130033a2646970667358221220374bcf12d6c6e67b317ce638a4e5c9105031ebc5efd063eaef3ecb4f3b2747fe64736f6c63430008130033608060405234801561001057600080fd5b506101e7806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80637ed0c3b21461003b5780638bf4515c1461006a575b600080fd5b61004e610049366004610100565b61009a565b6040516001600160a01b03909116815260200160405180910390f35b61004e610078366004610100565b805160209182012060009081529081905260409020546001600160a01b031690565b60008151602083016000f09050803b15600181036100b757600080fd5b508151602092830120600090815291829052604090912080546001600160a01b0319166001600160a01b03831617905590565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561011257600080fd5b813567ffffffffffffffff8082111561012a57600080fd5b818401915084601f83011261013e57600080fd5b813581811115610150576101506100ea565b604051601f8201601f19908116603f01168101908382118183101715610178576101786100ea565b8160405282815287602084870101111561019157600080fd5b82602086016020830137600092810160200192909252509594505050505056fea2646970667358221220a9c9fca6107ebf965fd78397d3d7ac4d975d83d669bafda1a35ff53fcfa20f1f64736f6c63430008130033"} diff --git a/ethers-middleware/contracts/DsProxyFactory.json b/ethers-middleware/contracts/DsProxyFactory.json deleted file mode 100644 index 72161d66..00000000 --- a/ethers-middleware/contracts/DsProxyFactory.json +++ /dev/null @@ -1,95 +0,0 @@ -[ - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "isProxy", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "cache", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "build", - "outputs": [ - { - "name": "proxy", - "type": "address" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "owner", - "type": "address" - } - ], - "name": "build", - "outputs": [ - { - "name": "proxy", - "type": "address" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "sender", - "type": "address" - }, - { - "indexed": true, - "name": "owner", - "type": "address" - }, - { - "indexed": false, - "name": "proxy", - "type": "address" - }, - { - "indexed": false, - "name": "cache", - "type": "address" - } - ], - "name": "Created", - "type": "event" - } -] \ No newline at end of file diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs index 1a58129f..648d2039 100644 --- a/ethers-middleware/src/signer.rs +++ b/ethers-middleware/src/signer.rs @@ -499,6 +499,9 @@ mod tests { None, ) .await + .unwrap() + .await + .unwrap() .unwrap(); let client = SignerMiddleware::new_with_provider_chain(provider, key).await.unwrap(); diff --git a/ethers-middleware/src/transformer/ds_proxy/factory.rs b/ethers-middleware/src/transformer/ds_proxy/factory.rs index 665d5059..2572e74e 100644 --- a/ethers-middleware/src/transformer/ds_proxy/factory.rs +++ b/ethers-middleware/src/transformer/ds_proxy/factory.rs @@ -1,204 +1,20 @@ -use ethers_contract::Lazy; -use ethers_core::types::*; +use ethers_contract::{abigen, Lazy}; +use ethers_core::types::{Address, U256}; use std::collections::HashMap; /// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding /// DsProxyFactory contract addresses as values pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { - let mut m = HashMap::with_capacity(1); - - // mainnet - let addr = "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap(); - m.insert(U256::from(1_u64), addr); - - m + HashMap::from([ + // Mainnet + (U256::from(1_u64), "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap()), + ]) }); -/// Generated with abigen: -/// -/// ```ignore -/// # use ethers_contract::abigen; -/// abigen!(DsProxyFactory, -/// "ethers-middleware/contracts/DsProxyFactory.json", -/// methods { -/// build() as build_with_sender; -/// } -/// ); -/// ``` -pub use dsproxyfactory_mod::*; -#[allow(clippy::too_many_arguments)] -mod dsproxyfactory_mod { - #![allow(dead_code)] - #![allow(unused_imports)] - use ethers_contract::{ - builders::{ContractCall, Event}, - Contract, EthEvent, Lazy, - }; - use ethers_core::{ - abi::{ - parse_abi, Abi, Detokenize, InvalidOutputType, ParamType, Token, Tokenizable, - TokenizableItem, - }, - types::*, - }; - use ethers_providers::Middleware; - #[doc = "DsProxyFactory was auto-generated with ethers-rs Abigen. More information at: "] - use std::sync::Arc; - - pub static DSPROXYFACTORY_ABI: Lazy = Lazy::new(|| { - serde_json :: from_str ("[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"isProxy\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"cache\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"build\",\"outputs\":[{\"name\":\"proxy\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"build\",\"outputs\":[{\"name\":\"proxy\",\"type\":\"address\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"proxy\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"cache\",\"type\":\"address\"}],\"name\":\"Created\",\"type\":\"event\"}]\n") . expect ("invalid abi") - }); - pub struct DsProxyFactory(Contract); - impl Clone for DsProxyFactory { - fn clone(&self) -> Self { - DsProxyFactory(self.0.clone()) - } +abigen!( + DsProxyFactory, + "./contracts/DSProxyFactory.json", + methods { + build() as build_with_sender; } - impl std::ops::Deref for DsProxyFactory { - type Target = Contract; - fn deref(&self) -> &Self::Target { - &self.0 - } - } - impl std::fmt::Debug for DsProxyFactory { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.debug_tuple(stringify!(DsProxyFactory)).field(&self.address()).finish() - } - } - impl DsProxyFactory { - #[doc = r" Creates a new contract instance with the specified `ethers`"] - #[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"] - #[doc = r" object"] - pub fn new>(address: T, client: Arc) -> Self { - let contract = Contract::new(address.into(), DSPROXYFACTORY_ABI.clone(), client); - Self(contract) - } - #[doc = "Calls the contract's `isProxy` (0x29710388) function"] - pub fn is_proxy(&self, p0: Address) -> ContractCall { - self.0 - .method_hash([41, 113, 3, 136], p0) - .expect("method not found (this should never happen)") - } - ///Calls the contract's `build` (0x8e1a55fc) function - pub fn build_with_sender(&self) -> ContractCall { - self.0 - .method_hash([142, 26, 85, 252], ()) - .expect("method not found (this should never happen)") - } - ///Calls the contract's `build` (0xf3701da2) function - pub fn build(&self, owner: Address) -> ContractCall { - self.0 - .method_hash([243, 112, 29, 162], owner) - .expect("method not found (this should never happen)") - } - #[doc = "Calls the contract's `cache` (0x60c7d295) function"] - pub fn cache(&self) -> ContractCall { - self.0 - .method_hash([96, 199, 210, 149], ()) - .expect("method not found (this should never happen)") - } - ///Gets the contract's `Created` event - pub fn created_filter(&self) -> Event, M, CreatedFilter> { - self.0.event() - } - - /// Returns an [`Event`](ethers_contract::builders::Event) builder for all events of this - /// contract - pub fn events(&self) -> Event, M, CreatedFilter> { - self.0.event_with_filter(Default::default()) - } - } - - #[derive(Clone, Debug, Default, Eq, PartialEq)] - pub struct CreatedFilter { - pub sender: Address, - pub owner: Address, - pub proxy: Address, - pub cache: Address, - } - - impl ethers_core::abi::AbiType for CreatedFilter { - fn param_type() -> ParamType { - ParamType::Tuple(::std::vec![ - ParamType::Address; 4 - ]) - } - } - impl ethers_core::abi::AbiArrayType for CreatedFilter {} - - impl Tokenizable for CreatedFilter { - fn from_token(token: Token) -> Result - where - Self: Sized, - { - if let Token::Tuple(tokens) = token { - if tokens.len() != 4usize { - return Err(InvalidOutputType(format!( - "Expected {} tokens, got {}: {:?}", - 4usize, - tokens.len(), - tokens - ))) - } - let mut iter = tokens.into_iter(); - Ok(Self { - sender: Tokenizable::from_token(iter.next().unwrap())?, - owner: Tokenizable::from_token(iter.next().unwrap())?, - proxy: Tokenizable::from_token(iter.next().unwrap())?, - cache: Tokenizable::from_token(iter.next().unwrap())?, - }) - } else { - Err(InvalidOutputType(format!("Expected Tuple, got {token:?}"))) - } - } - fn into_token(self) -> Token { - Token::Tuple(::std::vec![ - self.sender.into_token(), - self.owner.into_token(), - self.proxy.into_token(), - self.cache.into_token(), - ]) - } - } - impl TokenizableItem for CreatedFilter {} - - impl ethers_contract::EthEvent for CreatedFilter { - fn name() -> std::borrow::Cow<'static, str> { - "Created".into() - } - fn signature() -> H256 { - H256([ - 37, 155, 48, 202, 57, 136, 92, 109, 128, 26, 11, 93, 188, 152, 134, 64, 243, 194, - 94, 47, 55, 83, 31, 225, 56, 197, 197, 175, 137, 85, 212, 27, - ]) - } - fn abi_signature() -> std::borrow::Cow<'static, str> { - "Created(address,address,address,address)".into() - } - fn decode_log(log: ðers_core::abi::RawLog) -> Result - where - Self: Sized, - { - let ethers_core::abi::RawLog { data, topics } = log; - let event_signature = topics.get(0).ok_or(ethers_core::abi::Error::InvalidData)?; - if event_signature != &Self::signature() { - return Err(ethers_core::abi::Error::InvalidData) - } - let topic_types = ::std::vec![ParamType::Address, ParamType::Address]; - let data_types = [ParamType::Address, ParamType::Address]; - let flat_topics = - topics.iter().skip(1).flat_map(|t| t.as_ref().to_vec()).collect::>(); - let topic_tokens = ethers_core::abi::decode(&topic_types, &flat_topics)?; - if topic_tokens.len() != topics.len() - 1 { - return Err(ethers_core::abi::Error::InvalidData) - } - let data_tokens = ethers_core::abi::decode(&data_types, data)?; - let tokens: Vec<_> = topic_tokens.into_iter().chain(data_tokens.into_iter()).collect(); - Tokenizable::from_token(Token::Tuple(tokens)) - .map_err(|_| ethers_core::abi::Error::InvalidData) - } - fn is_anonymous() -> bool { - false - } - } -} +); diff --git a/ethers-middleware/src/transformer/ds_proxy/mod.rs b/ethers-middleware/src/transformer/ds_proxy/mod.rs index d97e7a16..3819d085 100644 --- a/ethers-middleware/src/transformer/ds_proxy/mod.rs +++ b/ethers-middleware/src/transformer/ds_proxy/mod.rs @@ -1,4 +1,4 @@ -mod factory; +pub mod factory; use factory::{CreatedFilter, DsProxyFactory, ADDRESS_BOOK}; use super::{Transformer, TransformerError}; @@ -18,7 +18,6 @@ const DS_PROXY_EXECUTE_TARGET: &str = const DS_PROXY_EXECUTE_CODE: &str = "function execute(bytes memory code, bytes memory data) public payable returns (address target, bytes memory response)"; -#[derive(Debug, Clone)] /// Represents the DsProxy type that implements the [Transformer](super::Transformer) trait. /// /// # Example @@ -57,6 +56,7 @@ const DS_PROXY_EXECUTE_CODE: &str = /// # Ok(()) /// # } /// ``` +#[derive(Clone, Debug)] pub struct DsProxy { address: Address, contract: BaseContract, diff --git a/ethers-middleware/src/transformer/mod.rs b/ethers-middleware/src/transformer/mod.rs index 4bd1542e..09aaf86f 100644 --- a/ethers-middleware/src/transformer/mod.rs +++ b/ethers-middleware/src/transformer/mod.rs @@ -1,4 +1,4 @@ -mod ds_proxy; +pub mod ds_proxy; pub use ds_proxy::DsProxy; mod middleware; diff --git a/ethers-middleware/tests/gas_escalator.rs b/ethers-middleware/tests/gas_escalator.rs deleted file mode 100644 index 66d7d4ff..00000000 --- a/ethers-middleware/tests/gas_escalator.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg(not(target_arch = "wasm32"))] - -use ethers_core::types::*; -use ethers_middleware::{ - gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, - signer::SignerMiddleware, -}; -use ethers_providers::Middleware; -use ethers_signers::{LocalWallet, Signer}; -use std::time::Duration; - -#[tokio::test] -#[ignore] -async fn gas_escalator_live() { - // connect to ropsten for getting bad block times - #[allow(deprecated)] - let provider = ethers_providers::ROPSTEN.ws().await; - let provider = provider.interval(Duration::from_millis(2000u64)); - let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169" - .parse::() - .unwrap(); - let address = wallet.address(); - let provider = SignerMiddleware::new(provider, wallet); - - let escalator = GeometricGasPrice::new(5.0, 10u64, Some(2_000_000_000_000u64)); - - let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::Duration(3000)); - - let nonce = provider.get_transaction_count(address, None).await.unwrap(); - let tx = TransactionRequest::pay(Address::zero(), 1u64).gas_price(10_000_000); - - // broadcast 3 txs - provider.send_transaction(tx.clone().nonce(nonce), None).await.unwrap(); - provider.send_transaction(tx.clone().nonce(nonce + 1), None).await.unwrap(); - provider.send_transaction(tx.clone().nonce(nonce + 2), None).await.unwrap(); - - // Wait a bunch of seconds and refresh etherscan to see the transactions get bumped - tokio::time::sleep(Duration::from_secs(100)).await; - - // TODO: Figure out how to test this behavior properly in a local network. If the gas price was - // bumped then the tx hash will be different -} diff --git a/ethers-middleware/tests/builder.rs b/ethers-middleware/tests/it/builder.rs similarity index 98% rename from ethers-middleware/tests/builder.rs rename to ethers-middleware/tests/it/builder.rs index d99c603e..a0c504e0 100644 --- a/ethers-middleware/tests/builder.rs +++ b/ethers-middleware/tests/it/builder.rs @@ -1,5 +1,3 @@ -#![cfg(not(target_arch = "wasm32"))] - use ethers_core::{rand::thread_rng, types::U64}; use ethers_middleware::{ builder::MiddlewareBuilder, diff --git a/ethers-middleware/tests/it/gas_escalator.rs b/ethers-middleware/tests/it/gas_escalator.rs new file mode 100644 index 00000000..903746a1 --- /dev/null +++ b/ethers-middleware/tests/it/gas_escalator.rs @@ -0,0 +1,41 @@ +use ethers_core::{types::*, utils::Anvil}; +use ethers_middleware::{ + gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, + MiddlewareBuilder, +}; +use ethers_providers::{Http, Middleware, Provider}; +use ethers_signers::{LocalWallet, Signer}; + +#[tokio::test] +#[ignore] +async fn gas_escalator() { + let anvil = Anvil::new().block_time(2u64).spawn(); + let chain_id = anvil.chain_id(); + let provider = Provider::::try_from(anvil.endpoint()).unwrap(); + + // wrap with signer + let wallet: LocalWallet = anvil.keys().first().unwrap().clone().into(); + let wallet = wallet.with_chain_id(chain_id); + let address = wallet.address(); + let provider = provider.with_signer(wallet); + + // wrap with escalator + let escalator = GeometricGasPrice::new(5.0, 10u64, Some(2_000_000_000_000u64)); + let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::Duration(300)); + + let nonce = provider.get_transaction_count(address, None).await.unwrap(); + // 1 gwei default base fee + let gas_price = U256::from(1_000_000_000_u64); + let tx = TransactionRequest::pay(Address::zero(), 1u64) + .gas_price(gas_price) + .nonce(nonce) + .chain_id(chain_id); + + eprintln!("sending"); + let pending = provider.send_transaction(tx, None).await.expect("could not send"); + eprintln!("waiting"); + let receipt = pending.await.expect("reverted").expect("dropped"); + assert_eq!(receipt.from, address); + assert_eq!(receipt.to, Some(Address::zero())); + assert!(receipt.effective_gas_price.unwrap() > gas_price * 2, "{receipt:?}"); +} diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/it/gas_oracle.rs similarity index 95% rename from ethers-middleware/tests/gas_oracle.rs rename to ethers-middleware/tests/it/gas_oracle.rs index 9464fb3d..14aee0a3 100644 --- a/ethers-middleware/tests/gas_oracle.rs +++ b/ethers-middleware/tests/it/gas_oracle.rs @@ -1,14 +1,11 @@ -#![cfg(not(target_arch = "wasm32"))] - use async_trait::async_trait; use ethers_core::{types::*, utils::Anvil}; +use ethers_etherscan::Client; use ethers_middleware::gas_oracle::{ BlockNative, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleError, GasOracleMiddleware, Polygon, ProviderOracle, Result, }; use ethers_providers::{Http, Middleware, Provider}; -use serial_test::serial; -use std::convert::TryFrom; #[derive(Debug)] struct FakeGasOracle { @@ -28,7 +25,6 @@ impl GasOracle for FakeGasOracle { } #[tokio::test] -#[serial] async fn provider_using_gas_oracle() { let anvil = Anvil::new().spawn(); @@ -54,7 +50,6 @@ async fn provider_using_gas_oracle() { } #[tokio::test] -#[serial] async fn provider_oracle() { // spawn anvil and connect to it let anvil = Anvil::new().spawn(); @@ -93,7 +88,8 @@ async fn etherchain() { #[tokio::test] async fn etherscan() { - let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap(); + let chain = Chain::Mainnet; + let etherscan_client = Client::new_from_opt_env(chain).unwrap(); // initialize and fetch gas estimates from Etherscan // since etherscan does not support `fastest` category, we expect an error diff --git a/ethers-middleware/tests/it/main.rs b/ethers-middleware/tests/it/main.rs new file mode 100644 index 00000000..9f5c3e0f --- /dev/null +++ b/ethers-middleware/tests/it/main.rs @@ -0,0 +1,49 @@ +#![allow(clippy::extra_unused_type_parameters)] +#![cfg(not(target_arch = "wasm32"))] + +use ethers_core::utils::{Anvil, AnvilInstance}; +use ethers_providers::{Http, Provider, Ws}; +use ethers_signers::{LocalWallet, Signer}; +use std::time::Duration; + +mod builder; + +mod gas_escalator; + +mod gas_oracle; + +#[cfg(not(feature = "celo"))] +mod signer; + +#[cfg(not(feature = "celo"))] +mod nonce_manager; + +#[cfg(not(feature = "celo"))] +mod stack; + +#[cfg(not(feature = "celo"))] +mod transformer; + +/// Spawns Anvil and instantiates an Http provider. +pub fn spawn_anvil() -> (Provider, AnvilInstance) { + let anvil = Anvil::new().block_time(1u64).spawn(); + let provider = Provider::::try_from(anvil.endpoint()) + .unwrap() + .interval(Duration::from_millis(50u64)); + (provider, anvil) +} + +/// Spawns Anvil and instantiates a Ws provider. +pub async fn spawn_anvil_ws() -> (Provider, AnvilInstance) { + let anvil = Anvil::new().block_time(1u64).spawn(); + let provider = Provider::::connect(anvil.ws_endpoint()) + .await + .unwrap() + .interval(Duration::from_millis(50u64)); + (provider, anvil) +} + +/// Gets `idx` wallet from the given anvil instance. +pub fn get_wallet(anvil: &AnvilInstance, idx: usize) -> LocalWallet { + LocalWallet::from(anvil.keys()[idx].clone()).with_chain_id(anvil.chain_id()) +} diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/it/nonce_manager.rs similarity index 69% rename from ethers-middleware/tests/nonce_manager.rs rename to ethers-middleware/tests/it/nonce_manager.rs index 85368aa2..61b88f57 100644 --- a/ethers-middleware/tests/nonce_manager.rs +++ b/ethers-middleware/tests/it/nonce_manager.rs @@ -1,18 +1,13 @@ -#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] - -use ethers_core::{types::*, utils::Anvil}; +use crate::spawn_anvil; +use ethers_core::types::*; use ethers_middleware::MiddlewareBuilder; -use ethers_providers::{Http, Middleware, Provider}; +use ethers_providers::Middleware; #[tokio::test] async fn nonce_manager() { - let anvil = Anvil::new().spawn(); - let endpoint = anvil.endpoint(); - - let provider = Provider::::try_from(endpoint).unwrap(); - let accounts = provider.get_accounts().await.unwrap(); - let address = accounts[0]; - let to = accounts[1]; + let (provider, anvil) = spawn_anvil(); + let address = anvil.addresses()[0]; + let to = anvil.addresses()[1]; // the nonce manager must be over the Client so that it overrides the nonce // before the client gets it diff --git a/ethers-middleware/tests/it/signer.rs b/ethers-middleware/tests/it/signer.rs new file mode 100644 index 00000000..0699de76 --- /dev/null +++ b/ethers-middleware/tests/it/signer.rs @@ -0,0 +1,115 @@ +use crate::{get_wallet, spawn_anvil, spawn_anvil_ws}; +use ethers_core::types::*; +use ethers_middleware::{signer::SignerMiddleware, MiddlewareBuilder}; +use ethers_providers::{JsonRpcClient, Middleware}; +use ethers_signers::{LocalWallet, Signer}; + +#[tokio::test] +async fn send_eth() { + let (provider, anvil) = spawn_anvil(); + let wallet = get_wallet(&anvil, 0); + let address = wallet.address(); + let provider = provider.with_signer(wallet); + + let to = anvil.addresses()[1]; + + // craft the transaction + let tx = TransactionRequest::new().to(to).value(10000); + + let balance_before = provider.get_balance(address, None).await.unwrap(); + + // send it! + provider.send_transaction(tx, None).await.unwrap().await.unwrap().unwrap(); + + let balance_after = provider.get_balance(address, None).await.unwrap(); + + assert!(balance_before > balance_after); +} + +#[tokio::test] +async fn typed_txs() { + let (provider, anvil) = spawn_anvil(); + let wallet = get_wallet(&anvil, 0); + let address = wallet.address(); + let provider = provider.with_signer(wallet); + + let nonce = provider.get_transaction_count(address, None).await.unwrap(); + let bn = Some(BlockNumber::Pending.into()); + let gas_price = provider.get_gas_price().await.unwrap() * 125 / 100; + + let tx = TransactionRequest::new().from(address).to(address).nonce(nonce).gas_price(gas_price); + let tx1 = provider.send_transaction(tx.clone(), bn).await.unwrap(); + + let tx = tx.from(address).to(address).nonce(nonce + 1).with_access_list(vec![]); + let tx2 = provider.send_transaction(tx, bn).await.unwrap(); + + let tx = Eip1559TransactionRequest::new() + .from(address) + .to(address) + .nonce(nonce + 2) + .max_fee_per_gas(gas_price) + .max_priority_fee_per_gas(gas_price); + let tx3 = provider.send_transaction(tx, bn).await.unwrap(); + + futures_util::join!(check_tx(tx1, 0), check_tx(tx2, 1), check_tx(tx3, 2)); +} + +#[tokio::test] +async fn send_transaction_handles_tx_from_field() { + // launch anvil + let (provider, anvil) = spawn_anvil_ws().await; + + // grab 2 wallets + let signer: LocalWallet = anvil.keys()[0].clone().into(); + let other: LocalWallet = anvil.keys()[1].clone().into(); + + // connect to the network + let provider = + SignerMiddleware::new_with_provider_chain(provider, signer.clone()).await.unwrap(); + + // sending a TransactionRequest with a from field of None should result + // in a transaction from the signer address + let request_from_none = TransactionRequest::new(); + let receipt = + provider.send_transaction(request_from_none, None).await.unwrap().await.unwrap().unwrap(); + let sent_tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); + + assert_eq!(sent_tx.from, signer.address()); + + // sending a TransactionRequest with the signer as the from address should + // result in a transaction from the signer address + let request_from_signer = TransactionRequest::new().from(signer.address()); + let receipt = + provider.send_transaction(request_from_signer, None).await.unwrap().await.unwrap().unwrap(); + let sent_tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); + + assert_eq!(sent_tx.from, signer.address()); + + // sending a TransactionRequest with a from address that is not the signer + // should result in a transaction from the specified address + let request_from_other = TransactionRequest::new().from(other.address()); + let receipt = + provider.send_transaction(request_from_other, None).await.unwrap().await.unwrap().unwrap(); + let sent_tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); + + assert_eq!(sent_tx.from, other.address()); +} + +async fn check_tx( + pending_tx: ethers_providers::PendingTransaction<'_, P>, + expected: u64, +) { + let provider = pending_tx.provider(); + let receipt = pending_tx.await.unwrap().unwrap(); + let tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); + + let expected = U64::from(expected); + for ty in [receipt.transaction_type, tx.transaction_type] { + // legacy can be either None or Some(0) + if expected.is_zero() { + assert!(ty.is_none() || ty == Some(0.into())); + } else { + assert_eq!(ty, Some(expected)); + } + } +} diff --git a/ethers-middleware/tests/stack.rs b/ethers-middleware/tests/it/stack.rs similarity index 98% rename from ethers-middleware/tests/stack.rs rename to ethers-middleware/tests/it/stack.rs index bf70056f..d0512d86 100644 --- a/ethers-middleware/tests/stack.rs +++ b/ethers-middleware/tests/it/stack.rs @@ -1,5 +1,3 @@ -#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] - use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil}; use ethers_middleware::{ gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, diff --git a/ethers-middleware/tests/it/transformer.rs b/ethers-middleware/tests/it/transformer.rs new file mode 100644 index 00000000..87bdfcc0 --- /dev/null +++ b/ethers-middleware/tests/it/transformer.rs @@ -0,0 +1,120 @@ +use crate::{get_wallet, spawn_anvil}; +use ethers_contract::abigen; +use ethers_core::{abi::AbiEncode, types::*}; +use ethers_middleware::{ + transformer::{ds_proxy::factory::DsProxyFactory, DsProxy, TransformerMiddleware}, + MiddlewareBuilder, SignerMiddleware, +}; +use ethers_providers::{Http, Middleware, Provider}; +use ethers_signers::{LocalWallet, Signer}; +use rand::Rng; +use std::sync::Arc; + +type HttpWallet = SignerMiddleware, LocalWallet>; + +abigen!(SimpleStorage, "../tests/testdata/SimpleStorage.json"); + +#[tokio::test] +#[ignore] +async fn ds_proxy_transformer() { + // randomness + let mut rng = rand::thread_rng(); + + // spawn anvil and instantiate a signer middleware. + let (provider, anvil) = spawn_anvil(); + let wallet = get_wallet(&anvil, 0); + let address = wallet.address(); + let provider = Arc::new(provider.with_signer(wallet)); + + // deploy DsProxyFactory which we'll use to deploy a new DsProxy contract. + let deploy_tx = DsProxyFactory::deploy(provider.clone(), ()).unwrap(); + let ds_proxy_factory = deploy_tx.send().await.unwrap(); + + // deploy a new DsProxy contract. + let ds_proxy = DsProxy::build::>( + provider.clone(), + Some(ds_proxy_factory.address()), + provider.address(), + ) + .await + .unwrap(); + let ds_proxy_addr = ds_proxy.address(); + + // deploy SimpleStorage and try to update its value via transformer middleware. + let deploy_tx = SimpleStorage::deploy(provider.clone(), ()).unwrap(); + let simple_storage = deploy_tx.send().await.unwrap(); + + // instantiate a new transformer middleware. + let provider = TransformerMiddleware::new(provider, ds_proxy); + + // broadcast the setValue tx via transformer middleware (first wallet). + let expected_value: u64 = rng.gen(); + let _receipt = simple_storage + .set_value(expected_value.into()) + .send() + .await + .unwrap() + .await + .unwrap() + .unwrap(); + + // verify that DsProxy's state was updated. + let last_sender = provider.get_storage_at(ds_proxy_addr, H256::zero(), None).await.unwrap(); + let last_value = + provider.get_storage_at(ds_proxy_addr, H256::from_low_u64_be(1u64), None).await.unwrap(); + assert_eq!(last_sender, address.into()); + assert_eq!(last_value, H256::from_low_u64_be(expected_value)); +} + +#[tokio::test] +async fn ds_proxy_code() { + // randomness + let mut rng = rand::thread_rng(); + + // spawn anvil and instantiate a signer middleware. + let (provider, anvil) = spawn_anvil(); + let wallet = get_wallet(&anvil, 0); + let address = wallet.address(); + let provider = Arc::new(provider.with_signer(wallet)); + + // deploy DsProxyFactory which we'll use to deploy a new DsProxy contract. + let deploy_tx = DsProxyFactory::deploy(provider.clone(), ()).unwrap(); + let ds_proxy_factory = deploy_tx.send().await.unwrap(); + + // deploy a new DsProxy contract. + let ds_proxy = DsProxy::build::>( + provider.clone(), + Some(ds_proxy_factory.address()), + provider.address(), + ) + .await + .unwrap(); + let ds_proxy_addr = ds_proxy.address(); + + // encode the calldata + let expected_value: u64 = rng.gen(); + let calldata = SetValueCall { value: expected_value.into() }.encode(); + + // execute code via the deployed DsProxy contract. + ds_proxy + .execute::, Bytes>( + Arc::clone(&provider), + SIMPLESTORAGE_BYTECODE.clone(), + calldata.into(), + ) + .expect("could not construct DSProxy contract call") + .legacy() + .send() + .await + .unwrap() + .await + .unwrap() + .unwrap(); + + // verify that DsProxy's state was updated. + let last_sender = provider.get_storage_at(ds_proxy_addr, H256::zero(), None).await.unwrap(); + let last_value = + provider.get_storage_at(ds_proxy_addr, H256::from_low_u64_be(1u64), None).await.unwrap(); + assert_eq!(last_sender, address.into()); + assert_eq!(last_value, H256::from_low_u64_be(expected_value)); +} diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs deleted file mode 100644 index 0129deca..00000000 --- a/ethers-middleware/tests/signer.rs +++ /dev/null @@ -1,342 +0,0 @@ -#![allow(unused)] - -use ethers_contract::ContractFactory; -use ethers_core::{ - abi::Abi, - types::*, - utils::{parse_ether, parse_units, Anvil}, -}; -use ethers_middleware::signer::SignerMiddleware; -use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI}; -use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer}; -use ethers_solc::Solc; -use once_cell::sync::Lazy; -use std::{ - convert::TryFrom, - iter::Cycle, - sync::{atomic::AtomicU8, Arc}, - time::Duration, -}; - -static WALLETS: Lazy = Lazy::new(|| { - TestWallets { - mnemonic: MnemonicBuilder::default() - // Please don't drain this :) - .phrase("impose air often almost medal sudden finish quote dwarf devote theme layer"), - next: Default::default(), - } -}); - -#[tokio::test] -#[cfg(not(feature = "celo"))] -async fn send_eth() { - let anvil = Anvil::new().spawn(); - - // this private key belongs to the above mnemonic - let wallet: LocalWallet = anvil.keys()[0].clone().into(); - let wallet2: LocalWallet = anvil.keys()[1].clone().into(); - - // connect to the network - let provider = Provider::::try_from(anvil.endpoint()) - .unwrap() - .interval(Duration::from_millis(10u64)); - let chain_id = provider.get_chainid().await.unwrap().as_u64(); - let provider = SignerMiddleware::new_with_provider_chain(provider, wallet).await.unwrap(); - - // craft the transaction - let tx = TransactionRequest::new().to(wallet2.address()).value(10000).chain_id(chain_id); - - let balance_before = provider.get_balance(provider.address(), None).await.unwrap(); - - // send it! - provider.send_transaction(tx, None).await.unwrap(); - - let balance_after = provider.get_balance(provider.address(), None).await.unwrap(); - - assert!(balance_before > balance_after); -} - -// hardhat compatibility test, to show hardhat rejects tx signed for other chains -#[tokio::test] -#[cfg(not(feature = "celo"))] -#[ignore] -async fn send_with_chain_id_hardhat() { - let wallet: LocalWallet = - "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse().unwrap(); - let provider = Provider::try_from("http://localhost:8545").unwrap(); - let client = SignerMiddleware::new(provider, wallet); - - let tx = TransactionRequest::new().to(Address::random()).value(100u64); - let res = client.send_transaction(tx, None).await; - - let err = res.unwrap_err(); - assert!(err - .to_string() - .contains("Trying to send an incompatible EIP-155 transaction, signed for another chain.")); -} - -#[tokio::test] -#[cfg(not(feature = "celo"))] -#[ignore] -async fn send_with_chain_id_anvil() { - let wallet: LocalWallet = - "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse().unwrap(); - let provider = Provider::try_from("http://localhost:8545").unwrap(); - let client = SignerMiddleware::new(provider, wallet); - - let tx = TransactionRequest::new().to(Address::random()).value(100u64); - let res = client.send_transaction(tx, None).await; - - let _err = res.unwrap_err(); -} - -// #[tokio::test] -// #[cfg(not(feature = "celo"))] -// async fn pending_txs_with_confirmations_testnet() { -// let provider = GOERLI.provider().interval(Duration::from_millis(3000)); -// let chain_id = provider.get_chainid().await.unwrap(); -// let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); -// let address = wallet.address(); -// let provider = SignerMiddleware::new(provider, wallet); -// generic_pending_txs_test(provider, address).await; -// } -// -// // different keys to avoid nonce errors -// #[tokio::test] -// #[cfg(not(feature = "celo"))] -// async fn websocket_pending_txs_with_confirmations_testnet() { -// let provider = GOERLI.ws().await.interval(Duration::from_millis(3000)); -// let chain_id = provider.get_chainid().await.unwrap(); -// let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); -// let address = wallet.address(); -// let provider = SignerMiddleware::new(provider, wallet); -// generic_pending_txs_test(provider, address).await; -// } - -// #[cfg(not(feature = "celo"))] -// async fn generic_pending_txs_test(provider: M, who: Address) { -// let tx = TransactionRequest::new().to(who).from(who); -// let pending_tx = provider.send_transaction(tx, None).await.unwrap(); -// let tx_hash = *pending_tx; -// let receipt = pending_tx.confirmations(1).await.unwrap().unwrap(); -// // got the correct receipt -// assert_eq!(receipt.transaction_hash, tx_hash); -// } -// -// #[tokio::test] -// #[cfg(not(feature = "celo"))] -// async fn typed_txs() { -// let provider = GOERLI.provider(); -// -// let chain_id = provider.get_chainid().await.unwrap(); -// let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); -// let address = wallet.address(); -// -// dbg!(&wallet, &address); -// // our wallet -// let provider = SignerMiddleware::new(provider, wallet); -// -// // Uncomment the below and run this test to re-fund the wallets if they get drained. -// // Would be ideal if we'd have a way to do this automatically, but this should be -// // happening rarely enough that it doesn't matter. -// // WALLETS.fund(provider.provider(), 10u32).await; -// -// async fn check_tx( -// pending_tx: ethers_providers::PendingTransaction<'_, P>, -// expected: u64, -// ) { -// let provider = pending_tx.provider(); -// let receipt = pending_tx.await.unwrap().unwrap(); -// let tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); -// assert_eq!(receipt.transaction_type, Some(expected.into())); -// assert_eq!(tx.transaction_type, Some(expected.into())); -// } -// -// let nonce = provider.get_transaction_count(address, None).await.unwrap(); -// let bn = Some(BlockNumber::Pending.into()); -// let gas_price = provider.get_gas_price().await.unwrap() * 125 / 100; -// -// let tx = -// TransactionRequest::new().from(address).to(address).nonce(nonce).gas_price(gas_price); -// let tx1 = provider.send_transaction(tx.clone(), bn).await.unwrap(); -// -// let tx = tx.clone().from(address).to(address).nonce(nonce + 1).with_access_list(vec![]); -// let tx2 = provider.send_transaction(tx, bn).await.unwrap(); -// -// let tx = Eip1559TransactionRequest::new() -// .from(address) -// .to(address) -// .nonce(nonce + 2) -// .max_fee_per_gas(gas_price) -// .max_priority_fee_per_gas(gas_price); -// let tx3 = provider.send_transaction(tx, bn).await.unwrap(); -// -// futures_util::join!(check_tx(tx1, 0), check_tx(tx2, 1), check_tx(tx3, 2),); -// } - -#[tokio::test] -#[cfg(feature = "celo")] -async fn test_send_transaction() { - // Celo testnet - let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") - .unwrap() - .interval(Duration::from_millis(3000u64)); - let chain_id = provider.get_chainid().await.unwrap().as_u64(); - - // Funded with https://celo.org/developers/faucet - // Please do not drain this account :) - let wallet = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" - .parse::() - .unwrap() - .with_chain_id(chain_id); - let client = SignerMiddleware::new(provider, wallet); - - let balance_before = client.get_balance(client.address(), None).await.unwrap(); - let tx = TransactionRequest::pay(client.address(), 100); - let _receipt = client.send_transaction(tx, None).await.unwrap().confirmations(3).await.unwrap(); - let balance_after = client.get_balance(client.address(), None).await.unwrap(); - assert!(balance_before > balance_after); -} - -#[tokio::test] -#[cfg(not(feature = "celo"))] -async fn send_transaction_handles_tx_from_field() { - // launch anvil - let anvil = Anvil::new().spawn(); - - // grab 2 wallets - let signer: LocalWallet = anvil.keys()[0].clone().into(); - let other: LocalWallet = anvil.keys()[1].clone().into(); - - // connect to the network - let provider = Provider::try_from(anvil.endpoint()).unwrap(); - let provider = - SignerMiddleware::new_with_provider_chain(provider, signer.clone()).await.unwrap(); - - // sending a TransactionRequest with a from field of None should result - // in a transaction from the signer address - let request_from_none = TransactionRequest::new(); - let receipt = - provider.send_transaction(request_from_none, None).await.unwrap().await.unwrap().unwrap(); - let sent_tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); - - assert_eq!(sent_tx.from, signer.address()); - - // sending a TransactionRequest with the signer as the from address should - // result in a transaction from the signer address - let request_from_signer = TransactionRequest::new().from(signer.address()); - let receipt = - provider.send_transaction(request_from_signer, None).await.unwrap().await.unwrap().unwrap(); - let sent_tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); - - assert_eq!(sent_tx.from, signer.address()); - - // sending a TransactionRequest with a from address that is not the signer - // should result in a transaction from the specified address - let request_from_other = TransactionRequest::new().from(other.address()); - let receipt = - provider.send_transaction(request_from_other, None).await.unwrap().await.unwrap().unwrap(); - let sent_tx = provider.get_transaction(receipt.transaction_hash).await.unwrap().unwrap(); - - assert_eq!(sent_tx.from, other.address()); -} - -#[tokio::test] -#[cfg(feature = "celo")] -async fn deploy_and_call_contract() { - // compiles the given contract and returns the ABI and Bytecode - fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { - let path = format!("./tests/solidity-contracts/{path}"); - let compiled = Solc::default().compile_source(&path).unwrap(); - let contract = compiled.get(&path, name).expect("could not find contract"); - let (abi, bin, _) = contract.into_parts_or_default(); - (abi, bin) - } - - let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage"); - - // Celo testnet - let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") - .unwrap() - .interval(Duration::from_millis(6000)); - let chain_id = provider.get_chainid().await.unwrap().as_u64(); - - // Funded with https://celo.org/developers/faucet - let wallet = "58ea5643a78c36926ad5128a6b0d8dfcc7fc705788a993b1c724be3469bc9697" - .parse::() - .unwrap() - .with_chain_id(chain_id); - let client = SignerMiddleware::new_with_provider_chain(provider, wallet).await.unwrap(); - let client = Arc::new(client); - - let factory = ContractFactory::new(abi, bytecode, client); - let deployer = factory.deploy(()).unwrap().legacy(); - let contract = deployer.block(BlockNumber::Pending).send().await.unwrap(); - - let value: U256 = contract.method("value", ()).unwrap().call().await.unwrap(); - assert_eq!(value, 0.into()); - - // make a state mutating transaction - // gas estimation costs are sometimes under-reported on celo, - // so we manually set it to avoid failures - let call = contract.method::<_, H256>("setValue", U256::from(1)).unwrap().gas(100000); - let pending_tx = call.send().await.unwrap(); - let _receipt = pending_tx.await.unwrap(); - - let value: U256 = contract.method("value", ()).unwrap().call().await.unwrap(); - assert_eq!(value, 1.into()); -} - -#[derive(Debug, Default)] -struct TestWallets { - mnemonic: MnemonicBuilder, - next: AtomicU8, -} - -impl TestWallets { - /// Helper for funding the wallets with an instantiated provider - #[allow(unused)] - pub async fn fund>(&self, provider: &Provider, n: U) { - let addrs = (0..n.into()).map(|i| self.get(i).address()).collect::>(); - // hardcoded funder address private key, GOERLI - let signer = "9867bd0f8d9e16c57f5251b35a73f6f903eb8eee1bdc7f15256d0dc09d1945fb" - .parse::() - .unwrap() - .with_chain_id(provider.get_chainid().await.unwrap().as_u64()); - let provider = SignerMiddleware::new(provider, signer); - let addr = provider.address(); - - let mut nonce = provider.get_transaction_count(addr, None).await.unwrap(); - let mut pending_txs = Vec::new(); - for addr in addrs { - println!("Funding wallet {addr:?}"); - let tx = TransactionRequest::new() - .nonce(nonce) - .to(addr) - // 0.1 eth per wallet - .value(parse_ether("1").unwrap()); - pending_txs.push( - provider.send_transaction(tx, Some(BlockNumber::Pending.into())).await.unwrap(), - ); - nonce += 1.into(); - } - - futures_util::future::join_all(pending_txs).await; - } - - pub fn next(&self) -> LocalWallet { - let idx = self.next.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - - // println!("Got wallet {:?}", wallet.address()); - self.get(idx) - } - - pub fn get>(&self, idx: T) -> LocalWallet { - self.mnemonic - .clone() - .index(idx) - .expect("index not found") - .build() - .expect("cannot build wallet") - } -} diff --git a/ethers-middleware/tests/solidity-contracts/DSProxy.sol b/ethers-middleware/tests/solidity-contracts/DSProxy.sol deleted file mode 100644 index 267d90cf..00000000 --- a/ethers-middleware/tests/solidity-contracts/DSProxy.sol +++ /dev/null @@ -1,206 +0,0 @@ -pragma solidity >=0.6.0; - -pragma solidity >=0.4.23; - -interface DSAuthority { - function canCall( - address src, address dst, bytes4 sig - ) external view returns (bool); -} - -contract DSAuthEvents { - event LogSetAuthority (address indexed authority); - event LogSetOwner (address indexed owner); -} - -contract DSAuth is DSAuthEvents { - DSAuthority public authority; - address public owner; - - constructor() public { - owner = msg.sender; - emit LogSetOwner(msg.sender); - } - - function setOwner(address owner_) - public - auth - { - owner = owner_; - emit LogSetOwner(owner); - } - - function setAuthority(DSAuthority authority_) - public - auth - { - authority = authority_; - emit LogSetAuthority(address(authority)); - } - - modifier auth { - require(isAuthorized(msg.sender, msg.sig), "ds-auth-unauthorized"); - _; - } - - function isAuthorized(address src, bytes4 sig) internal view returns (bool) { - if (src == address(this)) { - return true; - } else if (src == owner) { - return true; - } else if (authority == DSAuthority(address(uint160(0)))) { - return false; - } else { - return authority.canCall(src, address(this), sig); - } - } -} - -contract DSNote { - event LogNote( - bytes4 indexed sig, - address indexed guy, - bytes32 indexed foo, - bytes32 indexed bar, - uint256 wad, - bytes fax - ) anonymous; - - modifier note { - bytes32 foo; - bytes32 bar; - uint256 wad; - - assembly { - foo := calldataload(4) - bar := calldataload(36) - wad := callvalue() - } - - _; - - emit LogNote(msg.sig, msg.sender, foo, bar, wad, msg.data); - } -} - -// DSProxy -// Allows code execution using a persistant identity This can be very -// useful to execute a sequence of atomic actions. Since the owner of -// the proxy can be changed, this allows for dynamic ownership models -// i.e. a multisig -contract DSProxy is DSAuth, DSNote { - DSProxyCache public cache; // global cache for contracts - - constructor(address _cacheAddr) public { - require(setCache(_cacheAddr)); - } - - fallback() external payable { - } - - receive() external payable { - } - - // use the proxy to execute calldata _data on contract _code - function execute(bytes memory _code, bytes memory _data) - public - payable - returns (address target, bytes32 response) - { - target = cache.read(_code); - if (target == address(0x0)) { - // deploy contract & store its address in cache - target = cache.write(_code); - } - - response = execute(target, _data); - } - - function execute(address _target, bytes memory _data) - public - auth - note - payable - returns (bytes32 response) - { - require(_target != address(0x0)); - - // call contract in current context - assembly { - let succeeded := delegatecall(sub(gas(), 5000), _target, add(_data, 0x20), mload(_data), 0, 32) - response := mload(0) // load delegatecall output - switch iszero(succeeded) - case 1 { - // throw if delegatecall failed - revert(0, 0) - } - } - } - - //set new cache - function setCache(address _cacheAddr) - public - auth - note - returns (bool) - { - require(_cacheAddr != address(0x0)); // invalid cache address - cache = DSProxyCache(_cacheAddr); // overwrite cache - return true; - } -} - -// DSProxyFactory -// This factory deploys new proxy instances through build() -// Deployed proxy addresses are logged -contract DSProxyFactory { - event Created(address indexed sender, address indexed owner, address proxy, address cache); - mapping(address=>bool) public isProxy; - DSProxyCache public cache = new DSProxyCache(); - - // deploys a new proxy instance - // sets owner of proxy to caller - function build() public returns (DSProxy proxy) { - proxy = build(msg.sender); - } - - // deploys a new proxy instance - // sets custom owner of proxy - function build(address owner) public returns (DSProxy proxy) { - proxy = new DSProxy(address(cache)); - emit Created(msg.sender, owner, address(proxy), address(cache)); - proxy.setOwner(owner); - isProxy[address(proxy)] = true; - } -} - -// DSProxyCache -// This global cache stores addresses of contracts previously deployed -// by a proxy. This saves gas from repeat deployment of the same -// contracts and eliminates blockchain bloat. - -// By default, all proxies deployed from the same factory store -// contracts in the same cache. The cache a proxy instance uses can be -// changed. The cache uses the sha3 hash of a contract's bytecode to -// lookup the address -contract DSProxyCache { - mapping(bytes32 => address) cache; - - function read(bytes memory _code) public view returns (address) { - bytes32 hash = keccak256(_code); - return cache[hash]; - } - - function write(bytes memory _code) public returns (address target) { - assembly { - target := create(0, add(_code, 0x20), mload(_code)) - switch iszero(extcodesize(target)) - case 1 { - // throw if contract failed to deploy - revert(0, 0) - } - } - bytes32 hash = keccak256(_code); - cache[hash] = target; - } -} diff --git a/ethers-middleware/tests/solidity-contracts/SimpleStorage.sol b/ethers-middleware/tests/solidity-contracts/SimpleStorage.sol deleted file mode 100644 index 592913e1..00000000 --- a/ethers-middleware/tests/solidity-contracts/SimpleStorage.sol +++ /dev/null @@ -1,15 +0,0 @@ -pragma solidity >=0.4.24; - -contract SimpleStorage { - - event ValueChanged(address indexed author, address indexed oldAuthor, uint256 oldValue, uint256 newValue); - - address public lastSender; - uint256 public value; - - function setValue(uint256 _value) public { - emit ValueChanged(msg.sender, lastSender, value, _value); - value = _value; - lastSender = msg.sender; - } -} diff --git a/ethers-middleware/tests/transformer.rs b/ethers-middleware/tests/transformer.rs deleted file mode 100644 index c4b37c0a..00000000 --- a/ethers-middleware/tests/transformer.rs +++ /dev/null @@ -1,144 +0,0 @@ -#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] - -use ethers_contract::{BaseContract, ContractFactory}; -use ethers_core::{abi::Abi, types::*, utils::Anvil}; -use ethers_middleware::{ - transformer::{DsProxy, TransformerMiddleware}, - SignerMiddleware, -}; -use ethers_providers::{Http, Middleware, Provider}; -use ethers_signers::{LocalWallet, Signer}; -use ethers_solc::Solc; -use rand::Rng; -use std::{convert::TryFrom, sync::Arc, time::Duration}; - -type HttpWallet = SignerMiddleware, LocalWallet>; - -// compiles the given contract and returns the ABI and Bytecode -fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { - let path = format!("./tests/solidity-contracts/{path}"); - let compiled = Solc::default().compile_source(&path).unwrap(); - let contract = compiled.get(&path, name).expect("could not find contract"); - let (abi, bin, _) = contract.into_parts_or_default(); - (abi, bin) -} - -#[tokio::test] -async fn ds_proxy_transformer() { - // randomness - let mut rng = rand::thread_rng(); - - // spawn anvil and instantiate a signer middleware. - let anvil = Anvil::new().spawn(); - let wallet: LocalWallet = anvil.keys()[0].clone().into(); - let provider = Provider::::try_from(anvil.endpoint()) - .unwrap() - .interval(Duration::from_millis(10u64)); - let chain_id = provider.get_chainid().await.unwrap().as_u64(); - let wallet = wallet.with_chain_id(chain_id); - let signer_middleware = SignerMiddleware::new(provider.clone(), wallet); - let wallet_addr = signer_middleware.address(); - let provider = Arc::new(signer_middleware.clone()); - - // deploy DsProxyFactory which we'll use to deploy a new DsProxy contract. - let (abi, bytecode) = compile_contract("DSProxy.sol", "DSProxyFactory"); - let factory = ContractFactory::new(abi, bytecode, Arc::clone(&provider)); - let ds_proxy_factory = factory.deploy(()).unwrap().legacy(); - let ds_proxy_factory = ds_proxy_factory.send().await.unwrap(); - - // deploy a new DsProxy contract. - let ds_proxy = DsProxy::build::>( - Arc::clone(&provider), - Some(ds_proxy_factory.address()), - provider.address(), - ) - .await - .unwrap(); - let ds_proxy_addr = ds_proxy.address(); - - // deploy SimpleStorage and try to update its value via transformer middleware. - let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage"); - let factory = ContractFactory::new(abi, bytecode, Arc::clone(&provider)); - let deployer = factory.deploy(()).unwrap().legacy(); - let simple_storage = deployer.send().await.unwrap(); - - // instantiate a new transformer middleware. - let provider = TransformerMiddleware::new(signer_middleware, ds_proxy.clone()); - - // broadcast the setValue tx via transformer middleware (first wallet). - let expected_value: u64 = rng.gen(); - let calldata = simple_storage - .encode("setValue", U256::from(expected_value)) - .expect("could not get ABI encoded data"); - let tx = TransactionRequest::new().to(simple_storage.address()).data(calldata); - provider.send_transaction(tx, None).await.unwrap().await.unwrap(); - - // verify that DsProxy's state was updated. - let last_sender = provider.get_storage_at(ds_proxy_addr, H256::zero(), None).await.unwrap(); - let last_value = - provider.get_storage_at(ds_proxy_addr, H256::from_low_u64_be(1u64), None).await.unwrap(); - assert_eq!(last_sender, wallet_addr.into()); - assert_eq!(last_value, H256::from_low_u64_be(expected_value)); -} - -#[tokio::test] -async fn ds_proxy_code() { - // randomness - let mut rng = rand::thread_rng(); - - // spawn anvil and instantiate a signer middleware. - let anvil = Anvil::new().spawn(); - let wallet: LocalWallet = anvil.keys()[1].clone().into(); - let provider = Provider::::try_from(anvil.endpoint()) - .unwrap() - .interval(Duration::from_millis(10u64)); - let chain_id = provider.get_chainid().await.unwrap().as_u64(); - let wallet = wallet.with_chain_id(chain_id); - let signer_middleware = SignerMiddleware::new(provider.clone(), wallet); - let wallet_addr = signer_middleware.address(); - let provider = Arc::new(signer_middleware.clone()); - - // deploy DsProxyFactory which we'll use to deploy a new DsProxy contract. - let (abi, bytecode) = compile_contract("DSProxy.sol", "DSProxyFactory"); - let factory = ContractFactory::new(abi, bytecode, Arc::clone(&provider)); - let ds_proxy_factory = factory.deploy(()).unwrap().legacy(); - let ds_proxy_factory = ds_proxy_factory.send().await.unwrap(); - - // deploy a new DsProxy contract. - let ds_proxy = DsProxy::build::>( - Arc::clone(&provider), - Some(ds_proxy_factory.address()), - provider.address(), - ) - .await - .unwrap(); - let ds_proxy_addr = ds_proxy.address(); - - // compile the SimpleStorage contract which we will use to interact via DsProxy. - let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage"); - let ss_base_contract: BaseContract = abi.into(); - let expected_value: u64 = rng.gen(); - let calldata = ss_base_contract - .encode("setValue", U256::from(expected_value)) - .expect("could not get ABI encoded data"); - - // execute code via the deployed DsProxy contract. - ds_proxy - .execute::, Bytes>( - Arc::clone(&provider), - bytecode.clone(), - calldata, - ) - .expect("could not construct DSProxy contract call") - .legacy() - .send() - .await - .unwrap(); - - // verify that DsProxy's state was updated. - let last_sender = provider.get_storage_at(ds_proxy_addr, H256::zero(), None).await.unwrap(); - let last_value = - provider.get_storage_at(ds_proxy_addr, H256::from_low_u64_be(1u64), None).await.unwrap(); - assert_eq!(last_sender, wallet_addr.into()); - assert_eq!(last_value, H256::from_low_u64_be(expected_value)); -} diff --git a/ethers-providers/src/rpc/provider.rs b/ethers-providers/src/rpc/provider.rs index 5f835393..aab670ea 100644 --- a/ethers-providers/src/rpc/provider.rs +++ b/ethers-providers/src/rpc/provider.rs @@ -1864,27 +1864,14 @@ mod tests { #[tokio::test] async fn geth_admin_nodeinfo() { // we can't use the test provider because infura does not expose admin endpoints - let port = 8546u16; - let p2p_listener_port = 13337u16; - let authrpc_port = 8552u16; let network = 1337u64; let temp_dir = tempfile::tempdir().unwrap().into_path(); - let (geth, provider) = spawn_geth_and_create_provider( - network, - port, - p2p_listener_port, - authrpc_port, - Some(temp_dir), - None, - ); + let (geth, provider) = spawn_geth_and_create_provider(network, Some(temp_dir), None); let info = provider.node_info().await.unwrap(); drop(geth); - // check that the port we set works - assert_eq!(info.ports.listener, p2p_listener_port); - // make sure it is running eth assert!(info.protocols.eth.is_some()); @@ -1897,18 +1884,10 @@ mod tests { /// These will all use the same genesis config. fn spawn_geth_and_create_provider( chain_id: u64, - rpc_port: u16, - p2p_port: u16, - authrpc_port: u16, datadir: Option, genesis: Option, ) -> (GethInstance, Provider) { - let geth = Geth::new() - .port(rpc_port) - .p2p_port(p2p_port) - .authrpc_port(authrpc_port) - .chain_id(chain_id) - .disable_discovery(); + let geth = Geth::new().chain_id(chain_id).disable_discovery(); let geth = match genesis { Some(genesis) => geth.genesis(genesis), @@ -1921,8 +1900,7 @@ mod tests { } .spawn(); - let url = format!("http://127.0.0.1:{rpc_port}"); - let provider = Provider::try_from(url).unwrap(); + let provider = Provider::try_from(geth.endpoint()).unwrap(); (geth, provider) } @@ -1934,26 +1912,13 @@ mod tests { chain_id: u64, genesis: Option, ) -> Vec<(GethInstance, Provider)> { - let mut geths = Vec::new(); - let mut p2p_port = 30303; - let mut rpc_port = 8545; - let mut authrpc_port = 8551; + let mut geths = Vec::with_capacity(datadirs.len()); for dir in datadirs { - let (geth, provider) = spawn_geth_and_create_provider( - chain_id, - rpc_port, - p2p_port, - authrpc_port, - Some(dir), - genesis.clone(), - ); + let (geth, provider) = + spawn_geth_and_create_provider(chain_id, Some(dir), genesis.clone()); geths.push((geth, provider)); - - p2p_port += 1; - rpc_port += 1; - authrpc_port += 1; } geths diff --git a/ethers-providers/src/rpc/transports/ipc.rs b/ethers-providers/src/rpc/transports/ipc.rs index 8d892257..2ead4729 100644 --- a/ethers-providers/src/rpc/transports/ipc.rs +++ b/ethers-providers/src/rpc/transports/ipc.rs @@ -513,6 +513,7 @@ impl crate::RpcError for IpcError { mod tests { use super::*; use ethers_core::utils::{Geth, GethInstance}; + use std::time::Duration; use tempfile::NamedTempFile; async fn connect() -> (Ipc, GethInstance) { @@ -534,7 +535,7 @@ mod tests { let (ipc, _geth) = connect().await; let block_num: U256 = ipc.request("eth_blockNumber", ()).await.unwrap(); - std::thread::sleep(std::time::Duration::new(3, 0)); + tokio::time::sleep(Duration::from_secs(2)).await; let block_num2: U256 = ipc.request("eth_blockNumber", ()).await.unwrap(); assert!(block_num2 > block_num); } @@ -559,6 +560,8 @@ mod tests { }) .collect() .await; - assert_eq!(blocks, vec![1, 2, 3]); + // `[1, 2, 3]` or `[2, 3, 4]` etc, depending on test latency + assert_eq!(blocks[2], blocks[1] + 1); + assert_eq!(blocks[1], blocks[0] + 1); } } diff --git a/ethers-providers/tests/it/main.rs b/ethers-providers/tests/it/main.rs new file mode 100644 index 00000000..06d192e2 --- /dev/null +++ b/ethers-providers/tests/it/main.rs @@ -0,0 +1,53 @@ +#![cfg(not(target_arch = "wasm32"))] + +use ethers_core::utils::{Anvil, AnvilInstance}; +use ethers_providers::{Http, Provider}; +use std::time::Duration; + +#[cfg(feature = "ipc")] +use ethers_providers::Ipc; +#[cfg(feature = "ipc")] +use tempfile::NamedTempFile; + +#[cfg(feature = "ws")] +use ethers_providers::Ws; + +mod provider; + +mod txpool; + +#[cfg(not(feature = "celo"))] +mod ws_errors; + +/// Spawns Anvil and instantiates an Http provider. +pub fn spawn_anvil() -> (Provider, AnvilInstance) { + let anvil = Anvil::new().block_time(1u64).spawn(); + let provider = Provider::::try_from(anvil.endpoint()) + .unwrap() + .interval(Duration::from_millis(50u64)); + (provider, anvil) +} + +/// Spawns Anvil and instantiates a Ws provider. +#[cfg(feature = "ws")] +pub async fn spawn_anvil_ws() -> (Provider, AnvilInstance) { + let anvil = Anvil::new().block_time(1u64).spawn(); + let provider = Provider::::connect(anvil.ws_endpoint()) + .await + .unwrap() + .interval(Duration::from_millis(50u64)); + (provider, anvil) +} + +/// Spawns Anvil and instantiates a Ipc provider. +#[cfg(feature = "ipc")] +pub async fn spawn_anvil_ipc() -> (Provider, AnvilInstance, NamedTempFile) { + let ipc = NamedTempFile::new().unwrap(); + let anvil = + Anvil::new().block_time(1u64).arg("--ipc").arg(ipc.path().display().to_string()).spawn(); + let provider = Provider::::connect_ipc(ipc.path()) + .await + .unwrap() + .interval(Duration::from_millis(50u64)); + (provider, anvil, ipc) +} diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/it/provider.rs similarity index 52% rename from ethers-providers/tests/provider.rs rename to ethers-providers/tests/it/provider.rs index 4878964b..ec595281 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/it/provider.rs @@ -1,15 +1,8 @@ -#![cfg(not(target_arch = "wasm32"))] -use ethers_providers::{Http, Middleware, Provider}; -use std::{convert::TryFrom, time::Duration}; - #[cfg(not(feature = "celo"))] mod eth_tests { - use super::*; - use ethers_core::{ - types::{Address, BlockId, TransactionRequest, H256}, - utils::Anvil, - }; - use ethers_providers::GOERLI; + use crate::spawn_anvil; + use ethers_core::types::{Address, BlockId, BlockNumber, TransactionRequest, H256}; + use ethers_providers::{Middleware, StreamExt, GOERLI}; #[tokio::test] async fn non_existing_data_works() { @@ -35,53 +28,10 @@ mod eth_tests { // Without TLS this would error with "TLS Support not compiled in" #[tokio::test] + #[cfg(any(feature = "openssl", feature = "rustls"))] async fn ssl_websocket() { let provider = GOERLI.ws().await; - let _number = provider.get_block_number().await.unwrap(); - } - - #[tokio::test] - async fn watch_blocks_websocket() { - use ethers_core::types::H256; - use ethers_providers::{StreamExt, Ws}; - - let anvil = Anvil::new().block_time(2u64).spawn(); - let (ws, _) = tokio_tungstenite::connect_async(anvil.ws_endpoint()).await.unwrap(); - let provider = Provider::new(Ws::new(ws)).interval(Duration::from_millis(500u64)); - - let stream = provider.watch_blocks().await.unwrap().stream(); - - let _blocks = stream.take(3usize).collect::>().await; - let _number = provider.get_block_number().await.unwrap(); - } - - #[tokio::test] - async fn pending_txs_with_confirmations_anvil() { - let anvil = Anvil::new().block_time(2u64).spawn(); - let provider = Provider::::try_from(anvil.endpoint()) - .unwrap() - .interval(Duration::from_millis(500u64)); - let accounts = provider.get_accounts().await.unwrap(); - generic_pending_txs_test(provider, accounts[0]).await; - } - - #[tokio::test] - async fn websocket_pending_txs_with_confirmations_anvil() { - use ethers_providers::Ws; - let anvil = Anvil::new().block_time(2u64).spawn(); - let ws = Ws::connect(anvil.ws_endpoint()).await.unwrap(); - let provider = Provider::new(ws); - let accounts = provider.get_accounts().await.unwrap(); - generic_pending_txs_test(provider, accounts[0]).await; - } - - async fn generic_pending_txs_test(provider: M, who: Address) { - let tx = TransactionRequest::new().to(who).from(who); - let pending_tx = provider.send_transaction(tx, None).await.unwrap(); - let tx_hash = *pending_tx; - let receipt = pending_tx.confirmations(3).await.unwrap().unwrap(); - // got the correct receipt - assert_eq!(receipt.transaction_hash, tx_hash); + assert_ne!(provider.get_block_number().await.unwrap(), 0.into()); } #[tokio::test] @@ -93,33 +43,93 @@ mod eth_tests { } #[tokio::test] - #[ignore] - async fn test_hardhat_compatibility() { - use ethers_providers::RetryClient; + async fn watch_blocks_http() { + let (provider, _anvil) = spawn_anvil(); + generic_watch_blocks_test(provider).await; + } - async fn send_zst_requests(provider: M) { - let _ = provider.get_chainid().await.unwrap(); - let _ = provider.get_block_number().await.unwrap(); - let _ = provider.get_gas_price().await.unwrap(); - let _ = provider.get_accounts().await.unwrap(); - let _ = provider.get_net_version().await.unwrap(); - } + #[tokio::test] + #[cfg(feature = "ws")] + async fn watch_blocks_ws() { + let (provider, _anvil) = crate::spawn_anvil_ws().await; + generic_watch_blocks_test(provider).await; + } - let provider = Provider::::try_from("http://localhost:8545").unwrap(); - send_zst_requests(provider).await; + #[tokio::test] + #[cfg(feature = "ipc")] + async fn watch_blocks_ipc() { + let (provider, _anvil, _ipc) = crate::spawn_anvil_ipc().await; + generic_watch_blocks_test(provider).await; + } - let provider = - Provider::>::new_client("http://localhost:8545", 10, 200).unwrap(); + async fn generic_watch_blocks_test(provider: M) { + let stream = provider.watch_blocks().await.unwrap().stream(); + let hashes = stream.take(3).collect::>().await; + let block = provider.get_block(BlockNumber::Latest).await.unwrap().unwrap(); + assert_eq!(block.hash.unwrap(), *hashes.last().unwrap()); + } - send_zst_requests(provider).await; + #[tokio::test] + #[cfg(feature = "ws")] + async fn subscribe_blocks_ws() { + let (provider, _anvil) = crate::spawn_anvil_ws().await; + generic_subscribe_blocks_test(provider).await; + } + + #[tokio::test] + #[cfg(feature = "ipc")] + async fn subscribe_blocks_ipc() { + let (provider, _anvil, _ipc) = crate::spawn_anvil_ipc().await; + generic_subscribe_blocks_test(provider).await; + } + + #[cfg(any(feature = "ws", feature = "ipc"))] + async fn generic_subscribe_blocks_test(provider: M) + where + M: Middleware, + M::Provider: ethers_providers::PubsubClient, + { + let stream = provider.subscribe_blocks().await.unwrap(); + let blocks = stream.take(3).collect::>().await; + let block = provider.get_block(BlockNumber::Latest).await.unwrap().unwrap(); + assert_eq!(&block, blocks.last().unwrap()); + } + + #[tokio::test] + async fn send_tx_http() { + let (provider, anvil) = spawn_anvil(); + generic_send_tx_test(provider, anvil.addresses()[0]).await; + } + + #[tokio::test] + #[cfg(feature = "ws")] + async fn send_tx_ws() { + let (provider, anvil) = crate::spawn_anvil_ws().await; + generic_send_tx_test(provider, anvil.addresses()[0]).await; + } + + #[tokio::test] + #[cfg(feature = "ipc")] + async fn send_tx_ipc() { + let (provider, anvil, _ipc) = crate::spawn_anvil_ipc().await; + generic_send_tx_test(provider, anvil.addresses()[0]).await; + } + + async fn generic_send_tx_test(provider: M, who: Address) { + let tx = TransactionRequest::new().to(who).from(who); + let pending_tx = provider.send_transaction(tx, None).await.unwrap(); + let tx_hash = *pending_tx; + let receipt = pending_tx.confirmations(3).await.unwrap().unwrap(); + assert_eq!(receipt.transaction_hash, tx_hash); } } #[cfg(feature = "celo")] mod celo_tests { - use super::*; use ethers_core::types::{Randomness, H256}; + use ethers_providers::{Http, Middleware, Provider}; use futures_util::stream::StreamExt; + use std::time::Duration; #[tokio::test] async fn get_block() { diff --git a/ethers-providers/tests/txpool.rs b/ethers-providers/tests/it/txpool.rs similarity index 77% rename from ethers-providers/tests/txpool.rs rename to ethers-providers/tests/it/txpool.rs index 868e57f6..bb5f2a7d 100644 --- a/ethers-providers/tests/txpool.rs +++ b/ethers-providers/tests/it/txpool.rs @@ -1,4 +1,3 @@ -#![cfg(not(target_arch = "wasm32"))] use ethers_core::{ types::{TransactionRequest, U256}, utils::Anvil, @@ -8,8 +7,8 @@ use std::convert::TryFrom; #[tokio::test] async fn txpool() { - let geth = Anvil::new().block_time(20u64).spawn(); - let provider = Provider::::try_from(geth.endpoint()).unwrap(); + let anvil = Anvil::new().block_time(5u64).spawn(); + let provider = Provider::::try_from(anvil.endpoint()).unwrap(); let account = provider.get_accounts().await.unwrap()[0]; let value: u64 = 42; @@ -17,13 +16,11 @@ async fn txpool() { let tx = TransactionRequest::new().to(account).from(account).value(value).gas_price(gas_price); // send a few transactions - let mut txs = Vec::new(); for _ in 0..10 { - let tx_hash = provider.send_transaction(tx.clone(), None).await.unwrap(); - txs.push(tx_hash); + drop(provider.send_transaction(tx.clone(), None).await.unwrap()); } - // we gave a 20s block time, should be plenty for us to get the txpool's content + // we gave a 5s block time, should be plenty for us to get the txpool's content let status = provider.txpool_status().await.unwrap(); assert_eq!(status.pending.as_u64(), 10); assert_eq!(status.queued.as_u64(), 0); diff --git a/ethers-providers/tests/ws_errors.rs b/ethers-providers/tests/it/ws_errors.rs similarity index 95% rename from ethers-providers/tests/ws_errors.rs rename to ethers-providers/tests/it/ws_errors.rs index 170771a8..578bc1fd 100644 --- a/ethers-providers/tests/ws_errors.rs +++ b/ethers-providers/tests/it/ws_errors.rs @@ -1,11 +1,10 @@ -#![cfg(not(feature = "celo"))] - +use ethers_core::types::Filter; use ethers_providers::{Middleware, Provider, StreamExt, Ws}; use futures_util::SinkExt; use std::time::Duration; use tokio::net::{TcpListener, TcpStream}; use tokio_tungstenite::{ - accept_async, + accept_async, connect_async, tungstenite::{ self, protocol::{frame::coding::CloseCode, CloseFrame}, @@ -16,9 +15,6 @@ use tungstenite::protocol::Message; const WS_ENDPOINT: &str = "127.0.0.1:9002"; -use ethers_core::types::Filter; -use tokio_tungstenite::connect_async; - #[tokio::test] async fn graceful_disconnect_on_ws_errors() { // Spawn a fake Ws server that will drop our connection after a while diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 505db6a7..d9551109 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -46,11 +46,8 @@ home = { version = "0.5.4", optional = true } [dev-dependencies] ethers-contract-derive = { version = "^1.0.0", path = "../ethers-contract/ethers-contract-derive" } ethers-derive-eip712 = { version = "^1.0.0", path = "../ethers-core/ethers-derive-eip712" } -serde_json = { version = "1.0.64" } -tracing-subscriber = "0.3.16" -yubihsm = { version = "0.41.0", features = ["secp256k1", "usb", "mockhsm"] } -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +serde_json = { version = "1.0.64" } yubihsm = { version = "0.41.0", features = ["secp256k1", "usb", "mockhsm"] } tokio = { version = "1.18", default-features = false, features = ["macros", "rt"] } tempfile = "3.4.0" diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs index 1d20cabe..ca2e7aa7 100644 --- a/ethers-signers/src/wallet/private_key.rs +++ b/ethers-signers/src/wallet/private_key.rs @@ -164,7 +164,7 @@ mod tests { #[test] fn parse_pk() { let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b"; - let pk: Wallet = s.parse().unwrap(); + let _pk: Wallet = s.parse().unwrap(); } #[tokio::test] diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs index e1ec2854..17da2b87 100644 --- a/ethers-solc/src/artifact_output/mod.rs +++ b/ethers-solc/src/artifact_output/mod.rs @@ -1082,6 +1082,7 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback { } #[cfg(test)] +#[allow(clippy::extra_unused_type_parameters)] mod tests { use super::*; diff --git a/examples/geth_clique.rs b/examples/geth_clique.rs index 4df83b73..68be285e 100644 --- a/examples/geth_clique.rs +++ b/examples/geth_clique.rs @@ -1,3 +1,5 @@ +//! Instantiate `Geth` with Clique enabled. + use ethers::{ core::{rand::thread_rng, utils::Geth}, signers::LocalWallet, @@ -5,7 +7,6 @@ use ethers::{ use eyre::Result; #[tokio::main] -/// Shows how to instantiate a Geth with Clique enabled. async fn main() -> Result<()> { // Generate a random clique signer and set it on Geth. let data_dir = tempfile::tempdir().expect("should be able to create temp geth datadir"); diff --git a/examples/middleware/examples/gas_oracle.rs b/examples/middleware/examples/gas_oracle.rs index fdf41801..9393a5c5 100644 --- a/examples/middleware/examples/gas_oracle.rs +++ b/examples/middleware/examples/gas_oracle.rs @@ -38,15 +38,7 @@ async fn blocknative() { } async fn etherscan() { - let chain = Chain::Mainnet; - let client = match Client::new_from_env(chain) { - Ok(client) => client, - Err(_) => Client::builder() - .chain(chain) - .expect("Mainnet is valid") - .build() - .expect("Mainnet is valid"), - }; + let client = Client::new_from_opt_env(Chain::Mainnet).unwrap(); let oracle = Etherscan::new(client).category(GasCategory::Fast); match oracle.fetch().await { Ok(gas_price) => println!("[Etherscan]: Gas price is {gas_price:?}"), diff --git a/examples/middleware/examples/nonce_manager.rs b/examples/middleware/examples/nonce_manager.rs index e76bcc1c..594c8ed5 100644 --- a/examples/middleware/examples/nonce_manager.rs +++ b/examples/middleware/examples/nonce_manager.rs @@ -37,7 +37,7 @@ async fn main() -> Result<()> { assert_eq!(curr_nonce, 0); - nonce_manager.send_transaction(tx, None).await?; + nonce_manager.send_transaction(tx, None).await?.await?.unwrap(); let next_nonce = nonce_manager.next().as_u64(); assert_eq!(next_nonce, 1); diff --git a/src/lib.rs b/src/lib.rs index 19b5a35b..5fd61c2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,3 @@ -#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms, unreachable_pub)] -#![allow(rustdoc::broken_intra_doc_links)] -#![doc(test( - no_crate_inject, - attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) -))] //! # ethers-rs //! //! A complete Ethereum and Celo Rust library. diff --git a/tests/live/celo.rs b/tests/live/celo.rs new file mode 100644 index 00000000..7457e0dd --- /dev/null +++ b/tests/live/celo.rs @@ -0,0 +1,59 @@ +use crate::simple_storage::SimpleStorage; +use ethers::prelude::*; +use std::{sync::Arc, time::Duration}; + +static CELO_TESTNET_URL: &str = "https://alfajores-forno.celo-testnet.org"; + +#[tokio::test] +async fn test_send_transaction() { + // Celo testnet + let provider = + Provider::::try_from(CELO_TESTNET_URL).unwrap().interval(Duration::from_secs(3)); + let chain_id = provider.get_chainid().await.unwrap().as_u64(); + + // Funded with https://celo.org/developers/faucet + // Please do not drain this account :) + let wallet = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" + .parse::() + .unwrap() + .with_chain_id(chain_id); + let client = SignerMiddleware::new(provider, wallet); + + let balance_before = client.get_balance(client.address(), None).await.unwrap(); + let tx = TransactionRequest::pay(client.address(), 100); + let _receipt = client.send_transaction(tx, None).await.unwrap().confirmations(3).await.unwrap(); + let balance_after = client.get_balance(client.address(), None).await.unwrap(); + assert!(balance_before > balance_after); +} + +#[tokio::test] +async fn deploy_and_call_contract() { + // Celo testnet + let provider = + Provider::::try_from(CELO_TESTNET_URL).unwrap().interval(Duration::from_secs(3)); + let chain_id = provider.get_chainid().await.unwrap().as_u64(); + + // Funded with https://celo.org/developers/faucet + let wallet = "58ea5643a78c36926ad5128a6b0d8dfcc7fc705788a993b1c724be3469bc9697" + .parse::() + .unwrap() + .with_chain_id(chain_id); + let client = provider.with_signer(wallet); + let client = Arc::new(client); + + let deploy_tx = SimpleStorage::deploy(client, ()).unwrap(); + let contract = deploy_tx.send().await.unwrap(); + + let value: U256 = contract.value().call().await.unwrap(); + assert_eq!(value, 0.into()); + + // make a state mutating transaction + // gas estimation costs are sometimes under-reported on celo, + // so we manually set it to avoid failures + let call = contract.set_value(1.into()).gas(100000); + let pending_tx = call.send().await.unwrap(); + let _receipt = pending_tx.await.unwrap(); + + let value: U256 = contract.method("value", ()).unwrap().call().await.unwrap(); + assert_eq!(value, 1.into()); +} diff --git a/tests/live/main.rs b/tests/live/main.rs new file mode 100644 index 00000000..175e4b80 --- /dev/null +++ b/tests/live/main.rs @@ -0,0 +1,13 @@ +//! Ethers live tests. +//! +//! If a feature or external binary is added, like Solc, please also update +//! `.github/workflows/ci.yml` at `job.live-test`. + +#![cfg(not(target_arch = "wasm32"))] + +#[cfg(feature = "celo")] +mod celo; + +pub(crate) mod simple_storage { + ethers::contract::abigen!(SimpleStorage, "./tests/testdata/SimpleStorage.json"); +} diff --git a/tests/testdata/SimpleStorage.json b/tests/testdata/SimpleStorage.json new file mode 100644 index 00000000..b52ec0a5 --- /dev/null +++ b/tests/testdata/SimpleStorage.json @@ -0,0 +1 @@ +{"abi":[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldValue","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"value","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}],"bytecode":"608060405234801561001057600080fd5b50610155806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063256fec88146100465780633fa4f24514610076578063552410771461008d575b600080fd5b600054610059906001600160a01b031681565b6040516001600160a01b0390911681526020015b60405180910390f35b61007f60015481565b60405190815260200161006d565b6100a061009b366004610106565b6100a2565b005b60005460015460408051918252602082018490526001600160a01b039092169133917f43a35cb4f6bbf56c64a97aba2d057d75ae7c2b008cfbbcf77c9bd7f2a525d969910160405180910390a3600155600080546001600160a01b03191633179055565b60006020828403121561011857600080fd5b503591905056fea26469706673582212202aa583b44008e0a0ecde172d743bed0508a31118ec816f86cd3551d73480e13d64736f6c63430008130033"} diff --git a/tests/testdata/the_dao_abi.expr b/tests/testdata/the_dao_abi.expr new file mode 100644 index 00000000..b2c3cfaa --- /dev/null +++ b/tests/testdata/the_dao_abi.expr @@ -0,0 +1 @@ +"[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"proposals\",\"outputs\":[{\"name\":\"recipient\",\"type\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"votingDeadline\",\"type\":\"uint256\"},{\"name\":\"open\",\"type\":\"bool\"},{\"name\":\"proposalPassed\",\"type\":\"bool\"},{\"name\":\"proposalHash\",\"type\":\"bytes32\"},{\"name\":\"proposalDeposit\",\"type\":\"uint256\"},{\"name\":\"newCurator\",\"type\":\"bool\"},{\"name\":\"yea\",\"type\":\"uint256\"},{\"name\":\"nay\",\"type\":\"uint256\"},{\"name\":\"creator\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_spender\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minTokensToCreate\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"rewardAccount\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"daoCreator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"divisor\",\"outputs\":[{\"name\":\"divisor\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"extraBalance\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"}],\"name\":\"executeProposal\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"unblockMe\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalRewardToken\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"actualBalance\",\"outputs\":[{\"name\":\"_actualBalance\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"closingTime\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowedRecipients\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferWithoutReward\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"refund\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"},{\"name\":\"_description\",\"type\":\"string\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"},{\"name\":\"_debatingPeriod\",\"type\":\"uint256\"},{\"name\":\"_newCurator\",\"type\":\"bool\"}],\"name\":\"newProposal\",\"outputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"DAOpaidOut\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minQuorumDivisor\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newContract\",\"type\":\"address\"}],\"name\":\"newContract\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"balance\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_allowed\",\"type\":\"bool\"}],\"name\":\"changeAllowedRecipients\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"halveMinQuorum\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"paidOut\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_newCurator\",\"type\":\"address\"}],\"name\":\"splitDAO\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"DAOrewardAccount\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"proposalDeposit\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"numberOfProposals\",\"outputs\":[{\"name\":\"_numberOfProposals\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"lastTimeMinQuorumMet\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_toMembers\",\"type\":\"bool\"}],\"name\":\"retrieveDAOReward\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"receiveEther\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"isFueled\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_tokenHolder\",\"type\":\"address\"}],\"name\":\"createTokenProxy\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"}],\"name\":\"getNewDAOAddress\",\"outputs\":[{\"name\":\"_newDAO\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_supportsProposal\",\"type\":\"bool\"}],\"name\":\"vote\",\"outputs\":[{\"name\":\"_voteID\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"getMyReward\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"rewardToken\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFromWithoutReward\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"remaining\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalDeposit\",\"type\":\"uint256\"}],\"name\":\"changeProposalDeposit\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"blocked\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"curator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"}],\"name\":\"checkProposalCode\",\"outputs\":[{\"name\":\"_codeChecksOut\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"privateCreation\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"inputs\":[{\"name\":\"_curator\",\"type\":\"address\"},{\"name\":\"_daoCreator\",\"type\":\"address\"},{\"name\":\"_proposalDeposit\",\"type\":\"uint256\"},{\"name\":\"_minTokensToCreate\",\"type\":\"uint256\"},{\"name\":\"_closingTime\",\"type\":\"uint256\"},{\"name\":\"_privateCreation\",\"type\":\"address\"}],\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"FuelingToDate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"CreatedToken\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Refund\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"newCurator\",\"type\":\"bool\"},{\"indexed\":false,\"name\":\"description\",\"type\":\"string\"}],\"name\":\"ProposalAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"position\",\"type\":\"bool\"},{\"indexed\":true,\"name\":\"voter\",\"type\":\"address\"}],\"name\":\"Voted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"result\",\"type\":\"bool\"},{\"indexed\":false,\"name\":\"quorum\",\"type\":\"uint256\"}],\"name\":\"ProposalTallied\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_newCurator\",\"type\":\"address\"}],\"name\":\"NewCurator\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_recipient\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_allowed\",\"type\":\"bool\"}],\"name\":\"AllowedRecipientChanged\",\"type\":\"event\"}]" diff --git a/ethers-etherscan/resources/IERC20.sol b/tests/testdata/uniswap/IERC20.sol similarity index 100% rename from ethers-etherscan/resources/IERC20.sol rename to tests/testdata/uniswap/IERC20.sol diff --git a/ethers-etherscan/resources/UniswapExchange.sol b/tests/testdata/uniswap/UniswapExchange.sol similarity index 100% rename from ethers-etherscan/resources/UniswapExchange.sol rename to tests/testdata/uniswap/UniswapExchange.sol