From 2d51c523badc117c4eadefb0e412a391492543e8 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Fri, 25 Sep 2020 00:33:09 +0300 Subject: [PATCH] feature: Middleware Architecture (#65) * feat: convert Provider to Middleware trait * feat: move gas oracle to middleware crate * feat: move signer to middleware crate * feat: add nonce manager middleware and test stacking * refactor: convert generic middleware jsonrpc type to associated type * feat: move ethers-contract to middleware arch * test(provider): make tests pass * test(middleware): move middleware tests from signer * test: fix ethers examples * fix(contract): make tests compile * chore: fix clippy * feat: deduplicate trait delegation * refactor(signer): deduplicate tx signing logic across signers * fix doctests * fix: examples, celo tests and ci --- .github/workflows/ci.yml | 7 +- Cargo.lock | 22 +- Cargo.toml | 1 + ethers-contract/Cargo.toml | 6 +- .../ethers-contract-abigen/src/contract.rs | 6 +- .../src/contract/common.rs | 11 +- .../src/contract/events.rs | 4 +- .../src/contract/methods.rs | 2 +- ethers-contract/src/call.rs | 46 +-- ethers-contract/src/contract.rs | 50 +-- ethers-contract/src/event.rs | 46 ++- ethers-contract/src/factory.rs | 59 ++- ethers-contract/src/multicall/mod.rs | 61 ++- .../src/multicall/multicall_contract.rs | 31 +- ethers-contract/tests/common/mod.rs | 25 +- ethers-contract/tests/contract.rs | 20 +- ethers-core/src/types/crypto/keys.rs | 110 +----- ethers-core/src/types/crypto/mod.rs | 2 +- ethers-middleware/Cargo.toml | 39 ++ ethers-middleware/src/client.rs | 346 ++++++++++++++++++ .../src/gas_oracle/eth_gas_station.rs | 0 .../src/gas_oracle/etherchain.rs | 0 .../src/gas_oracle/etherscan.rs | 0 .../src/gas_oracle/gas_now.rs | 0 .../src/gas_oracle/middleware.rs | 71 ++++ .../src/gas_oracle/mod.rs | 7 +- ethers-middleware/src/lib.rs | 21 ++ ethers-middleware/src/nonce_manager.rs | 112 ++++++ ethers-middleware/tests/gas_oracle.rs | 74 ++++ ethers-middleware/tests/nonce_manager.rs | 56 +++ ethers-middleware/tests/signer.rs | 69 ++++ ethers-middleware/tests/stack.rs | 52 +++ ethers-providers/src/lib.rs | 236 +++++++++++- ethers-providers/src/pending_transaction.rs | 1 + ethers-providers/src/provider.rs | 196 ++++++---- ethers-providers/src/stream.rs | 6 +- ethers-providers/src/transports/ws.rs | 11 +- ethers-providers/tests/provider.rs | 43 +-- ethers-signers/Cargo.toml | 4 +- ethers-signers/src/client.rs | 334 ----------------- ethers-signers/src/ledger/app.rs | 126 ++----- ethers-signers/src/ledger/mod.rs | 18 +- ethers-signers/src/ledger/types.rs | 4 +- ethers-signers/src/lib.rs | 66 ++-- ethers-signers/src/nonce_manager.rs | 24 -- ethers-signers/src/wallet.rs | 62 +--- ethers-signers/tests/signer.rs | 186 ---------- ethers/Cargo.toml | 4 + ethers/examples/contract.rs | 2 +- ethers/examples/ens.rs | 6 +- ethers/examples/ledger.rs | 5 +- ethers/examples/local_signer.rs | 2 +- ethers/examples/transfer_eth.rs | 2 +- ethers/src/lib.rs | 6 + 54 files changed, 1511 insertions(+), 1189 deletions(-) create mode 100644 ethers-middleware/Cargo.toml create mode 100644 ethers-middleware/src/client.rs rename {ethers-providers => ethers-middleware}/src/gas_oracle/eth_gas_station.rs (100%) rename {ethers-providers => ethers-middleware}/src/gas_oracle/etherchain.rs (100%) rename {ethers-providers => ethers-middleware}/src/gas_oracle/etherscan.rs (100%) rename {ethers-providers => ethers-middleware}/src/gas_oracle/gas_now.rs (100%) create mode 100644 ethers-middleware/src/gas_oracle/middleware.rs rename {ethers-providers => ethers-middleware}/src/gas_oracle/mod.rs (93%) create mode 100644 ethers-middleware/src/lib.rs create mode 100644 ethers-middleware/src/nonce_manager.rs create mode 100644 ethers-middleware/tests/gas_oracle.rs create mode 100644 ethers-middleware/tests/nonce_manager.rs create mode 100644 ethers-middleware/tests/signer.rs create mode 100644 ethers-middleware/tests/stack.rs delete mode 100644 ethers-signers/src/client.rs delete mode 100644 ethers-signers/src/nonce_manager.rs delete mode 100644 ethers-signers/tests/signer.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee1396f3..5c5a0718 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,8 @@ jobs: node-version: 10 - name: Install ganache run: npm install -g ganache-cli + - name: Install libusb (for Ledger) + run: sudo apt install pkg-config libudev-dev - name: Install Solc run: | @@ -47,10 +49,7 @@ jobs: - name: cargo test (Celo) run: | export PATH=$HOME/bin:$PATH - cd ethers-core && cargo test --features="celo" && cd ../ - cd ethers-providers && cargo test --features="celo" && cd ../ - cd ethers-signers && cargo test --features="celo" && cd ../ - cd ethers-contract && cargo test --features="celo" && cd ../ + cargo test --all-features - name: cargo fmt run: cargo fmt --all -- --check diff --git a/Cargo.lock b/Cargo.lock index cbb615d8..6264e537 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,7 @@ dependencies = [ "anyhow", "ethers-contract", "ethers-core", + "ethers-middleware", "ethers-providers", "ethers-signers", "rand", @@ -471,6 +472,7 @@ dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", "ethers-core", + "ethers-middleware", "ethers-providers", "ethers-signers", "futures", @@ -531,6 +533,25 @@ dependencies = [ "tiny-keccak 2.0.2", ] +[[package]] +name = "ethers-middleware" +version = "0.1.3" +dependencies = [ + "async-trait", + "ethers", + "ethers-core", + "ethers-providers", + "ethers-signers", + "futures-util", + "reqwest", + "rustc-hex", + "serde", + "serde-aux", + "thiserror", + "tokio", + "url", +] + [[package]] name = "ethers-providers" version = "0.1.3" @@ -564,7 +585,6 @@ dependencies = [ "coins-ledger", "ethers", "ethers-core", - "ethers-providers", "futures-util", "rustc-hex", "serde", diff --git a/Cargo.toml b/Cargo.toml index f6e967a9..c4a9afbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "./ethers-providers", "./ethers-signers", "./ethers-core", + "./ethers-middleware", ] diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 4d2552a5..f19cd253 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -14,7 +14,6 @@ ethers-contract-abigen = { version = "0.1.3", path = "ethers-contract-abigen", o ethers-contract-derive = { version = "0.1.3", path = "ethers-contract-derive", optional = true } ethers-providers = { version = "0.1.3", path = "../ethers-providers" } -ethers-signers = { version = "0.1.3", path = "../ethers-signers" } ethers-core = { version = "0.1.3", path = "../ethers-core" } serde = { version = "1.0.110", default-features = false } @@ -29,9 +28,12 @@ ethers = { version = "0.1.3", path = "../ethers" } tokio = { version = "0.2.21", default-features = false, features = ["macros"] } serde_json = "1.0.55" +ethers-signers = { version = "0.1.3", path = "../ethers-signers" } +ethers-middleware = { version = "0.1.3", path = "../ethers-middleware" } + [features] abigen = ["ethers-contract-abigen", "ethers-contract-derive"] -celo = ["ethers-core/celo", "ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo"] +celo = ["ethers-core/celo", "ethers-core/celo", "ethers-providers/celo"] [package.metadata.docs.rs] all-features = true diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 4f89938c..277f5150 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -67,12 +67,12 @@ impl Context { #struct_decl - impl<'a, P: JsonRpcClient, S: Signer> #name { + impl<'a, M: Middleware> #name { /// Creates a new contract instance with the specified `ethers` /// client at the given `Address`. The contract derefs to a `ethers::Contract` /// object - pub fn new, C: Into>>>(address: T, client: C) -> Self { - let contract = Contract::new(address.into(), #abi_name.clone(), client.into()); + pub fn new>(address: T, client: Arc) -> Self { + let contract = Contract::new(address.into(), #abi_name.clone(), client); Self(contract) } diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 2e969254..5fa9dcee 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -19,8 +19,7 @@ pub(crate) fn imports(name: &str) -> TokenStream { types::*, // import all the types so that we can codegen for everything }, contract::{Contract, builders::{ContractCall, Event}, Lazy}, - signers::{Client, Signer}, - providers::JsonRpcClient, + providers::Middleware, }; } } @@ -36,17 +35,17 @@ pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> // Struct declaration #[derive(Clone)] - pub struct #name(Contract); + pub struct #name(Contract); // Deref to the inner contract in order to access more specific functions functions - impl std::ops::Deref for #name { - type Target = Contract; + impl std::ops::Deref for #name { + type Target = Contract; fn deref(&self) -> &Self::Target { &self.0 } } - impl std::fmt::Debug for #name { + impl std::fmt::Debug for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_tuple(stringify!(#name)) .field(&self.address()) diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index 17fb8552..f1ab0fe7 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -56,7 +56,7 @@ fn expand_filter(event: &Event) -> Result { Ok(quote! { #doc - pub fn #name(&self) -> Event { + pub fn #name(&self) -> Event { self.0.event(#ev_name).expect("event not found (this should never happen)") } }) @@ -319,7 +319,7 @@ mod tests { assert_quote!(expand_filter(&event).unwrap(), { #[doc = "Gets the contract's `Transfer` event"] - pub fn transfer_filter(&self) -> Event { + pub fn transfer_filter(&self) -> Event { self.0 .event("Transfer") .expect("event not found (this should never happen)") diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index 1f166a03..dc13fd35 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -40,7 +40,7 @@ fn expand_function(function: &Function, alias: Option) -> Result }; + let result = quote! { ContractCall }; let arg = expand_inputs_call_arg(&function.inputs); let doc = util::expand_doc(&format!( diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 4ca09660..7b684219 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -2,8 +2,7 @@ use ethers_core::{ abi::{Detokenize, Error as AbiError, Function, InvalidOutputType}, types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256}, }; -use ethers_providers::{JsonRpcClient, ProviderError}; -use ethers_signers::{Client, ClientError, Signer}; +use ethers_providers::Middleware; use std::{fmt::Debug, marker::PhantomData, sync::Arc}; @@ -11,7 +10,7 @@ use thiserror::Error as ThisError; #[derive(ThisError, Debug)] /// An Error which is thrown when interacting with a smart contract -pub enum ContractError { +pub enum ContractError { /// Thrown when the ABI decoding fails #[error(transparent)] DecodingError(#[from] AbiError), @@ -20,13 +19,9 @@ pub enum ContractError { #[error(transparent)] DetokenizationError(#[from] InvalidOutputType), - /// Thrown when a client call fails - #[error(transparent)] - ClientError(#[from] ClientError), - /// Thrown when a provider call fails - #[error(transparent)] - ProviderError(#[from] ProviderError), + #[error("{0}")] + MiddlewareError(M::Error), /// Thrown during deployment if a constructor argument was passed in the `deploy` /// call but a constructor was not present in the ABI @@ -42,18 +37,18 @@ pub enum ContractError { #[derive(Debug, Clone)] #[must_use = "contract calls do nothing unless you `send` or `call` them"] /// Helper for managing a transaction before submitting it to a node -pub struct ContractCall { +pub struct ContractCall { /// The raw transaction object pub tx: TransactionRequest, /// The ABI of the function being called pub function: Function, /// Optional block number to be used when calculating the transaction's gas and nonce pub block: Option, - pub(crate) client: Arc>, + pub(crate) client: Arc, pub(crate) datatype: PhantomData, } -impl ContractCall { +impl ContractCall { /// Sets the `from` field in the transaction to the provided value pub fn from>(mut self, from: T) -> Self { self.tx.from = Some(from.into()); @@ -85,10 +80,9 @@ impl ContractCall { } } -impl ContractCall +impl ContractCall where - S: Signer, - P: JsonRpcClient, + M: Middleware, D: Detokenize, { /// Returns the underlying transaction's ABI encoded data @@ -97,8 +91,11 @@ where } /// Returns the estimated gas cost for the underlying transaction to be executed - pub async fn estimate_gas(&self) -> Result { - Ok(self.client.estimate_gas(&self.tx).await?) + pub async fn estimate_gas(&self) -> Result> { + self.client + .estimate_gas(&self.tx) + .await + .map_err(ContractError::MiddlewareError) } /// Queries the blockchain via an `eth_call` for the provided transaction. @@ -110,8 +107,12 @@ where /// and return the return type of the transaction without mutating the state /// /// Note: this function _does not_ send a transaction from your account - pub async fn call(&self) -> Result { - let bytes = self.client.call(&self.tx, self.block).await?; + pub async fn call(&self) -> Result> { + let bytes = self + .client + .call(&self.tx, self.block) + .await + .map_err(ContractError::MiddlewareError)?; let tokens = self.function.decode_output(&bytes.0)?; let data = D::from_tokens(tokens)?; @@ -120,7 +121,10 @@ where } /// Signs and broadcasts the provided transaction - pub async fn send(self) -> Result { - Ok(self.client.send_transaction(self.tx, self.block).await?) + pub async fn send(self) -> Result> { + self.client + .send_transaction(self.tx, self.block) + .await + .map_err(ContractError::MiddlewareError) } } diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index cb31a07c..861c086d 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -4,8 +4,7 @@ use ethers_core::{ abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}, types::{Address, Filter, NameOrAddress, Selector, TransactionRequest, TxHash}, }; -use ethers_providers::{JsonRpcClient, PendingTransaction}; -use ethers_signers::{Client, Signer}; +use ethers_providers::{Middleware, PendingTransaction}; use rustc_hex::ToHex; use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, sync::Arc}; @@ -74,9 +73,7 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, syn /// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","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":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?; /// /// // connect to the network -/// let provider = Provider::::try_from("http://localhost:8545").unwrap(); -/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" -/// .parse::()?.connect(provider); +/// let client = Provider::::try_from("http://localhost:8545").unwrap(); /// /// // create the contract object at the address /// let contract = Contract::new(address, abi, client); @@ -108,15 +105,14 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, syn /// # async fn foo() -> Result<(), Box> { /// use ethers_core::{abi::Abi, types::Address}; /// use ethers_contract::Contract; -/// use ethers_providers::{Provider, Http}; +/// use ethers_providers::{Provider, Http, Middleware}; /// use ethers_signers::Wallet; /// use std::convert::TryFrom; /// use ethers_core::abi::{Detokenize, Token, InvalidOutputType}; /// # // this is a fake address used just for this example /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; /// # let abi: Abi = serde_json::from_str(r#"[]"#)?; -/// # let provider = Provider::::try_from("http://localhost:8545").unwrap(); -/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::()?.connect(provider); +/// # let client = Provider::::try_from("http://localhost:8545").unwrap(); /// # let contract = Contract::new(address, abi, client); /// /// #[derive(Clone, Debug)] @@ -163,8 +159,8 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, syn /// [`event`]: method@crate::Contract::event /// [`method`]: method@crate::Contract::method #[derive(Debug, Clone)] -pub struct Contract { - client: Arc>, +pub struct Contract { + client: Arc, abi: Abi, address: Address, @@ -175,13 +171,9 @@ pub struct Contract { methods: HashMap, } -impl Contract -where - S: Signer, - P: JsonRpcClient, -{ +impl Contract { /// Creates a new contract from the provided client, abi and address - pub fn new(address: Address, abi: Abi, client: impl Into>>) -> Self { + pub fn new(address: Address, abi: Abi, client: impl Into>) -> Self { let methods = create_mapping(&abi.functions, |function| function.selector()); Self { @@ -193,11 +185,11 @@ where } /// Returns an [`Event`](crate::builders::Event) builder for the provided event name. - pub fn event(&self, name: &str) -> Result, Error> { + pub fn event(&self, name: &str) -> Result, Error> { // get the event's full name let event = self.abi.event(name)?; Ok(Event { - provider: &self.client.provider(), + provider: &self.client, filter: Filter::new() .event(&event.abi_signature()) .address(self.address), @@ -213,7 +205,7 @@ where &self, name: &str, args: T, - ) -> Result, Error> { + ) -> Result, Error> { // get the function let function = self.abi.function(name)?; self.method_func(function, args) @@ -225,7 +217,7 @@ where &self, signature: Selector, args: T, - ) -> Result, Error> { + ) -> Result, Error> { let function = self .methods .get(&signature) @@ -238,7 +230,7 @@ where &self, function: &Function, args: T, - ) -> Result, Error> { + ) -> Result, Error> { let tokens = args.into_tokens(); // create the calldata @@ -265,8 +257,7 @@ where /// Clones `self` internally pub fn at>(&self, address: T) -> Self where - P: Clone, - S: Clone, + M: Clone, { let mut this = self.clone(); this.address = address.into(); @@ -276,10 +267,9 @@ where /// Returns a new contract instance using the provided client /// /// Clones `self` internally - pub fn connect(&self, client: Arc>) -> Self + pub fn connect(&self, client: Arc) -> Self where - P: Clone, - S: Clone, + M: Clone, { let mut this = self.clone(); this.client = client; @@ -297,14 +287,12 @@ where } /// Returns a reference to the contract's client - pub fn client(&self) -> &Client { + pub fn client(&self) -> &M { &self.client } - /// Helper which creates a pending transaction object from a transaction hash - /// using the provider's polling interval - pub fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> { - self.client.provider().pending_transaction(tx_hash) + pub fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, M::Provider> { + self.client.pending_transaction(tx_hash) } } diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 8f5997f9..401dc01b 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -1,6 +1,6 @@ use crate::ContractError; -use ethers_providers::{JsonRpcClient, Provider}; +use ethers_providers::Middleware; use ethers_core::{ abi::{Detokenize, Event as AbiEvent, RawLog}, @@ -13,17 +13,17 @@ use std::marker::PhantomData; /// Helper for managing the event filter before querying or streaming its logs #[derive(Debug)] #[must_use = "event filters do nothing unless you `query` or `stream` them"] -pub struct Event<'a: 'b, 'b, P, D> { +pub struct Event<'a: 'b, 'b, M, D> { /// The event filter's state pub filter: Filter, /// The ABI of the event which is being filtered pub event: &'b AbiEvent, - pub(crate) provider: &'a Provider

, + pub(crate) provider: &'a M, pub(crate) datatype: PhantomData, } // TODO: Improve these functions -impl Event<'_, '_, P, D> { +impl Event<'_, '_, M, D> { /// Sets the filter's `from` block #[allow(clippy::wrong_self_convention)] pub fn from_block>(mut self, block: T) -> Self { @@ -63,41 +63,53 @@ impl Event<'_, '_, P, D> { } } -impl<'a, 'b, P, D> Event<'a, 'b, P, D> +impl<'a, 'b, M, D> Event<'a, 'b, M, D> where - P: JsonRpcClient, + M: Middleware, D: 'b + Detokenize + Clone, 'a: 'b, { /// Returns a stream for the event pub async fn stream( self, - ) -> Result> + 'b, ContractError> { - let filter = self.provider.watch(&self.filter).await?; + ) -> Result>> + 'b, ContractError> { + let filter = self + .provider + .watch(&self.filter) + .await + .map_err(ContractError::MiddlewareError)?; Ok(filter.stream().map(move |log| self.parse_log(log))) } } -impl Event<'_, '_, P, D> +impl Event<'_, '_, M, D> where - P: JsonRpcClient, + M: Middleware, D: Detokenize + Clone, { /// Queries the blockchain for the selected filter and returns a vector of matching /// event logs - pub async fn query(&self) -> Result, ContractError> { - let logs = self.provider.get_logs(&self.filter).await?; + pub async fn query(&self) -> Result, ContractError> { + let logs = self + .provider + .get_logs(&self.filter) + .await + .map_err(ContractError::MiddlewareError)?; let events = logs .into_iter() .map(|log| self.parse_log(log)) - .collect::, ContractError>>()?; + .collect::, ContractError>>()?; Ok(events) } /// Queries the blockchain for the selected filter and returns a vector of logs /// along with their metadata - pub async fn query_with_meta(&self) -> Result, ContractError> { - let logs = self.provider.get_logs(&self.filter).await?; + pub async fn query_with_meta(&self) -> Result, ContractError> { + let logs = self + .provider + .get_logs(&self.filter) + .await + .map_err(ContractError::MiddlewareError)?; let events = logs .into_iter() .map(|log| { @@ -105,11 +117,11 @@ where let event = self.parse_log(log)?; Ok((event, meta)) }) - .collect::>()?; + .collect::>>()?; Ok(events) } - fn parse_log(&self, log: Log) -> Result { + fn parse_log(&self, log: Log) -> Result> { // ethabi parses the unindexed and indexed logs together to a // vector of tokens let tokens = self diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index c8d8f4b2..8e24e2c6 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -4,40 +4,28 @@ use ethers_core::{ abi::{Abi, Tokenize}, types::{BlockNumber, Bytes, TransactionRequest}, }; -use ethers_providers::JsonRpcClient; -use ethers_signers::{Client, Signer}; +use ethers_providers::Middleware; -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; #[derive(Debug, Clone)] /// Helper which manages the deployment transaction of a smart contract -pub struct Deployer { +pub struct Deployer { /// The deployer's transaction, exposed for overriding the defaults pub tx: TransactionRequest, abi: Abi, - client: Arc>, + client: Arc, confs: usize, block: BlockNumber, - interval: Duration, } -impl Deployer -where - S: Signer, - P: JsonRpcClient, -{ +impl Deployer { /// Sets the number of confirmations to wait for the contract deployment transaction pub fn confirmations>(mut self, confirmations: T) -> Self { self.confs = confirmations.into(); self } - /// Sets the poll interval for the pending deployment transaction's inclusion - pub fn interval>(mut self, interval: T) -> Self { - self.interval = interval.into(); - self - } - pub fn block>(mut self, block: T) -> Self { self.block = block.into(); self @@ -46,18 +34,20 @@ where /// Broadcasts the contract deployment transaction and after waiting for it to /// be sufficiently confirmed (default: 1), it returns a [`Contract`](crate::Contract) /// struct at the deployed contract's address. - pub async fn send(self) -> Result, ContractError> { + pub async fn send(self) -> Result, ContractError> { let tx_hash = self .client .send_transaction(self.tx, Some(self.block)) - .await?; + .await + .map_err(ContractError::MiddlewareError)?; + + // TODO: Should this be calculated "optimistically" by address/nonce? let receipt = self .client .pending_transaction(tx_hash) - .interval(self.interval) .confirmations(self.confs) - .await?; - + .await + .map_err(|_| ContractError::ContractNotDeployed)?; let address = receipt .contract_address .ok_or(ContractError::ContractNotDeployed)?; @@ -72,7 +62,7 @@ where } /// Returns a reference to the deployer's client - pub fn client(&self) -> &Client { + pub fn client(&self) -> &M { &self.client } } @@ -105,9 +95,8 @@ where /// .expect("could not find contract"); /// /// // connect to the network -/// let provider = Provider::::try_from("http://localhost:8545").unwrap(); -/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" -/// .parse::()?.connect(provider); +/// let client = Provider::::try_from("http://localhost:8545").unwrap(); +/// let client = std::sync::Arc::new(client); /// /// // create a factory which will be used to deploy instances of the contract /// let factory = ContractFactory::new(contract.abi.clone(), contract.bytecode.clone(), client); @@ -122,23 +111,19 @@ where /// println!("{}", contract.address()); /// # Ok(()) /// # } -pub struct ContractFactory { - client: Arc>, +pub struct ContractFactory { + client: Arc, abi: Abi, bytecode: Bytes, } -impl ContractFactory -where - S: Signer, - P: JsonRpcClient, -{ +impl ContractFactory { /// Creates a factory for deployment of the Contract with bytecode, and the /// constructor defined in the abi. The client will be used to send any deployment /// transaction. - pub fn new(abi: Abi, bytecode: Bytes, client: impl Into>>) -> Self { + pub fn new(abi: Abi, bytecode: Bytes, client: Arc) -> Self { Self { - client: client.into(), + client, abi, bytecode, } @@ -152,7 +137,7 @@ where /// 1. If there are no constructor arguments, you should pass `()` as the argument. /// 1. The default poll duration is 7 seconds. /// 1. The default number of confirmations is 1 block. - pub fn deploy(self, constructor_args: T) -> Result, ContractError> { + pub fn deploy(self, constructor_args: T) -> Result, ContractError> { // Encode the constructor args & concatenate with the bytecode if necessary let params = constructor_args.into_tokens(); let data: Bytes = match (self.abi.constructor(), params.is_empty()) { @@ -167,7 +152,6 @@ where // create the tx object. Since we're deploying a contract, `to` is `None` let tx = TransactionRequest { - from: Some(self.client.address()), to: None, data: Some(data), ..Default::default() @@ -179,7 +163,6 @@ where tx, confs: 1, block: BlockNumber::Latest, - interval: self.client.get_interval(), }) } } diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 6ea2eaca..d33931e9 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -2,8 +2,7 @@ use ethers_core::{ abi::{Detokenize, Function, Token}, types::{Address, BlockNumber, NameOrAddress, TxHash, U256}, }; -use ethers_providers::JsonRpcClient; -use ethers_signers::{Client, Signer}; +use ethers_providers::Middleware; use std::{collections::HashMap, str::FromStr, sync::Arc}; @@ -62,8 +61,7 @@ pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { /// use ethers::{ /// abi::Abi, /// contract::{Contract, Multicall}, -/// providers::{Http, Provider}, -/// signers::{Client, Wallet}, +/// providers::{Middleware, Http, Provider}, /// types::{Address, H256, U256}, /// }; /// use std::{convert::TryFrom, sync::Arc}; @@ -76,13 +74,11 @@ pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { /// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","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":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?; /// /// // connect to the network -/// let provider = Provider::::try_from("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?; -/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" -/// .parse::()?.connect(provider); +/// let client = Provider::::try_from("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?; /// /// // create the contract object. This will be used to construct the calls for multicall /// let client = Arc::new(client); -/// let contract = Contract::new(address, abi, Arc::clone(&client)); +/// let contract = Contract::>::new(address, abi, Arc::clone(&client)); /// /// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve. /// // But we will let `Multicall` to take care of that for us @@ -114,7 +110,7 @@ pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { /// // `await`ing the `send` method waits for the transaction to be broadcast, which also /// // returns the transaction hash /// let tx_hash = multicall.send().await?; -/// let _tx_receipt = client.provider().pending_transaction(tx_hash).await?; +/// let _tx_receipt = client.pending_transaction(tx_hash).await?; /// /// // you can also query ETH balances of multiple addresses /// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::

()?; @@ -132,10 +128,10 @@ pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { /// [`block`]: method@crate::Multicall::block /// [`add_call`]: methond@crate::Multicall::add_call #[derive(Clone)] -pub struct Multicall { +pub struct Multicall { calls: Vec, block: Option, - contract: MulticallContract, + contract: MulticallContract, } #[derive(Clone)] @@ -147,11 +143,7 @@ pub struct Call { function: Function, } -impl Multicall -where - P: JsonRpcClient, - S: Signer, -{ +impl Multicall { /// Creates a new Multicall instance from the provided client. If provided with an `address`, /// it instantiates the Multicall contract with that address. Otherwise it fetches the address /// from the address book. @@ -159,10 +151,10 @@ where /// # Panics /// If a `None` address is provided, and the provided client also does not belong to one of /// the supported network IDs (mainnet, kovan, rinkeby and goerli) - pub async fn new>>>( + pub async fn new>>( client: C, address: Option
, - ) -> Result { + ) -> Result> { let client = client.into(); // Fetch chain id and the corresponding address of Multicall contract @@ -171,7 +163,10 @@ where let address: Address = match address { Some(addr) => addr, None => { - let chain_id = client.get_chainid().await?; + let chain_id = client + .get_chainid() + .await + .map_err(ContractError::MiddlewareError)?; match ADDRESS_BOOK.get(&chain_id) { Some(addr) => *addr, None => panic!( @@ -203,7 +198,7 @@ where /// /// If more than the maximum number of supported calls are added. The maximum /// limits is constrained due to tokenization/detokenization support for tuples - pub fn add_call(&mut self, call: ContractCall) -> &mut Self { + pub fn add_call(&mut self, call: ContractCall) -> &mut Self { if self.calls.len() >= 16 { panic!("Cannot support more than {} calls", 16); } @@ -242,14 +237,12 @@ where /// # use ethers::prelude::*; /// # use std::{sync::Arc, convert::TryFrom}; /// # - /// # let provider = Provider::::try_from("http://localhost:8545")?; - /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" - /// # .parse::()?.connect(provider); + /// # let client = Provider::::try_from("http://localhost:8545")?; /// # let client = Arc::new(client); /// # /// # let abi = serde_json::from_str("")?; /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; - /// # let contract = Contract::new(address, abi, client.clone()); + /// # let contract = Contract::>::new(address, abi, client.clone()); /// # /// # let broadcast_1 = contract.method::<_, H256>("setValue", "some value".to_owned())?; /// # let broadcast_2 = contract.method::<_, H256>("setValue", "new value".to_owned())?; @@ -278,7 +271,7 @@ where /// Queries the Ethereum blockchain via an `eth_call`, but via the Multicall contract. /// - /// It returns a [`ContractError`] if there is any error in the RPC call or while + /// It returns a [`ContractError`] if there is any error in the RPC call or while /// detokenizing the tokens back to the expected return type. The return type must be /// annonated while calling this method. /// @@ -287,9 +280,7 @@ where /// # use ethers::prelude::*; /// # use std::convert::TryFrom; /// # - /// # let provider = Provider::::try_from("http://localhost:8545")?; - /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" - /// # .parse::()?.connect(provider); + /// # let client = Provider::::try_from("http://localhost:8545")?; /// # /// # let multicall = Multicall::new(client, None).await?; /// // If the Solidity function calls has the following return types: @@ -303,8 +294,8 @@ where /// /// Note: this method _does not_ send a transaction from your account /// - /// [`ContractError`]: crate::ContractError - pub async fn call(&self) -> Result { + /// [`ContractError`]: crate::ContractError + pub async fn call(&self) -> Result> { let contract_call = self.as_contract_call(); // Fetch response from the Multicall contract @@ -324,7 +315,7 @@ where _ => Token::Tuple(tokens), }) }) - .collect::, ContractError>>()?; + .collect::, ContractError>>()?; // Form tokens that represent tuples let tokens = vec![Token::Tuple(tokens)]; @@ -341,9 +332,7 @@ where /// # async fn foo() -> Result<(), Box> { /// # use ethers::prelude::*; /// # use std::convert::TryFrom; - /// # let provider = Provider::::try_from("http://localhost:8545")?; - /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" - /// # .parse::()?.connect(provider); + /// # let client = Provider::::try_from("http://localhost:8545")?; /// # let multicall = Multicall::new(client, None).await?; /// let tx_hash = multicall.send().await?; /// # Ok(()) @@ -352,7 +341,7 @@ where /// /// Note: this method sends a transaction from your account, and will return an error /// if you do not have sufficient funds to pay for gas - pub async fn send(&self) -> Result { + pub async fn send(&self) -> Result> { let contract_call = self.as_contract_call(); // Broadcast transaction and return the transaction hash @@ -361,7 +350,7 @@ where Ok(tx_hash) } - fn as_contract_call(&self) -> ContractCall>)> { + fn as_contract_call(&self) -> ContractCall>)> { // Map the Multicall struct into appropriate types for `aggregate` function let calls: Vec<(Address, Vec)> = self .calls diff --git a/ethers-contract/src/multicall/multicall_contract.rs b/ethers-contract/src/multicall/multicall_contract.rs index 6280c688..393eb913 100644 --- a/ethers-contract/src/multicall/multicall_contract.rs +++ b/ethers-contract/src/multicall/multicall_contract.rs @@ -10,33 +10,32 @@ mod multicallcontract_mod { abi::{Abi, Detokenize, InvalidOutputType, Token, Tokenizable}, types::*, }; - use ethers_providers::JsonRpcClient; - use ethers_signers::{Client, Signer}; + use ethers_providers::{JsonRpcClient, Middleware}; #[doc = "MulticallContract was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs"] use std::sync::Arc; pub static MULTICALLCONTRACT_ABI: Lazy = Lazy::new(|| { serde_json :: from_str ( "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct MulticallContract.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]" ) . expect ( "invalid abi" ) }); #[derive(Clone)] - pub struct MulticallContract(Contract); - impl std::ops::Deref for MulticallContract { - type Target = Contract; + pub struct MulticallContract(Contract); + impl std::ops::Deref for MulticallContract { + type Target = Contract; fn deref(&self) -> &Self::Target { &self.0 } } - impl std::fmt::Debug for MulticallContract { + impl std::fmt::Debug for MulticallContract { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_tuple(stringify!(MulticallContract)) .field(&self.address()) .finish() } } - impl<'a, P: JsonRpcClient, S: Signer> MulticallContract { + impl<'a, M: Middleware> MulticallContract { #[doc = r" Creates a new contract instance with the specified `ethers`"] #[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"] #[doc = r" object"] - pub fn new, C: Into>>>(address: T, client: C) -> Self { + pub fn new, C: Into>>(address: T, client: C) -> Self { let contract = Contract::new(address.into(), MULTICALLCONTRACT_ABI.clone(), client.into()); Self(contract) @@ -45,49 +44,49 @@ mod multicallcontract_mod { pub fn aggregate( &self, calls: Vec<(Address, Vec)>, - ) -> ContractCall>)> { + ) -> ContractCall>)> { self.0 .method_hash([37, 45, 186, 66], calls) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getCurrentBlockDifficulty` (0x72425d9d) function"] - pub fn get_current_block_difficulty(&self) -> ContractCall { + pub fn get_current_block_difficulty(&self) -> ContractCall { self.0 .method_hash([114, 66, 93, 157], ()) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getCurrentBlockGasLimit` (0x86d516e8) function"] - pub fn get_current_block_gas_limit(&self) -> ContractCall { + pub fn get_current_block_gas_limit(&self) -> ContractCall { self.0 .method_hash([134, 213, 22, 232], ()) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getCurrentBlockTimestamp` (0x0f28c97d) function"] - pub fn get_current_block_timestamp(&self) -> ContractCall { + pub fn get_current_block_timestamp(&self) -> ContractCall { self.0 .method_hash([15, 40, 201, 125], ()) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getCurrentBlockCoinbase` (0xa8b0574e) function"] - pub fn get_current_block_coinbase(&self) -> ContractCall { + pub fn get_current_block_coinbase(&self) -> ContractCall { self.0 .method_hash([168, 176, 87, 78], ()) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getBlockHash` (0xee82ac5e) function"] - pub fn get_block_hash(&self, block_number: U256) -> ContractCall { + pub fn get_block_hash(&self, block_number: U256) -> ContractCall { self.0 .method_hash([238, 130, 172, 94], block_number) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getEthBalance` (0x4d2301cc) function"] - pub fn get_eth_balance(&self, addr: Address) -> ContractCall { + pub fn get_eth_balance(&self, addr: Address) -> ContractCall { self.0 .method_hash([77, 35, 1, 204], addr) .expect("method not found (this should never happen)") } #[doc = "Calls the contract's `getLastBlockHash` (0x27e86d6e) function"] - pub fn get_last_block_hash(&self) -> ContractCall { + pub fn get_last_block_hash(&self) -> ContractCall { self.0 .method_hash([39, 232, 109, 110], ()) .expect("method not found (this should never happen)") diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index c22802b0..6697a240 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -5,8 +5,9 @@ use ethers_core::{ use ethers_contract::{Contract, ContractFactory}; use ethers_core::utils::{GanacheInstance, Solc}; -use ethers_providers::{Http, Provider}; -use ethers_signers::{Client, Wallet}; +use ethers_middleware::Client; +use ethers_providers::{Http, Middleware, Provider}; +use ethers_signers::Wallet; use std::{convert::TryFrom, sync::Arc, time::Duration}; // Note: We also provide the `abigen` macro for generating these bindings automatically @@ -43,23 +44,19 @@ pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) { (contract.abi.clone(), contract.bytecode.clone()) } +type HttpWallet = Client, Wallet>; + /// connects the private key to http://localhost:8545 -pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc> { - let provider = Provider::::try_from(ganache.endpoint()).unwrap(); +pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc { + let provider = Provider::::try_from(ganache.endpoint()) + .unwrap() + .interval(Duration::from_millis(10u64)); let wallet: Wallet = ganache.keys()[idx].clone().into(); - Arc::new( - wallet - .connect(provider) - .interval(Duration::from_millis(10u64)), - ) + Arc::new(Client::new(provider, wallet)) } /// Launches a ganache instance and deploys the SimpleStorage contract -pub async fn deploy( - client: Arc>, - abi: Abi, - bytecode: Bytes, -) -> Contract { +pub async fn deploy(client: Arc, abi: Abi, bytecode: Bytes) -> Contract { let factory = ContractFactory::new(abi, bytecode, client); factory .deploy("initial value".to_string()) diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 824c91f8..a030b8df 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -8,8 +8,7 @@ mod eth_tests { use super::*; use ethers::{ contract::Multicall, - providers::{Http, Provider, StreamExt}, - signers::Client, + providers::{Http, Middleware, Provider, StreamExt}, types::{Address, U256}, utils::Ganache, }; @@ -169,7 +168,8 @@ mod eth_tests { // get the first account let deployer = provider.get_accounts().await.unwrap()[0]; - let client = Arc::new(Client::from(provider).with_sender(deployer)); + let client = Arc::new(provider.with_sender(deployer)); + dbg!(deployer); let contract = deploy(client, abi, bytecode).await; @@ -341,6 +341,7 @@ mod eth_tests { mod celo_tests { use super::*; use ethers::{ + middleware::Client, providers::{Http, Provider}, signers::Wallet, types::BlockNumber, @@ -352,15 +353,16 @@ mod celo_tests { let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); // Celo testnet - let provider = - Provider::::try_from("https://alfajores-forno.celo-testnet.org").unwrap(); + let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") + .unwrap() + .interval(Duration::from_millis(6000)); // Funded with https://celo.org/developers/faucet - let client = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" + let wallet = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" .parse::() - .unwrap() - .connect(provider) - .interval(Duration::from_millis(6000)); + .unwrap(); + + let client = Client::new(provider, wallet); let client = Arc::new(client); let factory = ContractFactory::new(abi, bytecode, client); diff --git a/ethers-core/src/types/crypto/keys.rs b/ethers-core/src/types/crypto/keys.rs index f38e7eb9..55361133 100644 --- a/ethers-core/src/types/crypto/keys.rs +++ b/ethers-core/src/types/crypto/keys.rs @@ -1,5 +1,5 @@ use crate::{ - types::{Address, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256}, + types::{Address, Signature, TransactionRequest, H256}, utils::{hash_message, keccak256}, }; @@ -17,7 +17,6 @@ use serde::{ Deserialize, Deserializer, Serialize, Serializer, }; use std::{fmt, ops::Deref, str::FromStr}; -use thiserror::Error; /// A private key on Secp256k1 #[derive(Clone, Debug, PartialEq, Eq)] @@ -60,21 +59,6 @@ impl FromStr for PrivateKey { } } -/// An error which may be thrown when attempting to sign a transaction with -/// missing fields -#[derive(Clone, Debug, Error)] -pub enum TxError { - /// Thrown if the `nonce` field is missing - #[error("no nonce was specified")] - NonceMissing, - /// Thrown if the `gas_price` field is missing - #[error("no gas price was specified")] - GasPriceMissing, - /// Thrown if the `gas` field is missing - #[error("no gas was specified")] - GasMissing, -} - impl PrivateKey { pub fn new(rng: &mut R) -> Self { PrivateKey(SecretKey::random(rng)) @@ -110,62 +94,13 @@ impl PrivateKey { /// /// If `tx.to` is an ENS name. The caller MUST take care of name resolution before /// calling this function. - pub fn sign_transaction( - &self, - tx: TransactionRequest, - chain_id: Option, - ) -> Result { - // The nonce, gas and gasprice fields must already be populated - let nonce = tx.nonce.ok_or(TxError::NonceMissing)?; - let gas_price = tx.gas_price.ok_or(TxError::GasPriceMissing)?; - let gas = tx.gas.ok_or(TxError::GasMissing)?; - + pub fn sign_transaction(&self, tx: &TransactionRequest, chain_id: Option) -> Signature { // Get the transaction's sighash let sighash = tx.sighash(chain_id); let message = Message::parse_slice(sighash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); - // Sign it (with replay protection if applicable) - let signature = self.sign_with_eip155(&message, chain_id); - - // Get the actual transaction hash - let rlp = tx.rlp_signed(&signature); - let hash = keccak256(&rlp.0); - - // This function should not be called with ENS names - let to = tx.to.map(|to| match to { - NameOrAddress::Address(inner) => inner, - NameOrAddress::Name(_) => { - panic!("Expected `to` to be an Ethereum Address, not an ENS name") - } - }); - - Ok(Transaction { - hash: hash.into(), - nonce, - from: self.into(), - to, - value: tx.value.unwrap_or_default(), - gas_price, - gas, - input: tx.data.unwrap_or_default(), - v: signature.v.into(), - r: U256::from_big_endian(signature.r.as_bytes()), - s: U256::from_big_endian(signature.s.as_bytes()), - - // Leave these empty as they're only used for included transactions - block_hash: None, - block_number: None, - transaction_index: None, - - // Celo support - #[cfg(feature = "celo")] - fee_currency: tx.fee_currency, - #[cfg(feature = "celo")] - gateway_fee: tx.gateway_fee, - #[cfg(feature = "celo")] - gateway_fee_recipient: tx.gateway_fee_recipient, - }) + self.sign_with_eip155(&message, chain_id) } fn sign_with_eip155(&self, message: &Message, chain_id: Option) -> Signature { @@ -325,45 +260,6 @@ mod tests { } } - #[test] - #[cfg(not(feature = "celo"))] - fn signs_tx() { - use crate::types::{Address, Bytes}; - // retrieved test vector from: - // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx = TransactionRequest { - from: None, - to: Some( - "F0109fC8DF283027b6285cc889F5aA624EaC1F55" - .parse::
() - .unwrap() - .into(), - ), - value: Some(1_000_000_000.into()), - gas: Some(2_000_000.into()), - nonce: Some(0.into()), - gas_price: Some(21_000_000_000u128.into()), - data: None, - }; - let chain_id = 1; - - let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" - .parse() - .unwrap(); - - let tx = key.sign_transaction(tx, Some(chain_id)).unwrap(); - - assert_eq!( - tx.hash, - "de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593" - .parse() - .unwrap() - ); - - let expected_rlp = Bytes("f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68".from_hex().unwrap()); - assert_eq!(tx.rlp(), expected_rlp); - } - #[test] fn signs_data() { // test vector taken from: diff --git a/ethers-core/src/types/crypto/mod.rs b/ethers-core/src/types/crypto/mod.rs index e7e1b4ee..20ffaa34 100644 --- a/ethers-core/src/types/crypto/mod.rs +++ b/ethers-core/src/types/crypto/mod.rs @@ -1,5 +1,5 @@ mod keys; -pub use keys::{PrivateKey, PublicKey, TxError}; +pub use keys::{PrivateKey, PublicKey}; mod signature; pub use signature::{Signature, SignatureError}; diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml new file mode 100644 index 00000000..f6b6c6eb --- /dev/null +++ b/ethers-middleware/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "ethers-middleware" +license = "MIT OR Apache-2.0" +version = "0.1.3" +authors = ["Georgios Konstantopoulos "] +edition = "2018" +description = "Middleware implementations for the ethers-rs crate" +homepage = "https://docs.rs/ethers" +repository = "https://github.com/gakonst/ethers-rs" +keywords = ["ethereum", "web3", "celo", "ethers"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +ethers-core = { version = "0.1.3", path = "../ethers-core" } +ethers-providers = { version = "0.1.3", path = "../ethers-providers" } +ethers-signers = { version = "0.1.3", path = "../ethers-signers" } + +async-trait = { version = "0.1.31", default-features = false } +serde = { version = "1.0.110", default-features = false, features = ["derive"] } +thiserror = { version = "1.0.15", default-features = false } + +futures-util = { version = "0.3.5", default-features = false } + +# for gas oracles +serde-aux = "0.6.1" +reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] } +url = { version = "2.1.1", default-features = false } + +[dev-dependencies] +ethers = { version = "0.1.3", path = "../ethers" } + +rustc-hex = "2.1.0" +tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] } + +[features] +celo = ["ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo"] diff --git a/ethers-middleware/src/client.rs b/ethers-middleware/src/client.rs new file mode 100644 index 00000000..1ff27042 --- /dev/null +++ b/ethers-middleware/src/client.rs @@ -0,0 +1,346 @@ +use ethers_signers::Signer; + +use ethers_core::{ + types::{ + Address, BlockNumber, Bytes, NameOrAddress, Signature, Transaction, TransactionRequest, + TxHash, U256, + }, + utils::keccak256, +}; +use ethers_providers::Middleware; + +use async_trait::async_trait; +use futures_util::{future::ok, join}; +use std::future::Future; +use thiserror::Error; + +#[derive(Clone, Debug)] +/// Middleware used for locally signing transactions, compatible with any implementer +/// of the [`Signer`] trait. +/// +/// # Example +/// +/// ```no_run +/// use ethers::{ +/// providers::{Middleware, Provider, Http}, +/// signers::Wallet, +/// middleware::Client, +/// types::{Address, TransactionRequest}, +/// }; +/// use std::convert::TryFrom; +/// +/// # async fn foo() -> Result<(), Box> { +/// let provider = Provider::::try_from("http://localhost:8545") +/// .expect("could not instantiate HTTP Provider"); +/// +/// // Transactions will be signed with the private key below and will be broadcast +/// // via the eth_sendRawTransaction API) +/// let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// .parse()?; +/// +/// let mut client = Client::new(provider, wallet); +/// +/// // since it derefs to `Provider`, we can just call any of the JSON-RPC API methods +/// let block = client.get_block(100u64).await?; +/// +/// // You can use the node's `eth_sign` and `eth_sendTransaction` calls by calling the +/// // internal provider's method. +/// let signed_msg = client.sign(b"hello".to_vec(), &client.address()).await?; +/// +/// let tx = TransactionRequest::pay("vitalik.eth", 100); +/// let tx_hash = client.send_transaction(tx, None).await?; +/// +/// // You can `await` on the pending transaction to get the receipt with a pre-specified +/// // number of confirmations +/// let receipt = client.pending_transaction(tx_hash).confirmations(6).await?; +/// +/// // You can connect with other wallets at runtime via the `with_signer` function +/// let wallet2: Wallet = "cd8c407233c0560f6de24bb2dc60a8b02335c959a1a17f749ce6c1ccf63d74a7" +/// .parse()?; +/// +/// let signed_msg2 = client.with_signer(wallet2).sign(b"hello".to_vec(), &client.address()).await?; +/// +/// // This call will be made with `wallet2` since `with_signer` takes a mutable reference. +/// let tx2 = TransactionRequest::new() +/// .to("0xd8da6bf26964af9d7eed9e03e53415d37aa96045".parse::
()?) +/// .value(200); +/// let tx_hash2 = client.send_transaction(tx2, None).await?; +/// +/// # Ok(()) +/// # } +/// +/// ``` +/// +/// [`Provider`]: ethers_providers::Provider +pub struct Client { + pub(crate) inner: M, + pub(crate) signer: S, + pub(crate) address: Address, +} + +use ethers_providers::FromErr; + +impl FromErr for ClientError { + fn from(src: M::Error) -> ClientError { + ClientError::MiddlewareError(src) + } +} + +#[derive(Error, Debug)] +/// Error thrown when the client interacts with the blockchain +pub enum ClientError { + #[error("{0}")] + /// Thrown when the internal call to the signer fails + SignerError(S::Error), + + #[error("{0}")] + /// Thrown when an internal middleware errors + MiddlewareError(M::Error), + + /// Thrown if the `nonce` field is missing + #[error("no nonce was specified")] + NonceMissing, + /// Thrown if the `gas_price` field is missing + #[error("no gas price was specified")] + GasPriceMissing, + /// Thrown if the `gas` field is missing + #[error("no gas was specified")] + GasMissing, +} + +// Helper functions for locally signing transactions +impl Client +where + M: Middleware, + S: Signer, +{ + /// Creates a new client from the provider and signer. + pub fn new(inner: M, signer: S) -> Self { + let address = signer.address(); + Client { + inner, + signer, + address, + } + } + + async fn sign_transaction( + &self, + tx: TransactionRequest, + ) -> Result> { + // The nonce, gas and gasprice fields must already be populated + let nonce = tx.nonce.ok_or(ClientError::NonceMissing)?; + let gas_price = tx.gas_price.ok_or(ClientError::GasPriceMissing)?; + let gas = tx.gas.ok_or(ClientError::GasMissing)?; + + let signature = self + .signer + .sign_transaction(&tx) + .await + .map_err(ClientError::SignerError)?; + + // Get the actual transaction hash + let rlp = tx.rlp_signed(&signature); + let hash = keccak256(&rlp.0); + + // This function should not be called with ENS names + let to = tx.to.map(|to| match to { + NameOrAddress::Address(inner) => inner, + NameOrAddress::Name(_) => { + panic!("Expected `to` to be an Ethereum Address, not an ENS name") + } + }); + + Ok(Transaction { + hash: hash.into(), + nonce, + from: self.address(), + to, + value: tx.value.unwrap_or_default(), + gas_price, + gas, + input: tx.data.unwrap_or_default(), + v: signature.v.into(), + r: U256::from_big_endian(signature.r.as_bytes()), + s: U256::from_big_endian(signature.s.as_bytes()), + + // Leave these empty as they're only used for included transactions + block_hash: None, + block_number: None, + transaction_index: None, + + // Celo support + #[cfg(feature = "celo")] + fee_currency: tx.fee_currency, + #[cfg(feature = "celo")] + gateway_fee: tx.gateway_fee, + #[cfg(feature = "celo")] + gateway_fee_recipient: tx.gateway_fee_recipient, + }) + } + + async fn fill_transaction( + &self, + tx: &mut TransactionRequest, + block: Option, + ) -> Result<(), ClientError> { + // set the `from` field + if tx.from.is_none() { + tx.from = Some(self.address()); + } + + // will poll and await the futures concurrently + let (gas_price, gas, nonce) = join!( + maybe(tx.gas_price, self.inner.get_gas_price()), + maybe(tx.gas, self.inner.estimate_gas(&tx)), + maybe( + tx.nonce, + self.inner.get_transaction_count(self.address(), block) + ), + ); + tx.gas_price = Some(gas_price.map_err(ClientError::MiddlewareError)?); + tx.gas = Some(gas.map_err(ClientError::MiddlewareError)?); + tx.nonce = Some(nonce.map_err(ClientError::MiddlewareError)?); + + Ok(()) + } + + /// Returns the client's address + pub fn address(&self) -> Address { + self.address + } + + /// Returns a reference to the client's signer + pub fn signer(&self) -> &S { + &self.signer + } + + pub fn with_signer(&self, signer: S) -> Self + where + S: Clone, + M: Clone, + { + let mut this = self.clone(); + this.address = signer.address(); + this.signer = signer; + this + } +} + +#[async_trait(?Send)] +impl Middleware for Client +where + M: Middleware, + S: Signer, +{ + type Error = ClientError; + type Provider = M::Provider; + type Inner = M; + + fn inner(&self) -> &M { + &self.inner + } + + /// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that + /// gas cost and nonce calculations take it into account. For simple transactions this can be + /// left to `None`. + async fn send_transaction( + &self, + mut tx: TransactionRequest, + block: Option, + ) -> Result { + if let Some(ref to) = tx.to { + if let NameOrAddress::Name(ens_name) = to { + let addr = self + .inner + .resolve_name(&ens_name) + .await + .map_err(ClientError::MiddlewareError)?; + tx.to = Some(addr.into()) + } + } + + // fill any missing fields + self.fill_transaction(&mut tx, block).await?; + + // if we have a nonce manager set, we should try handling the result in + // case there was a nonce mismatch + let signed_tx = self.sign_transaction(tx).await?; + + // Submit the raw transaction + self.inner + .send_raw_transaction(&signed_tx) + .await + .map_err(ClientError::MiddlewareError) + } + + /// Signs a message with the internal signer, or if none is present it will make a call to + /// the connected node's `eth_call` API. + async fn sign + Send + Sync>( + &self, + data: T, + _: &Address, + ) -> Result { + Ok(self.signer.sign_message(data.into()).await.unwrap()) + } +} + +/// Calls the future if `item` is None, otherwise returns a `futures::ok` +async fn maybe(item: Option, f: F) -> Result +where + F: Future>, +{ + if let Some(item) = item { + ok(item).await + } else { + f.await + } +} + +#[cfg(all(test, not(feature = "celo")))] +mod tests { + use super::*; + use ethers::{providers::Provider, signers::Wallet}; + use rustc_hex::FromHex; + use std::convert::TryFrom; + + #[tokio::test] + async fn signs_tx() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let tx = TransactionRequest { + from: None, + to: Some( + "F0109fC8DF283027b6285cc889F5aA624EaC1F55" + .parse::
() + .unwrap() + .into(), + ), + value: Some(1_000_000_000.into()), + gas: Some(2_000_000.into()), + nonce: Some(0.into()), + gas_price: Some(21_000_000_000u128.into()), + data: None, + }; + let chain_id = 1u64; + + let provider = Provider::try_from("http://localhost:8545").unwrap(); + let key = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse::() + .unwrap() + .set_chain_id(chain_id); + let client = Client::new(provider, key); + + let tx = client.sign_transaction(tx).await.unwrap(); + + assert_eq!( + tx.hash, + "de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593" + .parse() + .unwrap() + ); + + let expected_rlp = Bytes("f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68".from_hex().unwrap()); + assert_eq!(tx.rlp(), expected_rlp); + } +} diff --git a/ethers-providers/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs similarity index 100% rename from ethers-providers/src/gas_oracle/eth_gas_station.rs rename to ethers-middleware/src/gas_oracle/eth_gas_station.rs diff --git a/ethers-providers/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs similarity index 100% rename from ethers-providers/src/gas_oracle/etherchain.rs rename to ethers-middleware/src/gas_oracle/etherchain.rs diff --git a/ethers-providers/src/gas_oracle/etherscan.rs b/ethers-middleware/src/gas_oracle/etherscan.rs similarity index 100% rename from ethers-providers/src/gas_oracle/etherscan.rs rename to ethers-middleware/src/gas_oracle/etherscan.rs diff --git a/ethers-providers/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs similarity index 100% rename from ethers-providers/src/gas_oracle/gas_now.rs rename to ethers-middleware/src/gas_oracle/gas_now.rs diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs new file mode 100644 index 00000000..c1b2aeaf --- /dev/null +++ b/ethers-middleware/src/gas_oracle/middleware.rs @@ -0,0 +1,71 @@ +use super::{GasOracle, GasOracleError}; +use async_trait::async_trait; +use ethers_core::types::*; +use ethers_providers::{FromErr, Middleware}; +use thiserror::Error; + +#[derive(Debug)] +pub struct GasOracleMiddleware { + inner: M, + gas_oracle: G, +} + +impl GasOracleMiddleware +where + M: Middleware, + G: GasOracle, +{ + pub fn new(inner: M, gas_oracle: G) -> Self { + Self { inner, gas_oracle } + } +} + +#[derive(Error, Debug)] +pub enum MiddlewareError { + #[error(transparent)] + GasOracleError(#[from] GasOracleError), + + #[error("{0}")] + MiddlewareError(M::Error), +} + +impl FromErr for MiddlewareError { + fn from(src: M::Error) -> MiddlewareError { + MiddlewareError::MiddlewareError(src) + } +} + +#[async_trait(?Send)] +impl Middleware for GasOracleMiddleware +where + M: Middleware, + G: GasOracle, +{ + type Error = MiddlewareError; + type Provider = M::Provider; + type Inner = M; + + // OVERRIDEN METHODS + + fn inner(&self) -> &M { + &self.inner + } + + async fn get_gas_price(&self) -> Result { + Ok(self.gas_oracle.fetch().await?) + } + + async fn send_transaction( + &self, + mut tx: TransactionRequest, + block: Option, + ) -> Result { + if tx.gas_price.is_none() { + tx.gas_price = Some(self.get_gas_price().await?); + } + self.inner + .send_transaction(tx, block) + .await + .map_err(MiddlewareError::MiddlewareError) + } +} diff --git a/ethers-providers/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs similarity index 93% rename from ethers-providers/src/gas_oracle/mod.rs rename to ethers-middleware/src/gas_oracle/mod.rs index e40d4ef3..f6a94850 100644 --- a/ethers-providers/src/gas_oracle/mod.rs +++ b/ethers-middleware/src/gas_oracle/mod.rs @@ -10,6 +10,9 @@ pub use etherscan::Etherscan; mod gas_now; pub use gas_now::GasNow; +mod middleware; +pub use middleware::{GasOracleMiddleware, MiddlewareError}; + use ethers_core::types::U256; use async_trait::async_trait; @@ -46,7 +49,7 @@ pub enum GasOracleError { /// # Example /// /// ```no_run -/// use ethers::providers::{ +/// use ethers::middleware::{ /// gas_oracle::{EthGasStation, Etherscan, GasCategory, GasOracle}, /// }; /// @@ -66,7 +69,7 @@ pub trait GasOracle: Send + Sync + std::fmt::Debug { /// # Example /// /// ``` - /// use ethers::providers::{ + /// use ethers::middleware::{ /// gas_oracle::{Etherchain, GasCategory, GasOracle}, /// }; /// diff --git a/ethers-middleware/src/lib.rs b/ethers-middleware/src/lib.rs new file mode 100644 index 00000000..d37d6ed1 --- /dev/null +++ b/ethers-middleware/src/lib.rs @@ -0,0 +1,21 @@ +//! Ethers Middleware +//! +//! Ethers uses a middleware architecture. You start the middleware stack with +//! a [`Provider`], and wrap it with additional middleware functionalities that +//! you need. +//! +//! # Middlewares +//! +//! ## Gas Oracle +//! +//! ## Signer +//! +//! ## Nonce Manager +pub mod gas_oracle; +pub use gas_oracle::GasOracleMiddleware; + +pub mod client; +pub use client::Client; + +mod nonce_manager; +pub use nonce_manager::NonceManager; diff --git a/ethers-middleware/src/nonce_manager.rs b/ethers-middleware/src/nonce_manager.rs new file mode 100644 index 00000000..c2fb634f --- /dev/null +++ b/ethers-middleware/src/nonce_manager.rs @@ -0,0 +1,112 @@ +use async_trait::async_trait; +use ethers_core::types::*; +use ethers_providers::{FromErr, Middleware}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use thiserror::Error; + +#[derive(Debug)] +pub struct NonceManager { + pub inner: M, + pub initialized: AtomicBool, + pub nonce: AtomicU64, + pub address: Address, +} + +impl NonceManager +where + M: Middleware, +{ + /// Instantiates the nonce manager with a 0 nonce. + pub fn new(inner: M, address: Address) -> Self { + NonceManager { + initialized: false.into(), + nonce: 0.into(), + inner, + address, + } + } + + /// Returns the next nonce to be used + pub fn next(&self) -> U256 { + let nonce = self.nonce.fetch_add(1, Ordering::SeqCst); + nonce.into() + } + + async fn get_transaction_count_with_manager( + &self, + block: Option, + ) -> Result> { + // initialize the nonce the first time the manager is called + if !self.initialized.load(Ordering::SeqCst) { + let nonce = self + .inner + .get_transaction_count(self.address, block) + .await + .map_err(FromErr::from)?; + self.nonce.store(nonce.as_u64(), Ordering::SeqCst); + self.initialized.store(true, Ordering::SeqCst); + } + + Ok(self.next()) + } +} + +#[derive(Error, Debug)] +pub enum NonceManagerError { + #[error("{0}")] + MiddlewareError(M::Error), +} + +impl FromErr for NonceManagerError { + fn from(src: M::Error) -> Self { + NonceManagerError::MiddlewareError(src) + } +} + +#[async_trait(?Send)] +impl Middleware for NonceManager +where + M: Middleware, +{ + type Error = NonceManagerError; + type Provider = M::Provider; + type Inner = M; + + fn inner(&self) -> &M { + &self.inner + } + + /// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that + /// gas cost and nonce calculations take it into account. For simple transactions this can be + /// left to `None`. + async fn send_transaction( + &self, + mut tx: TransactionRequest, + block: Option, + ) -> Result { + if tx.nonce.is_none() { + tx.nonce = Some(self.get_transaction_count_with_manager(block).await?); + } + + let mut tx_clone = tx.clone(); + match self.inner.send_transaction(tx, block).await { + Ok(tx_hash) => Ok(tx_hash), + Err(err) => { + let nonce = self.get_transaction_count(self.address, block).await?; + if nonce != self.nonce.load(Ordering::SeqCst).into() { + // try re-submitting the transaction with the correct nonce if there + // was a nonce mismatch + self.nonce.store(nonce.as_u64(), Ordering::SeqCst); + tx_clone.nonce = Some(nonce); + self.inner + .send_transaction(tx_clone, block) + .await + .map_err(FromErr::from) + } else { + // propagate the error otherwise + Err(FromErr::from(err)) + } + } + } + } +} diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs new file mode 100644 index 00000000..238bd138 --- /dev/null +++ b/ethers-middleware/tests/gas_oracle.rs @@ -0,0 +1,74 @@ +use ethers_core::{types::*, utils::Ganache}; +use ethers_middleware::gas_oracle::{ + EthGasStation, Etherchain, Etherscan, GasCategory, GasNow, GasOracle, GasOracleMiddleware, +}; +use ethers_providers::{Http, Middleware, Provider}; +use std::convert::TryFrom; + +#[tokio::test] +async fn using_gas_oracle() { + let ganache = Ganache::new().spawn(); + + let from = Address::from(ganache.keys()[0].clone()); + + // connect to the network + let provider = Provider::::try_from(ganache.endpoint()).unwrap(); + + // assign a gas oracle to use + let gas_oracle = Etherchain::new().category(GasCategory::Fastest); + let expected_gas_price = gas_oracle.fetch().await.unwrap(); + + let provider = GasOracleMiddleware::new(provider, gas_oracle); + + // broadcast a transaction + let tx = TransactionRequest::new() + .from(from) + .to(Address::zero()) + .value(10000); + let tx_hash = provider.send_transaction(tx, None).await.unwrap(); + + let tx = provider.get_transaction(tx_hash).await.unwrap().unwrap(); + assert_eq!(tx.gas_price, expected_gas_price); +} + +#[tokio::test] +async fn eth_gas_station() { + // initialize and fetch gas estimates from EthGasStation + let eth_gas_station_oracle = EthGasStation::new(None); + let data = eth_gas_station_oracle.fetch().await; + assert!(data.is_ok()); +} + +#[tokio::test] +async fn etherscan() { + let api_key = std::env::var("ETHERSCAN_API_KEY").unwrap(); + let api_key = Some(api_key.as_str()); + + // initialize and fetch gas estimates from Etherscan + // since etherscan does not support `fastest` category, we expect an error + let etherscan_oracle = Etherscan::new(api_key).category(GasCategory::Fastest); + let data = etherscan_oracle.fetch().await; + assert!(data.is_err()); + + // but fetching the `standard` gas price should work fine + let etherscan_oracle_2 = Etherscan::new(api_key).category(GasCategory::SafeLow); + + let data = etherscan_oracle_2.fetch().await; + assert!(data.is_ok()); +} + +#[tokio::test] +async fn etherchain() { + // initialize and fetch gas estimates from Etherchain + let etherchain_oracle = Etherchain::new().category(GasCategory::Fast); + let data = etherchain_oracle.fetch().await; + assert!(data.is_ok()); +} + +#[tokio::test] +async fn gas_now() { + // initialize and fetch gas estimates from SparkPool + let gas_now_oracle = GasNow::new().category(GasCategory::Fastest); + let data = gas_now_oracle.fetch().await; + assert!(data.is_ok()); +} diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs new file mode 100644 index 00000000..b437c286 --- /dev/null +++ b/ethers-middleware/tests/nonce_manager.rs @@ -0,0 +1,56 @@ +#[tokio::test] +#[cfg(not(feature = "celo"))] +async fn nonce_manager() { + use ethers_core::types::*; + use ethers_middleware::{Client, NonceManager}; + use ethers_providers::{Http, Middleware, Provider}; + use ethers_signers::Wallet; + use std::convert::TryFrom; + use std::time::Duration; + + let provider = + Provider::::try_from("https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778") + .unwrap() + .interval(Duration::from_millis(2000u64)); + + let wallet = "59c37cb6b16fa2de30675f034c8008f890f4b2696c729d6267946d29736d73e4" + .parse::() + .unwrap(); + let address = wallet.address(); + + let provider = Client::new(provider, wallet); + + // the nonce manager must be over the Client so that it overrides the nonce + // before the client gets it + let provider = NonceManager::new(provider, address); + + let nonce = provider + .get_transaction_count(address, Some(BlockNumber::Pending)) + .await + .unwrap() + .as_u64(); + + let mut tx_hashes = Vec::new(); + for _ in 0..10 { + let tx = provider + .send_transaction(TransactionRequest::pay(address, 100u64), None) + .await + .unwrap(); + tx_hashes.push(tx); + } + + let mut nonces = Vec::new(); + for tx_hash in tx_hashes { + nonces.push( + provider + .get_transaction(tx_hash) + .await + .unwrap() + .unwrap() + .nonce + .as_u64(), + ); + } + + assert_eq!(nonces, (nonce..nonce + 10).collect::>()) +} diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs new file mode 100644 index 00000000..de28831d --- /dev/null +++ b/ethers-middleware/tests/signer.rs @@ -0,0 +1,69 @@ +use ethers_providers::{Http, Middleware, Provider}; + +use ethers_core::types::TransactionRequest; +use ethers_middleware::Client; +use ethers_signers::Wallet; +use std::{convert::TryFrom, time::Duration}; + +#[tokio::test] +#[cfg(not(feature = "celo"))] +async fn send_eth() { + use ethers_core::utils::Ganache; + + let ganache = Ganache::new().spawn(); + + // this private key belongs to the above mnemonic + let wallet: Wallet = ganache.keys()[0].clone().into(); + let wallet2: Wallet = ganache.keys()[1].clone().into(); + + // connect to the network + let provider = Provider::::try_from(ganache.endpoint()) + .unwrap() + .interval(Duration::from_millis(10u64)); + let provider = Client::new(provider, wallet); + + // craft the transaction + let tx = TransactionRequest::new().to(wallet2.address()).value(10000); + + let balance_before = provider + .get_balance(provider.address(), None) + .await + .unwrap(); + + // send it! + provider.send_transaction(tx, None).await.unwrap(); + + let balance_after = provider + .get_balance(provider.address(), None) + .await + .unwrap(); + + assert!(balance_before > balance_after); +} + +#[tokio::test] +#[cfg(feature = "celo")] +async fn test_send_transaction() { + // Celo testnet + let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") + .unwrap() + .interval(Duration::from_millis(3000u64)); + + // Funded with https://celo.org/developers/faucet + // Please do not drain this account :) + let wallet = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" + .parse::() + .unwrap(); + let client = Client::new(provider, wallet); + + let balance_before = client.get_balance(client.address(), None).await.unwrap(); + let tx = TransactionRequest::pay(client.address(), 100); + let tx_hash = client.send_transaction(tx, None).await.unwrap(); + let _receipt = client + .pending_transaction(tx_hash) + .confirmations(3) + .await + .unwrap(); + let balance_after = client.get_balance(client.address(), None).await.unwrap(); + assert!(balance_before > balance_after); +} diff --git a/ethers-middleware/tests/stack.rs b/ethers-middleware/tests/stack.rs new file mode 100644 index 00000000..02ae6b24 --- /dev/null +++ b/ethers-middleware/tests/stack.rs @@ -0,0 +1,52 @@ +#[tokio::test] +#[cfg(not(feature = "celo"))] +async fn can_stack_middlewares() { + use ethers_core::{types::TransactionRequest, utils::Ganache}; + use ethers_middleware::{ + gas_oracle::{GasCategory, GasNow}, + Client, GasOracleMiddleware, NonceManager, + }; + use ethers_providers::{Http, Middleware, Provider}; + use ethers_signers::Wallet; + use std::convert::TryFrom; + + let ganache = Ganache::new().block_time(5u64).spawn(); + let gas_oracle = GasNow::new().category(GasCategory::SafeLow); + let signer: Wallet = ganache.keys()[0].clone().into(); + let address = signer.address(); + + // the base provider + let provider = Provider::::try_from(ganache.endpoint()).unwrap(); + let provider_clone = provider.clone(); + + // 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 + let provider = Client::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 = NonceManager::new(provider, address); + + let tx = TransactionRequest::new(); + let mut tx_hash = None; + for _ in 0..10 { + tx_hash = Some(provider.send_transaction(tx.clone(), None).await.unwrap()); + dbg!( + provider + .get_transaction(tx_hash.unwrap()) + .await + .unwrap() + .unwrap() + .gas_price + ); + } + + let receipt = provider_clone + .pending_transaction(tx_hash.unwrap()) + .await + .unwrap(); + + dbg!(receipt); +} diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index 48c05f17..84def62c 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -11,7 +11,7 @@ //! # Examples //! //! ```no_run -//! use ethers::providers::{Provider, Http}; +//! use ethers::providers::{Provider, Http, Middleware}; //! use std::convert::TryFrom; //! //! # async fn foo() -> Result<(), Box> { @@ -83,7 +83,7 @@ //! to addresses (and vice versa). The default ENS address is [mainnet](https://etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e) and can be overriden by calling the [`ens`](method@crate::Provider::ens) method on the provider. //! //! ```no_run -//! # use ethers::providers::{Provider, Http}; +//! # use ethers::providers::{Provider, Http, Middleware}; //! # use std::convert::TryFrom; //! # async fn foo() -> Result<(), Box> { //! # let provider = Provider::::try_from( @@ -107,8 +107,6 @@ mod provider; // ENS support mod ens; -pub mod gas_oracle; - mod pending_transaction; pub use pending_transaction::PendingTransaction; @@ -120,16 +118,15 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::{error::Error, fmt::Debug, future::Future, pin::Pin}; -pub use provider::{Provider, ProviderError}; +pub use provider::{FilterKind, Provider, ProviderError}; // Helper type alias -pub(crate) type PinBoxFut<'a, T> = - Pin> + 'a + Send>>; +pub(crate) type PinBoxFut<'a, T> = Pin> + 'a>>; #[async_trait] /// Trait which must be implemented by data transports to be used with the Ethereum /// JSON-RPC provider. -pub trait JsonRpcClient: Send + Sync { +pub trait JsonRpcClient: Debug + Send + Sync { /// A JSON-RPC Error type Error: Error + Into; @@ -139,3 +136,226 @@ pub trait JsonRpcClient: Send + Sync { T: Debug + Serialize + Send + Sync, R: for<'a> Deserialize<'a>; } + +use ethers_core::types::*; + +pub trait FromErr { + fn from(src: T) -> Self; +} + +#[async_trait(?Send)] +pub trait Middleware: Sync + Send + Debug { + type Error: Error + FromErr<::Error>; + type Provider: JsonRpcClient; + type Inner: Middleware; + + fn inner(&self) -> &Self::Inner; + + async fn get_block_number(&self) -> Result { + self.inner().get_block_number().await.map_err(FromErr::from) + } + + async fn send_transaction( + &self, + tx: TransactionRequest, + block: Option, + ) -> Result { + self.inner() + .send_transaction(tx, block) + .await + .map_err(FromErr::from) + } + + async fn resolve_name(&self, ens_name: &str) -> Result { + self.inner() + .resolve_name(ens_name) + .await + .map_err(FromErr::from) + } + + async fn lookup_address(&self, address: Address) -> Result { + self.inner() + .lookup_address(address) + .await + .map_err(FromErr::from) + } + + async fn get_block + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result>, Self::Error> { + self.inner() + .get_block(block_hash_or_number) + .await + .map_err(FromErr::from) + } + + async fn get_block_with_txs + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result>, Self::Error> { + self.inner() + .get_block_with_txs(block_hash_or_number) + .await + .map_err(FromErr::from) + } + + async fn get_transaction_count + Send + Sync>( + &self, + from: T, + block: Option, + ) -> Result { + self.inner() + .get_transaction_count(from, block) + .await + .map_err(FromErr::from) + } + + async fn estimate_gas(&self, tx: &TransactionRequest) -> Result { + self.inner().estimate_gas(tx).await.map_err(FromErr::from) + } + + async fn call( + &self, + tx: &TransactionRequest, + block: Option, + ) -> Result { + self.inner().call(tx, block).await.map_err(FromErr::from) + } + + async fn get_chainid(&self) -> Result { + self.inner().get_chainid().await.map_err(FromErr::from) + } + + async fn get_balance + Send + Sync>( + &self, + from: T, + block: Option, + ) -> Result { + self.inner() + .get_balance(from, block) + .await + .map_err(FromErr::from) + } + + async fn get_transaction>( + &self, + transaction_hash: T, + ) -> Result, Self::Error> { + self.inner() + .get_transaction(transaction_hash) + .await + .map_err(FromErr::from) + } + + async fn get_transaction_receipt>( + &self, + transaction_hash: T, + ) -> Result, Self::Error> { + self.inner() + .get_transaction_receipt(transaction_hash) + .await + .map_err(FromErr::from) + } + + async fn get_gas_price(&self) -> Result { + self.inner().get_gas_price().await.map_err(FromErr::from) + } + + async fn get_accounts(&self) -> Result, Self::Error> { + self.inner().get_accounts().await.map_err(FromErr::from) + } + + async fn send_raw_transaction(&self, tx: &Transaction) -> Result { + self.inner() + .send_raw_transaction(tx) + .await + .map_err(FromErr::from) + } + + async fn sign + Send + Sync>( + &self, + data: T, + from: &Address, + ) -> Result { + self.inner().sign(data, from).await.map_err(FromErr::from) + } + + ////// Contract state + + async fn get_logs(&self, filter: &Filter) -> Result, Self::Error> { + self.inner().get_logs(filter).await.map_err(FromErr::from) + } + + async fn new_filter(&self, filter: FilterKind<'_>) -> Result { + self.inner().new_filter(filter).await.map_err(FromErr::from) + } + + async fn uninstall_filter + Send + Sync>( + &self, + id: T, + ) -> Result { + self.inner() + .uninstall_filter(id) + .await + .map_err(FromErr::from) + } + + async fn watch<'a>( + &'a self, + filter: &Filter, + ) -> Result, Self::Error> { + self.inner().watch(filter).await.map_err(FromErr::from) + } + + async fn watch_pending_transactions( + &self, + ) -> Result, Self::Error> { + self.inner() + .watch_pending_transactions() + .await + .map_err(FromErr::from) + } + + async fn get_filter_changes(&self, id: T) -> Result, Self::Error> + where + T: Into + Send + Sync, + R: for<'a> Deserialize<'a> + Send + Sync, + { + self.inner() + .get_filter_changes(id) + .await + .map_err(FromErr::from) + } + + async fn watch_blocks(&self) -> Result, Self::Error> { + self.inner().watch_blocks().await.map_err(FromErr::from) + } + + async fn get_code + Send + Sync>( + &self, + at: T, + block: Option, + ) -> Result { + self.inner() + .get_code(at, block) + .await + .map_err(FromErr::from) + } + + async fn get_storage_at + Send + Sync>( + &self, + from: T, + location: H256, + block: Option, + ) -> Result { + self.inner() + .get_storage_at(from, location, block) + .await + .map_err(FromErr::from) + } + + fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, Self::Provider> { + self.inner().pending_transaction(tx_hash) + } +} diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/pending_transaction.rs index 099f80f4..3b604898 100644 --- a/ethers-providers/src/pending_transaction.rs +++ b/ethers-providers/src/pending_transaction.rs @@ -1,3 +1,4 @@ +use crate::Middleware; use crate::{ stream::{interval, DEFAULT_POLL_INTERVAL}, JsonRpcClient, PinBoxFut, Provider, ProviderError, diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 326c1134..73177d0e 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -1,7 +1,7 @@ use crate::{ ens, stream::{FilterWatcher, DEFAULT_POLL_INTERVAL}, - Http as HttpProvider, JsonRpcClient, PendingTransaction, + FromErr, Http as HttpProvider, JsonRpcClient, PendingTransaction, }; use ethers_core::{ @@ -13,6 +13,8 @@ use ethers_core::{ utils, }; +use crate::Middleware; +use async_trait::async_trait; use serde::Deserialize; use thiserror::Error; use url::{ParseError, Url}; @@ -27,21 +29,28 @@ use std::{convert::TryFrom, fmt::Debug, time::Duration}; /// # Example /// /// ```no_run -/// use ethers::providers::{JsonRpcClient, Provider, Http}; +/// # async fn foo() -> Result<(), Box> { +/// use ethers::providers::{Middleware, Provider, Http}; /// use std::convert::TryFrom; /// /// let provider = Provider::::try_from( /// "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27" /// ).expect("could not instantiate HTTP Provider"); /// -/// # async fn foo(provider: &Provider

) -> Result<(), Box> { /// let block = provider.get_block(100u64).await?; /// println!("Got block: {}", serde_json::to_string(&block)?); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug)] -pub struct Provider

(P, Option

, Option); +// TODO: Convert to proper struct +pub struct Provider

(P, Option

, Option, Option
); + +impl FromErr for ProviderError { + fn from(src: ProviderError) -> Self { + src + } +} #[derive(Debug, Error)] /// An error thrown when making a call to the provider @@ -72,40 +81,12 @@ pub enum FilterKind<'a> { impl Provider

{ /// Instantiate a new provider with a backend. pub fn new(provider: P) -> Self { - Self(provider, None, None) + Self(provider, None, None, None) } - ////// Blockchain Status - // - // Functions for querying the state of the blockchain - - /// Gets the latest block number via the `eth_BlockNumber` API - pub async fn get_block_number(&self) -> Result { - Ok(self - .0 - .request("eth_blockNumber", ()) - .await - .map_err(Into::into)?) - } - - /// Gets the block at `block_hash_or_number` (transaction hashes only) - pub async fn get_block( - &self, - block_hash_or_number: impl Into, - ) -> Result>, ProviderError> { - Ok(self - .get_block_gen(block_hash_or_number.into(), false) - .await?) - } - - /// Gets the block at `block_hash_or_number` (full transactions included) - pub async fn get_block_with_txs( - &self, - block_hash_or_number: impl Into, - ) -> Result>, ProviderError> { - Ok(self - .get_block_gen(block_hash_or_number.into(), true) - .await?) + pub fn with_sender(mut self, address: impl Into

) -> Self { + self.3 = Some(address.into()); + self } async fn get_block_gen Deserialize<'a>>( @@ -132,9 +113,53 @@ impl Provider

{ } }) } +} + +#[async_trait(?Send)] +impl Middleware for Provider

{ + type Error = ProviderError; + type Provider = P; + type Inner = Self; + + fn inner(&self) -> &Self::Inner { + unreachable!("There is no inner provider here") + } + + ////// Blockchain Status + // + // Functions for querying the state of the blockchain + + /// Gets the latest block number via the `eth_BlockNumber` API + async fn get_block_number(&self) -> Result { + Ok(self + .0 + .request("eth_blockNumber", ()) + .await + .map_err(Into::into)?) + } + + /// Gets the block at `block_hash_or_number` (transaction hashes only) + async fn get_block + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result>, Self::Error> { + Ok(self + .get_block_gen(block_hash_or_number.into(), false) + .await?) + } + + /// Gets the block at `block_hash_or_number` (full transactions included) + async fn get_block_with_txs + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result>, ProviderError> { + Ok(self + .get_block_gen(block_hash_or_number.into(), true) + .await?) + } /// Gets the transaction with `transaction_hash` - pub async fn get_transaction>( + async fn get_transaction>( &self, transaction_hash: T, ) -> Result, ProviderError> { @@ -147,7 +172,7 @@ impl Provider

{ } /// Gets the transaction receipt with `transaction_hash` - pub async fn get_transaction_receipt>( + async fn get_transaction_receipt>( &self, transaction_hash: T, ) -> Result, ProviderError> { @@ -160,7 +185,7 @@ impl Provider

{ } /// Gets the current gas price as estimated by the node - pub async fn get_gas_price(&self) -> Result { + async fn get_gas_price(&self) -> Result { Ok(self .0 .request("eth_gasPrice", ()) @@ -169,7 +194,7 @@ impl Provider

{ } /// Gets the accounts on the node - pub async fn get_accounts(&self) -> Result, ProviderError> { + async fn get_accounts(&self) -> Result, ProviderError> { Ok(self .0 .request("eth_accounts", ()) @@ -178,9 +203,9 @@ impl Provider

{ } /// Returns the nonce of the address - pub async fn get_transaction_count( + async fn get_transaction_count + Send + Sync>( &self, - from: impl Into, + from: T, block: Option, ) -> Result { let from = match from.into() { @@ -198,9 +223,9 @@ impl Provider

{ } /// Returns the account's balance - pub async fn get_balance( + async fn get_balance + Send + Sync>( &self, - from: impl Into, + from: T, block: Option, ) -> Result { let from = match from.into() { @@ -219,7 +244,7 @@ impl Provider

{ /// Returns the currently configured chain id, a value used in replay-protected /// transaction signing as introduced by EIP-155. - pub async fn get_chainid(&self) -> Result { + async fn get_chainid(&self) -> Result { Ok(self .0 .request("eth_chainId", ()) @@ -233,7 +258,7 @@ impl Provider

{ /// Sends the read-only (constant) transaction to a single Ethereum node and return the result (as bytes) of executing it. /// This is free, since it does not change any state on the blockchain. - pub async fn call( + async fn call( &self, tx: &TransactionRequest, block: Option, @@ -250,7 +275,7 @@ impl Provider

{ /// Sends a transaction to a single Ethereum node and return the estimated amount of gas required (as a U256) to send it /// This is free, but only an estimate. Providing too little gas will result in a transaction being rejected /// (while still consuming all provided gas). - pub async fn estimate_gas(&self, tx: &TransactionRequest) -> Result { + async fn estimate_gas(&self, tx: &TransactionRequest) -> Result { let tx = utils::serialize(tx); Ok(self @@ -262,10 +287,19 @@ impl Provider

{ /// Sends the transaction to the entire Ethereum network and returns the transaction's hash /// This will consume gas from the account that signed the transaction. - pub async fn send_transaction( + async fn send_transaction( &self, mut tx: TransactionRequest, + _: Option, ) -> Result { + if tx.from.is_none() { + tx.from = self.3; + } + + if tx.gas.is_none() { + tx.gas = Some(self.estimate_gas(&tx).await?); + } + if let Some(ref to) = tx.to { if let NameOrAddress::Name(ens_name) = to { // resolve to an address @@ -285,7 +319,7 @@ impl Provider

{ /// Send the raw RLP encoded transaction to the entire Ethereum network and returns the transaction's hash /// This will consume gas from the account that signed the transaction. - pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result { + async fn send_raw_transaction(&self, tx: &Transaction) -> Result { let rlp = utils::serialize(&tx.rlp()); Ok(self .0 @@ -295,7 +329,7 @@ impl Provider

{ } /// Signs data using a specific account. This account needs to be unlocked. - pub async fn sign>( + async fn sign + Send + Sync>( &self, data: T, from: &Address, @@ -312,7 +346,7 @@ impl Provider

{ ////// Contract state /// Returns an array (possibly empty) of logs that match the filter - pub async fn get_logs(&self, filter: &Filter) -> Result, ProviderError> { + async fn get_logs(&self, filter: &Filter) -> Result, ProviderError> { Ok(self .0 .request("eth_getLogs", [filter]) @@ -321,22 +355,24 @@ impl Provider

{ } /// Streams matching filter logs - pub async fn watch(&self, filter: &Filter) -> Result, ProviderError> { + async fn watch<'a>( + &'a self, + filter: &Filter, + ) -> Result, ProviderError> { let id = self.new_filter(FilterKind::Logs(filter)).await?; let filter = FilterWatcher::new(id, self).interval(self.get_interval()); - Ok(filter) } /// Streams new block hashes - pub async fn watch_blocks(&self) -> Result, ProviderError> { + async fn watch_blocks(&self) -> Result, ProviderError> { let id = self.new_filter(FilterKind::NewBlocks).await?; let filter = FilterWatcher::new(id, self).interval(self.get_interval()); Ok(filter) } /// Streams pending transactions - pub async fn watch_pending_transactions( + async fn watch_pending_transactions( &self, ) -> Result, ProviderError> { let id = self.new_filter(FilterKind::PendingTransactions).await?; @@ -346,7 +382,7 @@ impl Provider

{ /// Creates a filter object, based on filter options, to notify when the state changes (logs). /// To check if the state has changed, call `get_filter_changes` with the filter id. - pub async fn new_filter(&self, filter: FilterKind<'_>) -> Result { + async fn new_filter(&self, filter: FilterKind<'_>) -> Result { let (method, args) = match filter { FilterKind::NewBlocks => ("eth_newBlockFilter", vec![]), FilterKind::PendingTransactions => ("eth_newPendingTransactionFilter", vec![]), @@ -357,7 +393,10 @@ impl Provider

{ } /// Uninstalls a filter - pub async fn uninstall_filter>(&self, id: T) -> Result { + async fn uninstall_filter + Send + Sync>( + &self, + id: T, + ) -> Result { let id = utils::serialize(&id.into()); Ok(self .0 @@ -379,10 +418,10 @@ impl Provider

{ /// /// [`H256`]: ethers_core::types::H256 /// [`Log`]: ethers_core::types::Log - pub async fn get_filter_changes(&self, id: T) -> Result, ProviderError> + async fn get_filter_changes(&self, id: T) -> Result, ProviderError> where - T: Into, - R: for<'a> Deserialize<'a>, + T: Into + Send + Sync, + R: for<'a> Deserialize<'a> + Send + Sync, { let id = utils::serialize(&id.into()); Ok(self @@ -393,9 +432,9 @@ impl Provider

{ } /// Get the storage of an address for a particular slot location - pub async fn get_storage_at( + async fn get_storage_at + Send + Sync>( &self, - from: impl Into, + from: T, location: H256, block: Option, ) -> Result { @@ -415,9 +454,9 @@ impl Provider

{ } /// Returns the deployed code at a given address - pub async fn get_code( + async fn get_code + Send + Sync>( &self, - at: impl Into, + at: T, block: Option, ) -> Result { let at = match at.into() { @@ -447,7 +486,7 @@ impl Provider

{ /// /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as /// an address. This should theoretically never happen. - pub async fn resolve_name(&self, ens_name: &str) -> Result { + async fn resolve_name(&self, ens_name: &str) -> Result { self.query_resolver(ParamType::Address, ens_name, ens::ADDR_SELECTOR) .await } @@ -457,12 +496,20 @@ impl Provider

{ /// /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as /// a string. This should theoretically never happen. - pub async fn lookup_address(&self, address: Address) -> Result { + async fn lookup_address(&self, address: Address) -> Result { let ens_name = ens::reverse_address(address); self.query_resolver(ParamType::String, &ens_name, ens::NAME_SELECTOR) .await } + /// Helper which creates a pending transaction object from a transaction hash + /// using the provider's polling interval + fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> { + PendingTransaction::new(tx_hash, self).interval(self.get_interval()) + } +} + +impl Provider

{ async fn query_resolver( &self, param: ParamType, @@ -521,12 +568,6 @@ impl Provider

{ pub fn get_interval(&self) -> Duration { self.2.unwrap_or(DEFAULT_POLL_INTERVAL) } - - /// Helper which creates a pending transaction object from a transaction hash - /// using the provider's polling interval - pub fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> { - PendingTransaction::new(tx_hash, self).interval(self.get_interval()) - } } /// infallbile conversion of Bytes to Address/String @@ -544,7 +585,12 @@ impl TryFrom<&str> for Provider { type Error = ParseError; fn try_from(src: &str) -> Result { - Ok(Provider(HttpProvider::new(Url::parse(src)?), None, None)) + Ok(Provider( + HttpProvider::new(Url::parse(src)?), + None, + None, + None, + )) } } @@ -663,7 +709,7 @@ mod tests { .value(1e18 as u64); for _ in 0..num_txs { - tx_hashes.push(provider.send_transaction(tx.clone()).await.unwrap()); + tx_hashes.push(provider.send_transaction(tx.clone(), None).await.unwrap()); } let hashes: Vec = stream.take(num_txs).collect::>().await; @@ -681,7 +727,7 @@ mod tests { let accounts = provider.get_accounts().await.unwrap(); let tx = TransactionRequest::pay(accounts[0], parse_ether(1u64).unwrap()).from(accounts[0]); - let tx_hash = provider.send_transaction(tx).await.unwrap(); + let tx_hash = provider.send_transaction(tx, None).await.unwrap(); assert!(provider .get_transaction_receipt(tx_hash) diff --git a/ethers-providers/src/stream.rs b/ethers-providers/src/stream.rs index a953255c..c038a195 100644 --- a/ethers-providers/src/stream.rs +++ b/ethers-providers/src/stream.rs @@ -1,4 +1,4 @@ -use crate::{JsonRpcClient, PinBoxFut, Provider}; +use crate::{JsonRpcClient, Middleware, PinBoxFut, Provider}; use ethers_core::types::U256; @@ -45,7 +45,7 @@ pub struct FilterWatcher<'a, P, R> { impl<'a, P, R> FilterWatcher<'a, P, R> where P: JsonRpcClient, - R: for<'de> Deserialize<'de>, + R: Send + Sync + for<'de> Deserialize<'de>, { /// Creates a new watcher with the provided factory and filter id. pub fn new>(id: T, provider: &'a Provider

) -> Self { @@ -75,7 +75,7 @@ where impl<'a, P, R> Stream for FilterWatcher<'a, P, R> where P: JsonRpcClient, - R: for<'de> Deserialize<'de> + 'a, + R: Send + Sync + for<'de> Deserialize<'de> + 'a, { type Item = R; diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs index 15d938bc..25362045 100644 --- a/ethers-providers/src/transports/ws.rs +++ b/ethers-providers/src/transports/ws.rs @@ -8,7 +8,7 @@ use futures_util::{ stream::{Stream, StreamExt}, }; use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +use std::fmt::{self, Debug}; use std::sync::atomic::{AtomicU64, Ordering}; use thiserror::Error; @@ -95,6 +95,15 @@ pub struct Provider { ws: Mutex, } +impl Debug for Provider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WebsocketProvider") + .field("id", &self.id) + .field("ws", &stringify!(ws)) + .finish() + } +} + #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] impl Provider> { /// Initializes a new WebSocket Client. diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs index bb63ad5c..cd3f7005 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/provider.rs @@ -1,15 +1,11 @@ #![allow(unused_braces)] -use ethers::providers::{ - gas_oracle::{EthGasStation, Etherchain, Etherscan, GasCategory, GasNow, GasOracle}, - Http, Provider, -}; +use ethers::providers::{Http, Middleware, Provider}; use std::{convert::TryFrom, time::Duration}; #[cfg(not(feature = "celo"))] mod eth_tests { use super::*; use ethers::{ - providers::JsonRpcClient, types::{BlockId, TransactionRequest, H256}, utils::{parse_ether, Ganache}, }; @@ -105,44 +101,11 @@ mod eth_tests { generic_pending_txs_test(provider).await; } - #[tokio::test] - async fn gas_oracle() { - // initialize and fetch gas estimates from EthGasStation - let eth_gas_station_oracle = EthGasStation::new(None); - let data_1 = eth_gas_station_oracle.fetch().await; - assert!(data_1.is_ok()); - - let api_key = std::env::var("ETHERSCAN_API_KEY").unwrap(); - let api_key = Some(api_key.as_str()); - - // initialize and fetch gas estimates from Etherscan - // since etherscan does not support `fastest` category, we expect an error - let etherscan_oracle = Etherscan::new(api_key).category(GasCategory::Fastest); - let data_2 = etherscan_oracle.fetch().await; - assert!(data_2.is_err()); - - // but fetching the `standard` gas price should work fine - let etherscan_oracle_2 = Etherscan::new(api_key).category(GasCategory::SafeLow); - - let data_3 = etherscan_oracle_2.fetch().await; - assert!(data_3.is_ok()); - - // initialize and fetch gas estimates from Etherchain - let etherchain_oracle = Etherchain::new().category(GasCategory::Fast); - let data_4 = etherchain_oracle.fetch().await; - assert!(data_4.is_ok()); - - // initialize and fetch gas estimates from SparkPool - let gas_now_oracle = GasNow::new().category(GasCategory::Fastest); - let data_5 = gas_now_oracle.fetch().await; - assert!(data_5.is_ok()); - } - - async fn generic_pending_txs_test(provider: Provider

) { + async fn generic_pending_txs_test(provider: M) { let accounts = provider.get_accounts().await.unwrap(); let tx = TransactionRequest::pay(accounts[0], parse_ether(1u64).unwrap()).from(accounts[0]); - let tx_hash = provider.send_transaction(tx).await.unwrap(); + let tx_hash = provider.send_transaction(tx, None).await.unwrap(); let pending_tx = provider.pending_transaction(tx_hash); let receipt = pending_tx.confirmations(5).await.unwrap(); diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 3400aa3c..53a572a1 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -15,7 +15,6 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] ethers-core = { version = "0.1.3", path = "../ethers-core" } -ethers-providers = { version = "0.1.3", path = "../ethers-providers" } thiserror = { version = "1.0.15", default-features = false } futures-util = { version = "0.3.5", default-features = false } serde = { version = "1.0.112", default-features = false } @@ -31,6 +30,5 @@ tokio = { version = "0.2.21", features = ["macros"] } serde_json = "1.0.55" [features] -celo = ["ethers-core/celo", "ethers-providers/celo"] +celo = ["ethers-core/celo"] ledger = ["coins-ledger", "rustc-hex"] -ledger-tests= ["ledger"] diff --git a/ethers-signers/src/client.rs b/ethers-signers/src/client.rs deleted file mode 100644 index e9f91369..00000000 --- a/ethers-signers/src/client.rs +++ /dev/null @@ -1,334 +0,0 @@ -use crate::{NonceManager, Signer}; - -use ethers_core::types::{ - Address, BlockNumber, Bytes, NameOrAddress, Signature, TransactionRequest, TxHash, U256, -}; -use ethers_providers::{ - gas_oracle::{GasOracle, GasOracleError}, - JsonRpcClient, Provider, ProviderError, -}; - -use futures_util::{future::ok, join}; -use std::{future::Future, ops::Deref, sync::atomic::Ordering, time::Duration}; - -use thiserror::Error; - -#[derive(Debug)] -/// A client provides an interface for signing and broadcasting locally signed transactions -/// It Derefs to [`Provider`], which allows interacting with the Ethereum JSON-RPC provider -/// via the same API. Sending transactions also supports using [ENS](https://ens.domains/) as a receiver. If you will -/// not be using a local signer, it is recommended to use a [`Provider`] instead. -/// -/// # Example -/// -/// ```no_run -/// use ethers_providers::{Provider, Http}; -/// use ethers_signers::{Client, ClientError, Wallet}; -/// use ethers_core::types::{Address, TransactionRequest}; -/// use std::convert::TryFrom; -/// -/// # async fn foo() -> Result<(), Box> { -/// let provider = Provider::::try_from("http://localhost:8545") -/// .expect("could not instantiate HTTP Provider"); -/// -/// // By default, signing of messages and transactions is done locally -/// // (transactions will be broadcast via the eth_sendRawTransaction API) -/// let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" -/// .parse()?; -/// -/// let mut client = Client::new(provider, wallet).await?; -/// -/// // since it derefs to `Provider`, we can just call any of the JSON-RPC API methods -/// let block = client.get_block(100u64).await?; -/// -/// // You can use the node's `eth_sign` and `eth_sendTransaction` calls by calling the -/// // internal provider's method. -/// let signed_msg = client.provider().sign(b"hello".to_vec(), &client.address()).await?; -/// -/// let tx = TransactionRequest::pay("vitalik.eth", 100); -/// let tx_hash = client.send_transaction(tx, None).await?; -/// -/// // You can `await` on the pending transaction to get the receipt with a pre-specified -/// // number of confirmations -/// let receipt = client.pending_transaction(tx_hash).confirmations(6).await?; -/// -/// // You can connect with other wallets at runtime via the `with_signer` function -/// let wallet2: Wallet = "cd8c407233c0560f6de24bb2dc60a8b02335c959a1a17f749ce6c1ccf63d74a7" -/// .parse()?; -/// -/// let signed_msg2 = client.with_signer(wallet2).sign_message(b"hello".to_vec()).await?; -/// -/// // This call will be made with `wallet2` since `with_signer` takes a mutable reference. -/// let tx2 = TransactionRequest::new() -/// .to("0xd8da6bf26964af9d7eed9e03e53415d37aa96045".parse::

()?) -/// .value(200); -/// let tx_hash2 = client.send_transaction(tx2, None).await?; -/// -/// # Ok(()) -/// # } -/// -/// ``` -/// -/// [`Provider`]: ethers_providers::Provider -pub struct Client { - pub(crate) provider: Provider

, - pub(crate) signer: Option, - pub(crate) address: Address, - pub(crate) gas_oracle: Option>, - pub(crate) nonce_manager: Option, -} - -#[derive(Debug, Error)] -/// Error thrown when the client interacts with the blockchain -pub enum ClientError { - #[error(transparent)] - /// Throw when the call to the provider fails - ProviderError(#[from] ProviderError), - - #[error(transparent)] - /// Throw when a call to the gas oracle fails - GasOracleError(#[from] GasOracleError), - - #[error(transparent)] - /// Thrown when the internal call to the signer fails - SignerError(#[from] Box), - - #[error("ens name not found: {0}")] - /// Thrown when an ENS name is not found - EnsError(String), -} - -// Helper functions for locally signing transactions -impl Client -where - P: JsonRpcClient, - S: Signer, -{ - /// Creates a new client from the provider and signer. - pub async fn new(provider: Provider

, signer: S) -> Result { - let address = signer.address().await.map_err(Into::into)?; - Ok(Client { - provider, - signer: Some(signer), - address, - gas_oracle: None, - nonce_manager: None, - }) - } - - /// Signs a message with the internal signer, or if none is present it will make a call to - /// the connected node's `eth_call` API. - pub async fn sign_message>(&self, msg: T) -> Result { - Ok(if let Some(ref signer) = self.signer { - signer.sign_message(msg.into()).await.map_err(Into::into)? - } else { - self.provider.sign(msg, &self.address()).await? - }) - } - - /// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that - /// gas cost and nonce calculations take it into account. For simple transactions this can be - /// left to `None`. - pub async fn send_transaction( - &self, - mut tx: TransactionRequest, - block: Option, - ) -> Result { - if let Some(ref to) = tx.to { - if let NameOrAddress::Name(ens_name) = to { - let addr = self.resolve_name(&ens_name).await?; - tx.to = Some(addr.into()) - } - } - - // fill any missing fields - self.fill_transaction(&mut tx, block).await?; - - // if we have a nonce manager set, we should try handling the result in - // case there was a nonce mismatch - let tx_hash = if let Some(ref nonce_manager) = self.nonce_manager { - let mut tx_clone = tx.clone(); - match self.submit_transaction(tx).await { - Ok(tx_hash) => tx_hash, - Err(err) => { - let nonce = self.get_transaction_count(block).await?; - if nonce != nonce_manager.nonce.load(Ordering::SeqCst).into() { - // try re-submitting the transaction with the correct nonce if there - // was a nonce mismatch - nonce_manager.nonce.store(nonce.as_u64(), Ordering::SeqCst); - tx_clone.nonce = Some(nonce); - self.submit_transaction(tx_clone).await? - } else { - // propagate the error otherwise - return Err(err); - } - } - } - } else { - self.submit_transaction(tx).await? - }; - - Ok(tx_hash) - } - - async fn submit_transaction(&self, tx: TransactionRequest) -> Result { - Ok(if let Some(ref signer) = self.signer { - let signed_tx = signer.sign_transaction(tx).await.map_err(Into::into)?; - self.provider.send_raw_transaction(&signed_tx).await? - } else { - self.provider.send_transaction(tx).await? - }) - } - - async fn fill_transaction( - &self, - tx: &mut TransactionRequest, - block: Option, - ) -> Result<(), ClientError> { - // set the `from` field - if tx.from.is_none() { - tx.from = Some(self.address()); - } - - // assign gas price if a gas oracle has been provided - if let Some(gas_oracle) = &self.gas_oracle { - if let Ok(gas_price) = gas_oracle.fetch().await { - tx.gas_price = Some(gas_price); - } - } - - // will poll and await the futures concurrently - let (gas_price, gas, nonce) = join!( - maybe(tx.gas_price, self.provider.get_gas_price()), - maybe(tx.gas, self.provider.estimate_gas(&tx)), - maybe(tx.nonce, self.get_transaction_count_with_manager(block)), - ); - tx.gas_price = Some(gas_price?); - tx.gas = Some(gas?); - tx.nonce = Some(nonce?); - - Ok(()) - } - - async fn get_transaction_count_with_manager( - &self, - block: Option, - ) -> Result { - // If there's a nonce manager set, short circuit by just returning the next nonce - if let Some(ref nonce_manager) = self.nonce_manager { - // initialize the nonce the first time the manager is called - if !nonce_manager.initialized.load(Ordering::SeqCst) { - let nonce = self - .provider - .get_transaction_count(self.address(), block) - .await?; - nonce_manager.nonce.store(nonce.as_u64(), Ordering::SeqCst); - nonce_manager.initialized.store(true, Ordering::SeqCst); - } - - return Ok(nonce_manager.next()); - } - - self.get_transaction_count(block).await - } - - pub async fn get_transaction_count( - &self, - block: Option, - ) -> Result { - Ok(self - .provider - .get_transaction_count(self.address(), block) - .await?) - } - - /// Returns the client's address - pub fn address(&self) -> Address { - self.address - } - - /// Returns a reference to the client's provider - pub fn provider(&self) -> &Provider

{ - &self.provider - } - - /// Returns a reference to the client's signer - pub fn signer(&self) -> Option<&S> { - self.signer.as_ref() - } - - /// Sets the signer and returns a mutable reference to self so that it can be used in chained - /// calls. - pub fn with_signer(&mut self, signer: S) -> &Self { - self.signer = Some(signer); - self - } - - /// Sets the provider and returns a mutable reference to self so that it can be used in chained - /// calls. - pub fn with_provider(&mut self, provider: Provider

) -> &Self { - self.provider = provider; - self - } - - /// Sets the address which will be used for interacting with the blockchain. - /// Useful if no signer is set and you want to specify a default sender for - /// your transactions or if you have changed the signer manually. - pub fn with_sender>(mut self, address: T) -> Self { - self.address = address.into(); - self - } - - /// Sets the default polling interval for event filters and pending transactions - pub fn interval>(mut self, interval: T) -> Self { - let provider = self.provider.interval(interval.into()); - self.provider = provider; - self - } - - /// Sets the gas oracle to query for gas estimates while broadcasting transactions - pub fn gas_oracle(mut self, gas_oracle: Box) -> Self { - self.gas_oracle = Some(gas_oracle); - self - } - - pub fn with_nonce_manager(mut self) -> Self { - self.nonce_manager = Some(NonceManager::new()); - self - } -} - -/// Calls the future if `item` is None, otherwise returns a `futures::ok` -async fn maybe(item: Option, f: F) -> Result -where - F: Future>, -{ - if let Some(item) = item { - ok(item).await - } else { - f.await - } -} - -// Abuse Deref to use the Provider's methods without re-writing everything. -// This is an anti-pattern and should not be encouraged, but this improves the UX while -// keeping the LoC low -impl Deref for Client { - type Target = Provider

; - - fn deref(&self) -> &Self::Target { - &self.provider - } -} - -impl From> for Client { - fn from(provider: Provider

) -> Self { - Self { - provider, - signer: None, - address: Address::zero(), - gas_oracle: None, - nonce_manager: None, - } - } -} diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index 9b7c85ad..e0875bb4 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -7,8 +7,7 @@ use futures_util::lock::Mutex; use ethers_core::{ types::{ - Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxError, TxHash, H256, - U256, + Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256, }, utils::keccak256, }; @@ -20,25 +19,40 @@ use super::types::*; /// A Ledger Ethereum App. /// /// This is a simple wrapper around the [Ledger transport](Ledger) +#[derive(Debug)] pub struct LedgerEthereum { transport: Mutex, derivation: DerivationType, pub chain_id: Option, + + /// The ledger's address, instantiated at runtime + pub address: Address, } impl LedgerEthereum { /// Instantiate the application by acquiring a lock on the ledger device. /// - /// # Notes /// + /// ``` + /// # async fn foo() -> Result<(), Box> { + /// use ethers::signers::{Ledger, HDPath}; + /// + /// let ledger = Ledger::new(HDPath::LedgerLive(0), Some(1)).await?; + /// # Ok(()) + /// # } + /// ``` pub async fn new( derivation: DerivationType, chain_id: Option, ) -> Result { + let transport = Ledger::init().await?; + let address = Self::get_address_with_path_transport(&transport, &derivation).await?; + Ok(Self { - transport: Mutex::new(Ledger::init().await?), + transport: Mutex::new(transport), derivation, chain_id, + address, }) } @@ -55,8 +69,16 @@ impl LedgerEthereum { &self, derivation: &DerivationType, ) -> Result { - let data = APDUData::new(&self.path_to_bytes(&derivation)); + let data = APDUData::new(&Self::path_to_bytes(&derivation)); let transport = self.transport.lock().await; + Self::get_address_with_path_transport(&transport, derivation).await + } + + async fn get_address_with_path_transport( + transport: &Ledger, + derivation: &DerivationType, + ) -> Result { + let data = APDUData::new(&Self::path_to_bytes(&derivation)); let command = APDUCommand { ins: INS::GET_PUBLIC_KEY as u8, @@ -98,59 +120,21 @@ impl LedgerEthereum { } /// Signs an Ethereum transaction (requires confirmation on the ledger) - // TODO: Remove code duplication between this and the PrivateKey::sign_transaction - // method pub async fn sign_tx( &self, - tx: TransactionRequest, + tx: &TransactionRequest, chain_id: Option, - ) -> Result { - // The nonce, gas and gasprice fields must already be populated - let nonce = tx.nonce.ok_or(TxError::NonceMissing)?; - let gas_price = tx.gas_price.ok_or(TxError::GasPriceMissing)?; - let gas = tx.gas.ok_or(TxError::GasMissing)?; - - let mut payload = self.path_to_bytes(&self.derivation); + ) -> Result { + let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(tx.rlp(chain_id).as_ref()); - let signature = self.sign_payload(INS::SIGN, payload).await?; - - // Get the actual transaction hash - let rlp = tx.rlp_signed(&signature); - let hash = keccak256(&rlp.0); - - // This function should not be called with ENS names - let to = tx.to.map(|to| match to { - NameOrAddress::Address(inner) => inner, - NameOrAddress::Name(_) => { - panic!("Expected `to` to be an Ethereum Address, not an ENS name") - } - }); - - Ok(Transaction { - hash: hash.into(), - nonce, - from: self.get_address().await?, - to, - value: tx.value.unwrap_or_default(), - gas_price, - gas, - input: tx.data.unwrap_or_default(), - v: signature.v.into(), - r: U256::from_big_endian(signature.r.as_bytes()), - s: U256::from_big_endian(signature.s.as_bytes()), - - // Leave these empty as they're only used for included transactions - block_hash: None, - block_number: None, - transaction_index: None, - }) + self.sign_payload(INS::SIGN, payload).await } /// Signs an ethereum personal message pub async fn sign_message>(&self, message: S) -> Result { let message = message.as_ref(); - let mut payload = self.path_to_bytes(&self.derivation); + let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); payload.extend_from_slice(message); @@ -197,7 +181,7 @@ impl LedgerEthereum { } // helper which converts a derivation path to bytes - fn path_to_bytes(&self, derivation: &DerivationType) -> Vec { + fn path_to_bytes(derivation: &DerivationType) -> Vec { let derivation = derivation.to_string(); let elements = derivation.split('/').skip(1).collect::>(); let depth = elements.len(); @@ -220,12 +204,13 @@ impl LedgerEthereum { #[cfg(all(test, feature = "ledger-tests"))] mod tests { use super::*; - use crate::{Client, Signer}; + use crate::Signer; use ethers::prelude::*; use rustc_hex::FromHex; use std::str::FromStr; #[tokio::test] + #[ignore] // Replace this with your ETH addresses. async fn test_get_address() { // Instantiate it with the default ledger derivation path @@ -246,6 +231,7 @@ mod tests { } #[tokio::test] + #[ignore] async fn test_sign_tx() { let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), None) .await @@ -262,46 +248,11 @@ mod tests { .nonce(5) .data(data) .value(ethers_core::utils::parse_ether(100).unwrap()); - let tx = ledger.sign_transaction(tx_req.clone()).await.unwrap(); - } - - #[tokio::test] - async fn test_send_transaction() { - let ledger = LedgerEthereum::new(DerivationType::Legacy(0), None) - .await - .unwrap(); - let addr = ledger.get_address().await.unwrap(); - let amt = ethers_core::utils::parse_ether(10).unwrap(); - let amt_with_gas = amt + U256::from_str("420000000000000").unwrap(); - - // fund our account - let ganache = ethers_core::utils::Ganache::new().spawn(); - let provider = Provider::::try_from(ganache.endpoint()).unwrap(); - let accounts = provider.get_accounts().await.unwrap(); - let req = TransactionRequest::new() - .from(accounts[0]) - .to(addr) - .value(amt_with_gas); - let tx = provider.send_transaction(req).await.unwrap(); - assert_eq!( - provider.get_balance(addr, None).await.unwrap(), - amt_with_gas - ); - - // send a tx and check that it works - let client = Client::new(provider, ledger).await.unwrap(); - let receiver = Address::zero(); - client - .send_transaction( - TransactionRequest::new().from(addr).to(receiver).value(amt), - None, - ) - .await - .unwrap(); - assert_eq!(client.get_balance(receiver, None).await.unwrap(), amt); + let tx = ledger.sign_transaction(&tx_req).await.unwrap(); } #[tokio::test] + #[ignore] async fn test_version() { let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), None) .await @@ -312,6 +263,7 @@ mod tests { } #[tokio::test] + #[ignore] async fn test_sign_message() { let ledger = LedgerEthereum::new(DerivationType::Legacy(0), None) .await diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index ba6d0b47..503527b3 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -1,10 +1,10 @@ pub mod app; pub mod types; -use crate::{ClientError, Signer}; +use crate::Signer; use app::LedgerEthereum; use async_trait::async_trait; -use ethers_core::types::{Address, Signature, Transaction, TransactionRequest}; +use ethers_core::types::{Address, Signature, TransactionRequest}; use types::LedgerError; #[async_trait(?Send)] @@ -22,19 +22,13 @@ impl Signer for LedgerEthereum { /// Signs the transaction async fn sign_transaction( &self, - message: TransactionRequest, - ) -> Result { + message: &TransactionRequest, + ) -> Result { self.sign_tx(message, self.chain_id).await } /// Returns the signer's Ethereum Address - async fn address(&self) -> Result { - self.get_address().await - } -} - -impl From for ClientError { - fn from(src: LedgerError) -> Self { - ClientError::SignerError(Box::new(src)) + fn address(&self) -> Address { + self.address } } diff --git a/ethers-signers/src/ledger/types.rs b/ethers-signers/src/ledger/types.rs index eba06171..09c0c4d4 100644 --- a/ethers-signers/src/ledger/types.rs +++ b/ethers-signers/src/ledger/types.rs @@ -2,6 +2,7 @@ //! [Official Docs](https://github.com/LedgerHQ/app-ethereum/blob/master/doc/ethapp.asc) use thiserror::Error; +#[derive(Clone, Debug)] pub enum DerivationType { LedgerLive(usize), Legacy(usize), @@ -33,9 +34,6 @@ pub enum LedgerError { #[error("Error when decoding UTF8 Response: {0}")] Utf8Error(#[from] std::str::Utf8Error), - #[error(transparent)] - TxError(#[from] ethers_core::types::TxError), - #[error(transparent)] SignatureError(#[from] ethers_core::types::SignatureError), } diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 5aceca3f..0a21f069 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -1,40 +1,36 @@ -//! Provides a unified interface for locally signing transactions and interacting -//! with the Ethereum JSON-RPC. You can implement the `Signer` trait to extend -//! functionality to other signers such as Hardware Security Modules, KMS etc. +//! Provides a unified interface for locally signing transactions. +//! +//! You can implement the `Signer` trait to extend functionality to other signers +//! such as Hardware Security Modules, KMS etc. +//! +//! The exposed interfaces return a recoverable signature. In order to convert the signature +//! and the [`TransactionRequest`] to a [`Transaction`], look at the signing middleware. +//! +//! Supported signers: +//! - Private key +//! - Ledger //! //! ```no_run //! # use ethers::{ -//! providers::{Http, Provider}, -//! signers::Wallet, +//! signers::{Wallet, Signer}, //! core::types::TransactionRequest //! }; -//! # use std::convert::TryFrom; //! # async fn foo() -> Result<(), Box> { -//! // connect to the network -//! let provider = Provider::::try_from("http://localhost:8545")?; -//! -//! // instantiate the wallet and connect it to the provider to get a client -//! let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" -//! .parse::()? -//! .connect(provider); +//! // instantiate the wallet +//! let wallet = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" +//! .parse::()?; //! //! // create a transaction //! let tx = TransactionRequest::new() //! .to("vitalik.eth") // this will use ENS //! .value(10000); //! -//! // send it! (this will resolve the ENS name to an address under the hood) -//! let tx_hash = client.send_transaction(tx, None).await?; -//! -//! // get the receipt -//! let receipt = client.pending_transaction(tx_hash).await?; -//! -//! // get the mined tx -//! let tx = client.get_transaction(receipt.transaction_hash).await?; -//! -//! println!("{}", serde_json::to_string(&tx)?); -//! println!("{}", serde_json::to_string(&receipt)?); +//! // sign it +//! let signature = wallet.sign_transaction(&tx).await?; //! +//! // can also sign a message +//! let signature = wallet.sign_message("hello world").await?; +//! signature.verify("hello world", wallet.address()).unwrap(); //! # Ok(()) //! # } mod wallet; @@ -48,23 +44,16 @@ pub use ledger::{ types::{DerivationType as HDPath, LedgerError}, }; -mod nonce_manager; -pub(crate) use nonce_manager::NonceManager; - -mod client; -pub use client::{Client, ClientError}; - use async_trait::async_trait; -use ethers_core::types::{Address, Signature, Transaction, TransactionRequest}; -use ethers_providers::Http; +use ethers_core::types::{Address, Signature, TransactionRequest}; use std::error::Error; /// Trait for signing transactions and messages /// /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. #[async_trait(?Send)] -pub trait Signer { - type Error: Error + Into; +pub trait Signer: Send + Sync + std::fmt::Debug { + type Error: Error + Send + Sync; /// Signs the hash of the provided message after prefixing it async fn sign_message>( &self, @@ -74,12 +63,9 @@ pub trait Signer { /// Signs the transaction async fn sign_transaction( &self, - message: TransactionRequest, - ) -> Result; + message: &TransactionRequest, + ) -> Result; /// Returns the signer's Ethereum Address - async fn address(&self) -> Result; + fn address(&self) -> Address; } - -/// An HTTP client configured to work with ANY blockchain without replay protection -pub type HttpClient = Client; diff --git a/ethers-signers/src/nonce_manager.rs b/ethers-signers/src/nonce_manager.rs deleted file mode 100644 index 1f005640..00000000 --- a/ethers-signers/src/nonce_manager.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ethers_core::types::U256; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; - -#[derive(Debug)] -pub(crate) struct NonceManager { - pub initialized: AtomicBool, - pub nonce: AtomicU64, -} - -impl NonceManager { - /// Instantiates the nonce manager with a 0 nonce. - pub fn new() -> Self { - NonceManager { - initialized: false.into(), - nonce: 0.into(), - } - } - - /// Returns the next nonce to be used - pub fn next(&self) -> U256 { - let nonce = self.nonce.fetch_add(1, Ordering::SeqCst); - nonce.into() - } -} diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs index c168d79b..1addf65a 100644 --- a/ethers-signers/src/wallet.rs +++ b/ethers-signers/src/wallet.rs @@ -1,19 +1,16 @@ -use crate::{Client, ClientError, Signer}; - -use ethers_providers::{JsonRpcClient, Provider}; +use crate::Signer; use ethers_core::{ rand::Rng, secp256k1, - types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError}, + types::{Address, PrivateKey, PublicKey, Signature, TransactionRequest}, }; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::str::FromStr; -/// An Ethereum private-public key pair which can be used for signing messages. It can be connected to a provider -/// via the [`connect`] method to produce a [`Client`]. +/// An Ethereum private-public key pair which can be used for signing messages. /// /// # Examples /// @@ -42,29 +39,6 @@ use std::str::FromStr; /// # } /// ``` /// -/// ## Connecting to a Provider -/// -/// The wallet can also be used to connect to a provider, which results in a [`Client`] -/// object. -/// -/// ``` -/// use ethers_core::rand::thread_rng; -/// use ethers_signers::Wallet; -/// use ethers_providers::{Provider, Http}; -/// use std::convert::TryFrom; -/// -/// // create a provider -/// let provider = Provider::::try_from("http://localhost:8545") -/// .expect("could not instantiate HTTP Provider"); -/// -/// // generate a wallet and connect to the provider -/// // (this is equivalent with calling `Client::new`) -/// let client = Wallet::new(&mut thread_rng()).connect(provider); -/// ``` -/// -/// -/// [`Client`]: crate::Client -/// [`connect`]: method@crate::Wallet::connect /// [`Signature`]: ethers_core::types::Signature /// [`hash_message`]: fn@ethers_core::utils::hash_message #[derive(Clone, Debug, Serialize, Deserialize)] @@ -81,27 +55,21 @@ pub struct Wallet { #[async_trait(?Send)] impl Signer for Wallet { - type Error = TxError; + type Error = std::convert::Infallible; async fn sign_message>( &self, message: S, - ) -> Result { + ) -> Result { Ok(self.private_key.sign(message)) } - async fn sign_transaction(&self, tx: TransactionRequest) -> Result { - self.private_key.sign_transaction(tx, self.chain_id) + async fn sign_transaction(&self, tx: &TransactionRequest) -> Result { + Ok(self.private_key.sign_transaction(tx, self.chain_id)) } - async fn address(&self) -> Result { - Ok(self.address) - } -} - -impl From for ClientError { - fn from(src: TxError) -> Self { - ClientError::SignerError(Box::new(src)) + fn address(&self) -> Address { + self.address } } @@ -122,18 +90,6 @@ impl Wallet { } } - /// Connects to a provider and returns a client - pub fn connect(self, provider: Provider

) -> Client { - let address = self.address(); - Client { - address, - signer: Some(self), - provider, - gas_oracle: None, - nonce_manager: None, - } - } - /// Sets the wallet's chain_id, used in conjunction with EIP-155 signing pub fn set_chain_id>(mut self, chain_id: T) -> Self { self.chain_id = Some(chain_id.into()); diff --git a/ethers-signers/tests/signer.rs b/ethers-signers/tests/signer.rs deleted file mode 100644 index 6f3f753f..00000000 --- a/ethers-signers/tests/signer.rs +++ /dev/null @@ -1,186 +0,0 @@ -use ethers::{ - providers::{ - gas_oracle::{Etherchain, GasCategory, GasOracle}, - Http, Provider, - }, - signers::Wallet, - types::TransactionRequest, -}; -use std::{convert::TryFrom, time::Duration}; - -#[cfg(not(feature = "celo"))] -mod eth_tests { - use super::*; - use ethers::{ - types::BlockNumber, - utils::{parse_ether, Ganache}, - }; - - #[tokio::test] - async fn pending_txs_with_confirmations_rinkeby_infura() { - let provider = Provider::::try_from( - "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", - ) - .unwrap() - .interval(Duration::from_millis(2000u64)); - - // pls do not drain this key :) - // note: this works even if there's no EIP-155 configured! - let client = "FF7F80C6E9941865266ED1F481263D780169F1D98269C51167D20C630A5FDC8A" - .parse::() - .unwrap() - .connect(provider); - - let tx = TransactionRequest::pay(client.address(), parse_ether(1u64).unwrap()); - let tx_hash = client - .send_transaction(tx, Some(BlockNumber::Pending)) - .await - .unwrap(); - let receipt = client - .pending_transaction(tx_hash) - .confirmations(3) - .await - .unwrap(); - - // got the correct receipt - assert_eq!(receipt.transaction_hash, tx_hash); - } - - #[tokio::test] - async fn send_eth() { - let ganache = Ganache::new().spawn(); - - // this private key belongs to the above mnemonic - let wallet: Wallet = ganache.keys()[0].clone().into(); - let wallet2: Wallet = ganache.keys()[1].clone().into(); - - // connect to the network - let provider = Provider::::try_from(ganache.endpoint()) - .unwrap() - .interval(Duration::from_millis(10u64)); - - // connect the wallet to the provider - let client = wallet.connect(provider); - - // craft the transaction - let tx = TransactionRequest::new().to(wallet2.address()).value(10000); - - let balance_before = client.get_balance(client.address(), None).await.unwrap(); - - // send it! - client.send_transaction(tx, None).await.unwrap(); - - let balance_after = client.get_balance(client.address(), None).await.unwrap(); - - assert!(balance_before > balance_after); - } - - #[tokio::test] - async fn nonce_manager() { - let provider = Provider::::try_from( - "https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778", - ) - .unwrap() - .interval(Duration::from_millis(2000u64)); - - let client = "59c37cb6b16fa2de30675f034c8008f890f4b2696c729d6267946d29736d73e4" - .parse::() - .unwrap() - .connect(provider) - .with_nonce_manager(); - - let nonce = client - .get_transaction_count(Some(BlockNumber::Pending)) - .await - .unwrap() - .as_u64(); - - let mut tx_hashes = Vec::new(); - for _ in 0..10 { - let tx = client - .send_transaction( - TransactionRequest::pay(client.address(), 100u64), - Some(BlockNumber::Pending), - ) - .await - .unwrap(); - tx_hashes.push(tx); - } - - let mut nonces = Vec::new(); - for tx_hash in tx_hashes { - nonces.push( - client - .get_transaction(tx_hash) - .await - .unwrap() - .unwrap() - .nonce - .as_u64(), - ); - } - - assert_eq!(nonces, (nonce..nonce + 10).collect::>()) - } - - #[tokio::test] - async fn using_gas_oracle() { - let ganache = Ganache::new().spawn(); - - // this private key belongs to the above mnemonic - let wallet: Wallet = ganache.keys()[0].clone().into(); - let wallet2: Wallet = ganache.keys()[1].clone().into(); - - // connect to the network - let provider = Provider::::try_from(ganache.endpoint()) - .unwrap() - .interval(Duration::from_millis(10u64)); - - // connect the wallet to the provider - let client = wallet.connect(provider); - - // assign a gas oracle to use - let gas_oracle = Etherchain::new().category(GasCategory::Fastest); - let expected_gas_price = gas_oracle.fetch().await.unwrap(); - - let client = client.gas_oracle(Box::new(gas_oracle)); - - // broadcast a transaction - let tx = TransactionRequest::new().to(wallet2.address()).value(10000); - let tx_hash = client.send_transaction(tx, None).await.unwrap(); - - let tx = client.get_transaction(tx_hash).await.unwrap().unwrap(); - assert_eq!(tx.gas_price, expected_gas_price); - } -} - -#[cfg(feature = "celo")] -mod celo_tests { - use super::*; - - #[tokio::test] - async fn test_send_transaction() { - // Celo testnet - let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") - .unwrap() - .interval(Duration::from_millis(3000u64)); - - // Funded with https://celo.org/developers/faucet - // Please do not drain this account :) - let client = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" - .parse::() - .unwrap() - .connect(provider); - - let balance_before = client.get_balance(client.address(), None).await.unwrap(); - let tx = TransactionRequest::pay(client.address(), 100); - let tx_hash = client.send_transaction(tx, None).await.unwrap(); - let _receipt = client - .pending_transaction(tx_hash) - .confirmations(3) - .await - .unwrap(); - let balance_after = client.get_balance(client.address(), None).await.unwrap(); - assert!(balance_before > balance_after); - } -} diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index 1901dc8a..1989bbfe 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -27,6 +27,7 @@ full = [ "providers", "signers", "core", + "middleware", ] celo = [ @@ -34,11 +35,13 @@ celo = [ "ethers-providers/celo", "ethers-signers/celo", "ethers-contract/celo", + "ethers-middleware/celo", ] core = ["ethers-core"] contract = ["ethers-contract"] providers = ["ethers-providers"] +middleware = ["ethers-middleware"] signers = ["ethers-signers"] ledger = ["ethers-signers/ledger"] @@ -47,6 +50,7 @@ ethers-contract = { version = "0.1.3", path = "../ethers-contract", optional = t ethers-core = { version = "0.1.3", path = "../ethers-core", optional = true } ethers-providers = { version = "0.1.3", path = "../ethers-providers", optional = true } ethers-signers = { version = "0.1.3", path = "../ethers-signers", optional = true } +ethers-middleware = { version = "0.1.3", path = "../ethers-middleware", optional = true } [dev-dependencies] ethers-contract = { version = "0.1.3", path = "../ethers-contract", features = ["abigen"] } diff --git a/ethers/examples/contract.rs b/ethers/examples/contract.rs index 5ba5b6b5..f144a879 100644 --- a/ethers/examples/contract.rs +++ b/ethers/examples/contract.rs @@ -31,7 +31,7 @@ async fn main() -> Result<()> { Provider::::try_from(ganache.endpoint())?.interval(Duration::from_millis(10u64)); // 5. instantiate the client with the wallet - let client = wallet.connect(provider); + let client = Client::new(provider, wallet); let client = Arc::new(client); // 6. create a factory which will be used to deploy instances of the contract diff --git a/ethers/examples/ens.rs b/ethers/examples/ens.rs index 9ee7e192..98d541b7 100644 --- a/ethers/examples/ens.rs +++ b/ethers/examples/ens.rs @@ -10,9 +10,9 @@ async fn main() -> Result<()> { )?; // create a wallet and connect it to the provider - let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" - .parse::()? - .connect(provider); + let wallet = + "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7".parse::()?; + let client = Client::new(provider, wallet); // craft the transaction let tx = TransactionRequest::new().to("vitalik.eth").value(100_000); diff --git a/ethers/examples/ledger.rs b/ethers/examples/ledger.rs index 3e6d9b21..f7f786f4 100644 --- a/ethers/examples/ledger.rs +++ b/ethers/examples/ledger.rs @@ -1,7 +1,6 @@ #[tokio::main] #[cfg(feature = "ledger")] -async fn main() -> Result<()> { - use anyhow::Result; +async fn main() -> anyhow::Result<()> { use ethers::{prelude::*, utils::parse_ether}; // Connect over websockets @@ -11,7 +10,7 @@ async fn main() -> Result<()> { // index or supply the full HD path string. You may also provide the chain_id // (here: mainnet) for EIP155 support. let ledger = Ledger::new(HDPath::LedgerLive(0), Some(1)).await?; - let client = Client::new(provider, ledger).await?; + let client = Client::new(provider, ledger); // Create and broadcast a transaction (ENS enabled!) // (this will require confirming the tx on the device) diff --git a/ethers/examples/local_signer.rs b/ethers/examples/local_signer.rs index c4d09091..43ce36d5 100644 --- a/ethers/examples/local_signer.rs +++ b/ethers/examples/local_signer.rs @@ -13,7 +13,7 @@ async fn main() -> Result<()> { let provider = Provider::::try_from(ganache.endpoint())?; // connect the wallet to the provider - let client = wallet.connect(provider); + let client = Client::new(provider, wallet); // craft the transaction let tx = TransactionRequest::new().to(wallet2.address()).value(10000); diff --git a/ethers/examples/transfer_eth.rs b/ethers/examples/transfer_eth.rs index 2080509a..ffd1b128 100644 --- a/ethers/examples/transfer_eth.rs +++ b/ethers/examples/transfer_eth.rs @@ -18,7 +18,7 @@ async fn main() -> Result<()> { let balance_before = provider.get_balance(from, None).await?; // broadcast it via the eth_sendTransaction API - let tx_hash = provider.send_transaction(tx).await?; + let tx_hash = provider.send_transaction(tx, None).await?; let tx = provider.pending_transaction(tx_hash).await?; diff --git a/ethers/src/lib.rs b/ethers/src/lib.rs index a5fc52df..3d19be87 100644 --- a/ethers/src/lib.rs +++ b/ethers/src/lib.rs @@ -99,6 +99,9 @@ pub use ethers_signers as signers; #[cfg(feature = "core")] pub use ethers_core as core; +#[cfg(feature = "middleware")] +pub use ethers_middleware as middleware; + // Re-export ethers_core::utils/types/abi // We hide these docs so that the rustdoc links send the visitor // to the corresponding crate, instead of the re-export @@ -124,6 +127,9 @@ pub mod prelude { #[cfg(feature = "signers")] pub use ethers_signers::*; + #[cfg(feature = "middleware")] + pub use ethers_middleware::*; + #[cfg(feature = "core")] pub use ethers_core::types::*; }