Merge branch 'master' into franfran/ast-fix

merge master (rebase was swallowing code)
This commit is contained in:
franfran 2023-01-04 00:08:18 +01:00
commit e5d2368f96
149 changed files with 10069 additions and 1321 deletions

View File

@ -1 +1 @@
msrv = "1.62" msrv = "1.64"

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
Cargo.lock linguist-generated=true
flake.lock linguist-generated=true

View File

@ -232,32 +232,7 @@ jobs:
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v1
with: with:
cache-on-failure: true cache-on-failure: true
- name: Build all examples - name: Build and run all examples
run: |
export PATH=$HOME/bin:$PATH
examples=$(cargo metadata --format-version 1 | \
jq -c '.packages[]
| select(.name == "ethers")
| .targets[]
| select(.kind[] | contains("example"))
| with_entries(select([.key]
| inside(["name", "required-features"])))'
)
for example in $examples; do
name="$(echo "$example" | jq -r '.name')"
args=(
-p ethers
--example "$name"
)
features="$(echo "$example" | jq -r 'try(."required-features" | join(","))')"
if [[ ! -z "$features" ]]; then
args+=(--features "$features")
fi
echo "building $name"
cargo build "${args[@]}"
done
- name: Run all examples
run: | run: |
export PATH=$HOME/bin:$PATH export PATH=$HOME/bin:$PATH
chmod +x ./scripts/examples.sh chmod +x ./scripts/examples.sh

5
.gitignore vendored
View File

@ -2,3 +2,8 @@
.vscode .vscode
/.envrc /.envrc
.idea .idea
.nlsp-settings
.devenv*
devenv.local.nix
.direnv
.pre-commit-config.yaml

View File

@ -4,6 +4,8 @@
### Unreleased ### Unreleased
- Fix typo in `RwClient` docs for `write_client` method.
- Add support for Geth `debug_traceCall` [#1949](https://github.com/gakonst/ethers-rs/pull/1949)
- Graceful handling of WebSocket transport errors [#1889](https://github.com/gakonst/ethers-rs/issues/1889) [#1815](https://github.com/gakonst/ethers-rs/issues/1815) - Graceful handling of WebSocket transport errors [#1889](https://github.com/gakonst/ethers-rs/issues/1889) [#1815](https://github.com/gakonst/ethers-rs/issues/1815)
- `MiddlewareBuilder` trait to instantiate a `Provider` as `Middleware` layers. - `MiddlewareBuilder` trait to instantiate a `Provider` as `Middleware` layers.
- An `Event` builder can be instantiated specifying the event filter type, without the need to instantiate a contract. - An `Event` builder can be instantiated specifying the event filter type, without the need to instantiate a contract.
@ -103,6 +105,7 @@
- [#842](https://github.com/gakonst/ethers-rs/issues/842) Add support for I256 types in `parse_units` and `format_units`. - [#842](https://github.com/gakonst/ethers-rs/issues/842) Add support for I256 types in `parse_units` and `format_units`.
Added `twos_complement` function for I256. Added `twos_complement` function for I256.
- [#1934](https://github.com/gakonst/ethers-rs/pull/1934) Allow 16 calls in multicall. - [#1934](https://github.com/gakonst/ethers-rs/pull/1934) Allow 16 calls in multicall.
- [#1941](https://github.com/gakonst/ethers-rs/pull/1941) Add `add_calls` and `call_array` for `Multicall`.
## ethers-contract-abigen ## ethers-contract-abigen
@ -272,6 +275,7 @@
- `eth-keystore-rs` crate updated. Allow an optional name for the to-be-generated - `eth-keystore-rs` crate updated. Allow an optional name for the to-be-generated
keystore file [#910](https://github.com/gakonst/ethers-rs/pull/910) keystore file [#910](https://github.com/gakonst/ethers-rs/pull/910)
- [1983](https://github.com/gakonst/ethers-rs/pull/1983) Added a `from_bytes` function for the `Wallet` type.
### 0.6.0 ### 0.6.0
@ -340,6 +344,8 @@
### Unreleased ### Unreleased
- Added `openssl` and `rustls` feature flags
[#1961](https://github.com/gakonst/ethers-rs/pull/1961)
- Relax Clone requirements when Arc<Middleware> is used - Relax Clone requirements when Arc<Middleware> is used
[#1183](https://github.com/gakonst/ethers-rs/pull/1183) [#1183](https://github.com/gakonst/ethers-rs/pull/1183)
- Ensure a consistent chain ID between a Signer and Provider in SignerMiddleware - Ensure a consistent chain ID between a Signer and Provider in SignerMiddleware

View File

@ -121,6 +121,7 @@ cargo check --all-features
cargo +nightly fmt --all cargo +nightly fmt --all
cargo build --all-features cargo build --all-features
cargo test --all-features cargo test --all-features
cargo +nightly clippy --all-features
``` ```
### Tests ### Tests

136
Cargo.lock generated
View File

@ -1420,6 +1420,7 @@ dependencies = [
"hex", "hex",
"hex-literal", "hex-literal",
"k256", "k256",
"num_enum",
"once_cell", "once_cell",
"open-fastrlp", "open-fastrlp",
"proc-macro2", "proc-macro2",
@ -1626,6 +1627,111 @@ dependencies = [
"wee_alloc", "wee_alloc",
] ]
[[package]]
name = "examples-anvil"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"tokio",
]
[[package]]
name = "examples-big-numbers"
version = "1.0.2"
dependencies = [
"ethers",
]
[[package]]
name = "examples-contracts"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "examples-events"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "examples-middleware"
version = "1.0.2"
dependencies = [
"async-trait",
"ethers",
"eyre",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "examples-providers"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "examples-queries"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "examples-subscriptions"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "examples-transactions"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "examples-wallets"
version = "1.0.2"
dependencies = [
"ethers",
"eyre",
"serde",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "eyre" name = "eyre"
version = "0.6.8" version = "0.6.8"
@ -2577,6 +2683,27 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_enum"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "num_threads" name = "num_threads"
version = "0.1.3" version = "0.1.3"
@ -3750,9 +3877,9 @@ dependencies = [
[[package]] [[package]]
name = "serial_test" name = "serial_test"
version = "0.9.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92761393ee4dc3ff8f4af487bd58f4307c9329bbedea02cac0089ad9c411e153" checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2"
dependencies = [ dependencies = [
"dashmap", "dashmap",
"futures", "futures",
@ -3764,11 +3891,10 @@ dependencies = [
[[package]] [[package]]
name = "serial_test_derive" name = "serial_test_derive"
version = "0.9.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b6f5d1c3087fb119617cff2966fe3808a80e5eb59a8c1601d5994d66f4346a5" checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3"
dependencies = [ dependencies = [
"proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",

View File

@ -2,7 +2,7 @@
name = "ethers" name = "ethers"
version = "1.0.2" version = "1.0.2"
edition = "2021" edition = "2021"
rust-version = "1.64" rust-version = "1.64" # must also be changed in **/Cargo.toml and .clippy.toml
authors = ["Georgios Konstantopoulos <me@gakonst.com>"] authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -21,7 +21,8 @@ members = [
"ethers-middleware", "ethers-middleware",
"ethers-etherscan", "ethers-etherscan",
"ethers-solc", "ethers-solc",
"examples/ethers-wasm", # Example crates
"examples/*",
] ]
default-members = [ default-members = [
@ -63,12 +64,14 @@ eip712 = ["ethers-contract/eip712", "ethers-core/eip712"]
ws = ["ethers-providers/ws"] ws = ["ethers-providers/ws"]
ipc = ["ethers-providers/ipc"] ipc = ["ethers-providers/ipc"]
rustls = [ rustls = [
"ethers-middleware/rustls",
"ethers-providers/rustls", "ethers-providers/rustls",
"ethers-etherscan/rustls", "ethers-etherscan/rustls",
"ethers-contract/rustls", "ethers-contract/rustls",
"ethers-solc/rustls", "ethers-solc/rustls",
] ]
openssl = [ openssl = [
"ethers-middleware/openssl",
"ethers-providers/openssl", "ethers-providers/openssl",
"ethers-etherscan/openssl", "ethers-etherscan/openssl",
"ethers-contract/openssl", "ethers-contract/openssl",
@ -84,6 +87,7 @@ abigen = ["ethers-contract/abigen"]
### abigen without reqwest ### abigen without reqwest
abigen-offline = ["ethers-contract/abigen-offline"] abigen-offline = ["ethers-contract/abigen-offline"]
## solc ## solc
ethers-solc = ["dep:ethers-solc", "ethers-etherscan/ethers-solc"]
solc-full = ["ethers-solc", "ethers-solc/full"] solc-full = ["ethers-solc", "ethers-solc/full"]
solc-tests = ["ethers-solc", "ethers-solc/tests"] solc-tests = ["ethers-solc", "ethers-solc/tests"]
solc-sha2-asm = ["ethers-solc", "ethers-solc/asm"] solc-sha2-asm = ["ethers-solc", "ethers-solc/asm"]
@ -125,62 +129,3 @@ bytes = "1.3.0"
# Tell `rustc` to optimize for small code size. # Tell `rustc` to optimize for small code size.
opt-level = "s" opt-level = "s"
[[example]]
name = "abigen"
path = "examples/abigen.rs"
required-features = ["ethers-solc"]
[[example]]
name = "contract_human_readable"
path = "examples/contract_human_readable.rs"
required-features = ["ethers-solc"]
[[example]]
name = "contract_with_abi"
path = "examples/contract_with_abi.rs"
required-features = ["ethers-solc"]
[[example]]
name = "contract_with_abi_and_bytecode"
path = "examples/contract_with_abi_and_bytecode.rs"
required-features = ["ethers-solc"]
[[example]]
name = "ipc"
path = "examples/ipc.rs"
required-features = ["ipc"]
[[example]]
name = "ledger"
path = "examples/ledger.rs"
required-features = ["ledger"]
[[example]]
name = "moonbeam_with_abi"
path = "examples/moonbeam_with_abi.rs"
required-features = ["legacy", "ethers-solc"]
[[example]]
name = "trezor"
path = "examples/trezor.rs"
required-features = ["trezor"]
[[example]]
name = "yubi"
path = "examples/yubi.rs"
required-features = ["yubi"]
[[example]]
name = "paginated_logs"
path = "examples/paginated_logs.rs"
required-features = ["rustls"]
[[example]]
name = "subscribe_contract_events"
path = "examples/subscribe_contract_events.rs"
required-features = ["rustls", "ws"]
[[example]]
name = "subscribe_contract_events_with_meta"
path = "examples/subscribe_contract_events_with_meta.rs"
required-features = ["rustls", "ws"]

View File

@ -15,7 +15,12 @@ Extensive documentation and examples are available [here](https://docs.rs/ethers
Alternatively, you may clone the repository and run `cd ethers/ && cargo doc --open` Alternatively, you may clone the repository and run `cd ethers/ && cargo doc --open`
You can also run any of the examples by executing: `cargo run -p ethers --example <name>` Examples are organized into individual crates under the `/examples` folder.
You can run any of the examples by executing:
```bash
# cargo run -p <example-crate-name> --example <name>
cargo run -p examples-big-numbers --example math_operations
```
## Add ethers-rs to your repository ## Add ethers-rs to your repository

View File

@ -11,7 +11,7 @@ repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"] keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies] [dependencies]
once_cell = "1.16.0" once_cell = "1.17.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -20,7 +20,7 @@ ethers-derive-eip712 = { version = "^1.0.0", path = "../ethers-core/ethers-deriv
serde = { version = "1.0.124", default-features = false } serde = { version = "1.0.124", default-features = false }
serde_json = { version = "1.0.64", default-features = false } serde_json = { version = "1.0.64", default-features = false }
thiserror = { version = "1.0", default-features = false } thiserror = { version = "1.0", default-features = false }
once_cell = { version = "1.16.0" } once_cell = { version = "1.17.0" }
pin-project = { version = "1.0.11", default-features = false } pin-project = { version = "1.0.11", default-features = false }
futures-util = { version = "^0.3" } futures-util = { version = "^0.3" }
hex = { version = "0.4.3", default-features = false, features = ["std"] } hex = { version = "0.4.3", default-features = false, features = ["std"] }

View File

@ -1,11 +1,12 @@
[package] [package]
name = "ethers-contract-abigen" name = "ethers-contract-abigen"
version = "1.0.2" version = "1.0.2"
edition = "2021"
rust-version = "1.64"
authors = [ authors = [
"Nicholas Rodrigues Lordello <nlordell@gmail.com>", "Nicholas Rodrigues Lordello <nlordell@gmail.com>",
"Georgios Konstantopoulos <me@gakonst.com>", "Georgios Konstantopoulos <me@gakonst.com>",
] ]
edition = "2018"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Code generation for type-safe bindings to Ethereum smart contracts" description = "Code generation for type-safe bindings to Ethereum smart contracts"
homepage = "https://docs.rs/ethers" homepage = "https://docs.rs/ethers"

View File

@ -31,6 +31,12 @@ impl std::ops::Deref for MultiAbigen {
} }
} }
impl std::ops::DerefMut for MultiAbigen {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.abigens
}
}
impl From<Vec<Abigen>> for MultiAbigen { impl From<Vec<Abigen>> for MultiAbigen {
fn from(abigens: Vec<Abigen>) -> Self { fn from(abigens: Vec<Abigen>) -> Self {
Self { abigens } Self { abigens }
@ -372,6 +378,16 @@ impl MultiBindings {
self.expansion.contracts.is_empty() self.expansion.contracts.is_empty()
} }
/// Specify whether or not to format the code using a locally installed copy
/// of `rustfmt`.
///
/// Note that in case `rustfmt` does not exist or produces an error, the
/// unformatted code will be used.
pub fn rustfmt(mut self, rustfmt: bool) -> Self {
self.rustfmt = rustfmt;
self
}
fn into_inner(self, single_file: bool) -> MultiBindingsInner { fn into_inner(self, single_file: bool) -> MultiBindingsInner {
self.expansion.into_bindings(single_file, self.rustfmt) self.expansion.into_bindings(single_file, self.rustfmt)
} }

View File

@ -20,6 +20,9 @@ pub enum Source {
/// An ABI to be retrieved over HTTP(S). /// An ABI to be retrieved over HTTP(S).
Http(Url), Http(Url),
/// An address of a mainnet contract that has been verified on Bscscan.com.
Bscscan(Address),
/// An address of a mainnet contract that has been verified on Etherscan.io. /// An address of a mainnet contract that has been verified on Etherscan.io.
Etherscan(Address), Etherscan(Address),
@ -54,7 +57,12 @@ impl Source {
/// ///
/// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a address or URL of a /// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a address or URL of a
/// verified contract on Etherscan. /// verified contract on Etherscan.
/// /// - `bscscan:0xXX..XX` or `https://bscscan.io/address/0xXX..XX`: a address or URL of a
/// verified contract on Bscscan.
/// - `polygonscan:0xXX..XX` or `https://polygonscan.io/address/0xXX..XX`: a address or URL of a
/// verified contract on Polygonscan.
/// - `snowtrace:0xXX..XX` or `https://snowtrace.io/address/0xXX..XX`: a address or URL of a
/// verified contract on Snowtrace.
/// - `npm:@org/package@1.0.0/path/to/contract.json` an npmjs package with an optional version /// - `npm:@org/package@1.0.0/path/to/contract.json` an npmjs package with an optional version
/// and path (defaulting to the latest version and `index.js`). The contract ABI will be /// and path (defaulting to the latest version and `index.js`). The contract ABI will be
/// retrieved through `unpkg.io`. /// retrieved through `unpkg.io`.
@ -99,6 +107,12 @@ impl Source {
match url.scheme() { match url.scheme() {
"file" => Ok(Source::local(source)), "file" => Ok(Source::local(source)),
"http" | "https" => match url.host_str() { "http" | "https" => match url.host_str() {
Some("bscscan.com") => Source::etherscan(
url.path()
.rsplit('/')
.next()
.ok_or_else(|| eyre!("HTTP URL does not have a path"))?,
),
Some("etherscan.io") => Source::etherscan( Some("etherscan.io") => Source::etherscan(
url.path() url.path()
.rsplit('/') .rsplit('/')
@ -119,6 +133,7 @@ impl Source {
), ),
_ => Ok(Source::Http(url)), _ => Ok(Source::Http(url)),
}, },
"bscscan" => Source::bscscan(url.path()),
"etherscan" => Source::etherscan(url.path()), "etherscan" => Source::etherscan(url.path()),
"polygonscan" => Source::polygonscan(url.path()), "polygonscan" => Source::polygonscan(url.path()),
"snowtrace" => Source::snowtrace(url.path()), "snowtrace" => Source::snowtrace(url.path()),
@ -140,6 +155,16 @@ impl Source {
Ok(Source::Http(Url::parse(url.as_ref())?)) Ok(Source::Http(Url::parse(url.as_ref())?))
} }
/// Creates an Bscscan source from an address string.
pub fn bscscan<S>(address: S) -> Result<Self>
where
S: AsRef<str>,
{
let address =
util::parse_address(address).context("failed to parse address for Bscscan source")?;
Ok(Source::Bscscan(address))
}
/// Creates an Etherscan source from an address string. /// Creates an Etherscan source from an address string.
pub fn etherscan<S>(address: S) -> Result<Self> pub fn etherscan<S>(address: S) -> Result<Self>
where where
@ -187,6 +212,7 @@ impl Source {
match self { match self {
Source::Local(path) => get_local_contract(path), Source::Local(path) => get_local_contract(path),
Source::Http(_) => panic!("Http abi location are not supported for wasm"), Source::Http(_) => panic!("Http abi location are not supported for wasm"),
Source::Bscscan(_) => panic!("Bscscan abi location are not supported for wasm"),
Source::Etherscan(_) => panic!("Etherscan abi location are not supported for wasm"), Source::Etherscan(_) => panic!("Etherscan abi location are not supported for wasm"),
Source::Polygonscan(_) => panic!("Polygonscan abi location are not supported for wasm"), Source::Polygonscan(_) => panic!("Polygonscan abi location are not supported for wasm"),
Source::Snowtrace(_) => panic!("Snowtrace abi location are not supported for wasm"), Source::Snowtrace(_) => panic!("Snowtrace abi location are not supported for wasm"),
@ -197,6 +223,7 @@ impl Source {
match self { match self {
Source::Local(path) => get_local_contract(path), Source::Local(path) => get_local_contract(path),
Source::Http(url) => get_http_contract(url), Source::Http(url) => get_http_contract(url),
Source::Bscscan(address) => get_etherscan_contract(*address, "bscscan.com"),
Source::Etherscan(address) => get_etherscan_contract(*address, "etherscan.io"), Source::Etherscan(address) => get_etherscan_contract(*address, "etherscan.io"),
Source::Polygonscan(address) => get_etherscan_contract(*address, "polygonscan.com"), Source::Polygonscan(address) => get_etherscan_contract(*address, "polygonscan.com"),
Source::Snowtrace(address) => get_etherscan_contract(*address, "snowtrace.io"), Source::Snowtrace(address) => get_etherscan_contract(*address, "snowtrace.io"),
@ -261,6 +288,7 @@ fn get_etherscan_contract(address: Address, domain: &str) -> Result<String> {
// probably don't reference anything when deploying on other networks. // probably don't reference anything when deploying on other networks.
let api_key = { let api_key = {
let key_res = match domain { let key_res = match domain {
"bscscan.com" => env::var("BSCSCAN_API_KEY").ok(),
"etherscan.io" => env::var("ETHERSCAN_API_KEY").ok(), "etherscan.io" => env::var("ETHERSCAN_API_KEY").ok(),
"polygonscan.com" => env::var("POLYGONSCAN_API_KEY").ok(), "polygonscan.com" => env::var("POLYGONSCAN_API_KEY").ok(),
"snowtrace.io" => env::var("SNOWTRACE_API_KEY").ok(), "snowtrace.io" => env::var("SNOWTRACE_API_KEY").ok(),
@ -311,6 +339,10 @@ mod tests {
"https://my.domain.eth/path/to/Contract.json", "https://my.domain.eth/path/to/Contract.json",
Source::http("https://my.domain.eth/path/to/Contract.json").unwrap(), Source::http("https://my.domain.eth/path/to/Contract.json").unwrap(),
), ),
(
"bscscan:0x0001020304050607080910111213141516171819",
Source::bscscan("0x0001020304050607080910111213141516171819").unwrap(),
),
( (
"etherscan:0x0001020304050607080910111213141516171819", "etherscan:0x0001020304050607080910111213141516171819",
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(), Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
@ -323,6 +355,10 @@ mod tests {
"snowtrace:0x0001020304050607080910111213141516171819", "snowtrace:0x0001020304050607080910111213141516171819",
Source::snowtrace("0x0001020304050607080910111213141516171819").unwrap(), Source::snowtrace("0x0001020304050607080910111213141516171819").unwrap(),
), ),
(
"https://bscscan.io/address/0x0001020304050607080910111213141516171819",
Source::bscscan("0x0001020304050607080910111213141516171819").unwrap(),
),
( (
"https://etherscan.io/address/0x0001020304050607080910111213141516171819", "https://etherscan.io/address/0x0001020304050607080910111213141516171819",
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(), Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),

View File

@ -1,11 +1,12 @@
[package] [package]
name = "ethers-contract-derive" name = "ethers-contract-derive"
version = "1.0.2" version = "1.0.2"
edition = "2021"
rust-version = "1.64"
authors = [ authors = [
"Nicholas Rodrigues Lordello <nlordell@gmail.com>", "Nicholas Rodrigues Lordello <nlordell@gmail.com>",
"Georgios Konstantopoulos <me@gakonst.com>", "Georgios Konstantopoulos <me@gakonst.com>",
] ]
edition = "2018"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Proc macro for type-safe bindings generation to Ethereum and Celo smart contracts" description = "Proc macro for type-safe bindings generation to Ethereum and Celo smart contracts"
homepage = "https://docs.rs/ethers" homepage = "https://docs.rs/ethers"
@ -17,7 +18,7 @@ proc-macro = true
[dependencies] [dependencies]
ethers-core = { version = "^1.0.0", path = "../../ethers-core" } ethers-core = { version = "^1.0.0", path = "../../ethers-core" }
ethers-contract-abigen = { version = "^1.0.0", path = "../ethers-contract-abigen" } ethers-contract-abigen = { version = "^1.0.0", path = "../ethers-contract-abigen", default-features = false }
serde_json = "1.0.53" serde_json = "1.0.53"
hex = { version = "0.4.3", default-features = false, features = ["std"] } hex = { version = "0.4.3", default-features = false, features = ["std"] }

View File

@ -437,6 +437,27 @@ impl<M: Middleware> Multicall<M> {
} }
} }
/// Appends multiple `call`s to the list of calls of the Multicall instance.
///
/// Version specific details:
/// - 1: `allow_failure` is ignored.
/// - >=2: `allow_failure` specifies whether or not this call is allowed to revert in the
/// multicall.
/// - 3: Transaction values are used when broadcasting transactions with [`send`], otherwise
/// they are always ignored.
///
/// [`send`]: #method.send
pub fn add_calls<D: Detokenize>(
&mut self,
allow_failure: bool,
calls: impl IntoIterator<Item = ContractCall<M, D>>,
) -> &mut Self {
for call in calls {
self.add_call(call, allow_failure);
}
self
}
/// Appends a `call` to the list of calls of the Multicall instance for querying the block hash /// Appends a `call` to the list of calls of the Multicall instance for querying the block hash
/// of a given block number. /// of a given block number.
/// ///
@ -615,6 +636,45 @@ impl<M: Middleware> Multicall<M> {
Ok(data) Ok(data)
} }
/// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming
/// that every call returns same data type.
///
/// Note: this method _does not_ send a transaction from your account.
///
/// # Errors
///
/// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
/// the tokens back to the expected return type.
///
/// # Examples
///
/// The return type must be annotated while calling this method:
///
/// ```no_run
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// # use ethers_core::types::{U256, Address};
/// # use ethers_providers::{Provider, Http};
/// # use ethers_contract::Multicall;
/// # use std::convert::TryFrom;
/// #
/// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
/// #
/// # let multicall = Multicall::new(client, None).await?;
/// // If the all Solidity function calls `returns (uint256)`:
/// let result: Vec<U256> = multicall.call_array().await?;
/// # Ok(())
/// # }
/// ```
pub async fn call_array<D: Detokenize>(&self) -> Result<Vec<D>, M> {
let tokens = self.call_raw().await?;
let res: std::result::Result<Vec<D>, ContractError<M>> = tokens
.into_iter()
.map(|token| D::from_tokens(vec![token]).map_err(ContractError::DetokenizationError))
.collect();
Ok(res?)
}
/// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract and /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract and
/// without detokenization. /// without detokenization.
/// ///

View File

@ -15,7 +15,7 @@ mod eth_tests {
use ethers_derive_eip712::*; use ethers_derive_eip712::*;
use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt}; use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt};
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use std::{convert::TryFrom, sync::Arc, time::Duration}; use std::{convert::TryFrom, iter::FromIterator, sync::Arc, time::Duration};
#[tokio::test] #[tokio::test]
async fn deploy_and_call_contract() { async fn deploy_and_call_contract() {
@ -517,10 +517,26 @@ mod eth_tests {
.add_get_eth_balance(addrs[5], false) .add_get_eth_balance(addrs[5], false)
.add_get_eth_balance(addrs[6], false); .add_get_eth_balance(addrs[6], false);
let valid_balances = [
U256::from(10_000_000_000_000_000_000_000u128),
U256::from(10_000_000_000_000_000_000_000u128),
U256::from(10_000_000_000_000_000_000_000u128),
];
let balances: (U256, U256, U256) = multicall.call().await.unwrap(); let balances: (U256, U256, U256) = multicall.call().await.unwrap();
assert_eq!(balances.0, U256::from(10_000_000_000_000_000_000_000u128)); assert_eq!(balances.0, valid_balances[0]);
assert_eq!(balances.1, U256::from(10_000_000_000_000_000_000_000u128)); assert_eq!(balances.1, valid_balances[1]);
assert_eq!(balances.2, U256::from(10_000_000_000_000_000_000_000u128)); assert_eq!(balances.2, valid_balances[2]);
// call_array
multicall
.clear_calls()
.add_get_eth_balance(addrs[4], false)
.add_get_eth_balance(addrs[5], false)
.add_get_eth_balance(addrs[6], false);
let balances: Vec<U256> = multicall.call_array().await.unwrap();
assert_eq!(balances, Vec::from_iter(valid_balances.iter().copied()));
// clear multicall so we can test `call_raw` w/ >16 calls // clear multicall so we can test `call_raw` w/ >16 calls
multicall.clear_calls(); multicall.clear_calls();
@ -536,10 +552,11 @@ mod eth_tests {
.unwrap(); .unwrap();
// build up a list of calls greater than the 16 max restriction // build up a list of calls greater than the 16 max restriction
for i in 0..=16 { multicall.add_calls(
let call = simple_contract.method::<_, String>("getValue", ()).unwrap(); false,
multicall.add_call(call, false); std::iter::repeat(simple_contract.method::<_, String>("getValue", ()).unwrap())
} .take(17), // .collect(),
);
// must use `call_raw` as `.calls` > 16 // must use `call_raw` as `.calls` > 16
let tokens = multicall.call_raw().await.unwrap(); let tokens = multicall.call_raw().await.unwrap();

View File

@ -1,10 +1,10 @@
[package] [package]
name = "ethers-core" name = "ethers-core"
license = "MIT OR Apache-2.0"
version = "1.0.2" version = "1.0.2"
edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"] authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018" license = "MIT OR Apache-2.0"
description = "Core structures for the ethers-rs crate" description = "Core structures for the ethers-rs crate"
homepage = "https://docs.rs/ethers" homepage = "https://docs.rs/ethers"
repository = "https://github.com/gakonst/ethers-rs" repository = "https://github.com/gakonst/ethers-rs"
@ -31,7 +31,7 @@ serde_json = { version = "1.0.64", default-features = false, features = ["arbitr
thiserror = { version = "1.0", default-features = false } thiserror = { version = "1.0", default-features = false }
bytes = { version = "1.3.0", features = ["serde"] } bytes = { version = "1.3.0", features = ["serde"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] } hex = { version = "0.4.3", default-features = false, features = ["std"] }
once_cell = { version = "1.16.0", optional = true } once_cell = { version = "1.17.0", optional = true }
unicode-xid = "0.2.4" unicode-xid = "0.2.4"
strum = { version = "0.24", features = ["derive"] } strum = { version = "0.24", features = ["derive"] }
@ -40,14 +40,15 @@ cargo_metadata = { version = "0.15.2", optional = true }
# eip712 feature enabled dependencies # eip712 feature enabled dependencies
convert_case = { version = "0.6.0", optional = true } convert_case = { version = "0.6.0", optional = true }
syn = { version = "1.0.105", optional = true } syn = { version = "1.0.107", optional = true }
proc-macro2 = { version = "1.0.47", optional = true } proc-macro2 = { version = "1.0.49", optional = true }
num_enum = "0.5.7"
[dev-dependencies] [dev-dependencies]
tempfile = { version = "3.3.0", default-features = false } tempfile = { version = "3.3.0", default-features = false }
serde_json = { version = "1.0.64", default-features = false } serde_json = { version = "1.0.64", default-features = false }
bincode = { version = "1.3.3", default-features = false } bincode = { version = "1.3.3", default-features = false }
once_cell = { version = "1.16.0" } once_cell = { version = "1.17.0" }
hex-literal = "0.3.4" hex-literal = "0.3.4"
rand = "0.8.5" rand = "0.8.5"

View File

@ -1,7 +1,8 @@
[package] [package]
name = "ethers-derive-eip712" name = "ethers-derive-eip712"
version = "1.0.2" version = "1.0.2"
edition = "2018" edition = "2021"
rust-version = "1.64"
description = "Custom derive macro for EIP-712 typed data" description = "Custom derive macro for EIP-712 typed data"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -11,7 +12,10 @@ proc-macro = true
[dependencies] [dependencies]
quote = "1.0.9" quote = "1.0.9"
syn = "1.0.77" syn = "1.0.77"
ethers-core = { version = "^1.0.0", path = "../", default-features = false, features = ["eip712", "macros"] } ethers-core = { version = "^1.0.0", path = "../", default-features = false, features = [
"eip712",
"macros",
] }
hex = "0.4.3" hex = "0.4.3"
serde_json = "1.0.68" serde_json = "1.0.68"

View File

@ -63,7 +63,7 @@ pub fn determine_ethers_crates() -> (&'static str, &'static str, &'static str) {
let needs_lock_file_cleanup = !std::path::Path::new(&lock_file).exists(); let needs_lock_file_cleanup = !std::path::Path::new(&lock_file).exists();
let res = MetadataCommand::new() let res = MetadataCommand::new()
.manifest_path(&format!("{manifest_dir}/Cargo.toml")) .manifest_path(format!("{manifest_dir}/Cargo.toml"))
.exec() .exec()
.ok() .ok()
.and_then(|metadata| { .and_then(|metadata| {

View File

@ -1,138 +1,160 @@
use super::U256; use super::{U128, U256, U512, U64};
use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use std::{ use std::{
convert::{TryFrom, TryInto}, convert::{TryFrom, TryInto},
fmt, fmt,
str::FromStr,
time::Duration, time::Duration,
}; };
use strum::EnumVariantNames; use strum::{AsRefStr, EnumString, EnumVariantNames};
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[error("Failed to parse chain: {0}")]
pub struct ParseChainError(String);
// When adding a new chain: // When adding a new chain:
// 1. add new variant to the Chain enum; // 1. add new variant to the Chain enum;
// 2. update Display/FromStr impl; // 2. add extra information in the last `impl` block (explorer URLs, block time) when applicable;
// 3. add etherscan_keys if supported. // 3. (optional) add aliases: `#[strum(serialize = "main", serialize = "alias", ...)]`;
// "main" must be present and will be used in `Display`, `Serialize` and `FromStr`,
// while the aliases will be added only to `FromStr`.
/// Enum for all known chains. /// An Ethereum EIP-155 chain.
#[repr(u64)] #[derive(
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Deserialize, EnumVariantNames)] Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
AsRefStr, // also for fmt::Display and serde::Serialize
EnumVariantNames, // Self::VARIANTS
EnumString, // FromStr, TryFrom<&str>
TryFromPrimitive, // TryFrom<u64>
Deserialize,
)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
#[repr(u64)]
pub enum Chain { pub enum Chain {
#[default]
Mainnet = 1, Mainnet = 1,
Morden = 2, Morden = 2,
Ropsten = 3, Ropsten = 3,
Rinkeby = 4, Rinkeby = 4,
Goerli = 5, Goerli = 5,
Optimism = 10,
Cronos = 25,
Rsk = 30,
Kovan = 42, Kovan = 42,
#[strum(serialize = "bsc")] Sepolia = 11155111,
BinanceSmartChain = 56,
Optimism = 10,
OptimismKovan = 69, OptimismKovan = 69,
Sokol = 77,
#[strum(serialize = "bsc-testnet")]
BinanceSmartChainTestnet = 97,
Poa = 99,
#[strum(serialize = "gnosis")]
XDai = 100,
Polygon = 137,
Fantom = 250,
CronosTestnet = 338,
OptimismGoerli = 420, OptimismGoerli = 420,
MoonbeamDev = 1281,
Moonbeam = 1284,
Moonriver = 1285,
Moonbase = 1287,
Dev = 1337,
FantomTestnet = 4002,
EvmosTestnet = 9000,
Evmos = 9001,
Chiado = 10200,
Oasis = 26863,
AnvilHardhat = 31337,
Arbitrum = 42161, Arbitrum = 42161,
EmeraldTestnet = 42261,
Emerald = 42262,
AvalancheFuji = 43113,
Avalanche = 43114,
PolygonMumbai = 80001,
ArbitrumTestnet = 421611, ArbitrumTestnet = 421611,
ArbitrumGoerli = 421613, ArbitrumGoerli = 421613,
Sepolia = 11155111,
Aurora = 1313161554, Cronos = 25,
AuroraTestnet = 1313161555, CronosTestnet = 338,
Rsk = 30,
#[strum(serialize = "bsc")]
BinanceSmartChain = 56,
#[strum(serialize = "bsc-testnet")]
BinanceSmartChainTestnet = 97,
Poa = 99,
Sokol = 77,
#[strum(serialize = "gnosis", serialize = "xdai", serialize = "gnosis-chain")]
XDai = 100,
Polygon = 137,
#[strum(serialize = "mumbai", serialize = "polygon-mumbai")]
PolygonMumbai = 80001,
Fantom = 250,
FantomTestnet = 4002,
Moonbeam = 1284,
MoonbeamDev = 1281,
Moonriver = 1285,
Moonbase = 1287,
#[strum(serialize = "dev")]
Dev = 1337,
#[strum(serialize = "anvil-hardhat", serialize = "anvil", serialize = "hardhat")]
AnvilHardhat = 31337,
Evmos = 9001,
EvmosTestnet = 9000,
Chiado = 10200,
Oasis = 26863,
Emerald = 42262,
EmeraldTestnet = 42261,
Avalanche = 43114,
#[strum(serialize = "fuji", serialize = "avalanche-fuji")]
AvalancheFuji = 43113,
Celo = 42220, Celo = 42220,
CeloAlfajores = 44787, CeloAlfajores = 44787,
CeloBaklava = 62320, CeloBaklava = 62320,
Aurora = 1313161554,
AuroraTestnet = 1313161555,
} }
// === impl Chain === // === impl Chain ===
impl fmt::Display for Chain { // This must be implemented manually so we avoid a conflict with `TryFromPrimitive` where it treats
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // the `#[default]` attribute as its own `#[num_enum(default)]`
let chain = match self { impl Default for Chain {
Chain::Mainnet => "mainnet", fn default() -> Self {
Chain::Morden => "morden", Self::Mainnet
Chain::Ropsten => "ropsten",
Chain::Rinkeby => "rinkeby",
Chain::Goerli => "goerli",
Chain::Kovan => "kovan",
Chain::XDai => "gnosis",
Chain::Chiado => "chiado",
Chain::Polygon => "polygon",
Chain::PolygonMumbai => "mumbai",
Chain::Avalanche => "avalanche",
Chain::AvalancheFuji => "fuji",
Chain::Sepolia => "sepolia",
Chain::Moonbeam => "moonbeam",
Chain::Moonbase => "moonbase",
Chain::MoonbeamDev => "moonbeam-dev",
Chain::Moonriver => "moonriver",
Chain::Optimism => "optimism",
Chain::OptimismGoerli => "optimism-goerli",
Chain::OptimismKovan => "optimism-kovan",
Chain::Fantom => "fantom",
Chain::Dev => "dev",
Chain::FantomTestnet => "fantom-testnet",
Chain::BinanceSmartChain => "bsc",
Chain::BinanceSmartChainTestnet => "bsc-testnet",
Chain::Arbitrum => "arbitrum",
Chain::ArbitrumTestnet => "arbitrum-testnet",
Chain::ArbitrumGoerli => "arbitrum-goerli",
Chain::Cronos => "cronos",
Chain::CronosTestnet => "cronos-testnet",
Chain::Poa => "poa",
Chain::Sokol => "sokol",
Chain::Rsk => "rsk",
Chain::Oasis => "oasis",
Chain::Emerald => "emerald",
Chain::EmeraldTestnet => "emerald-testnet",
Chain::AnvilHardhat => "anvil-hardhat",
Chain::Evmos => "evmos",
Chain::EvmosTestnet => "evmos-testnet",
Chain::Aurora => "aurora",
Chain::AuroraTestnet => "aurora-testnet",
Chain::Celo => "celo",
Chain::CeloAlfajores => "celo-alfajores",
Chain::CeloBaklava => "celo-baklava",
};
f.pad(chain)
} }
} }
impl From<Chain> for u32 { macro_rules! impl_into_numeric {
fn from(chain: Chain) -> Self { ($($ty:ty)+) => {$(
chain as u32 impl From<Chain> for $ty {
} fn from(chain: Chain) -> Self {
u64::from(chain).into()
}
}
)+};
}
macro_rules! impl_try_from_numeric {
($($native:ty)+ ; $($primitive:ty)*) => {
$(
impl TryFrom<$native> for Chain {
type Error = TryFromPrimitiveError<Self>;
fn try_from(value: $native) -> Result<Self, Self::Error> {
(value as u64).try_into()
}
}
)+
$(
impl TryFrom<$primitive> for Chain {
type Error = TryFromPrimitiveError<Self>;
fn try_from(value: $primitive) -> Result<Self, Self::Error> {
if value.bits() > 64 {
// `TryFromPrimitiveError` only has a `number` field which has the same type
// as the `#[repr(_)]` attribute on the enum.
return Err(TryFromPrimitiveError { number: value.low_u64() })
}
value.low_u64().try_into()
}
}
)*
};
} }
impl From<Chain> for u64 { impl From<Chain> for u64 {
@ -141,134 +163,21 @@ impl From<Chain> for u64 {
} }
} }
impl From<Chain> for U256 { impl_into_numeric!(u128 U64 U128 U256 U512);
fn from(chain: Chain) -> Self {
u64::from(chain).into() impl TryFrom<U64> for Chain {
type Error = TryFromPrimitiveError<Self>;
fn try_from(value: U64) -> Result<Self, Self::Error> {
value.low_u64().try_into()
} }
} }
impl TryFrom<u32> for Chain { impl_try_from_numeric!(u8 u16 u32 usize; U128 U256 U512);
type Error = ParseChainError;
fn try_from(chain: u32) -> Result<Chain, Self::Error> { impl fmt::Display for Chain {
(chain as u64).try_into() fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
} f.pad(self.as_ref())
}
impl TryFrom<u64> for Chain {
type Error = ParseChainError;
fn try_from(chain: u64) -> Result<Chain, Self::Error> {
Ok(match chain {
1 => Chain::Mainnet,
2 => Chain::Morden,
3 => Chain::Ropsten,
4 => Chain::Rinkeby,
5 => Chain::Goerli,
42 => Chain::Kovan,
100 => Chain::XDai,
10200 => Chain::Chiado,
137 => Chain::Polygon,
1337 => Chain::Dev,
31337 => Chain::AnvilHardhat,
250 => Chain::Fantom,
4002 => Chain::FantomTestnet,
80001 => Chain::PolygonMumbai,
43114 => Chain::Avalanche,
43113 => Chain::AvalancheFuji,
11155111 => Chain::Sepolia,
1284 => Chain::Moonbeam,
1287 => Chain::Moonbase,
1281 => Chain::MoonbeamDev,
1285 => Chain::Moonriver,
10 => Chain::Optimism,
420 => Chain::OptimismGoerli,
69 => Chain::OptimismKovan,
56 => Chain::BinanceSmartChain,
97 => Chain::BinanceSmartChainTestnet,
42161 => Chain::Arbitrum,
421611 => Chain::ArbitrumTestnet,
421613 => Chain::ArbitrumGoerli,
25 => Chain::Cronos,
338 => Chain::CronosTestnet,
99 => Chain::Poa,
77 => Chain::Sokol,
30 => Chain::Rsk,
26863 => Chain::Oasis,
42262 => Chain::Emerald,
42261 => Chain::EmeraldTestnet,
9001 => Chain::Evmos,
9000 => Chain::EvmosTestnet,
1313161554 => Chain::Aurora,
1313161555 => Chain::AuroraTestnet,
42220 => Chain::Celo,
44787 => Chain::CeloAlfajores,
62320 => Chain::CeloBaklava,
_ => return Err(ParseChainError(chain.to_string())),
})
}
}
impl TryFrom<U256> for Chain {
type Error = ParseChainError;
fn try_from(chain: U256) -> Result<Chain, Self::Error> {
if chain.bits() > 64 {
return Err(ParseChainError(chain.to_string()))
}
chain.as_u64().try_into()
}
}
impl FromStr for Chain {
type Err = ParseChainError;
fn from_str(chain: &str) -> Result<Self, Self::Err> {
Ok(match chain {
"mainnet" => Chain::Mainnet,
"morden" => Chain::Morden,
"ropsten" => Chain::Ropsten,
"rinkeby" => Chain::Rinkeby,
"goerli" => Chain::Goerli,
"kovan" => Chain::Kovan,
"xdai" | "gnosis" | "gnosis-chain" => Chain::XDai,
"chiado" => Chain::Chiado,
"polygon" => Chain::Polygon,
"mumbai" | "polygon-mumbai" => Chain::PolygonMumbai,
"avalanche" => Chain::Avalanche,
"fuji" | "avalanche-fuji" => Chain::AvalancheFuji,
"sepolia" => Chain::Sepolia,
"moonbeam" => Chain::Moonbeam,
"moonbase" => Chain::Moonbase,
"moonbeam-dev" => Chain::MoonbeamDev,
"moonriver" => Chain::Moonriver,
"optimism" => Chain::Optimism,
"optimism-goerli" => Chain::OptimismGoerli,
"optimism-kovan" => Chain::OptimismKovan,
"fantom" => Chain::Fantom,
"fantom-testnet" => Chain::FantomTestnet,
"dev" => Chain::Dev,
"anvil" | "hardhat" | "anvil-hardhat" => Chain::AnvilHardhat,
"bsc" => Chain::BinanceSmartChain,
"bsc-testnet" => Chain::BinanceSmartChainTestnet,
"arbitrum" => Chain::Arbitrum,
"arbitrum-testnet" => Chain::ArbitrumTestnet,
"arbitrum-goerli" => Chain::ArbitrumGoerli,
"cronos" => Chain::Cronos,
"cronos-testnet" => Chain::CronosTestnet,
"poa" => Chain::Poa,
"sokol" => Chain::Sokol,
"rsk" => Chain::Rsk,
"oasis" => Chain::Oasis,
"emerald" => Chain::Emerald,
"emerald-testnet" => Chain::EmeraldTestnet,
"aurora" => Chain::Aurora,
"aurora-testnet" => Chain::AuroraTestnet,
"celo" => Chain::Celo,
"celo-alfajores" => Chain::CeloAlfajores,
"celo-baklava" => Chain::CeloBaklava,
_ => return Err(ParseChainError(chain.to_owned())),
})
} }
} }
@ -277,163 +186,135 @@ impl Serialize for Chain {
where where
S: Serializer, S: Serializer,
{ {
s.serialize_str(self.to_string().as_ref()) s.serialize_str(self.as_ref())
} }
} }
// NB: all utility functions *should* be explicitly exhaustive (not use `_` matcher) so we don't
// forget to update them when adding a new `Chain` variant.
impl Chain { impl Chain {
/// The blocktime varies from chain to chain. /// Returns the chain's average blocktime, if applicable.
/// ///
/// It can be beneficial to know the average blocktime to adjust the polling of an HTTP provider /// It can be beneficial to know the average blocktime to adjust the polling of an HTTP provider
/// for example. /// for example.
/// ///
/// **Note:** this will not return the accurate average depending on the time but is rather a /// **Note:** this is not an accurate average, but is rather a sensible default derived from
/// sensible default derived from blocktime charts like <https://etherscan.com/chart/blocktime> /// blocktime charts such as [Etherscan's](https://etherscan.com/chart/blocktime)
/// <https://polygonscan.com/chart/blocktime> /// or [Polygonscan's](https://polygonscan.com/chart/blocktime).
pub fn average_blocktime_hint(&self) -> Option<Duration> { pub const fn average_blocktime_hint(&self) -> Option<Duration> {
use Chain::*;
let ms = match self { let ms = match self {
Chain::Arbitrum | Chain::ArbitrumTestnet | Chain::ArbitrumGoerli => 1_300, Arbitrum | ArbitrumTestnet | ArbitrumGoerli => 1_300,
Chain::Mainnet | Chain::Optimism => 13_000, Mainnet | Optimism => 13_000,
Chain::Polygon | Chain::PolygonMumbai => 2_100, Polygon | PolygonMumbai => 2_100,
Chain::Moonbeam | Chain::Moonriver => 12_500, Moonbeam | Moonriver => 12_500,
Chain::BinanceSmartChain | Chain::BinanceSmartChainTestnet => 3_000, BinanceSmartChain | BinanceSmartChainTestnet => 3_000,
Chain::Avalanche | Chain::AvalancheFuji => 2_000, Avalanche | AvalancheFuji => 2_000,
Chain::Fantom | Chain::FantomTestnet => 1_200, Fantom | FantomTestnet => 1_200,
Chain::Cronos | Chain::CronosTestnet => 5_700, Cronos | CronosTestnet => 5_700,
Chain::Evmos | Chain::EvmosTestnet => 1_900, Evmos | EvmosTestnet => 1_900,
Chain::Aurora | Chain::AuroraTestnet => 1_100, Aurora | AuroraTestnet => 1_100,
Chain::Oasis => 5_500, Oasis => 5_500,
Chain::Emerald => 6_000, Emerald => 6_000,
Chain::Dev | Chain::AnvilHardhat => 200, Dev | AnvilHardhat => 200,
Chain::Celo | Chain::CeloAlfajores | Chain::CeloBaklava => 5_000, Celo | CeloAlfajores | CeloBaklava => 5_000,
// Explictly handle all network to make it easier not to forget this match when new // Explictly handle all network to make it easier not to forget this match when new
// networks are added. // networks are added.
Chain::Morden | Morden | Ropsten | Rinkeby | Goerli | Kovan | XDai | Chiado | Sepolia | Moonbase |
Chain::Ropsten | MoonbeamDev | OptimismGoerli | OptimismKovan | Poa | Sokol | Rsk | EmeraldTestnet => {
Chain::Rinkeby | return None
Chain::Goerli | }
Chain::Kovan |
Chain::XDai |
Chain::Chiado |
Chain::Sepolia |
Chain::Moonbase |
Chain::MoonbeamDev |
Chain::OptimismGoerli |
Chain::OptimismKovan |
Chain::Poa |
Chain::Sokol |
Chain::Rsk |
Chain::EmeraldTestnet => return None,
}; };
Some(Duration::from_millis(ms)) Some(Duration::from_millis(ms))
} }
/// Returns the corresponding etherscan URLs /// Returns the chain's blockchain explorer and its API (Etherscan and Etherscan-like) URLs.
/// ///
/// Returns `(API URL, BASE_URL)`, like `("https://api(-chain).etherscan.io/api", "https://etherscan.io")` /// Returns `(API URL, BASE_URL)`, like `("https://api(-chain).etherscan.io/api", "https://etherscan.io")`
pub fn etherscan_urls(&self) -> Option<(&'static str, &'static str)> { pub const fn etherscan_urls(&self) -> Option<(&'static str, &'static str)> {
use Chain::*;
let urls = match self { let urls = match self {
Chain::Mainnet => ("https://api.etherscan.io/api", "https://etherscan.io"), Mainnet => ("https://api.etherscan.io/api", "https://etherscan.io"),
Chain::Ropsten => { Ropsten => ("https://api-ropsten.etherscan.io/api", "https://ropsten.etherscan.io"),
("https://api-ropsten.etherscan.io/api", "https://ropsten.etherscan.io") Kovan => ("https://api-kovan.etherscan.io/api", "https://kovan.etherscan.io"),
} Rinkeby => ("https://api-rinkeby.etherscan.io/api", "https://rinkeby.etherscan.io"),
Chain::Kovan => ("https://api-kovan.etherscan.io/api", "https://kovan.etherscan.io"), Goerli => ("https://api-goerli.etherscan.io/api", "https://goerli.etherscan.io"),
Chain::Rinkeby => { Sepolia => ("https://api-sepolia.etherscan.io/api", "https://sepolia.etherscan.io"),
("https://api-rinkeby.etherscan.io/api", "https://rinkeby.etherscan.io") Polygon => ("https://api.polygonscan.com/api", "https://polygonscan.com"),
} PolygonMumbai => {
Chain::Goerli => ("https://api-goerli.etherscan.io/api", "https://goerli.etherscan.io"),
Chain::Sepolia => {
("https://api-sepolia.etherscan.io/api", "https://sepolia.etherscan.io")
}
Chain::Polygon => ("https://api.polygonscan.com/api", "https://polygonscan.com"),
Chain::PolygonMumbai => {
("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com") ("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com")
} }
Chain::Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"), Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"),
Chain::AvalancheFuji => { AvalancheFuji => {
("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io") ("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io")
} }
Chain::Optimism => { Optimism => {
("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io") ("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io")
} }
Chain::OptimismGoerli => ( OptimismGoerli => (
"https://api-goerli-optimistic.etherscan.io/api", "https://api-goerli-optimistic.etherscan.io/api",
"https://goerli-optimism.etherscan.io", "https://goerli-optimism.etherscan.io",
), ),
Chain::OptimismKovan => ( OptimismKovan => (
"https://api-kovan-optimistic.etherscan.io/api", "https://api-kovan-optimistic.etherscan.io/api",
"https://kovan-optimistic.etherscan.io", "https://kovan-optimistic.etherscan.io",
), ),
Chain::Fantom => ("https://api.ftmscan.com/api", "https://ftmscan.com"), Fantom => ("https://api.ftmscan.com/api", "https://ftmscan.com"),
Chain::FantomTestnet => { FantomTestnet => ("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com"),
("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com") BinanceSmartChain => ("https://api.bscscan.com/api", "https://bscscan.com"),
} BinanceSmartChainTestnet => {
Chain::BinanceSmartChain => ("https://api.bscscan.com/api", "https://bscscan.com"),
Chain::BinanceSmartChainTestnet => {
("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com") ("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com")
} }
Chain::Arbitrum => ("https://api.arbiscan.io/api", "https://arbiscan.io"), Arbitrum => ("https://api.arbiscan.io/api", "https://arbiscan.io"),
Chain::ArbitrumTestnet => { ArbitrumTestnet => {
("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io") ("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io")
} }
Chain::ArbitrumGoerli => ( ArbitrumGoerli => (
"https://goerli-rollup-explorer.arbitrum.io/api", "https://goerli-rollup-explorer.arbitrum.io/api",
"https://goerli-rollup-explorer.arbitrum.io", "https://goerli-rollup-explorer.arbitrum.io",
), ),
Chain::Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"), Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"),
Chain::CronosTestnet => { CronosTestnet => {
("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com") ("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com")
} }
Chain::Moonbeam => { Moonbeam => ("https://api-moonbeam.moonscan.io/api", "https://moonbeam.moonscan.io/"),
("https://api-moonbeam.moonscan.io/api", "https://moonbeam.moonscan.io/") Moonbase => ("https://api-moonbase.moonscan.io/api", "https://moonbase.moonscan.io/"),
} Moonriver => ("https://api-moonriver.moonscan.io/api", "https://moonriver.moonscan.io"),
Chain::Moonbase => {
("https://api-moonbase.moonscan.io/api", "https://moonbase.moonscan.io/")
}
Chain::Moonriver => {
("https://api-moonriver.moonscan.io/api", "https://moonriver.moonscan.io")
}
// blockscout API is etherscan compatible // blockscout API is etherscan compatible
Chain::XDai => { XDai => {
("https://blockscout.com/xdai/mainnet/api", "https://blockscout.com/xdai/mainnet") ("https://blockscout.com/xdai/mainnet/api", "https://blockscout.com/xdai/mainnet")
} }
Chain::Chiado => { Chiado => {
("https://blockscout.chiadochain.net/api", "https://blockscout.chiadochain.net") ("https://blockscout.chiadochain.net/api", "https://blockscout.chiadochain.net")
} }
Chain::Sokol => { Sokol => ("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol"),
("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol") Poa => ("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core"),
} Rsk => ("https://blockscout.com/rsk/mainnet/api", "https://blockscout.com/rsk/mainnet"),
Chain::Poa => { Oasis => ("https://scan.oasischain.io/api", "https://scan.oasischain.io/"),
("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core") Emerald => {
}
Chain::Rsk => {
("https://blockscout.com/rsk/mainnet/api", "https://blockscout.com/rsk/mainnet")
}
Chain::Oasis => ("https://scan.oasischain.io/api", "https://scan.oasischain.io/"),
Chain::Emerald => {
("https://explorer.emerald.oasis.dev/api", "https://explorer.emerald.oasis.dev/") ("https://explorer.emerald.oasis.dev/api", "https://explorer.emerald.oasis.dev/")
} }
Chain::EmeraldTestnet => ( EmeraldTestnet => (
"https://testnet.explorer.emerald.oasis.dev/api", "https://testnet.explorer.emerald.oasis.dev/api",
"https://testnet.explorer.emerald.oasis.dev/", "https://testnet.explorer.emerald.oasis.dev/",
), ),
Chain::Aurora => ("https://api.aurorascan.dev/api", "https://aurorascan.dev"), Aurora => ("https://api.aurorascan.dev/api", "https://aurorascan.dev"),
Chain::AuroraTestnet => { AuroraTestnet => {
("https://testnet.aurorascan.dev/api", "https://testnet.aurorascan.dev") ("https://testnet.aurorascan.dev/api", "https://testnet.aurorascan.dev")
} }
Chain::Evmos => ("https://evm.evmos.org/api", "https://evm.evmos.org/"), Evmos => ("https://evm.evmos.org/api", "https://evm.evmos.org/"),
Chain::EvmosTestnet => ("https://evm.evmos.dev/api", "https://evm.evmos.dev/"), EvmosTestnet => ("https://evm.evmos.dev/api", "https://evm.evmos.dev/"),
Chain::Celo => { Celo => ("https://explorer.celo.org/mainnet", "https://explorer.celo.org/mainnet/api"),
("https://explorer.celo.org/mainnet", "https://explorer.celo.org/mainnet/api") CeloAlfajores => {
}
Chain::CeloAlfajores => {
("https://explorer.celo.org/alfajores", "https://explorer.celo.org/alfajores/api") ("https://explorer.celo.org/alfajores", "https://explorer.celo.org/alfajores/api")
} }
Chain::CeloBaklava => { CeloBaklava => {
("https://explorer.celo.org/baklava", "https://explorer.celo.org/baklava/api") ("https://explorer.celo.org/baklava", "https://explorer.celo.org/baklava/api")
} }
Chain::AnvilHardhat | Chain::Dev | Chain::Morden | Chain::MoonbeamDev => { AnvilHardhat | Dev | Morden | MoonbeamDev => {
// this is explicitly exhaustive so we don't forget to add new urls when adding a // this is explicitly exhaustive so we don't forget to add new urls when adding a
// new chain // new chain
return None return None
@ -443,34 +324,49 @@ impl Chain {
Some(urls) Some(urls)
} }
/// Helper function for checking if a chainid corresponds to a legacy chainid /// Returns whether the chain implements EIP-1559 (with the type 2 EIP-2718 transaction type).
/// without eip1559 pub const fn is_legacy(&self) -> bool {
pub fn is_legacy(&self) -> bool { use Chain::*;
// TODO: Add other chains which do not support EIP1559.
matches!( match self {
self, // Known legacy chains / non EIP-1559 compliant
Chain::Optimism | Optimism |
Chain::OptimismGoerli | OptimismGoerli |
Chain::OptimismKovan | OptimismKovan |
Chain::Fantom | Fantom |
Chain::FantomTestnet | FantomTestnet |
Chain::BinanceSmartChain | BinanceSmartChain |
Chain::BinanceSmartChainTestnet | BinanceSmartChainTestnet |
Chain::Arbitrum | Arbitrum |
Chain::ArbitrumTestnet | ArbitrumTestnet |
Chain::ArbitrumGoerli | ArbitrumGoerli |
Chain::Rsk | Rsk |
Chain::Oasis | Oasis |
Chain::Emerald | Emerald |
Chain::EmeraldTestnet | EmeraldTestnet |
Chain::Celo | Celo |
Chain::CeloAlfajores | CeloAlfajores |
Chain::CeloBaklava, CeloBaklava => true,
)
// Known EIP-1559 chains
Mainnet | Goerli | Sepolia | Polygon | PolygonMumbai | Avalanche | AvalancheFuji => {
false
}
// Unknown / not applicable, default to false for backwards compatibility
Dev | AnvilHardhat | Morden | Ropsten | Rinkeby | Cronos | CronosTestnet | Kovan |
Sokol | Poa | XDai | Moonbeam | MoonbeamDev | Moonriver | Moonbase | Evmos |
EvmosTestnet | Chiado | Aurora | AuroraTestnet => false,
}
} }
} }
#[test] #[cfg(test)]
fn test_default_chain() { mod tests {
assert_eq!(serde_json::to_string(&Chain::Mainnet).unwrap(), "\"mainnet\""); use super::*;
#[test]
fn test_default_chain() {
assert_eq!(serde_json::to_string(&Chain::default()).unwrap(), "\"mainnet\"");
}
} }

View File

@ -1,4 +1,7 @@
use crate::types::{Bytes, H256, U256}; use crate::{
types::{Bytes, H256, U256},
utils::from_int_or_hex,
};
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -6,7 +9,8 @@ use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GethTrace { pub struct GethTrace {
pub failed: bool, pub failed: bool,
pub gas: u64, #[serde(deserialize_with = "from_int_or_hex")]
pub gas: U256,
#[serde(serialize_with = "serialize_bytes", rename = "returnValue")] #[serde(serialize_with = "serialize_bytes", rename = "returnValue")]
pub return_value: Bytes, pub return_value: Bytes,
#[serde(rename = "structLogs")] #[serde(rename = "structLogs")]
@ -35,6 +39,16 @@ pub struct StructLog {
pub storage: Option<BTreeMap<H256, H256>>, pub storage: Option<BTreeMap<H256, H256>>,
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
/// Available built-in tracers
///
/// See <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers>
pub enum GethDebugTracerType {
/// callTracer (native)
#[serde(rename = "callTracer")]
CallTracer,
}
/// Bindings for additional `debug_traceTransaction` options /// Bindings for additional `debug_traceTransaction` options
/// ///
/// See <https://geth.ethereum.org/docs/rpc/ns-debug#debug_tracetransaction> /// See <https://geth.ethereum.org/docs/rpc/ns-debug#debug_tracetransaction>
@ -50,11 +64,22 @@ pub struct GethDebugTracingOptions {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_return_data: Option<bool>, pub enable_return_data: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub tracer: Option<String>, pub tracer: Option<GethDebugTracerType>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>, pub timeout: Option<String>,
} }
/// Bindings for additional `debug_traceCall` options
///
/// See <https://geth.ethereum.org/docs/rpc/ns-debug#debug_tracecall>
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GethDebugTracingCallOptions {
#[serde(flatten)]
pub tracing_options: GethDebugTracingOptions,
// TODO: Add stateoverrides and blockoverrides options
}
fn serialize_bytes<S, T>(x: T, s: S) -> Result<S::Ok, S::Error> fn serialize_bytes<S, T>(x: T, s: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,

View File

@ -208,7 +208,7 @@ impl Eip1559TransactionRequest {
*offset += 1; *offset += 1;
tx.gas = Some(rlp.val_at(*offset)?); tx.gas = Some(rlp.val_at(*offset)?);
*offset += 1; *offset += 1;
tx.to = decode_to(rlp, offset)?; tx.to = decode_to(rlp, offset)?.map(NameOrAddress::Address);
tx.value = Some(rlp.val_at(*offset)?); tx.value = Some(rlp.val_at(*offset)?);
*offset += 1; *offset += 1;
let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;

View File

@ -75,7 +75,7 @@ fn decode_signature(
fn decode_to( fn decode_to(
rlp: &rlp::Rlp, rlp: &rlp::Rlp,
offset: &mut usize, offset: &mut usize,
) -> Result<Option<super::NameOrAddress>, rlp::DecoderError> { ) -> Result<Option<super::Address>, rlp::DecoderError> {
let to = { let to = {
let to = rlp.at(*offset)?; let to = rlp.at(*offset)?;
if to.is_empty() { if to.is_empty() {

View File

@ -233,7 +233,7 @@ impl TransactionRequest {
*offset += 1; *offset += 1;
} }
txn.to = decode_to(rlp, offset)?; txn.to = decode_to(rlp, offset)?.map(NameOrAddress::Address);
txn.value = Some(rlp.at(*offset)?.as_val()?); txn.value = Some(rlp.at(*offset)?.as_val()?);
*offset += 1; *offset += 1;

View File

@ -1,7 +1,7 @@
//! Transaction types //! Transaction types
use super::{ use super::{
decode_signature, eip2718::TypedTransaction, eip2930::AccessList, normalize_v, rlp_opt, decode_signature, decode_to, eip2718::TypedTransaction, eip2930::AccessList, normalize_v,
rlp_opt_list, rlp_opt, rlp_opt_list,
}; };
use crate::{ use crate::{
types::{ types::{
@ -248,8 +248,7 @@ impl Transaction {
*offset += 1; *offset += 1;
self.gas = rlp.val_at(*offset)?; self.gas = rlp.val_at(*offset)?;
*offset += 1; *offset += 1;
self.to = Some(rlp.val_at(*offset)?); self.to = decode_to(rlp, offset)?;
*offset += 1;
self.value = rlp.val_at(*offset)?; self.value = rlp.val_at(*offset)?;
*offset += 1; *offset += 1;
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@ -279,8 +278,7 @@ impl Transaction {
#[cfg(feature = "celo")] #[cfg(feature = "celo")]
self.decode_celo_metadata(rlp, offset)?; self.decode_celo_metadata(rlp, offset)?;
self.to = Some(rlp.val_at(*offset)?); self.to = decode_to(rlp, offset)?;
*offset += 1;
self.value = rlp.val_at(*offset)?; self.value = rlp.val_at(*offset)?;
*offset += 1; *offset += 1;
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@ -310,8 +308,7 @@ impl Transaction {
#[cfg(feature = "celo")] #[cfg(feature = "celo")]
self.decode_celo_metadata(rlp, offset)?; self.decode_celo_metadata(rlp, offset)?;
self.to = Some(rlp.val_at(*offset)?); self.to = decode_to(rlp, offset)?;
*offset += 1;
self.value = rlp.val_at(*offset)?; self.value = rlp.val_at(*offset)?;
*offset += 1; *offset += 1;
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@ -473,7 +470,7 @@ impl PartialOrd<Self> for TransactionReceipt {
#[cfg(test)] #[cfg(test)]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
mod tests { mod tests {
use rlp::Encodable; use rlp::{Encodable, Rlp};
use crate::types::transaction::eip2930::AccessListItem; use crate::types::transaction::eip2930::AccessListItem;
@ -1093,4 +1090,24 @@ mod tests {
let r = rlp::Rlp::new(b.as_slice()); let r = rlp::Rlp::new(b.as_slice());
Transaction::decode(&r).unwrap(); Transaction::decode(&r).unwrap();
} }
#[test]
fn test_rlp_decoding_create_roundtrip() {
let tx = Transaction {
block_hash: None,
block_number: None,
from: Address::from_str("c26ad91f4e7a0cad84c4b9315f420ca9217e315d").unwrap(),
gas: U256::from_str_radix("0x10e2b", 16).unwrap(),
gas_price: Some(U256::from_str_radix("0x12ec276caf", 16).unwrap()),
hash: H256::from_str("929ff27a5c7833953df23103c4eb55ebdfb698678139d751c51932163877fada").unwrap(),
input: Bytes::from(
hex::decode("a9059cbb000000000000000000000000fdae129ecc2c27d166a3131098bc05d143fa258e0000000000000000000000000000000000000000000000000000000002faf080").unwrap()
),
nonce: U256::zero(),
transaction_index: None,
value: U256::zero(),
..Default::default()
};
Transaction::decode(&Rlp::new(&tx.rlp())).unwrap();
}
} }

View File

@ -164,6 +164,7 @@ impl Default for PrivateNetOptions {
/// ``` /// ```
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Geth { pub struct Geth {
program: Option<PathBuf>,
port: Option<u16>, port: Option<u16>,
authrpc_port: Option<u16>, authrpc_port: Option<u16>,
ipc_path: Option<PathBuf>, ipc_path: Option<PathBuf>,
@ -180,6 +181,32 @@ impl Geth {
Self::default() Self::default()
} }
/// Creates a Geth builder which will execute `geth` at the given path.
///
/// # Example
///
/// ```
/// use ethers_core::utils::Geth;
/// # fn a() {
/// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn();
///
/// println!("Geth running at `{}`", geth.endpoint());
/// # }
/// ```
pub fn at(path: impl Into<PathBuf>) -> Self {
Self::new().path(path)
}
/// Sets the `path` to the `geth` executable
///
/// By default, it's expected that `geth` is in `$PATH`, see also
/// [`std::process::Command::new()`]
#[must_use]
pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
self.program = Some(path.into());
self
}
/// Sets the port which will be used when the `geth-cli` instance is launched. /// Sets the port which will be used when the `geth-cli` instance is launched.
#[must_use] #[must_use]
pub fn port<T: Into<u16>>(mut self, port: T) -> Self { pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
@ -274,9 +301,10 @@ impl Geth {
/// Consumes the builder and spawns `geth` with stdout redirected /// Consumes the builder and spawns `geth` with stdout redirected
/// to /dev/null. /// to /dev/null.
pub fn spawn(self) -> GethInstance { pub fn spawn(self) -> GethInstance {
let mut cmd = Command::new(GETH); let mut cmd =
if let Some(ref prg) = self.program { Command::new(prg) } else { Command::new(GETH) };
// geth uses stderr for its logs // geth uses stderr for its logs
cmd.stderr(std::process::Stdio::piped()); cmd.stderr(Stdio::piped());
let port = if let Some(port) = self.port { port } else { unused_port() }; let port = if let Some(port) = self.port { port } else { unused_port() };
let authrpc_port = if let Some(port) = self.authrpc_port { port } else { unused_port() }; let authrpc_port = if let Some(port) = self.authrpc_port { port } else { unused_port() };

View File

@ -12,7 +12,7 @@ pub use geth::{Geth, GethInstance};
/// Utilities for working with a `genesis.json` and other chain config structs. /// Utilities for working with a `genesis.json` and other chain config structs.
mod genesis; mod genesis;
pub use genesis::{ChainConfig, Genesis}; pub use genesis::{ChainConfig, CliqueConfig, EthashConfig, Genesis, GenesisAccount};
/// Utilities for launching an anvil instance /// Utilities for launching an anvil instance
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View File

@ -1,13 +1,13 @@
[package] [package]
name = "ethers-etherscan" name = "ethers-etherscan"
version = "1.0.2" version = "1.0.2"
edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = [ authors = [
"Matthias Seitz <matthias.seitz@outlook.de>", "Matthias Seitz <matthias.seitz@outlook.de>",
"Georgios Konstantopoulos <me@gakonst.com>", "Georgios Konstantopoulos <me@gakonst.com>",
] ]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018"
readme = "../README.md" readme = "../README.md"
documentation = "https://docs.rs/ethers" documentation = "https://docs.rs/ethers"
repository = "https://github.com/gakonst/ethers-rs" repository = "https://github.com/gakonst/ethers-rs"
@ -26,7 +26,7 @@ serde_json = { version = "1.0.64", default-features = false }
serde-aux = { version = "4.1.2", default-features = false } serde-aux = { version = "4.1.2", default-features = false }
thiserror = "1.0" thiserror = "1.0"
tracing = "0.1.37" tracing = "0.1.37"
semver = "1.0.14" semver = "1.0.16"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
# NOTE: this enables wasm compatibility for getrandom indirectly # NOTE: this enables wasm compatibility for getrandom indirectly
@ -35,7 +35,7 @@ getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3.0"
tokio = { version = "1.18", features = ["macros", "rt-multi-thread", "time"] } tokio = { version = "1.18", features = ["macros", "rt-multi-thread", "time"] }
serial_test = "0.9.0" serial_test = "0.10.0"
tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] } tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] }
ethers-solc = { version = "^1.0.0", path = "../ethers-solc", default-features = false } ethers-solc = { version = "^1.0.0", path = "../ethers-solc", default-features = false }

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
source_tree::{SourceTree, SourceTreeEntry}, source_tree::{SourceTree, SourceTreeEntry},
utils::{deserialize_address_opt, deserialize_stringified_source_code}, utils::{deserialize_address_opt, deserialize_source_code},
Client, EtherscanError, Response, Result, Client, EtherscanError, Response, Result,
}; };
use ethers_core::{ use ethers_core::{
@ -107,7 +107,7 @@ impl SourceCodeMetadata {
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct Metadata { pub struct Metadata {
/// Includes metadata for compiler settings and language. /// Includes metadata for compiler settings and language.
#[serde(deserialize_with = "deserialize_stringified_source_code")] #[serde(deserialize_with = "deserialize_source_code")]
pub source_code: SourceCodeMetadata, pub source_code: SourceCodeMetadata,
/// The ABI of the contract. /// The ABI of the contract.
@ -148,7 +148,11 @@ pub struct Metadata {
pub proxy: u64, pub proxy: u64,
/// If this contract is a proxy, the address of its implementation. /// If this contract is a proxy, the address of its implementation.
#[serde(deserialize_with = "deserialize_address_opt")] #[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_address_opt"
)]
pub implementation: Option<Address>, pub implementation: Option<Address>,
/// The swarm source of the contract. /// The swarm source of the contract.

View File

@ -36,20 +36,35 @@ pub fn deserialize_address_opt<'de, D: Deserializer<'de>>(
/// Deserializes as JSON: /// Deserializes as JSON:
/// ///
/// `{ "SourceCode": "{{ .. }}", ..}` /// Object: `{ "SourceCode": { language: "Solidity", .. }, ..}`
/// ///
/// or /// or
/// ///
/// `{ "SourceCode": "..", .. }` /// Stringified JSON: `{ "SourceCode": "{{\r\n \"language\": \"Solidity\", ..}}", ..}`
pub fn deserialize_stringified_source_code<'de, D: Deserializer<'de>>( ///
/// or
///
/// Normal source code: `{ "SourceCode": "// SPDX-License-Identifier: ...", .. }`
pub fn deserialize_source_code<'de, D: Deserializer<'de>>(
deserializer: D, deserializer: D,
) -> std::result::Result<SourceCodeMetadata, D::Error> { ) -> std::result::Result<SourceCodeMetadata, D::Error> {
let s = String::deserialize(deserializer)?; #[derive(Deserialize)]
if s.starts_with("{{") && s.ends_with("}}") { #[serde(untagged)]
let s = &s[1..s.len() - 1]; enum SourceCode {
serde_json::from_str(s).map_err(serde::de::Error::custom) String(String), // this must come first
} else { Obj(SourceCodeMetadata),
Ok(SourceCodeMetadata::SourceCode(s)) }
let s = SourceCode::deserialize(deserializer)?;
match s {
SourceCode::String(s) => {
if s.starts_with("{{") && s.ends_with("}}") {
let s = &s[1..s.len() - 1];
serde_json::from_str(s).map_err(serde::de::Error::custom)
} else {
Ok(SourceCodeMetadata::SourceCode(s))
}
}
SourceCode::Obj(obj) => Ok(obj),
} }
} }
@ -108,17 +123,29 @@ mod tests {
} }
#[test] #[test]
fn can_deserialize_stringified_source_code() { fn can_deserialize_source_code() {
#[derive(Deserialize)] #[derive(Deserialize)]
struct Test { struct Test {
#[serde(deserialize_with = "deserialize_stringified_source_code")] #[serde(deserialize_with = "deserialize_source_code")]
source_code: SourceCodeMetadata, source_code: SourceCodeMetadata,
} }
let src = "source code text"; let src = "source code text";
// Normal JSON
let json = r#"{ let json = r#"{
"source_code": "{{ \"language\": \"Solidity\", \"sources\": {\"Contract\": { \"content\": \"source code text\" } } }}" "source_code": { "language": "Solidity", "sources": { "Contract": { "content": "source code text" } } }
}"#;
let de: Test = serde_json::from_str(json).unwrap();
assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
assert_eq!(de.source_code.sources().len(), 1);
assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
#[cfg(feature = "ethers-solc")]
assert!(matches!(de.source_code.settings().unwrap(), None));
// Stringified JSON
let json = r#"{
"source_code": "{{ \"language\": \"Solidity\", \"sources\": { \"Contract\": { \"content\": \"source code text\" } } }}"
}"#; }"#;
let de: Test = serde_json::from_str(json).unwrap(); let de: Test = serde_json::from_str(json).unwrap();
assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity)); assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ethers-middleware" name = "ethers-middleware"
version = "1.0.2" version = "1.0.2"
edition = "2018" edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"] authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -47,12 +47,16 @@ ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-f
"ws", "ws",
"rustls", "rustls",
] } ] }
once_cell = "1.16.0" once_cell = "1.17.0"
ethers-solc = { version = "^1.0.0", path = "../ethers-solc" } ethers-solc = { version = "^1.0.0", path = "../ethers-solc" }
serial_test = "0.9.0" serial_test = "0.10.0"
reqwest = { version = "0.11.13", default-features = false, features = ["json", "rustls"] }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.18", default-features = false, features = ["rt", "macros", "time"] } tokio = { version = "1.18", default-features = false, features = ["rt", "macros", "time"] }
[features] [features]
default = ["rustls"]
celo = ["ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo", "ethers-contract/celo"] celo = ["ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo", "ethers-contract/celo"]
openssl = ["reqwest/native-tls"]
rustls = ["reqwest/rustls-tls"]

View File

@ -23,13 +23,13 @@ use ethers_providers::{Middleware, Provider, Http};
use std::sync::Arc; use std::sync::Arc;
use std::convert::TryFrom; use std::convert::TryFrom;
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{*,gas_oracle::*}; use ethers_middleware::{gas_oracle::{GasOracle, GasNow}, MiddlewareBuilder};
fn builder_example() { fn builder_example() {
let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"; let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
let signer = key.parse::<LocalWallet>().unwrap(); let signer = key.parse::<LocalWallet>().unwrap();
let address = signer.address(); let address = signer.address();
let gas_oracle = EthGasStation::new(None); let gas_oracle = GasNow::new();
let provider = Provider::<Http>::try_from("http://localhost:8545") let provider = Provider::<Http>::try_from("http://localhost:8545")
.unwrap() .unwrap()
@ -58,7 +58,7 @@ fn builder_example_wrap_into() {
.unwrap() .unwrap()
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.wrap_into(|p| SignerMiddleware::new(p, signer)) .wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None))) .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
.wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
} }
``` ```
@ -72,7 +72,7 @@ use ethers_providers::{Provider, Http};
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{ use ethers_middleware::{
gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency}, gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency},
gas_oracle::{GasOracleMiddleware, EthGasStation, GasCategory}, gas_oracle::{GasOracleMiddleware, GasCategory, GasNow},
signer::SignerMiddleware, signer::SignerMiddleware,
nonce_manager::NonceManagerMiddleware, nonce_manager::NonceManagerMiddleware,
}; };
@ -91,8 +91,8 @@ let signer = LocalWallet::new(&mut rand::thread_rng());
let address = signer.address(); let address = signer.address();
let provider = SignerMiddleware::new(provider, signer); let provider = SignerMiddleware::new(provider, signer);
// Use EthGasStation as the gas oracle // Use GasNow as the gas oracle
let gas_oracle = EthGasStation::new(None); let gas_oracle = GasNow::new();
let provider = GasOracleMiddleware::new(provider, gas_oracle); let provider = GasOracleMiddleware::new(provider, gas_oracle);
// Manage nonces locally // Manage nonces locally

View File

@ -16,14 +16,14 @@ use ethers_signers::Signer;
/// use std::sync::Arc; /// use std::sync::Arc;
/// use std::convert::TryFrom; /// use std::convert::TryFrom;
/// use ethers_signers::{LocalWallet, Signer}; /// use ethers_signers::{LocalWallet, Signer};
/// use ethers_middleware::{*,gas_escalator::*,gas_oracle::*}; /// use ethers_middleware::{*, gas_escalator::*, gas_oracle::*};
/// ///
/// fn builder_example() { /// fn builder_example() {
/// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"; /// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
/// let signer = key.parse::<LocalWallet>().unwrap(); /// let signer = key.parse::<LocalWallet>().unwrap();
/// let address = signer.address(); /// let address = signer.address();
/// let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>); /// let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
/// let gas_oracle = EthGasStation::new(None); /// let gas_oracle = GasNow::new();
/// ///
/// let provider = Provider::<Http>::try_from("http://localhost:8545") /// let provider = Provider::<Http>::try_from("http://localhost:8545")
/// .unwrap() /// .unwrap()
@ -43,7 +43,7 @@ use ethers_signers::Signer;
/// .unwrap() /// .unwrap()
/// .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) /// .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
/// .wrap_into(|p| SignerMiddleware::new(p, signer)) /// .wrap_into(|p| SignerMiddleware::new(p, signer))
/// .wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None))) /// .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
/// .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer /// .wrap_into(|p| NonceManagerMiddleware::new(p, address)); // Outermost layer
/// } /// }
/// ``` /// ```

View File

@ -46,7 +46,7 @@ pub enum Frequency {
/// use ethers_providers::{Provider, Http}; /// use ethers_providers::{Provider, Http};
/// use ethers_middleware::{ /// use ethers_middleware::{
/// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware}, /// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware},
/// gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware}, /// gas_oracle::{GasNow, GasCategory, GasOracleMiddleware},
/// }; /// };
/// use std::{convert::TryFrom, time::Duration, sync::Arc}; /// use std::{convert::TryFrom, time::Duration, sync::Arc};
/// ///
@ -60,7 +60,7 @@ pub enum Frequency {
/// }; /// };
/// ///
/// // ... proceed to wrap it in other middleware /// // ... proceed to wrap it in other middleware
/// let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); /// let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
/// let provider = GasOracleMiddleware::new(provider, gas_oracle); /// let provider = GasOracleMiddleware::new(provider, gas_oracle);
/// ``` /// ```
pub struct GasEscalatorMiddleware<M, E> { pub struct GasEscalatorMiddleware<M, E> {

View File

@ -1,13 +1,128 @@
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use reqwest::{header::AUTHORIZATION, Client}; use reqwest::{header::AUTHORIZATION, Client};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, convert::TryInto}; use std::collections::HashMap;
use url::Url; use url::Url;
const BLOCKNATIVE_GAS_PRICE_ENDPOINT: &str = "https://api.blocknative.com/gasprices/blockprices"; const URL: &str = "https://api.blocknative.com/gasprices/blockprices";
/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API
/// that implements the `GasOracle` trait.
#[derive(Clone, Debug)]
#[must_use]
pub struct BlockNative {
client: Client,
url: Url,
api_key: Option<String>,
gas_category: GasCategory,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Response {
pub system: String,
pub network: String,
pub unit: String,
pub max_price: u64,
pub block_prices: Vec<BlockPrice>,
pub estimated_base_fees: Option<Vec<HashMap<String, Vec<BaseFeeEstimate>>>>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockPrice {
pub block_number: u64,
pub estimated_transaction_count: u64,
pub base_fee_per_gas: f64,
pub estimated_prices: Vec<GasEstimate>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GasEstimate {
pub confidence: u64,
pub price: u64,
pub max_priority_fee_per_gas: f64,
pub max_fee_per_gas: f64,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BaseFeeEstimate {
pub confidence: u64,
pub base_fee: f64,
}
impl Response {
#[inline]
pub fn estimate_from_category(&self, gas_category: &GasCategory) -> Result<GasEstimate> {
let confidence = gas_category_to_confidence(gas_category);
let price = self
.block_prices
.first()
.ok_or(GasOracleError::InvalidResponse)?
.estimated_prices
.iter()
.find(|p| p.confidence == confidence)
.ok_or(GasOracleError::GasCategoryNotSupported)?;
Ok(*price)
}
}
impl Default for BlockNative {
fn default() -> Self {
Self::new(None)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for BlockNative {
async fn fetch(&self) -> Result<U256> {
let estimate = self.query().await?.estimate_from_category(&self.gas_category)?;
Ok(U256::from(estimate.price) * GWEI_TO_WEI_U256)
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let estimate = self.query().await?.estimate_from_category(&self.gas_category)?;
let max = from_gwei_f64(estimate.max_fee_per_gas);
let prio = from_gwei_f64(estimate.max_priority_fee_per_gas);
Ok((max, prio))
}
}
impl BlockNative {
/// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
pub fn new(api_key: Option<String>) -> Self {
Self::with_client(Client::new(), api_key)
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client, api_key: Option<String>) -> Self {
let url = Url::parse(URL).unwrap();
Self { client, api_key, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response, GasOracleError> {
let mut request = self.client.get(self.url.clone());
if let Some(api_key) = self.api_key.as_ref() {
request = request.header(AUTHORIZATION, api_key);
}
let response = request.send().await?.error_for_status()?.json().await?;
Ok(response)
}
}
#[inline]
fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 { fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
match gas_category { match gas_category {
GasCategory::SafeLow => 80, GasCategory::SafeLow => 80,
@ -16,124 +131,3 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
GasCategory::Fastest => 99, GasCategory::Fastest => 99,
} }
} }
/// A client over HTTP for the [BlockNative](https://www.blocknative.com/gas-estimator) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)]
pub struct BlockNative {
client: Client,
url: Url,
api_key: String,
gas_category: GasCategory,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockNativeGasResponse {
system: Option<String>,
network: Option<String>,
unit: Option<String>,
max_price: Option<u64>,
block_prices: Vec<BlockPrice>,
estimated_base_fees: Vec<HashMap<String, Vec<BaseFeeEstimate>>>,
}
impl BlockNativeGasResponse {
pub fn get_estimation_for(
&self,
gas_category: &GasCategory,
) -> Result<EstimatedPrice, GasOracleError> {
let confidence = gas_category_to_confidence(gas_category);
Ok(self
.block_prices
.first()
.ok_or(GasOracleError::InvalidResponse)?
.estimated_prices
.iter()
.find(|p| p.confidence == confidence)
.ok_or(GasOracleError::GasCategoryNotSupported)?
.clone())
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BlockPrice {
block_number: u64,
estimated_transaction_count: u64,
base_fee_per_gas: f64,
estimated_prices: Vec<EstimatedPrice>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EstimatedPrice {
confidence: u64,
price: u64,
max_priority_fee_per_gas: f64,
max_fee_per_gas: f64,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BaseFeeEstimate {
confidence: u64,
base_fee: f64,
}
impl BlockNative {
/// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
pub fn new(api_key: String) -> Self {
Self::with_client(Client::new(), api_key)
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client, api_key: String) -> Self {
Self {
client,
api_key,
url: BLOCKNATIVE_GAS_PRICE_ENDPOINT.try_into().unwrap(),
gas_category: GasCategory::Standard,
}
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform request to Blocknative, decode response
pub async fn request(&self) -> Result<BlockNativeGasResponse, GasOracleError> {
self.client
.get(self.url.as_ref())
.header(AUTHORIZATION, &self.api_key)
.send()
.await?
.error_for_status()?
.json()
.await
.map_err(GasOracleError::HttpClientError)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for BlockNative {
async fn fetch(&self) -> Result<U256, GasOracleError> {
let prices = self.request().await?.get_estimation_for(&self.gas_category)?;
Ok(U256::from(prices.price * 100_u64) * U256::from(GWEI_TO_WEI) / U256::from(100))
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
let prices = self.request().await?.get_estimation_for(&self.gas_category)?;
let base_fee = U256::from((prices.max_fee_per_gas * 100.0) as u64) *
U256::from(GWEI_TO_WEI) /
U256::from(100);
let prio_fee = U256::from((prices.max_priority_fee_per_gas * 100.0) as u64) *
U256::from(GWEI_TO_WEI) /
U256::from(100);
Ok((base_fee, prio_fee))
}
}

View File

@ -1,4 +1,4 @@
use crate::gas_oracle::{GasOracle, GasOracleError}; use super::{GasOracle, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use futures_locks::RwLock; use futures_locks::RwLock;
@ -19,6 +19,24 @@ pub struct Cache<T: GasOracle> {
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct Cached<T: Clone>(RwLock<Option<(Instant, T)>>); struct Cached<T: Clone>(RwLock<Option<(Instant, T)>>);
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: GasOracle> GasOracle for Cache<T> {
async fn fetch(&self) -> Result<U256> {
self.fee.get(self.validity, || self.inner.fetch()).await
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
}
}
impl<T: GasOracle> Cache<T> {
pub fn new(validity: Duration, inner: T) -> Self {
Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
}
}
impl<T: Clone> Cached<T> { impl<T: Clone> Cached<T> {
async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E> async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E>
where where
@ -50,21 +68,3 @@ impl<T: Clone> Cached<T> {
} }
} }
} }
impl<T: GasOracle> Cache<T> {
pub fn new(validity: Duration, inner: T) -> Self {
Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: GasOracle> GasOracle for Cache<T> {
async fn fetch(&self) -> Result<U256, GasOracleError> {
self.fee.get(self.validity, || self.inner.fetch()).await
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
}
}

View File

@ -1,88 +1,78 @@
use std::collections::HashMap; #![allow(deprecated)]
use ethers_core::types::U256;
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; const URL: &str = "https://ethgasstation.info/api/ethgasAPI.json";
const ETH_GAS_STATION_URL_PREFIX: &str = "https://ethgasstation.info/api/ethgasAPI.json"; /// A client over HTTP for the [EthGasStation](https://ethgasstation.info) gas tracker API
/// that implements the `GasOracle` trait.
/// A client over HTTP for the [EthGasStation](https://ethgasstation.info/api/ethgasAPI.json) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[deprecated = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
#[must_use]
pub struct EthGasStation { pub struct EthGasStation {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
/// Eth Gas Station's response for the current recommended fast, standard and /// Eth Gas Station's response for the current recommended fast, standard and
/// safe low gas prices on the Ethereum network, along with the current block /// safe low gas prices on the Ethereum network, along with the current block
/// and wait times for each "speed". /// and wait times for each "speed".
pub struct EthGasStationResponse { #[derive(Clone, Debug, Deserialize, PartialEq)]
/// Recommended safe(expected to be mined in < 30 minutes) gas price in #[serde(rename_all = "camelCase")]
/// x10 Gwei (divide by 10 to convert it to gwei) pub struct Response {
pub safe_low: f64, /// Recommended safe (expected to be mined in < 30 minutes).
/// Recommended average(expected to be mined in < 5 minutes) gas price in ///
/// x10 Gwei (divide by 10 to convert it to gwei) /// In gwei * 10 (divide by 10 to convert it to gwei).
pub safe_low: u64,
/// Recommended average (expected to be mined in < 5 minutes).
///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub average: u64, pub average: u64,
/// Recommended fast(expected to be mined in < 2 minutes) gas price in /// Recommended fast (expected to be mined in < 2 minutes).
/// x10 Gwei (divide by 10 to convert it to gwei) ///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub fast: u64, pub fast: u64,
/// Recommended fastest(expected to be mined in < 30 seconds) gas price /// Recommended fastest (expected to be mined in < 30 seconds).
/// in x10 Gwei(divide by 10 to convert it to gwei) ///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub fastest: u64, pub fastest: u64,
// post eip-1559 fields // post eip-1559 fields
/// Average time (in seconds) to mine a single block.
#[serde(rename = "block_time")] // inconsistent json response naming... #[serde(rename = "block_time")] // inconsistent json response naming...
/// Average time(in seconds) to mine one single block
pub block_time: f64, pub block_time: f64,
/// The latest block number /// The latest block number.
pub block_num: u64, pub block_num: u64,
/// Smallest value of (gasUsed / gaslimit) from last 10 blocks /// Smallest value of `gasUsed / gaslimit` from the last 10 blocks.
pub speed: f64, pub speed: f64,
/// Waiting time(in minutes) for the `safe_low` gas price /// Waiting time (in minutes) for the `safe_low` gas price.
pub safe_low_wait: f64, pub safe_low_wait: f64,
/// Waiting time(in minutes) for the `average` gas price /// Waiting time (in minutes) for the `average` gas price.
pub avg_wait: f64, pub avg_wait: f64,
/// Waiting time(in minutes) for the `fast` gas price /// Waiting time (in minutes) for the `fast` gas price.
pub fast_wait: f64, pub fast_wait: f64,
/// Waiting time(in minutes) for the `fastest` gas price /// Waiting time (in minutes) for the `fastest` gas price.
pub fastest_wait: f64, pub fastest_wait: f64,
// What is this? // What is this?
pub gas_price_range: HashMap<u64, f64>, pub gas_price_range: HashMap<u64, f64>,
} }
impl EthGasStation { impl Response {
/// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle #[inline]
pub fn new(api_key: Option<&str>) -> Self { pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
Self::with_client(Client::new(), api_key) match gas_category {
} GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.average,
/// Same as [`Self::new`] but with a custom [`Client`]. GasCategory::Fast => self.fast,
pub fn with_client(client: Client, api_key: Option<&str>) -> Self { GasCategory::Fastest => self.fastest,
let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url");
if let Some(key) = api_key {
url.query_pairs_mut().append_pair("api-key", key);
} }
EthGasStation { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
pub async fn query(&self) -> Result<EthGasStationResponse, GasOracleError> {
Ok(self.client.get(self.url.as_ref()).send().await?.json::<EthGasStationResponse>().await?)
} }
} }
@ -95,19 +85,43 @@ impl Default for EthGasStation {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for EthGasStation { impl GasOracle for EthGasStation {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let res = self.query().await?; let res = self.query().await?;
let gas_price = match self.gas_category { let gas_price = res.gas_from_category(self.gas_category);
GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10), // gas_price is in `gwei * 10`
GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10), Ok(U256::from(gas_price) * GWEI_TO_WEI_U256 / U256::from(10_u64))
GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10),
GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10),
};
Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
impl EthGasStation {
/// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle.
pub fn new(api_key: Option<&str>) -> Self {
Self::with_client(Client::new(), api_key)
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client, api_key: Option<&str>) -> Self {
let mut url = Url::parse(URL).unwrap();
if let Some(key) = api_key {
url.query_pairs_mut().append_pair("api-key", key);
}
EthGasStation { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}

View File

@ -1,56 +1,42 @@
use ethers_core::types::U256; use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI}; const URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
const ETHERCHAIN_URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
/// A client over HTTP for the [Etherchain](https://www.etherchain.org/api/gasPriceOracle) gas tracker API /// A client over HTTP for the [Etherchain](https://www.etherchain.org/api/gasPriceOracle) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct Etherchain { pub struct Etherchain {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EtherchainResponse { pub struct Response {
pub safe_low: f32, pub safe_low: f64,
pub standard: f32, pub standard: f64,
pub fast: f32, pub fast: f64,
pub fastest: f32, pub fastest: f64,
pub current_base_fee: f32, pub current_base_fee: f64,
pub recommended_base_fee: f32, pub recommended_base_fee: f64,
} }
impl Etherchain { impl Response {
/// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle. #[inline]
pub fn new() -> Self { pub fn gas_from_category(&self, gas_category: GasCategory) -> f64 {
Self::with_client(Client::new()) match gas_category {
} GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.standard,
/// Same as [`Self::new`] but with a custom [`Client`]. GasCategory::Fast => self.fast,
pub fn with_client(client: Client) -> Self { GasCategory::Fastest => self.fastest,
let url = Url::parse(ETHERCHAIN_URL).expect("invalid url"); }
Etherchain { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
pub async fn query(&self) -> Result<EtherchainResponse, GasOracleError> {
Ok(self.client.get(self.url.as_ref()).send().await?.json::<EtherchainResponse>().await?)
} }
} }
@ -63,19 +49,39 @@ impl Default for Etherchain {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Etherchain { impl GasOracle for Etherchain {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let res = self.query().await?; let res = self.query().await?;
let gas_price = match self.gas_category { let gas_price = res.gas_from_category(self.gas_category);
GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI), Ok(from_gwei_f64(gas_price))
GasCategory::Standard => U256::from((res.standard as u64) * GWEI_TO_WEI),
GasCategory::Fast => U256::from((res.fast as u64) * GWEI_TO_WEI),
GasCategory::Fastest => U256::from((res.fastest as u64) * GWEI_TO_WEI),
};
Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
impl Etherchain {
/// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle.
pub fn new() -> Self {
Self::with_client(Client::new())
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client) -> Self {
let url = Url::parse(URL).unwrap();
Etherchain { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}

View File

@ -1,18 +1,57 @@
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use ethers_etherscan::Client; use ethers_etherscan::Client;
use std::ops::{Deref, DerefMut};
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
/// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API /// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API
/// that implements the `GasOracle` trait /// that implements the `GasOracle` trait
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct Etherscan { pub struct Etherscan {
client: Client, client: Client,
gas_category: GasCategory, gas_category: GasCategory,
} }
impl Deref for Etherscan {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl DerefMut for Etherscan {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Etherscan {
async fn fetch(&self) -> Result<U256> {
// handle unsupported gas categories before making the request
match self.gas_category {
GasCategory::SafeLow | GasCategory::Standard | GasCategory::Fast => {}
GasCategory::Fastest => return Err(GasOracleError::GasCategoryNotSupported),
}
let result = self.query().await?;
let gas_price = match self.gas_category {
GasCategory::SafeLow => result.safe_gas_price,
GasCategory::Standard => result.propose_gas_price,
GasCategory::Fast => result.fast_gas_price,
_ => unreachable!(),
};
Ok(U256::from(gas_price) * GWEI_TO_WEI_U256)
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported)
}
}
impl Etherscan { impl Etherscan {
/// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle. /// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle.
pub fn new(client: Client) -> Self { pub fn new(client: Client) -> Self {
@ -20,32 +59,13 @@ impl Etherscan {
} }
/// Sets the gas price category to be used when fetching the gas price. /// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self { pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category; self.gas_category = gas_category;
self self
} }
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] /// Perform a request to the gas price API and deserialize the response.
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub async fn query(&self) -> Result<ethers_etherscan::gas::GasOracle> {
impl GasOracle for Etherscan { Ok(self.client.gas_oracle().await?)
async fn fetch(&self) -> Result<U256, GasOracleError> {
if matches!(self.gas_category, GasCategory::Fastest) {
return Err(GasOracleError::GasCategoryNotSupported)
}
let result = self.client.gas_oracle().await?;
match self.gas_category {
GasCategory::SafeLow => Ok(U256::from(result.safe_gas_price * GWEI_TO_WEI)),
GasCategory::Standard => Ok(U256::from(result.propose_gas_price * GWEI_TO_WEI)),
GasCategory::Fast => Ok(U256::from(result.fast_gas_price * GWEI_TO_WEI)),
_ => Err(GasOracleError::GasCategoryNotSupported),
}
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }

View File

@ -1,64 +1,54 @@
use ethers_core::types::U256; use super::{GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; const URL: &str = "https://beaconcha.in/api/v1/execution/gasnow";
const GAS_NOW_URL: &str = "https://etherchain.org/api/gasnow"; /// A client over HTTP for the [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas tracker API
/// that implements the `GasOracle` trait.
/// A client over HTTP for the [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct GasNow { pub struct GasNow {
client: Client, client: Client,
url: Url, url: Url,
gas_category: GasCategory, gas_category: GasCategory,
} }
#[derive(Deserialize)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
struct GasNowResponseWrapper { pub struct Response {
data: GasNowResponse, pub code: u64,
pub data: ResponseData,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct GasNowResponse { pub struct ResponseData {
pub rapid: u64, pub rapid: u64,
pub fast: u64, pub fast: u64,
pub standard: u64, pub standard: u64,
pub slow: u64, pub slow: u64,
pub timestamp: u64,
#[serde(rename = "priceUSD")]
pub price_usd: f64,
} }
impl GasNow { impl Response {
/// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle. #[inline]
pub fn new() -> Self { pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
Self::with_client(Client::new()) self.data.gas_from_category(gas_category)
} }
}
/// Same as [`Self::new`] but with a custom [`Client`]. impl ResponseData {
pub fn with_client(client: Client) -> Self { fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
let url = Url::parse(GAS_NOW_URL).expect("invalid url"); match gas_category {
GasCategory::SafeLow => self.slow,
Self { client, url, gas_category: GasCategory::Standard } GasCategory::Standard => self.standard,
} GasCategory::Fast => self.fast,
GasCategory::Fastest => self.rapid,
/// Sets the gas price category to be used when fetching the gas price. }
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
pub async fn query(&self) -> Result<GasNowResponse, GasOracleError> {
let res = self
.client
.get(self.url.as_ref())
.send()
.await?
.json::<GasNowResponseWrapper>()
.await?;
Ok(res.data)
} }
} }
@ -71,19 +61,39 @@ impl Default for GasNow {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for GasNow { impl GasOracle for GasNow {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let res = self.query().await?; let res = self.query().await?;
let gas_price = match self.gas_category { let gas_price = res.gas_from_category(self.gas_category);
GasCategory::SafeLow => U256::from(res.slow), Ok(U256::from(gas_price))
GasCategory::Standard => U256::from(res.standard),
GasCategory::Fast => U256::from(res.fast),
GasCategory::Fastest => U256::from(res.rapid),
};
Ok(gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
impl GasNow {
/// Creates a new [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas price oracle.
pub fn new() -> Self {
Self::with_client(Client::new())
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client) -> Self {
let url = Url::parse(URL).unwrap();
Self { client, url, gas_category: GasCategory::Standard }
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}

View File

@ -1,4 +1,4 @@
use crate::gas_oracle::{GasOracle, GasOracleError}; use super::{GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use futures_util::future::join_all; use futures_util::future::join_all;
@ -28,13 +28,10 @@ impl Median {
self.oracles.push((weight, Box::new(oracle))); self.oracles.push((weight, Box::new(oracle)));
} }
pub async fn query_all<'a, Fn, Fut, O>( pub async fn query_all<'a, Fn, Fut, O>(&'a self, mut f: Fn) -> Result<Vec<(f32, O)>>
&'a self,
mut f: Fn,
) -> Result<Vec<(f32, O)>, GasOracleError>
where where
Fn: FnMut(&'a dyn GasOracle) -> Fut, Fn: FnMut(&'a dyn GasOracle) -> Fut,
Fut: Future<Output = Result<O, GasOracleError>>, Fut: Future<Output = Result<O>>,
{ {
// Process the oracles in parallel // Process the oracles in parallel
let futures = self.oracles.iter().map(|(_, oracle)| f(oracle.as_ref())); let futures = self.oracles.iter().map(|(_, oracle)| f(oracle.as_ref()));
@ -62,13 +59,13 @@ impl Median {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Median { impl GasOracle for Median {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let mut values = self.query_all(|oracle| oracle.fetch()).await?; let mut values = self.query_all(|oracle| oracle.fetch()).await?;
// `query_all` guarantees `values` is not empty // `query_all` guarantees `values` is not empty
Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap()) Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap())
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?; let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?;
// `query_all` guarantees `values` is not empty // `query_all` guarantees `values` is not empty
Ok(( Ok((

View File

@ -4,8 +4,8 @@ use ethers_core::types::{transaction::eip2718::TypedTransaction, *};
use ethers_providers::{FromErr, Middleware, PendingTransaction}; use ethers_providers::{FromErr, Middleware, PendingTransaction};
use thiserror::Error; use thiserror::Error;
/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`.
#[derive(Debug)] #[derive(Debug)]
/// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`
pub struct GasOracleMiddleware<M, G> { pub struct GasOracleMiddleware<M, G> {
inner: M, inner: M,
gas_oracle: G, gas_oracle: G,
@ -21,7 +21,7 @@ where
} }
} }
#[derive(Error, Debug)] #[derive(Debug, Error)]
pub enum MiddlewareError<M: Middleware> { pub enum MiddlewareError<M: Middleware> {
#[error(transparent)] #[error(transparent)]
GasOracleError(#[from] GasOracleError), GasOracleError(#[from] GasOracleError),

View File

@ -1,54 +1,58 @@
mod blocknative; pub mod blocknative;
pub use blocknative::BlockNative; pub use blocknative::BlockNative;
mod eth_gas_station; pub mod eth_gas_station;
#[allow(deprecated)]
pub use eth_gas_station::EthGasStation; pub use eth_gas_station::EthGasStation;
mod etherchain; pub mod etherchain;
pub use etherchain::Etherchain; pub use etherchain::Etherchain;
mod etherscan; pub mod etherscan;
pub use etherscan::Etherscan; pub use etherscan::Etherscan;
mod middleware; pub mod middleware;
pub use middleware::{GasOracleMiddleware, MiddlewareError}; pub use middleware::{GasOracleMiddleware, MiddlewareError};
mod median; pub mod median;
pub use median::Median; pub use median::Median;
mod cache; pub mod cache;
pub use cache::Cache; pub use cache::Cache;
mod polygon; pub mod polygon;
pub use polygon::Polygon; pub use polygon::Polygon;
mod gas_now; pub mod gas_now;
pub use gas_now::GasNow; pub use gas_now::GasNow;
mod provider_oracle; pub mod provider_oracle;
pub use provider_oracle::ProviderOracle; pub use provider_oracle::ProviderOracle;
use ethers_core::types::U256;
use async_trait::async_trait; use async_trait::async_trait;
use auto_impl::auto_impl; use auto_impl::auto_impl;
use ethers_core::types::U256;
use reqwest::Error as ReqwestError; use reqwest::Error as ReqwestError;
use std::error::Error; use std::{error::Error, fmt::Debug};
use thiserror::Error; use thiserror::Error;
const GWEI_TO_WEI: u64 = 1000000000; pub(crate) const GWEI_TO_WEI: u64 = 1_000_000_000;
pub(crate) const GWEI_TO_WEI_U256: U256 = U256([GWEI_TO_WEI, 0, 0, 0]);
/// Various gas price categories. Choose one of the available pub type Result<T, E = GasOracleError> = std::result::Result<T, E>;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
/// Generic [`GasOracle`] gas price categories.
#[derive(Clone, Copy, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum GasCategory { pub enum GasCategory {
SafeLow, SafeLow,
#[default]
Standard, Standard,
Fast, Fast,
Fastest, Fastest,
} }
#[derive(Error, Debug)] /// Error thrown by a [`GasOracle`].
/// Error thrown when fetching data from the `GasOracle` #[derive(Debug, Error)]
pub enum GasOracleError { pub enum GasOracleError {
/// An internal error in the HTTP request made from the underlying /// An internal error in the HTTP request made from the underlying
/// gas oracle /// gas oracle
@ -83,48 +87,81 @@ pub enum GasOracleError {
UnsupportedChain, UnsupportedChain,
/// Error thrown when the provider failed. /// Error thrown when the provider failed.
#[error("Chain is not supported by the oracle")] #[error("Provider error: {0}")]
ProviderError(#[from] Box<dyn Error + Send + Sync>), ProviderError(#[from] Box<dyn Error + Send + Sync>),
} }
/// `GasOracle` is a trait that an underlying gas oracle needs to implement. /// An Ethereum gas price oracle.
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```no_run
/// use ethers_middleware::{ /// use ethers_core::types::U256;
/// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle}, /// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
/// };
/// ///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> { /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let eth_gas_station_oracle = EthGasStation::new(Some("my-api-key")); /// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let etherscan_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); /// let gas_price = oracle.fetch().await?;
/// /// assert!(gas_price > U256::zero());
/// let data_1 = eth_gas_station_oracle.fetch().await?;
/// let data_2 = etherscan_oracle.fetch().await?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[auto_impl(&, Box, Arc)] #[auto_impl(&, Box, Arc)]
pub trait GasOracle: Send + Sync + std::fmt::Debug { pub trait GasOracle: Send + Sync + Debug {
/// Makes an asynchronous HTTP query to the underlying `GasOracle` /// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current gas
/// price estimate.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use ethers_middleware::{ /// use ethers_core::types::U256;
/// gas_oracle::{Etherchain, GasCategory, GasOracle}, /// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
/// };
/// ///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> { /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let etherchain_oracle = Etherchain::new().category(GasCategory::Fastest); /// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let data = etherchain_oracle.fetch().await?; /// let gas_price = oracle.fetch().await?;
/// assert!(gas_price > U256::zero());
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
async fn fetch(&self) -> Result<U256, GasOracleError>; async fn fetch(&self) -> Result<U256>;
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError>; /// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current max
/// gas fee and priority gas fee estimates.
///
/// # Example
///
/// ```no_run
/// use ethers_core::types::U256;
/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let (max_fee, priority_fee) = oracle.estimate_eip1559_fees().await?;
/// assert!(max_fee > U256::zero());
/// assert!(priority_fee > U256::zero());
/// # Ok(())
/// # }
/// ```
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)>;
}
#[inline]
#[doc(hidden)]
pub(crate) fn from_gwei_f64(gwei: f64) -> U256 {
ethers_core::types::u256_from_f64_saturating(gwei) * GWEI_TO_WEI_U256
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gwei_wei_constants() {
let as_u256: U256 = GWEI_TO_WEI.into();
assert_eq!(as_u256, GWEI_TO_WEI_U256);
assert_eq!(GWEI_TO_WEI_U256.as_u64(), GWEI_TO_WEI);
}
} }

View File

@ -1,16 +1,17 @@
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError}; use super::{from_gwei_f64, GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::{u256_from_f64_saturating, Chain, U256}; use ethers_core::types::{Chain, U256};
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2"; const MAINNET_URL: &str = "https://gasstation-mainnet.matic.network/v2";
const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2"; const MUMBAI_URL: &str = "https://gasstation-mumbai.matic.today/v2";
/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API /// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API
/// Queries over HTTP and implements the `GasOracle` trait /// Queries over HTTP and implements the `GasOracle` trait.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct Polygon { pub struct Polygon {
client: Client, client: Client,
url: Url, url: Url,
@ -18,74 +19,98 @@ pub struct Polygon {
} }
/// The response from the Polygon gas station API. /// The response from the Polygon gas station API.
/// Gas prices are in Gwei. ///
/// Gas prices are in __Gwei__.
#[derive(Debug, Deserialize, PartialEq)] #[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Response { pub struct Response {
estimated_base_fee: f64, pub estimated_base_fee: f64,
safe_low: GasEstimate, pub safe_low: GasEstimate,
standard: GasEstimate, pub standard: GasEstimate,
fast: GasEstimate, pub fast: GasEstimate,
} }
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GasEstimate { pub struct GasEstimate {
max_priority_fee: f64, pub max_priority_fee: f64,
max_fee: f64, pub max_fee: f64,
} }
impl Polygon { impl Response {
pub fn new(chain: Chain) -> Result<Self, GasOracleError> { #[inline]
Self::with_client(Client::new(), chain) pub fn estimate_from_category(&self, gas_category: GasCategory) -> GasEstimate {
match gas_category {
GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.standard,
GasCategory::Fast => self.fast,
GasCategory::Fastest => self.fast,
}
} }
}
pub fn with_client(client: Client, chain: Chain) -> Result<Self, GasOracleError> { impl Default for Polygon {
// TODO: Sniff chain from chain id. fn default() -> Self {
let url = match chain { Self::new(Chain::Polygon).unwrap()
Chain::Polygon => Url::parse(GAS_PRICE_ENDPOINT).unwrap(),
Chain::PolygonMumbai => Url::parse(MUMBAI_GAS_PRICE_ENDPOINT).unwrap(),
_ => return Err(GasOracleError::UnsupportedChain),
};
Ok(Self { client, url, gas_category: GasCategory::Standard })
}
/// Sets the gas price category to be used when fetching the gas price.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform request to Blocknative, decode response
pub async fn request(&self) -> Result<(f64, GasEstimate), GasOracleError> {
let response: Response =
self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
let estimate = match self.gas_category {
GasCategory::SafeLow => response.safe_low,
GasCategory::Standard => response.standard,
GasCategory::Fast => response.fast,
GasCategory::Fastest => response.fast,
};
Ok((response.estimated_base_fee, estimate))
} }
} }
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Polygon { impl GasOracle for Polygon {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
let (base_fee, estimate) = self.request().await?; let response = self.query().await?;
let fee = base_fee + estimate.max_priority_fee; let base = response.estimated_base_fee;
Ok(from_gwei(fee)) let prio = response.estimate_from_category(self.gas_category).max_priority_fee;
let fee = base + prio;
Ok(from_gwei_f64(fee))
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let (_, estimate) = self.request().await?; let response = self.query().await?;
Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee))) let estimate = response.estimate_from_category(self.gas_category);
let max = from_gwei_f64(estimate.max_fee);
let prio = from_gwei_f64(estimate.max_priority_fee);
Ok((max, prio))
} }
} }
fn from_gwei(gwei: f64) -> U256 { impl Polygon {
u256_from_f64_saturating(gwei * 1.0e9_f64) pub fn new(chain: Chain) -> Result<Self> {
Self::with_client(Client::new(), chain)
}
pub fn with_client(client: Client, chain: Chain) -> Result<Self> {
// TODO: Sniff chain from chain id.
let url = match chain {
Chain::Polygon => MAINNET_URL,
Chain::PolygonMumbai => MUMBAI_URL,
_ => return Err(GasOracleError::UnsupportedChain),
};
Ok(Self { client, url: Url::parse(url).unwrap(), gas_category: GasCategory::Standard })
}
/// Sets the gas price category to be used when fetching the gas price.
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<Response> {
let response =
self.client.get(self.url.clone()).send().await?.error_for_status()?.json().await?;
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_polygon_gas_station_response() {
let s = r#"{"safeLow":{"maxPriorityFee":2.1267086610666666,"maxFee":2.1267086760666665},"standard":{"maxPriorityFee":2.3482958369333335,"maxFee":2.3482958519333335},"fast":{"maxPriorityFee":2.793454819,"maxFee":2.793454834},"estimatedBaseFee":1.5e-8,"blockTime":2,"blockNumber":30328888}"#;
let _resp: Response = serde_json::from_str(s).unwrap();
}
} }

View File

@ -1,4 +1,4 @@
use crate::gas_oracle::{GasOracle, GasOracleError}; use super::{GasOracle, GasOracleError, Result};
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::types::U256; use ethers_core::types::U256;
use ethers_providers::Middleware; use ethers_providers::Middleware;
@ -6,7 +6,8 @@ use std::fmt::Debug;
/// Gas oracle from a [`Middleware`] implementation such as an /// Gas oracle from a [`Middleware`] implementation such as an
/// Ethereum RPC provider. /// Ethereum RPC provider.
#[derive(Debug)] #[derive(Clone, Debug)]
#[must_use]
pub struct ProviderOracle<M: Middleware> { pub struct ProviderOracle<M: Middleware> {
provider: M, provider: M,
} }
@ -21,16 +22,16 @@ impl<M: Middleware> ProviderOracle<M> {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<M: Middleware> GasOracle for ProviderOracle<M> impl<M: Middleware> GasOracle for ProviderOracle<M>
where where
<M as Middleware>::Error: 'static, M::Error: 'static,
{ {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
self.provider self.provider
.get_gas_price() .get_gas_price()
.await .await
.map_err(|err| GasOracleError::ProviderError(Box::new(err))) .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
// TODO: Allow configuring different estimation functions. // TODO: Allow configuring different estimation functions.
self.provider self.provider
.estimate_eip1559_fees(None) .estimate_eip1559_fees(None)

View File

@ -1,22 +1,21 @@
use ethers_contract::Lazy; use ethers_contract::Lazy;
use ethers_core::types::*; use ethers_core::types::*;
use std::{collections::HashMap, str::FromStr}; use std::collections::HashMap;
/// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding /// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding
/// DsProxyFactory contract addresses as values /// DsProxyFactory contract addresses as values
pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| { pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
let mut m = HashMap::new(); let mut m = HashMap::with_capacity(1);
// mainnet // mainnet
let addr = let addr = "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap();
Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed"); m.insert(U256::from(1_u64), addr);
m.insert(U256::from(1u8), addr);
m m
}); });
/// Generated with abigen:
/// ///
/// Generated with
/// ```ignore /// ```ignore
/// # use ethers_contract::abigen; /// # use ethers_contract::abigen;
/// abigen!(DsProxyFactory, /// abigen!(DsProxyFactory,
@ -26,7 +25,6 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
/// } /// }
/// ); /// );
/// ``` /// ```
// Auto-generated type-safe bindings
pub use dsproxyfactory_mod::*; pub use dsproxyfactory_mod::*;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
mod dsproxyfactory_mod { mod dsproxyfactory_mod {

View File

@ -1,69 +1,67 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(not(target_arch = "wasm32"))]
#[cfg(not(feature = "celo"))]
mod tests {
use ethers_core::{rand::thread_rng, types::U64};
use ethers_middleware::{
builder::MiddlewareBuilder,
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{EthGasStation, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
#[tokio::test] use ethers_core::{rand::thread_rng, types::U64};
async fn build_raw_middleware_stack() { use ethers_middleware::{
let (provider, mock) = Provider::mocked(); builder::MiddlewareBuilder,
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{GasNow, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
let signer = LocalWallet::new(&mut thread_rng()); #[tokio::test]
let address = signer.address(); async fn build_raw_middleware_stack() {
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>); let (provider, mock) = Provider::mocked();
let provider = provider let signer = LocalWallet::new(&mut thread_rng());
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) let address = signer.address();
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None))) let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
.wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
// push a response let provider = provider
mock.push(U64::from(12u64)).unwrap(); .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
let block: U64 = provider.get_block_number().await.unwrap(); .wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
assert_eq!(block.as_u64(), 12); .wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
provider.get_block_number().await.unwrap_err(); // push a response
mock.push(U64::from(12u64)).unwrap();
let block: U64 = provider.get_block_number().await.unwrap();
assert_eq!(block.as_u64(), 12);
// 2 calls were made provider.get_block_number().await.unwrap_err();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
}
#[tokio::test] // 2 calls were made
async fn build_declarative_middleware_stack() { mock.assert_request("eth_blockNumber", ()).unwrap();
let (provider, mock) = Provider::mocked(); mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
let signer = LocalWallet::new(&mut thread_rng()); }
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>); #[tokio::test]
let gas_oracle = EthGasStation::new(None); async fn build_declarative_middleware_stack() {
let (provider, mock) = Provider::mocked();
let provider = provider
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock)) let signer = LocalWallet::new(&mut thread_rng());
.gas_oracle(gas_oracle) let address = signer.address();
.with_signer(signer) let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
.nonce_manager(address); let gas_oracle = GasNow::new();
// push a response let provider = provider
mock.push(U64::from(12u64)).unwrap(); .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
let block: U64 = provider.get_block_number().await.unwrap(); .gas_oracle(gas_oracle)
assert_eq!(block.as_u64(), 12); .with_signer(signer)
.nonce_manager(address);
provider.get_block_number().await.unwrap_err();
// push a response
// 2 calls were made mock.push(U64::from(12u64)).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap(); let block: U64 = provider.get_block_number().await.unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap(); assert_eq!(block.as_u64(), 12);
mock.assert_request("eth_blockNumber", ()).unwrap_err();
} provider.get_block_number().await.unwrap_err();
// 2 calls were made
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
} }

View File

@ -1,4 +1,5 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(not(target_arch = "wasm32"))]
use ethers_core::types::*; use ethers_core::types::*;
use ethers_middleware::{ use ethers_middleware::{
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
@ -12,6 +13,7 @@ use std::time::Duration;
#[ignore] #[ignore]
async fn gas_escalator_live() { async fn gas_escalator_live() {
// connect to ropsten for getting bad block times // connect to ropsten for getting bad block times
#[allow(deprecated)]
let provider = ethers_providers::ROPSTEN.ws().await; let provider = ethers_providers::ROPSTEN.ws().await;
let provider = provider.interval(Duration::from_millis(2000u64)); let provider = provider.interval(Duration::from_millis(2000u64));
let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169" let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"
@ -33,7 +35,7 @@ async fn gas_escalator_live() {
provider.send_transaction(tx.clone().nonce(nonce + 2), None).await.unwrap(); provider.send_transaction(tx.clone().nonce(nonce + 2), None).await.unwrap();
// Wait a bunch of seconds and refresh etherscan to see the transactions get bumped // Wait a bunch of seconds and refresh etherscan to see the transactions get bumped
tokio::time::sleep(std::time::Duration::from_secs(100)).await; tokio::time::sleep(Duration::from_secs(100)).await;
// TODO: Figure out how to test this behavior properly in a local network. If the gas price was // TODO: Figure out how to test this behavior properly in a local network. If the gas price was
// bumped then the tx hash will be different // bumped then the tx hash will be different

View File

@ -1,16 +1,14 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(not(target_arch = "wasm32"))]
use std::convert::TryFrom;
use async_trait::async_trait; use async_trait::async_trait;
use ethers_core::{types::*, utils::Anvil}; use ethers_core::{types::*, utils::Anvil};
use ethers_middleware::gas_oracle::{ use ethers_middleware::gas_oracle::{
EthGasStation, Etherchain, Etherscan, GasCategory, GasOracle, GasOracleError, BlockNative, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleError,
GasOracleMiddleware, GasOracleMiddleware, Polygon, ProviderOracle, Result,
}; };
use ethers_providers::{Http, Middleware, Provider}; use ethers_providers::{Http, Middleware, Provider};
use serial_test::serial; use serial_test::serial;
use std::convert::TryFrom;
#[derive(Debug)] #[derive(Debug)]
struct FakeGasOracle { struct FakeGasOracle {
@ -20,17 +18,18 @@ struct FakeGasOracle {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for FakeGasOracle { impl GasOracle for FakeGasOracle {
async fn fetch(&self) -> Result<U256, GasOracleError> { async fn fetch(&self) -> Result<U256> {
Ok(self.gas_price) Ok(self.gas_price)
} }
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> { async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
Err(GasOracleError::Eip1559EstimationNotSupported) Err(GasOracleError::Eip1559EstimationNotSupported)
} }
} }
#[tokio::test] #[tokio::test]
async fn using_gas_oracle() { #[serial]
async fn provider_using_gas_oracle() {
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
let from = anvil.addresses()[0]; let from = anvil.addresses()[0];
@ -38,11 +37,11 @@ async fn using_gas_oracle() {
// connect to the network // connect to the network
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap(); let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
// initial base fee
let base_fee = 1_000_000_000u64;
// assign a gas oracle to use // assign a gas oracle to use
let gas_oracle = FakeGasOracle { gas_price: (base_fee + 1337).into() }; let expected_gas_price = U256::from(1234567890_u64);
let expected_gas_price = gas_oracle.fetch().await.unwrap(); let gas_oracle = FakeGasOracle { gas_price: expected_gas_price };
let gas_price = gas_oracle.fetch().await.unwrap();
assert_eq!(gas_price, expected_gas_price);
let provider = GasOracleMiddleware::new(provider, gas_oracle); let provider = GasOracleMiddleware::new(provider, gas_oracle);
@ -55,35 +54,70 @@ async fn using_gas_oracle() {
} }
#[tokio::test] #[tokio::test]
async fn eth_gas_station() { #[serial]
// initialize and fetch gas estimates from EthGasStation async fn provider_oracle() {
let eth_gas_station_oracle = EthGasStation::default(); // spawn anvil and connect to it
let data = eth_gas_station_oracle.fetch().await; let anvil = Anvil::new().spawn();
data.unwrap(); let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
// assert that provider.get_gas_price() and oracle.fetch() return the same value
let expected_gas_price = provider.get_gas_price().await.unwrap();
let provider_oracle = ProviderOracle::new(provider);
let gas = provider_oracle.fetch().await.unwrap();
assert_eq!(gas, expected_gas_price);
}
#[tokio::test]
async fn blocknative() {
let gas_now_oracle = BlockNative::default();
let gas_price = gas_now_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
}
#[tokio::test]
#[ignore = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
#[allow(deprecated)]
async fn eth_gas_station() {
let eth_gas_station_oracle = ethers_middleware::gas_oracle::EthGasStation::default();
let gas_price = eth_gas_station_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
}
#[tokio::test]
#[ignore = "Etherchain / beaconcha.in's `gasPriceOracle` API currently returns 404: https://www.etherchain.org/api/gasPriceOracle"]
async fn etherchain() {
let etherchain_oracle = Etherchain::default();
let gas_price = etherchain_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
} }
#[tokio::test] #[tokio::test]
#[serial]
async fn etherscan() { async fn etherscan() {
let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap(); let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap();
// initialize and fetch gas estimates from Etherscan // initialize and fetch gas estimates from Etherscan
// since etherscan does not support `fastest` category, we expect an error // since etherscan does not support `fastest` category, we expect an error
let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest); let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest);
let data = etherscan_oracle.fetch().await; let error = etherscan_oracle.fetch().await.unwrap_err();
data.unwrap_err(); assert!(matches!(error, GasOracleError::GasCategoryNotSupported));
// but fetching the `standard` gas price should work fine // but fetching the `standard` gas price should work fine
let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow); let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow);
let data = etherscan_oracle.fetch().await; let gas_price = etherscan_oracle.fetch().await.unwrap();
data.unwrap(); assert!(gas_price > U256::zero());
} }
#[tokio::test] #[tokio::test]
async fn etherchain() { async fn gas_now() {
// initialize and fetch gas estimates from Etherchain let gas_now_oracle = GasNow::default();
let etherchain_oracle = Etherchain::default().category(GasCategory::Fast); let gas_price = gas_now_oracle.fetch().await.unwrap();
let data = etherchain_oracle.fetch().await; assert!(gas_price > U256::zero());
data.unwrap(); }
#[tokio::test]
async fn polygon() {
let polygon_oracle = Polygon::default();
let gas_price = polygon_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
} }

View File

@ -1,18 +1,18 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn nonce_manager() {
use ethers_core::types::*;
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
use ethers_providers::Middleware;
use ethers_signers::{LocalWallet, Signer};
use std::time::Duration;
let provider = ethers_providers::GOERLI.provider().interval(Duration::from_millis(2000u64)); use ethers_core::types::*;
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
use ethers_providers::Middleware;
use ethers_signers::{LocalWallet, Signer};
use std::time::Duration;
#[tokio::test]
async fn nonce_manager() {
let provider = ethers_providers::GOERLI.provider().interval(Duration::from_millis(2000));
let chain_id = provider.get_chainid().await.unwrap().as_u64(); let chain_id = provider.get_chainid().await.unwrap().as_u64();
let wallet = std::env::var("GOERLI_PRIVATE_KEY") let wallet = std::env::var("GOERLI_PRIVATE_KEY")
.unwrap() .expect("GOERLI_PRIVATE_KEY is not defined")
.parse::<LocalWallet>() .parse::<LocalWallet>()
.unwrap() .unwrap()
.with_chain_id(chain_id); .with_chain_id(chain_id);
@ -44,12 +44,12 @@ async fn nonce_manager() {
} }
// sleep a bit to ensure there's no flakiness in the test // sleep a bit to ensure there's no flakiness in the test
std::thread::sleep(std::time::Duration::new(5, 0)); tokio::time::sleep(Duration::from_secs(10)).await;
let mut nonces = Vec::with_capacity(num_tx); let mut nonces = Vec::with_capacity(num_tx);
for tx_hash in tx_hashes { for tx_hash in tx_hashes {
nonces.push(provider.get_transaction(tx_hash).await.unwrap().unwrap().nonce.as_u64()); nonces.push(provider.get_transaction(tx_hash).await.unwrap().unwrap().nonce.as_u64());
} }
assert_eq!(nonces, (nonce..nonce + (num_tx as u64)).collect::<Vec<_>>()) assert_eq!(nonces, (nonce..nonce + num_tx as u64).collect::<Vec<_>>())
} }

View File

@ -1,14 +1,22 @@
#![allow(unused)] #![allow(unused)]
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
use ethers_contract::ContractFactory;
use ethers_core::{ use ethers_core::{
types::{BlockNumber, TransactionRequest}, abi::Abi,
utils::parse_units, types::*,
utils::{parse_ether, parse_units, Anvil},
}; };
use ethers_middleware::signer::SignerMiddleware; use ethers_middleware::signer::SignerMiddleware;
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer}; use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer};
use ethers_solc::Solc;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{convert::TryFrom, iter::Cycle, sync::atomic::AtomicU8, time::Duration}; use std::{
convert::TryFrom,
iter::Cycle,
sync::{atomic::AtomicU8, Arc},
time::Duration,
};
static WALLETS: Lazy<TestWallets> = Lazy::new(|| { static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
TestWallets { TestWallets {
@ -22,8 +30,6 @@ static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn send_eth() { async fn send_eth() {
use ethers_core::utils::Anvil;
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
// this private key belongs to the above mnemonic // this private key belongs to the above mnemonic
@ -95,10 +101,6 @@ async fn pending_txs_with_confirmations_testnet() {
generic_pending_txs_test(provider, address).await; generic_pending_txs_test(provider, address).await;
} }
#[cfg(not(feature = "celo"))]
use ethers_core::types::{Address, Eip1559TransactionRequest};
use ethers_core::utils::parse_ether;
// different keys to avoid nonce errors // different keys to avoid nonce errors
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
@ -195,8 +197,6 @@ async fn test_send_transaction() {
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn send_transaction_handles_tx_from_field() { async fn send_transaction_handles_tx_from_field() {
use ethers_core::utils::Anvil;
// launch anvil // launch anvil
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
@ -240,14 +240,6 @@ async fn send_transaction_handles_tx_from_field() {
#[tokio::test] #[tokio::test]
#[cfg(feature = "celo")] #[cfg(feature = "celo")]
async fn deploy_and_call_contract() { async fn deploy_and_call_contract() {
use ethers_contract::ContractFactory;
use ethers_core::{
abi::Abi,
types::{BlockNumber, Bytes, H256, U256},
};
use ethers_solc::Solc;
use std::sync::Arc;
// compiles the given contract and returns the ABI and Bytecode // compiles the given contract and returns the ABI and Bytecode
fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
let path = format!("./tests/solidity-contracts/{path}"); let path = format!("./tests/solidity-contracts/{path}");
@ -302,7 +294,7 @@ impl TestWallets {
#[allow(unused)] #[allow(unused)]
pub async fn fund<T: JsonRpcClient, U: Into<u32>>(&self, provider: &Provider<T>, n: U) { pub async fn fund<T: JsonRpcClient, U: Into<u32>>(&self, provider: &Provider<T>, n: U) {
let addrs = (0..n.into()).map(|i| self.get(i).address()).collect::<Vec<_>>(); let addrs = (0..n.into()).map(|i| self.get(i).address()).collect::<Vec<_>>();
// hardcoded funder address private key, goerli // hardcoded funder address private key, GOERLI
let signer = "39aa18eeb5d12c071e5f19d8e9375a872e90cb1f2fa640384ffd8800a2f3e8f1" let signer = "39aa18eeb5d12c071e5f19d8e9375a872e90cb1f2fa640384ffd8800a2f3e8f1"
.parse::<LocalWallet>() .parse::<LocalWallet>()
.unwrap() .unwrap()

View File

@ -1,95 +1,93 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#[cfg(not(feature = "celo"))]
mod tests {
use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
use ethers_middleware::{
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Http, Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
use std::convert::TryFrom;
#[tokio::test] use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
async fn mock_with_middleware() { use ethers_middleware::{
let (provider, mock) = Provider::mocked(); gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
gas_oracle::{GasCategory, GasNow, GasOracleMiddleware},
nonce_manager::NonceManagerMiddleware,
signer::SignerMiddleware,
};
use ethers_providers::{Http, Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
use std::convert::TryFrom;
// add a bunch of middlewares #[tokio::test]
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); async fn mock_with_middleware() {
let signer = LocalWallet::new(&mut thread_rng()); let (provider, mock) = Provider::mocked();
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
let provider = GasOracleMiddleware::new(provider, gas_oracle);
let provider = SignerMiddleware::new(provider, signer);
let provider = NonceManagerMiddleware::new(provider, address);
// push a response // add a bunch of middlewares
use ethers_core::types::U64; let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
mock.push(U64::from(12u64)).unwrap(); let signer = LocalWallet::new(&mut thread_rng());
let blk = provider.get_block_number().await.unwrap(); let address = signer.address();
assert_eq!(blk.as_u64(), 12); let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
let provider = GasOracleMiddleware::new(provider, gas_oracle);
let provider = SignerMiddleware::new(provider, signer);
let provider = NonceManagerMiddleware::new(provider, address);
// now that the response is gone, there's nothing left // push a response
// TODO: This returns: use ethers_core::types::U64;
// MiddlewareError( mock.push(U64::from(12u64)).unwrap();
// MiddlewareError( let blk = provider.get_block_number().await.unwrap();
// MiddlewareError( assert_eq!(blk.as_u64(), 12);
// MiddlewareError(
// JsonRpcClientError(EmptyResponses)
// ))))
// Can we flatten it in any way? Maybe inherent to the middleware
// infrastructure
provider.get_block_number().await.unwrap_err();
// 2 calls were made // now that the response is gone, there's nothing left
mock.assert_request("eth_blockNumber", ()).unwrap(); // TODO: This returns:
mock.assert_request("eth_blockNumber", ()).unwrap(); // MiddlewareError(
mock.assert_request("eth_blockNumber", ()).unwrap_err(); // MiddlewareError(
} // MiddlewareError(
// MiddlewareError(
// JsonRpcClientError(EmptyResponses)
// ))))
// Can we flatten it in any way? Maybe inherent to the middleware
// infrastructure
provider.get_block_number().await.unwrap_err();
#[tokio::test] // 2 calls were made
async fn can_stack_middlewares() { mock.assert_request("eth_blockNumber", ()).unwrap();
let anvil = Anvil::new().block_time(5u64).spawn(); mock.assert_request("eth_blockNumber", ()).unwrap();
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow); mock.assert_request("eth_blockNumber", ()).unwrap_err();
let signer: LocalWallet = anvil.keys()[0].clone().into(); }
let address = signer.address();
#[tokio::test]
// the base provider async fn can_stack_middlewares() {
let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap()); let anvil = Anvil::new().block_time(5u64).spawn();
let chain_id = provider.get_chainid().await.unwrap().as_u64(); let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
let signer = signer.with_chain_id(chain_id); let signer: LocalWallet = anvil.keys()[0].clone().into();
let address = signer.address();
// the Gas Price escalator middleware is the first middleware above the provider,
// so that it receives the transaction last, after all the other middleware // the base provider
// have modified it accordingly let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap());
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>); let chain_id = provider.get_chainid().await.unwrap().as_u64();
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); let signer = signer.with_chain_id(chain_id);
// The gas price middleware MUST be below the signing middleware for things to work // the Gas Price escalator middleware is the first middleware above the provider,
let provider = GasOracleMiddleware::new(provider, gas_oracle); // so that it receives the transaction last, after all the other middleware
// have modified it accordingly
// The signing middleware signs txs let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
use std::sync::Arc; let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
let provider = Arc::new(SignerMiddleware::new(provider, signer));
// The gas price middleware MUST be below the signing middleware for things to work
// The nonce manager middleware MUST be above the signing middleware so that it overrides let provider = GasOracleMiddleware::new(provider, gas_oracle);
// the nonce and the signer does not make any eth_getTransaction count calls
let provider = NonceManagerMiddleware::new(provider, address); // The signing middleware signs txs
use std::sync::Arc;
let tx = TransactionRequest::new(); let provider = Arc::new(SignerMiddleware::new(provider, signer));
let mut pending_txs = Vec::new();
for _ in 0..10 { // The nonce manager middleware MUST be above the signing middleware so that it overrides
let pending = provider.send_transaction(tx.clone(), None).await.unwrap(); // the nonce and the signer does not make any eth_getTransaction count calls
let hash = *pending; let provider = NonceManagerMiddleware::new(provider, address);
let gas_price = provider.get_transaction(hash).await.unwrap().unwrap().gas_price;
dbg!(gas_price); let tx = TransactionRequest::new();
pending_txs.push(pending); let mut pending_txs = Vec::new();
} for _ in 0..10 {
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
let receipts = futures_util::future::join_all(pending_txs); let hash = *pending;
dbg!(receipts.await); let gas_price = provider.get_transaction(hash).await.unwrap().unwrap().gas_price;
} dbg!(gas_price);
pending_txs.push(pending);
}
let receipts = futures_util::future::join_all(pending_txs);
dbg!(receipts.await);
} }

View File

@ -1,5 +1,5 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#![allow(unused)]
use ethers_contract::{BaseContract, ContractFactory}; use ethers_contract::{BaseContract, ContractFactory};
use ethers_core::{abi::Abi, types::*, utils::Anvil}; use ethers_core::{abi::Abi, types::*, utils::Anvil};
use ethers_middleware::{ use ethers_middleware::{
@ -24,7 +24,6 @@ fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
} }
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_transformer() { async fn ds_proxy_transformer() {
// randomness // randomness
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -83,7 +82,6 @@ async fn ds_proxy_transformer() {
} }
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_code() { async fn ds_proxy_code() {
// randomness // randomness
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ethers-providers" name = "ethers-providers"
version = "1.0.2" version = "1.0.2"
edition = "2018" edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"] authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -43,7 +43,7 @@ tracing = { version = "0.1.37", default-features = false }
tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] } tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
bytes = { version = "1.3.0", default-features = false, optional = true } bytes = { version = "1.3.0", default-features = false, optional = true }
once_cell = "1.16.0" once_cell = "1.17.0"
hashers = "1.0.1" hashers = "1.0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@ -540,6 +540,7 @@ pub trait Middleware: Sync + Send + Debug {
} }
// Geth `trace` support // Geth `trace` support
/// After replaying any previous transactions in the same block, /// After replaying any previous transactions in the same block,
/// Replays a transaction, returning the traces configured with passed options /// Replays a transaction, returning the traces configured with passed options
async fn debug_trace_transaction( async fn debug_trace_transaction(
@ -550,6 +551,16 @@ pub trait Middleware: Sync + Send + Debug {
self.inner().debug_trace_transaction(tx_hash, trace_options).await.map_err(FromErr::from) self.inner().debug_trace_transaction(tx_hash, trace_options).await.map_err(FromErr::from)
} }
/// Executes the given call and returns a number of possible traces for it
async fn debug_trace_call<T: Into<TypedTransaction> + Send + Sync>(
&self,
req: T,
block: Option<BlockId>,
trace_options: GethDebugTracingCallOptions,
) -> Result<GethTrace, ProviderError> {
self.inner().debug_trace_call(req, block, trace_options).await.map_err(FromErr::from)
}
// Parity `trace` support // Parity `trace` support
/// Executes the given call and returns a number of possible traces for it /// Executes the given call and returns a number of possible traces for it
@ -712,7 +723,8 @@ pub trait CeloMiddleware: Middleware {
} }
} }
pub use test_provider::{GOERLI, MAINNET, ROPSTEN}; #[allow(deprecated)]
pub use test_provider::{GOERLI, MAINNET, ROPSTEN, SEPOLIA};
/// Pre-instantiated Infura HTTP clients which rotate through multiple API keys /// Pre-instantiated Infura HTTP clients which rotate through multiple API keys
/// to prevent rate limits /// to prevent rate limits
@ -732,9 +744,13 @@ pub mod test_provider {
"5c812e02193c4ba793f8c214317582bd", "5c812e02193c4ba793f8c214317582bd",
]; ];
pub static GOERLI: Lazy<TestProvider> = Lazy::new(|| TestProvider::new(INFURA_KEYS, "goerli"));
pub static MAINNET: Lazy<TestProvider> = pub static MAINNET: Lazy<TestProvider> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "mainnet")); Lazy::new(|| TestProvider::new(INFURA_KEYS, "mainnet"));
pub static GOERLI: Lazy<TestProvider> = Lazy::new(|| TestProvider::new(INFURA_KEYS, "goerli"));
pub static SEPOLIA: Lazy<TestProvider> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "sepolia"));
#[deprecated = "Ropsten testnet has been deprecated in favor of Goerli or Sepolia."]
pub static ROPSTEN: Lazy<TestProvider> = pub static ROPSTEN: Lazy<TestProvider> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "ropsten")); Lazy::new(|| TestProvider::new(INFURA_KEYS, "ropsten"));
@ -745,16 +761,14 @@ pub mod test_provider {
} }
impl TestProvider { impl TestProvider {
pub fn new(keys: &'static [&'static str], network: &str) -> Self { pub fn new(keys: &'static [&'static str], network: impl Into<String>) -> Self {
Self { keys: Mutex::new(keys.iter().cycle()), network: network.to_owned() } Self { keys: keys.iter().cycle().into(), network: network.into() }
} }
pub fn url(&self) -> String { pub fn url(&self) -> String {
format!( let Self { network, keys } = self;
"https://{}.infura.io/v3/{}", let key = keys.lock().unwrap().next().unwrap();
self.network, format!("https://{network}.infura.io/v3/{key}")
self.keys.lock().unwrap().next().unwrap()
)
} }
pub fn provider(&self) -> Provider<Http> { pub fn provider(&self) -> Provider<Http> {

View File

@ -22,9 +22,10 @@ use ethers_core::{
types::{ types::{
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed}, transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, EIP1186ProofResponse, FeeHistory, Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, EIP1186ProofResponse, FeeHistory,
Filter, FilterBlockOption, GethDebugTracingOptions, GethTrace, Log, NameOrAddress, Filter, FilterBlockOption, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace,
Selector, Signature, Trace, TraceFilter, TraceType, Transaction, TransactionReceipt, Log, NameOrAddress, Selector, Signature, Trace, TraceFilter, TraceType, Transaction,
TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus, H256, U256, U64, TransactionReceipt, TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus,
H256, U256, U64,
}, },
utils, utils,
}; };
@ -351,8 +352,19 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
if inner.max_fee_per_gas.is_none() || inner.max_priority_fee_per_gas.is_none() { if inner.max_fee_per_gas.is_none() || inner.max_priority_fee_per_gas.is_none() {
let (max_fee_per_gas, max_priority_fee_per_gas) = let (max_fee_per_gas, max_priority_fee_per_gas) =
self.estimate_eip1559_fees(None).await?; self.estimate_eip1559_fees(None).await?;
inner.max_fee_per_gas = Some(max_fee_per_gas); // we want to avoid overriding the user if either of these
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); // are set. In order to do this, we refuse to override the
// `max_fee_per_gas` if already set.
// However, we must preserve the constraint that the tip
// cannot be higher than max fee, so we override user
// intent if that is so. We override by
// - first: if set, set to the min(current value, MFPG)
// - second, if still unset, use the RPC estimated amount
let mfpg = inner.max_fee_per_gas.get_or_insert(max_fee_per_gas);
inner
.max_priority_fee_per_gas
.map(|tip| std::cmp::min(tip, *mfpg))
.get_or_insert(max_priority_fee_per_gas);
}; };
} }
} }
@ -1048,6 +1060,20 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
self.request("debug_traceTransaction", [tx_hash, trace_options]).await self.request("debug_traceTransaction", [tx_hash, trace_options]).await
} }
/// Executes the given call and returns a number of possible traces for it
async fn debug_trace_call<T: Into<TypedTransaction> + Send + Sync>(
&self,
req: T,
block: Option<BlockId>,
trace_options: GethDebugTracingCallOptions,
) -> Result<GethTrace, ProviderError> {
let req = req.into();
let req = utils::serialize(&req);
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
let trace_options = utils::serialize(&trace_options);
self.request("debug_traceCall", [req, block, trace_options]).await
}
/// Executes the given call and returns a number of possible traces for it /// Executes the given call and returns a number of possible traces for it
async fn trace_call<T: Into<TypedTransaction> + Send + Sync>( async fn trace_call<T: Into<TypedTransaction> + Send + Sync>(
&self, &self,

View File

@ -9,7 +9,7 @@ use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error; use thiserror::Error;
/// A client contains two clients. /// A client containing two clients.
/// ///
/// One is used for _read_ operations /// One is used for _read_ operations
/// One is used for _write_ operations that consume gas `["eth_sendTransaction", /// One is used for _write_ operations that consume gas `["eth_sendTransaction",
@ -48,7 +48,7 @@ impl<Read, Write> RwClient<Read, Write> {
&self.r &self.r
} }
/// Returns the client used for read operations /// Returns the client used for write operations
pub fn write_client(&self) -> &Write { pub fn write_client(&self) -> &Write {
&self.w &self.w
} }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ethers-signers" name = "ethers-signers"
version = "1.0.2" version = "1.0.2"
edition = "2018" edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"] authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -16,7 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
ethers-core = { version = "^1.0.0", path = "../ethers-core", features = ["eip712"] } ethers-core = { version = "^1.0.0", path = "../ethers-core", features = ["eip712"] }
thiserror = { version = "1.0.37", default-features = false } thiserror = { version = "1.0.38", default-features = false }
coins-bip32 = "0.7.0" coins-bip32 = "0.7.0"
coins-bip39 = "0.7.0" coins-bip39 = "0.7.0"
coins-ledger = { version = "0.7.0", default-features = false, optional = true } coins-ledger = { version = "0.7.0", default-features = false, optional = true }
@ -28,7 +28,7 @@ rand = { version = "0.8.5", default-features = false }
yubihsm = { version = "0.41.0", features = ["secp256k1", "http", "usb"], optional = true } yubihsm = { version = "0.41.0", features = ["secp256k1", "http", "usb"], optional = true }
futures-util = { version = "^0.3", optional = true } futures-util = { version = "^0.3", optional = true }
futures-executor = { version = "^0.3", optional = true } futures-executor = { version = "^0.3", optional = true }
semver = { version = "1.0.14", optional = true } semver = { version = "1.0.16", optional = true }
trezor-client = { version = "0.0.7", optional = true, default-features = false, features = [ trezor-client = { version = "0.0.7", optional = true, default-features = false, features = [
"f_ethereum", "f_ethereum",
] } ] }

View File

@ -90,6 +90,13 @@ impl Wallet<SigningKey> {
let address = secret_key_to_address(&signer); let address = secret_key_to_address(&signer);
Self { signer, address, chain_id: 1 } Self { signer, address, chain_id: 1 }
} }
/// Creates a new Wallet instance from a raw scalar value (big endian).
pub fn from_bytes(bytes: &[u8]) -> Result<Self, WalletError> {
let signer = SigningKey::from_bytes(bytes)?;
let address = secret_key_to_address(&signer);
Ok(Self { signer, address, chain_id: 1 })
}
} }
impl PartialEq for Wallet<SigningKey> { impl PartialEq for Wallet<SigningKey> {
@ -305,4 +312,17 @@ mod tests {
Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed") Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed")
); );
} }
#[test]
fn key_from_bytes() {
let wallet: Wallet<SigningKey> =
"0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
let key_as_bytes = wallet.signer.to_bytes();
let wallet_from_bytes = Wallet::from_bytes(&key_as_bytes).unwrap();
assert_eq!(wallet.address, wallet_from_bytes.address);
assert_eq!(wallet.chain_id, wallet_from_bytes.chain_id);
assert_eq!(wallet.signer, wallet_from_bytes.signer);
}
} }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ethers-solc" name = "ethers-solc"
version = "1.0.2" version = "1.0.2"
edition = "2018" edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = [ authors = [
"Matthias Seitz <matthias.seitz@outlook.de>", "Matthias Seitz <matthias.seitz@outlook.de>",
@ -19,11 +19,11 @@ keywords = ["ethereum", "web3", "solc", "solidity", "ethers"]
ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features = false } ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features = false }
serde_json = "1.0.68" serde_json = "1.0.68"
serde = { version = "1.0.130", features = ["derive"] } serde = { version = "1.0.130", features = ["derive"] }
semver = { version = "1.0.14", features = ["serde"] } semver = { version = "1.0.16", features = ["serde"] }
walkdir = "2.3.2" walkdir = "2.3.2"
tokio = { version = "1.18", default-features = false, features = ["rt"] } tokio = { version = "1.18", default-features = false, features = ["rt"] }
futures-util = { version = "^0.3", optional = true } futures-util = { version = "^0.3", optional = true }
once_cell = "1.16.0" once_cell = "1.17.0"
regex = "1.7.0" regex = "1.7.0"
md-5 = "0.10.5" md-5 = "0.10.5"
thiserror = "1.0" thiserror = "1.0"
@ -31,7 +31,7 @@ hex = "0.4.3"
yansi = "0.5.1" yansi = "0.5.1"
glob = "0.3.0" glob = "0.3.0"
tracing = "0.1.37" tracing = "0.1.37"
num_cpus = "1.14.0" num_cpus = "1.15.0"
tiny-keccak = { version = "2.0.2", default-features = false } tiny-keccak = { version = "2.0.2", default-features = false }
tempfile = { version = "3.3.0", optional = true } tempfile = { version = "3.3.0", optional = true }
fs_extra = { version = "1.2.0", optional = true } fs_extra = { version = "1.2.0", optional = true }
@ -62,7 +62,7 @@ rand = "0.8.5"
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
tempfile = "3.3.0" tempfile = "3.3.0"
tokio = { version = "1.18", features = ["full"] } tokio = { version = "1.18", features = ["full"] }
serde_path_to_error = "0.1.8" serde_path_to_error = "0.1.9"
[[bench]] [[bench]]
name = "compile_many" name = "compile_many"

View File

@ -36,7 +36,6 @@ fn load_compiler_inputs() -> Vec<CompilerInput> {
let mut inputs = Vec::new(); let mut inputs = Vec::new();
for file in std::fs::read_dir(Path::new(&env!("CARGO_MANIFEST_DIR")).join("test-data/in")) for file in std::fs::read_dir(Path::new(&env!("CARGO_MANIFEST_DIR")).join("test-data/in"))
.unwrap() .unwrap()
.into_iter()
.take(5) .take(5)
{ {
let file = file.unwrap(); let file = file.unwrap();

View File

@ -1086,11 +1086,11 @@ mod tests {
let result: Result<SourceUnit, _> = serde_path_to_error::deserialize(deserializer); let result: Result<SourceUnit, _> = serde_path_to_error::deserialize(deserializer);
match result { match result {
Err(e) => { Err(e) => {
println!("... {} fail: {e}", path_str); println!("... {path_str} fail: {e}");
panic!(); panic!();
} }
Ok(_) => { Ok(_) => {
println!("... {} ok", path_str); println!("... {path_str} ok");
} }
} }
}) })

View File

@ -26,7 +26,10 @@ pub mod contract;
pub mod output_selection; pub mod output_selection;
pub mod serde_helpers; pub mod serde_helpers;
use crate::{ use crate::{
artifacts::output_selection::{ContractOutputSelection, OutputSelection}, artifacts::{
lowfidelity::NodeType,
output_selection::{ContractOutputSelection, OutputSelection},
},
filter::FilteredSources, filter::FilteredSources,
}; };
pub use bytecode::*; pub use bytecode::*;

View File

@ -457,7 +457,7 @@ impl Solc {
use sha2::Digest; use sha2::Digest;
let mut hasher = sha2::Sha256::new(); let mut hasher = sha2::Sha256::new();
hasher.update(&content); hasher.update(content);
let checksum_calc = &hasher.finalize()[..]; let checksum_calc = &hasher.finalize()[..];
let checksum_found = &RELEASES.0.get_checksum(&version).expect("checksum not found"); let checksum_found = &RELEASES.0.get_checksum(&version).expect("checksum not found");

View File

@ -649,7 +649,7 @@ impl Graph {
return Vec::new() return Vec::new()
} }
let mut result = sets.pop().cloned().expect("not empty; qed.").clone(); let mut result = sets.pop().cloned().expect("not empty; qed.");
if !sets.is_empty() { if !sets.is_empty() {
result.retain(|item| sets.iter().all(|set| set.contains(item))); result.retain(|item| sets.iter().all(|set| set.contains(item)));
} }

File diff suppressed because it is too large Load Diff

81
examples/README.md Normal file
View File

@ -0,0 +1,81 @@
# Examples
- [ ] Address book
- [ ] Anvil
- [ ] Boot anvil
- [ ] Deploy contracts
- [x] Fork
- [ ] Testing
- [x] Big numbers
- [x] Comparison and equivalence
- [x] Conversion
- [x] Creating Instances
- [x] Math operations
- [x] Utilities
- [ ] Contracts
- [x] Abigen
- [x] Compile
- [ ] Creating Instances
- [x] Deploy Anvil
- [x] Deploy from ABI and bytecode
- [x] Deploy Moonbeam
- [x] Events
- [x] Events with meta
- [ ] Methods
- [ ] Events
- [ ] Logs and filtering
- [ ] Solidity topics
- [ ] Middleware
- [x] Builder
- [x] Create custom middleware
- [x] Gas escalator
- [x] Gas oracle
- [x] Nonce manager
- [x] Policy
- [x] Signer
- [ ] Time lag
- [ ] Transformer
- [ ] Providers
- [ ] Http
- [x] IPC
- [ ] Mock
- [x] Quorum
- [ ] Retry
- [x] RW
- [ ] WS
- [ ] Queries
- [ ] Blocks
- [x] Contracts
- [ ] Events
- [x] Paginated logs
- [x] UniswapV2 pair
- [ ] Transactions
- [x] Subscriptions
- [x] Watch blocks
- [x] Subscribe events by type
- [x] Subscribe logs
- [ ] Transactions
- [x] Call override
- [ ] Create raw transaction
- [ ] Create typed transaction
- [x] Decode input
- [ ] EIP-1559
- [x] ENS
- [ ] Estimate gas
- [ ] Get gas price
- [x] Get gas price USD
- [x] Remove liquidity
- [ ] Set gas for a transaction
- [ ] Send raw transaction
- [ ] Send typed transaction
- [x] Trace
- [ ] Transaction receipt
- [ ] Transaction status
- [x] Transfer ETH
- [x] Wallets
- [x] Mnemonic
- [x] Ledger
- [x] Local
- [x] Permit hash
- [x] Sign message
- [x] Trezor
- [x] Yubi

10
examples/anvil/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "examples-anvil"
version = "1.0.2"
authors = ["Andrea Simeoni <andreasimeoni84@gmail.com>"]
edition = "2021"
[dev-dependencies]
ethers = { path = "../..", version = "1.0.0" }
eyre = "0.6"
tokio = { version = "1.18", features = ["full"] }

0
examples/anvil/README.md Normal file
View File

View File

@ -1,9 +1,10 @@
//! Spawn an [anvil](https://github.com/foundry-rs/foundry/tree/master/anvil) instance in forking mode //! Spawn an [anvil](https://github.com/foundry-rs/foundry/tree/master/anvil) instance in forking mode
use ethers::utils::Anvil; use ethers::utils::Anvil;
use eyre::Result;
#[tokio::main] #[tokio::main]
async fn main() -> eyre::Result<()> { async fn main() -> Result<()> {
// ensure `anvil` is available in $PATH // ensure `anvil` is available in $PATH
let anvil = let anvil =
Anvil::new().fork("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").spawn(); Anvil::new().fork("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").spawn();

View File

@ -0,0 +1,10 @@
[package]
name = "examples-big-numbers"
version = "1.0.2"
authors = ["Andrea Simeoni <andreasimeoni84@gmail.com>"]
edition = "2021"
[dev-dependencies]
#ethers-core = { version = "^1.0.0", path = "../../ethers-core" }
ethers = { path = "../..", version = "1.0.0" }

View File

@ -0,0 +1,7 @@
# Big numbers
Ethereum uses big numbers (also known as "bignums" or "arbitrary-precision integers") to represent certain values in its codebase and in blockchain transactions. This is necessary because [the EVM](https://ethereum.org/en/developers/docs/evm) operates on a 256-bit word size, which is different from the usual 32-bit or 64-bit of modern machines. This was chosen for the ease of use with 256-bit cryptography (such as Keccak-256 hashes or secp256k1 signatures).
It is worth noting that Ethereum is not the only blockchain or cryptocurrency that uses big numbers. Many other blockchains and cryptocurrencies also use big numbers to represent values in their respective systems.
## Utilities
In order to create an application, it is often necessary to convert between the representation of values that is easily understood by humans (such as ether) and the machine-readable form that is used by contracts and math functions (such as wei). This is particularly important when working with Ethereum, as certain values, such as balances and gas prices, must be expressed in wei when sending transactions, even if they are displayed to the user in a different format, such as ether or gwei. To help with this conversion, ethers-rs provides two functions, `parse_units` and `format_units`, which allow you to easily convert between human-readable and machine-readable forms of values. `parse_units` can be used to convert a string representing a value in ether, such as "1.1", into a big number in wei, which can be used in contracts and math functions. `format_units` can be used to convert a big number value into a human-readable string, which is useful for displaying values to users.

View File

@ -0,0 +1,32 @@
use ethers::types::U256;
fn main() {
// a == b
let a = U256::from(100_u32);
let b = U256::from(100_u32);
assert!(a == b);
// a < b
let a = U256::from(1_u32);
let b = U256::from(100_u32);
assert!(a < b);
// a <= b
let a = U256::from(100_u32);
let b = U256::from(100_u32);
assert!(a <= b);
// a > b
let a = U256::from(100_u32);
let b = U256::from(1_u32);
assert!(a > b);
// a >= b
let a = U256::from(100_u32);
let b = U256::from(100_u32);
assert!(a >= b);
// a == 0
let a = U256::zero();
assert!(a.is_zero());
}

View File

@ -0,0 +1,23 @@
use ethers::{types::U256, utils::format_units};
fn main() {
let num = U256::from(42_u8);
let a: u128 = num.as_u128();
assert_eq!(a, 42);
let b: u64 = num.as_u64();
assert_eq!(b, 42);
let c: u32 = num.as_u32();
assert_eq!(c, 42);
let d: usize = num.as_usize();
assert_eq!(d, 42);
let e: String = num.to_string();
assert_eq!(e, "42");
let f: String = format_units(num, 4).unwrap();
assert_eq!(f, "0.0042");
}

View File

@ -0,0 +1,47 @@
use ethers::{
types::{serde_helpers::Numeric, U256},
utils::{parse_units, ParseUnits},
};
fn main() {
// From strings
let a = U256::from_dec_str("42").unwrap();
assert_eq!(format!("{a:?}"), "42");
let amount = "42";
let units = 4;
let pu: ParseUnits = parse_units(amount, units).unwrap();
let b = U256::from(pu);
assert_eq!(format!("{b:?}"), "420000");
// From numbers
let c = U256::from(42_u8);
assert_eq!(format!("{c:?}"), "42");
let d = U256::from(42_u16);
assert_eq!(format!("{d:?}"), "42");
let e = U256::from(42_u32);
assert_eq!(format!("{e:?}"), "42");
let f = U256::from(42_u64);
assert_eq!(format!("{f:?}"), "42");
let g = U256::from(42_u128);
assert_eq!(format!("{g:?}"), "42");
let h = U256::from(0x2a);
assert_eq!(format!("{h:?}"), "42");
let i: U256 = 42.into();
assert_eq!(format!("{i:?}"), "42");
// From `Numeric`
let num: Numeric = Numeric::U256(U256::one());
let l = U256::from(num);
assert_eq!(format!("{l:?}"), "1");
let num: Numeric = Numeric::Num(42);
let m = U256::from(num);
assert_eq!(format!("{m:?}"), "42");
}

View File

@ -0,0 +1,49 @@
use ethers::{types::U256, utils::format_units};
use std::ops::{Div, Mul};
fn main() {
let a = U256::from(10);
let b = U256::from(2);
// addition
let sum = a + b;
assert_eq!(sum, U256::from(12));
// subtraction
let difference = a - b;
assert_eq!(difference, U256::from(8));
// multiplication
let product = a * b;
assert_eq!(product, U256::from(20));
// division
let quotient = a / b;
assert_eq!(quotient, U256::from(5));
// modulo
let remainder = a % b;
assert_eq!(remainder, U256::zero()); // equivalent to `U256::from(0)`
// exponentiation
let power = a.pow(b);
assert_eq!(power, U256::from(100));
// powers of 10 can also be expressed like this:
let power_of_10 = U256::exp10(2);
assert_eq!(power_of_10, U256::from(100));
// Multiply two 'ether' numbers:
// Big numbers are integers, that can represent fixed point numbers.
// For instance, 1 ether has 18 fixed
// decimal places (1.000000000000000000), and its big number
// representation is 10^18 = 1000000000000000000.
// When we multiply such numbers we are summing up their exponents.
// So if we multiply 10^18 * 10^18 we get 10^36, that is obviously incorrect.
// In order to get the correct result we need to divide by 10^18.
let eth1 = U256::from(10_000000000000000000_u128); // 10 ether
let eth2 = U256::from(20_000000000000000000_u128); // 20 ether
let base = U256::from(10).pow(18.into());
let mul = eth1.mul(eth2).div(base); // We also divide by 10^18
let s: String = format_units(mul, "ether").unwrap();
assert_eq!(s, "200.000000000000000000"); // 200
}

View File

@ -0,0 +1,92 @@
use ethers::{
types::U256,
utils::{format_units, parse_units, ParseUnits},
};
fn main() {
parse_units_example();
format_units_example();
}
/// DApps business logics handles big numbers in 'wei' units (i.e. sending transactions, on-chain
/// math, etc.). We provide convenient methods to map user inputs (usually in 'ether' or 'gwei')
/// into 'wei' format.
fn parse_units_example() {
let pu: ParseUnits = parse_units("1.0", "wei").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::one());
let pu: ParseUnits = parse_units("1.0", "kwei").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000));
let pu: ParseUnits = parse_units("1.0", "mwei").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000000));
let pu: ParseUnits = parse_units("1.0", "gwei").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000000000));
let pu: ParseUnits = parse_units("1.0", "szabo").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000000000000_u128));
let pu: ParseUnits = parse_units("1.0", "finney").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000000000000000_u128));
let pu: ParseUnits = parse_units("1.0", "ether").unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000000000000000000_u128));
let pu: ParseUnits = parse_units("1.0", 18).unwrap();
let num = U256::from(pu);
assert_eq!(num, U256::from(1000000000000000000_u128));
}
/// DApps business logics handles big numbers in 'wei' units (i.e. sending transactions, on-chain
/// math, etc.). On the other hand it is useful to convert big numbers into user readable formats
/// when displaying on a UI. Generally dApps display numbers in 'ether' and 'gwei' units,
/// respectively for displaying amounts and gas. The `format_units` function will format a big
/// number into a user readable string.
fn format_units_example() {
// 1 ETHER = 10^18 WEI
let one_ether = U256::from(1000000000000000000_u128);
let num: String = format_units(one_ether, "wei").unwrap();
assert_eq!(num, "1000000000000000000.0");
let num: String = format_units(one_ether, "gwei").unwrap();
assert_eq!(num, "1000000000.000000000");
let num: String = format_units(one_ether, "ether").unwrap();
assert_eq!(num, "1.000000000000000000");
// 1 GWEI = 10^9 WEI
let one_gwei = U256::from(1000000000_u128);
let num: String = format_units(one_gwei, 0).unwrap();
assert_eq!(num, "1000000000.0");
let num: String = format_units(one_gwei, "wei").unwrap();
assert_eq!(num, "1000000000.0");
let num: String = format_units(one_gwei, "kwei").unwrap();
assert_eq!(num, "1000000.000");
let num: String = format_units(one_gwei, "mwei").unwrap();
assert_eq!(num, "1000.000000");
let num: String = format_units(one_gwei, "gwei").unwrap();
assert_eq!(num, "1.000000000");
let num: String = format_units(one_gwei, "szabo").unwrap();
assert_eq!(num, "0.001000000000");
let num: String = format_units(one_gwei, "finney").unwrap();
assert_eq!(num, "0.000001000000000");
let num: String = format_units(one_gwei, "ether").unwrap();
assert_eq!(num, "0.000000001000000000");
}

View File

@ -0,0 +1,19 @@
[package]
name = "examples-contracts"
version = "1.0.2"
authors = ["Andrea Simeoni <andreasimeoni84@gmail.com>"]
edition = "2021"
[features]
default = ["legacy"]
legacy = []
[dev-dependencies]
ethers = { path = "../..", version = "1.0.0", features = ["abigen", "ethers-solc", "legacy", "rustls", "ws"] }
eyre = "0.6"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.64"
tokio = { version = "1.18", features = ["macros"] }

View File

@ -0,0 +1,39 @@
# Contracts
In this guide, we will go over some examples of using ethers-rs to work with contracts, including using abigen to generate Rust bindings for a contract, listening for contract events, calling contract methods, and instantiating contracts.
## Generating Rust bindings with abigen
To use a contract with ethers-rs, you will need to generate Rust bindings using the abigen tool. abigen is included with the ethers-rs library and can be used to generate Rust bindings for any Solidity contract.
### Generate a Rust file
This method takes a smart contract's Application Binary Interface (ABI) file and generates a Rust file to interact with it. This is useful if the smart contract is referenced in different places in a project. File generation from ABI can also be easily included as a build step of your application.
Running the code below will generate a file called `token.rs` containing the bindings inside, which exports an `ERC20Token` struct, along with all its events and methods. Put into a `build.rs` file this will generate the bindings during cargo build.
```rust
Abigen::new("ERC20Token", "./abi.json")?.generate()?.write_to_file("token.rs")?;
```
### Generate inline Rust bindings
This method takes a smart contract's solidity definition and generates inline Rust code to interact with it. This is useful for fast prototyping and for tight scoped use-cases of your contracts. Inline Rust generation uses the `abigen!` macro to expand Rust contract bindings.
Running the code below will generate bindings for the `ERC20Token` struct, along with all its events and methods.
```rust
abigen!(
ERC20Token,
r#"[
function approve(address spender, uint256 amount) external returns (bool)
event Transfer(address indexed from, address indexed to, uint256 value)
event Approval(address indexed owner, address indexed spender, uint256 value)
]"#,
);
```
Another way to get the same result, is to provide the ABI contract's definition as follows.
```rust
abigen!(ERC20Token, "./abi.json",);
```
## Contract instances
## Contract methods
## Contract events

View File

@ -0,0 +1,153 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "guy", "type": "address" },
{ "name": "wad", "type": "uint256" }
],
"name": "approve",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "src", "type": "address" },
{ "name": "dst", "type": "address" },
{ "name": "wad", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "wad", "type": "uint256" }],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "dst", "type": "address" },
{ "name": "wad", "type": "uint256" }
],
"name": "transfer",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "", "type": "address" },
{ "name": "", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "src", "type": "address" },
{ "indexed": true, "name": "guy", "type": "address" },
{ "indexed": false, "name": "wad", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "src", "type": "address" },
{ "indexed": true, "name": "dst", "type": "address" },
{ "indexed": false, "name": "wad", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "dst", "type": "address" },
{ "indexed": false, "name": "wad", "type": "uint256" }
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "src", "type": "address" },
{ "indexed": false, "name": "wad", "type": "uint256" }
],
"name": "Withdrawal",
"type": "event"
}
]

View File

@ -0,0 +1,77 @@
use ethers::{
prelude::{abigen, Abigen},
providers::{Http, Provider},
types::Address,
};
use eyre::Result;
use std::sync::Arc;
/// Abigen is used to generate Rust code to interact with smart contracts on the blockchain.
/// It provides a way to encode and decode data that is passed to and from smart contracts.
/// The output of abigen is Rust code, that is bound to the contract's interface, allowing
/// developers to call its methods to read/write on-chain state and subscribe to realtime events.
///
/// The abigen tool can be used in two ways, addressing different use-cases scenarios and developer
/// taste:
///
/// 1. **Rust file generation:** takes a smart contract's Application Binary Interface (ABI)
/// file and generates a Rust file to interact with it. This is useful if the smart contract is
/// referenced in different places in a project. File generation from ABI can also be easily
/// included as a build step of your application.
/// 2. **Rust inline generation:** takes a smart contract's solidity definition and generates inline
/// Rust code to interact with it. This is useful for fast prototyping and for tight scoped
/// use-cases of your contracts.
/// 3. **Rust inline generation from ABI:** similar to the previous point but instead of Solidity
/// code takes in input a smart contract's Application Binary Interface (ABI) file.
#[tokio::main]
async fn main() -> Result<()> {
rust_file_generation()?;
rust_inline_generation().await?;
rust_inline_generation_from_abi();
Ok(())
}
fn rust_file_generation() -> Result<()> {
let base_dir = "./examples/contracts/examples/abi";
Abigen::new("IERC20", format!("{base_dir}/IERC20.json"))?
.generate()?
.write_to_file(format!("{base_dir}/ierc20.rs"))?;
Ok(())
}
fn rust_inline_generation_from_abi() {
abigen!(IERC20, "./examples/contracts/examples/abi/IERC20.json");
}
async fn rust_inline_generation() -> Result<()> {
// The abigen! macro expands the contract's code in the current scope
// so that you can interface your Rust program with the blockchain
// counterpart of the contract.
abigen!(
IERC20,
r#"[
function totalSupply() external view returns (uint256)
function balanceOf(address account) external view returns (uint256)
function transfer(address recipient, uint256 amount) external returns (bool)
function allowance(address owner, address spender) external view returns (uint256)
function approve(address spender, uint256 amount) external returns (bool)
function transferFrom( address sender, address recipient, uint256 amount) external returns (bool)
event Transfer(address indexed from, address indexed to, uint256 value)
event Approval(address indexed owner, address indexed spender, uint256 value)
]"#,
);
const RPC_URL: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
let provider = Provider::<Http>::try_from(RPC_URL)?;
let client = Arc::new(provider);
let address: Address = WETH_ADDRESS.parse()?;
let contract = IERC20::new(address, client);
if let Ok(total_supply) = contract.total_supply().call().await {
println!("WETH total supply is {total_supply:?}");
}
Ok(())
}

View File

@ -1,11 +1,14 @@
use ethers::{contract::Abigen, solc::Solc}; use ethers::{prelude::Abigen, solc::Solc};
use eyre::Result;
fn main() -> eyre::Result<()> { fn main() -> Result<()> {
let mut args = std::env::args(); let mut args = std::env::args();
args.next().unwrap(); // skip program name args.next().unwrap(); // skip program name
let contract_name = args.next().unwrap_or_else(|| "SimpleStorage".to_owned()); let contract_name = args.next().unwrap_or_else(|| "SimpleStorage".to_owned());
let contract: String = args.next().unwrap_or_else(|| "examples/contract.sol".to_owned()); let contract: String = args
.next()
.unwrap_or_else(|| "examples/contracts/examples/contracts/contract.sol".to_owned());
println!("Generating bindings for {contract}\n"); println!("Generating bindings for {contract}\n");

View File

@ -1,3 +1,4 @@
// SPDX-License-Identifier: Unlicense
pragma solidity >=0.4.24; pragma solidity >=0.4.24;
contract SimpleStorage { contract SimpleStorage {

View File

@ -1,10 +1,13 @@
use ethers::{ use ethers::{
prelude::*, contract::{abigen, ContractFactory},
solc::{Project, ProjectPathsConfig}, core::utils::Anvil,
utils::Anvil, middleware::SignerMiddleware,
providers::{Http, Provider},
signers::{LocalWallet, Signer},
solc::{Artifact, Project, ProjectPathsConfig},
}; };
use eyre::Result; use eyre::Result;
use std::{convert::TryFrom, path::PathBuf, sync::Arc, time::Duration}; use std::{path::PathBuf, sync::Arc, time::Duration};
// Generate the type-safe contract bindings by providing the ABI // Generate the type-safe contract bindings by providing the ABI
// definition in human readable format // definition in human readable format

View File

@ -1,4 +1,10 @@
use ethers::{prelude::*, utils::Anvil}; use ethers::{
contract::abigen,
core::utils::Anvil,
middleware::SignerMiddleware,
providers::{Http, Provider},
signers::{LocalWallet, Signer},
};
use eyre::Result; use eyre::Result;
use std::{convert::TryFrom, sync::Arc, time::Duration}; use std::{convert::TryFrom, sync::Arc, time::Duration};

View File

@ -1,4 +1,11 @@
use ethers::{prelude::*, utils::Anvil}; use ethers::{
contract::{abigen, ContractFactory},
core::utils::Anvil,
middleware::SignerMiddleware,
providers::{Http, Provider},
signers::{LocalWallet, Signer},
solc::Solc,
};
use eyre::Result; use eyre::Result;
use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration}; use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
@ -6,7 +13,7 @@ use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
// definition // definition
abigen!( abigen!(
SimpleContract, SimpleContract,
"./examples/contract_abi.json", "./examples/contracts/examples/abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize) event_derives(serde::Deserialize, serde::Serialize)
); );
@ -17,8 +24,8 @@ async fn main() -> Result<()> {
let anvil = Anvil::new().spawn(); let anvil = Anvil::new().spawn();
// set the path to the contract, `CARGO_MANIFEST_DIR` points to the directory containing the // set the path to the contract, `CARGO_MANIFEST_DIR` points to the directory containing the
// manifest of `ethers`. which will be `../` relative to this file // manifest of `example/contracts`. which will be `../` relative to this file
let source = Path::new(&env!("CARGO_MANIFEST_DIR")).join("examples/contract.sol"); let source = Path::new(&env!("CARGO_MANIFEST_DIR")).join("examples/contracts/contract.sol");
let compiled = Solc::default().compile_source(source).expect("Could not compile contracts"); let compiled = Solc::default().compile_source(source).expect("Could not compile contracts");
let (abi, bytecode, _runtime_bytecode) = let (abi, bytecode, _runtime_bytecode) =
compiled.find("SimpleStorage").expect("could not find contract").into_parts_or_default(); compiled.find("SimpleStorage").expect("could not find contract").into_parts_or_default();

View File

@ -1,8 +1,8 @@
use ethers::prelude::*; use ethers::contract::abigen;
abigen!( abigen!(
SimpleContract, SimpleContract,
"./examples/contract_abi.json", "./examples/contracts/examples/abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize) event_derives(serde::Deserialize, serde::Serialize)
); );
@ -19,7 +19,9 @@ abigen!(
#[tokio::main] #[tokio::main]
#[cfg(feature = "legacy")] #[cfg(feature = "legacy")]
async fn main() -> eyre::Result<()> { async fn main() -> eyre::Result<()> {
use ethers::prelude::*;
use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration}; use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
const MOONBEAM_DEV_ENDPOINT: &str = "http://localhost:9933"; const MOONBEAM_DEV_ENDPOINT: &str = "http://localhost:9933";
// set the path to the contract, `CARGO_MANIFEST_DIR` points to the directory containing the // set the path to the contract, `CARGO_MANIFEST_DIR` points to the directory containing the

View File

@ -0,0 +1,68 @@
use ethers::{
contract::abigen,
core::types::Address,
providers::{Provider, StreamExt, Ws},
};
use eyre::Result;
use std::sync::Arc;
abigen!(
IERC20,
r#"[
event Transfer(address indexed from, address indexed to, uint256 value)
event Approval(address indexed owner, address indexed spender, uint256 value)
]"#,
);
const WSS_URL: &str = "wss://mainnet.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27";
const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
#[tokio::main]
async fn main() -> Result<()> {
let provider = Provider::<Ws>::connect(WSS_URL).await?;
let client = Arc::new(provider);
let address: Address = WETH_ADDRESS.parse()?;
let contract = IERC20::new(address, client);
listen_all_events(&contract).await?;
listen_specific_events(&contract).await?;
Ok(())
}
/// Given a contract instance, subscribe to all possible events.
/// This allows to centralize the event handling logic and dispatch
/// proper actions.
///
/// Note that all event bindings have been generated
/// by abigen. Feel free to investigate the abigen expanded code to
/// better understand types and functionalities.
async fn listen_all_events(contract: &IERC20<Provider<Ws>>) -> Result<()> {
let events = contract.events().from_block(16232696);
let mut stream = events.stream().await?.take(1);
while let Some(Ok(evt)) = stream.next().await {
match evt {
IERC20Events::ApprovalFilter(f) => println!("{f:?}"),
IERC20Events::TransferFilter(f) => println!("{f:?}"),
}
}
Ok(())
}
/// Given a contract instance subscribe to a single type of event.
///
/// Note that all event bindings have been generated
/// by abigen. Feel free to investigate the abigen expanded code to
/// better understand types and functionalities.
async fn listen_specific_events(contract: &IERC20<Provider<Ws>>) -> Result<()> {
let events = contract.event::<ApprovalFilter>().from_block(16232696);
let mut stream = events.stream().await?.take(1);
while let Some(Ok(f)) = stream.next().await {
println!("ApprovalFilter event: {f:?}");
}
Ok(())
}

View File

@ -1,4 +1,8 @@
use ethers::prelude::*; use ethers::{
contract::abigen,
core::types::Address,
providers::{Provider, StreamExt, Ws},
};
use eyre::Result; use eyre::Result;
use std::sync::Arc; use std::sync::Arc;
@ -11,9 +15,6 @@ abigen!(
]"#, ]"#,
); );
// In order to run this example you need to include Ws and TLS features
// Run this example with
// `cargo run -p ethers --example subscribe_contract_events_with_meta --features="ws","rustls"`
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let client = let client =
@ -27,8 +28,8 @@ async fn main() -> Result<()> {
let weth = ERC20::new(address, Arc::clone(&client)); let weth = ERC20::new(address, Arc::clone(&client));
// Subscribe Transfer events // Subscribe Transfer events
let events = weth.events(); let events = weth.events().from_block(16232698);
let mut stream = events.stream().await?.with_meta(); let mut stream = events.stream().await?.with_meta().take(1);
while let Some(Ok((event, meta))) = stream.next().await { while let Some(Ok((event, meta))) = stream.next().await {
println!("src: {:?}, dst: {:?}, wad: {:?}", event.src, event.dst, event.wad); println!("src: {:?}, dst: {:?}, wad: {:?}", event.src, event.dst, event.wad);

View File

@ -0,0 +1,2 @@
#[tokio::main]
async fn main() {}

View File

@ -0,0 +1,2 @@
#[tokio::main]
async fn main() {}

View File

@ -1,7 +1,17 @@
//! Main entry point for ContractMonitor //! Main entry point for ContractMonitor
use ethers::{prelude::*, utils::Anvil}; use std::{sync::Arc, time::Duration};
use std::{convert::TryFrom, sync::Arc, time::Duration};
use ethers::{
contract::abigen,
core::{
types::{Address, U256},
utils::Anvil,
},
middleware::SignerMiddleware,
providers::{Http, Provider},
signers::LocalWallet,
};
abigen!(VerifierContract, "ethers-contract/tests/solidity-contracts/verifier_abi.json"); abigen!(VerifierContract, "ethers-contract/tests/solidity-contracts/verifier_abi.json");

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ethers-wasm" name = "ethers-wasm"
version = "0.1.0" version = "0.1.0"
edition = "2018" edition = "2021"
rust-version = "1.64" rust-version = "1.64"
authors = ["Matthias Seitz <matthias.seitz@outlook.de>"] authors = ["Matthias Seitz <matthias.seitz@outlook.de>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@ -0,0 +1 @@
[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]

View File

@ -24,7 +24,7 @@ macro_rules! log {
abigen!( abigen!(
SimpleContract, SimpleContract,
"./../contract_abi.json", "./abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize) event_derives(serde::Deserialize, serde::Serialize)
); );

View File

@ -16,7 +16,7 @@ wasm_bindgen_test_configure!(run_in_browser);
// definition in human readable format // definition in human readable format
abigen!( abigen!(
SimpleContract, SimpleContract,
"../contract_abi.json", "./abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize) event_derives(serde::Deserialize, serde::Serialize)
); );

View File

@ -0,0 +1,13 @@
[package]
name = "examples-events"
version = "1.0.2"
authors = ["Andrea Simeoni <andreasimeoni84@gmail.com>"]
edition = "2021"
[dev-dependencies]
ethers = { path = "../..", version = "1.0.0" }
eyre = "0.6"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.64"
tokio = { version = "1.18", features = ["macros"] }

View File

Some files were not shown because too many files have changed in this diff Show More