├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── pull-request.yaml │ └── release.yaml ├── .gitignore ├── .mergify.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── ci ├── bump.sh └── deploy.sh ├── ethcontract-common ├── Cargo.toml └── src │ ├── abiext.rs │ ├── artifact.rs │ ├── artifact │ ├── hardhat.rs │ └── truffle.rs │ ├── bytecode.rs │ ├── contract.rs │ ├── errors.rs │ ├── hash.rs │ └── lib.rs ├── ethcontract-derive ├── Cargo.toml └── src │ ├── lib.rs │ └── spanned.rs ├── ethcontract-generate ├── Cargo.toml ├── README.md └── src │ ├── generate.rs │ ├── generate │ ├── common.rs │ ├── deployment.rs │ ├── events.rs │ ├── methods.rs │ └── types.rs │ ├── lib.rs │ ├── rustfmt.rs │ ├── source.rs │ ├── test │ └── macros.rs │ └── util.rs ├── ethcontract-mock ├── Cargo.toml └── src │ ├── details │ ├── default.rs │ ├── mod.rs │ ├── parse.rs │ ├── sign.rs │ └── transaction.rs │ ├── lib.rs │ ├── predicate.rs │ ├── range.rs │ ├── test │ ├── batch.rs │ ├── doctest │ │ └── common.rs │ ├── eth_block_number.rs │ ├── eth_chain_id.rs │ ├── eth_estimate_gas.rs │ ├── eth_gas_price.rs │ ├── eth_get_transaction_receipt.rs │ ├── eth_send_transaction.rs │ ├── eth_transaction_count.rs │ ├── mod.rs │ ├── net_version.rs │ └── returns.rs │ └── utils.rs ├── ethcontract ├── Cargo.toml └── src │ ├── batch.rs │ ├── contract.rs │ ├── contract │ ├── deploy.rs │ ├── event.rs │ ├── event │ │ └── data.rs │ └── method.rs │ ├── errors.rs │ ├── errors │ ├── ganache.rs │ ├── geth.rs │ ├── hardhat.rs │ ├── nethermind.rs │ ├── parity.rs │ └── revert.rs │ ├── int.rs │ ├── lib.rs │ ├── log.rs │ ├── secret.rs │ ├── test │ ├── macros.rs │ ├── prelude.rs │ └── transport.rs │ ├── tokens.rs │ ├── transaction.rs │ ├── transaction │ ├── build.rs │ ├── confirm.rs │ ├── gas_price.rs │ ├── kms.rs │ └── send.rs │ └── transport.rs └── examples ├── Cargo.toml ├── documentation ├── Cargo.toml └── src │ └── main.rs ├── examples ├── abi.rs ├── async.rs ├── batch.rs ├── bytecode.rs ├── deployments.rs ├── events.rs ├── kms.rs ├── linked.rs ├── overloaded.rs ├── past_events.rs ├── revert.rs ├── rinkeby.rs └── sources.rs ├── generate ├── Cargo.toml ├── README.md ├── build.rs └── src │ └── main.rs ├── hardhat ├── README.md ├── contracts │ └── DeployedContract.sol ├── deploy │ └── 00_init.js ├── deployments │ ├── localhost │ │ ├── .chainId │ │ ├── DeployedContract.json │ │ └── solcInputs │ │ │ └── d7ef70c507cb995ea3e81152c437ca74.json │ └── rinkeby │ │ ├── .chainId │ │ ├── DeployedContract.json │ │ └── solcInputs │ │ └── d7ef70c507cb995ea3e81152c437ca74.json ├── hardhat.config.js ├── package.json └── yarn.lock └── truffle ├── .network-restore.conf.js ├── README.md ├── contracts ├── AbiTypes.sol ├── DeployedContract.sol ├── DocumentedContract.sol ├── LinkedContract.sol ├── Migrations.sol ├── OverloadedMethods.sol ├── Revert.sol ├── RustCoin.sol └── SimpleLibrary.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── networks.json ├── package.json ├── truffle-config.js └── yarn.lock /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "02:00" 8 | timezone: Europe/Berlin 9 | open-pull-requests-limit: 10 10 | ignore: 11 | # These crates are actually transitive dependencies 12 | # that we just happen to use directly. Their versions 13 | # depend on other crate's versions, so they should 14 | # be updated manually. 15 | - dependency-name: tokio 16 | - dependency-name: primitive-types 17 | rebase-strategy: disabled 18 | - package-ecosystem: npm 19 | directory: "/examples/truffle" 20 | schedule: 21 | interval: weekly 22 | time: "02:00" 23 | timezone: Europe/Berlin 24 | open-pull-requests-limit: 10 25 | versioning-strategy: increase 26 | rebase-strategy: disabled 27 | - package-ecosystem: npm 28 | directory: "/examples/hardhat" 29 | schedule: 30 | interval: weekly 31 | time: "02:00" 32 | timezone: Europe/Berlin 33 | open-pull-requests-limit: 10 34 | versioning-strategy: increase 35 | rebase-strategy: disabled 36 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | [Optional] 2 | Fixes #issue 3 | 4 | ** 5 | 6 | ### Test Plan 7 | ** 8 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: pull request 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | jobs: 7 | rust: 8 | strategy: 9 | matrix: 10 | include: 11 | - rust: 1.74.0 12 | examples: false 13 | continue-on-error: false 14 | - rust: stable 15 | examples: true 16 | continue-on-error: false 17 | - rust: beta 18 | examples: false 19 | continue-on-error: true 20 | - rust: nightly 21 | examples: false 22 | continue-on-error: true 23 | runs-on: ubuntu-latest 24 | continue-on-error: ${{ matrix.continue-on-error }} 25 | env: 26 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} 27 | INFURA_PROJECT_ID: ${{ secrets.INFURA_PROJECT_ID }} 28 | PK: ${{ secrets.PK }} 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: ${{ matrix.rust }} 34 | profile: minimal 35 | components: rustfmt, clippy 36 | default: true 37 | - uses: Swatinem/rust-cache@v1 38 | - uses: foundry-rs/foundry-toolchain@v1 39 | - uses: actions/setup-node@v3 40 | with: 41 | node-version: '18' 42 | - run: cargo fmt -- --check 43 | - run: cd examples/truffle && yarn --frozen-lockfile && yarn build 44 | # Can't use --all-features here because web3 has mutually exclusive features. 45 | - run: cargo clippy --all-targets -- -D warnings 46 | # This is a workaround for a rustc/cargo bug we started encountering on Github Actions where 47 | # running `cargo test` in the top level directory would fail with a linker error. 48 | - run: | 49 | (cd ethcontract && cargo test) 50 | (cd ethcontract-common && cargo test) 51 | (cd ethcontract-derive && cargo test) 52 | (cd ethcontract-generate && cargo test) 53 | - run: | 54 | if ${{ matrix.examples }}; then 55 | anvil -p 9545 & 56 | # wait for anvil to start 57 | while ! curl --silent http://127.0.0.1:9545; do 58 | sleep 1 59 | done 60 | cargo run --package examples --example abi 61 | cargo run --package examples --example async 62 | cargo run --package examples --example batch 63 | cargo run --package examples --example bytecode 64 | cargo run --package examples --example deployments 65 | cargo run --package examples --example events 66 | cargo run --package examples --example revert 67 | cargo run --package examples --example linked 68 | # if [ "$PK" ] && [ "$INFURA_PROJECT_ID" ]; then 69 | # cargo run --package examples-generate 70 | # cargo run --package examples --example rinkeby 71 | # cargo run --package examples --example sources 72 | # fi 73 | kill %1 74 | fi 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | env: 9 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: ci/deploy.sh --tag ${GITHUB_REF#refs/tags/} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | node_modules/ 6 | examples/*/build/ 7 | examples/*/artifacts 8 | examples/*/cache 9 | 10 | .idea 11 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Merge approved and green PRs with merge-when-green label 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - status-success~=^rust \(.*, false\)$ 6 | - base=main 7 | - label=merge when green 8 | actions: 9 | merge: 10 | method: squash 11 | strict: smart+fasttrack 12 | commit_message: title+body 13 | - name: Automatic merge for Dependabot pull requests 14 | conditions: 15 | - author~=^dependabot(|-preview)\[bot\]$ 16 | - status-success~=^rust \(.*, false\)$ 17 | - base=main 18 | actions: 19 | merge: 20 | method: squash 21 | strict: smart+fasttrack 22 | commit_message: title+body 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "ethcontract", 5 | "ethcontract-common", 6 | "ethcontract-derive", 7 | "ethcontract-generate", 8 | "ethcontract-mock", 9 | "examples", 10 | "examples/documentation", 11 | "examples/generate", 12 | ] 13 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2020 Gnosis Ltd 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/gnosis/ethcontract-rs.svg?branch=main)](https://travis-ci.org/gnosis/ethcontract-rs) 2 | [![Crates.io](https://img.shields.io/crates/v/ethcontract.svg)](https://crates.io/crates/ethcontract) 3 | [![Docs.rs](https://docs.rs/ethcontract/badge.svg)](https://docs.rs/ethcontract) 4 | [![Rustc Version](https://img.shields.io/badge/rustc-1.70+-lightgray.svg)](https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html) 5 | 6 | # `ethcontract-rs` 7 | 8 | Crate used for generating code for Ethereum smart contracts. It provides a 9 | function procedural macro that generates safe bindings for contract interaction 10 | based on the contract ABI. 11 | 12 | ## Getting Started 13 | 14 | Add a dependency to the `ethcontract` crate in your `Cargo.toml`: 15 | 16 | ```toml 17 | [dependencies] 18 | ethcontract = "..." 19 | ``` 20 | 21 | Then generate a struct for interacting with the smart contract with a type-safe 22 | API: 23 | 24 | ```rust 25 | ethcontract::contract!("path/to/truffle/build/contract/Contract.json"); 26 | ``` 27 | 28 | This will generate a new struct `ContractName` with contract generated methods 29 | for interacting with contract functions in a type-safe way. 30 | 31 | ### Minimum Supported Rust Version 32 | 33 | The minimum supported Rust version is 1.64.0 34 | 35 | ## Generator API 36 | 37 | As an alternative to the procedural macro, a generator API is provided for 38 | generating contract bindings from `build.rs` scripts. More information can be 39 | found in the `ethcontract-generate` [README](ethcontract-generate/README.md). 40 | 41 | ## Running the Examples 42 | 43 | In order to run local examples you will additionally need: 44 | - NodeJS LTS 45 | - Yarn 46 | 47 | For all examples, the smart contracts must first be built: 48 | 49 | ```sh 50 | cd examples/truffle 51 | yarn && yarn build 52 | ``` 53 | 54 | ### Truffle Examples 55 | 56 | Truffle examples rely on the local truffle development server. In a separate 57 | terminal run: 58 | 59 | ```sh 60 | cd examples/truffle 61 | yarn start 62 | ``` 63 | 64 | #### ABI: 65 | 66 | The `abi` example deploys a simple contract and performs various `eth_call`s 67 | to illustrate how Solidity types are mapped to Rust types by `ethcontract`. 68 | 69 | ```sh 70 | cargo run --example abi 71 | ``` 72 | 73 | #### Async/Await: 74 | 75 | The `async` example deploys an ERC20 token and interacts with the contract 76 | with various accounts. 77 | 78 | ```sh 79 | cargo run --example async 80 | ``` 81 | 82 | #### Manual Deployments: 83 | 84 | The `deployments` example illustrates how the `deployments` parameter can be 85 | specified when generating a contract with the `ethcontract::contract!` macro. 86 | This can be useful for specifying addresses in testing environments that are 87 | deterministic but either not included, or inaccurate in the artifact's 88 | `networks` property (when for example the contract is developed upstream, but 89 | a separate testnet deployment wants to be used). 90 | 91 | ```sh 92 | cargo run --example deployments 93 | ``` 94 | 95 | #### Events: 96 | 97 | The `events` example illustrates how to listen to logs emitted by smart 98 | contract events. 99 | 100 | ```sh 101 | cargo run --example events 102 | ``` 103 | 104 | #### Generator API (with `build.rs` script): 105 | 106 | The `generator` example (actually a separate crate to be able to have a build 107 | script) demonstrates how the generator API can be used for creating type-safe 108 | bindings to a smart contract with a `build.rs` build script. 109 | 110 | ```sh 111 | cargo run --package examples-generate 112 | ``` 113 | 114 | #### Contract Linking: 115 | 116 | The `linked` example deploys a library and a contract that links to it then 117 | makes a method call. 118 | 119 | ```sh 120 | cargo run --example linked 121 | ``` 122 | 123 | #### Deployed Bytecode 124 | 125 | The `bytecode` example deploys a contract and verifies its bytecode matches the 126 | expected value from the contract artifact. 127 | 128 | ```sh 129 | cargo run --example bytecode 130 | ``` 131 | 132 | ### Rinkeby Example 133 | 134 | There is a provided example that runs with Rinkeby and Infura. Running this 135 | example is a little more involved to run because it requires a private key with 136 | funds on Rinkeby (for gas) as well as an Infura project ID in order to connect 137 | to a node. Parameters are provided to the Rinkeby example by environment 138 | variables: 139 | 140 | ```sh 141 | export PK="private key" 142 | export INFURA_PROJECT_ID="Infura project ID" 143 | cargo run --example rinkeby 144 | ``` 145 | 146 | ### Mainnet Examples 147 | 148 | #### Sources: 149 | 150 | This example generates contract bindings from online sources: 151 | - A verified contract on Etherscan 152 | - An npmjs contract 153 | 154 | It also queries some contract state with Infura. Running this example requires 155 | an Infura project ID in order to connect to a node. Parameters are provided to 156 | the example by environment variables: 157 | 158 | ```sh 159 | export INFURA_PROJECT_ID="Infura project ID" 160 | cargo run --example sources 161 | ``` 162 | 163 | #### Past Events: 164 | 165 | This example retrieves the entire event history of token OWL contract and prints 166 | the total number of events since deployment. 167 | 168 | Note the special handling of the `tokenOWLProxy` contract and how it is cast into 169 | a `tokenOWL` instance using Contract's `with_deployment_info` feature. 170 | 171 | ```sh 172 | export INFURA_PROJECT_ID="Infura project ID" 173 | cargo run --example past_events 174 | ``` 175 | 176 | ## Sample Contracts Documentation 177 | 178 | You can view example generated contract documentation by fist building the 179 | contracts and then generating documentation for the crate: 180 | 181 | ```sh 182 | (cd examples/truffle; yarn && yarn build) 183 | cargo doc --package examples-documentation --no-deps --open 184 | ``` 185 | 186 | This will open a browser at the documentation root. 187 | -------------------------------------------------------------------------------- /ci/bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | verbose="" 6 | version="" 7 | while [[ $# -gt 0 ]]; do 8 | case $1 in 9 | -v|--verbose) verbose=t;; 10 | -h|--help) cat << EOF 11 | ci/bump.sh 12 | Bump the version of all crates and packages in the repository to VERSION 13 | 14 | USAGE: 15 | $0 [OPTIONS] 16 | 17 | OPTIONS: 18 | -v, --verbose Use verbose output 19 | -h, --help Prints this help information 20 | 21 | ARGUMENTS: 22 | VERSION The new version to use for the workspace. It must 23 | be a valid SemVer version number of the format 24 | 'MAJOR.MINOR.PATCH'. 25 | EOF 26 | exit 27 | ;; 28 | *) 29 | if [[ -z "$version" ]]; then 30 | version="$1" 31 | else 32 | >&2 cat << EOF 33 | ERROR: Invalid option '$1'. 34 | For more information try '$0 --help' 35 | EOF 36 | exit 1 37 | fi 38 | ;; 39 | esac 40 | shift 41 | done 42 | 43 | if [[ -z "$version" ]]; then 44 | >&2 cat << EOF 45 | ERROR: Missing version argument. 46 | For more information try '$0 --help' 47 | EOF 48 | exit 1 49 | fi 50 | if [[ ! $version =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 51 | >&2 cat << EOF 52 | ERROR: Invalid version format. 53 | For more information try '$0 --help' 54 | EOF 55 | exit 1 56 | fi 57 | version=${version/v/} 58 | 59 | function msg { 60 | if [[ -n $verbose ]]; then 61 | echo $* 62 | fi 63 | } 64 | 65 | msg "Updating Cargo manifests with new version '$version':" 66 | for manifest in ethcontract*/Cargo.toml; do 67 | msg " - $manifest" 68 | if [[ $(uname) == Darwin ]]; then 69 | sed -i '' -E -e 's/^((ethcontract(-[a-z]+)? = \{ )?version) = "[0-9\.]+"/\1 = "'"$version"'"/g' "$manifest" 70 | else 71 | sed -i -E -e 's/^((ethcontract(-[a-z]+)? = \{ )?version) = "[0-9\.]+"/\1 = "'"$version"'"/g' "$manifest" 72 | fi 73 | done 74 | -------------------------------------------------------------------------------- /ci/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | token="" 6 | tag="" 7 | options="" 8 | while [[ $# -gt 0 ]]; do 9 | case $1 in 10 | --tag) tag="$2"; shift;; 11 | --dry-run) options+="$1 ";; 12 | -v|-vv|--verbose) options+="$1 ";; 13 | -h|--help) cat << EOF 14 | ci/deploy.sh 15 | Deploy the workspace to crates.io 16 | 17 | USAGE: 18 | $0 [OPTIONS] 19 | 20 | OPTIONS: 21 | --tag The current tag being deployed 22 | --dry-run Perform all checks without uploading 23 | -v, --verbose Use verbose output (-vv very verbose output) 24 | -h, --help Prints this help information 25 | EOF 26 | exit 27 | ;; 28 | *) >&2 cat << EOF 29 | ERROR: Invalid option '$1'. 30 | For more information try '$0 --help' 31 | EOF 32 | exit 1 33 | ;; 34 | esac 35 | shift 36 | done 37 | 38 | if [[ -z "$tag" ]]; then 39 | >&2 echo "ERROR: missing tag parameter" 40 | exit 1 41 | fi 42 | 43 | function check_manifest_version { 44 | version=$(cat $1 | grep '^version' | sed -n 's/version = "\(.*\)"/v\1/p') 45 | if [[ ! $version = $tag ]]; then 46 | >&2 echo "ERROR: $1 is at $version but expected $tag" 47 | exit 1 48 | fi 49 | } 50 | 51 | check_manifest_version ethcontract-common/Cargo.toml 52 | check_manifest_version ethcontract-generate/Cargo.toml 53 | check_manifest_version ethcontract-derive/Cargo.toml 54 | check_manifest_version ethcontract/Cargo.toml 55 | check_manifest_version ethcontract-mock/Cargo.toml 56 | 57 | function cargo_publish { 58 | (cd $1; cargo publish $options) 59 | 60 | # NOTE(nlordell): For some reason, the next publish fails on not being able 61 | # to find the new version; maybe it takes a second for crates.io to update 62 | # its index. To make the deployment script more robust wait until the 63 | # crate is available on `crates.io` by polling its download link. 64 | if [[ $1 != "." ]]; then 65 | retries=15 66 | url="https://crates.io/api/v1/crates/ethcontract-$(basename $1)/${tag/#v/}/download" 67 | until [[ $retries -le 0 ]] || curl -Ifs "$url" > /dev/null; do 68 | retries=$(($retries - 1)) 69 | sleep 10 70 | done 71 | fi 72 | } 73 | 74 | cargo_publish ethcontract-common 75 | cargo_publish ethcontract-generate 76 | cargo_publish ethcontract-derive 77 | cargo_publish ethcontract 78 | cargo_publish ethcontract-mock 79 | -------------------------------------------------------------------------------- /ethcontract-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethcontract-common" 3 | version = "0.25.8" 4 | authors = ["Gnosis developers "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/gnosis/ethcontract-rs" 8 | homepage = "https://github.com/gnosis/ethcontract-rs" 9 | documentation = "https://docs.rs/ethcontract-common" 10 | description = """ 11 | Common types for ethcontract-rs runtime and proc macro. 12 | """ 13 | 14 | [dependencies] 15 | ethabi = "18.0" 16 | hex = "0.4" 17 | serde= { version = "1.0", features = ["rc"] } 18 | serde_derive = "1.0" 19 | serde_json = "1.0" 20 | thiserror = "1.0" 21 | tiny-keccak = { version = "2.0", features = ["keccak"] } 22 | web3 = { version = "0.19", default-features = false } 23 | -------------------------------------------------------------------------------- /ethcontract-common/src/abiext.rs: -------------------------------------------------------------------------------- 1 | //! This module implements extensions to the `ethabi` API. 2 | 3 | use crate::abi::{Event, Function, ParamType}; 4 | use crate::errors::ParseParamTypeError; 5 | use crate::hash::{self, H32}; 6 | use serde_json::json; 7 | 8 | /// Extension trait for `ethabi::Function`. 9 | pub trait FunctionExt { 10 | /// Computes the method signature in the standard ABI format. This does not 11 | /// include the output types. 12 | fn abi_signature(&self) -> String; 13 | 14 | /// Computes the Keccak256 function selector used by contract ABIs. 15 | fn selector(&self) -> H32; 16 | } 17 | 18 | impl FunctionExt for Function { 19 | fn abi_signature(&self) -> String { 20 | let mut full_signature = self.signature(); 21 | if let Some(colon) = full_signature.find(':') { 22 | full_signature.truncate(colon); 23 | } 24 | 25 | full_signature 26 | } 27 | 28 | fn selector(&self) -> H32 { 29 | hash::function_selector(self.abi_signature()) 30 | } 31 | } 32 | 33 | /// Extension trait for `ethabi::Event`. 34 | pub trait EventExt { 35 | /// Computes the event signature in human-readable format. The `keccak256` 36 | /// hash of this value is the actual event signature that is used as topic0 37 | /// in the transaction logs. 38 | fn abi_signature(&self) -> String; 39 | } 40 | 41 | impl EventExt for Event { 42 | fn abi_signature(&self) -> String { 43 | format!( 44 | "{}({}){}", 45 | self.name, 46 | self.inputs 47 | .iter() 48 | .map(|input| input.kind.to_string()) 49 | .collect::>() 50 | .join(","), 51 | if self.anonymous { " anonymous" } else { "" }, 52 | ) 53 | } 54 | } 55 | 56 | /// An extension trait for Solidity parameter types. 57 | pub trait ParamTypeExt { 58 | /// Parses a parameter type from a string value. 59 | fn from_str(s: &str) -> Result { 60 | serde_json::from_value(json!(s)).map_err(|_| ParseParamTypeError(s.into())) 61 | } 62 | } 63 | 64 | impl ParamTypeExt for ParamType {} 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn format_function_signature() { 72 | for (f, expected) in &[ 73 | (r#"{"name":"foo","inputs":[],"outputs":[]}"#, "foo()"), 74 | ( 75 | r#"{"name":"bar","inputs":[{"name":"a","type":"uint256"},{"name":"b","type":"bool"}],"outputs":[]}"#, 76 | "bar(uint256,bool)", 77 | ), 78 | ( 79 | r#"{"name":"baz","inputs":[{"name":"a","type":"uint256"}],"outputs":[{"name":"b","type":"bool"}]}"#, 80 | "baz(uint256)", 81 | ), 82 | ( 83 | r#"{"name":"bax","inputs":[],"outputs":[{"name":"a","type":"uint256"},{"name":"b","type":"bool"}]}"#, 84 | "bax()", 85 | ), 86 | ] { 87 | let function: Function = serde_json::from_str(f).expect("invalid function JSON"); 88 | let signature = function.abi_signature(); 89 | assert_eq!(signature, *expected); 90 | } 91 | } 92 | 93 | #[test] 94 | fn format_event_signature() { 95 | for (e, expected) in &[ 96 | (r#"{"name":"foo","inputs":[],"anonymous":false}"#, "foo()"), 97 | ( 98 | r#"{"name":"bar","inputs":[{"name":"a","type":"uint256"},{"name":"b","type":"bool"}],"anonymous":false}"#, 99 | "bar(uint256,bool)", 100 | ), 101 | ( 102 | r#"{"name":"baz","inputs":[{"name":"a","type":"uint256"}],"anonymous":true}"#, 103 | "baz(uint256) anonymous", 104 | ), 105 | ( 106 | r#"{"name":"bax","inputs":[],"anonymous":true}"#, 107 | "bax() anonymous", 108 | ), 109 | ] { 110 | let event: Event = serde_json::from_str(e).expect("invalid event JSON"); 111 | let signature = event.abi_signature(); 112 | assert_eq!(signature, *expected); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ethcontract-common/src/artifact/truffle.rs: -------------------------------------------------------------------------------- 1 | //! Implements the most common artifact format used in Truffle, Waffle 2 | //! and some other libraries. 3 | //! 4 | //! This artifact is represented as a JSON file containing information about 5 | //! a single contract. We parse the following fields: 6 | //! 7 | //! - `contractName`: name of the contract (optional); 8 | //! - `abi`: information about contract's interface; 9 | //! - `bytecode`: contract's compiled bytecode (optional); 10 | //! - `networks`: info about known contract deployments (optional); 11 | //! - `devdoc`, `userdoc`: additional documentation for contract's methods. 12 | 13 | use crate::artifact::Artifact; 14 | use crate::errors::ArtifactError; 15 | use crate::Contract; 16 | use serde_json::{from_reader, from_slice, from_str, from_value, to_string, Value}; 17 | use std::fs::File; 18 | use std::io::{BufReader, Read}; 19 | use std::path::Path; 20 | 21 | /// Loads truffle artifacts. 22 | #[must_use = "truffle loaders do nothing unless you load them"] 23 | pub struct TruffleLoader { 24 | /// Override for artifact's origin. 25 | /// 26 | /// If empty, origin will be derived automatically. 27 | pub origin: Option, 28 | 29 | /// Override for contract's name. 30 | /// 31 | /// Truffle artifacts contain a single contract which may be unnamed. 32 | pub name: Option, 33 | } 34 | 35 | impl TruffleLoader { 36 | /// Creates a new truffle loader. 37 | pub fn new() -> Self { 38 | TruffleLoader { 39 | origin: None, 40 | name: None, 41 | } 42 | } 43 | 44 | /// Creates a new truffle loader and sets an override for artifact's origins. 45 | pub fn with_origin(origin: impl Into) -> Self { 46 | TruffleLoader { 47 | origin: Some(origin.into()), 48 | name: None, 49 | } 50 | } 51 | 52 | /// Sets new override for artifact's origin. See [`origin`] for more info. 53 | /// 54 | /// [`origin`]: #structfield.origin 55 | pub fn origin(mut self, origin: impl Into) -> Self { 56 | self.origin = Some(origin.into()); 57 | self 58 | } 59 | 60 | /// Sets new override for artifact's name. See [`name`] for more info. 61 | /// 62 | /// [`name`]: #structfield.name 63 | pub fn name(mut self, name: impl Into) -> Self { 64 | self.name = Some(name.into()); 65 | self 66 | } 67 | 68 | /// Loads an artifact from a loaded JSON value. 69 | pub fn load_from_reader(&self, v: impl Read) -> Result { 70 | self.load_artifact("", v, from_reader) 71 | } 72 | 73 | /// Loads an artifact from bytes of JSON text. 74 | pub fn load_from_slice(&self, v: &[u8]) -> Result { 75 | self.load_artifact("", v, from_slice) 76 | } 77 | 78 | /// Loads an artifact from string of JSON text. 79 | pub fn load_from_str(&self, v: &str) -> Result { 80 | self.load_artifact("", v, from_str) 81 | } 82 | 83 | /// Loads an artifact from a loaded JSON value. 84 | pub fn load_from_value(&self, v: Value) -> Result { 85 | self.load_artifact("", v, from_value) 86 | } 87 | 88 | /// Loads an artifact from disk. 89 | pub fn load_from_file(&self, p: impl AsRef) -> Result { 90 | let path = p.as_ref(); 91 | let file = File::open(path)?; 92 | let reader = BufReader::new(file); 93 | self.load_artifact(path.display(), reader, from_reader) 94 | } 95 | 96 | /// Loads a contract from a loaded JSON value. 97 | pub fn load_contract_from_reader(&self, v: impl Read) -> Result { 98 | self.load_contract(v, from_reader) 99 | } 100 | 101 | /// Loads a contract from bytes of JSON text. 102 | pub fn load_contract_from_slice(&self, v: &[u8]) -> Result { 103 | self.load_contract(v, from_slice) 104 | } 105 | 106 | /// Loads a contract from string of JSON text. 107 | pub fn load_contract_from_str(&self, v: &str) -> Result { 108 | self.load_contract(v, from_str) 109 | } 110 | 111 | /// Loads a contract from a loaded JSON value. 112 | pub fn load_contract_from_value(&self, v: Value) -> Result { 113 | self.load_contract(v, from_value) 114 | } 115 | 116 | /// Loads a contract from disk. 117 | pub fn load_contract_from_file(&self, p: impl AsRef) -> Result { 118 | let path = p.as_ref(); 119 | let file = File::open(path)?; 120 | let reader = BufReader::new(file); 121 | self.load_contract(reader, from_reader) 122 | } 123 | 124 | fn load_artifact( 125 | &self, 126 | origin: impl ToString, 127 | source: T, 128 | loader: impl FnOnce(T) -> serde_json::Result, 129 | ) -> Result { 130 | let origin = self.origin.clone().unwrap_or_else(|| origin.to_string()); 131 | let mut artifact = Artifact::with_origin(origin); 132 | artifact.insert(self.load_contract(source, loader)?); 133 | Ok(artifact) 134 | } 135 | 136 | fn load_contract( 137 | &self, 138 | source: T, 139 | loader: impl FnOnce(T) -> serde_json::Result, 140 | ) -> Result { 141 | let mut contract: Contract = loader(source)?; 142 | 143 | if let Some(name) = &self.name { 144 | contract.name.clone_from(name); 145 | } 146 | 147 | Ok(contract) 148 | } 149 | 150 | /// Serializes a single contract. 151 | pub fn save_to_string(contract: &Contract) -> Result { 152 | to_string(contract).map_err(Into::into) 153 | } 154 | } 155 | 156 | impl Default for TruffleLoader { 157 | fn default() -> Self { 158 | TruffleLoader::new() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /ethcontract-common/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! Module for reading and examining data produced by truffle. 2 | 3 | use crate::abiext::FunctionExt; 4 | use crate::hash::H32; 5 | use crate::Abi; 6 | use crate::{bytecode::Bytecode, DeploymentInformation}; 7 | use ethabi::ethereum_types::H256; 8 | use serde::Deserializer; 9 | use serde::Serializer; 10 | use serde::{Deserialize, Serialize}; 11 | use std::collections::{BTreeMap, HashMap}; 12 | use std::hash::Hash; 13 | use std::sync::Arc; 14 | use web3::types::Address; 15 | 16 | /// Represents a contract data. 17 | #[derive(Clone, Debug, Serialize, Deserialize)] 18 | #[serde(default = "Contract::empty")] 19 | pub struct Contract { 20 | /// The contract name. Unnamed contracts have an empty string as their name. 21 | #[serde(rename = "contractName")] 22 | pub name: String, 23 | /// The contract interface. 24 | #[serde(rename = "abi")] 25 | pub interface: Arc, 26 | /// The contract deployment bytecode. 27 | pub bytecode: Bytecode, 28 | /// The contract's expected deployed bytecode. 29 | #[serde(rename = "deployedBytecode")] 30 | pub deployed_bytecode: Bytecode, 31 | /// The configured networks by network ID for the contract. 32 | pub networks: HashMap, 33 | /// The developer documentation. 34 | pub devdoc: Documentation, 35 | /// The user documentation. 36 | pub userdoc: Documentation, 37 | } 38 | 39 | /// Struct representing publicly accessible interface of a smart contract. 40 | #[derive(Clone, Debug, Default, PartialEq)] 41 | pub struct Interface { 42 | /// The contract ABI 43 | pub abi: Abi, 44 | /// A mapping from method signature to a name-index pair for accessing 45 | /// functions in the contract ABI. This is used to avoid allocation when 46 | /// searching for matching functions by signature. 47 | pub methods: HashMap, 48 | /// A mapping from event signature to a name-index pair for resolving 49 | /// events in the contract ABI. 50 | pub events: HashMap, 51 | } 52 | 53 | impl<'de> Deserialize<'de> for Interface { 54 | fn deserialize(deserializer: D) -> Result 55 | where 56 | D: Deserializer<'de>, 57 | { 58 | let abi = Abi::deserialize(deserializer)?; 59 | Ok(abi.into()) 60 | } 61 | } 62 | 63 | impl Serialize for Interface { 64 | fn serialize(&self, serializer: S) -> Result 65 | where 66 | S: Serializer, 67 | { 68 | self.abi.serialize(serializer) 69 | } 70 | } 71 | 72 | impl From for Interface { 73 | fn from(abi: Abi) -> Self { 74 | Self { 75 | methods: create_mapping(&abi.functions, |function| function.selector()), 76 | events: create_mapping(&abi.events, |event| event.signature()), 77 | abi, 78 | } 79 | } 80 | } 81 | 82 | /// Utility function for creating a mapping between a unique signature and a 83 | /// name-index pair for accessing contract ABI items. 84 | fn create_mapping( 85 | elements: &BTreeMap>, 86 | signature: F, 87 | ) -> HashMap 88 | where 89 | S: Hash + Eq + Ord, 90 | F: Fn(&T) -> S, 91 | { 92 | let signature = &signature; 93 | elements 94 | .iter() 95 | .flat_map(|(name, sub_elements)| { 96 | sub_elements 97 | .iter() 98 | .enumerate() 99 | .map(move |(index, element)| (signature(element), (name.to_owned(), index))) 100 | }) 101 | .collect() 102 | } 103 | 104 | impl Contract { 105 | /// Creates an empty contract instance. 106 | pub fn empty() -> Self { 107 | Contract::with_name(String::default()) 108 | } 109 | 110 | /// Creates an empty contract instance with the given name. 111 | pub fn with_name(name: impl Into) -> Self { 112 | Contract { 113 | name: name.into(), 114 | interface: Default::default(), 115 | bytecode: Default::default(), 116 | deployed_bytecode: Default::default(), 117 | networks: HashMap::new(), 118 | devdoc: Default::default(), 119 | userdoc: Default::default(), 120 | } 121 | } 122 | } 123 | 124 | /// A contract's network configuration. 125 | #[derive(Clone, Debug, Serialize, Deserialize)] 126 | pub struct Network { 127 | /// The address at which the contract is deployed on this network. 128 | pub address: Address, 129 | /// The hash of the transaction that deployed the contract on this network. 130 | #[serde(rename = "transactionHash")] 131 | pub deployment_information: Option, 132 | } 133 | 134 | /// A contract's documentation. 135 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 136 | pub struct Documentation { 137 | /// Contract documentation 138 | pub details: Option, 139 | /// Contract method documentation. 140 | pub methods: HashMap, 141 | } 142 | 143 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 144 | /// A documentation entry. 145 | pub struct DocEntry { 146 | /// The documentation details for this entry. 147 | pub details: Option, 148 | } 149 | -------------------------------------------------------------------------------- /ethcontract-common/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Module with common error types. 2 | 3 | use serde_json::Error as JsonError; 4 | use std::io::Error as IoError; 5 | use thiserror::Error; 6 | 7 | /// An error in loading or parsing an artifact. 8 | #[derive(Debug, Error)] 9 | pub enum ArtifactError { 10 | /// An IO error occurred when loading a truffle artifact from disk. 11 | #[error("failed to open contract artifact file: {0}")] 12 | Io(#[from] IoError), 13 | 14 | /// A JSON error occurred while parsing a truffle artifact. 15 | #[error("failed to parse contract artifact JSON: {0}")] 16 | Json(#[from] JsonError), 17 | 18 | /// Contract was deployed onto different chains, and ABIs don't match. 19 | #[error("contract {0} has different ABIs on different chains")] 20 | AbiMismatch(String), 21 | 22 | /// Contract have multiple deployment addresses on the same chain. 23 | #[error("chain with id {0} appears several times in the artifact")] 24 | DuplicateChain(String), 25 | } 26 | 27 | /// An error reading bytecode string representation. 28 | #[derive(Debug, Error)] 29 | pub enum BytecodeError { 30 | /// Bytecode string is not an even length. 31 | #[error("invalid bytecode length")] 32 | InvalidLength, 33 | 34 | /// Placeholder is not long enough at end of bytecode string. 35 | #[error("placeholder at end of bytecode is too short")] 36 | PlaceholderTooShort, 37 | 38 | /// Invalid hex digit 39 | #[error("invalid hex digit '{0}'")] 40 | InvalidHexDigit(char), 41 | } 42 | 43 | /// An error linking a library to bytecode. 44 | #[derive(Debug, Error)] 45 | pub enum LinkError { 46 | /// Error when attempting to link a library when its placeholder cannot be 47 | /// found. 48 | #[error("unable to link library: can't find link placeholder for {0}")] 49 | NotFound(String), 50 | 51 | /// Error producing final bytecode binary when there are missing libraries 52 | /// that are not linked. Analogous to "undefinied symbol" error for 53 | /// traditional linkers. 54 | #[error("undefined library {0}")] 55 | UndefinedLibrary(String), 56 | } 57 | 58 | /// An error representing an error parsing a parameter type. 59 | #[derive(Clone, Debug, Error)] 60 | #[error("'{0}' is not a valid Solidity type")] 61 | pub struct ParseParamTypeError(pub String); 62 | -------------------------------------------------------------------------------- /ethcontract-common/src/hash.rs: -------------------------------------------------------------------------------- 1 | //! Keccak256 hash utilities. 2 | 3 | use tiny_keccak::{Hasher, Keccak}; 4 | 5 | /// Perform a Keccak256 hash of data and return its 32-byte result. 6 | pub fn keccak256(data: B) -> [u8; 32] 7 | where 8 | B: AsRef<[u8]>, 9 | { 10 | let mut output = [0u8; 32]; 11 | let mut hasher = Keccak::v256(); 12 | hasher.update(data.as_ref()); 13 | hasher.finalize(&mut output); 14 | output 15 | } 16 | 17 | /// A 32-bit prefix of a standard 256-bit Keccak hash. 18 | /// 19 | /// This 32-bit prefix is generally used as the first 4 bytes of transaction 20 | /// data in order to select which Solidity method will be called. 21 | pub type H32 = [u8; 4]; 22 | 23 | /// Calculates the function selector as per the contract ABI specification. This 24 | /// is definied as the first 4 bytes of the Keccak256 hash of the function 25 | /// signature. 26 | pub fn function_selector(signature: S) -> H32 27 | where 28 | S: AsRef, 29 | { 30 | let hash = keccak256(signature.as_ref()); 31 | let mut selector = H32::default(); 32 | selector.copy_from_slice(&hash[0..4]); 33 | selector 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[test] 41 | fn simple_keccak_hash() { 42 | // test vector retrieved from 43 | // https://web3js.readthedocs.io/en/v1.2.4/web3-utils.html#sha3 44 | assert_eq!( 45 | &keccak256([0xea]), 46 | b"\x2f\x20\x67\x74\x59\x12\x06\x77\x48\x4f\x71\x04\xc7\x6d\xeb\x68\ 47 | \x46\xa2\xc0\x71\xf9\xb3\x15\x2c\x10\x3b\xb1\x2c\xd5\x4d\x1a\x4a" 48 | ); 49 | } 50 | 51 | #[test] 52 | fn simple_function_signature() { 53 | // test vector retrieved from 54 | // https://web3js.readthedocs.io/en/v1.2.4/web3-eth-abi.html#encodefunctionsignature 55 | assert_eq!( 56 | function_selector("myMethod(uint256,string)"), 57 | [0x24, 0xee, 0x00, 0x97], 58 | ); 59 | } 60 | 61 | #[test] 62 | fn revert_function_signature() { 63 | assert_eq!(function_selector("Error(string)"), [0x08, 0xc3, 0x79, 0xa0]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ethcontract-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs, unsafe_code)] 2 | 3 | //! Crate for common times shared between the `ethcontract` runtime crate as and 4 | //! the `ethcontract-derive` crate. 5 | 6 | pub mod abiext; 7 | pub mod artifact; 8 | pub mod bytecode; 9 | pub mod contract; 10 | pub mod errors; 11 | pub mod hash; 12 | 13 | pub use crate::abiext::FunctionExt; 14 | pub use crate::bytecode::Bytecode; 15 | pub use crate::contract::Contract; 16 | pub use ethabi::{self as abi, Contract as Abi}; 17 | use serde::{Deserialize, Serialize}; 18 | pub use web3::types::Address; 19 | pub use web3::types::H256 as TransactionHash; 20 | 21 | /// Information about when a contract instance was deployed 22 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] 23 | #[serde(untagged)] 24 | pub enum DeploymentInformation { 25 | /// The block at which the contract was deployed 26 | BlockNumber(u64), 27 | /// The transaction hash at which the contract was deployed 28 | TransactionHash(TransactionHash), 29 | } 30 | 31 | impl From for DeploymentInformation { 32 | fn from(block: u64) -> Self { 33 | Self::BlockNumber(block) 34 | } 35 | } 36 | 37 | impl From for DeploymentInformation { 38 | fn from(hash: TransactionHash) -> Self { 39 | Self::TransactionHash(hash) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ethcontract-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethcontract-derive" 3 | version = "0.25.8" 4 | authors = ["Gnosis developers "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/gnosis/ethcontract-rs" 8 | homepage = "https://github.com/gnosis/ethcontract-rs" 9 | documentation = "https://docs.rs/ethcontract-derive" 10 | description = """ 11 | Proc macro for generating type-safe bindings to Ethereum smart contracts. 12 | """ 13 | 14 | [features] 15 | default = ["http"] 16 | http = ["ethcontract-generate/http"] 17 | 18 | [lib] 19 | proc-macro = true 20 | 21 | [dependencies] 22 | anyhow = "1.0" 23 | ethcontract-common = { version = "0.25.8", path = "../ethcontract-common" } 24 | ethcontract-generate = { version = "0.25.8", path = "../ethcontract-generate", default-features = false } 25 | proc-macro2 = "1.0" 26 | quote = "1.0" 27 | syn = "2.0" 28 | -------------------------------------------------------------------------------- /ethcontract-derive/src/spanned.rs: -------------------------------------------------------------------------------- 1 | //! Provides implementation for helpers used in parsing `TokenStream`s where the 2 | //! data ultimately does not care about its `Span` information, but it is useful 3 | //! during intermediate processing. 4 | 5 | use proc_macro2::Span; 6 | use std::ops::Deref; 7 | use syn::parse::{Parse, ParseStream, Result as ParseResult}; 8 | 9 | /// Trait that abstracts functionality for inner data that can be parsed and 10 | /// wrapped with a specific `Span`. 11 | pub trait ParseInner: Sized { 12 | fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)>; 13 | } 14 | 15 | impl ParseInner for T { 16 | fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> { 17 | Ok((input.span(), T::parse(input)?)) 18 | } 19 | } 20 | 21 | impl Parse for Spanned { 22 | fn parse(input: ParseStream) -> ParseResult { 23 | let (span, value) = T::spanned_parse(input)?; 24 | Ok(Spanned(span, value)) 25 | } 26 | } 27 | 28 | /// A struct that captures `Span` information for inner parsable data. 29 | #[cfg_attr(test, derive(Clone, Debug))] 30 | pub struct Spanned(Span, T); 31 | 32 | impl Spanned { 33 | /// Retrieves the captured `Span` information for the parsed data. 34 | pub fn span(&self) -> Span { 35 | self.0 36 | } 37 | 38 | /// Retrieves the inner data. 39 | pub fn into_inner(self) -> T { 40 | self.1 41 | } 42 | } 43 | 44 | impl Deref for Spanned { 45 | type Target = T; 46 | 47 | fn deref(&self) -> &Self::Target { 48 | &self.1 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ethcontract-generate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethcontract-generate" 3 | version = "0.25.8" 4 | authors = ["Gnosis developers "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/gnosis/ethcontract-rs" 8 | homepage = "https://github.com/gnosis/ethcontract-rs" 9 | documentation = "https://docs.rs/ethcontract-generate" 10 | description = """ 11 | Code generation for type-safe bindings to Ethereum smart contracts. 12 | """ 13 | 14 | [features] 15 | default = ["http"] 16 | http = ["curl"] 17 | 18 | [dependencies] 19 | anyhow = "1.0" 20 | curl = { version = "0.4", optional = true } 21 | ethcontract-common = { version = "0.25.8", path = "../ethcontract-common" } 22 | Inflector = "0.11" 23 | proc-macro2 = "1.0" 24 | quote = "1.0" 25 | syn = "2.0" 26 | url = "2.1" 27 | -------------------------------------------------------------------------------- /ethcontract-generate/README.md: -------------------------------------------------------------------------------- 1 | # `ethcontract-generate` 2 | 3 | An alternative API for generating type-safe contract bindings from `build.rs` 4 | scripts. Using this method instead of the procedural macro has a couple 5 | advantages: 6 | 7 | - proper integration with with RLS and Racer for autocomplete support; 8 | - ability to inspect the generated code. 9 | 10 | The downside of using the generator API is the requirement of having a build 11 | script instead of a macro invocation. 12 | 13 | ## Getting Started 14 | 15 | Using crate requires two dependencies - one for the runtime and one for the 16 | generator: 17 | 18 | ```toml 19 | [dependencies] 20 | ethcontract = { version = "...", default-features = false } 21 | 22 | [build-dependencies] 23 | ethcontract-generate = "..." 24 | ``` 25 | 26 | It is recommended that both versions be kept in sync or else unexpected 27 | behaviour may occur. 28 | 29 | Then, in your `build.rs` include the following code: 30 | 31 | ```rust 32 | use ethcontract_generate::loaders::TruffleLoader; 33 | use ethcontract_generate::ContractBuilder; 34 | 35 | fn main() { 36 | // Prepare filesystem paths. 37 | let out_dir = std::env::var("OUT_DIR").unwrap(); 38 | let dest = std::path::Path::new(&out_dir).join("rust_coin.rs"); 39 | 40 | // Load a contract. 41 | let contract = TruffleLoader::new() 42 | .load_contract_from_file("../build/Contract.json") 43 | .unwrap(); 44 | 45 | // Generate bindings for it. 46 | ContractBuilder::new() 47 | .generate(&contract) 48 | .unwrap() 49 | .write_to_file(dest) 50 | .unwrap(); 51 | } 52 | 53 | ``` 54 | 55 | ## Relation to `ethcontract-derive` 56 | 57 | `ethcontract-derive` uses `ethcontract-generate` under the hood so their 58 | generated bindings should be identical, they just provide different APIs to the 59 | same functionality. 60 | 61 | The long term goal of this project is to maintain `ethcontract-derive`. For now 62 | there is no extra work in having it split into two separate crates. That being 63 | said if RLS support improves for procedural macro generated code, it is possible 64 | that this crate be deprecated in favour of `ethcontract-derive` as long as there 65 | is no good argument to keep it around. 66 | -------------------------------------------------------------------------------- /ethcontract-generate/src/generate.rs: -------------------------------------------------------------------------------- 1 | //! Crate for generating type-safe bindings to Ethereum smart contracts. This 2 | //! crate is intended to be used either indirectly with the `ethcontract` 3 | //! crate's `contract` procedural macro or directly from a build script. 4 | 5 | mod common; 6 | mod deployment; 7 | mod events; 8 | mod methods; 9 | mod types; 10 | 11 | use crate::{util, ContractBuilder}; 12 | use anyhow::{anyhow, Context as _, Result}; 13 | use ethcontract_common::contract::Network; 14 | use ethcontract_common::Contract; 15 | use inflector::Inflector; 16 | use proc_macro2::{Ident, TokenStream}; 17 | use quote::quote; 18 | use std::collections::HashMap; 19 | use syn::{Path, Visibility}; 20 | 21 | /// Internal shared context for generating smart contract bindings. 22 | pub(crate) struct Context<'a> { 23 | /// The parsed contract. 24 | contract: &'a Contract, 25 | 26 | /// The identifier for the runtime crate. Usually this is `ethcontract` but 27 | /// it can be different if the crate was renamed in the Cargo manifest for 28 | /// example. 29 | runtime_crate: Ident, 30 | 31 | /// The visibility for the generated module and re-exported contract type. 32 | visibility: Visibility, 33 | 34 | /// The name of the module as an identifier in which to place the contract 35 | /// implementation. Note that the main contract type gets re-exported in the 36 | /// root. 37 | contract_mod: Ident, 38 | 39 | /// The contract name as an identifier. 40 | contract_name: Ident, 41 | 42 | /// Additional contract deployments. 43 | networks: HashMap, 44 | 45 | /// Manually specified method aliases. 46 | method_aliases: HashMap, 47 | 48 | /// Derives added to event structs and enums. 49 | event_derives: Vec, 50 | } 51 | 52 | impl<'a> Context<'a> { 53 | /// Creates a context from the code generation arguments. 54 | fn from_builder(contract: &'a Contract, builder: ContractBuilder) -> Result { 55 | let raw_contract_name = if let Some(name) = &builder.contract_name_override { 56 | name 57 | } else if !contract.name.is_empty() { 58 | &contract.name 59 | } else { 60 | return Err(anyhow!( 61 | "contract artifact is missing a name, this can happen when \ 62 | using a source that does not provide a contract name such as \ 63 | Etherscan; in this case the contract must be manually \ 64 | specified" 65 | )); 66 | }; 67 | 68 | let runtime_crate = util::ident(&builder.runtime_crate_name); 69 | let visibility = match &builder.visibility_modifier { 70 | Some(vis) => syn::parse_str(vis)?, 71 | None => Visibility::Inherited, 72 | }; 73 | let contract_mod = if let Some(name) = &builder.contract_mod_override { 74 | util::ident(name) 75 | } else { 76 | util::ident(&raw_contract_name.to_snake_case()) 77 | }; 78 | let contract_name = util::ident(raw_contract_name); 79 | 80 | // NOTE: We only check for duplicate signatures here, since if there are 81 | // duplicate aliases, the compiler will produce a warning because a 82 | // method will be re-defined. 83 | let mut method_aliases = HashMap::new(); 84 | for (signature, alias) in builder.method_aliases.into_iter() { 85 | let alias = syn::parse_str(&alias)?; 86 | if method_aliases.insert(signature.clone(), alias).is_some() { 87 | return Err(anyhow!( 88 | "duplicate method signature '{}' in method aliases", 89 | signature, 90 | )); 91 | } 92 | } 93 | 94 | let event_derives = builder 95 | .event_derives 96 | .iter() 97 | .map(|derive| syn::parse_str::(derive)) 98 | .collect::, _>>() 99 | .context("failed to parse event derives")?; 100 | 101 | Ok(Context { 102 | contract, 103 | runtime_crate, 104 | visibility, 105 | contract_mod, 106 | contract_name, 107 | networks: builder.networks, 108 | method_aliases, 109 | event_derives, 110 | }) 111 | } 112 | } 113 | 114 | pub(crate) fn expand(contract: &Contract, builder: ContractBuilder) -> Result { 115 | let cx = Context::from_builder(contract, builder)?; 116 | let contract = expand_contract(&cx).context("error expanding contract from its ABI")?; 117 | 118 | Ok(contract) 119 | } 120 | 121 | fn expand_contract(cx: &Context) -> Result { 122 | let runtime_crate = &cx.runtime_crate; 123 | let vis = &cx.visibility; 124 | let contract_mod = &cx.contract_mod; 125 | let contract_name = &cx.contract_name; 126 | 127 | let common = common::expand(cx); 128 | let deployment = deployment::expand(cx)?; 129 | let methods = methods::expand(cx)?; 130 | let events = events::expand(cx)?; 131 | 132 | Ok(quote! { 133 | #[allow(dead_code, clippy::type_complexity, clippy::large_enum_variant)] 134 | #vis mod #contract_mod { 135 | #[rustfmt::skip] 136 | use #runtime_crate as ethcontract; 137 | 138 | #common 139 | #deployment 140 | #methods 141 | #events 142 | } 143 | #vis use self::#contract_mod::Contract as #contract_name; 144 | }) 145 | } 146 | -------------------------------------------------------------------------------- /ethcontract-generate/src/generate/types.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use ethcontract_common::abi::ParamType; 3 | use proc_macro2::{Literal, TokenStream}; 4 | use quote::quote; 5 | 6 | pub(crate) fn expand(kind: &ParamType) -> Result { 7 | match kind { 8 | ParamType::Address => Ok(quote! { self::ethcontract::Address }), 9 | ParamType::Bytes => Ok(quote! { self::ethcontract::tokens::Bytes> }), 10 | ParamType::Int(n) => match n / 8 { 11 | 1 => Ok(quote! { i8 }), 12 | 2 => Ok(quote! { i16 }), 13 | 3..=4 => Ok(quote! { i32 }), 14 | 5..=8 => Ok(quote! { i64 }), 15 | 9..=16 => Ok(quote! { i128 }), 16 | 17..=32 => Ok(quote! { self::ethcontract::I256 }), 17 | _ => Err(anyhow!("unsupported solidity type int{}", n)), 18 | }, 19 | ParamType::Uint(n) => match n / 8 { 20 | 1 => Ok(quote! { u8 }), 21 | 2 => Ok(quote! { u16 }), 22 | 3..=4 => Ok(quote! { u32 }), 23 | 5..=8 => Ok(quote! { u64 }), 24 | 9..=16 => Ok(quote! { u128 }), 25 | 17..=32 => Ok(quote! { self::ethcontract::U256 }), 26 | _ => Err(anyhow!("unsupported solidity type uint{}", n)), 27 | }, 28 | ParamType::Bool => Ok(quote! { bool }), 29 | ParamType::String => Ok(quote! { String }), 30 | ParamType::Array(t) => { 31 | let inner = expand(t)?; 32 | Ok(quote! { Vec<#inner> }) 33 | } 34 | ParamType::FixedBytes(n) => { 35 | // TODO(nlordell): what is the performance impact of returning large 36 | // `FixedBytes` and `FixedArray`s with `web3`? 37 | let size = Literal::usize_unsuffixed(*n); 38 | Ok(quote! { self::ethcontract::tokens::Bytes<[u8; #size]> }) 39 | } 40 | ParamType::FixedArray(t, n) => { 41 | // TODO(nlordell): see above 42 | let inner = expand(t)?; 43 | let size = Literal::usize_unsuffixed(*n); 44 | Ok(quote! { [#inner; #size] }) 45 | } 46 | ParamType::Tuple(t) => { 47 | let inner = t.iter().map(expand).collect::>>()?; 48 | Ok(quote! { (#(#inner,)*) }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ethcontract-generate/src/rustfmt.rs: -------------------------------------------------------------------------------- 1 | //! This module implements basic `rustfmt` code formatting. 2 | 3 | use anyhow::{anyhow, Result}; 4 | use std::io::Write; 5 | use std::process::{Command, Stdio}; 6 | 7 | /// Formats the raw input source string and return formatted output. 8 | pub fn format(source: &str) -> Result { 9 | let mut rustfmt = Command::new("rustfmt") 10 | .args(["--edition", "2018"]) 11 | .stdin(Stdio::piped()) 12 | .stdout(Stdio::piped()) 13 | .spawn()?; 14 | 15 | { 16 | let stdin = rustfmt 17 | .stdin 18 | .as_mut() 19 | .ok_or_else(|| anyhow!("stdin was not created for `rustfmt` child process"))?; 20 | stdin.write_all(source.as_bytes())?; 21 | } 22 | 23 | let output = rustfmt.wait_with_output()?; 24 | if !output.status.success() { 25 | return Err(anyhow!( 26 | "`rustfmt` exited with code {}:\n{}", 27 | output.status, 28 | String::from_utf8_lossy(&output.stderr), 29 | )); 30 | } 31 | 32 | let stdout = String::from_utf8(output.stdout)?; 33 | Ok(stdout) 34 | } 35 | -------------------------------------------------------------------------------- /ethcontract-generate/src/test/macros.rs: -------------------------------------------------------------------------------- 1 | /// Asserts the result of an expansion matches source output. 2 | /// 3 | /// # Panics 4 | /// 5 | /// If the expanded source does not match the quoted source. 6 | macro_rules! assert_quote { 7 | ($ex:expr, { $($t:tt)* } $(,)?) => { 8 | assert_eq!($ex.to_string(), quote::quote! { $($t)* }.to_string()) 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /ethcontract-generate/src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | #[cfg(feature = "http")] 3 | use curl::easy::Easy; 4 | use ethcontract_common::Address; 5 | use inflector::Inflector; 6 | use proc_macro2::{Ident, Literal, Span, TokenStream}; 7 | use quote::quote; 8 | use syn::Ident as SynIdent; 9 | 10 | /// Expands a identifier string into an token. 11 | pub fn ident(name: &str) -> Ident { 12 | Ident::new(name, Span::call_site()) 13 | } 14 | 15 | /// Expands an identifier string into a token and appending `_` if the 16 | /// identifier is for a reserved keyword. 17 | /// 18 | /// Parsing keywords like `self` can fail, in this case we add an underscore. 19 | pub fn safe_ident(name: &str) -> Ident { 20 | syn::parse_str::(name).unwrap_or_else(|_| ident(&format!("{}_", name))) 21 | } 22 | 23 | /// Expands a positional identifier string that may be empty. 24 | /// 25 | /// Note that this expands the parameter name with `safe_ident`, meaning that 26 | /// identifiers that are reserved keywords get `_` appended to them. 27 | pub fn expand_input_name(index: usize, name: &str) -> TokenStream { 28 | let name_str = match name { 29 | "" => format!("p{}", index), 30 | n => n.to_snake_case(), 31 | }; 32 | let name = safe_ident(&name_str); 33 | 34 | quote! { #name } 35 | } 36 | 37 | /// Expands a doc string into an attribute token stream. 38 | pub fn expand_doc(s: &str) -> TokenStream { 39 | let doc = Literal::string(s); 40 | quote! { 41 | #[doc = #doc] 42 | } 43 | } 44 | 45 | /// Parses the given address string 46 | pub fn parse_address(address_str: S) -> Result
47 | where 48 | S: AsRef, 49 | { 50 | let address_str = address_str.as_ref(); 51 | if !address_str.starts_with("0x") { 52 | return Err(anyhow!("address must start with '0x'")); 53 | } 54 | Ok(address_str[2..].parse()?) 55 | } 56 | 57 | /// Performs an HTTP GET request and return the contents of the response. 58 | #[cfg(feature = "http")] 59 | pub fn http_get(url: &str) -> Result { 60 | let mut buffer = Vec::new(); 61 | let mut handle = Easy::new(); 62 | handle.url(url)?; 63 | { 64 | let mut transfer = handle.transfer(); 65 | transfer.write_function(|data| { 66 | buffer.extend_from_slice(data); 67 | Ok(data.len()) 68 | })?; 69 | transfer.perform()?; 70 | } 71 | 72 | let buffer = String::from_utf8(buffer)?; 73 | Ok(buffer) 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | 80 | #[test] 81 | fn input_name_to_ident_empty() { 82 | assert_quote!(expand_input_name(0, ""), { p0 }); 83 | } 84 | 85 | #[test] 86 | fn input_name_to_ident_keyword() { 87 | assert_quote!(expand_input_name(0, "self"), { self_ }); 88 | } 89 | 90 | #[test] 91 | fn input_name_to_ident_snake_case() { 92 | assert_quote!(expand_input_name(0, "CamelCase1"), { camel_case_1 }); 93 | } 94 | 95 | #[test] 96 | fn parse_address_missing_prefix() { 97 | assert!( 98 | parse_address("0000000000000000000000000000000000000000").is_err(), 99 | "parsing address not starting with 0x should fail" 100 | ); 101 | } 102 | 103 | #[test] 104 | fn parse_address_address_too_short() { 105 | assert!( 106 | parse_address("0x00000000000000").is_err(), 107 | "parsing address not starting with 0x should fail" 108 | ); 109 | } 110 | 111 | #[test] 112 | fn parse_address_ok() { 113 | let expected = Address::from([ 114 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 115 | ]); 116 | assert_eq!( 117 | parse_address("0x000102030405060708090a0b0c0d0e0f10111213").unwrap(), 118 | expected 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /ethcontract-mock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethcontract-mock" 3 | version = "0.25.8" 4 | authors = ["Gnosis developers "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/gnosis/ethcontract-rs" 8 | homepage = "https://github.com/gnosis/ethcontract-rs" 9 | documentation = "https://docs.rs/ethcontract-mock" 10 | description = """ 11 | Tools for mocking ethereum contracts. 12 | """ 13 | 14 | [dependencies] 15 | ethcontract = { version = "0.25.8", path = "../ethcontract", default-features = false, features = ["derive"] } 16 | hex = "0.4" 17 | mockall = "0.11" 18 | rlp = "0.5" 19 | predicates = "3.0" 20 | 21 | [dev-dependencies] 22 | tokio = { version = "1.6", features = ["macros", "rt"] } 23 | ethcontract-derive = { version = "0.25.8", path = "../ethcontract-derive", default-features = false } 24 | -------------------------------------------------------------------------------- /ethcontract-mock/src/details/default.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for building default values for tokens. 2 | 3 | use ethcontract::common::abi::{Bytes, Int, ParamType, Token, Uint}; 4 | use ethcontract::Address; 5 | 6 | /// Builds a default value for the given solidity type. 7 | pub fn default(ty: &ParamType) -> Token { 8 | match ty { 9 | ParamType::Address => Token::Address(Address::default()), 10 | ParamType::Bytes => Token::Bytes(Bytes::default()), 11 | ParamType::Int(_) => Token::Int(Int::default()), 12 | ParamType::Uint(_) => Token::Uint(Uint::default()), 13 | ParamType::Bool => Token::Bool(false), 14 | ParamType::String => Token::String(String::default()), 15 | ParamType::Array(_) => Token::Array(Vec::new()), 16 | ParamType::FixedBytes(n) => Token::FixedBytes(vec![0; *n]), 17 | ParamType::FixedArray(ty, n) => Token::FixedArray(vec![default(ty); *n]), 18 | ParamType::Tuple(tys) => Token::Tuple(tys.iter().map(default).collect()), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ethcontract-mock/src/details/parse.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to parse RPC call arguments. 2 | 3 | use ethcontract::json::{from_value, Value}; 4 | use ethcontract::jsonrpc::serde::Deserialize; 5 | use ethcontract::web3::types::BlockNumber; 6 | use std::fmt::Display; 7 | 8 | /// A helper to parse RPC call arguments. 9 | /// 10 | /// RPC call arguments are parsed from JSON string into an array 11 | /// of `Value`s before they're passed to method handlers. 12 | /// This struct helps to transform `Value`s into actual rust types, 13 | /// while also handling optional arguments. 14 | pub struct Parser { 15 | name: &'static str, 16 | args: Vec, 17 | current: usize, 18 | } 19 | 20 | impl Parser { 21 | /// Create new parser. 22 | pub fn new(name: &'static str, args: Vec) -> Self { 23 | Parser { 24 | name, 25 | args, 26 | current: 0, 27 | } 28 | } 29 | 30 | /// Parse an argument. 31 | pub fn arg Deserialize<'b>>(&mut self) -> T { 32 | if let Some(arg) = self.args.get_mut(self.current) { 33 | self.current += 1; 34 | let val = from_value(std::mem::take(arg)); 35 | self.res(val) 36 | } else { 37 | panic!("not enough arguments for rpc call {:?}", self.name); 38 | } 39 | } 40 | 41 | /// Parse an optional argument, return `None` if arguments list is exhausted. 42 | pub fn arg_opt Deserialize<'b>>(&mut self) -> Option { 43 | if self.current < self.args.len() { 44 | Some(self.arg()) 45 | } else { 46 | None 47 | } 48 | } 49 | 50 | /// Parse an optional argument with a block number. 51 | /// 52 | /// Since [`BlockNumber`] does not implement [`Deserialize`], 53 | /// we can't use [`arg`] to parse it, so we use this helper method. 54 | pub fn block_number_opt(&mut self) -> Option { 55 | let value = self.arg_opt(); 56 | value.map(|value| self.parse_block_number(value)) 57 | } 58 | 59 | /// Finish parsing arguments. 60 | /// 61 | /// If there are unparsed arguments, report them as extraneous. 62 | pub fn done(self) { 63 | // nothing here, actual check is in the `drop` method. 64 | } 65 | 66 | // Helper for parsing block numbers. 67 | fn parse_block_number(&self, value: Value) -> BlockNumber { 68 | match value.as_str() { 69 | Some("latest") => BlockNumber::Latest, 70 | Some("earliest") => BlockNumber::Earliest, 71 | Some("pending") => BlockNumber::Pending, 72 | Some(number) => BlockNumber::Number(self.res(number.parse())), 73 | None => self.err("block number should be a string"), 74 | } 75 | } 76 | 77 | // Unwraps `Result`, adds info with current arg index and rpc name. 78 | fn res(&self, res: Result) -> T { 79 | res.unwrap_or_else(|err| self.err(err)) 80 | } 81 | 82 | // Panics, adds info with current arg index and rpc name. 83 | fn err(&self, err: E) -> ! { 84 | panic!( 85 | "argument {} for rpc call {:?} is invalid: {}", 86 | self.current, self.name, err 87 | ) 88 | } 89 | } 90 | 91 | impl Drop for Parser { 92 | fn drop(&mut self) { 93 | assert!( 94 | std::thread::panicking() || self.current >= self.args.len(), 95 | "too many arguments for rpc call {:?}", 96 | self.name 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ethcontract-mock/src/details/sign.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to work with signed transactions. 2 | 3 | use crate::details::transaction::Transaction; 4 | use ethcontract::common::abi::ethereum_types::BigEndianHash; 5 | use ethcontract::web3::signing; 6 | use ethcontract::web3::types::{Address, H256, U256}; 7 | 8 | /// Parses and verifies raw transaction, including chain ID. 9 | /// 10 | /// Panics if transaction is malformed or if verification fails. 11 | pub fn verify(raw_tx: &[u8], node_chain_id: u64) -> Transaction { 12 | let rlp = rlp::Rlp::new(raw_tx); 13 | 14 | fn err() -> ! { 15 | panic!("invalid transaction data"); 16 | } 17 | fn res(r: Result) -> T { 18 | r.unwrap_or_else(|_| err()) 19 | } 20 | 21 | if !matches!(rlp.prototype(), Ok(rlp::Prototype::List(9))) { 22 | err(); 23 | } 24 | 25 | // TODO: 26 | // 27 | // We could support deployments via RPC calls by introducing 28 | // something like `expect_deployment` method to `Mock` struct. 29 | assert!(res(rlp.at(3)).size() != 0, "mock client does not support deploying contracts via transaction, use `Mock::deploy` instead"); 30 | 31 | let nonce: U256 = res(rlp.val_at(0)); 32 | let gas_price: U256 = res(rlp.val_at(1)); 33 | let gas: U256 = res(rlp.val_at(2)); 34 | let to: Address = res(rlp.val_at(3)); 35 | let value: U256 = res(rlp.val_at(4)); 36 | let data: Vec = res(rlp.val_at(5)); 37 | let v: u64 = res(rlp.val_at(6)); 38 | let r = H256::from_uint(&res(rlp.val_at(7))); 39 | let s = H256::from_uint(&res(rlp.val_at(8))); 40 | 41 | let (chain_id, standard_v) = match v { 42 | v if v >= 35 => ((v - 35) / 2, (v - 25) % 2), 43 | 27 | 28 => panic!("transactions must use eip-155 signatures"), 44 | _ => panic!("invalid transaction signature, v value is out of range"), 45 | }; 46 | 47 | assert!( 48 | chain_id == node_chain_id, 49 | "invalid transaction signature, chain id mismatch" 50 | ); 51 | 52 | let msg_hash = { 53 | let mut rlp = rlp::RlpStream::new(); 54 | 55 | rlp.begin_list(9); 56 | rlp.append(&nonce); 57 | rlp.append(&gas_price); 58 | rlp.append(&gas); 59 | rlp.append(&to); 60 | rlp.append(&value); 61 | rlp.append(&data); 62 | rlp.append(&chain_id); 63 | rlp.append(&0u8); 64 | rlp.append(&0u8); 65 | 66 | signing::keccak256(rlp.as_raw()) 67 | }; 68 | 69 | let signature = { 70 | let mut signature = [0u8; 64]; 71 | signature[..32].copy_from_slice(r.as_bytes()); 72 | signature[32..].copy_from_slice(s.as_bytes()); 73 | signature 74 | }; 75 | 76 | let from = signing::recover(&msg_hash, &signature, standard_v as _) 77 | .unwrap_or_else(|_| panic!("invalid transaction signature, verification failed")); 78 | 79 | Transaction { 80 | from, 81 | to, 82 | nonce, 83 | gas, 84 | gas_price, 85 | value, 86 | data, 87 | hash: signing::keccak256(raw_tx).into(), 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ethcontract-mock/src/details/transaction.rs: -------------------------------------------------------------------------------- 1 | //! Common transaction types. 2 | 3 | use ethcontract::{Address, H256, U256}; 4 | 5 | /// Basic transaction parameters. 6 | pub struct Transaction { 7 | pub from: Address, 8 | pub to: Address, 9 | pub nonce: U256, 10 | pub gas: U256, 11 | pub gas_price: U256, 12 | pub value: U256, 13 | pub data: Vec, 14 | pub hash: H256, 15 | } 16 | 17 | /// Transaction execution result. 18 | pub struct TransactionResult { 19 | /// Result of a method call, error if call is aborted. 20 | pub result: Result, String>, 21 | 22 | /// How many blocks should be mined on top of transaction's block 23 | /// for confirmation to be successful. 24 | pub confirmations: u64, 25 | } 26 | -------------------------------------------------------------------------------- /ethcontract-mock/src/predicate.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for working with predicates. 2 | //! 3 | //! Note: contents of this module are meant to be used via the [`Into`] trait. 4 | //! They are not a part of public API. 5 | 6 | use predicates::reflection::{Child, PredicateReflection}; 7 | use predicates::Predicate; 8 | 9 | /// This trait allows converting tuples of predicates into predicates that 10 | /// accept tuples. That is, if `T = (T1, T2, ...)`, this trait can convert 11 | /// a tuple of predicates `(Predicate, Predicate, ...)` 12 | /// into a `Predicate<(T1, T2, ...)>`. 13 | pub trait TuplePredicate { 14 | /// Concrete implementation of a tuple predicate, depends on tuple length. 15 | type P: Predicate; 16 | 17 | /// Given that `self` is a tuple of predicates `ps = (p1, p2, ...)`, 18 | /// returns a predicate that accepts a tuple `ts = (t1, t2, ...)` 19 | /// and applies predicates element-wise: `ps.0(ts.0) && ps.1(ts.1) && ...`. 20 | fn into_predicate(self) -> Self::P; 21 | } 22 | 23 | pub mod detail { 24 | use super::*; 25 | 26 | macro_rules! impl_tuple_predicate { 27 | ($name: ident, $count: expr, $( $t: ident : $p: ident : $n: tt, )*) => { 28 | pub struct $name<$($t, $p: Predicate<$t>, )*>(($($p, )*), std::marker::PhantomData<($($t, )*)>); 29 | 30 | impl<$($t, $p: Predicate<$t>, )*> std::fmt::Display for $name<$($t, $p, )*> { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "element-wise tuple predicate") 33 | } 34 | } 35 | 36 | impl<$($t, $p: Predicate<$t>, )*> PredicateReflection for $name<$($t, $p, )*> { 37 | fn children(&self) -> Box> + '_> { 38 | let params = vec![$(predicates::reflection::Child::new(stringify!($n), &self.0.$n), )*]; 39 | Box::new(params.into_iter()) 40 | } 41 | } 42 | 43 | impl<$($t, $p: Predicate<$t>, )*> Predicate<($($t, )*)> for $name<$($t, $p, )*> { 44 | #[allow(unused_variables)] 45 | fn eval(&self, variable: &($($t, )*)) -> bool { 46 | $(self.0.$n.eval(&variable.$n) && )* true 47 | } 48 | } 49 | 50 | impl<$($t, $p: Predicate<$t>, )*> TuplePredicate<($($t, )*)> for ($($p, )*) { 51 | type P = $name<$($t, $p, )*>; 52 | fn into_predicate(self) -> Self::P { 53 | $name(self, std::marker::PhantomData) 54 | } 55 | } 56 | } 57 | } 58 | 59 | impl_tuple_predicate!(TuplePredicate0, 0,); 60 | impl_tuple_predicate!(TuplePredicate1, 1, T0:P0:0, ); 61 | impl_tuple_predicate!(TuplePredicate2, 2, T0:P0:0, T1:P1:1, ); 62 | impl_tuple_predicate!(TuplePredicate3, 3, T0:P0:0, T1:P1:1, T2:P2:2, ); 63 | impl_tuple_predicate!(TuplePredicate4, 4, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, ); 64 | impl_tuple_predicate!(TuplePredicate5, 5, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, ); 65 | impl_tuple_predicate!(TuplePredicate6, 6, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, ); 66 | impl_tuple_predicate!(TuplePredicate7, 7, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, ); 67 | impl_tuple_predicate!(TuplePredicate8, 8, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, ); 68 | impl_tuple_predicate!(TuplePredicate9, 9, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, ); 69 | impl_tuple_predicate!(TuplePredicate10, 10, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, ); 70 | impl_tuple_predicate!(TuplePredicate11, 11, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, ); 71 | impl_tuple_predicate!(TuplePredicate12, 12, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, ); 72 | impl_tuple_predicate!(TuplePredicate13, 13, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, ); 73 | impl_tuple_predicate!(TuplePredicate14, 14, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, T13:P13:13, ); 74 | impl_tuple_predicate!(TuplePredicate15, 15, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, T13:P13:13, T14:P14:14, ); 75 | impl_tuple_predicate!(TuplePredicate16, 16, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, T13:P13:13, T14:P14:14, T15:P15:15, ); 76 | } 77 | -------------------------------------------------------------------------------- /ethcontract-mock/src/range.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for working with rust's ranges. 2 | //! 3 | //! Note: contents of this module are meant to be used via the [`Into`] trait. 4 | //! They are not a part of public API. 5 | 6 | use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}; 7 | 8 | /// A type that represents a rust's range, i.e., a struct produced 9 | /// by range syntax like `..`, `a..`, `..b`, `..=c`, `d..e`, or `f..=g`. 10 | /// 11 | /// Each of the above range types is represented by a distinct `std` struct. 12 | /// Standard library does not export a single struct to represent all of them, 13 | /// so we have to implement it ourselves. 14 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 15 | pub struct TimesRange(usize, usize); 16 | 17 | impl TimesRange { 18 | /// Checks if expectation can be called if it was already called 19 | /// this number of times. 20 | pub fn can_call(&self, x: usize) -> bool { 21 | x + 1 < self.1 22 | } 23 | 24 | /// Checks if the given element is contained by this range. 25 | pub fn contains(&self, x: usize) -> bool { 26 | self.0 <= x && x < self.1 27 | } 28 | 29 | /// Checks if this range contains exactly one element. 30 | pub fn is_exact(&self) -> bool { 31 | (self.1 - self.0) == 1 32 | } 33 | 34 | /// Returns lower bound on this range. 35 | pub fn lower_bound(&self) -> usize { 36 | self.0 37 | } 38 | 39 | /// Returns upper bound on this range. 40 | pub fn upper_bound(&self) -> usize { 41 | self.1 42 | } 43 | } 44 | 45 | impl Default for TimesRange { 46 | fn default() -> TimesRange { 47 | TimesRange(0, usize::MAX) 48 | } 49 | } 50 | 51 | impl From for TimesRange { 52 | fn from(n: usize) -> TimesRange { 53 | TimesRange(n, n + 1) 54 | } 55 | } 56 | 57 | impl From> for TimesRange { 58 | fn from(r: Range) -> TimesRange { 59 | assert!(r.end > r.start, "backwards range"); 60 | TimesRange(r.start, r.end) 61 | } 62 | } 63 | 64 | impl From> for TimesRange { 65 | fn from(r: RangeFrom) -> TimesRange { 66 | TimesRange(r.start, usize::MAX) 67 | } 68 | } 69 | 70 | impl From for TimesRange { 71 | fn from(_: RangeFull) -> TimesRange { 72 | TimesRange(0, usize::MAX) 73 | } 74 | } 75 | 76 | impl From> for TimesRange { 77 | fn from(r: RangeInclusive) -> TimesRange { 78 | assert!(r.end() >= r.start(), "backwards range"); 79 | TimesRange(*r.start(), *r.end() + 1) 80 | } 81 | } 82 | 83 | impl From> for TimesRange { 84 | fn from(r: RangeTo) -> TimesRange { 85 | TimesRange(0, r.end) 86 | } 87 | } 88 | 89 | impl From> for TimesRange { 90 | fn from(r: RangeToInclusive) -> TimesRange { 91 | TimesRange(0, r.end + 1) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/batch.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | async fn batch_ok() -> Result { 5 | let (_, _, contract, instance) = setup(); 6 | 7 | let mut seq = mockall::Sequence::new(); 8 | 9 | contract 10 | .expect(ERC20::signatures().name()) 11 | .once() 12 | .in_sequence(&mut seq) 13 | .returns("WrappedEther".into()); 14 | contract 15 | .expect(ERC20::signatures().symbol()) 16 | .once() 17 | .in_sequence(&mut seq) 18 | .returns("WETH".into()); 19 | contract 20 | .expect(ERC20::signatures().decimals()) 21 | .once() 22 | .returns(18) 23 | .in_sequence(&mut seq); 24 | contract 25 | .expect(ERC20::signatures().total_supply()) 26 | .once() 27 | .in_sequence(&mut seq) 28 | .returns_error("failed calculating total supply".into()); 29 | 30 | let mut batch = ethcontract::batch::CallBatch::new(contract.transport()); 31 | 32 | let name = instance.name().batch_call(&mut batch); 33 | let symbol = instance.symbol().batch_call(&mut batch); 34 | let decimals = instance.decimals().batch_call(&mut batch); 35 | let total_supply = instance.total_supply().batch_call(&mut batch); 36 | 37 | batch.execute_all(4).await; 38 | 39 | assert_eq!(name.await?, "WrappedEther"); 40 | assert_eq!(symbol.await?, "WETH"); 41 | assert_eq!(decimals.await?, 18); 42 | assert!(total_supply 43 | .await 44 | .unwrap_err() 45 | .to_string() 46 | .contains("failed calculating total supply")); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/doctest/common.rs: -------------------------------------------------------------------------------- 1 | // Common types used in tests and doctests. 2 | // 3 | // This file is `include!`d by doctests, it is not a part of the crate. 4 | 5 | use ethcontract::dyns::DynInstance; 6 | use ethcontract::prelude::*; 7 | use ethcontract_mock::{CallContext, Contract, Expectation, Mock, Signature}; 8 | use predicates::prelude::*; 9 | 10 | fn simple_abi() -> ethcontract::common::Abi { 11 | static ABI: &str = r#" 12 | { 13 | "abi": [ 14 | { 15 | "inputs": [ 16 | { 17 | "internalType": "uint256", 18 | "name": "a", 19 | "type": "uint256" 20 | }, 21 | { 22 | "internalType": "uint256", 23 | "name": "b", 24 | "type": "uint256" 25 | } 26 | ], 27 | "name": "Foo", 28 | "outputs": [ 29 | { 30 | "internalType": "uint256", 31 | "name": "a", 32 | "type": "uint256" 33 | } 34 | ], 35 | "stateMutability": "view", 36 | "type": "function" 37 | } 38 | ] 39 | } 40 | "#; 41 | 42 | ethcontract::common::artifact::truffle::TruffleLoader::new() 43 | .load_contract_from_str(ABI) 44 | .unwrap() 45 | .abi 46 | } 47 | 48 | fn voting_abi() -> ethcontract::common::Abi { 49 | static ABI: &str = r#" 50 | { 51 | "abi": [ 52 | { 53 | "inputs": [ 54 | { 55 | "internalType": "uint256", 56 | "name": "proposal", 57 | "type": "uint256" 58 | } 59 | ], 60 | "name": "vote", 61 | "outputs": [], 62 | "stateMutability": "nonpayable", 63 | "type": "function" 64 | }, 65 | { 66 | "inputs": [], 67 | "name": "winningProposal", 68 | "outputs": [ 69 | { 70 | "internalType": "uint256", 71 | "name": "winningProposal_", 72 | "type": "uint256" 73 | } 74 | ], 75 | "stateMutability": "view", 76 | "type": "function" 77 | } 78 | ] 79 | } 80 | "#; 81 | 82 | ethcontract::common::artifact::truffle::TruffleLoader::new() 83 | .load_contract_from_str(ABI) 84 | .unwrap() 85 | .abi 86 | } 87 | 88 | fn contract() -> Contract { 89 | Mock::new(10).deploy(simple_abi()) 90 | } 91 | 92 | fn signature() -> Signature<(u64, u64), u64> { 93 | Signature::new([54, 175, 98, 158]) 94 | } 95 | 96 | fn address_for(who: &str) -> Address { 97 | account_for(who).address() 98 | } 99 | 100 | fn account_for(who: &str) -> Account { 101 | use ethcontract::web3::signing::keccak256; 102 | Account::Offline( 103 | PrivateKey::from_raw(keccak256(who.as_bytes())).unwrap(), 104 | None, 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_block_number.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | async fn block_number_initially_zero() -> Result { 5 | let web3 = Mock::new(1234).web3(); 6 | 7 | assert_eq!(web3.eth().block_number().await?, 0.into()); 8 | 9 | Ok(()) 10 | } 11 | 12 | #[tokio::test] 13 | async fn block_number_advanced_after_tx() -> Result { 14 | let (_, web3, contract, instance) = setup(); 15 | 16 | contract.expect(ERC20::signatures().transfer()); 17 | 18 | assert_eq!(web3.eth().block_number().await?, 0.into()); 19 | 20 | instance 21 | .transfer(address_for("Alice"), 100.into()) 22 | .send() 23 | .await?; 24 | 25 | assert_eq!(web3.eth().block_number().await?, 1.into()); 26 | 27 | Ok(()) 28 | } 29 | 30 | #[tokio::test] 31 | async fn block_number_advanced_and_confirmed_after_tx() -> Result { 32 | let (_, web3, contract, instance) = setup(); 33 | 34 | contract 35 | .expect(ERC20::signatures().transfer()) 36 | .confirmations(5); 37 | 38 | assert_eq!(web3.eth().block_number().await?, 0.into()); 39 | 40 | instance 41 | .transfer(address_for("Alice"), 100.into()) 42 | .send() 43 | .await?; 44 | 45 | assert_eq!(web3.eth().block_number().await?, 6.into()); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[tokio::test] 51 | async fn block_number_is_not_advanced_after_call_or_gas_estimation() -> Result { 52 | let (_, web3, contract, instance) = setup(); 53 | 54 | contract.expect(ERC20::signatures().transfer()); 55 | 56 | assert_eq!(web3.eth().block_number().await?, 0.into()); 57 | 58 | instance 59 | .transfer(address_for("Alice"), 100.into()) 60 | .call() 61 | .await?; 62 | 63 | assert_eq!(web3.eth().block_number().await?, 0.into()); 64 | 65 | instance 66 | .transfer(address_for("Alice"), 100.into()) 67 | .into_inner() 68 | .estimate_gas() 69 | .await?; 70 | 71 | assert_eq!(web3.eth().block_number().await?, 0.into()); 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_chain_id.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | async fn chain_id() -> Result { 5 | let web3 = Mock::new(1234).web3(); 6 | 7 | assert_eq!(web3.eth().chain_id().await?, 1234.into()); 8 | 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_estimate_gas.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use ethcontract::web3::types::CallRequest; 3 | 4 | #[tokio::test] 5 | async fn estimate_gas_returns_one() -> Result { 6 | let (_, _, contract, instance) = setup(); 7 | 8 | contract.expect(ERC20::signatures().transfer()); 9 | 10 | let gas = instance 11 | .transfer(address_for("Alice"), 100.into()) 12 | .into_inner() 13 | .estimate_gas() 14 | .await?; 15 | 16 | assert_eq!(gas, 1.into()); 17 | 18 | Ok(()) 19 | } 20 | 21 | #[tokio::test] 22 | async fn estimate_gas_is_supported_for_edge_block() -> Result { 23 | let (_, web3, contract, instance) = setup(); 24 | 25 | contract.expect(ERC20::signatures().transfer()); 26 | 27 | instance 28 | .transfer(address_for("Bob"), 100.into()) 29 | .send() 30 | .await?; 31 | instance 32 | .transfer(address_for("Bob"), 100.into()) 33 | .send() 34 | .await?; 35 | 36 | let request = { 37 | let tx = instance 38 | .transfer(address_for("Alice"), 100.into()) 39 | .into_inner(); 40 | 41 | CallRequest { 42 | from: Some(address_for("Alice")), 43 | to: Some(contract.address), 44 | gas: None, 45 | gas_price: None, 46 | value: None, 47 | data: tx.data, 48 | transaction_type: None, 49 | access_list: None, 50 | max_fee_per_gas: None, 51 | max_priority_fee_per_gas: None, 52 | } 53 | }; 54 | 55 | assert_eq!( 56 | web3.eth() 57 | .estimate_gas(request.clone(), Some(BlockNumber::Latest)) 58 | .await?, 59 | 1.into() 60 | ); 61 | assert_eq!( 62 | web3.eth() 63 | .estimate_gas(request.clone(), Some(BlockNumber::Pending)) 64 | .await?, 65 | 1.into() 66 | ); 67 | assert_eq!( 68 | web3.eth() 69 | .estimate_gas(request.clone(), Some(BlockNumber::Number(2.into()))) 70 | .await?, 71 | 1.into() 72 | ); 73 | 74 | Ok(()) 75 | } 76 | 77 | #[tokio::test] 78 | #[should_panic(expected = "mock node does not support executing methods on non-last block")] 79 | async fn estimate_gas_is_not_supported_for_custom_block() { 80 | let (_, web3, contract, instance) = setup(); 81 | 82 | contract.expect(ERC20::signatures().transfer()); 83 | 84 | let request = { 85 | let tx = instance 86 | .transfer(address_for("Alice"), 100.into()) 87 | .into_inner(); 88 | 89 | CallRequest { 90 | from: Some(address_for("Alice")), 91 | to: Some(contract.address), 92 | gas: None, 93 | gas_price: None, 94 | value: None, 95 | data: tx.data, 96 | transaction_type: None, 97 | access_list: None, 98 | max_fee_per_gas: None, 99 | max_priority_fee_per_gas: None, 100 | } 101 | }; 102 | 103 | web3.eth() 104 | .estimate_gas(request.clone(), Some(BlockNumber::Number(1.into()))) 105 | .await 106 | .unwrap(); 107 | } 108 | 109 | #[tokio::test] 110 | #[should_panic(expected = "mock node does not support executing methods on earliest block")] 111 | async fn estimate_gas_is_not_supported_for_earliest_block() { 112 | let (_, web3, contract, instance) = setup(); 113 | 114 | contract.expect(ERC20::signatures().transfer()); 115 | 116 | let request = { 117 | let tx = instance 118 | .transfer(address_for("Alice"), 100.into()) 119 | .into_inner(); 120 | 121 | CallRequest { 122 | from: Some(address_for("Alice")), 123 | to: Some(contract.address), 124 | gas: None, 125 | gas_price: None, 126 | value: None, 127 | data: tx.data, 128 | transaction_type: None, 129 | access_list: None, 130 | max_fee_per_gas: None, 131 | max_priority_fee_per_gas: None, 132 | } 133 | }; 134 | 135 | web3.eth() 136 | .estimate_gas(request.clone(), Some(BlockNumber::Earliest)) 137 | .await 138 | .unwrap(); 139 | } 140 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_gas_price.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | async fn gas_price() -> Result { 5 | let mock = Mock::new(1234); 6 | let web3 = mock.web3(); 7 | 8 | assert_eq!(web3.eth().gas_price().await?, 1.into()); 9 | 10 | mock.update_gas_price(10); 11 | 12 | assert_eq!(web3.eth().gas_price().await?, 10.into()); 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_get_transaction_receipt.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use ethcontract::transaction::ResolveCondition; 3 | 4 | #[tokio::test] 5 | async fn transaction_receipt_is_returned() -> Result { 6 | let (_, web3, contract, instance) = setup(); 7 | 8 | contract.expect(ERC20::signatures().transfer()); 9 | 10 | let hash = instance 11 | .transfer(address_for("Bob"), 100.into()) 12 | .into_inner() 13 | .resolve(ResolveCondition::Pending) 14 | .send() 15 | .await? 16 | .hash(); 17 | 18 | let receipt = web3.eth().transaction_receipt(hash).await?.unwrap(); 19 | assert_eq!(receipt.transaction_hash, hash); 20 | assert_eq!(receipt.block_number, Some(1.into())); 21 | assert_eq!(receipt.status, Some(1.into())); 22 | 23 | Ok(()) 24 | } 25 | 26 | #[tokio::test] 27 | #[should_panic(expected = "there is no transaction with hash")] 28 | async fn transaction_receipt_is_panicking_when_hash_not_fount() { 29 | let web3 = Mock::new(1234).web3(); 30 | 31 | web3.eth() 32 | .transaction_receipt(Default::default()) 33 | .await 34 | .unwrap(); 35 | } 36 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_send_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::Mock; 2 | use ethcontract::web3::types::TransactionRequest; 3 | 4 | #[tokio::test] 5 | #[should_panic(expected = "mock node can't sign transactions")] 6 | async fn send_transaction() { 7 | // When we implement `send_transaction`, we should add same tests as for 8 | // send_raw_transaction (expect for raw transaction format/signing) 9 | // and also a test that checks that returned transaction hash is correct. 10 | 11 | let web3 = Mock::new(1234).web3(); 12 | 13 | web3.eth() 14 | .send_transaction(TransactionRequest::default()) 15 | .await 16 | .unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/eth_transaction_count.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | async fn transaction_count_initially_zero() -> Result { 5 | let web3 = Mock::new(1234).web3(); 6 | 7 | assert_eq!( 8 | web3.eth() 9 | .transaction_count(address_for("Alice"), None) 10 | .await?, 11 | 0.into() 12 | ); 13 | 14 | Ok(()) 15 | } 16 | 17 | #[tokio::test] 18 | async fn transaction_count_advanced_after_tx() -> Result { 19 | let (_, web3, contract, instance) = setup(); 20 | 21 | contract.expect(ERC20::signatures().transfer()); 22 | 23 | assert_eq!( 24 | web3.eth() 25 | .transaction_count(address_for("Alice"), None) 26 | .await?, 27 | 0.into() 28 | ); 29 | 30 | instance 31 | .transfer(address_for("Bob"), 100.into()) 32 | .send() 33 | .await?; 34 | 35 | assert_eq!( 36 | web3.eth() 37 | .transaction_count(address_for("Alice"), None) 38 | .await?, 39 | 1.into() 40 | ); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[tokio::test] 46 | async fn transaction_count_is_not_advanced_after_call_or_gas_estimation() -> Result { 47 | let (_, web3, contract, instance) = setup(); 48 | 49 | contract.expect(ERC20::signatures().transfer()); 50 | 51 | assert_eq!( 52 | web3.eth() 53 | .transaction_count(address_for("Alice"), None) 54 | .await?, 55 | 0.into() 56 | ); 57 | 58 | instance 59 | .transfer(address_for("Bob"), 100.into()) 60 | .call() 61 | .await?; 62 | 63 | assert_eq!( 64 | web3.eth() 65 | .transaction_count(address_for("Alice"), None) 66 | .await?, 67 | 0.into() 68 | ); 69 | 70 | instance 71 | .transfer(address_for("Bob"), 100.into()) 72 | .into_inner() 73 | .estimate_gas() 74 | .await?; 75 | 76 | assert_eq!( 77 | web3.eth() 78 | .transaction_count(address_for("Alice"), None) 79 | .await?, 80 | 0.into() 81 | ); 82 | 83 | Ok(()) 84 | } 85 | 86 | #[tokio::test] 87 | async fn transaction_count_is_supported_for_edge_block() -> Result { 88 | let (_, web3, contract, instance) = setup(); 89 | 90 | contract.expect(ERC20::signatures().transfer()); 91 | 92 | instance 93 | .transfer(address_for("Bob"), 100.into()) 94 | .send() 95 | .await?; 96 | instance 97 | .transfer(address_for("Bob"), 100.into()) 98 | .send() 99 | .await?; 100 | 101 | assert_eq!( 102 | web3.eth() 103 | .transaction_count(address_for("Alice"), Some(BlockNumber::Earliest)) 104 | .await?, 105 | 0.into() 106 | ); 107 | assert_eq!( 108 | web3.eth() 109 | .transaction_count(address_for("Alice"), Some(BlockNumber::Number(0.into()))) 110 | .await?, 111 | 0.into() 112 | ); 113 | 114 | assert_eq!( 115 | web3.eth() 116 | .transaction_count(address_for("Alice"), Some(BlockNumber::Latest)) 117 | .await?, 118 | 2.into() 119 | ); 120 | assert_eq!( 121 | web3.eth() 122 | .transaction_count(address_for("Alice"), Some(BlockNumber::Pending)) 123 | .await?, 124 | 2.into() 125 | ); 126 | assert_eq!( 127 | web3.eth() 128 | .transaction_count(address_for("Alice"), Some(BlockNumber::Number(2.into()))) 129 | .await?, 130 | 2.into() 131 | ); 132 | 133 | Ok(()) 134 | } 135 | 136 | #[tokio::test] 137 | #[should_panic( 138 | expected = "mock node does not support returning transaction count for specific block number" 139 | )] 140 | async fn transaction_count_is_not_supported_for_custom_block() { 141 | let web3 = Mock::new(1234).web3(); 142 | 143 | web3.eth() 144 | .transaction_count(address_for("Alice"), Some(BlockNumber::Number(1.into()))) 145 | .await 146 | .unwrap(); 147 | } 148 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/mod.rs: -------------------------------------------------------------------------------- 1 | //! Tests for mock crate. 2 | //! 3 | //! # TODO 4 | //! 5 | //! Some tests for API are missing: 6 | //! 7 | //! - malformed input in 8 | //! - eth_call 9 | //! - eth_sendTransaction 10 | //! - eth_sendRawTransaction 11 | //! - eth_estimateGas 12 | //! 13 | //! - deployment works 14 | //! - different contracts have different addresses 15 | //! - returned instance has correct address 16 | //! 17 | //! - call to method with no expectations panics 18 | //! - tx to method with no expectations panics 19 | //! - call to method with an expectation succeeds 20 | //! - tx to method with an expectation succeeds 21 | //! 22 | //! - call expectations only match calls 23 | //! - tx expectations only match txs 24 | //! - regular expectations match both calls and txs 25 | //! 26 | //! - predicate filters expectation so test panics 27 | //! - predicate filters multiple expectations so test panics 28 | //! - expectations are evaluated in FIFO order 29 | //! - predicate_fn gets called 30 | //! - predicate_fn_ctx gets called 31 | //! 32 | //! - times can be set for expectation 33 | //! - if expectation called not enough times, test panics 34 | //! - if expectation called enough times, test passes 35 | //! - if expectation called enough times, it is satisfied and test panics 36 | //! - if expectation called enough times, it is satisfied and next expectation is used 37 | //! - expectation is not satisfied if calls are not matched by a predicate 38 | //! 39 | //! - expectations can be added to sequences 40 | //! - expectation can only be in one sequence 41 | //! - adding expectation to sequence requires exact time greater than zero 42 | //! - updating times after expectation was set requires exact time greater than zero 43 | //! - when method called in-order, test passes 44 | //! - when method called in-order multiple times, test passes 45 | //! - when method called out-of-order, test panics 46 | //! - when method called out-of-order first time with times(2), test panics 47 | //! - when method called out-of-order last time with times(2), test panics 48 | //! 49 | //! - default value for solidity type is returned 50 | //! - rust's Default trait is not honored 51 | //! - you can set return value 52 | //! - returns_fn gets called 53 | //! - returns_fn_ctx gets called 54 | //! 55 | //! - expectations become immutable after use in calls and txs 56 | //! - expectations become immutable after use in calls and txs even if they are not matched by a predicate 57 | //! - new expectations are not immutable 58 | //! 59 | //! - checkpoint verifies expectations 60 | //! - checkpoint clears expectations 61 | //! - expectations become invalid 62 | //! 63 | //! - confirmations plays nicely with tx.confirmations 64 | 65 | use crate::utils::*; 66 | use crate::{Contract, Mock}; 67 | use ethcontract::dyns::DynWeb3; 68 | use ethcontract::prelude::*; 69 | use predicates::prelude::*; 70 | 71 | mod batch; 72 | mod eth_block_number; 73 | mod eth_chain_id; 74 | mod eth_estimate_gas; 75 | mod eth_gas_price; 76 | mod eth_get_transaction_receipt; 77 | mod eth_send_transaction; 78 | mod eth_transaction_count; 79 | mod net_version; 80 | mod returns; 81 | 82 | type Result = std::result::Result<(), Box>; 83 | 84 | ethcontract::contract!("examples/truffle/build/contracts/ERC20.json"); 85 | 86 | fn setup() -> (Mock, DynWeb3, Contract, ERC20) { 87 | let mock = Mock::new(1234); 88 | let web3 = mock.web3(); 89 | let contract = mock.deploy(ERC20::raw_contract().interface.abi.clone()); 90 | let mut instance = ERC20::at(&web3, contract.address); 91 | instance.defaults_mut().from = Some(account_for("Alice")); 92 | 93 | (mock, web3, contract, instance) 94 | } 95 | 96 | #[tokio::test] 97 | async fn general_test() { 98 | let mock = crate::Mock::new(1234); 99 | let contract = mock.deploy(ERC20::raw_contract().interface.abi.clone()); 100 | 101 | let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); 102 | 103 | let mut sequence = mockall::Sequence::new(); 104 | 105 | contract 106 | .expect(ERC20::signatures().balance_of()) 107 | .once() 108 | .predicate((predicate::eq(address_for("Bob")),)) 109 | .returns(U256::from(0)) 110 | .in_sequence(&mut sequence); 111 | 112 | contract 113 | .expect(ERC20::signatures().transfer()) 114 | .once() 115 | .predicate_fn_ctx(|ctx, _| !ctx.is_view_call) 116 | .returns_fn_ctx({ 117 | let called = called.clone(); 118 | move |ctx, (recipient, amount)| { 119 | assert_eq!(ctx.from, address_for("Alice")); 120 | assert_eq!(ctx.nonce.as_u64(), 0); 121 | assert_eq!(ctx.gas.as_u64(), 1); 122 | assert_eq!(ctx.gas_price.as_u64(), 1); 123 | assert_eq!(recipient, address_for("Bob")); 124 | assert_eq!(amount.as_u64(), 100); 125 | 126 | called.store(true, std::sync::atomic::Ordering::Relaxed); 127 | 128 | Ok(true) 129 | } 130 | }) 131 | .confirmations(3) 132 | .in_sequence(&mut sequence); 133 | 134 | contract 135 | .expect(ERC20::signatures().balance_of()) 136 | .once() 137 | .predicate((predicate::eq(address_for("Bob")),)) 138 | .returns(U256::from(100)) 139 | .in_sequence(&mut sequence); 140 | 141 | contract 142 | .expect(ERC20::signatures().balance_of()) 143 | .predicate((predicate::eq(address_for("Alice")),)) 144 | .returns(U256::from(100000)); 145 | 146 | let actual_contract = ERC20::at(&mock.web3(), contract.address); 147 | 148 | let balance = actual_contract 149 | .balance_of(address_for("Bob")) 150 | .call() 151 | .await 152 | .unwrap(); 153 | assert_eq!(balance.as_u64(), 0); 154 | 155 | assert!(!called.load(std::sync::atomic::Ordering::Relaxed)); 156 | actual_contract 157 | .transfer(address_for("Bob"), U256::from(100)) 158 | .from(account_for("Alice")) 159 | .confirmations(3) 160 | .send() 161 | .await 162 | .unwrap(); 163 | assert!(called.load(std::sync::atomic::Ordering::Relaxed)); 164 | 165 | let balance = actual_contract 166 | .balance_of(address_for("Bob")) 167 | .call() 168 | .await 169 | .unwrap(); 170 | assert_eq!(balance.as_u64(), 100); 171 | 172 | let balance = actual_contract 173 | .balance_of(address_for("Alice")) 174 | .call() 175 | .await 176 | .unwrap(); 177 | assert_eq!(balance.as_u64(), 100000); 178 | } 179 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/net_version.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[tokio::test] 4 | async fn chain_id() -> Result { 5 | let web3 = Mock::new(1234).web3(); 6 | 7 | assert_eq!(web3.eth().chain_id().await?, 1234.into()); 8 | 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /ethcontract-mock/src/test/returns.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | ethcontract::contract!("examples/truffle/build/contracts/AbiTypes.json"); 4 | 5 | #[tokio::test] 6 | async fn returns_default() -> Result { 7 | let contract = Mock::new(1234).deploy(AbiTypes::raw_contract().interface.abi.clone()); 8 | 9 | contract.expect(AbiTypes::signatures().get_void()); 10 | contract.expect(AbiTypes::signatures().get_u8()); 11 | contract.expect(AbiTypes::signatures().abiv_2_struct()); 12 | contract.expect(AbiTypes::signatures().abiv_2_array_of_struct()); 13 | contract.expect(AbiTypes::signatures().multiple_results()); 14 | contract.expect(AbiTypes::signatures().multiple_results_struct()); 15 | 16 | let instance = AbiTypes::at(&contract.web3(), contract.address); 17 | 18 | instance.get_void().call().await?; 19 | assert_eq!(instance.get_u8().call().await?, 0); 20 | assert_eq!(instance.abiv_2_struct((1, 2)).call().await?, (0, 0)); 21 | assert_eq!( 22 | instance 23 | .abiv_2_array_of_struct(vec![(1, 2), (3, 4)]) 24 | .call() 25 | .await?, 26 | vec![] 27 | ); 28 | assert_eq!(instance.multiple_results().call().await?, (0, 0, 0)); 29 | assert_eq!( 30 | instance.multiple_results_struct().call().await?, 31 | ((0, 0), (0, 0)) 32 | ); 33 | 34 | Ok(()) 35 | } 36 | 37 | #[tokio::test] 38 | async fn returns_const() -> Result { 39 | let contract = Mock::new(1234).deploy(AbiTypes::raw_contract().interface.abi.clone()); 40 | 41 | contract 42 | .expect(AbiTypes::signatures().get_void()) 43 | .returns(()); 44 | contract.expect(AbiTypes::signatures().get_u8()).returns(42); 45 | contract 46 | .expect(AbiTypes::signatures().abiv_2_struct()) 47 | .returns((1, 2)); 48 | contract 49 | .expect(AbiTypes::signatures().abiv_2_array_of_struct()) 50 | .returns(vec![(1, 2), (3, 4)]); 51 | contract 52 | .expect(AbiTypes::signatures().multiple_results()) 53 | .returns((1, 2, 3)); 54 | contract 55 | .expect(AbiTypes::signatures().multiple_results_struct()) 56 | .returns(((1, 2), (3, 4))); 57 | 58 | let instance = AbiTypes::at(&contract.web3(), contract.address); 59 | 60 | instance.get_void().call().await?; 61 | assert_eq!(instance.get_u8().call().await?, 42); 62 | assert_eq!(instance.abiv_2_struct((1, 2)).call().await?, (1, 2)); 63 | assert_eq!( 64 | instance 65 | .abiv_2_array_of_struct(vec![(1, 2), (3, 4)]) 66 | .call() 67 | .await?, 68 | vec![(1, 2), (3, 4)] 69 | ); 70 | assert_eq!(instance.multiple_results().call().await?, (1, 2, 3)); 71 | assert_eq!( 72 | instance.multiple_results_struct().call().await?, 73 | ((1, 2), (3, 4)) 74 | ); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[tokio::test] 80 | async fn returns_fn() -> Result { 81 | let contract = Mock::new(1234).deploy(AbiTypes::raw_contract().interface.abi.clone()); 82 | 83 | contract 84 | .expect(AbiTypes::signatures().get_void()) 85 | .returns_fn(|_| Ok(())); 86 | contract 87 | .expect(AbiTypes::signatures().get_u8()) 88 | .returns_fn(|_| Ok(42)); 89 | contract 90 | .expect(AbiTypes::signatures().abiv_2_struct()) 91 | .returns_fn(|(x,)| Ok(x)); 92 | contract 93 | .expect(AbiTypes::signatures().abiv_2_array_of_struct()) 94 | .returns_fn(|(x,)| Ok(x)); 95 | contract 96 | .expect(AbiTypes::signatures().multiple_results()) 97 | .returns_fn(|_| Ok((1, 2, 3))); 98 | contract 99 | .expect(AbiTypes::signatures().multiple_results_struct()) 100 | .returns_fn(|_| Ok(((1, 2), (3, 4)))); 101 | 102 | let instance = AbiTypes::at(&contract.web3(), contract.address); 103 | 104 | instance.get_void().call().await?; 105 | assert_eq!(instance.get_u8().call().await?, 42); 106 | assert_eq!(instance.abiv_2_struct((1, 2)).call().await?, (1, 2)); 107 | assert_eq!( 108 | instance 109 | .abiv_2_array_of_struct(vec![(1, 2), (3, 4)]) 110 | .call() 111 | .await?, 112 | vec![(1, 2), (3, 4)] 113 | ); 114 | assert_eq!(instance.multiple_results().call().await?, (1, 2, 3)); 115 | assert_eq!( 116 | instance.multiple_results_struct().call().await?, 117 | ((1, 2), (3, 4)) 118 | ); 119 | 120 | Ok(()) 121 | } 122 | 123 | #[tokio::test] 124 | async fn returns_fn_ctx() -> Result { 125 | let contract = Mock::new(1234).deploy(AbiTypes::raw_contract().interface.abi.clone()); 126 | 127 | contract 128 | .expect(AbiTypes::signatures().get_void()) 129 | .returns_fn_ctx(|_, _| Ok(())); 130 | contract 131 | .expect(AbiTypes::signatures().get_u8()) 132 | .returns_fn_ctx(|_, _| Ok(42)); 133 | contract 134 | .expect(AbiTypes::signatures().abiv_2_struct()) 135 | .returns_fn_ctx(|_, (x,)| Ok(x)); 136 | contract 137 | .expect(AbiTypes::signatures().abiv_2_array_of_struct()) 138 | .returns_fn_ctx(|_, (x,)| Ok(x)); 139 | contract 140 | .expect(AbiTypes::signatures().multiple_results()) 141 | .returns_fn_ctx(|_, _| Ok((1, 2, 3))); 142 | contract 143 | .expect(AbiTypes::signatures().multiple_results_struct()) 144 | .returns_fn_ctx(|_, _| Ok(((1, 2), (3, 4)))); 145 | 146 | let instance = AbiTypes::at(&contract.web3(), contract.address); 147 | 148 | instance.get_void().call().await?; 149 | assert_eq!(instance.get_u8().call().await?, 42); 150 | assert_eq!(instance.abiv_2_struct((1, 2)).call().await?, (1, 2)); 151 | assert_eq!( 152 | instance 153 | .abiv_2_array_of_struct(vec![(1, 2), (3, 4)]) 154 | .call() 155 | .await?, 156 | vec![(1, 2), (3, 4)] 157 | ); 158 | assert_eq!(instance.multiple_results().call().await?, (1, 2, 3)); 159 | assert_eq!( 160 | instance.multiple_results_struct().call().await?, 161 | ((1, 2), (3, 4)) 162 | ); 163 | 164 | Ok(()) 165 | } 166 | -------------------------------------------------------------------------------- /ethcontract-mock/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Convenience utilities for tests. 2 | 3 | use ethcontract::{Account, Address, PrivateKey}; 4 | 5 | /// Generate public address by hashing the given string. 6 | /// 7 | /// # Safety 8 | /// 9 | /// This function is intended for tests and should not be used in production. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// # use ethcontract_mock::utils::address_for; 15 | /// let address = address_for("Alice"); 16 | /// # assert_eq!(address, "0xbf0b5a4099f0bf6c8bc4252ebec548bae95602ea".parse().unwrap()); 17 | /// ``` 18 | pub fn address_for(who: &str) -> Address { 19 | account_for(who).address() 20 | } 21 | 22 | /// Shortcut for [`address_for`]`("Alice")`. 23 | /// 24 | /// # Examples 25 | /// 26 | /// ``` 27 | /// # use ethcontract_mock::utils::address; 28 | /// let address = address(); 29 | /// # assert_eq!(address, "0xbf0b5a4099f0bf6c8bc4252ebec548bae95602ea".parse().unwrap()); 30 | /// ``` 31 | pub fn address() -> Address { 32 | address_for("Alice") 33 | } 34 | 35 | /// Generate a private key by hashing the given string. 36 | /// 37 | /// # Safety 38 | /// 39 | /// This function is intended for tests and should not be used in production. 40 | /// 41 | /// # Examples 42 | /// 43 | /// ``` 44 | /// # use ethcontract_mock::utils::account_for; 45 | /// let account = account_for("Bob"); 46 | /// # assert_eq!(account.address(), "0x4dba461ca9342f4a6cf942abd7eacf8ae259108c".parse().unwrap()); 47 | /// ``` 48 | pub fn account_for(who: &str) -> Account { 49 | use ethcontract::web3::signing::keccak256; 50 | Account::Offline( 51 | PrivateKey::from_raw(keccak256(who.as_bytes())).unwrap(), 52 | None, 53 | ) 54 | } 55 | 56 | /// Shortcut for [`account_for`]`("Alice")`. 57 | /// 58 | /// # Examples 59 | /// 60 | /// ``` 61 | /// # use ethcontract_mock::utils::account; 62 | /// let account = account(); 63 | /// # assert_eq!(account.address(), "0xbf0b5a4099f0bf6c8bc4252ebec548bae95602ea".parse().unwrap()); 64 | /// ``` 65 | pub fn account() -> Account { 66 | account_for("Alice") 67 | } 68 | 69 | /// Deploy a mocked version of a generated contract. 70 | /// 71 | /// # Parameters 72 | /// 73 | /// - `mock`: a [Mock] instance. 74 | /// - `contract` type of the contract. 75 | /// 76 | /// # Examples 77 | /// 78 | /// ``` 79 | /// # use ethcontract_mock::{Mock, mock_contract}; 80 | /// # ethcontract::contract!( 81 | /// # "../examples/truffle/build/contracts/IERC20.json", 82 | /// # contract = IERC20 as ERC20, 83 | /// # ); 84 | /// # fn main() { 85 | /// let mock = Mock::new(1234); 86 | /// let (contract, instance) = mock_contract!(mock, ERC20); 87 | /// # } 88 | /// ``` 89 | /// 90 | /// [Mock]: crate::Mock 91 | #[macro_export] 92 | macro_rules! mock_contract { 93 | ($mock:ident, $contract:ident) => {{ 94 | let mock = $mock; 95 | let contract = mock.deploy($contract::raw_contract().abi.clone()); 96 | let instance = $contract::at(&contract.web3(), contract.address()); 97 | 98 | (contract, instance) 99 | }}; 100 | } 101 | -------------------------------------------------------------------------------- /ethcontract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethcontract" 3 | version = "0.25.8" 4 | authors = ["Gnosis developers "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/gnosis/ethcontract-rs" 8 | homepage = "https://github.com/gnosis/ethcontract-rs" 9 | documentation = "https://docs.rs/ethcontract" 10 | keywords = ["web3", "ethereum", "contract", "async"] 11 | description = """ 12 | Runtime library and proc macro for interacting and generating type-safe bindings 13 | to Ethereum smart contracts. 14 | """ 15 | 16 | [lib] 17 | name = "ethcontract" 18 | 19 | [features] 20 | aws-kms = ["aws-config", "aws-sdk-kms", "rlp"] 21 | default = ["derive", "http-tls", "ws-tls-tokio", "derive-http"] 22 | derive = ["ethcontract-derive"] 23 | derive-http = ["ethcontract-derive/http"] 24 | http = ["web3/http"] 25 | http-native-tls = ["http", "web3/http-native-tls"] 26 | http-rustls-tls = ["http", "web3/http-rustls-tls"] 27 | http-tls = ["http", "web3/http-tls"] 28 | ipc-tokio = ["web3/ipc-tokio"] 29 | ws-async-std = ["web3/ws-async-std"] 30 | ws-tls-async-std = ["web3/ws-tls-async-std"] 31 | ws-tls-tokio = ["web3/ws-tls-tokio"] 32 | ws-tokio = ["web3/ws-tokio"] 33 | 34 | [dependencies] 35 | aws-config = { version = "0.55", optional = true } 36 | aws-sdk-kms = { version = "0.28", optional = true } 37 | arrayvec = "0.7" 38 | ethcontract-common = { version = "0.25.8", path = "../ethcontract-common" } 39 | ethcontract-derive = { version = "0.25.8", path = "../ethcontract-derive", optional = true, default-features = false } 40 | futures = "0.3" 41 | futures-timer = "3.0" 42 | hex = "0.4" 43 | jsonrpc-core = "18.0" 44 | lazy_static = "1.4" 45 | primitive-types = { version = "0.12", features = ["fp-conversion"] } 46 | rlp = { version = "0.5", default-features = false, optional = true } 47 | secp256k1 = { version = "0.27", features = ["recovery"] } 48 | serde = { version = "1.0", features = ["derive"] } 49 | serde_json = "1.0" 50 | thiserror = "1.0" 51 | uint = "0.9" 52 | web3 = { version = "0.19", default-features = false, features = ["signing"] } 53 | zeroize = "1.1" 54 | 55 | [dev-dependencies] 56 | hex-literal = "0.4" 57 | tokio = { version = "1.6", features = ["macros"] } 58 | -------------------------------------------------------------------------------- /ethcontract/src/batch.rs: -------------------------------------------------------------------------------- 1 | //! Module containing components to batch multiple contract calls 2 | //! into a single request to the Node. 3 | 4 | use futures::channel::oneshot::{channel, Sender}; 5 | use web3::{ 6 | error::{Error as Web3Error, TransportError}, 7 | helpers::{self}, 8 | types::{BlockId, BlockNumber, Bytes, CallRequest}, 9 | BatchTransport as Web3BatchTransport, 10 | }; 11 | 12 | /// Struct allowing to batch multiple calls into a single Node request 13 | pub struct CallBatch { 14 | inner: T, 15 | requests: Vec<(Request, CompletionHandler)>, 16 | } 17 | 18 | type Request = (CallRequest, Option); 19 | type CompletionHandler = Sender>; 20 | 21 | impl CallBatch { 22 | /// Create a new instance from a BatchTransport 23 | pub fn new(inner: T) -> Self { 24 | Self { 25 | inner, 26 | requests: Default::default(), 27 | } 28 | } 29 | 30 | /// Adds a call request to the current batch. The resulting future can only resolve after 31 | /// the batch has been resolved via `execute_all`. 32 | /// Explicitly returns a Future instead of being declared `async` so that we can split the 33 | /// logic into a synchronous and asynchronous section and don't want to capture `&mut self` 34 | /// in the future. 35 | /// Panics, if the batch is dropped before executing. 36 | pub fn push( 37 | &mut self, 38 | call: CallRequest, 39 | block: Option, 40 | ) -> impl std::future::Future> { 41 | let (tx, rx) = channel(); 42 | self.requests.push(((call, block), tx)); 43 | async move { 44 | rx.await.unwrap_or_else(|_| { 45 | Err(Web3Error::Transport(TransportError::Message( 46 | "Batch has been dropped without executing".to_owned(), 47 | ))) 48 | }) 49 | } 50 | } 51 | 52 | /// Execute and resolve all enqueued CallRequests in a batched RPC call, `chunk_size` requests per roundtrip. 53 | /// Top level request failures will be forwarded to the individual requests. 54 | pub async fn execute_all(self, batch_size: usize) { 55 | let Self { inner, requests } = self; 56 | let mut iterator = requests.into_iter().peekable(); 57 | while iterator.peek().is_some() { 58 | let (requests, senders): (Vec<_>, Vec<_>) = iterator.by_ref().take(batch_size).unzip(); 59 | 60 | // Send requests in a single call 61 | let batch_result = inner 62 | .send_batch(requests.iter().map(|(request, block)| { 63 | let req = helpers::serialize(request); 64 | let block = 65 | helpers::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into())); 66 | let (id, request) = inner.prepare("eth_call", vec![req, block]); 67 | (id, request) 68 | })) 69 | .await; 70 | 71 | // Process results 72 | for (i, sender) in senders.into_iter().enumerate() { 73 | let _ = match &batch_result { 74 | Ok(results) => sender.send( 75 | results 76 | .get(i) 77 | .unwrap_or(&Err(Web3Error::Decoder( 78 | "Batch result did not contain enough responses".to_owned(), 79 | ))) 80 | .clone() 81 | .and_then(helpers::decode), 82 | ), 83 | Err(err) => sender.send(Err(Web3Error::Transport(TransportError::Message( 84 | format!("Batch failed with: {}", err), 85 | )))), 86 | }; 87 | } 88 | } 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use futures::future::join_all; 95 | use serde_json::json; 96 | 97 | use super::*; 98 | use crate::test::prelude::FutureTestExt; 99 | use crate::test::transport::TestTransport; 100 | 101 | #[test] 102 | fn batches_calls() { 103 | let mut transport = TestTransport::new(); 104 | transport.add_response(json!([json!("0x01"), json!("0x02")])); 105 | 106 | let mut batch = CallBatch::new(transport); 107 | 108 | let results = vec![ 109 | batch.push(CallRequest::default(), None), 110 | batch.push(CallRequest::default(), None), 111 | ]; 112 | 113 | batch.execute_all(usize::MAX).immediate(); 114 | 115 | let results = join_all(results).immediate(); 116 | assert_eq!(results[0].clone().unwrap().0, vec![1u8]); 117 | assert_eq!(results[1].clone().unwrap().0, vec![2u8]); 118 | } 119 | 120 | #[test] 121 | fn resolves_calls_to_error_if_dropped() { 122 | let future = { 123 | let transport = TestTransport::new(); 124 | let mut batch = CallBatch::new(transport); 125 | batch.push(CallRequest::default(), None) 126 | }; 127 | 128 | assert!(matches!( 129 | future.immediate().unwrap_err(), 130 | Web3Error::Transport(_) 131 | )); 132 | } 133 | 134 | #[test] 135 | fn fails_all_calls_if_batch_fails() { 136 | let transport = TestTransport::new(); 137 | let mut batch = CallBatch::new(transport); 138 | let call = batch.push(CallRequest::default(), None); 139 | 140 | batch.execute_all(usize::MAX).immediate(); 141 | match call.immediate().unwrap_err() { 142 | Web3Error::Transport(TransportError::Message(reason)) => { 143 | assert!(reason.starts_with("Batch failed with:")) 144 | } 145 | _ => panic!("Wrong Error type"), 146 | }; 147 | } 148 | 149 | #[test] 150 | fn splits_batch_into_multiple_calls() { 151 | let mut transport = TestTransport::new(); 152 | transport.add_response(json!([json!("0x01"), json!("0x02")])); 153 | transport.add_response(json!([json!("0x03")])); 154 | 155 | let mut batch = CallBatch::new(transport); 156 | 157 | let results = vec![ 158 | batch.push(CallRequest::default(), None), 159 | batch.push(CallRequest::default(), None), 160 | batch.push(CallRequest::default(), None), 161 | ]; 162 | 163 | batch.execute_all(2).immediate(); 164 | 165 | let results = join_all(results).immediate(); 166 | assert_eq!(results[0].clone().unwrap().0, vec![1u8]); 167 | assert_eq!(results[1].clone().unwrap().0, vec![2u8]); 168 | assert_eq!(results[2].clone().unwrap().0, vec![3u8]); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /ethcontract/src/errors/ganache.rs: -------------------------------------------------------------------------------- 1 | //! This module implements Ganache specific error decoding in order to try and 2 | //! provide more accurate errors from Ganache nodes. 3 | 4 | use crate::errors::ExecutionError; 5 | use jsonrpc_core::Error as JsonrpcError; 6 | use web3::types::H256; 7 | 8 | /// Tries to get a more accurate error from a generic Ganache JSON RPC error. 9 | /// Returns `None` when a more accurate error cannot be determined. 10 | pub fn get_encoded_error(err: &JsonrpcError) -> Option { 11 | match get_error_param(err, "error") { 12 | Some("revert") => { 13 | let reason = get_error_param(err, "reason").map(|reason| reason.to_owned()); 14 | Some(ExecutionError::Revert(reason)) 15 | } 16 | Some("invalid opcode") => Some(ExecutionError::InvalidOpcode), 17 | _ => None, 18 | } 19 | } 20 | 21 | /// Gets an error parameters from a Ganache JSON RPC error. 22 | /// 23 | /// these parameters are the fields inside the transaction object (by tx hash) 24 | /// inside the error data object. Note that we don't need to know the fake tx 25 | /// hash for getting the error params as there should only be one. 26 | fn get_error_param<'a>(err: &'a JsonrpcError, name: &str) -> Option<&'a str> { 27 | fn is_hash_str(s: &str) -> bool { 28 | s.len() == 66 && s[2..].parse::().is_ok() 29 | } 30 | 31 | err.data 32 | .as_ref()? 33 | .as_object()? 34 | .iter() 35 | .find_map(|(k, v)| if is_hash_str(k) { Some(v) } else { None })? 36 | .get(name)? 37 | .as_str() 38 | } 39 | 40 | #[cfg(test)] 41 | pub use tests::*; 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | use crate::errors::revert; 47 | use crate::test::prelude::*; 48 | use jsonrpc_core::ErrorCode; 49 | use std::borrow::Cow; 50 | 51 | pub fn rpc_error(error: &str, reason: Option<&str>) -> JsonrpcError { 52 | let return_data: Cow = if let Some(reason) = reason { 53 | revert::encode_reason_hex(reason).into() 54 | } else { 55 | "0x".into() 56 | }; 57 | 58 | JsonrpcError { 59 | code: ErrorCode::from(-32000), 60 | message: "error".to_owned(), 61 | data: Some(json!({ 62 | "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f": { 63 | "error": error, 64 | "program_counter": 42, 65 | "return": return_data, 66 | "reason": reason, 67 | }, 68 | "stack": "RuntimeError: VM Exception while processing transaction ...", 69 | "name": "RuntimeError", 70 | })), 71 | } 72 | } 73 | 74 | #[test] 75 | fn execution_error_from_revert_with_message() { 76 | let jsonrpc_err = rpc_error("revert", Some("message")); 77 | let err = get_encoded_error(&jsonrpc_err); 78 | 79 | assert!( 80 | matches!( 81 | &err, 82 | Some(ExecutionError::Revert(Some(reason))) if reason == "message" 83 | ), 84 | "bad error conversion {:?}", 85 | err 86 | ); 87 | } 88 | 89 | #[test] 90 | fn execution_error_from_revert() { 91 | let jsonrpc_err = rpc_error("revert", None); 92 | let err = get_encoded_error(&jsonrpc_err); 93 | 94 | assert!( 95 | matches!(err, Some(ExecutionError::Revert(None))), 96 | "bad error conversion {:?}", 97 | err 98 | ); 99 | } 100 | 101 | #[test] 102 | fn execution_error_from_invalid_opcode() { 103 | let jsonrpc_err = rpc_error("invalid opcode", None); 104 | let err = get_encoded_error(&jsonrpc_err); 105 | 106 | assert!( 107 | matches!(err, Some(ExecutionError::InvalidOpcode)), 108 | "bad error conversion {:?}", 109 | err 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ethcontract/src/errors/geth.rs: -------------------------------------------------------------------------------- 1 | //! This module implements Geth specific error decoding in order to try and 2 | //! provide more accurate errors from Geth nodes. 3 | 4 | use crate::errors::ExecutionError; 5 | use jsonrpc_core::Error as JsonrpcError; 6 | 7 | const REVERTED: &str = "execution reverted"; 8 | const INVALID_OPCODE: &str = "invalid opcode"; 9 | 10 | /// Tries to get a more accurate error from a generic Geth JSON RPC error. 11 | /// Returns `None` when a more accurate error cannot be determined. 12 | pub fn get_encoded_error(err: &JsonrpcError) -> Option { 13 | if let Some(str) = err.message.strip_prefix(REVERTED) { 14 | let reason = str.strip_prefix(": ").map(ToString::to_string); 15 | Some(ExecutionError::Revert(reason)) 16 | } else if err.message.strip_prefix(INVALID_OPCODE).is_some() { 17 | Some(ExecutionError::InvalidOpcode) 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | 27 | #[test] 28 | fn revert_without_reason() { 29 | let error = JsonrpcError { 30 | code: 3.into(), 31 | message: REVERTED.to_string(), 32 | data: None, 33 | }; 34 | let result = get_encoded_error(&error); 35 | assert!(matches!(result, Some(ExecutionError::Revert(None)))); 36 | } 37 | 38 | #[test] 39 | fn revert_with_reason() { 40 | let reason = "SafeMath: subtraction overflow"; 41 | let error = JsonrpcError { 42 | code: 3.into(), 43 | message: format!("{}: {}", REVERTED, reason), 44 | data: None, 45 | }; 46 | let result = get_encoded_error(&error); 47 | assert!(matches!(result, Some(ExecutionError::Revert(Some(reason_))) if reason_ == reason)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ethcontract/src/errors/hardhat.rs: -------------------------------------------------------------------------------- 1 | //! This module implements Hardhat specific error decoding in order to try and 2 | //! provide more accurate errors from Hardhat nodes. 3 | //! 4 | //! Error messages can be found here: 5 | //! 6 | 7 | use crate::errors::ExecutionError; 8 | use jsonrpc_core::Error as JsonrpcError; 9 | 10 | /// Tries to get a more accurate error from a generic Hardhat JSON RPC error. 11 | /// Returns `None` when a more accurate error cannot be determined. 12 | pub fn get_encoded_error(err: &JsonrpcError) -> Option { 13 | if err.message == "VM Exception while processing transaction: invalid opcode" { 14 | return Some(ExecutionError::InvalidOpcode); 15 | } 16 | 17 | if let Some(reason) = err 18 | .message 19 | .strip_prefix( 20 | "Error: VM Exception while processing transaction: reverted with reason string '", 21 | ) 22 | .and_then(|rest| rest.strip_suffix('\'')) 23 | { 24 | return Some(ExecutionError::Revert(Some(reason.to_owned()))); 25 | } 26 | 27 | for needle in ["VM Exception", "Transaction reverted"] { 28 | if err.message.contains(needle) { 29 | return Some(ExecutionError::Revert(None)); 30 | } 31 | } 32 | 33 | None 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | use crate::test::prelude::*; 40 | use jsonrpc_core::ErrorCode; 41 | 42 | #[test] 43 | fn execution_error_from_revert_with_message() { 44 | let jsonrpc_err = JsonrpcError { 45 | code: ErrorCode::InternalError, 46 | message: "Error: VM Exception while processing transaction: \ 47 | reverted with reason string 'GS020'" 48 | .to_owned(), 49 | data: Some(json!({ 50 | "data": "0x08c379a0\ 51 | 0000000000000000000000000000000000000000000000000000000000000020\ 52 | 0000000000000000000000000000000000000000000000000000000000000005\ 53 | 4753303230000000000000000000000000000000000000000000000000000000", 54 | "message": "Error: VM Exception while processing transaction: \ 55 | reverted with reason string 'GS020'" 56 | })), 57 | }; 58 | let err = get_encoded_error(&jsonrpc_err); 59 | 60 | assert!( 61 | matches!( 62 | &err, 63 | Some(ExecutionError::Revert(Some(reason))) if reason == "GS020" 64 | ), 65 | "bad error conversion {err:?}", 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ethcontract/src/errors/nethermind.rs: -------------------------------------------------------------------------------- 1 | //! This module implements Nethermind specific error decoding in order to try and 2 | //! provide more accurate errors from Nethermind nodes. 3 | 4 | use crate::errors::{revert, ExecutionError}; 5 | use jsonrpc_core::Error as JsonrpcError; 6 | 7 | /// Revert error discriminant. 8 | const REVERTED: &str = "Reverted 0x"; 9 | /// Invalid op-code error discriminant. 10 | const INVALID: &str = "Bad instruction"; 11 | /// Error messages for VM execution errors. 12 | const MESSAGES: &[&str] = &["VM execution error", "VM execution error."]; 13 | 14 | /// Tries to get a more accurate error from a generic Nethermind JSON RPC error. 15 | /// Returns `None` when a more accurate error cannot be determined. 16 | pub fn get_encoded_error(err: &JsonrpcError) -> Option { 17 | let message = get_error_message(err); 18 | if let Some(hex) = message.strip_prefix(REVERTED) { 19 | if hex.is_empty() { 20 | return Some(ExecutionError::Revert(None)); 21 | } else { 22 | match hex::decode(hex) 23 | .ok() 24 | .and_then(|bytes| revert::decode_reason(&bytes)) 25 | { 26 | Some(reason) => return Some(ExecutionError::Revert(Some(reason))), 27 | None => return Some(ExecutionError::Revert(None)), 28 | } 29 | } 30 | } else if message.starts_with(INVALID) { 31 | return Some(ExecutionError::InvalidOpcode); 32 | } 33 | 34 | if MESSAGES.contains(&&*err.message) { 35 | return Some(ExecutionError::Revert(None)); 36 | } 37 | 38 | None 39 | } 40 | 41 | /// Returns the error message from the JSON RPC error data. 42 | fn get_error_message(err: &JsonrpcError) -> &str { 43 | err.data 44 | .as_ref() 45 | .and_then(|data| data.as_str()) 46 | .unwrap_or(&err.message) 47 | } 48 | 49 | #[allow(unused)] 50 | #[cfg(test)] 51 | pub use tests::*; 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | use crate::test::prelude::*; 57 | use jsonrpc_core::ErrorCode; 58 | 59 | pub fn rpc_errors(data: &str) -> Vec { 60 | // Nethermind has two flavours of revert errors: 61 | // ``` 62 | // {"jsonrpc":"2.0","error":{"code":-32015,"message":"VM execution error.","data":"Reverted 0x..."},"id":0} 63 | // {"jsonrpc":"2.0","error":{"code":-32015,"message":"Reverted 0x..."},"id":1} 64 | // ``` 65 | vec![ 66 | JsonrpcError { 67 | code: ErrorCode::from(-32015), 68 | message: "VM execution error".to_owned(), 69 | data: Some(json!(data)), 70 | }, 71 | JsonrpcError { 72 | code: ErrorCode::from(-32015), 73 | message: data.to_owned(), 74 | data: None, 75 | }, 76 | ] 77 | } 78 | 79 | #[test] 80 | fn execution_error_from_revert_with_message() { 81 | for jsonrpc_err in rpc_errors(&format!( 82 | "Reverted {}", 83 | revert::encode_reason_hex("message") 84 | )) { 85 | let err = get_encoded_error(&jsonrpc_err); 86 | 87 | assert!( 88 | matches!( 89 | &err, 90 | Some(ExecutionError::Revert(Some(reason))) if reason == "message" 91 | ), 92 | "bad error conversion {:?}", 93 | err 94 | ); 95 | } 96 | } 97 | 98 | #[test] 99 | fn execution_error_from_revert() { 100 | for jsonrpc_err in rpc_errors("Reverted 0x") { 101 | let err = get_encoded_error(&jsonrpc_err); 102 | 103 | assert!( 104 | matches!(err, Some(ExecutionError::Revert(None))), 105 | "bad error conversion {:?}", 106 | err 107 | ); 108 | } 109 | } 110 | 111 | #[test] 112 | fn execution_error_from_revert_failed_decode() { 113 | for jsonrpc_err in rpc_errors("Reverted 0x01020304") { 114 | let err = get_encoded_error(&jsonrpc_err); 115 | 116 | assert!( 117 | matches!(err, Some(ExecutionError::Revert(None))), 118 | "bad error conversion {:?}", 119 | err 120 | ); 121 | } 122 | } 123 | 124 | #[test] 125 | fn execution_error_from_invalid_opcode() { 126 | for jsonrpc_err in rpc_errors("Bad instruction fd") { 127 | let err = get_encoded_error(&jsonrpc_err); 128 | 129 | assert!( 130 | matches!(err, Some(ExecutionError::InvalidOpcode)), 131 | "bad error conversion {:?}", 132 | err 133 | ); 134 | } 135 | } 136 | 137 | #[test] 138 | fn execution_error_from_message() { 139 | let jsonrpc_err = JsonrpcError { 140 | code: ErrorCode::from(-32015), 141 | message: "VM execution error.".to_owned(), 142 | data: Some(json!("revert")), 143 | }; 144 | let err = get_encoded_error(&jsonrpc_err); 145 | 146 | assert!( 147 | matches!(err, Some(ExecutionError::Revert(None))), 148 | "bad error conversion {:?}", 149 | err 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ethcontract/src/errors/parity.rs: -------------------------------------------------------------------------------- 1 | //! This module implements Parity specific error decoding in order to try and 2 | //! provide more accurate errors from Parity nodes. 3 | 4 | use crate::errors::{revert, ExecutionError}; 5 | use jsonrpc_core::Error as JsonrpcError; 6 | 7 | /// Revert error discriminant. 8 | const REVERTED: &str = "Reverted 0x"; 9 | /// Invalid op-code error discriminant. 10 | const INVALID: &str = "Bad instruction"; 11 | 12 | /// Tries to get a more accurate error from a generic Parity JSON RPC error. 13 | /// Returns `None` when a more accurate error cannot be determined. 14 | pub fn get_encoded_error(err: &JsonrpcError) -> Option { 15 | let message = get_error_message(err)?; 16 | if let Some(hex) = message.strip_prefix(REVERTED) { 17 | if hex.is_empty() { 18 | return Some(ExecutionError::Revert(None)); 19 | } else { 20 | let bytes = hex::decode(hex).ok()?; 21 | let reason = revert::decode_reason(&bytes)?; 22 | return Some(ExecutionError::Revert(Some(reason))); 23 | } 24 | } else if message.starts_with(INVALID) { 25 | return Some(ExecutionError::InvalidOpcode); 26 | } 27 | 28 | None 29 | } 30 | 31 | /// Returns the error message from the JSON RPC error data. 32 | fn get_error_message(err: &JsonrpcError) -> Option<&'_ str> { 33 | err.data.as_ref().and_then(|data| data.as_str()) 34 | } 35 | 36 | #[cfg(test)] 37 | pub use tests::*; 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | use crate::test::prelude::*; 43 | use jsonrpc_core::ErrorCode; 44 | 45 | pub fn rpc_error(data: &str) -> JsonrpcError { 46 | JsonrpcError { 47 | code: ErrorCode::from(-32015), 48 | message: "vm execution error".to_owned(), 49 | data: Some(json!(data)), 50 | } 51 | } 52 | 53 | #[test] 54 | fn execution_error_from_revert_with_message() { 55 | let jsonrpc_err = rpc_error(&format!( 56 | "Reverted {}", 57 | revert::encode_reason_hex("message") 58 | )); 59 | let err = get_encoded_error(&jsonrpc_err); 60 | 61 | assert!( 62 | matches!( 63 | &err, 64 | Some(ExecutionError::Revert(Some(reason))) if reason == "message" 65 | ), 66 | "bad error conversion {:?}", 67 | err 68 | ); 69 | } 70 | 71 | #[test] 72 | fn execution_error_from_revert() { 73 | let jsonrpc_err = rpc_error("Reverted 0x"); 74 | let err = get_encoded_error(&jsonrpc_err); 75 | 76 | assert!( 77 | matches!(err, Some(ExecutionError::Revert(None))), 78 | "bad error conversion {:?}", 79 | err 80 | ); 81 | } 82 | 83 | #[test] 84 | fn execution_error_from_invalid_opcode() { 85 | let jsonrpc_err = rpc_error("Bad instruction fd"); 86 | let err = get_encoded_error(&jsonrpc_err); 87 | 88 | assert!( 89 | matches!(err, Some(ExecutionError::InvalidOpcode)), 90 | "bad error conversion {:?}", 91 | err 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ethcontract/src/errors/revert.rs: -------------------------------------------------------------------------------- 1 | //! Module implements decoding ABI encoded revert reasons. 2 | 3 | use ethcontract_common::abi::{self, ParamType}; 4 | use ethcontract_common::hash::{self, H32}; 5 | use lazy_static::lazy_static; 6 | 7 | lazy_static! { 8 | /// The ABI function selector for identifying encoded revert reasons. 9 | static ref ERROR_SELECTOR: H32 = hash::function_selector("Error(string)"); 10 | } 11 | 12 | /// Decodes an ABI encoded revert reason. Returns `Some(reason)` when the ABI 13 | /// encoded bytes represent a revert reason and `None` otherwise. 14 | /// 15 | /// These reasons are prefixed by a 4-byte error followed by an ABI encoded 16 | /// string. 17 | pub fn decode_reason(bytes: &[u8]) -> Option { 18 | if (bytes.len() + 28) % 32 != 0 || bytes[0..4] != ERROR_SELECTOR[..] { 19 | // check to make sure that the length is of the form `4 + (n * 32)` 20 | // bytes and it starts with `keccak256("Error(string)")` which means 21 | // it is an encoded revert reason from Geth nodes. 22 | return None; 23 | } 24 | 25 | let reason = abi::decode(&[ParamType::String], &bytes[4..]) 26 | .ok()? 27 | .pop() 28 | .expect("decoded single parameter will yield single token") 29 | .to_string(); 30 | 31 | Some(reason) 32 | } 33 | 34 | #[cfg(test)] 35 | pub use tests::*; 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use ethcontract_common::abi::{Function, Param, Token}; 41 | 42 | pub fn encode_reason(reason: &str) -> Vec { 43 | #[allow(deprecated)] 44 | let revert = Function { 45 | name: "Error".into(), 46 | inputs: vec![Param { 47 | name: "".into(), 48 | kind: ParamType::String, 49 | internal_type: None, 50 | }], 51 | outputs: Vec::new(), 52 | constant: None, 53 | state_mutability: Default::default(), 54 | }; 55 | revert 56 | .encode_input(&[Token::String(reason.into())]) 57 | .expect("error encoding revert reason") 58 | } 59 | 60 | pub fn encode_reason_hex(reason: &str) -> String { 61 | let encoded = encode_reason(reason); 62 | format!("0x{}", hex::encode(encoded)) 63 | } 64 | 65 | #[test] 66 | fn decode_revert_reason() { 67 | let reason = "ethcontract rocks!"; 68 | let encoded = encode_reason(reason); 69 | 70 | assert_eq!(decode_reason(&encoded).as_deref(), Some(reason)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ethcontract/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs, unsafe_code)] 2 | 3 | //! Generate bindings for Ethereum smart contracts. Internally, the generated 4 | //! types use `web3` crate to interact with the Ethereum network and uses a 5 | //! custom [`Instance`](ethcontract::contract::Instance) runtime that can be 6 | //! used directly without code generation. 7 | //! 8 | //! This crate is using `std::future::Future` for futures by wrapping `web3` 9 | //! `futures` 0.1 with `futures` 0.3 compatibility layer. This means that this 10 | //! crate is ready for `async`/`await`! 11 | //! 12 | //! Here is an example of interacing with a smart contract `MyContract`. The 13 | //! builder pattern is used for configuring transactions. 14 | //! 15 | //! ```text 16 | //! pragma solidity ^0.6.0; 17 | //! 18 | //! contract MyContract { 19 | //! function my_view_function(uint64 some_val) public view returns (string) { 20 | //! // ... 21 | //! } 22 | //! 23 | //! function my_function(bool some_bool, string some_str) public returns (uint256) { 24 | //! // ... 25 | //! } 26 | //! 27 | //! function my_other_function() public { 28 | //! // ... 29 | //! } 30 | //! } 31 | //! ``` 32 | //! 33 | //! Once this contract is built and deployed with truffle the following example 34 | //! demonstrates how to interact with it from Rust. 35 | //! 36 | //! ```ignore 37 | //! use ethcontract::transaction::Account; 38 | //! use std::time::Duration; 39 | //! use web3::api::Web3; 40 | //! use web3::types::*; 41 | //! 42 | //! // this proc macro generates a `MyContract` type with type-safe bindings to 43 | //! // contract functions 44 | //! ethcontract::contract!("path/to/MyContract.json"); 45 | //! 46 | //! // create a web3 instance as usual 47 | //! let transport = ...; 48 | //! let web3 = Web3::new(transport); 49 | //! 50 | //! // now create an instance of an interface to the contract 51 | //! let instance = MyContract::deployed(web3).await?; 52 | //! 53 | //! let addr: Address = "0x000102030405060708090a0b0c0d0e0f10111213".parse()?; 54 | //! let some_uint: U256 = U256::from_dec_str("1000000000000000000")?; 55 | //! let some_bool = true; 56 | //! let some_val = u64; 57 | //! 58 | //! // call instance view functions with type-safe bindings! will only compile 59 | //! // if contract function accepts a single `u64` value parameter and returns 60 | //! // a concrete type based on the contract function's return type 61 | //! let value = instance 62 | //! .my_view_function(some_val) 63 | //! .from(addr) 64 | //! .execute() 65 | //! .await?; 66 | //! 67 | //! // contract functions also have type-safe bindings and return the tx hash 68 | //! // of the submitted transaction; allows for multiple ways of signing txs 69 | //! let tx = instance 70 | //! .my_function(some_bool, value) 71 | //! .from(Account::Locked(addr, "password".into(), None)) 72 | //! .value(some_uint) 73 | //! .gas_price(1_000_000.into()) 74 | //! .execute() 75 | //! .await?; 76 | //! 77 | //! // wait for confirmations when needed 78 | //! let receipt = instance 79 | //! .my_important_function() 80 | //! .poll_interval(Duration::from_secs(5)) 81 | //! .confirmations(2) 82 | //! .execute_confirm() 83 | //! .await?; 84 | //! ``` 85 | //! 86 | //! See [`contract!`](ethcontract::contract) proc macro documentation for more 87 | //! information on usage and parameters as well on how to use contract ABI 88 | //! directly from Etherscan. 89 | 90 | #[cfg(test)] 91 | #[allow(missing_docs)] 92 | #[macro_use] 93 | #[path = "test/macros.rs"] 94 | mod test_macros; 95 | 96 | pub mod batch; 97 | pub mod contract; 98 | pub mod errors; 99 | mod int; 100 | pub mod log; 101 | pub mod secret; 102 | pub mod tokens; 103 | pub mod transaction; 104 | pub mod transport; 105 | 106 | pub use crate::contract::Instance; 107 | pub use crate::prelude::*; 108 | #[cfg(feature = "aws-kms")] 109 | pub use aws_config; 110 | pub use ethcontract_common as common; 111 | pub use ethcontract_common::contract::Contract; 112 | #[cfg(feature = "derive")] 113 | pub use ethcontract_derive::contract; 114 | pub use futures; 115 | pub use jsonrpc_core as jsonrpc; 116 | pub use serde_json as json; 117 | pub use web3; 118 | 119 | pub mod prelude { 120 | //! A prelude module for importing commonly used types when interacting with 121 | //! generated contracts. 122 | 123 | pub use crate::contract::{Event, EventMetadata, EventStatus, RawLog, StreamEvent, Topic}; 124 | pub use crate::int::I256; 125 | pub use crate::secret::{Password, PrivateKey}; 126 | pub use crate::tokens::Bytes; 127 | pub use crate::transaction::{Account, GasPrice}; 128 | pub use ethcontract_common::TransactionHash; 129 | pub use web3::api::Web3; 130 | #[cfg(feature = "http")] 131 | pub use web3::transports::Http; 132 | pub use web3::types::{Address, BlockId, BlockNumber, TransactionCondition, H160, H256, U256}; 133 | } 134 | 135 | pub mod dyns { 136 | //! Type aliases to various runtime types that use an underlying 137 | //! `DynTransport`. These types are used extensively throughout the 138 | //! generated code. 139 | 140 | use crate::contract::{ 141 | AllEventsBuilder, DeployBuilder, EventBuilder, Instance, MethodBuilder, ViewMethodBuilder, 142 | }; 143 | pub use crate::transport::DynTransport; 144 | use web3::api::Web3; 145 | 146 | /// Type alias for a `Web3` with an underlying `DynTransport`. 147 | pub type DynWeb3 = Web3; 148 | 149 | /// Type alias for an `Instance` with an underlying `DynTransport`. 150 | pub type DynInstance = Instance; 151 | 152 | /// Type alias for a `DeployBuilder` with an underlying `DynTransport`. 153 | pub type DynDeployBuilder = DeployBuilder; 154 | 155 | /// Type alias for a `MethodBuilder` with an underlying `DynTransport`. 156 | pub type DynMethodBuilder = MethodBuilder; 157 | 158 | /// Type alias for a `ViewMethodBuilder` with an underlying `DynTransport`. 159 | pub type DynViewMethodBuilder = ViewMethodBuilder; 160 | 161 | /// Type alias for a `EventBuilder` with an underlying `DynTransport`. 162 | pub type DynEventBuilder = EventBuilder; 163 | 164 | /// Type alias for a `LogStream` with an underlying `DynTransport`. 165 | pub type DynAllEventsBuilder = AllEventsBuilder; 166 | } 167 | 168 | #[doc(hidden)] 169 | pub mod private { 170 | // Private definitions that are needed by the generated contract code or 171 | // but do not appear in public interfaces. No documentation is generated 172 | // for these definitions. 173 | 174 | pub use lazy_static::lazy_static; 175 | } 176 | 177 | #[cfg(test)] 178 | #[allow(missing_docs)] 179 | mod test { 180 | pub mod prelude; 181 | pub mod transport; 182 | } 183 | -------------------------------------------------------------------------------- /ethcontract/src/test/macros.rs: -------------------------------------------------------------------------------- 1 | /// Parse a string address of the form "0x...". 2 | /// 3 | /// # Panics 4 | /// 5 | /// If the address is invalid. 6 | macro_rules! addr { 7 | ($value:expr) => { 8 | $value[2..] 9 | .parse::() 10 | .expect("invalid address") 11 | }; 12 | } 13 | 14 | /// Parse a string uint256 of the form "0x...". 15 | /// 16 | /// # Panics 17 | /// 18 | /// If the uint is invalid. 19 | macro_rules! uint { 20 | ($value:expr) => { 21 | $value[2..] 22 | .parse::() 23 | .expect("invalid address") 24 | }; 25 | } 26 | 27 | /// Parse a string 256-bit hash of the form "0x...". 28 | /// 29 | /// # Panics 30 | /// 31 | /// If the hash is invalid. 32 | macro_rules! hash { 33 | ($value:expr) => { 34 | $value[2..] 35 | .parse::() 36 | .expect("invalid hash") 37 | }; 38 | } 39 | 40 | /// Parse hex encoded string of the form "0x...". 41 | /// 42 | /// # Panics 43 | /// 44 | /// If the hex string is invalid. 45 | macro_rules! bytes { 46 | ($value:expr) => { 47 | serde_json::from_str::(&format!("\"{}\"", $value)) 48 | .expect("invalid bytes") 49 | }; 50 | } 51 | 52 | /// Parse a string 256-bit private key of the form "0x...". 53 | /// 54 | /// # Panics 55 | /// 56 | /// If the private key is invalid. 57 | macro_rules! key { 58 | ($value:expr) => { 59 | $crate::secret::PrivateKey::from_slice(&hash!($value)[..]).expect("invalid key") 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /ethcontract/src/test/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Prelude module with common types used for unit tests. 2 | 3 | pub use crate::test::transport::TestTransport; 4 | use futures::future::FutureExt; 5 | pub use serde_json::json; 6 | use std::future::Future; 7 | #[allow(unused)] 8 | pub use web3::api::Web3; 9 | 10 | /// An extension future to wait for a future. 11 | pub trait FutureTestExt: Future { 12 | /// Block thread on a future completing. 13 | fn wait(self) -> Self::Output; 14 | /// Assert that future is ready immediately and return the output. 15 | fn immediate(self) -> Self::Output; 16 | } 17 | 18 | impl FutureTestExt for F { 19 | fn wait(self) -> Self::Output { 20 | futures::executor::block_on(self) 21 | } 22 | fn immediate(self) -> Self::Output { 23 | self.now_or_never() 24 | .expect("future did not resolve immediately") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ethcontract/src/test/transport.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of a transport for testing purposes. This is largely based on 2 | //! the `rust-web3` `TestTransport` type with some modifications. 3 | 4 | use jsonrpc_core::{Call, Value}; 5 | use std::collections::VecDeque; 6 | use std::sync::{Arc, Mutex}; 7 | use web3::futures::future::{self, Ready}; 8 | use web3::helpers; 9 | use web3::{error::Error, BatchTransport}; 10 | use web3::{RequestId, Transport}; 11 | 12 | /// Type alias for request method and value pairs 13 | type Requests = Vec<(String, Vec)>; 14 | 15 | #[derive(Debug, Default)] 16 | struct Inner { 17 | asserted: usize, 18 | requests: Requests, 19 | responses: VecDeque, 20 | } 21 | 22 | /// Test transport 23 | #[derive(Debug, Default, Clone)] 24 | pub struct TestTransport { 25 | inner: Arc>, 26 | } 27 | 28 | impl Transport for TestTransport { 29 | type Out = Ready>; 30 | 31 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { 32 | let request = helpers::build_request(1, method, params.clone()); 33 | let mut inner = self.inner.lock().unwrap(); 34 | inner.requests.push((method.into(), params)); 35 | (inner.requests.len(), request) 36 | } 37 | 38 | fn send(&self, id: RequestId, request: Call) -> Self::Out { 39 | let response = self.inner.lock().unwrap().responses.pop_front(); 40 | match response { 41 | Some(response) => future::ok(response), 42 | None => { 43 | println!("Unexpected request (id: {:?}): {:?}", id, request); 44 | future::err(Error::Unreachable) 45 | } 46 | } 47 | } 48 | } 49 | 50 | impl BatchTransport for TestTransport { 51 | type Batch = Ready>, Error>>; 52 | 53 | fn send_batch(&self, requests: T) -> Self::Batch 54 | where 55 | T: IntoIterator, 56 | { 57 | let mut requests: Vec<_> = requests.into_iter().collect(); 58 | 59 | // Only send the first request to receive a response for all requests in the batch 60 | let (id, call) = match requests.pop() { 61 | Some(request) => request, 62 | None => return future::err(Error::Unreachable), 63 | }; 64 | 65 | let responses = match self 66 | .send(id, call) 67 | .into_inner() 68 | .ok() 69 | .and_then(|value| value.as_array().cloned()) 70 | { 71 | Some(array) => array.into_iter(), 72 | None => { 73 | println!("Response should return a list of values"); 74 | return future::err(Error::Unreachable); 75 | } 76 | }; 77 | future::ok(responses.map(Ok).collect()) 78 | } 79 | } 80 | 81 | impl TestTransport { 82 | /// Create a new test transport instance. 83 | pub fn new() -> Self { 84 | Default::default() 85 | } 86 | 87 | /// Add a response to an eventual request. 88 | pub fn add_response(&mut self, value: Value) { 89 | let mut inner = self.inner.lock().unwrap(); 90 | inner.responses.push_back(value); 91 | } 92 | 93 | /// Assert that a request was made. 94 | pub fn assert_request(&mut self, method: &str, params: &[Value]) { 95 | let mut inner = self.inner.lock().unwrap(); 96 | let idx = inner.asserted; 97 | inner.asserted += 1; 98 | 99 | let (m, p) = inner.requests.get(idx).expect("Expected result.").clone(); 100 | assert_eq!(&m, method); 101 | assert_eq!(&p[..], params); 102 | } 103 | 104 | /// Assert that there are no more pending requests. 105 | pub fn assert_no_more_requests(&self) { 106 | let inner = self.inner.lock().unwrap(); 107 | assert_eq!( 108 | inner.asserted, 109 | inner.requests.len(), 110 | "Expected no more requests, got: {:?}", 111 | &inner.requests[inner.asserted..] 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ethcontract/src/transaction/gas_price.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of gas price estimation. 2 | 3 | use primitive_types::U256; 4 | use web3::types::U64; 5 | 6 | #[derive(Debug, Default, Eq, PartialEq)] 7 | /// Data related to gas price, prepared for populating the transaction object. 8 | pub struct ResolvedTransactionGasPrice { 9 | /// Legacy gas price, populated if transaction type is legacy 10 | pub gas_price: Option, 11 | /// Maximum gas price willing to pay for the transaction, populated if transaction type is eip1559 12 | pub max_fee_per_gas: Option, 13 | /// Priority fee used to incentivize miners to include the tx in case of network congestion. 14 | /// Populated if transaction type is eip1559 15 | pub max_priority_fee_per_gas: Option, 16 | /// Equal to None for legacy transaction, equal to 2 for eip1559 transaction 17 | pub transaction_type: Option, 18 | } 19 | 20 | /// The gas price setting to use. 21 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 22 | pub enum GasPrice { 23 | /// Legacy type of transactions, using single gas price value. Equivalent to sending 24 | /// eip1559 transaction with max_fee_per_gas = max_priority_fee_per_gas = gas_price 25 | Legacy(U256), 26 | 27 | /// Eip1559 type of transactions, using two values (max_fee_per_gas, max_priority_fee_per_gas) 28 | Eip1559 { 29 | /// Maximum gas price willing to pay for the transaction. 30 | max_fee_per_gas: U256, 31 | /// Priority fee used to incentivize miners to include the tx in case of network congestion. 32 | max_priority_fee_per_gas: U256, 33 | }, 34 | } 35 | 36 | impl GasPrice { 37 | /// Prepares the data for transaction. 38 | pub fn resolve_for_transaction(&self) -> ResolvedTransactionGasPrice { 39 | match self { 40 | GasPrice::Legacy(value) => ResolvedTransactionGasPrice { 41 | gas_price: Some(*value), 42 | ..Default::default() 43 | }, 44 | GasPrice::Eip1559 { 45 | max_fee_per_gas, 46 | max_priority_fee_per_gas, 47 | } => ResolvedTransactionGasPrice { 48 | max_fee_per_gas: Some(*max_fee_per_gas), 49 | max_priority_fee_per_gas: Some(*max_priority_fee_per_gas), 50 | transaction_type: Some(2.into()), 51 | ..Default::default() 52 | }, 53 | } 54 | } 55 | } 56 | 57 | impl From for GasPrice { 58 | fn from(value: U256) -> Self { 59 | GasPrice::Legacy(value) 60 | } 61 | } 62 | 63 | impl From for GasPrice { 64 | fn from(value: f64) -> Self { 65 | U256::from_f64_lossy(value).into() 66 | } 67 | } 68 | 69 | impl From<(U256, U256)> for GasPrice { 70 | fn from(value: (U256, U256)) -> Self { 71 | GasPrice::Eip1559 { 72 | max_fee_per_gas: value.0, 73 | max_priority_fee_per_gas: value.1, 74 | } 75 | } 76 | } 77 | 78 | impl From<(f64, f64)> for GasPrice { 79 | fn from(value: (f64, f64)) -> Self { 80 | (U256::from_f64_lossy(value.0), U256::from_f64_lossy(value.1)).into() 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | 88 | #[test] 89 | fn resolve_for_transaction_legacy() { 90 | //assert data for legacy type of transaction is prepared 91 | let resolved_gas_price = GasPrice::Legacy(100.into()).resolve_for_transaction(); 92 | assert_eq!(resolved_gas_price.gas_price, Some(100.into())); 93 | assert_eq!(resolved_gas_price.transaction_type, None); 94 | } 95 | 96 | #[test] 97 | fn resolve_for_transaction_eip1559() { 98 | //assert data for eip1559 type of transaction is prepared 99 | let resolved_gas_price = GasPrice::Eip1559 { 100 | max_fee_per_gas: 100.into(), 101 | max_priority_fee_per_gas: 50.into(), 102 | } 103 | .resolve_for_transaction(); 104 | assert_eq!(resolved_gas_price.max_fee_per_gas, Some(100.into())); 105 | assert_eq!(resolved_gas_price.max_priority_fee_per_gas, Some(50.into())); 106 | assert_eq!(resolved_gas_price.transaction_type, Some(2.into())); 107 | } 108 | 109 | #[test] 110 | fn gas_price_convertor_u256() { 111 | //assert that legacy type of transaction is built when single U256 value is provided 112 | let legacy_transaction_type: GasPrice = U256::from(100).into(); 113 | assert_eq!(legacy_transaction_type, GasPrice::Legacy(100.into())); 114 | 115 | //assert that legacy type of transaction is built when single f64 value is provided 116 | let legacy_transaction_type: GasPrice = 100.0.into(); 117 | assert_eq!(legacy_transaction_type, GasPrice::Legacy(100.into())); 118 | } 119 | 120 | #[test] 121 | fn gas_price_convertor_u256_u256() { 122 | //assert that EIP1559 type of transaction is built when double U256 value is provided 123 | let eip1559_transaction_type: GasPrice = (U256::from(100), U256::from(50)).into(); 124 | assert_eq!( 125 | eip1559_transaction_type, 126 | GasPrice::Eip1559 { 127 | max_fee_per_gas: 100.into(), 128 | max_priority_fee_per_gas: 50.into() 129 | } 130 | ); 131 | 132 | //assert that EIP1559 type of transaction is built when double f64 value is provided 133 | let eip1559_transaction_type: GasPrice = (100.0, 50.0).into(); 134 | assert_eq!( 135 | eip1559_transaction_type, 136 | GasPrice::Eip1559 { 137 | max_fee_per_gas: 100.into(), 138 | max_priority_fee_per_gas: 50.into() 139 | } 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ethcontract/src/transaction/send.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of a future for sending a transaction with optional 2 | //! confirmation. 3 | 4 | use crate::errors::ExecutionError; 5 | use crate::transaction::confirm; 6 | use crate::transaction::{ResolveCondition, Transaction, TransactionBuilder}; 7 | use web3::types::{TransactionReceipt, H256, U64}; 8 | use web3::Transport; 9 | 10 | impl TransactionBuilder { 11 | /// Sign (if required) and send the transaction. Returns the transaction 12 | /// hash that can be used to retrieve transaction information. 13 | pub async fn send(mut self) -> Result { 14 | let web3 = self.web3.clone(); 15 | let resolve = self.resolve.take().unwrap_or_default(); 16 | 17 | let tx = self.build().await?; 18 | let tx_hash = match tx { 19 | Transaction::Request(tx) => web3.eth().send_transaction(tx).await?, 20 | Transaction::Raw { bytes, hash } => { 21 | let node_hash = web3.eth().send_raw_transaction(bytes).await?; 22 | if node_hash != hash { 23 | return Err(ExecutionError::UnexpectedTransactionHash); 24 | } 25 | hash 26 | } 27 | }; 28 | 29 | let tx_receipt = match resolve { 30 | ResolveCondition::Pending => return Ok(TransactionResult::Hash(tx_hash)), 31 | ResolveCondition::Confirmed(params) => { 32 | confirm::wait_for_confirmation(&web3, tx_hash, params).await 33 | } 34 | }?; 35 | 36 | match tx_receipt.status { 37 | Some(U64([1])) => Ok(TransactionResult::Receipt(tx_receipt)), 38 | _ => Err(ExecutionError::Failure(Box::new(tx_receipt))), 39 | } 40 | } 41 | } 42 | 43 | /// Represents the result of a sent transaction that can either be a transaction 44 | /// hash, in the case the transaction was not confirmed, or a full transaction 45 | /// receipt if the `TransactionBuilder` was configured to wait for confirmation 46 | /// blocks. 47 | /// 48 | /// Note that the result will always be a `TransactionResult::Hash` if 49 | /// `Confirm::Skip` was used and `TransactionResult::Receipt` if 50 | /// `Confirm::Blocks` was used. 51 | #[derive(Clone, Debug)] 52 | #[allow(clippy::large_enum_variant)] 53 | pub enum TransactionResult { 54 | /// A transaction hash, this variant happens if and only if confirmation was 55 | /// skipped. 56 | Hash(H256), 57 | /// A transaction receipt, this variant happens if and only if the 58 | /// transaction was configured to wait for confirmations. 59 | Receipt(TransactionReceipt), 60 | } 61 | 62 | impl TransactionResult { 63 | /// Returns true if the `TransactionResult` is a `Hash` variant, i.e. it is 64 | /// only a hash and does not contain the transaction receipt. 65 | pub fn is_hash(&self) -> bool { 66 | matches!(self, TransactionResult::Hash(_)) 67 | } 68 | 69 | /// Get the transaction hash. 70 | pub fn hash(&self) -> H256 { 71 | match self { 72 | TransactionResult::Hash(hash) => *hash, 73 | TransactionResult::Receipt(tx) => tx.transaction_hash, 74 | } 75 | } 76 | 77 | /// Returns true if the `TransactionResult` is a `Receipt` variant, i.e. the 78 | /// transaction was confirmed and the full transaction receipt is available. 79 | pub fn is_receipt(&self) -> bool { 80 | self.as_receipt().is_some() 81 | } 82 | 83 | /// Extract a `TransactionReceipt` from the result. This will return `None` 84 | /// if the result is only a hash and the transaction receipt is not 85 | /// available. 86 | pub fn as_receipt(&self) -> Option<&TransactionReceipt> { 87 | match self { 88 | TransactionResult::Receipt(ref tx) => Some(tx), 89 | _ => None, 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | autobins = false 8 | 9 | [dev-dependencies] 10 | ethcontract = { path = "../ethcontract", features = ["aws-kms"] } 11 | futures = "0.3" 12 | tokio = { version = "1.6", features = ["macros"] } 13 | -------------------------------------------------------------------------------- /examples/documentation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples-documentation" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [dependencies] 9 | ethcontract = { path = "../../ethcontract", default-features = false, features = ["derive"] } 10 | -------------------------------------------------------------------------------- /examples/documentation/src/main.rs: -------------------------------------------------------------------------------- 1 | // This is a binary because when compiling as a library I would get compile errors about the 2 | // artifacts not being found. I suspect this is related to the working directory for tests or doc 3 | // tests being different. 4 | 5 | //! Samples of derived contracts for documentation purposes in order to 6 | //! illustrate what the generated API. 7 | 8 | ethcontract::contract!("examples/truffle/build/contracts/DocumentedContract.json",); 9 | ethcontract::contract!("examples/truffle/build/contracts/SimpleLibrary.json",); 10 | ethcontract::contract!("examples/truffle/build/contracts/LinkedContract.json",); 11 | ethcontract::contract!("examples/truffle/build/contracts/IERC20.json",); 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /examples/examples/abi.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | use std::any; 3 | 4 | ethcontract::contract!("examples/truffle/build/contracts/AbiTypes.json"); 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let http = Http::new("http://localhost:9545").expect("transport failure"); 9 | let web3 = Web3::new(http); 10 | 11 | let instance = AbiTypes::builder(&web3) 12 | .gas(4_712_388.into()) 13 | .deploy() 14 | .await 15 | .expect("contract deployment failure"); 16 | println!("Using contract at {:?}", instance.address()); 17 | 18 | calls(&instance).await; 19 | events(&instance).await; 20 | } 21 | 22 | async fn calls(instance: &AbiTypes) { 23 | macro_rules! debug_call { 24 | (instance. $call:ident ()) => {{ 25 | let value = instance 26 | .$call() 27 | .call() 28 | .await 29 | .expect(concat!(stringify!($call), " failed")); 30 | println!( 31 | "{}() -> {}\n ⏎ {:?}", 32 | stringify!($call), 33 | type_name_of(&value), 34 | value, 35 | ) 36 | }}; 37 | } 38 | 39 | debug_call!(instance.get_void()); 40 | debug_call!(instance.get_u8()); 41 | debug_call!(instance.get_u16()); 42 | debug_call!(instance.get_u32()); 43 | debug_call!(instance.get_u64()); 44 | debug_call!(instance.get_u128()); 45 | debug_call!(instance.get_u256()); 46 | 47 | debug_call!(instance.get_i8()); 48 | debug_call!(instance.get_i16()); 49 | debug_call!(instance.get_i32()); 50 | debug_call!(instance.get_i64()); 51 | debug_call!(instance.get_i128()); 52 | debug_call!(instance.get_i256()); 53 | 54 | debug_call!(instance.get_bool()); 55 | 56 | debug_call!(instance.get_bytes()); 57 | debug_call!(instance.get_fixed_bytes()); 58 | debug_call!(instance.get_address()); 59 | debug_call!(instance.get_string()); 60 | 61 | debug_call!(instance.get_array()); 62 | debug_call!(instance.get_fixed_array()); 63 | 64 | let value = (4, 2); 65 | let result = instance.abiv_2_struct(value).call().await.unwrap(); 66 | assert_eq!(result, value); 67 | 68 | let value = vec![(4, 2), (5, 3)]; 69 | let result = instance 70 | .abiv_2_array_of_struct(value.clone()) 71 | .call() 72 | .await 73 | .unwrap(); 74 | assert_eq!(result, value); 75 | 76 | let value = [vec![(4, 2)], vec![(5, 3), (6, 4)], vec![]]; 77 | let result = instance 78 | .abiv_2_array_of_array_of_struct(value.clone()) 79 | .call() 80 | .await 81 | .unwrap(); 82 | assert_eq!(result, value); 83 | 84 | let vec = vec![1u8, 2, 3]; 85 | let array = [1u8, 2, 3]; 86 | let result = instance 87 | .roundtrip_u8_array(vec.clone()) 88 | .call() 89 | .await 90 | .unwrap(); 91 | assert_eq!(result, vec); 92 | let result = instance 93 | .roundtrip_fixed_u8_array(array) 94 | .call() 95 | .await 96 | .unwrap(); 97 | assert_eq!(result, array); 98 | let result = instance 99 | .roundtrip_bytes(Bytes(vec.clone())) 100 | .call() 101 | .await 102 | .unwrap(); 103 | assert_eq!(result.0, vec); 104 | let result = instance 105 | .roundtrip_fixed_bytes(Bytes(array)) 106 | .call() 107 | .await 108 | .unwrap(); 109 | assert_eq!(result.0, array); 110 | } 111 | 112 | async fn events(instance: &AbiTypes) { 113 | macro_rules! debug_events { 114 | (instance.events(). $events:ident ()) => {{ 115 | let events = instance 116 | .events() 117 | .$events() 118 | .query() 119 | .await 120 | .expect(concat!(stringify!($events), " failed")); 121 | let event_data = events 122 | .into_iter() 123 | .map(|event| event.data) 124 | .collect::>(); 125 | println!("{}()\n ⏎ {:?}", stringify!($events), event_data); 126 | }}; 127 | } 128 | 129 | instance 130 | .emit_values() 131 | // NOTE: Gas estimation seems to not work for this call. 132 | .gas(4_712_388.into()) 133 | .send() 134 | .await 135 | .expect("failed to emit value events"); 136 | 137 | debug_events!(instance.events().value_uint()); 138 | debug_events!(instance.events().value_int()); 139 | debug_events!(instance.events().value_bool()); 140 | debug_events!(instance.events().value_bytes()); 141 | debug_events!(instance.events().value_array()); 142 | debug_events!(instance.events().value_indexed()); 143 | 144 | let all_events = instance 145 | .all_events() 146 | .query() 147 | .await 148 | .expect("failed to retrieve all events"); 149 | for event in all_events { 150 | if let abi_types::Event::Values(data) = event.data { 151 | println!("anonymous event\n ⏎ {:?}", data); 152 | } 153 | } 154 | } 155 | 156 | fn type_name_of(_: &T) -> &'static str { 157 | any::type_name::() 158 | } 159 | -------------------------------------------------------------------------------- /examples/examples/async.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | use ethcontract::web3::types::TransactionRequest; 3 | 4 | ethcontract::contract!("examples/truffle/build/contracts/RustCoin.json"); 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let http = Http::new("http://localhost:9545").expect("transport failed"); 9 | let web3 = Web3::new(http); 10 | 11 | let accounts = web3.eth().accounts().await.expect("get accounts failed"); 12 | 13 | let instance = RustCoin::builder(&web3) 14 | .gas(4_712_388.into()) 15 | .deploy() 16 | .await 17 | .expect("deployment failed"); 18 | let name = instance.name().call().await.expect("get name failed"); 19 | println!("Deployed {} at {:?}", name, instance.address()); 20 | 21 | instance 22 | .transfer(accounts[1], 1_000_000.into()) 23 | .send() 24 | .await 25 | .expect("transfer 0->1 failed"); 26 | instance 27 | .transfer(accounts[2], 500_000.into()) 28 | .from(Account::Local(accounts[1], None)) 29 | .send() 30 | .await 31 | .expect("transfer 1->2 failed"); 32 | 33 | print_balance_of(&instance, accounts[1]).await; 34 | print_balance_of(&instance, accounts[2]).await; 35 | 36 | let key: PrivateKey = "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" 37 | .parse() 38 | .expect("parse key"); 39 | let x = key.public_address(); 40 | println!("Created new account {:?}", x); 41 | 42 | // send some eth to x so that it can do transactions 43 | web3.eth() 44 | .send_transaction(TransactionRequest { 45 | from: accounts[0], 46 | to: Some(x), 47 | gas: None, 48 | gas_price: None, 49 | value: Some(1_000_000_000_000_000_000u64.into()), 50 | data: None, 51 | nonce: None, 52 | condition: None, 53 | transaction_type: None, 54 | access_list: None, 55 | max_fee_per_gas: None, 56 | max_priority_fee_per_gas: None, 57 | }) 58 | .await 59 | .expect("send eth failed"); 60 | 61 | instance 62 | .transfer(x, 1_000_000.into()) 63 | .send() 64 | .await 65 | .expect("transfer 0->x failed"); 66 | instance 67 | .transfer(accounts[4], 420.into()) 68 | .from(Account::Offline(key, None)) 69 | .send() 70 | .await 71 | .expect("transfer x->4 failed"); 72 | 73 | print_balance_of(&instance, x).await; 74 | print_balance_of(&instance, accounts[4]).await; 75 | 76 | // mint some RustCoin with the fallback method 77 | instance 78 | .fallback(vec![]) 79 | .from(Account::Local(accounts[3], None)) 80 | .value(1_000_000_000_000_000_000u64.into()) 81 | .send() 82 | .await 83 | .expect("mint 3 failed"); 84 | print_balance_of(&instance, accounts[3]).await; 85 | } 86 | 87 | async fn print_balance_of(instance: &RustCoin, account: Address) { 88 | let balance = instance 89 | .balance_of(account) 90 | .call() 91 | .await 92 | .expect("balance of failed"); 93 | println!("Account {:?} has balance of {}", account, balance); 94 | } 95 | -------------------------------------------------------------------------------- /examples/examples/batch.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::{batch::CallBatch, prelude::*}; 2 | 3 | ethcontract::contract!("examples/truffle/build/contracts/RustCoin.json"); 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let http = Http::new("http://localhost:9545").expect("transport failed"); 8 | let web3 = Web3::new(http); 9 | 10 | let accounts = web3.eth().accounts().await.expect("get accounts failed"); 11 | 12 | let instance = RustCoin::builder(&web3) 13 | .gas(4_712_388u64.into()) 14 | .deploy() 15 | .await 16 | .expect("deployment failed"); 17 | let name = instance.name().call().await.expect("get name failed"); 18 | println!("Deployed {} at {:?}", name, instance.address()); 19 | 20 | instance 21 | .transfer(accounts[1], 1_000_000u64.into()) 22 | .send() 23 | .await 24 | .expect("transfer 0->1 failed"); 25 | instance 26 | .transfer(accounts[2], 500_000u64.into()) 27 | .send() 28 | .await 29 | .expect("transfer 1->2 failed"); 30 | 31 | let mut batch = CallBatch::new(web3.transport()); 32 | let calls = vec![ 33 | instance.balance_of(accounts[1]).batch_call(&mut batch), 34 | instance.balance_of(accounts[2]).batch_call(&mut batch), 35 | ]; 36 | batch.execute_all(usize::MAX).await; 37 | for (id, call) in calls.into_iter().enumerate() { 38 | println!("Call {} returned {}", id, call.await.unwrap()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/examples/bytecode.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | 3 | ethcontract::contract!("examples/truffle/build/contracts/RustCoin.json"); 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let http = Http::new("http://localhost:9545").expect("transport failed"); 8 | let web3 = Web3::new(http); 9 | 10 | let instance = RustCoin::builder(&web3) 11 | .gas(4_712_388.into()) 12 | .deploy() 13 | .await 14 | .expect("deployment failed"); 15 | 16 | let code = web3 17 | .eth() 18 | .code(instance.address(), None) 19 | .await 20 | .expect("get code failed"); 21 | assert_eq!( 22 | code, 23 | RustCoin::raw_contract() 24 | .deployed_bytecode 25 | .to_bytes() 26 | .expect("failed to read contract deployed bytecode"), 27 | ); 28 | 29 | println!("RustCoin deployment matches expected bytecode"); 30 | } 31 | -------------------------------------------------------------------------------- /examples/examples/deployments.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | 3 | ethcontract::contract!( 4 | "examples/truffle/build/contracts/RustCoin.json", 5 | deployments { 6 | 31337 => "0x0123456789012345678901234567890123456789", 7 | }, 8 | ); 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let http = Http::new("http://localhost:9545").expect("transport failed"); 13 | let web3 = Web3::new(http); 14 | 15 | let network_id = web3 16 | .eth() 17 | .chain_id() 18 | .await 19 | .expect("failed to get network ID"); 20 | let instance = RustCoin::deployed(&web3) 21 | .await 22 | .expect("failed to find deployment"); 23 | 24 | println!( 25 | "RustCoin deployed on networks {} at {:?}", 26 | network_id, 27 | instance.address() 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/examples/events.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | use futures::join; 3 | use futures::stream::StreamExt; 4 | 5 | ethcontract::contract!("examples/truffle/build/contracts/RustCoin.json"); 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let http = Http::new("http://localhost:9545").expect("transport failed"); 10 | let web3 = Web3::new(http); 11 | 12 | let accounts = web3.eth().accounts().await.expect("get accounts failed"); 13 | 14 | let instance = RustCoin::builder(&web3) 15 | .gas(4_712_388.into()) 16 | .deploy() 17 | .await 18 | .expect("deployment failed"); 19 | let mut transfers = instance 20 | .events() 21 | .transfer() 22 | .from(Topic::This(accounts[0])) 23 | .stream() 24 | .boxed(); 25 | 26 | join! { 27 | async { 28 | instance 29 | .transfer(accounts[1], 1_000_000.into()) 30 | .send() 31 | .await 32 | .expect("transfer 0->1 failed"); 33 | }, 34 | async { 35 | let transfer = transfers.next() 36 | .await 37 | .expect("no more events") 38 | .expect("error querying event") 39 | .added() 40 | .expect("expected added event"); 41 | println!("Received a transfer event to {:?} with amount {}", transfer.to, transfer.value); 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /examples/examples/kms.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::{ 2 | aws_config, 3 | prelude::*, 4 | transaction::{kms, TransactionBuilder}, 5 | }; 6 | use std::env; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | // Run `aws configure export-credentials --profile cow-staging --format env` to get required env variable locally 11 | let config = aws_config::load_from_env().await; 12 | let account = kms::Account::new( 13 | (&config).into(), 14 | &env::var("KMS_KEY_ID").expect("KMS_KEY_ID not set"), 15 | ) 16 | .await 17 | .unwrap(); 18 | 19 | let web3 = { 20 | let url = env::var("NODE_URL").expect("NODE_URL not set"); 21 | let http = Http::new(&url).expect("transport failed"); 22 | Web3::new(http) 23 | }; 24 | println!( 25 | "Sending transaction to self: {:?}", 26 | account.public_address() 27 | ); 28 | 29 | let chain_id = web3.eth().chain_id().await.expect("Failed to get chainID"); 30 | let receipt = TransactionBuilder::new(web3) 31 | .from(Account::Kms(account.clone(), Some(chain_id.as_u64()))) 32 | .to(account.public_address()) 33 | .send() 34 | .await 35 | .unwrap(); 36 | println!("Transaction hash: {:?}", receipt.hash()); 37 | } 38 | -------------------------------------------------------------------------------- /examples/examples/linked.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | 3 | ethcontract::contract!("examples/truffle/build/contracts/SimpleLibrary.json"); 4 | ethcontract::contract!("examples/truffle/build/contracts/LinkedContract.json"); 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let http = Http::new("http://localhost:9545").expect("transport failure"); 9 | let web3 = Web3::new(http); 10 | 11 | let library = SimpleLibrary::builder(&web3) 12 | .gas(4_712_388.into()) 13 | .deploy() 14 | .await 15 | .expect("library deployment failure"); 16 | let instance = LinkedContract::builder( 17 | &web3, 18 | linked_contract::Libraries { 19 | simple_library: library.address(), 20 | }, 21 | 1337.into(), 22 | ) 23 | .gas(4_712_388.into()) 24 | .deploy() 25 | .await 26 | .expect("contract deployment failure"); 27 | 28 | println!( 29 | "The value is {}", 30 | instance.value().call().await.expect("get value failure") 31 | ); 32 | println!( 33 | "The answer is {}", 34 | instance 35 | .call_answer() 36 | .call() 37 | .await 38 | .expect("callAnswer failure") 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/examples/overloaded.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | 3 | ethcontract::contract!( 4 | "examples/truffle/build/contracts/OverloadedMethods.json", 5 | methods { 6 | getValue(bool) as get_bool_value; 7 | }, 8 | ); 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let http = Http::new("http://localhost:9545").expect("transport failure"); 13 | let web3 = Web3::new(http); 14 | 15 | let instance = OverloadedMethods::builder(&web3) 16 | .gas(4_712_388.into()) 17 | .deploy() 18 | .await 19 | .expect("contract deployment failure"); 20 | println!("Using contract at {:?}", instance.address()); 21 | 22 | println!( 23 | "U256 value: {}", 24 | instance 25 | .get_value(84.into()) 26 | .call() 27 | .await 28 | .expect("get value failed"), 29 | ); 30 | println!( 31 | "bool value: {}", 32 | instance 33 | .get_bool_value(false) 34 | .call() 35 | .await 36 | .expect("get value failed"), 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /examples/examples/past_events.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::{common::DeploymentInformation, prelude::*}; 2 | use futures::TryStreamExt as _; 3 | use std::env; 4 | 5 | ethcontract::contract!("npm:@gnosis.pm/owl-token@3.1.0/build/contracts/TokenOWLProxy.json"); 6 | ethcontract::contract!("npm:@gnosis.pm/owl-token@3.1.0/build/contracts/TokenOWL.json"); 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let infura_url = { 11 | let project_id = env::var("INFURA_PROJECT_ID").expect("INFURA_PROJECT_ID is not set"); 12 | format!("https://mainnet.infura.io/v3/{}", project_id) 13 | }; 14 | 15 | let http = Http::new(&infura_url).expect("transport failed"); 16 | let web3 = Web3::new(http); 17 | 18 | let owl_proxy = TokenOWLProxy::deployed(&web3) 19 | .await 20 | .expect("locating deployed contract failed"); 21 | 22 | // Casting proxy token into actual token 23 | let owl_token = TokenOWL::with_deployment_info( 24 | &web3, 25 | owl_proxy.address(), 26 | Some(DeploymentInformation::BlockNumber(12063584)), 27 | ); 28 | println!("Using OWL token at {:?}", owl_token.address()); 29 | println!("Retrieving all past events (this could take a while)..."); 30 | let event_history_stream = owl_token 31 | .all_events() 32 | .from_block(BlockNumber::Earliest) 33 | .query_paginated() 34 | .await 35 | .expect("Couldn't retrieve event history"); 36 | let event_history_vec = event_history_stream 37 | .try_collect::>() 38 | .await 39 | .expect("Couldn't parse event"); 40 | println!( 41 | "Total number of events emitted by OWL token {:}", 42 | event_history_vec.len() 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /examples/examples/revert.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::{errors::ExecutionError, prelude::*}; 2 | 3 | ethcontract::contract!("examples/truffle/build/contracts/Revert.json"); 4 | 5 | // Can use this to test with infura. 6 | async fn _contract_ropsten() -> Revert { 7 | let http = Http::new("https://ropsten.infura.io/v3/f27cfd9cca1a41d2a56ca5df0e9bff5e").unwrap(); 8 | let address: H160 = "0x2B8d1E12c4e87cEedf8B1DcA133983d6493Ff780" 9 | .parse() 10 | .unwrap(); 11 | let web3 = Web3::new(http); 12 | Revert::at(&web3, address) 13 | } 14 | 15 | async fn contract_ganache() -> Revert { 16 | let http = Http::new("http://localhost:9545").unwrap(); 17 | let web3 = Web3::new(http); 18 | Revert::builder(&web3).deploy().await.unwrap() 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() { 23 | let instance = contract_ganache().await; 24 | 25 | let result_0 = dbg!(instance.revert_with_reason().call().await); 26 | let result_1 = dbg!(instance.revert_without_reason().call().await); 27 | let result_2 = dbg!(instance.invalid_op_code().call().await); 28 | 29 | let error = result_0.unwrap_err().inner; 30 | assert!(matches!( 31 | error, 32 | ExecutionError::Revert(Some(reason)) if reason == "revert: reason" 33 | )); 34 | 35 | let error = result_1.unwrap_err().inner; 36 | assert!(matches!(error, ExecutionError::Revert(None))); 37 | 38 | let error = result_2.unwrap_err().inner; 39 | assert!(matches!(error, ExecutionError::Web3(_))); 40 | } 41 | -------------------------------------------------------------------------------- /examples/examples/rinkeby.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | use ethcontract::web3::transports::WebSocket; 3 | use std::env; 4 | 5 | ethcontract::contract!("examples/truffle/build/contracts/DeployedContract.json"); 6 | 7 | const RINKEBY_CHAIN_ID: u64 = 4; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let account = { 12 | let pk = env::var("PK").expect("PK is not set"); 13 | let key: PrivateKey = pk.parse().expect("invalid PK"); 14 | Account::Offline(key, Some(RINKEBY_CHAIN_ID)) 15 | }; 16 | let infura_url = { 17 | let project_id = env::var("INFURA_PROJECT_ID").expect("INFURA_PROJECT_ID is not set"); 18 | format!("wss://rinkeby.infura.io/ws/v3/{}", project_id) 19 | }; 20 | 21 | // NOTE: Use a WebSocket transport for `eth_newBlockFilter` support on 22 | // Infura, filters are disabled over HTTPS. Filters are needed for 23 | // confirmation support. 24 | let ws = WebSocket::new(&infura_url).await.expect("transport failed"); 25 | let web3 = Web3::new(ws); 26 | 27 | println!("Account {:?}", account.address()); 28 | 29 | let instance = { 30 | let mut instance = DeployedContract::deployed(&web3) 31 | .await 32 | .expect("locating deployed contract failed"); 33 | instance.defaults_mut().from = Some(account); 34 | instance 35 | }; 36 | 37 | println!( 38 | "Using contract at {:?} deployed with transaction {:?}", 39 | instance.address(), 40 | instance.deployment_information(), 41 | ); 42 | 43 | println!( 44 | " value before: {}", 45 | instance.value().call().await.expect("get value failed") 46 | ); 47 | println!(" incrementing (this may take a while)..."); 48 | instance 49 | .increment() 50 | .confirmations(1) // wait for 1 block confirmation 51 | .send() 52 | .await 53 | .expect("increment failed"); 54 | println!( 55 | " value after: {}", 56 | instance.value().call().await.expect("get value failed") 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /examples/examples/sources.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | use std::env; 3 | 4 | ethcontract::contract!( 5 | "etherscan:0x60fbbd1fb0076971e8060631b5dd895f55ad5ab7", 6 | contract = Owl, 7 | ); 8 | ethcontract::contract!("npm:@gnosis.pm/owl-token@3.1.0/build/contracts/TokenOWL.json"); 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let infura_url = { 13 | let project_id = env::var("INFURA_PROJECT_ID").expect("INFURA_PROJECT_ID is not set"); 14 | format!("https://mainnet.infura.io/v3/{}", project_id) 15 | }; 16 | 17 | let http = Http::new(&infura_url).expect("transport failed"); 18 | let web3 = Web3::new(http); 19 | 20 | let instance = Owl::deployed(&web3) 21 | .await 22 | .expect("locating deployed contract failed"); 23 | let symbol = instance.symbol().call().await.expect("get symbol failed"); 24 | 25 | println!("Etherscan.io ERC20 token {}", symbol); 26 | 27 | let instance = TokenOWL::deployed(&web3) 28 | .await 29 | .expect("locating deployed contract failed"); 30 | let symbol = instance.symbol().call().await.expect("get symbol failed"); 31 | 32 | println!("npmjs ERC20 token {}", symbol); 33 | } 34 | -------------------------------------------------------------------------------- /examples/generate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples-generate" 3 | version = "0.0.0" 4 | publish = false 5 | authors = ["Gnosis developers "] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [dependencies] 10 | ethcontract = { path = "../../ethcontract" } 11 | futures = "0.3" 12 | tokio = { version = "1.6", features = ["macros"] } 13 | 14 | [build-dependencies] 15 | ethcontract-generate = { path = "../../ethcontract-generate", default-features = false } 16 | -------------------------------------------------------------------------------- /examples/generate/README.md: -------------------------------------------------------------------------------- 1 | # Build script example 2 | 3 | This example shows a simple setup that uses a build script to generate contract 4 | bindings from hardhat `deployments` directory. 5 | 6 | We use `build.rs` to generate file `contracts.rs`. Then we include said file 7 | into our code using the `include!` macro. 8 | -------------------------------------------------------------------------------- /examples/generate/build.rs: -------------------------------------------------------------------------------- 1 | use ethcontract_generate::loaders::HardHatLoader; 2 | use ethcontract_generate::ContractBuilder; 3 | 4 | fn main() { 5 | let out_dir = std::env::var("OUT_DIR").unwrap(); 6 | let dest = std::path::Path::new(&out_dir).join("contracts.rs"); 7 | 8 | let artifact = HardHatLoader::new() 9 | .deny_network_by_name("localhost") 10 | .load_from_directory("../hardhat/deployments") 11 | .unwrap(); 12 | 13 | for contract in artifact.iter() { 14 | ContractBuilder::new() 15 | .generate(contract) 16 | .unwrap() 17 | .write_to_file(&dest) 18 | .unwrap(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/generate/src/main.rs: -------------------------------------------------------------------------------- 1 | use ethcontract::prelude::*; 2 | use std::env; 3 | 4 | include!(concat!(env!("OUT_DIR"), "/contracts.rs")); 5 | 6 | const RINKEBY_CHAIN_ID: u64 = 4; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let account = { 11 | let pk = env::var("PK").expect("PK is not set"); 12 | let key: PrivateKey = pk.parse().expect("invalid PK"); 13 | Account::Offline(key, Some(RINKEBY_CHAIN_ID)) 14 | }; 15 | let infura_url = { 16 | let project_id = env::var("INFURA_PROJECT_ID").expect("INFURA_PROJECT_ID is not set"); 17 | format!("https://rinkeby.infura.io/v3/{}", project_id) 18 | }; 19 | 20 | let http = Http::new(&infura_url).expect("create transport failed"); 21 | let web3 = Web3::new(http); 22 | 23 | let instance = { 24 | let mut instance = DeployedContract::deployed(&web3) 25 | .await 26 | .expect("locating deployed contract failed"); 27 | instance.defaults_mut().from = Some(account); 28 | instance 29 | }; 30 | 31 | println!( 32 | "Using contract at {:?} deployed with transaction {:?}", 33 | instance.address(), 34 | instance.deployment_information(), 35 | ); 36 | 37 | println!( 38 | " value before: {}", 39 | instance.value().call().await.expect("get value failed") 40 | ); 41 | println!(" incrementing (this may take a while)..."); 42 | instance 43 | .increment() 44 | .confirmations(1) // wait for 1 block confirmation 45 | .send() 46 | .await 47 | .expect("increment failed"); 48 | println!( 49 | " value after: {}", 50 | instance.value().call().await.expect("get value failed") 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /examples/hardhat/README.md: -------------------------------------------------------------------------------- 1 | # HardHat 2 | 3 | This subdirectory contains a hardhat project with sample contracts used by the 4 | `ethcontract-rs` crate for its examples and tests. 5 | 6 | Information about contracts ABI and their deployments is committed to the 7 | `deployments` directory. You don't need to build them or deploy them to run 8 | any examples. 9 | 10 | At the moment, there's only `DeployedContract.sol`, a simple contract that 11 | is deployed on the Rinkeby testnet. It is identical to `DeployedContract.sol` 12 | from truffle directory. 13 | 14 | ## Building and Deploying 15 | 16 | Building and deploying contracts is done with the same commands as in the 17 | Truffle package. 18 | 19 | To build: 20 | 21 | ```sh 22 | yarn run build 23 | ``` 24 | 25 | To deploy to Rinkeby, export private key and Infura key: 26 | 27 | ```sh 28 | export PK="private key" 29 | export INFURA_PROJECT_ID="Infura project ID" 30 | ``` 31 | 32 | Then run the deployment script: 33 | 34 | ```sh 35 | yarn run deploy 36 | ``` 37 | 38 | This will run hardhat deployment process. 39 | -------------------------------------------------------------------------------- /examples/hardhat/contracts/DeployedContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Rinkeby deployed contract used in examples. 6 | */ 7 | contract DeployedContract { 8 | mapping(address => uint256) private values; 9 | 10 | /** 11 | * @dev Gets the current value set in the contract for the `msg.sender`. 12 | */ 13 | function value() public view returns (uint256) { 14 | return values[msg.sender]; 15 | } 16 | 17 | /** 18 | * @dev Increments the value for the `msg.sender` by 1. 19 | */ 20 | function increment() public returns (uint256) { 21 | values[msg.sender]++; 22 | return (values[msg.sender]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/hardhat/deploy/00_init.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({getNamedAccounts, deployments}) => { 2 | const {deployer} = await getNamedAccounts(); 3 | await deployments.deploy('DeployedContract', {from: deployer}); 4 | }; 5 | -------------------------------------------------------------------------------- /examples/hardhat/deployments/localhost/.chainId: -------------------------------------------------------------------------------- 1 | 1337 2 | -------------------------------------------------------------------------------- /examples/hardhat/deployments/localhost/DeployedContract.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x29BE0588389993e7064C21f00761303eb51373F5", 3 | "abi": [ 4 | { 5 | "inputs": [], 6 | "name": "increment", 7 | "outputs": [ 8 | { 9 | "internalType": "uint256", 10 | "name": "", 11 | "type": "uint256" 12 | } 13 | ], 14 | "stateMutability": "nonpayable", 15 | "type": "function" 16 | }, 17 | { 18 | "inputs": [], 19 | "name": "value", 20 | "outputs": [ 21 | { 22 | "internalType": "uint256", 23 | "name": "", 24 | "type": "uint256" 25 | } 26 | ], 27 | "stateMutability": "view", 28 | "type": "function" 29 | } 30 | ], 31 | "transactionHash": "0xe0631d7f749fe73f94e59f6e25ff9b925980e8e29ed67b8f862ec76a783ea06e", 32 | "receipt": { 33 | "to": null, 34 | "from": "0x58F7bf16796d069a7590525eBD507921036Ce82B", 35 | "contractAddress": "0x29BE0588389993e7064C21f00761303eb51373F5", 36 | "transactionIndex": 0, 37 | "gasUsed": "175807", 38 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 39 | "blockHash": "0xbbc976772d85d908aedad3c88de6dd0f7adb6dc7af7524f6cf6bd3c726521c7e", 40 | "transactionHash": "0xe0631d7f749fe73f94e59f6e25ff9b925980e8e29ed67b8f862ec76a783ea06e", 41 | "logs": [], 42 | "blockNumber": 151, 43 | "cumulativeGasUsed": "175807", 44 | "status": 1, 45 | "byzantium": true 46 | }, 47 | "args": [], 48 | "solcInputHash": "d7ef70c507cb995ea3e81152c437ca74", 49 | "metadata": "{\"compiler\":{\"version\":\"0.8.0+commit.c7dfd78e\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"increment\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"value\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Rinkeby deployed contract used in examples.\",\"kind\":\"dev\",\"methods\":{\"increment()\":{\"details\":\"Increments the value for the `msg.sender` by 1.\"},\"value()\":{\"details\":\"Gets the current value set in the contract for the `msg.sender`.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/DeployedContract.sol\":\"DeployedContract\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"contracts/DeployedContract.sol\":{\"content\":\"pragma solidity ^0.8.0;\\n\\n/**\\n * @dev Rinkeby deployed contract used in examples.\\n */\\ncontract DeployedContract {\\n mapping(address => uint256) private values;\\n\\n /**\\n * @dev Gets the current value set in the contract for the `msg.sender`.\\n */\\n function value() public view returns (uint256) {\\n return values[msg.sender];\\n }\\n\\n /**\\n * @dev Increments the value for the `msg.sender` by 1.\\n */\\n function increment() public returns (uint256) {\\n values[msg.sender]++;\\n return (values[msg.sender]);\\n }\\n}\\n\",\"keccak256\":\"0x85ebb83768b7c9a3ca7db76560b0833e0e57d29716df381ef14e72a144e4e64d\"}},\"version\":1}", 50 | "bytecode": "0x608060405234801561001057600080fd5b50610239806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633fa4f2451461003b578063d09de08a14610059575b600080fd5b610043610077565b6040516100509190610166565b60405180910390f35b6100616100bd565b60405161006e9190610166565b60405180910390f35b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905090565b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600081548092919061010e9061018b565b91905055506000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905090565b61016081610181565b82525050565b600060208201905061017b6000830184610157565b92915050565b6000819050919050565b600061019682610181565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156101c9576101c86101d4565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea2646970667358221220993b4e7128d49168b275476d44461ca250c375b19974365fa3372ff084874faf64736f6c63430008000033", 51 | "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80633fa4f2451461003b578063d09de08a14610059575b600080fd5b610043610077565b6040516100509190610166565b60405180910390f35b6100616100bd565b60405161006e9190610166565b60405180910390f35b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905090565b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600081548092919061010e9061018b565b91905055506000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905090565b61016081610181565b82525050565b600060208201905061017b6000830184610157565b92915050565b6000819050919050565b600061019682610181565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156101c9576101c86101d4565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea2646970667358221220993b4e7128d49168b275476d44461ca250c375b19974365fa3372ff084874faf64736f6c63430008000033", 52 | "devdoc": { 53 | "details": "Rinkeby deployed contract used in examples.", 54 | "kind": "dev", 55 | "methods": { 56 | "increment()": { 57 | "details": "Increments the value for the `msg.sender` by 1." 58 | }, 59 | "value()": { 60 | "details": "Gets the current value set in the contract for the `msg.sender`." 61 | } 62 | }, 63 | "version": 1 64 | }, 65 | "userdoc": { 66 | "kind": "user", 67 | "methods": {}, 68 | "version": 1 69 | }, 70 | "storageLayout": { 71 | "storage": [ 72 | { 73 | "astId": 6, 74 | "contract": "contracts/DeployedContract.sol:DeployedContract", 75 | "label": "values", 76 | "offset": 0, 77 | "slot": "0", 78 | "type": "t_mapping(t_address,t_uint256)" 79 | } 80 | ], 81 | "types": { 82 | "t_address": { 83 | "encoding": "inplace", 84 | "label": "address", 85 | "numberOfBytes": "20" 86 | }, 87 | "t_mapping(t_address,t_uint256)": { 88 | "encoding": "mapping", 89 | "key": "t_address", 90 | "label": "mapping(address => uint256)", 91 | "numberOfBytes": "32", 92 | "value": "t_uint256" 93 | }, 94 | "t_uint256": { 95 | "encoding": "inplace", 96 | "label": "uint256", 97 | "numberOfBytes": "32" 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/hardhat/deployments/localhost/solcInputs/d7ef70c507cb995ea3e81152c437ca74.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Solidity", 3 | "sources": { 4 | "contracts/DeployedContract.sol": { 5 | "content": "pragma solidity ^0.8.0;\n\n/**\n * @dev Rinkeby deployed contract used in examples.\n */\ncontract DeployedContract {\n mapping(address => uint256) private values;\n\n /**\n * @dev Gets the current value set in the contract for the `msg.sender`.\n */\n function value() public view returns (uint256) {\n return values[msg.sender];\n }\n\n /**\n * @dev Increments the value for the `msg.sender` by 1.\n */\n function increment() public returns (uint256) {\n values[msg.sender]++;\n return (values[msg.sender]);\n }\n}\n" 6 | } 7 | }, 8 | "settings": { 9 | "optimizer": { 10 | "enabled": false, 11 | "runs": 200 12 | }, 13 | "outputSelection": { 14 | "*": { 15 | "*": [ 16 | "abi", 17 | "evm.bytecode", 18 | "evm.deployedBytecode", 19 | "evm.methodIdentifiers", 20 | "metadata", 21 | "devdoc", 22 | "userdoc", 23 | "storageLayout", 24 | "evm.gasEstimates" 25 | ], 26 | "": [ 27 | "ast" 28 | ] 29 | } 30 | }, 31 | "metadata": { 32 | "useLiteralContent": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/hardhat/deployments/rinkeby/.chainId: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /examples/hardhat/deployments/rinkeby/solcInputs/d7ef70c507cb995ea3e81152c437ca74.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Solidity", 3 | "sources": { 4 | "contracts/DeployedContract.sol": { 5 | "content": "pragma solidity ^0.8.0;\n\n/**\n * @dev Rinkeby deployed contract used in examples.\n */\ncontract DeployedContract {\n mapping(address => uint256) private values;\n\n /**\n * @dev Gets the current value set in the contract for the `msg.sender`.\n */\n function value() public view returns (uint256) {\n return values[msg.sender];\n }\n\n /**\n * @dev Increments the value for the `msg.sender` by 1.\n */\n function increment() public returns (uint256) {\n values[msg.sender]++;\n return (values[msg.sender]);\n }\n}\n" 6 | } 7 | }, 8 | "settings": { 9 | "optimizer": { 10 | "enabled": false, 11 | "runs": 200 12 | }, 13 | "outputSelection": { 14 | "*": { 15 | "*": [ 16 | "abi", 17 | "evm.bytecode", 18 | "evm.deployedBytecode", 19 | "evm.methodIdentifiers", 20 | "metadata", 21 | "devdoc", 22 | "userdoc", 23 | "storageLayout", 24 | "evm.gasEstimates" 25 | ], 26 | "": [ 27 | "ast" 28 | ] 29 | } 30 | }, 31 | "metadata": { 32 | "useLiteralContent": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/hardhat/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("hardhat-deploy"); 2 | 3 | const {PK, INFURA_PROJECT_ID} = process.env; 4 | 5 | const sharedNetworkConfig = { 6 | accounts: [PK], 7 | }; 8 | 9 | module.exports = { 10 | solidity: "0.8.0", 11 | 12 | networks: { 13 | localhost: { 14 | ...sharedNetworkConfig, 15 | live: false, 16 | }, 17 | rinkeby: { 18 | ...sharedNetworkConfig, 19 | url: `https://rinkeby.infura.io/v3/${INFURA_PROJECT_ID}`, 20 | }, 21 | }, 22 | 23 | namedAccounts: { 24 | deployer: 0, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /examples/hardhat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethcontract-contracts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Test contracts for ethcontract-rs runtime and proc macro.", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "hardhat compile", 9 | "deploy": "hardhat --network rinkeby deploy" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/gnosis/ethcontract-rs.git" 14 | }, 15 | "author": "Nicholas Rodrigues Lordello ", 16 | "license": "(MIT OR Apache-2.0)", 17 | "bugs": { 18 | "url": "https://github.com/gnosis/ethcontract-rs" 19 | }, 20 | "homepage": "https://github.com/gnosis/ethcontract-rs", 21 | "devDependencies": { 22 | "hardhat": "^2.7.1", 23 | "hardhat-deploy": "^0.9.14" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/truffle/.network-restore.conf.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | buildPath: path.join(__dirname, "build", "contracts"), 5 | buildDirDependencies: [], 6 | networkFilePath: path.join(__dirname, "networks.json"), 7 | }; 8 | -------------------------------------------------------------------------------- /examples/truffle/README.md: -------------------------------------------------------------------------------- 1 | # Truffle 2 | 3 | This subdirectory contains a truffle project with sample contracts used by the 4 | `ethcontract-rs` crate for its examples. 5 | 6 | - `AbiTypes.sol` a simple contract that just returns pseudo-random data for 7 | various primitive Solidity types. 8 | - `DeployedContract.sol` a simple contract that is deployed on the Rinkeby 9 | testnet and used for the rinkeby example. 10 | - `DocumentedContract.sol` a sample with contract level documentation. We use 11 | this to verify the `ethcontract-derive` is properly injecting the docstring 12 | for the generated struct. 13 | - `SampleLibrary.sol` and `LinkedContract.sol` a sample library and contract 14 | which uses the aforementioned library. We use this to test that linking and 15 | deployment with linking works. 16 | - `RustCoin.sol` a sample ERC20 coin that we interact with in our async example. 17 | The example shows how to call contract functions and sign them with various 18 | strategies (offline, on the node, etc.). 19 | 20 | ## Building 21 | 22 | This contract can be built with truffle. There is an npm script for doing this: 23 | 24 | ```sh 25 | yarn run build 26 | ``` 27 | 28 | ## Development Server 29 | 30 | We use `truffle develop` for the development server (which uses ganache under 31 | the hood). This is needed to run most examples. 32 | 33 | ## Rinkeby Deployment 34 | 35 | The `DeployedContract` used in the rinkeby example must be deployed prior for 36 | the example to work as expected. For this to work a few secrets are needed. 37 | These are provided to truffle with environment variables: 38 | 39 | ```sh 40 | export PK="private key" 41 | export INFURA_PROJECT_ID="Infura project ID" 42 | export ETHERSCAN_API_KEY="Etherscan API key" 43 | ``` 44 | 45 | In order to deploy, the following npm script should be used: 46 | ```sh 47 | yarn run deploy 48 | ``` 49 | 50 | This will: 51 | 1. Build and deploy the contract using `$PK`'s account for paying gas fees and 52 | the `$INFURA_PROJECT_ID` to connect to a node 53 | 2. Verify the contract on Etherscan.io 54 | -------------------------------------------------------------------------------- /examples/truffle/contracts/AbiTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Contract to illustract support for various Solidity types. 6 | */ 7 | contract AbiTypes { 8 | function getVoid() public pure {} 9 | 10 | function getU8() public view returns (uint8) { 11 | return uint8(getU256() & 0xff); 12 | } 13 | function getU16() public view returns (uint16) { 14 | return uint16(getU256() & 0xffff); 15 | } 16 | function getU32() public view returns (uint32) { 17 | return uint32(getU256() & 0xffffffff); 18 | } 19 | function getU64() public view returns (uint64) { 20 | return uint64(getU256() & 0xffffffffffffffff); 21 | } 22 | function getU128() public view returns (uint128) { 23 | return uint128(getU256() & 0xffffffffffffffffffffffffffffffff); 24 | } 25 | function getU256() public view returns (uint256) { 26 | return uint256(blockhash(block.number - 1)); 27 | } 28 | 29 | function getI8() public view returns (int8) { 30 | return int8(getI256() & 0xff); 31 | } 32 | function getI16() public view returns (int16) { 33 | return int16(getI256() & 0xffff); 34 | } 35 | function getI32() public view returns (int32) { 36 | return int32(getI256() & 0xffffffff); 37 | } 38 | function getI64() public view returns (int64) { 39 | return int64(getI256() & 0xffffffffffffffff); 40 | } 41 | function getI128() public view returns (int128) { 42 | return int128(getI256() & 0xffffffffffffffffffffffffffffffff); 43 | } 44 | function getI256() public view returns (int256) { 45 | return int256(getU256()); 46 | } 47 | 48 | function getBool() public view returns (bool) { 49 | return getU256() & 0x1 != 0; 50 | } 51 | function getBytes() public view returns (bytes memory) { 52 | return abi.encodePacked(getU32()); 53 | } 54 | function getFixedBytes() public view returns (bytes6) { 55 | return bytes6(uint48(getU64() & 0xffffffffffff)); 56 | } 57 | function getAddress() public view returns (address) { 58 | return address(uint160(getU256())); 59 | } 60 | function getString() public view returns (string memory) { 61 | bytes16 alphabet = "0123456789abcdef"; 62 | uint64 value = getU64(); 63 | bytes memory buf = new bytes(16); 64 | for (uint256 i = 16; i > 0; i--) { 65 | buf[i-1] = alphabet[value & 0xf]; 66 | value >>= 4; 67 | } 68 | return string(buf); 69 | } 70 | 71 | function getArray() public view returns (uint64[] memory) { 72 | uint256 value = getU256(); 73 | uint64[] memory buf = new uint64[](4); 74 | for (uint256 i = 4; i > 0; i--) { 75 | buf[i-1] = uint64(value & 0xffffffffffffffff); 76 | value >>= 64; 77 | } 78 | return buf; 79 | } 80 | function getFixedArray() public view returns (int32[3] memory) { 81 | uint256 value = getU256(); 82 | int32[3] memory buf = [int32(0), int32(0), int32(0)]; 83 | for (uint256 i = 3; i > 0; i--) { 84 | buf[i-1] = int32(uint32(value & 0xffffffff)); 85 | value >>= 32; 86 | } 87 | return buf; 88 | } 89 | 90 | event ValueUint(uint8 a, uint16 b, uint32 c, uint64 d, uint128 e, uint256 indexed value); 91 | event ValueInt(int8 a, int16 b, int32 c, int64 d, int128 e, int256 indexed value); 92 | 93 | event ValueBool(bool); 94 | 95 | event ValueBytes(string id, bytes a, bytes6 b, address whoami); 96 | event ValueArray(uint64[] a, int32[3] b); 97 | 98 | event ValueIndexed(string indexed a, uint64[] indexed b); 99 | 100 | event Values(bytes32 indexed block, address sender) anonymous; 101 | 102 | function emitValues() public { 103 | emit ValueUint(getU8(), getU16(), getU32(), getU64(), getU128(), getU256()); 104 | emit ValueInt(getI8(), getI16(), getI32(), getI64(), getI128(), getI256()); 105 | emit ValueBool(getBool()); 106 | emit ValueBytes(getString(), getBytes(), getFixedBytes(), getAddress()); 107 | emit ValueArray(getArray(), getFixedArray()); 108 | emit ValueIndexed(getString(), getArray()); 109 | emit Values(blockhash(block.number - 1), msg.sender); 110 | } 111 | 112 | // Abi v2 113 | 114 | struct S { 115 | uint8 u0; 116 | uint16 u1; 117 | } 118 | 119 | function abiv2Struct(S calldata s) public pure returns (S calldata) { 120 | return s; 121 | } 122 | function abiv2ArrayOfStruct(S[] calldata s) public pure returns (S[] calldata) { 123 | return s; 124 | } 125 | function abiv2ArrayOfArrayOfStruct(S[][3] calldata s) public pure returns (S[][3] calldata) { 126 | return s; 127 | } 128 | 129 | function roundtripBytes(bytes calldata a) public pure returns (bytes calldata) { 130 | return a; 131 | } 132 | function roundtripFixedBytes(bytes3 a) public pure returns (bytes3) { 133 | return a; 134 | } 135 | function roundtripU8Array(uint8[] calldata a) public pure returns (uint8[] calldata) { 136 | return a; 137 | } 138 | function roundtripFixedU8Array(uint8[3] calldata a) public pure returns (uint8[3] calldata) { 139 | return a; 140 | } 141 | function multipleResults() public pure returns (uint8, uint8, uint8) { 142 | return (1, 2, 3); 143 | } 144 | function multipleResultsStruct() public pure returns (S memory, S memory) { 145 | return (S(0, 1), S(2, 3)); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /examples/truffle/contracts/DeployedContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Rinkeby deployed contract used in examples. 6 | */ 7 | contract DeployedContract { 8 | mapping(address => uint256) private values; 9 | 10 | /** 11 | * @dev Gets the current value set in the contract for the `msg.sender`. 12 | */ 13 | function value() public view returns (uint256) { 14 | return values[msg.sender]; 15 | } 16 | 17 | /** 18 | * @dev Increments the value for the `msg.sender` by 1. 19 | */ 20 | function increment() public returns (uint256) { 21 | values[msg.sender]++; 22 | return (values[msg.sender]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/truffle/contracts/DocumentedContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Simple contract with lots of documentation 6 | * 7 | * This contract does nothing besides add a bunch of documentation to everything. 8 | */ 9 | contract DocumentedContract { 10 | /* 11 | * @dev The owner of this documented contract 12 | */ 13 | address public owner; 14 | 15 | /* 16 | * @dev Creates a new owned instance of `DocumentedContract`. 17 | */ 18 | constructor(address owner_) { 19 | owner = owner_; 20 | } 21 | 22 | /* 23 | * @dev Documented fallback function that does nothing. 24 | */ 25 | fallback() external { } 26 | 27 | /* 28 | * @dev Documented function that emits an event. 29 | * 30 | * Emits a {Invoked} event. 31 | */ 32 | function invoke(uint256 value, bool condition) public returns (uint256) { 33 | uint256 result = 0; 34 | if (condition && msg.sender == owner) { 35 | result = value; 36 | } 37 | emit Invoked(msg.sender, value); 38 | 39 | return result; 40 | } 41 | 42 | /* 43 | * @dev Event emitted when the contract is invoked. 44 | */ 45 | event Invoked(address indexed from, uint256 result); 46 | } 47 | -------------------------------------------------------------------------------- /examples/truffle/contracts/LinkedContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./SimpleLibrary.sol"; 5 | 6 | contract LinkedContract { 7 | using SimpleLibrary for uint256; 8 | 9 | uint256 public value; 10 | 11 | constructor(uint256 value_) { 12 | value = value_; 13 | } 14 | 15 | function callAnswer() public pure returns (uint256) { 16 | return uint256(0).answer(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/truffle/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public last_completed_migration; 7 | 8 | constructor() { 9 | owner = msg.sender; 10 | } 11 | 12 | modifier restricted() { 13 | if (msg.sender == owner) _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | 20 | function upgrade(address new_address) public restricted { 21 | Migrations upgraded = Migrations(new_address); 22 | upgraded.setCompleted(last_completed_migration); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/truffle/contracts/OverloadedMethods.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Contract to illustrate how to work around overloaded Solidity methods. 6 | */ 7 | contract OverloadedMethods { 8 | function getValue(uint256 value) public pure returns (uint256) { 9 | return value / 2; 10 | } 11 | 12 | function getValue(bool value) public pure returns (bool) { 13 | return !value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/truffle/contracts/Revert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract Revert { 5 | function revert_with_reason() public pure { 6 | revert("reason"); 7 | } 8 | 9 | function revert_without_reason() public pure { 10 | revert(); 11 | } 12 | 13 | function invalid_op_code() public pure { 14 | assembly { 15 | invalid() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/truffle/contracts/RustCoin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract RustCoin is ERC20 { 7 | constructor() ERC20("Rust Coin", "RUST") { 8 | _mint(msg.sender, 1337 * (10 ** uint256(decimals()))); 9 | } 10 | 11 | receive() external payable { 12 | _mint(msg.sender, msg.value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/truffle/contracts/SimpleLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library SimpleLibrary { 5 | function answer(uint256 self) public pure returns (uint256) { 6 | return self + 42; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/truffle/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /examples/truffle/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const DeployedContract = artifacts.require("DeployedContract"); 2 | 3 | module.exports = async function (deployer) { 4 | await deployer.deploy(DeployedContract); 5 | }; 6 | -------------------------------------------------------------------------------- /examples/truffle/networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeployedContract": { 3 | "4": { 4 | "events": {}, 5 | "links": {}, 6 | "address": "0xD944917755c3DC50EB8A291347Ac8e40510C7824", 7 | "transactionHash": "0x0e17b4703bb58551c1ea649b0b31915385dda1d74109a436f24b09dc5b4873ce" 8 | } 9 | }, 10 | "Migrations": { 11 | "4": { 12 | "events": {}, 13 | "links": {}, 14 | "address": "0x614Ddc9511f0420B9Fa08A7E5e381e2cF6ca71b9", 15 | "transactionHash": "0xd3d0527862a589366131247942837d815aba3715f538ce569132be666e988421" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/truffle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethcontract-contracts", 3 | "version": "0.0.0", 4 | "private": "true", 5 | "description": "Test contracts for ethcontract-rs runtime and proc macro.", 6 | "scripts": { 7 | "build": "truffle compile && yarn run network:inject", 8 | "deploy": "truffle migrate --network rinkeby && truffle verify --network rinkeby && yarn run network:extract", 9 | "network:extract": "CONF_FILE=$(pwd)/.network-restore.conf.js node node_modules/@gnosis.pm/util-contracts/src/extract_network_info.js", 10 | "network:inject": "CONF_FILE=$(pwd)/.network-restore.conf.js node node_modules/@gnosis.pm/util-contracts/src/inject_network_info.js", 11 | "start": "truffle develop", 12 | "test": "truffle test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/gnosis/ethcontract-rs.git" 17 | }, 18 | "author": "Nicholas Rodrigues Lordello ", 19 | "license": "(MIT OR Apache-2.0)", 20 | "bugs": { 21 | "url": "https://github.com/gnosis/ethcontract-rs" 22 | }, 23 | "homepage": "https://github.com/gnosis/ethcontract-rs", 24 | "devDependencies": { 25 | "@gnosis.pm/util-contracts": "^3.0.1", 26 | "@openzeppelin/contracts": "^4.4.2", 27 | "@truffle/hdwallet-provider": "^2.0.0", 28 | "truffle": "^5.4.29", 29 | "truffle-plugin-verify": "^0.5.20" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/truffle/truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 2 | 3 | const { 4 | PK, 5 | INFURA_PROJECT_ID, 6 | ETHERSCAN_API_KEY 7 | } = process.env; 8 | 9 | module.exports = { 10 | networks: { 11 | rinkeby: { 12 | provider: () => 13 | new HDWalletProvider(PK, `https://rinkeby.infura.io/v3/${INFURA_PROJECT_ID}`), 14 | network_id: 4, 15 | }, 16 | }, 17 | 18 | mocha: { }, 19 | 20 | compilers: { 21 | solc: { 22 | version: "^0.8.0", 23 | }, 24 | }, 25 | 26 | plugins: [ 27 | "truffle-plugin-verify", 28 | ], 29 | 30 | api_keys: { 31 | etherscan: process.env.ETHERSCAN_API_KEY, 32 | }, 33 | }; 34 | --------------------------------------------------------------------------------