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
with:
cache-on-failure: true
- name: Build 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
- name: Build and run all examples
run: |
export PATH=$HOME/bin:$PATH
chmod +x ./scripts/examples.sh

5
.gitignore vendored
View File

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

View File

@ -4,6 +4,8 @@
### 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)
- `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.
@ -103,6 +105,7 @@
- [#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.
- [#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
@ -272,6 +275,7 @@
- `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)
- [1983](https://github.com/gakonst/ethers-rs/pull/1983) Added a `from_bytes` function for the `Wallet` type.
### 0.6.0
@ -340,6 +344,8 @@
### Unreleased
- Added `openssl` and `rustls` feature flags
[#1961](https://github.com/gakonst/ethers-rs/pull/1961)
- Relax Clone requirements when Arc<Middleware> is used
[#1183](https://github.com/gakonst/ethers-rs/pull/1183)
- 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 build --all-features
cargo test --all-features
cargo +nightly clippy --all-features
```
### Tests

136
Cargo.lock generated
View File

@ -1420,6 +1420,7 @@ dependencies = [
"hex",
"hex-literal",
"k256",
"num_enum",
"once_cell",
"open-fastrlp",
"proc-macro2",
@ -1626,6 +1627,111 @@ dependencies = [
"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]]
name = "eyre"
version = "0.6.8"
@ -2577,6 +2683,27 @@ dependencies = [
"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]]
name = "num_threads"
version = "0.1.3"
@ -3750,9 +3877,9 @@ dependencies = [
[[package]]
name = "serial_test"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92761393ee4dc3ff8f4af487bd58f4307c9329bbedea02cac0089ad9c411e153"
checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2"
dependencies = [
"dashmap",
"futures",
@ -3764,11 +3891,10 @@ dependencies = [
[[package]]
name = "serial_test_derive"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b6f5d1c3087fb119617cff2966fe3808a80e5eb59a8c1601d5994d66f4346a5"
checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn",

View File

@ -2,7 +2,7 @@
name = "ethers"
version = "1.0.2"
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>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
@ -21,7 +21,8 @@ members = [
"ethers-middleware",
"ethers-etherscan",
"ethers-solc",
"examples/ethers-wasm",
# Example crates
"examples/*",
]
default-members = [
@ -63,12 +64,14 @@ eip712 = ["ethers-contract/eip712", "ethers-core/eip712"]
ws = ["ethers-providers/ws"]
ipc = ["ethers-providers/ipc"]
rustls = [
"ethers-middleware/rustls",
"ethers-providers/rustls",
"ethers-etherscan/rustls",
"ethers-contract/rustls",
"ethers-solc/rustls",
]
openssl = [
"ethers-middleware/openssl",
"ethers-providers/openssl",
"ethers-etherscan/openssl",
"ethers-contract/openssl",
@ -84,6 +87,7 @@ abigen = ["ethers-contract/abigen"]
### abigen without reqwest
abigen-offline = ["ethers-contract/abigen-offline"]
## solc
ethers-solc = ["dep:ethers-solc", "ethers-etherscan/ethers-solc"]
solc-full = ["ethers-solc", "ethers-solc/full"]
solc-tests = ["ethers-solc", "ethers-solc/tests"]
solc-sha2-asm = ["ethers-solc", "ethers-solc/asm"]
@ -125,62 +129,3 @@ bytes = "1.3.0"
# Tell `rustc` to optimize for small code size.
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`
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

View File

@ -11,7 +11,7 @@ repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies]
once_cell = "1.16.0"
once_cell = "1.17.0"
serde = { version = "1.0", features = ["derive"] }
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_json = { version = "1.0.64", 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 }
futures-util = { version = "^0.3" }
hex = { version = "0.4.3", default-features = false, features = ["std"] }

View File

@ -1,11 +1,12 @@
[package]
name = "ethers-contract-abigen"
version = "1.0.2"
edition = "2021"
rust-version = "1.64"
authors = [
"Nicholas Rodrigues Lordello <nlordell@gmail.com>",
"Georgios Konstantopoulos <me@gakonst.com>",
]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "Code generation for type-safe bindings to Ethereum smart contracts"
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 {
fn from(abigens: Vec<Abigen>) -> Self {
Self { abigens }
@ -372,6 +378,16 @@ impl MultiBindings {
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 {
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).
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.
Etherscan(Address),
@ -54,7 +57,12 @@ impl Source {
///
/// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a address or URL of a
/// 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
/// and path (defaulting to the latest version and `index.js`). The contract ABI will be
/// retrieved through `unpkg.io`.
@ -99,6 +107,12 @@ impl Source {
match url.scheme() {
"file" => Ok(Source::local(source)),
"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(
url.path()
.rsplit('/')
@ -119,6 +133,7 @@ impl Source {
),
_ => Ok(Source::Http(url)),
},
"bscscan" => Source::bscscan(url.path()),
"etherscan" => Source::etherscan(url.path()),
"polygonscan" => Source::polygonscan(url.path()),
"snowtrace" => Source::snowtrace(url.path()),
@ -140,6 +155,16 @@ impl Source {
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.
pub fn etherscan<S>(address: S) -> Result<Self>
where
@ -187,6 +212,7 @@ impl Source {
match self {
Source::Local(path) => get_local_contract(path),
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::Polygonscan(_) => panic!("Polygonscan 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 {
Source::Local(path) => get_local_contract(path),
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::Polygonscan(address) => get_etherscan_contract(*address, "polygonscan.com"),
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.
let api_key = {
let key_res = match domain {
"bscscan.com" => env::var("BSCSCAN_API_KEY").ok(),
"etherscan.io" => env::var("ETHERSCAN_API_KEY").ok(),
"polygonscan.com" => env::var("POLYGONSCAN_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",
Source::http("https://my.domain.eth/path/to/Contract.json").unwrap(),
),
(
"bscscan:0x0001020304050607080910111213141516171819",
Source::bscscan("0x0001020304050607080910111213141516171819").unwrap(),
),
(
"etherscan:0x0001020304050607080910111213141516171819",
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
@ -323,6 +355,10 @@ mod tests {
"snowtrace:0x0001020304050607080910111213141516171819",
Source::snowtrace("0x0001020304050607080910111213141516171819").unwrap(),
),
(
"https://bscscan.io/address/0x0001020304050607080910111213141516171819",
Source::bscscan("0x0001020304050607080910111213141516171819").unwrap(),
),
(
"https://etherscan.io/address/0x0001020304050607080910111213141516171819",
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),

View File

@ -1,11 +1,12 @@
[package]
name = "ethers-contract-derive"
version = "1.0.2"
edition = "2021"
rust-version = "1.64"
authors = [
"Nicholas Rodrigues Lordello <nlordell@gmail.com>",
"Georgios Konstantopoulos <me@gakonst.com>",
]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "Proc macro for type-safe bindings generation to Ethereum and Celo smart contracts"
homepage = "https://docs.rs/ethers"
@ -17,7 +18,7 @@ proc-macro = true
[dependencies]
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"
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
/// of a given block number.
///
@ -615,6 +636,45 @@ impl<M: Middleware> Multicall<M> {
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
/// without detokenization.
///

View File

@ -15,7 +15,7 @@ mod eth_tests {
use ethers_derive_eip712::*;
use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt};
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]
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[6], false);
let valid_balances = [
U256::from(10_000_000_000_000_000_000_000u128),
U256::from(10_000_000_000_000_000_000_000u128),
U256::from(10_000_000_000_000_000_000_000u128),
];
let balances: (U256, U256, U256) = multicall.call().await.unwrap();
assert_eq!(balances.0, U256::from(10_000_000_000_000_000_000_000u128));
assert_eq!(balances.1, U256::from(10_000_000_000_000_000_000_000u128));
assert_eq!(balances.2, U256::from(10_000_000_000_000_000_000_000u128));
assert_eq!(balances.0, valid_balances[0]);
assert_eq!(balances.1, valid_balances[1]);
assert_eq!(balances.2, valid_balances[2]);
// call_array
multicall
.clear_calls()
.add_get_eth_balance(addrs[4], false)
.add_get_eth_balance(addrs[5], false)
.add_get_eth_balance(addrs[6], false);
let balances: Vec<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
multicall.clear_calls();
@ -536,10 +552,11 @@ mod eth_tests {
.unwrap();
// build up a list of calls greater than the 16 max restriction
for i in 0..=16 {
let call = simple_contract.method::<_, String>("getValue", ()).unwrap();
multicall.add_call(call, false);
}
multicall.add_calls(
false,
std::iter::repeat(simple_contract.method::<_, String>("getValue", ()).unwrap())
.take(17), // .collect(),
);
// must use `call_raw` as `.calls` > 16
let tokens = multicall.call_raw().await.unwrap();

View File

@ -1,10 +1,10 @@
[package]
name = "ethers-core"
license = "MIT OR Apache-2.0"
version = "1.0.2"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "Core structures for the ethers-rs crate"
homepage = "https://docs.rs/ethers"
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 }
bytes = { version = "1.3.0", features = ["serde"] }
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"
strum = { version = "0.24", features = ["derive"] }
@ -40,14 +40,15 @@ cargo_metadata = { version = "0.15.2", optional = true }
# eip712 feature enabled dependencies
convert_case = { version = "0.6.0", optional = true }
syn = { version = "1.0.105", optional = true }
proc-macro2 = { version = "1.0.47", optional = true }
syn = { version = "1.0.107", optional = true }
proc-macro2 = { version = "1.0.49", optional = true }
num_enum = "0.5.7"
[dev-dependencies]
tempfile = { version = "3.3.0", default-features = false }
serde_json = { version = "1.0.64", 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"
rand = "0.8.5"

View File

@ -1,7 +1,8 @@
[package]
name = "ethers-derive-eip712"
version = "1.0.2"
edition = "2018"
edition = "2021"
rust-version = "1.64"
description = "Custom derive macro for EIP-712 typed data"
license = "MIT OR Apache-2.0"
@ -11,7 +12,10 @@ proc-macro = true
[dependencies]
quote = "1.0.9"
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"
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 res = MetadataCommand::new()
.manifest_path(&format!("{manifest_dir}/Cargo.toml"))
.manifest_path(format!("{manifest_dir}/Cargo.toml"))
.exec()
.ok()
.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 std::{
convert::{TryFrom, TryInto},
fmt,
str::FromStr,
time::Duration,
};
use strum::EnumVariantNames;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[error("Failed to parse chain: {0}")]
pub struct ParseChainError(String);
use strum::{AsRefStr, EnumString, EnumVariantNames};
// When adding a new chain:
// 1. add new variant to the Chain enum;
// 2. update Display/FromStr impl;
// 3. add etherscan_keys if supported.
// 2. add extra information in the last `impl` block (explorer URLs, block time) when applicable;
// 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.
#[repr(u64)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Deserialize, EnumVariantNames)]
/// An Ethereum EIP-155 chain.
#[derive(
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")]
#[strum(serialize_all = "kebab-case")]
#[repr(u64)]
pub enum Chain {
#[default]
Mainnet = 1,
Morden = 2,
Ropsten = 3,
Rinkeby = 4,
Goerli = 5,
Optimism = 10,
Cronos = 25,
Rsk = 30,
Kovan = 42,
#[strum(serialize = "bsc")]
BinanceSmartChain = 56,
Sepolia = 11155111,
Optimism = 10,
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,
MoonbeamDev = 1281,
Moonbeam = 1284,
Moonriver = 1285,
Moonbase = 1287,
Dev = 1337,
FantomTestnet = 4002,
EvmosTestnet = 9000,
Evmos = 9001,
Chiado = 10200,
Oasis = 26863,
AnvilHardhat = 31337,
Arbitrum = 42161,
EmeraldTestnet = 42261,
Emerald = 42262,
AvalancheFuji = 43113,
Avalanche = 43114,
PolygonMumbai = 80001,
ArbitrumTestnet = 421611,
ArbitrumGoerli = 421613,
Sepolia = 11155111,
Aurora = 1313161554,
AuroraTestnet = 1313161555,
Cronos = 25,
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,
CeloAlfajores = 44787,
CeloBaklava = 62320,
Aurora = 1313161554,
AuroraTestnet = 1313161555,
}
// === impl Chain ===
impl fmt::Display for Chain {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let chain = match self {
Chain::Mainnet => "mainnet",
Chain::Morden => "morden",
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)
// This must be implemented manually so we avoid a conflict with `TryFromPrimitive` where it treats
// the `#[default]` attribute as its own `#[num_enum(default)]`
impl Default for Chain {
fn default() -> Self {
Self::Mainnet
}
}
impl From<Chain> for u32 {
fn from(chain: Chain) -> Self {
chain as u32
}
macro_rules! impl_into_numeric {
($($ty:ty)+) => {$(
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 {
@ -141,134 +163,21 @@ impl From<Chain> for u64 {
}
}
impl From<Chain> for U256 {
fn from(chain: Chain) -> Self {
u64::from(chain).into()
impl_into_numeric!(u128 U64 U128 U256 U512);
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 {
type Error = ParseChainError;
impl_try_from_numeric!(u8 u16 u32 usize; U128 U256 U512);
fn try_from(chain: u32) -> Result<Chain, Self::Error> {
(chain as u64).try_into()
}
}
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())),
})
impl fmt::Display for Chain {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(self.as_ref())
}
}
@ -277,163 +186,135 @@ impl Serialize for Chain {
where
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 {
/// 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
/// for example.
///
/// **Note:** this will not return the accurate average depending on the time but is rather a
/// sensible default derived from blocktime charts like <https://etherscan.com/chart/blocktime>
/// <https://polygonscan.com/chart/blocktime>
pub fn average_blocktime_hint(&self) -> Option<Duration> {
/// **Note:** this is not an accurate average, but is rather a sensible default derived from
/// blocktime charts such as [Etherscan's](https://etherscan.com/chart/blocktime)
/// or [Polygonscan's](https://polygonscan.com/chart/blocktime).
pub const fn average_blocktime_hint(&self) -> Option<Duration> {
use Chain::*;
let ms = match self {
Chain::Arbitrum | Chain::ArbitrumTestnet | Chain::ArbitrumGoerli => 1_300,
Chain::Mainnet | Chain::Optimism => 13_000,
Chain::Polygon | Chain::PolygonMumbai => 2_100,
Chain::Moonbeam | Chain::Moonriver => 12_500,
Chain::BinanceSmartChain | Chain::BinanceSmartChainTestnet => 3_000,
Chain::Avalanche | Chain::AvalancheFuji => 2_000,
Chain::Fantom | Chain::FantomTestnet => 1_200,
Chain::Cronos | Chain::CronosTestnet => 5_700,
Chain::Evmos | Chain::EvmosTestnet => 1_900,
Chain::Aurora | Chain::AuroraTestnet => 1_100,
Chain::Oasis => 5_500,
Chain::Emerald => 6_000,
Chain::Dev | Chain::AnvilHardhat => 200,
Chain::Celo | Chain::CeloAlfajores | Chain::CeloBaklava => 5_000,
Arbitrum | ArbitrumTestnet | ArbitrumGoerli => 1_300,
Mainnet | Optimism => 13_000,
Polygon | PolygonMumbai => 2_100,
Moonbeam | Moonriver => 12_500,
BinanceSmartChain | BinanceSmartChainTestnet => 3_000,
Avalanche | AvalancheFuji => 2_000,
Fantom | FantomTestnet => 1_200,
Cronos | CronosTestnet => 5_700,
Evmos | EvmosTestnet => 1_900,
Aurora | AuroraTestnet => 1_100,
Oasis => 5_500,
Emerald => 6_000,
Dev | AnvilHardhat => 200,
Celo | CeloAlfajores | CeloBaklava => 5_000,
// Explictly handle all network to make it easier not to forget this match when new
// networks are added.
Chain::Morden |
Chain::Ropsten |
Chain::Rinkeby |
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,
Morden | Ropsten | Rinkeby | Goerli | Kovan | XDai | Chiado | Sepolia | Moonbase |
MoonbeamDev | OptimismGoerli | OptimismKovan | Poa | Sokol | Rsk | EmeraldTestnet => {
return None
}
};
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")`
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 {
Chain::Mainnet => ("https://api.etherscan.io/api", "https://etherscan.io"),
Chain::Ropsten => {
("https://api-ropsten.etherscan.io/api", "https://ropsten.etherscan.io")
}
Chain::Kovan => ("https://api-kovan.etherscan.io/api", "https://kovan.etherscan.io"),
Chain::Rinkeby => {
("https://api-rinkeby.etherscan.io/api", "https://rinkeby.etherscan.io")
}
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 => {
Mainnet => ("https://api.etherscan.io/api", "https://etherscan.io"),
Ropsten => ("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"),
Goerli => ("https://api-goerli.etherscan.io/api", "https://goerli.etherscan.io"),
Sepolia => ("https://api-sepolia.etherscan.io/api", "https://sepolia.etherscan.io"),
Polygon => ("https://api.polygonscan.com/api", "https://polygonscan.com"),
PolygonMumbai => {
("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com")
}
Chain::Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"),
Chain::AvalancheFuji => {
Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"),
AvalancheFuji => {
("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io")
}
Chain::Optimism => {
Optimism => {
("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io")
}
Chain::OptimismGoerli => (
OptimismGoerli => (
"https://api-goerli-optimistic.etherscan.io/api",
"https://goerli-optimism.etherscan.io",
),
Chain::OptimismKovan => (
OptimismKovan => (
"https://api-kovan-optimistic.etherscan.io/api",
"https://kovan-optimistic.etherscan.io",
),
Chain::Fantom => ("https://api.ftmscan.com/api", "https://ftmscan.com"),
Chain::FantomTestnet => {
("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com")
}
Chain::BinanceSmartChain => ("https://api.bscscan.com/api", "https://bscscan.com"),
Chain::BinanceSmartChainTestnet => {
Fantom => ("https://api.ftmscan.com/api", "https://ftmscan.com"),
FantomTestnet => ("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com"),
BinanceSmartChain => ("https://api.bscscan.com/api", "https://bscscan.com"),
BinanceSmartChainTestnet => {
("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com")
}
Chain::Arbitrum => ("https://api.arbiscan.io/api", "https://arbiscan.io"),
Chain::ArbitrumTestnet => {
Arbitrum => ("https://api.arbiscan.io/api", "https://arbiscan.io"),
ArbitrumTestnet => {
("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",
),
Chain::Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"),
Chain::CronosTestnet => {
Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"),
CronosTestnet => {
("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com")
}
Chain::Moonbeam => {
("https://api-moonbeam.moonscan.io/api", "https://moonbeam.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")
}
Moonbeam => ("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"),
// blockscout API is etherscan compatible
Chain::XDai => {
XDai => {
("https://blockscout.com/xdai/mainnet/api", "https://blockscout.com/xdai/mainnet")
}
Chain::Chiado => {
Chiado => {
("https://blockscout.chiadochain.net/api", "https://blockscout.chiadochain.net")
}
Chain::Sokol => {
("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol")
}
Chain::Poa => {
("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core")
}
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 => {
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"),
Oasis => ("https://scan.oasischain.io/api", "https://scan.oasischain.io/"),
Emerald => {
("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/",
),
Chain::Aurora => ("https://api.aurorascan.dev/api", "https://aurorascan.dev"),
Chain::AuroraTestnet => {
Aurora => ("https://api.aurorascan.dev/api", "https://aurorascan.dev"),
AuroraTestnet => {
("https://testnet.aurorascan.dev/api", "https://testnet.aurorascan.dev")
}
Chain::Evmos => ("https://evm.evmos.org/api", "https://evm.evmos.org/"),
Chain::EvmosTestnet => ("https://evm.evmos.dev/api", "https://evm.evmos.dev/"),
Chain::Celo => {
("https://explorer.celo.org/mainnet", "https://explorer.celo.org/mainnet/api")
}
Chain::CeloAlfajores => {
Evmos => ("https://evm.evmos.org/api", "https://evm.evmos.org/"),
EvmosTestnet => ("https://evm.evmos.dev/api", "https://evm.evmos.dev/"),
Celo => ("https://explorer.celo.org/mainnet", "https://explorer.celo.org/mainnet/api"),
CeloAlfajores => {
("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")
}
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
// new chain
return None
@ -443,34 +324,49 @@ impl Chain {
Some(urls)
}
/// Helper function for checking if a chainid corresponds to a legacy chainid
/// without eip1559
pub fn is_legacy(&self) -> bool {
// TODO: Add other chains which do not support EIP1559.
matches!(
self,
Chain::Optimism |
Chain::OptimismGoerli |
Chain::OptimismKovan |
Chain::Fantom |
Chain::FantomTestnet |
Chain::BinanceSmartChain |
Chain::BinanceSmartChainTestnet |
Chain::Arbitrum |
Chain::ArbitrumTestnet |
Chain::ArbitrumGoerli |
Chain::Rsk |
Chain::Oasis |
Chain::Emerald |
Chain::EmeraldTestnet |
Chain::Celo |
Chain::CeloAlfajores |
Chain::CeloBaklava,
)
/// Returns whether the chain implements EIP-1559 (with the type 2 EIP-2718 transaction type).
pub const fn is_legacy(&self) -> bool {
use Chain::*;
match self {
// Known legacy chains / non EIP-1559 compliant
Optimism |
OptimismGoerli |
OptimismKovan |
Fantom |
FantomTestnet |
BinanceSmartChain |
BinanceSmartChainTestnet |
Arbitrum |
ArbitrumTestnet |
ArbitrumGoerli |
Rsk |
Oasis |
Emerald |
EmeraldTestnet |
Celo |
CeloAlfajores |
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]
fn test_default_chain() {
assert_eq!(serde_json::to_string(&Chain::Mainnet).unwrap(), "\"mainnet\"");
#[cfg(test)]
mod tests {
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 std::collections::BTreeMap;
@ -6,7 +9,8 @@ use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GethTrace {
pub failed: bool,
pub gas: u64,
#[serde(deserialize_with = "from_int_or_hex")]
pub gas: U256,
#[serde(serialize_with = "serialize_bytes", rename = "returnValue")]
pub return_value: Bytes,
#[serde(rename = "structLogs")]
@ -35,6 +39,16 @@ pub struct StructLog {
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
///
/// 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")]
pub enable_return_data: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tracer: Option<String>,
pub tracer: Option<GethDebugTracerType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
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>
where
S: Serializer,

View File

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

View File

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

View File

@ -233,7 +233,7 @@ impl TransactionRequest {
*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()?);
*offset += 1;

View File

@ -1,7 +1,7 @@
//! Transaction types
use super::{
decode_signature, eip2718::TypedTransaction, eip2930::AccessList, normalize_v, rlp_opt,
rlp_opt_list,
decode_signature, decode_to, eip2718::TypedTransaction, eip2930::AccessList, normalize_v,
rlp_opt, rlp_opt_list,
};
use crate::{
types::{
@ -248,8 +248,7 @@ impl Transaction {
*offset += 1;
self.gas = rlp.val_at(*offset)?;
*offset += 1;
self.to = Some(rlp.val_at(*offset)?);
*offset += 1;
self.to = decode_to(rlp, offset)?;
self.value = rlp.val_at(*offset)?;
*offset += 1;
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@ -279,8 +278,7 @@ impl Transaction {
#[cfg(feature = "celo")]
self.decode_celo_metadata(rlp, offset)?;
self.to = Some(rlp.val_at(*offset)?);
*offset += 1;
self.to = decode_to(rlp, offset)?;
self.value = rlp.val_at(*offset)?;
*offset += 1;
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@ -310,8 +308,7 @@ impl Transaction {
#[cfg(feature = "celo")]
self.decode_celo_metadata(rlp, offset)?;
self.to = Some(rlp.val_at(*offset)?);
*offset += 1;
self.to = decode_to(rlp, offset)?;
self.value = rlp.val_at(*offset)?;
*offset += 1;
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@ -473,7 +470,7 @@ impl PartialOrd<Self> for TransactionReceipt {
#[cfg(test)]
#[cfg(not(feature = "celo"))]
mod tests {
use rlp::Encodable;
use rlp::{Encodable, Rlp};
use crate::types::transaction::eip2930::AccessListItem;
@ -1093,4 +1090,24 @@ mod tests {
let r = rlp::Rlp::new(b.as_slice());
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)]
pub struct Geth {
program: Option<PathBuf>,
port: Option<u16>,
authrpc_port: Option<u16>,
ipc_path: Option<PathBuf>,
@ -180,6 +181,32 @@ impl Geth {
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.
#[must_use]
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
/// to /dev/null.
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
cmd.stderr(std::process::Stdio::piped());
cmd.stderr(Stdio::piped());
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() };

View File

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

View File

@ -1,13 +1,13 @@
[package]
name = "ethers-etherscan"
version = "1.0.2"
edition = "2021"
rust-version = "1.64"
authors = [
"Matthias Seitz <matthias.seitz@outlook.de>",
"Georgios Konstantopoulos <me@gakonst.com>",
]
license = "MIT OR Apache-2.0"
edition = "2018"
readme = "../README.md"
documentation = "https://docs.rs/ethers"
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 }
thiserror = "1.0"
tracing = "0.1.37"
semver = "1.0.14"
semver = "1.0.16"
[target.'cfg(target_arch = "wasm32")'.dependencies]
# NOTE: this enables wasm compatibility for getrandom indirectly
@ -35,7 +35,7 @@ getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
tempfile = "3.3.0"
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"] }
ethers-solc = { version = "^1.0.0", path = "../ethers-solc", default-features = false }

View File

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

View File

@ -36,20 +36,35 @@ pub fn deserialize_address_opt<'de, D: Deserializer<'de>>(
/// Deserializes as JSON:
///
/// `{ "SourceCode": "{{ .. }}", ..}`
/// Object: `{ "SourceCode": { language: "Solidity", .. }, ..}`
///
/// or
///
/// `{ "SourceCode": "..", .. }`
pub fn deserialize_stringified_source_code<'de, D: Deserializer<'de>>(
/// Stringified JSON: `{ "SourceCode": "{{\r\n \"language\": \"Solidity\", ..}}", ..}`
///
/// or
///
/// Normal source code: `{ "SourceCode": "// SPDX-License-Identifier: ...", .. }`
pub fn deserialize_source_code<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<SourceCodeMetadata, D::Error> {
let s = String::deserialize(deserializer)?;
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))
#[derive(Deserialize)]
#[serde(untagged)]
enum SourceCode {
String(String), // this must come first
Obj(SourceCodeMetadata),
}
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]
fn can_deserialize_stringified_source_code() {
fn can_deserialize_source_code() {
#[derive(Deserialize)]
struct Test {
#[serde(deserialize_with = "deserialize_stringified_source_code")]
#[serde(deserialize_with = "deserialize_source_code")]
source_code: SourceCodeMetadata,
}
let src = "source code text";
// Normal JSON
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();
assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));

View File

@ -1,7 +1,7 @@
[package]
name = "ethers-middleware"
version = "1.0.2"
edition = "2018"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
@ -47,12 +47,16 @@ ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-f
"ws",
"rustls",
] }
once_cell = "1.16.0"
once_cell = "1.17.0"
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]
tokio = { version = "1.18", default-features = false, features = ["rt", "macros", "time"] }
[features]
default = ["rustls"]
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::convert::TryFrom;
use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{*,gas_oracle::*};
use ethers_middleware::{gas_oracle::{GasOracle, GasNow}, MiddlewareBuilder};
fn builder_example() {
let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
let signer = key.parse::<LocalWallet>().unwrap();
let address = signer.address();
let gas_oracle = EthGasStation::new(None);
let gas_oracle = GasNow::new();
let provider = Provider::<Http>::try_from("http://localhost:8545")
.unwrap()
@ -58,7 +58,7 @@ fn builder_example_wrap_into() {
.unwrap()
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.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
}
```
@ -72,7 +72,7 @@ use ethers_providers::{Provider, Http};
use ethers_signers::{LocalWallet, Signer};
use ethers_middleware::{
gas_escalator::{GasEscalatorMiddleware, GeometricGasPrice, Frequency},
gas_oracle::{GasOracleMiddleware, EthGasStation, GasCategory},
gas_oracle::{GasOracleMiddleware, GasCategory, GasNow},
signer::SignerMiddleware,
nonce_manager::NonceManagerMiddleware,
};
@ -91,8 +91,8 @@ let signer = LocalWallet::new(&mut rand::thread_rng());
let address = signer.address();
let provider = SignerMiddleware::new(provider, signer);
// Use EthGasStation as the gas oracle
let gas_oracle = EthGasStation::new(None);
// Use GasNow as the gas oracle
let gas_oracle = GasNow::new();
let provider = GasOracleMiddleware::new(provider, gas_oracle);
// Manage nonces locally

View File

@ -16,14 +16,14 @@ use ethers_signers::Signer;
/// use std::sync::Arc;
/// use std::convert::TryFrom;
/// use ethers_signers::{LocalWallet, Signer};
/// use ethers_middleware::{*,gas_escalator::*,gas_oracle::*};
/// use ethers_middleware::{*, gas_escalator::*, gas_oracle::*};
///
/// fn builder_example() {
/// let key = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169";
/// let signer = key.parse::<LocalWallet>().unwrap();
/// let address = signer.address();
/// 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")
/// .unwrap()
@ -43,7 +43,7 @@ use ethers_signers::Signer;
/// .unwrap()
/// .wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
/// .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
/// }
/// ```

View File

@ -46,7 +46,7 @@ pub enum Frequency {
/// use ethers_providers::{Provider, Http};
/// use ethers_middleware::{
/// gas_escalator::{GeometricGasPrice, Frequency, GasEscalatorMiddleware},
/// gas_oracle::{EthGasStation, GasCategory, GasOracleMiddleware},
/// gas_oracle::{GasNow, GasCategory, GasOracleMiddleware},
/// };
/// use std::{convert::TryFrom, time::Duration, sync::Arc};
///
@ -60,7 +60,7 @@ pub enum Frequency {
/// };
///
/// // ... 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);
/// ```
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 ethers_core::types::U256;
use reqwest::{header::AUTHORIZATION, Client};
use serde::Deserialize;
use std::{collections::HashMap, convert::TryInto};
use std::collections::HashMap;
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 {
match gas_category {
GasCategory::SafeLow => 80,
@ -16,124 +131,3 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
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 ethers_core::types::U256;
use futures_locks::RwLock;
@ -19,6 +19,24 @@ pub struct Cache<T: GasOracle> {
#[derive(Default, Debug)]
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> {
async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E>
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;
use ethers_core::types::U256;
#![allow(deprecated)]
use super::{GasCategory, GasOracle, GasOracleError, Result, GWEI_TO_WEI_U256};
use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client;
use serde::Deserialize;
use std::collections::HashMap;
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/api/ethgasAPI.json) gas tracker API
/// that implements the `GasOracle` trait
/// A client over HTTP for the [EthGasStation](https://ethgasstation.info) gas tracker API
/// that implements the `GasOracle` trait.
#[derive(Clone, Debug)]
#[deprecated = "ETHGasStation is shutting down: https://twitter.com/ETHGasStation/status/1597341610777317376"]
#[must_use]
pub struct EthGasStation {
client: Client,
url: Url,
gas_category: GasCategory,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
/// 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
/// and wait times for each "speed".
pub struct EthGasStationResponse {
/// Recommended safe(expected to be mined in < 30 minutes) gas price in
/// x10 Gwei (divide by 10 to convert it to gwei)
pub safe_low: f64,
/// Recommended average(expected to be mined in < 5 minutes) gas price in
/// x10 Gwei (divide by 10 to convert it to gwei)
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Response {
/// Recommended safe (expected to be mined in < 30 minutes).
///
/// 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,
/// Recommended fast(expected to be mined in < 2 minutes) gas price in
/// x10 Gwei (divide by 10 to convert it to gwei)
/// Recommended fast (expected to be mined in < 2 minutes).
///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub fast: u64,
/// Recommended fastest(expected to be mined in < 30 seconds) gas price
/// in x10 Gwei(divide by 10 to convert it to gwei)
/// Recommended fastest (expected to be mined in < 30 seconds).
///
/// In gwei * 10 (divide by 10 to convert it to gwei).
pub fastest: u64,
// post eip-1559 fields
/// Average time (in seconds) to mine a single block.
#[serde(rename = "block_time")] // inconsistent json response naming...
/// Average time(in seconds) to mine one single block
pub block_time: f64,
/// The latest block number
/// The latest block number.
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,
/// 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,
/// Waiting time(in minutes) for the `average` gas price
/// Waiting time (in minutes) for the `average` gas price.
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,
/// Waiting time(in minutes) for the `fastest` gas price
/// Waiting time (in minutes) for the `fastest` gas price.
pub fastest_wait: f64,
// What is this?
pub gas_price_range: HashMap<u64, f64>,
}
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(ETH_GAS_STATION_URL_PREFIX).expect("invalid url");
if let Some(key) = api_key {
url.query_pairs_mut().append_pair("api-key", key);
impl Response {
#[inline]
pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
match gas_category {
GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.average,
GasCategory::Fast => self.fast,
GasCategory::Fastest => self.fastest,
}
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(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for EthGasStation {
async fn fetch(&self) -> Result<U256, GasOracleError> {
async fn fetch(&self) -> Result<U256> {
let res = self.query().await?;
let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10),
GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10),
GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10),
GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10),
};
Ok(gas_price)
let gas_price = res.gas_from_category(self.gas_category);
// gas_price is in `gwei * 10`
Ok(U256::from(gas_price) * GWEI_TO_WEI_U256 / U256::from(10_u64))
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
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 ethers_core::types::U256;
use reqwest::Client;
use serde::Deserialize;
use url::Url;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
const ETHERCHAIN_URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
const URL: &str = "https://www.etherchain.org/api/gasPriceOracle";
/// 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)]
#[must_use]
pub struct Etherchain {
client: Client,
url: Url,
gas_category: GasCategory,
}
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EtherchainResponse {
pub safe_low: f32,
pub standard: f32,
pub fast: f32,
pub fastest: f32,
pub current_base_fee: f32,
pub recommended_base_fee: f32,
pub struct Response {
pub safe_low: f64,
pub standard: f64,
pub fast: f64,
pub fastest: f64,
pub current_base_fee: f64,
pub recommended_base_fee: f64,
}
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(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?)
impl Response {
#[inline]
pub fn gas_from_category(&self, gas_category: GasCategory) -> f64 {
match gas_category {
GasCategory::SafeLow => self.safe_low,
GasCategory::Standard => self.standard,
GasCategory::Fast => self.fast,
GasCategory::Fastest => self.fastest,
}
}
}
@ -63,19 +49,39 @@ impl Default for Etherchain {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Etherchain {
async fn fetch(&self) -> Result<U256, GasOracleError> {
async fn fetch(&self) -> Result<U256> {
let res = self.query().await?;
let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from((res.safe_low as u64) * GWEI_TO_WEI),
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)
let gas_price = res.gas_from_category(self.gas_category);
Ok(from_gwei_f64(gas_price))
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
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 ethers_core::types::U256;
use ethers_etherscan::Client;
use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
use std::ops::{Deref, DerefMut};
/// A client over HTTP for the [Etherscan](https://api.etherscan.io/api?module=gastracker&action=gasoracle) gas tracker API
/// that implements the `GasOracle` trait
#[derive(Clone, Debug)]
#[must_use]
pub struct Etherscan {
client: Client,
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 {
/// Creates a new [Etherscan](https://etherscan.io/gastracker) gas price oracle.
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.
#[must_use]
pub fn category(mut self, gas_category: GasCategory) -> Self {
self.gas_category = gas_category;
self
}
}
#[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, 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)
/// Perform a request to the gas price API and deserialize the response.
pub async fn query(&self) -> Result<ethers_etherscan::gas::GasOracle> {
Ok(self.client.gas_oracle().await?)
}
}

View File

@ -1,64 +1,54 @@
use ethers_core::types::U256;
use super::{GasCategory, GasOracle, GasOracleError, Result};
use async_trait::async_trait;
use ethers_core::types::U256;
use reqwest::Client;
use serde::Deserialize;
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 [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API
/// that implements the `GasOracle` trait
/// A client over HTTP for the [beaconcha.in GasNow](https://beaconcha.in/gasnow) gas tracker API
/// that implements the `GasOracle` trait.
#[derive(Clone, Debug)]
#[must_use]
pub struct GasNow {
client: Client,
url: Url,
gas_category: GasCategory,
}
#[derive(Deserialize)]
struct GasNowResponseWrapper {
data: GasNowResponse,
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct Response {
pub code: u64,
pub data: ResponseData,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct GasNowResponse {
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct ResponseData {
pub rapid: u64,
pub fast: u64,
pub standard: u64,
pub slow: u64,
pub timestamp: u64,
#[serde(rename = "priceUSD")]
pub price_usd: f64,
}
impl GasNow {
/// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle.
pub fn new() -> Self {
Self::with_client(Client::new())
impl Response {
#[inline]
pub fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
self.data.gas_from_category(gas_category)
}
}
/// Same as [`Self::new`] but with a custom [`Client`].
pub fn with_client(client: Client) -> Self {
let url = Url::parse(GAS_NOW_URL).expect("invalid url");
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
}
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)
impl ResponseData {
fn gas_from_category(&self, gas_category: GasCategory) -> u64 {
match gas_category {
GasCategory::SafeLow => self.slow,
GasCategory::Standard => self.standard,
GasCategory::Fast => self.fast,
GasCategory::Fastest => self.rapid,
}
}
}
@ -71,19 +61,39 @@ impl Default for GasNow {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for GasNow {
async fn fetch(&self) -> Result<U256, GasOracleError> {
async fn fetch(&self) -> Result<U256> {
let res = self.query().await?;
let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from(res.slow),
GasCategory::Standard => U256::from(res.standard),
GasCategory::Fast => U256::from(res.fast),
GasCategory::Fastest => U256::from(res.rapid),
};
Ok(gas_price)
let gas_price = res.gas_from_category(self.gas_category);
Ok(U256::from(gas_price))
}
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
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 ethers_core::types::U256;
use futures_util::future::join_all;
@ -28,13 +28,10 @@ impl Median {
self.oracles.push((weight, Box::new(oracle)));
}
pub async fn query_all<'a, Fn, Fut, O>(
&'a self,
mut f: Fn,
) -> Result<Vec<(f32, O)>, GasOracleError>
pub async fn query_all<'a, Fn, Fut, O>(&'a self, mut f: Fn) -> Result<Vec<(f32, O)>>
where
Fn: FnMut(&'a dyn GasOracle) -> Fut,
Fut: Future<Output = Result<O, GasOracleError>>,
Fut: Future<Output = Result<O>>,
{
// Process the oracles in parallel
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(not(target_arch = "wasm32"), async_trait)]
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?;
// `query_all` guarantees `values` is not empty
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?;
// `query_all` guarantees `values` is not empty
Ok((

View File

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

View File

@ -1,54 +1,58 @@
mod blocknative;
pub mod blocknative;
pub use blocknative::BlockNative;
mod eth_gas_station;
pub mod eth_gas_station;
#[allow(deprecated)]
pub use eth_gas_station::EthGasStation;
mod etherchain;
pub mod etherchain;
pub use etherchain::Etherchain;
mod etherscan;
pub mod etherscan;
pub use etherscan::Etherscan;
mod middleware;
pub mod middleware;
pub use middleware::{GasOracleMiddleware, MiddlewareError};
mod median;
pub mod median;
pub use median::Median;
mod cache;
pub mod cache;
pub use cache::Cache;
mod polygon;
pub mod polygon;
pub use polygon::Polygon;
mod gas_now;
pub mod gas_now;
pub use gas_now::GasNow;
mod provider_oracle;
pub mod provider_oracle;
pub use provider_oracle::ProviderOracle;
use ethers_core::types::U256;
use async_trait::async_trait;
use auto_impl::auto_impl;
use ethers_core::types::U256;
use reqwest::Error as ReqwestError;
use std::error::Error;
use std::{error::Error, fmt::Debug};
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
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub type Result<T, E = GasOracleError> = std::result::Result<T, E>;
/// Generic [`GasOracle`] gas price categories.
#[derive(Clone, Copy, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum GasCategory {
SafeLow,
#[default]
Standard,
Fast,
Fastest,
}
#[derive(Error, Debug)]
/// Error thrown when fetching data from the `GasOracle`
/// Error thrown by a [`GasOracle`].
#[derive(Debug, Error)]
pub enum GasOracleError {
/// An internal error in the HTTP request made from the underlying
/// gas oracle
@ -83,48 +87,81 @@ pub enum GasOracleError {
UnsupportedChain,
/// 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>),
}
/// `GasOracle` is a trait that an underlying gas oracle needs to implement.
/// An Ethereum gas price oracle.
///
/// # Example
///
/// ```no_run
/// use ethers_middleware::{
/// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle},
/// };
/// use ethers_core::types::U256;
/// use ethers_middleware::gas_oracle::{GasCategory, GasNow, GasOracle};
///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let eth_gas_station_oracle = EthGasStation::new(Some("my-api-key"));
/// let etherscan_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
///
/// let data_1 = eth_gas_station_oracle.fetch().await?;
/// let data_2 = etherscan_oracle.fetch().await?;
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let gas_price = oracle.fetch().await?;
/// assert!(gas_price > U256::zero());
/// # Ok(())
/// # }
/// ```
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[auto_impl(&, Box, Arc)]
pub trait GasOracle: Send + Sync + std::fmt::Debug {
/// Makes an asynchronous HTTP query to the underlying `GasOracle`
pub trait GasOracle: Send + Sync + Debug {
/// Makes an asynchronous HTTP query to the underlying [`GasOracle`] to fetch the current gas
/// price estimate.
///
/// # Example
///
/// ```
/// use ethers_middleware::{
/// gas_oracle::{Etherchain, GasCategory, GasOracle},
/// };
/// ```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 etherchain_oracle = Etherchain::new().category(GasCategory::Fastest);
/// let data = etherchain_oracle.fetch().await?;
/// let oracle = GasNow::default().category(GasCategory::SafeLow);
/// let gas_price = oracle.fetch().await?;
/// assert!(gas_price > U256::zero());
/// # 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 ethers_core::types::{u256_from_f64_saturating, Chain, U256};
use ethers_core::types::{Chain, U256};
use reqwest::Client;
use serde::Deserialize;
use url::Url;
const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2";
const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2";
const MAINNET_URL: &str = "https://gasstation-mainnet.matic.network/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
/// Queries over HTTP and implements the `GasOracle` trait
/// Queries over HTTP and implements the `GasOracle` trait.
#[derive(Clone, Debug)]
#[must_use]
pub struct Polygon {
client: Client,
url: Url,
@ -18,74 +19,98 @@ pub struct Polygon {
}
/// The response from the Polygon gas station API.
/// Gas prices are in Gwei.
///
/// Gas prices are in __Gwei__.
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Response {
estimated_base_fee: f64,
safe_low: GasEstimate,
standard: GasEstimate,
fast: GasEstimate,
pub estimated_base_fee: f64,
pub safe_low: GasEstimate,
pub standard: GasEstimate,
pub fast: GasEstimate,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GasEstimate {
max_priority_fee: f64,
max_fee: f64,
pub max_priority_fee: f64,
pub max_fee: f64,
}
impl Polygon {
pub fn new(chain: Chain) -> Result<Self, GasOracleError> {
Self::with_client(Client::new(), chain)
impl Response {
#[inline]
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> {
// TODO: Sniff chain from chain id.
let url = match chain {
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))
impl Default for Polygon {
fn default() -> Self {
Self::new(Chain::Polygon).unwrap()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for Polygon {
async fn fetch(&self) -> Result<U256, GasOracleError> {
let (base_fee, estimate) = self.request().await?;
let fee = base_fee + estimate.max_priority_fee;
Ok(from_gwei(fee))
async fn fetch(&self) -> Result<U256> {
let response = self.query().await?;
let base = response.estimated_base_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> {
let (_, estimate) = self.request().await?;
Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee)))
async fn estimate_eip1559_fees(&self) -> Result<(U256, U256)> {
let response = self.query().await?;
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 {
u256_from_f64_saturating(gwei * 1.0e9_f64)
impl Polygon {
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 ethers_core::types::U256;
use ethers_providers::Middleware;
@ -6,7 +6,8 @@ use std::fmt::Debug;
/// Gas oracle from a [`Middleware`] implementation such as an
/// Ethereum RPC provider.
#[derive(Debug)]
#[derive(Clone, Debug)]
#[must_use]
pub struct ProviderOracle<M: Middleware> {
provider: M,
}
@ -21,16 +22,16 @@ impl<M: Middleware> ProviderOracle<M> {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<M: Middleware> GasOracle for ProviderOracle<M>
where
<M as Middleware>::Error: 'static,
M::Error: 'static,
{
async fn fetch(&self) -> Result<U256, GasOracleError> {
async fn fetch(&self) -> Result<U256> {
self.provider
.get_gas_price()
.await
.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.
self.provider
.estimate_eip1559_fees(None)

View File

@ -1,22 +1,21 @@
use ethers_contract::Lazy;
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
/// DsProxyFactory contract addresses as values
pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
let mut m = HashMap::new();
let mut m = HashMap::with_capacity(1);
// mainnet
let addr =
Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed");
m.insert(U256::from(1u8), addr);
let addr = "eefba1e63905ef1d7acba5a8513c70307c1ce441".parse().unwrap();
m.insert(U256::from(1_u64), addr);
m
});
/// Generated with abigen:
///
/// Generated with
/// ```ignore
/// # use ethers_contract::abigen;
/// 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::*;
#[allow(clippy::too_many_arguments)]
mod dsproxyfactory_mod {

View File

@ -1,69 +1,67 @@
#![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]
async fn build_raw_middleware_stack() {
let (provider, mock) = Provider::mocked();
use ethers_core::{rand::thread_rng, types::U64};
use ethers_middleware::{
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());
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
#[tokio::test]
async fn build_raw_middleware_stack() {
let (provider, mock) = Provider::mocked();
let provider = provider
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.wrap_into(|p| GasOracleMiddleware::new(p, EthGasStation::new(None)))
.wrap_into(|p| SignerMiddleware::new(p, signer))
.wrap_into(|p| NonceManagerMiddleware::new(p, address));
let signer = LocalWallet::new(&mut thread_rng());
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
// push a response
mock.push(U64::from(12u64)).unwrap();
let block: U64 = provider.get_block_number().await.unwrap();
assert_eq!(block.as_u64(), 12);
let provider = provider
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.wrap_into(|p| GasOracleMiddleware::new(p, GasNow::new()))
.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
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
}
provider.get_block_number().await.unwrap_err();
#[tokio::test]
async fn build_declarative_middleware_stack() {
let (provider, mock) = Provider::mocked();
let signer = LocalWallet::new(&mut thread_rng());
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let gas_oracle = EthGasStation::new(None);
let provider = provider
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.gas_oracle(gas_oracle)
.with_signer(signer)
.nonce_manager(address);
// push a response
mock.push(U64::from(12u64)).unwrap();
let block: U64 = provider.get_block_number().await.unwrap();
assert_eq!(block.as_u64(), 12);
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();
}
// 2 calls were made
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
}
#[tokio::test]
async fn build_declarative_middleware_stack() {
let (provider, mock) = Provider::mocked();
let signer = LocalWallet::new(&mut thread_rng());
let address = signer.address();
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let gas_oracle = GasNow::new();
let provider = provider
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
.gas_oracle(gas_oracle)
.with_signer(signer)
.nonce_manager(address);
// push a response
mock.push(U64::from(12u64)).unwrap();
let block: U64 = provider.get_block_number().await.unwrap();
assert_eq!(block.as_u64(), 12);
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"))]
use ethers_core::types::*;
use ethers_middleware::{
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
@ -12,6 +13,7 @@ use std::time::Duration;
#[ignore]
async fn gas_escalator_live() {
// connect to ropsten for getting bad block times
#[allow(deprecated)]
let provider = ethers_providers::ROPSTEN.ws().await;
let provider = provider.interval(Duration::from_millis(2000u64));
let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"
@ -33,7 +35,7 @@ async fn gas_escalator_live() {
provider.send_transaction(tx.clone().nonce(nonce + 2), None).await.unwrap();
// Wait a bunch of seconds and refresh etherscan to see the transactions get bumped
tokio::time::sleep(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
// bumped then the tx hash will be different

View File

@ -1,16 +1,14 @@
#![cfg(not(target_arch = "wasm32"))]
use std::convert::TryFrom;
use async_trait::async_trait;
use ethers_core::{types::*, utils::Anvil};
use ethers_middleware::gas_oracle::{
EthGasStation, Etherchain, Etherscan, GasCategory, GasOracle, GasOracleError,
GasOracleMiddleware,
BlockNative, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleError,
GasOracleMiddleware, Polygon, ProviderOracle, Result,
};
use ethers_providers::{Http, Middleware, Provider};
use serial_test::serial;
use std::convert::TryFrom;
#[derive(Debug)]
struct FakeGasOracle {
@ -20,17 +18,18 @@ struct FakeGasOracle {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl GasOracle for FakeGasOracle {
async fn fetch(&self) -> Result<U256, GasOracleError> {
async fn fetch(&self) -> Result<U256> {
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)
}
}
#[tokio::test]
async fn using_gas_oracle() {
#[serial]
async fn provider_using_gas_oracle() {
let anvil = Anvil::new().spawn();
let from = anvil.addresses()[0];
@ -38,11 +37,11 @@ async fn using_gas_oracle() {
// connect to the network
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
let gas_oracle = FakeGasOracle { gas_price: (base_fee + 1337).into() };
let expected_gas_price = gas_oracle.fetch().await.unwrap();
let expected_gas_price = U256::from(1234567890_u64);
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);
@ -55,35 +54,70 @@ async fn using_gas_oracle() {
}
#[tokio::test]
async fn eth_gas_station() {
// initialize and fetch gas estimates from EthGasStation
let eth_gas_station_oracle = EthGasStation::default();
let data = eth_gas_station_oracle.fetch().await;
data.unwrap();
#[serial]
async fn provider_oracle() {
// spawn anvil and connect to it
let anvil = Anvil::new().spawn();
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]
#[serial]
async fn etherscan() {
let etherscan_client = ethers_etherscan::Client::new_from_env(Chain::Mainnet).unwrap();
// initialize and fetch gas estimates from Etherscan
// since etherscan does not support `fastest` category, we expect an error
let etherscan_oracle = Etherscan::new(etherscan_client.clone()).category(GasCategory::Fastest);
let data = etherscan_oracle.fetch().await;
data.unwrap_err();
let error = etherscan_oracle.fetch().await.unwrap_err();
assert!(matches!(error, GasOracleError::GasCategoryNotSupported));
// but fetching the `standard` gas price should work fine
let etherscan_oracle = Etherscan::new(etherscan_client).category(GasCategory::SafeLow);
let data = etherscan_oracle.fetch().await;
data.unwrap();
let gas_price = etherscan_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
}
#[tokio::test]
async fn etherchain() {
// initialize and fetch gas estimates from Etherchain
let etherchain_oracle = Etherchain::default().category(GasCategory::Fast);
let data = etherchain_oracle.fetch().await;
data.unwrap();
async fn gas_now() {
let gas_now_oracle = GasNow::default();
let gas_price = gas_now_oracle.fetch().await.unwrap();
assert!(gas_price > U256::zero());
}
#[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"))]
#[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;
#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
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 wallet = std::env::var("GOERLI_PRIVATE_KEY")
.unwrap()
.expect("GOERLI_PRIVATE_KEY is not defined")
.parse::<LocalWallet>()
.unwrap()
.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
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);
for tx_hash in tx_hashes {
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)]
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
use ethers_contract::ContractFactory;
use ethers_core::{
types::{BlockNumber, TransactionRequest},
utils::parse_units,
abi::Abi,
types::*,
utils::{parse_ether, parse_units, Anvil},
};
use ethers_middleware::signer::SignerMiddleware;
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, GOERLI};
use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer};
use ethers_solc::Solc;
use once_cell::sync::Lazy;
use std::{convert::TryFrom, iter::Cycle, sync::atomic::AtomicU8, time::Duration};
use std::{
convert::TryFrom,
iter::Cycle,
sync::{atomic::AtomicU8, Arc},
time::Duration,
};
static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
TestWallets {
@ -22,8 +30,6 @@ static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn send_eth() {
use ethers_core::utils::Anvil;
let anvil = Anvil::new().spawn();
// 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;
}
#[cfg(not(feature = "celo"))]
use ethers_core::types::{Address, Eip1559TransactionRequest};
use ethers_core::utils::parse_ether;
// different keys to avoid nonce errors
#[tokio::test]
#[cfg(not(feature = "celo"))]
@ -195,8 +197,6 @@ async fn test_send_transaction() {
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn send_transaction_handles_tx_from_field() {
use ethers_core::utils::Anvil;
// launch anvil
let anvil = Anvil::new().spawn();
@ -240,14 +240,6 @@ async fn send_transaction_handles_tx_from_field() {
#[tokio::test]
#[cfg(feature = "celo")]
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
fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
let path = format!("./tests/solidity-contracts/{path}");
@ -302,7 +294,7 @@ impl TestWallets {
#[allow(unused)]
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<_>>();
// hardcoded funder address private key, goerli
// hardcoded funder address private key, GOERLI
let signer = "39aa18eeb5d12c071e5f19d8e9375a872e90cb1f2fa640384ffd8800a2f3e8f1"
.parse::<LocalWallet>()
.unwrap()

View File

@ -1,95 +1,93 @@
#![cfg(not(target_arch = "wasm32"))]
#[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;
#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
#[tokio::test]
async fn mock_with_middleware() {
let (provider, mock) = Provider::mocked();
use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Anvil};
use ethers_middleware::{
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
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
let signer = LocalWallet::new(&mut thread_rng());
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);
#[tokio::test]
async fn mock_with_middleware() {
let (provider, mock) = Provider::mocked();
// push a response
use ethers_core::types::U64;
mock.push(U64::from(12u64)).unwrap();
let blk = provider.get_block_number().await.unwrap();
assert_eq!(blk.as_u64(), 12);
// add a bunch of middlewares
let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
let signer = LocalWallet::new(&mut thread_rng());
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);
// now that the response is gone, there's nothing left
// TODO: This returns:
// MiddlewareError(
// 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();
// push a response
use ethers_core::types::U64;
mock.push(U64::from(12u64)).unwrap();
let blk = provider.get_block_number().await.unwrap();
assert_eq!(blk.as_u64(), 12);
// 2 calls were made
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
}
// now that the response is gone, there's nothing left
// TODO: This returns:
// MiddlewareError(
// 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]
async fn can_stack_middlewares() {
let anvil = Anvil::new().block_time(5u64).spawn();
let gas_oracle = EthGasStation::new(None).category(GasCategory::SafeLow);
let signer: LocalWallet = anvil.keys()[0].clone().into();
let address = signer.address();
// the base provider
let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap());
let chain_id = provider.get_chainid().await.unwrap().as_u64();
let signer = signer.with_chain_id(chain_id);
// the Gas Price escalator middleware is the first middleware above the provider,
// so that it receives the transaction last, after all the other middleware
// have modified it accordingly
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
// The gas price middleware MUST be below the signing middleware for things to work
let provider = GasOracleMiddleware::new(provider, gas_oracle);
// The signing middleware signs txs
use std::sync::Arc;
let provider = Arc::new(SignerMiddleware::new(provider, signer));
// The nonce manager middleware MUST be above the signing middleware so that it overrides
// the nonce and the signer does not make any eth_getTransaction count calls
let provider = NonceManagerMiddleware::new(provider, address);
let tx = TransactionRequest::new();
let mut pending_txs = Vec::new();
for _ in 0..10 {
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
let hash = *pending;
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);
}
// 2 calls were made
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap();
mock.assert_request("eth_blockNumber", ()).unwrap_err();
}
#[tokio::test]
async fn can_stack_middlewares() {
let anvil = Anvil::new().block_time(5u64).spawn();
let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
let signer: LocalWallet = anvil.keys()[0].clone().into();
let address = signer.address();
// the base provider
let provider = Arc::new(Provider::<Http>::try_from(anvil.endpoint()).unwrap());
let chain_id = provider.get_chainid().await.unwrap().as_u64();
let signer = signer.with_chain_id(chain_id);
// the Gas Price escalator middleware is the first middleware above the provider,
// so that it receives the transaction last, after all the other middleware
// have modified it accordingly
let escalator = GeometricGasPrice::new(1.125, 60u64, None::<u64>);
let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock);
// The gas price middleware MUST be below the signing middleware for things to work
let provider = GasOracleMiddleware::new(provider, gas_oracle);
// The signing middleware signs txs
use std::sync::Arc;
let provider = Arc::new(SignerMiddleware::new(provider, signer));
// The nonce manager middleware MUST be above the signing middleware so that it overrides
// the nonce and the signer does not make any eth_getTransaction count calls
let provider = NonceManagerMiddleware::new(provider, address);
let tx = TransactionRequest::new();
let mut pending_txs = Vec::new();
for _ in 0..10 {
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
let hash = *pending;
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"))]
#![allow(unused)]
#![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
use ethers_contract::{BaseContract, ContractFactory};
use ethers_core::{abi::Abi, types::*, utils::Anvil};
use ethers_middleware::{
@ -24,7 +24,6 @@ fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
}
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_transformer() {
// randomness
let mut rng = rand::thread_rng();
@ -83,7 +82,6 @@ async fn ds_proxy_transformer() {
}
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_code() {
// randomness
let mut rng = rand::thread_rng();

View File

@ -1,7 +1,7 @@
[package]
name = "ethers-providers"
version = "1.0.2"
edition = "2018"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
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"] }
bytes = { version = "1.3.0", default-features = false, optional = true }
once_cell = "1.16.0"
once_cell = "1.17.0"
hashers = "1.0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@ -540,6 +540,7 @@ pub trait Middleware: Sync + Send + Debug {
}
// Geth `trace` support
/// After replaying any previous transactions in the same block,
/// Replays a transaction, returning the traces configured with passed options
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)
}
/// 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
/// 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
/// to prevent rate limits
@ -732,9 +744,13 @@ pub mod test_provider {
"5c812e02193c4ba793f8c214317582bd",
];
pub static GOERLI: Lazy<TestProvider> = Lazy::new(|| TestProvider::new(INFURA_KEYS, "goerli"));
pub static MAINNET: Lazy<TestProvider> =
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> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "ropsten"));
@ -745,16 +761,14 @@ pub mod test_provider {
}
impl TestProvider {
pub fn new(keys: &'static [&'static str], network: &str) -> Self {
Self { keys: Mutex::new(keys.iter().cycle()), network: network.to_owned() }
pub fn new(keys: &'static [&'static str], network: impl Into<String>) -> Self {
Self { keys: keys.iter().cycle().into(), network: network.into() }
}
pub fn url(&self) -> String {
format!(
"https://{}.infura.io/v3/{}",
self.network,
self.keys.lock().unwrap().next().unwrap()
)
let Self { network, keys } = self;
let key = keys.lock().unwrap().next().unwrap();
format!("https://{network}.infura.io/v3/{key}")
}
pub fn provider(&self) -> Provider<Http> {

View File

@ -22,9 +22,10 @@ use ethers_core::{
types::{
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, EIP1186ProofResponse, FeeHistory,
Filter, FilterBlockOption, GethDebugTracingOptions, GethTrace, Log, NameOrAddress,
Selector, Signature, Trace, TraceFilter, TraceType, Transaction, TransactionReceipt,
TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus, H256, U256, U64,
Filter, FilterBlockOption, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace,
Log, NameOrAddress, Selector, Signature, Trace, TraceFilter, TraceType, Transaction,
TransactionReceipt, TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus,
H256, U256, U64,
},
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() {
let (max_fee_per_gas, max_priority_fee_per_gas) =
self.estimate_eip1559_fees(None).await?;
inner.max_fee_per_gas = Some(max_fee_per_gas);
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
// we want to avoid overriding the user if either of these
// 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
}
/// 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
async fn trace_call<T: Into<TypedTransaction> + Send + Sync>(
&self,

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "ethers-signers"
version = "1.0.2"
edition = "2018"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
@ -16,7 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
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-bip39 = "0.7.0"
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 }
futures-util = { 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 = [
"f_ethereum",
] }

View File

@ -90,6 +90,13 @@ impl Wallet<SigningKey> {
let address = secret_key_to_address(&signer);
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> {
@ -305,4 +312,17 @@ mod tests {
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]
name = "ethers-solc"
version = "1.0.2"
edition = "2018"
edition = "2021"
rust-version = "1.64"
authors = [
"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 }
serde_json = "1.0.68"
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"
tokio = { version = "1.18", default-features = false, features = ["rt"] }
futures-util = { version = "^0.3", optional = true }
once_cell = "1.16.0"
once_cell = "1.17.0"
regex = "1.7.0"
md-5 = "0.10.5"
thiserror = "1.0"
@ -31,7 +31,7 @@ hex = "0.4.3"
yansi = "0.5.1"
glob = "0.3.0"
tracing = "0.1.37"
num_cpus = "1.14.0"
num_cpus = "1.15.0"
tiny-keccak = { version = "2.0.2", default-features = false }
tempfile = { version = "3.3.0", optional = true }
fs_extra = { version = "1.2.0", optional = true }
@ -62,7 +62,7 @@ rand = "0.8.5"
pretty_assertions = "1.3.0"
tempfile = "3.3.0"
tokio = { version = "1.18", features = ["full"] }
serde_path_to_error = "0.1.8"
serde_path_to_error = "0.1.9"
[[bench]]
name = "compile_many"

View File

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

View File

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

View File

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

View File

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

View File

@ -649,7 +649,7 @@ impl Graph {
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() {
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
use ethers::utils::Anvil;
use eyre::Result;
#[tokio::main]
async fn main() -> eyre::Result<()> {
async fn main() -> Result<()> {
// ensure `anvil` is available in $PATH
let anvil =
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();
args.next().unwrap(); // skip program name
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");

View File

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

View File

@ -1,10 +1,13 @@
use ethers::{
prelude::*,
solc::{Project, ProjectPathsConfig},
utils::Anvil,
contract::{abigen, ContractFactory},
core::utils::Anvil,
middleware::SignerMiddleware,
providers::{Http, Provider},
signers::{LocalWallet, Signer},
solc::{Artifact, Project, ProjectPathsConfig},
};
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
// 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 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 std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
@ -6,7 +13,7 @@ use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
// definition
abigen!(
SimpleContract,
"./examples/contract_abi.json",
"./examples/contracts/examples/abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize)
);
@ -17,8 +24,8 @@ async fn main() -> Result<()> {
let anvil = Anvil::new().spawn();
// 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
let source = Path::new(&env!("CARGO_MANIFEST_DIR")).join("examples/contract.sol");
// manifest of `example/contracts`. which will be `../` relative to this file
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 (abi, bytecode, _runtime_bytecode) =
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!(
SimpleContract,
"./examples/contract_abi.json",
"./examples/contracts/examples/abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize)
);
@ -19,7 +19,9 @@ abigen!(
#[tokio::main]
#[cfg(feature = "legacy")]
async fn main() -> eyre::Result<()> {
use ethers::prelude::*;
use std::{convert::TryFrom, path::Path, sync::Arc, time::Duration};
const MOONBEAM_DEV_ENDPOINT: &str = "http://localhost:9933";
// 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 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]
async fn main() -> Result<()> {
let client =
@ -27,8 +28,8 @@ async fn main() -> Result<()> {
let weth = ERC20::new(address, Arc::clone(&client));
// Subscribe Transfer events
let events = weth.events();
let mut stream = events.stream().await?.with_meta();
let events = weth.events().from_block(16232698);
let mut stream = events.stream().await?.with_meta().take(1);
while let Some(Ok((event, meta))) = stream.next().await {
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
use ethers::{prelude::*, utils::Anvil};
use std::{convert::TryFrom, sync::Arc, time::Duration};
use std::{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");

View File

@ -1,7 +1,7 @@
[package]
name = "ethers-wasm"
version = "0.1.0"
edition = "2018"
edition = "2021"
rust-version = "1.64"
authors = ["Matthias Seitz <matthias.seitz@outlook.de>"]
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!(
SimpleContract,
"./../contract_abi.json",
"./abi/contract_abi.json",
event_derives(serde::Deserialize, serde::Serialize)
);

View File

@ -16,7 +16,7 @@ wasm_bindgen_test_configure!(run_in_browser);
// definition in human readable format
abigen!(
SimpleContract,
"../contract_abi.json",
"./abi/contract_abi.json",
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