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:
parent
bf1d1e098f
commit
2d51c523ba
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -6,4 +6,5 @@ members = [
|
|||
"./ethers-providers",
|
||||
"./ethers-signers",
|
||||
"./ethers-core",
|
||||
"./ethers-middleware",
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod keys;
|
||||
pub use keys::{PrivateKey, PublicKey, TxError};
|
||||
pub use keys::{PrivateKey, PublicKey};
|
||||
|
||||
mod signature;
|
||||
pub use signature::{Signature, SignatureError};
|
||||
|
|
|
@ -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"]
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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},
|
||||
/// };
|
||||
///
|
|
@ -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;
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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<_>>())
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::Middleware;
|
||||
use crate::{
|
||||
stream::{interval, DEFAULT_POLL_INTERVAL},
|
||||
JsonRpcClient, PinBoxFut, Provider, ProviderError,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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?;
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue