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
This commit is contained in:
Georgios Konstantopoulos 2020-09-25 00:33:09 +03:00 committed by GitHub
parent bf1d1e098f
commit 2d51c523ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1511 additions and 1189 deletions

View File

@ -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

22
Cargo.lock generated
View File

@ -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",

View File

@ -6,4 +6,5 @@ members = [
"./ethers-providers",
"./ethers-signers",
"./ethers-core",
"./ethers-middleware",
]

View File

@ -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

View File

@ -67,12 +67,12 @@ impl Context {
#struct_decl
impl<'a, P: JsonRpcClient, S: Signer> #name<P, S> {
impl<'a, M: Middleware> #name<M> {
/// 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<T: Into<Address>, C: Into<Arc<Client<P, S>>>>(address: T, client: C) -> Self {
let contract = Contract::new(address.into(), #abi_name.clone(), client.into());
pub fn new<T: Into<Address>>(address: T, client: Arc<M>) -> Self {
let contract = Contract::new(address.into(), #abi_name.clone(), client);
Self(contract)
}

View File

@ -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<P, S>(Contract<P, S>);
pub struct #name<M>(Contract<M>);
// Deref to the inner contract in order to access more specific functions functions
impl<P, S> std::ops::Deref for #name<P, S> {
type Target = Contract<P, S>;
impl<M> std::ops::Deref for #name<M> {
type Target = Contract<M>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<P: JsonRpcClient, S: Signer> std::fmt::Debug for #name<P, S> {
impl<M: Middleware> std::fmt::Debug for #name<M> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple(stringify!(#name))
.field(&self.address())

View File

@ -56,7 +56,7 @@ fn expand_filter(event: &Event) -> Result<TokenStream> {
Ok(quote! {
#doc
pub fn #name(&self) -> Event<P, #result> {
pub fn #name(&self) -> Event<M, #result> {
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<P, TransferFilter> {
pub fn transfer_filter(&self) -> Event<M, TransferFilter> {
self.0
.event("Transfer")
.expect("event not found (this should never happen)")

View File

@ -40,7 +40,7 @@ fn expand_function(function: &Function, alias: Option<Ident>) -> Result<TokenStr
let outputs = expand_fn_outputs(&function.outputs)?;
let result = quote! { ContractCall<P, S, #outputs> };
let result = quote! { ContractCall<M, #outputs> };
let arg = expand_inputs_call_arg(&function.inputs);
let doc = util::expand_doc(&format!(

View File

@ -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<M: Middleware> {
/// 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<P, S, D> {
pub struct ContractCall<M, D> {
/// 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<BlockNumber>,
pub(crate) client: Arc<Client<P, S>>,
pub(crate) client: Arc<M>,
pub(crate) datatype: PhantomData<D>,
}
impl<P, S, D: Detokenize> ContractCall<P, S, D> {
impl<M, D: Detokenize> ContractCall<M, D> {
/// Sets the `from` field in the transaction to the provided value
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
self.tx.from = Some(from.into());
@ -85,10 +80,9 @@ impl<P, S, D: Detokenize> ContractCall<P, S, D> {
}
}
impl<P, S, D> ContractCall<P, S, D>
impl<M, D> ContractCall<M, D>
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<U256, ContractError> {
Ok(self.client.estimate_gas(&self.tx).await?)
pub async fn estimate_gas(&self) -> Result<U256, ContractError<M>> {
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<D, ContractError> {
let bytes = self.client.call(&self.tx, self.block).await?;
pub async fn call(&self) -> Result<D, ContractError<M>> {
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<TxHash, ContractError> {
Ok(self.client.send_transaction(self.tx, self.block).await?)
pub async fn send(self) -> Result<TxHash, ContractError<M>> {
self.client
.send_transaction(self.tx, self.block)
.await
.map_err(ContractError::MiddlewareError)
}
}

View File

@ -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::<Http>::try_from("http://localhost:8545").unwrap();
/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// .parse::<Wallet>()?.connect(provider);
/// let client = Provider::<Http>::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<dyn std::error::Error>> {
/// 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::<Address>()?;
/// # let abi: Abi = serde_json::from_str(r#"[]"#)?;
/// # let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::<Wallet>()?.connect(provider);
/// # let client = Provider::<Http>::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<P, S> {
client: Arc<Client<P, S>>,
pub struct Contract<M> {
client: Arc<M>,
abi: Abi,
address: Address,
@ -175,13 +171,9 @@ pub struct Contract<P, S> {
methods: HashMap<Selector, (String, usize)>,
}
impl<P, S> Contract<P, S>
where
S: Signer,
P: JsonRpcClient,
{
impl<M: Middleware> Contract<M> {
/// Creates a new contract from the provided client, abi and address
pub fn new(address: Address, abi: Abi, client: impl Into<Arc<Client<P, S>>>) -> Self {
pub fn new(address: Address, abi: Abi, client: impl Into<Arc<M>>) -> 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<D: Detokenize>(&self, name: &str) -> Result<Event<P, D>, Error> {
pub fn event<D: Detokenize>(&self, name: &str) -> Result<Event<M, D>, 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<ContractCall<P, S, D>, Error> {
) -> Result<ContractCall<M, D>, 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<ContractCall<P, S, D>, Error> {
) -> Result<ContractCall<M, D>, Error> {
let function = self
.methods
.get(&signature)
@ -238,7 +230,7 @@ where
&self,
function: &Function,
args: T,
) -> Result<ContractCall<P, S, D>, Error> {
) -> Result<ContractCall<M, D>, Error> {
let tokens = args.into_tokens();
// create the calldata
@ -265,8 +257,7 @@ where
/// Clones `self` internally
pub fn at<T: Into<Address>>(&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<Client<P, S>>) -> Self
pub fn connect(&self, client: Arc<M>) -> 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<P, S> {
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)
}
}

View File

@ -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<P>,
pub(crate) provider: &'a M,
pub(crate) datatype: PhantomData<D>,
}
// TODO: Improve these functions
impl<P, D: Detokenize> Event<'_, '_, P, D> {
impl<M, D: Detokenize> Event<'_, '_, M, D> {
/// Sets the filter's `from` block
#[allow(clippy::wrong_self_convention)]
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
@ -63,41 +63,53 @@ impl<P, D: Detokenize> 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<impl Stream<Item = Result<D, ContractError>> + 'b, ContractError> {
let filter = self.provider.watch(&self.filter).await?;
) -> Result<impl Stream<Item = Result<D, ContractError<M>>> + 'b, ContractError<M>> {
let filter = self
.provider
.watch(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
Ok(filter.stream().map(move |log| self.parse_log(log)))
}
}
impl<P, D> Event<'_, '_, P, D>
impl<M, D> 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<Vec<D>, ContractError> {
let logs = self.provider.get_logs(&self.filter).await?;
pub async fn query(&self) -> Result<Vec<D>, ContractError<M>> {
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::<Result<Vec<_>, ContractError>>()?;
.collect::<Result<Vec<_>, ContractError<M>>>()?;
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<Vec<(D, LogMeta)>, ContractError> {
let logs = self.provider.get_logs(&self.filter).await?;
pub async fn query_with_meta(&self) -> Result<Vec<(D, LogMeta)>, ContractError<M>> {
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::<Result<_, ContractError>>()?;
.collect::<Result<_, ContractError<M>>>()?;
Ok(events)
}
fn parse_log(&self, log: Log) -> Result<D, ContractError> {
fn parse_log(&self, log: Log) -> Result<D, ContractError<M>> {
// ethabi parses the unindexed and indexed logs together to a
// vector of tokens
let tokens = self

View File

@ -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<P, S> {
pub struct Deployer<M> {
/// The deployer's transaction, exposed for overriding the defaults
pub tx: TransactionRequest,
abi: Abi,
client: Arc<Client<P, S>>,
client: Arc<M>,
confs: usize,
block: BlockNumber,
interval: Duration,
}
impl<P, S> Deployer<P, S>
where
S: Signer,
P: JsonRpcClient,
{
impl<M: Middleware> Deployer<M> {
/// Sets the number of confirmations to wait for the contract deployment transaction
pub fn confirmations<T: Into<usize>>(mut self, confirmations: T) -> Self {
self.confs = confirmations.into();
self
}
/// Sets the poll interval for the pending deployment transaction's inclusion
pub fn interval<T: Into<Duration>>(mut self, interval: T) -> Self {
self.interval = interval.into();
self
}
pub fn block<T: Into<BlockNumber>>(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<Contract<P, S>, ContractError> {
pub async fn send(self) -> Result<Contract<M>, ContractError<M>> {
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<P, S> {
pub fn client(&self) -> &M {
&self.client
}
}
@ -105,9 +95,8 @@ where
/// .expect("could not find contract");
///
/// // connect to the network
/// let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// .parse::<Wallet>()?.connect(provider);
/// let client = Provider::<Http>::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<P, S> {
client: Arc<Client<P, S>>,
pub struct ContractFactory<M> {
client: Arc<M>,
abi: Abi,
bytecode: Bytes,
}
impl<P, S> ContractFactory<P, S>
where
S: Signer,
P: JsonRpcClient,
{
impl<M: Middleware> ContractFactory<M> {
/// 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<Arc<Client<P, S>>>) -> Self {
pub fn new(abi: Abi, bytecode: Bytes, client: Arc<M>) -> 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<T: Tokenize>(self, constructor_args: T) -> Result<Deployer<P, S>, ContractError> {
pub fn deploy<T: Tokenize>(self, constructor_args: T) -> Result<Deployer<M>, ContractError<M>> {
// 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(),
})
}
}

View File

@ -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<HashMap<U256, Address>> = 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<HashMap<U256, Address>> = 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::<Http>::try_from("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?;
/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// .parse::<Wallet>()?.connect(provider);
/// let client = Provider::<Http>::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::<Provider<Http>>::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<HashMap<U256, Address>> = 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::<Address>()?;
@ -132,10 +128,10 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
/// [`block`]: method@crate::Multicall::block
/// [`add_call`]: methond@crate::Multicall::add_call
#[derive(Clone)]
pub struct Multicall<P, S> {
pub struct Multicall<M> {
calls: Vec<Call>,
block: Option<BlockNumber>,
contract: MulticallContract<P, S>,
contract: MulticallContract<M>,
}
#[derive(Clone)]
@ -147,11 +143,7 @@ pub struct Call {
function: Function,
}
impl<P, S> Multicall<P, S>
where
P: JsonRpcClient,
S: Signer,
{
impl<M: Middleware> Multicall<M> {
/// 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<C: Into<Arc<Client<P, S>>>>(
pub async fn new<C: Into<Arc<M>>>(
client: C,
address: Option<Address>,
) -> Result<Self, ContractError> {
) -> Result<Self, ContractError<M>> {
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<D: Detokenize>(&mut self, call: ContractCall<P, S, D>) -> &mut Self {
pub fn add_call<D: Detokenize>(&mut self, call: ContractCall<M, D>) -> &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::<Http>::try_from("http://localhost:8545")?;
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// # .parse::<Wallet>()?.connect(provider);
/// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
/// # let client = Arc::new(client);
/// #
/// # let abi = serde_json::from_str("")?;
/// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
/// # let contract = Contract::new(address, abi, client.clone());
/// # let contract = Contract::<Provider<Http>>::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<M>`] 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::<Http>::try_from("http://localhost:8545")?;
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// # .parse::<Wallet>()?.connect(provider);
/// # let client = Provider::<Http>::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<D: Detokenize>(&self) -> Result<D, ContractError> {
/// [`ContractError<M>`]: crate::ContractError<M>
pub async fn call<D: Detokenize>(&self) -> Result<D, ContractError<M>> {
let contract_call = self.as_contract_call();
// Fetch response from the Multicall contract
@ -324,7 +315,7 @@ where
_ => Token::Tuple(tokens),
})
})
.collect::<Result<Vec<Token>, ContractError>>()?;
.collect::<Result<Vec<Token>, ContractError<M>>>()?;
// Form tokens that represent tuples
let tokens = vec![Token::Tuple(tokens)];
@ -341,9 +332,7 @@ where
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// # use ethers::prelude::*;
/// # use std::convert::TryFrom;
/// # let provider = Provider::<Http>::try_from("http://localhost:8545")?;
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// # .parse::<Wallet>()?.connect(provider);
/// # let client = Provider::<Http>::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<TxHash, ContractError> {
pub async fn send(&self) -> Result<TxHash, ContractError<M>> {
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<P, S, (U256, Vec<Vec<u8>>)> {
fn as_contract_call(&self) -> ContractCall<M, (U256, Vec<Vec<u8>>)> {
// Map the Multicall struct into appropriate types for `aggregate` function
let calls: Vec<(Address, Vec<u8>)> = self
.calls

View File

@ -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<Abi> = 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<P, S>(Contract<P, S>);
impl<P, S> std::ops::Deref for MulticallContract<P, S> {
type Target = Contract<P, S>;
pub struct MulticallContract<M>(Contract<M>);
impl<M> std::ops::Deref for MulticallContract<M> {
type Target = Contract<M>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<P: JsonRpcClient, S: Signer> std::fmt::Debug for MulticallContract<P, S> {
impl<M: Middleware> std::fmt::Debug for MulticallContract<M> {
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<P, S> {
impl<'a, M: Middleware> MulticallContract<M> {
#[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<T: Into<Address>, C: Into<Arc<Client<P, S>>>>(address: T, client: C) -> Self {
pub fn new<T: Into<Address>, C: Into<Arc<M>>>(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<u8>)>,
) -> ContractCall<P, S, (U256, Vec<Vec<u8>>)> {
) -> ContractCall<M, (U256, Vec<Vec<u8>>)> {
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<P, S, U256> {
pub fn get_current_block_difficulty(&self) -> ContractCall<M, U256> {
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<P, S, U256> {
pub fn get_current_block_gas_limit(&self) -> ContractCall<M, U256> {
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<P, S, U256> {
pub fn get_current_block_timestamp(&self) -> ContractCall<M, U256> {
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<P, S, Address> {
pub fn get_current_block_coinbase(&self) -> ContractCall<M, Address> {
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<P, S, [u8; 32]> {
pub fn get_block_hash(&self, block_number: U256) -> ContractCall<M, [u8; 32]> {
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<P, S, U256> {
pub fn get_eth_balance(&self, addr: Address) -> ContractCall<M, U256> {
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<P, S, [u8; 32]> {
pub fn get_last_block_hash(&self) -> ContractCall<M, [u8; 32]> {
self.0
.method_hash([39, 232, 109, 110], ())
.expect("method not found (this should never happen)")

View File

@ -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<Provider<Http>, Wallet>;
/// connects the private key to http://localhost:8545
pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc<Client<Http, Wallet>> {
let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc<HttpWallet> {
let provider = Provider::<Http>::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<Client<Http, Wallet>>,
abi: Abi,
bytecode: Bytes,
) -> Contract<Http, Wallet> {
pub async fn deploy<M: Middleware>(client: Arc<M>, abi: Abi, bytecode: Bytes) -> Contract<M> {
let factory = ContractFactory::new(abi, bytecode, client);
factory
.deploy("initial value".to_string())

View File

@ -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::<Http>::try_from("https://alfajores-forno.celo-testnet.org").unwrap();
let provider = Provider::<Http>::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::<Wallet>()
.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);

View File

@ -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<R: Rng>(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<u64>,
) -> Result<Transaction, TxError> {
// 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<u64>) -> 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<u64>) -> 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::<Address>()
.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:

View File

@ -1,5 +1,5 @@
mod keys;
pub use keys::{PrivateKey, PublicKey, TxError};
pub use keys::{PrivateKey, PublicKey};
mod signature;
pub use signature::{Signature, SignatureError};

View File

@ -0,0 +1,39 @@
[package]
name = "ethers-middleware"
license = "MIT OR Apache-2.0"
version = "0.1.3"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
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"]

View File

@ -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<dyn std::error::Error>> {
/// let provider = Provider::<Http>::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::<Address>()?)
/// .value(200);
/// let tx_hash2 = client.send_transaction(tx2, None).await?;
///
/// # Ok(())
/// # }
///
/// ```
///
/// [`Provider`]: ethers_providers::Provider
pub struct Client<M, S> {
pub(crate) inner: M,
pub(crate) signer: S,
pub(crate) address: Address,
}
use ethers_providers::FromErr;
impl<M: Middleware, S: Signer> FromErr<M::Error> for ClientError<M, S> {
fn from(src: M::Error) -> ClientError<M, S> {
ClientError::MiddlewareError(src)
}
}
#[derive(Error, Debug)]
/// Error thrown when the client interacts with the blockchain
pub enum ClientError<M: Middleware, S: Signer> {
#[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<M, S> Client<M, S>
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<Transaction, ClientError<M, S>> {
// 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<BlockNumber>,
) -> Result<(), ClientError<M, S>> {
// 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<M, S> Middleware for Client<M, S>
where
M: Middleware,
S: Signer,
{
type Error = ClientError<M, S>;
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<BlockNumber>,
) -> Result<TxHash, Self::Error> {
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<T: Into<Bytes> + Send + Sync>(
&self,
data: T,
_: &Address,
) -> Result<Signature, Self::Error> {
Ok(self.signer.sign_message(data.into()).await.unwrap())
}
}
/// Calls the future if `item` is None, otherwise returns a `futures::ok`
async fn maybe<F, T, E>(item: Option<T>, f: F) -> Result<T, E>
where
F: Future<Output = Result<T, E>>,
{
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::<Address>()
.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::<Wallet>()
.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);
}
}

View File

@ -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<M, G> {
inner: M,
gas_oracle: G,
}
impl<M, G> GasOracleMiddleware<M, G>
where
M: Middleware,
G: GasOracle,
{
pub fn new(inner: M, gas_oracle: G) -> Self {
Self { inner, gas_oracle }
}
}
#[derive(Error, Debug)]
pub enum MiddlewareError<M: Middleware> {
#[error(transparent)]
GasOracleError(#[from] GasOracleError),
#[error("{0}")]
MiddlewareError(M::Error),
}
impl<M: Middleware> FromErr<M::Error> for MiddlewareError<M> {
fn from(src: M::Error) -> MiddlewareError<M> {
MiddlewareError::MiddlewareError(src)
}
}
#[async_trait(?Send)]
impl<M, G> Middleware for GasOracleMiddleware<M, G>
where
M: Middleware,
G: GasOracle,
{
type Error = MiddlewareError<M>;
type Provider = M::Provider;
type Inner = M;
// OVERRIDEN METHODS
fn inner(&self) -> &M {
&self.inner
}
async fn get_gas_price(&self) -> Result<U256, Self::Error> {
Ok(self.gas_oracle.fetch().await?)
}
async fn send_transaction(
&self,
mut tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
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)
}
}

View File

@ -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},
/// };
///

View File

@ -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;

View File

@ -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<M> {
pub inner: M,
pub initialized: AtomicBool,
pub nonce: AtomicU64,
pub address: Address,
}
impl<M> NonceManager<M>
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<BlockNumber>,
) -> Result<U256, NonceManagerError<M>> {
// 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<M: Middleware> {
#[error("{0}")]
MiddlewareError(M::Error),
}
impl<M: Middleware> FromErr<M::Error> for NonceManagerError<M> {
fn from(src: M::Error) -> Self {
NonceManagerError::MiddlewareError(src)
}
}
#[async_trait(?Send)]
impl<M> Middleware for NonceManager<M>
where
M: Middleware,
{
type Error = NonceManagerError<M>;
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<BlockNumber>,
) -> Result<TxHash, Self::Error> {
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))
}
}
}
}
}

View File

@ -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::<Http>::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());
}

View File

@ -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::<Http>::try_from("https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778")
.unwrap()
.interval(Duration::from_millis(2000u64));
let wallet = "59c37cb6b16fa2de30675f034c8008f890f4b2696c729d6267946d29736d73e4"
.parse::<Wallet>()
.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::<Vec<_>>())
}

View File

@ -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::<Http>::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::<Http>::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::<Wallet>()
.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);
}

View File

@ -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::<Http>::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);
}

View File

@ -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<dyn std::error::Error>> {
@ -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<dyn std::error::Error>> {
//! # let provider = Provider::<Http>::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<Box<dyn Future<Output = Result<T, ProviderError>> + 'a + Send>>;
pub(crate) type PinBoxFut<'a, T> = Pin<Box<dyn Future<Output = Result<T, ProviderError>> + '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<ProviderError>;
@ -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<T> {
fn from(src: T) -> Self;
}
#[async_trait(?Send)]
pub trait Middleware: Sync + Send + Debug {
type Error: Error + FromErr<<Self::Inner as Middleware>::Error>;
type Provider: JsonRpcClient;
type Inner: Middleware<Provider = Self::Provider>;
fn inner(&self) -> &Self::Inner;
async fn get_block_number(&self) -> Result<U64, Self::Error> {
self.inner().get_block_number().await.map_err(FromErr::from)
}
async fn send_transaction(
&self,
tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
self.inner()
.send_transaction(tx, block)
.await
.map_err(FromErr::from)
}
async fn resolve_name(&self, ens_name: &str) -> Result<Address, Self::Error> {
self.inner()
.resolve_name(ens_name)
.await
.map_err(FromErr::from)
}
async fn lookup_address(&self, address: Address) -> Result<String, Self::Error> {
self.inner()
.lookup_address(address)
.await
.map_err(FromErr::from)
}
async fn get_block<T: Into<BlockId> + Send + Sync>(
&self,
block_hash_or_number: T,
) -> Result<Option<Block<TxHash>>, Self::Error> {
self.inner()
.get_block(block_hash_or_number)
.await
.map_err(FromErr::from)
}
async fn get_block_with_txs<T: Into<BlockId> + Send + Sync>(
&self,
block_hash_or_number: T,
) -> Result<Option<Block<Transaction>>, Self::Error> {
self.inner()
.get_block_with_txs(block_hash_or_number)
.await
.map_err(FromErr::from)
}
async fn get_transaction_count<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: T,
block: Option<BlockNumber>,
) -> Result<U256, Self::Error> {
self.inner()
.get_transaction_count(from, block)
.await
.map_err(FromErr::from)
}
async fn estimate_gas(&self, tx: &TransactionRequest) -> Result<U256, Self::Error> {
self.inner().estimate_gas(tx).await.map_err(FromErr::from)
}
async fn call(
&self,
tx: &TransactionRequest,
block: Option<BlockNumber>,
) -> Result<Bytes, Self::Error> {
self.inner().call(tx, block).await.map_err(FromErr::from)
}
async fn get_chainid(&self) -> Result<U256, Self::Error> {
self.inner().get_chainid().await.map_err(FromErr::from)
}
async fn get_balance<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: T,
block: Option<BlockNumber>,
) -> Result<U256, Self::Error> {
self.inner()
.get_balance(from, block)
.await
.map_err(FromErr::from)
}
async fn get_transaction<T: Send + Sync + Into<TxHash>>(
&self,
transaction_hash: T,
) -> Result<Option<Transaction>, Self::Error> {
self.inner()
.get_transaction(transaction_hash)
.await
.map_err(FromErr::from)
}
async fn get_transaction_receipt<T: Send + Sync + Into<TxHash>>(
&self,
transaction_hash: T,
) -> Result<Option<TransactionReceipt>, Self::Error> {
self.inner()
.get_transaction_receipt(transaction_hash)
.await
.map_err(FromErr::from)
}
async fn get_gas_price(&self) -> Result<U256, Self::Error> {
self.inner().get_gas_price().await.map_err(FromErr::from)
}
async fn get_accounts(&self) -> Result<Vec<Address>, Self::Error> {
self.inner().get_accounts().await.map_err(FromErr::from)
}
async fn send_raw_transaction(&self, tx: &Transaction) -> Result<TxHash, Self::Error> {
self.inner()
.send_raw_transaction(tx)
.await
.map_err(FromErr::from)
}
async fn sign<T: Into<Bytes> + Send + Sync>(
&self,
data: T,
from: &Address,
) -> Result<Signature, Self::Error> {
self.inner().sign(data, from).await.map_err(FromErr::from)
}
////// Contract state
async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>, Self::Error> {
self.inner().get_logs(filter).await.map_err(FromErr::from)
}
async fn new_filter(&self, filter: FilterKind<'_>) -> Result<U256, Self::Error> {
self.inner().new_filter(filter).await.map_err(FromErr::from)
}
async fn uninstall_filter<T: Into<U256> + Send + Sync>(
&self,
id: T,
) -> Result<bool, Self::Error> {
self.inner()
.uninstall_filter(id)
.await
.map_err(FromErr::from)
}
async fn watch<'a>(
&'a self,
filter: &Filter,
) -> Result<FilterWatcher<'a, Self::Provider, Log>, Self::Error> {
self.inner().watch(filter).await.map_err(FromErr::from)
}
async fn watch_pending_transactions(
&self,
) -> Result<FilterWatcher<'_, Self::Provider, H256>, Self::Error> {
self.inner()
.watch_pending_transactions()
.await
.map_err(FromErr::from)
}
async fn get_filter_changes<T, R>(&self, id: T) -> Result<Vec<R>, Self::Error>
where
T: Into<U256> + 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<FilterWatcher<'_, Self::Provider, H256>, Self::Error> {
self.inner().watch_blocks().await.map_err(FromErr::from)
}
async fn get_code<T: Into<NameOrAddress> + Send + Sync>(
&self,
at: T,
block: Option<BlockNumber>,
) -> Result<Bytes, Self::Error> {
self.inner()
.get_code(at, block)
.await
.map_err(FromErr::from)
}
async fn get_storage_at<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: T,
location: H256,
block: Option<BlockNumber>,
) -> Result<H256, Self::Error> {
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)
}
}

View File

@ -1,3 +1,4 @@
use crate::Middleware;
use crate::{
stream::{interval, DEFAULT_POLL_INTERVAL},
JsonRpcClient, PinBoxFut, Provider, ProviderError,

View File

@ -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<dyn std::error::Error>> {
/// use ethers::providers::{Middleware, Provider, Http};
/// use std::convert::TryFrom;
///
/// let provider = Provider::<Http>::try_from(
/// "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27"
/// ).expect("could not instantiate HTTP Provider");
///
/// # async fn foo<P: JsonRpcClient>(provider: &Provider<P>) -> Result<(), Box<dyn std::error::Error>> {
/// let block = provider.get_block(100u64).await?;
/// println!("Got block: {}", serde_json::to_string(&block)?);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug)]
pub struct Provider<P>(P, Option<Address>, Option<Duration>);
// TODO: Convert to proper struct
pub struct Provider<P>(P, Option<Address>, Option<Duration>, Option<Address>);
impl FromErr<ProviderError> 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<P: JsonRpcClient> Provider<P> {
/// 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<U64, ProviderError> {
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<BlockId>,
) -> Result<Option<Block<TxHash>>, 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<BlockId>,
) -> Result<Option<Block<Transaction>>, ProviderError> {
Ok(self
.get_block_gen(block_hash_or_number.into(), true)
.await?)
pub fn with_sender(mut self, address: impl Into<Address>) -> Self {
self.3 = Some(address.into());
self
}
async fn get_block_gen<Tx: for<'a> Deserialize<'a>>(
@ -132,9 +113,53 @@ impl<P: JsonRpcClient> Provider<P> {
}
})
}
}
#[async_trait(?Send)]
impl<P: JsonRpcClient> Middleware for Provider<P> {
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<U64, ProviderError> {
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<T: Into<BlockId> + Send + Sync>(
&self,
block_hash_or_number: T,
) -> Result<Option<Block<TxHash>>, 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<T: Into<BlockId> + Send + Sync>(
&self,
block_hash_or_number: T,
) -> Result<Option<Block<Transaction>>, ProviderError> {
Ok(self
.get_block_gen(block_hash_or_number.into(), true)
.await?)
}
/// Gets the transaction with `transaction_hash`
pub async fn get_transaction<T: Send + Sync + Into<TxHash>>(
async fn get_transaction<T: Send + Sync + Into<TxHash>>(
&self,
transaction_hash: T,
) -> Result<Option<Transaction>, ProviderError> {
@ -147,7 +172,7 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Gets the transaction receipt with `transaction_hash`
pub async fn get_transaction_receipt<T: Send + Sync + Into<TxHash>>(
async fn get_transaction_receipt<T: Send + Sync + Into<TxHash>>(
&self,
transaction_hash: T,
) -> Result<Option<TransactionReceipt>, ProviderError> {
@ -160,7 +185,7 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Gets the current gas price as estimated by the node
pub async fn get_gas_price(&self) -> Result<U256, ProviderError> {
async fn get_gas_price(&self) -> Result<U256, ProviderError> {
Ok(self
.0
.request("eth_gasPrice", ())
@ -169,7 +194,7 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Gets the accounts on the node
pub async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
Ok(self
.0
.request("eth_accounts", ())
@ -178,9 +203,9 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Returns the nonce of the address
pub async fn get_transaction_count(
async fn get_transaction_count<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: impl Into<NameOrAddress>,
from: T,
block: Option<BlockNumber>,
) -> Result<U256, ProviderError> {
let from = match from.into() {
@ -198,9 +223,9 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Returns the account's balance
pub async fn get_balance(
async fn get_balance<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: impl Into<NameOrAddress>,
from: T,
block: Option<BlockNumber>,
) -> Result<U256, ProviderError> {
let from = match from.into() {
@ -219,7 +244,7 @@ impl<P: JsonRpcClient> Provider<P> {
/// 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<U256, ProviderError> {
async fn get_chainid(&self) -> Result<U256, ProviderError> {
Ok(self
.0
.request("eth_chainId", ())
@ -233,7 +258,7 @@ impl<P: JsonRpcClient> Provider<P> {
/// 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<BlockNumber>,
@ -250,7 +275,7 @@ impl<P: JsonRpcClient> Provider<P> {
/// 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<U256, ProviderError> {
async fn estimate_gas(&self, tx: &TransactionRequest) -> Result<U256, ProviderError> {
let tx = utils::serialize(tx);
Ok(self
@ -262,10 +287,19 @@ impl<P: JsonRpcClient> Provider<P> {
/// 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<BlockNumber>,
) -> Result<TxHash, ProviderError> {
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<P: JsonRpcClient> Provider<P> {
/// 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<TxHash, ProviderError> {
async fn send_raw_transaction(&self, tx: &Transaction) -> Result<TxHash, ProviderError> {
let rlp = utils::serialize(&tx.rlp());
Ok(self
.0
@ -295,7 +329,7 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Signs data using a specific account. This account needs to be unlocked.
pub async fn sign<T: Into<Bytes>>(
async fn sign<T: Into<Bytes> + Send + Sync>(
&self,
data: T,
from: &Address,
@ -312,7 +346,7 @@ impl<P: JsonRpcClient> Provider<P> {
////// Contract state
/// Returns an array (possibly empty) of logs that match the filter
pub async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>, ProviderError> {
async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>, ProviderError> {
Ok(self
.0
.request("eth_getLogs", [filter])
@ -321,22 +355,24 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Streams matching filter logs
pub async fn watch(&self, filter: &Filter) -> Result<FilterWatcher<'_, P, Log>, ProviderError> {
async fn watch<'a>(
&'a self,
filter: &Filter,
) -> Result<FilterWatcher<'a, P, Log>, 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<FilterWatcher<'_, P, H256>, ProviderError> {
async fn watch_blocks(&self) -> Result<FilterWatcher<'_, P, H256>, 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<FilterWatcher<'_, P, H256>, ProviderError> {
let id = self.new_filter(FilterKind::PendingTransactions).await?;
@ -346,7 +382,7 @@ impl<P: JsonRpcClient> Provider<P> {
/// 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<U256, ProviderError> {
async fn new_filter(&self, filter: FilterKind<'_>) -> Result<U256, ProviderError> {
let (method, args) = match filter {
FilterKind::NewBlocks => ("eth_newBlockFilter", vec![]),
FilterKind::PendingTransactions => ("eth_newPendingTransactionFilter", vec![]),
@ -357,7 +393,10 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Uninstalls a filter
pub async fn uninstall_filter<T: Into<U256>>(&self, id: T) -> Result<bool, ProviderError> {
async fn uninstall_filter<T: Into<U256> + Send + Sync>(
&self,
id: T,
) -> Result<bool, ProviderError> {
let id = utils::serialize(&id.into());
Ok(self
.0
@ -379,10 +418,10 @@ impl<P: JsonRpcClient> Provider<P> {
///
/// [`H256`]: ethers_core::types::H256
/// [`Log`]: ethers_core::types::Log
pub async fn get_filter_changes<T, R>(&self, id: T) -> Result<Vec<R>, ProviderError>
async fn get_filter_changes<T, R>(&self, id: T) -> Result<Vec<R>, ProviderError>
where
T: Into<U256>,
R: for<'a> Deserialize<'a>,
T: Into<U256> + Send + Sync,
R: for<'a> Deserialize<'a> + Send + Sync,
{
let id = utils::serialize(&id.into());
Ok(self
@ -393,9 +432,9 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Get the storage of an address for a particular slot location
pub async fn get_storage_at(
async fn get_storage_at<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: impl Into<NameOrAddress>,
from: T,
location: H256,
block: Option<BlockNumber>,
) -> Result<H256, ProviderError> {
@ -415,9 +454,9 @@ impl<P: JsonRpcClient> Provider<P> {
}
/// Returns the deployed code at a given address
pub async fn get_code(
async fn get_code<T: Into<NameOrAddress> + Send + Sync>(
&self,
at: impl Into<NameOrAddress>,
at: T,
block: Option<BlockNumber>,
) -> Result<Bytes, ProviderError> {
let at = match at.into() {
@ -447,7 +486,7 @@ impl<P: JsonRpcClient> Provider<P> {
///
/// 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<Address, ProviderError> {
async fn resolve_name(&self, ens_name: &str) -> Result<Address, ProviderError> {
self.query_resolver(ParamType::Address, ens_name, ens::ADDR_SELECTOR)
.await
}
@ -457,12 +496,20 @@ impl<P: JsonRpcClient> Provider<P> {
///
/// 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<String, ProviderError> {
async fn lookup_address(&self, address: Address) -> Result<String, ProviderError> {
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<P: JsonRpcClient> Provider<P> {
async fn query_resolver<T: Detokenize>(
&self,
param: ParamType,
@ -521,12 +568,6 @@ impl<P: JsonRpcClient> Provider<P> {
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<HttpProvider> {
type Error = ParseError;
fn try_from(src: &str) -> Result<Self, Self::Error> {
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<H256> = stream.take(num_txs).collect::<Vec<H256>>().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)

View File

@ -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<T: Into<U256>>(id: T, provider: &'a Provider<P>) -> 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;

View File

@ -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<S> {
ws: Mutex<S>,
}
impl<S> Debug for Provider<S> {
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<WebSocketStream<MaybeTlsStream>> {
/// Initializes a new WebSocket Client.

View File

@ -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<P: JsonRpcClient>(provider: Provider<P>) {
async fn generic_pending_txs_test<M: Middleware>(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();

View File

@ -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"]

View File

@ -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<dyn std::error::Error>> {
/// let provider = Provider::<Http>::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::<Address>()?)
/// .value(200);
/// let tx_hash2 = client.send_transaction(tx2, None).await?;
///
/// # Ok(())
/// # }
///
/// ```
///
/// [`Provider`]: ethers_providers::Provider
pub struct Client<P, S> {
pub(crate) provider: Provider<P>,
pub(crate) signer: Option<S>,
pub(crate) address: Address,
pub(crate) gas_oracle: Option<Box<dyn GasOracle>>,
pub(crate) nonce_manager: Option<NonceManager>,
}
#[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<dyn std::error::Error + Send + Sync>),
#[error("ens name not found: {0}")]
/// Thrown when an ENS name is not found
EnsError(String),
}
// Helper functions for locally signing transactions
impl<P, S> Client<P, S>
where
P: JsonRpcClient,
S: Signer,
{
/// Creates a new client from the provider and signer.
pub async fn new(provider: Provider<P>, signer: S) -> Result<Self, ClientError> {
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<T: Into<Bytes>>(&self, msg: T) -> Result<Signature, ClientError> {
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<BlockNumber>,
) -> Result<TxHash, ClientError> {
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<TxHash, ClientError> {
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<BlockNumber>,
) -> 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<BlockNumber>,
) -> Result<U256, ClientError> {
// 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<BlockNumber>,
) -> Result<U256, ClientError> {
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<P> {
&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<P>) -> &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<T: Into<Address>>(mut self, address: T) -> Self {
self.address = address.into();
self
}
/// Sets the default polling interval for event filters and pending transactions
pub fn interval<T: Into<Duration>>(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<dyn GasOracle>) -> 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<F, T, E>(item: Option<T>, f: F) -> Result<T, E>
where
F: Future<Output = Result<T, E>>,
{
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<P, S> Deref for Client<P, S> {
type Target = Provider<P>;
fn deref(&self) -> &Self::Target {
&self.provider
}
}
impl<P: JsonRpcClient, S> From<Provider<P>> for Client<P, S> {
fn from(provider: Provider<P>) -> Self {
Self {
provider,
signer: None,
address: Address::zero(),
gas_oracle: None,
nonce_manager: None,
}
}
}

View File

@ -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<Ledger>,
derivation: DerivationType,
pub chain_id: Option<u64>,
/// 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<dyn std::error::Error>> {
/// 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<u64>,
) -> Result<Self, LedgerError> {
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<Address, LedgerError> {
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<Address, LedgerError> {
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<u64>,
) -> Result<Transaction, LedgerError> {
// 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<Signature, LedgerError> {
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<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, LedgerError> {
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<u8> {
fn path_to_bytes(derivation: &DerivationType) -> Vec<u8> {
let derivation = derivation.to_string();
let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
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::<Http>::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

View File

@ -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<Transaction, Self::Error> {
message: &TransactionRequest,
) -> Result<Signature, Self::Error> {
self.sign_tx(message, self.chain_id).await
}
/// Returns the signer's Ethereum Address
async fn address(&self) -> Result<Address, Self::Error> {
self.get_address().await
}
}
impl From<LedgerError> for ClientError {
fn from(src: LedgerError) -> Self {
ClientError::SignerError(Box::new(src))
fn address(&self) -> Address {
self.address
}
}

View File

@ -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),
}

View File

@ -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<dyn std::error::Error>> {
//! // connect to the network
//! let provider = Provider::<Http>::try_from("http://localhost:8545")?;
//!
//! // instantiate the wallet and connect it to the provider to get a client
//! let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7"
//! .parse::<Wallet>()?
//! .connect(provider);
//! // instantiate the wallet
//! let wallet = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7"
//! .parse::<Wallet>()?;
//!
//! // 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<ClientError>;
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<S: Send + Sync + AsRef<[u8]>>(
&self,
@ -74,12 +63,9 @@ pub trait Signer {
/// Signs the transaction
async fn sign_transaction(
&self,
message: TransactionRequest,
) -> Result<Transaction, Self::Error>;
message: &TransactionRequest,
) -> Result<Signature, Self::Error>;
/// Returns the signer's Ethereum Address
async fn address(&self) -> Result<Address, Self::Error>;
fn address(&self) -> Address;
}
/// An HTTP client configured to work with ANY blockchain without replay protection
pub type HttpClient = Client<Http, Wallet>;

View File

@ -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()
}
}

View File

@ -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::<Http>::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<S: Send + Sync + AsRef<[u8]>>(
&self,
message: S,
) -> Result<Signature, TxError> {
) -> Result<Signature, Self::Error> {
Ok(self.private_key.sign(message))
}
async fn sign_transaction(&self, tx: TransactionRequest) -> Result<Transaction, Self::Error> {
self.private_key.sign_transaction(tx, self.chain_id)
async fn sign_transaction(&self, tx: &TransactionRequest) -> Result<Signature, Self::Error> {
Ok(self.private_key.sign_transaction(tx, self.chain_id))
}
async fn address(&self) -> Result<Address, Self::Error> {
Ok(self.address)
}
}
impl From<TxError> 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<P: JsonRpcClient>(self, provider: Provider<P>) -> Client<P, Wallet> {
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<T: Into<u64>>(mut self, chain_id: T) -> Self {
self.chain_id = Some(chain_id.into());

View File

@ -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::<Http>::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::<Wallet>()
.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::<Http>::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::<Http>::try_from(
"https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778",
)
.unwrap()
.interval(Duration::from_millis(2000u64));
let client = "59c37cb6b16fa2de30675f034c8008f890f4b2696c729d6267946d29736d73e4"
.parse::<Wallet>()
.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::<Vec<_>>())
}
#[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::<Http>::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::<Http>::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::<Wallet>()
.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);
}
}

View File

@ -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"] }

View File

@ -31,7 +31,7 @@ async fn main() -> Result<()> {
Provider::<Http>::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

View File

@ -10,9 +10,9 @@ async fn main() -> Result<()> {
)?;
// create a wallet and connect it to the provider
let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7"
.parse::<Wallet>()?
.connect(provider);
let wallet =
"dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7".parse::<Wallet>()?;
let client = Client::new(provider, wallet);
// craft the transaction
let tx = TransactionRequest::new().to("vitalik.eth").value(100_000);

View File

@ -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)

View File

@ -13,7 +13,7 @@ async fn main() -> Result<()> {
let provider = Provider::<Http>::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);

View File

@ -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?;

View File

@ -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::*;
}