feat: typed txs provider / middleware changes (part 3) (#357)

* feat(providers): add eth_feeHistory api

* add access list

* feat: fill transactions with access list / default sender info

* feat: add helpers for operating on the txs enum

* feat: send_transaction takes TypedTransaction now

* fix(contract): temp wrap all contract txs as Legacy txs

* feat(middleware): use TypedTransaction in Transformer

* feat(signers): use TypedTransaction in Wallet/Ledger

* feat(core): add helpers for setting typed tx fields

* feat(signer): use typed transactions

* fix(middleware): adjust nonce/gas/escalators for TypedTxs

The GPO and the Escalators will throw an error if they are provided an EIP1559 transaction

* fix(providers): ensure the correct account's nonce is filled

* fix: add .into() to txs until we make the fn call more generic

* Revert "fix: add .into() to txs until we make the fn call more generic"

This reverts commit 04dc34b26d0e3f418ed3fc69ea35ad538b83dd50.

* feat: generalize send_transaction interface

* fix: only set the nonce manually in the Signer middleware

* fix(transformer): fill the transaction after transformation

* chore: fix compilation errors & lints

* fix(signer): set the correct account's nonce

* feat: make trace_call / call take TypedTransaction

* fix: set sender to transaction in signer

* chore: ethgasstation broke

* chore: cargo fmt / lints

* Fix(signer): pass the chain id

* fix: final tx encoding fixes

1. Normalize v values for eip1559/2730
2. Make access lists mandatory for 1559
3. do not double-rlp on rlp_signed

* fix: set access list only if available

* test: check 1559 / 2930 txs

* fix: do not prepend a 0 for Legacy txs & test

* chore: code review comments

* chore: fix aws signer signature
This commit is contained in:
Georgios Konstantopoulos 2021-08-09 03:31:11 +03:00 committed by GitHub
parent ccff9fc98f
commit dcbfacf5bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 418 additions and 204 deletions

View File

@ -102,7 +102,7 @@ where
/// Returns the estimated gas cost for the underlying transaction to be executed
pub async fn estimate_gas(&self) -> Result<U256, ContractError<M>> {
self.client
.estimate_gas(&self.tx)
.estimate_gas(&self.tx.clone().into())
.await
.map_err(ContractError::MiddlewareError)
}
@ -119,7 +119,7 @@ where
pub async fn call(&self) -> Result<D, ContractError<M>> {
let bytes = self
.client
.call(&self.tx, self.block)
.call(&self.tx.clone().into(), self.block)
.await
.map_err(ContractError::MiddlewareError)?;

View File

@ -1,4 +1,4 @@
use super::{eip2930::AccessList, rlp_opt};
use super::{eip2930::AccessList, normalize_v, rlp_opt};
use crate::{
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
utils::keccak256,
@ -37,12 +37,8 @@ pub struct Eip1559TransactionRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<U256>,
#[serde(
rename = "accessList",
default,
skip_serializing_if = "Option::is_none"
)]
pub access_list: Option<AccessList>,
#[serde(rename = "accessList", default)]
pub access_list: AccessList,
#[serde(
rename = "maxPriorityFeePerGas",
@ -121,7 +117,7 @@ impl Eip1559TransactionRequest {
/// Sets the `access_list` field in the transaction to the provided value
pub fn access_list<T: Into<AccessList>>(mut self, access_list: T) -> Self {
self.access_list = Some(access_list.into());
self.access_list = access_list.into();
self
}
@ -148,10 +144,12 @@ impl Eip1559TransactionRequest {
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
let mut rlp = RlpStream::new();
rlp.begin_unbounded_list();
let chain_id = chain_id.into();
self.rlp_base(chain_id, &mut rlp);
// append the signature
rlp.append(&signature.v);
let v = normalize_v(signature.v, chain_id);
rlp.append(&v);
rlp.append(&signature.r);
rlp.append(&signature.s);
rlp.finalize_unbounded_list();
@ -167,6 +165,6 @@ impl Eip1559TransactionRequest {
rlp_opt(rlp, &self.to.as_ref());
rlp_opt(rlp, &self.value);
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
rlp_opt(rlp, &self.access_list);
rlp.append(&self.access_list);
}
}

View File

@ -19,9 +19,10 @@ pub enum TypedTransaction {
Eip1559(Eip1559TransactionRequest),
}
use TypedTransaction::*;
impl TypedTransaction {
pub fn from(&self) -> Option<&Address> {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.from.as_ref(),
Eip2930(inner) => inner.tx.from.as_ref(),
@ -30,7 +31,6 @@ impl TypedTransaction {
}
pub fn set_from(&mut self, from: Address) {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.from = Some(from),
Eip2930(inner) => inner.tx.from = Some(from),
@ -39,7 +39,6 @@ impl TypedTransaction {
}
pub fn to(&self) -> Option<&NameOrAddress> {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.to.as_ref(),
Eip2930(inner) => inner.tx.to.as_ref(),
@ -49,7 +48,6 @@ impl TypedTransaction {
pub fn set_to<T: Into<NameOrAddress>>(&mut self, to: T) {
let to = to.into();
use TypedTransaction::*;
match self {
Legacy(inner) => inner.to = Some(to),
Eip2930(inner) => inner.tx.to = Some(to),
@ -58,7 +56,6 @@ impl TypedTransaction {
}
pub fn nonce(&self) -> Option<&U256> {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.nonce.as_ref(),
Eip2930(inner) => inner.tx.nonce.as_ref(),
@ -68,7 +65,6 @@ impl TypedTransaction {
pub fn set_nonce<T: Into<U256>>(&mut self, nonce: T) {
let nonce = nonce.into();
use TypedTransaction::*;
match self {
Legacy(inner) => inner.nonce = Some(nonce),
Eip2930(inner) => inner.tx.nonce = Some(nonce),
@ -77,7 +73,6 @@ impl TypedTransaction {
}
pub fn value(&self) -> Option<&U256> {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.value.as_ref(),
Eip2930(inner) => inner.tx.value.as_ref(),
@ -87,7 +82,6 @@ impl TypedTransaction {
pub fn set_value<T: Into<U256>>(&mut self, value: T) {
let value = value.into();
use TypedTransaction::*;
match self {
Legacy(inner) => inner.value = Some(value),
Eip2930(inner) => inner.tx.value = Some(value),
@ -96,7 +90,6 @@ impl TypedTransaction {
}
pub fn gas(&self) -> Option<&U256> {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.gas.as_ref(),
Eip2930(inner) => inner.tx.gas.as_ref(),
@ -106,7 +99,6 @@ impl TypedTransaction {
pub fn set_gas<T: Into<U256>>(&mut self, gas: T) {
let gas = gas.into();
use TypedTransaction::*;
match self {
Legacy(inner) => inner.gas = Some(gas),
Eip2930(inner) => inner.tx.gas = Some(gas),
@ -116,7 +108,6 @@ impl TypedTransaction {
pub fn set_gas_price<T: Into<U256>>(&mut self, gas_price: T) {
let gas_price = gas_price.into();
use TypedTransaction::*;
match self {
Legacy(inner) => inner.gas_price = Some(gas_price),
Eip2930(inner) => inner.tx.gas_price = Some(gas_price),
@ -128,7 +119,6 @@ impl TypedTransaction {
}
pub fn data(&self) -> Option<&Bytes> {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.data.as_ref(),
Eip2930(inner) => inner.tx.data.as_ref(),
@ -137,7 +127,6 @@ impl TypedTransaction {
}
pub fn set_data(&mut self, data: Bytes) {
use TypedTransaction::*;
match self {
Legacy(inner) => inner.data = Some(data),
Eip2930(inner) => inner.tx.data = Some(data),
@ -146,12 +135,10 @@ impl TypedTransaction {
}
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
use TypedTransaction::*;
let mut encoded = vec![];
match self {
Legacy(inner) => {
encoded.extend_from_slice(&[0x0]);
encoded.extend_from_slice(inner.rlp_signed(signature).as_ref());
Legacy(ref tx) => {
encoded.extend_from_slice(tx.rlp_signed(signature).as_ref());
}
Eip2930(inner) => {
encoded.extend_from_slice(&[0x1]);
@ -162,17 +149,14 @@ impl TypedTransaction {
encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
}
};
rlp::encode(&encoded).freeze().into()
encoded.into()
}
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
let chain_id = chain_id.into();
let mut encoded = vec![];
use TypedTransaction::*;
match self {
Legacy(inner) => {
encoded.extend_from_slice(&[0x0]);
encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
}
Eip2930(inner) => {

View File

@ -1,5 +1,5 @@
use super::request::TransactionRequest;
use crate::types::{Address, Bytes, Signature, H256, U64};
use super::{normalize_v, request::TransactionRequest};
use crate::types::{Address, Bytes, Signature, H256, U256, U64};
use rlp::RlpStream;
use rlp_derive::{RlpEncodable, RlpEncodableWrapper};
@ -13,6 +13,13 @@ const NUM_EIP2930_FIELDS: usize = 8;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodableWrapper)]
pub struct AccessList(pub Vec<AccessListItem>);
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AccessListWithGasUsed {
pub access_list: AccessList,
pub gas_used: U256,
}
impl From<Vec<AccessListItem>> for AccessList {
fn from(src: Vec<AccessListItem>) -> AccessList {
AccessList(src)
@ -69,13 +76,15 @@ impl Eip2930TransactionRequest {
let mut rlp = RlpStream::new();
rlp.begin_list(NUM_EIP2930_FIELDS + 3);
rlp.append(&chain_id.into());
let chain_id = chain_id.into();
rlp.append(&chain_id);
self.tx.rlp_base(&mut rlp);
// append the access list in addition to the base rlp encoding
rlp.append(&self.access_list);
// append the signature
rlp.append(&signature.v);
let v = normalize_v(signature.v, chain_id);
rlp.append(&v);
rlp.append(&signature.r);
rlp.append(&signature.s);
rlp.out().freeze().into()
@ -104,8 +113,6 @@ mod tests {
let hash = tx.sighash(1);
let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap();
let enc = tx.rlp_signed(1, &sig);
assert_eq!(
hash,
"49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3"
@ -113,6 +120,7 @@ mod tests {
.unwrap()
);
let enc = rlp::encode(&tx.rlp_signed(1, &sig).as_ref());
let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521";
assert_eq!(hex::encode(enc.to_vec()), expected);
}

View File

@ -21,3 +21,12 @@ pub(super) fn rlp_opt<T: rlp::Encodable>(rlp: &mut rlp::RlpStream, opt: &Option<
rlp.append(&"");
}
}
/// normalizes the signature back to 0/1
pub(crate) fn normalize_v(v: u64, chain_id: crate::types::U64) -> u64 {
if v > 1 {
v - chain_id.as_u64() * 2 - 35
} else {
v
}
}

View File

@ -1,4 +1,5 @@
mod geometric;
use ethers_core::types::transaction::eip2718::TypedTransaction;
pub use geometric::GeometricGasPrice;
mod linear;
@ -82,17 +83,25 @@ where
&self.inner
}
async fn send_transaction(
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
tx: TransactionRequest,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
let tx = tx.into();
let pending_tx = self
.inner()
.send_transaction(tx.clone(), block)
.await
.map_err(GasEscalatorError::MiddlewareError)?;
let tx = match tx {
TypedTransaction::Legacy(inner) => inner,
TypedTransaction::Eip2930(inner) => inner.tx,
_ => return Err(GasEscalatorError::UnsupportedTxType),
};
// insert the tx in the pending txs
let mut lock = self.txs.lock().await;
lock.push((*pending_tx, tx, Instant::now(), block));
@ -231,4 +240,7 @@ pub enum GasEscalatorError<M: Middleware> {
#[error("{0}")]
/// Thrown when an internal middleware errors
MiddlewareError(M::Error),
#[error("Gas escalation is only supported for EIP2930 or Legacy transactions")]
UnsupportedTxType,
}

View File

@ -21,7 +21,7 @@ pub struct EthGasStation {
#[derive(Deserialize)]
struct EthGasStationResponse {
#[serde(rename = "safeLow")]
safe_low: u64,
safe_low: f64,
average: u64,
fast: u64,
fastest: u64,
@ -63,7 +63,7 @@ impl GasOracle for EthGasStation {
.await?;
let gas_price = match self.gas_category {
GasCategory::SafeLow => U256::from((res.safe_low * GWEI_TO_WEI) / 10),
GasCategory::SafeLow => U256::from((res.safe_low.ceil() as u64 * GWEI_TO_WEI) / 10),
GasCategory::Standard => U256::from((res.average * GWEI_TO_WEI) / 10),
GasCategory::Fast => U256::from((res.fast * GWEI_TO_WEI) / 10),
GasCategory::Fastest => U256::from((res.fastest * GWEI_TO_WEI) / 10),

View File

@ -1,6 +1,6 @@
use super::{GasOracle, GasOracleError};
use async_trait::async_trait;
use ethers_core::types::*;
use ethers_core::types::{transaction::eip2718::TypedTransaction, *};
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use thiserror::Error;
@ -28,6 +28,9 @@ pub enum MiddlewareError<M: Middleware> {
#[error("{0}")]
MiddlewareError(M::Error),
#[error("This gas price oracle only works with Legacy and EIP2930 transactions.")]
UnsupportedTxType,
}
impl<M: Middleware> FromErr<M::Error> for MiddlewareError<M> {
@ -56,14 +59,28 @@ where
Ok(self.gas_oracle.fetch().await?)
}
async fn send_transaction(
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
mut tx: TransactionRequest,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
if tx.gas_price.is_none() {
tx.gas_price = Some(self.get_gas_price().await?);
}
let mut tx = tx.into();
match tx {
TypedTransaction::Legacy(ref mut tx) => {
if tx.gas_price.is_none() {
tx.gas_price = Some(self.get_gas_price().await?);
}
}
TypedTransaction::Eip2930(ref mut inner) => {
if inner.tx.gas_price.is_none() {
inner.tx.gas_price = Some(self.get_gas_price().await?);
}
}
TypedTransaction::Eip1559(_) => {
return Err(MiddlewareError::UnsupportedTxType);
}
};
self.inner
.send_transaction(tx, block)
.await

View File

@ -1,4 +1,5 @@
use async_trait::async_trait;
use ethers_core::types::transaction::eip2718::TypedTransaction;
use ethers_core::types::*;
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
@ -84,17 +85,18 @@ where
/// 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(
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
mut tx: TransactionRequest,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
if tx.nonce.is_none() {
tx.nonce = Some(self.get_transaction_count_with_manager(block).await?);
let mut tx = tx.into();
if tx.nonce().is_none() {
tx.set_nonce(self.get_transaction_count_with_manager(block).await?);
}
let mut tx_clone = tx.clone();
match self.inner.send_transaction(tx, block).await {
match self.inner.send_transaction(tx.clone(), block).await {
Ok(tx_hash) => Ok(tx_hash),
Err(err) => {
let nonce = self.get_transaction_count(self.address, block).await?;
@ -102,9 +104,9 @@ where
// 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);
tx.set_nonce(nonce);
self.inner
.send_transaction(tx_clone, block)
.send_transaction(tx, block)
.await
.map_err(FromErr::from)
} else {

View File

@ -1,10 +1,10 @@
use ethers_core::types::{Address, BlockId, Bytes, NameOrAddress, Signature, TransactionRequest};
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use ethers_core::types::{
transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Signature,
};
use ethers_providers::{maybe, FromErr, Middleware, PendingTransaction};
use ethers_signers::Signer;
use async_trait::async_trait;
use futures_util::{future::ok, join};
use std::future::Future;
use thiserror::Error;
#[derive(Clone, Debug)]
@ -118,7 +118,7 @@ where
/// Signs and returns the RLP encoding of the signed transaction
async fn sign_transaction(
&self,
tx: TransactionRequest,
tx: TypedTransaction,
) -> Result<Bytes, SignerMiddlewareError<M, S>> {
let signature = self
.signer
@ -127,33 +127,7 @@ where
.map_err(SignerMiddlewareError::SignerError)?;
// Return the raw rlp-encoded signed transaction
Ok(tx.rlp_signed(&signature))
}
async fn fill_transaction(
&self,
tx: &mut TransactionRequest,
block: Option<BlockId>,
) -> Result<(), SignerMiddlewareError<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(SignerMiddlewareError::MiddlewareError)?);
tx.gas = Some(gas.map_err(SignerMiddlewareError::MiddlewareError)?);
tx.nonce = Some(nonce.map_err(SignerMiddlewareError::MiddlewareError)?);
Ok(())
Ok(tx.rlp_signed(self.signer.chain_id(), &signature))
}
/// Returns the client's address
@ -192,21 +166,54 @@ where
&self.inner
}
/// Returns the client's address
fn default_sender(&self) -> Option<Address> {
Some(self.address)
}
/// `SignerMiddleware` is instantiated with a signer.
async fn is_signer(&self) -> bool {
true
}
/// Helper for filling a transaction's nonce using the wallet
async fn fill_transaction(
&self,
tx: &mut TypedTransaction,
block: Option<BlockId>,
) -> Result<(), Self::Error> {
// get the `from` field's nonce if it's set, else get the signer's nonce
let from = if tx.from().is_some() && tx.from() != Some(&self.address()) {
*tx.from().unwrap()
} else {
self.address
};
tx.set_from(from);
let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?;
tx.set_nonce(nonce);
self.inner()
.fill_transaction(tx, block)
.await
.map_err(SignerMiddlewareError::MiddlewareError)?;
Ok(())
}
/// 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(
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
mut tx: TransactionRequest,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
let mut tx = tx.into();
// fill any missing fields
self.fill_transaction(&mut tx, block).await?;
// If the from address is set and is not our signer, delegate to inner
if tx.from.is_some() && tx.from != Some(self.address()) {
if tx.from().is_some() && tx.from() != Some(&self.address()) {
return self
.inner
.send_transaction(tx, block)
@ -214,18 +221,6 @@ where
.map_err(SignerMiddlewareError::MiddlewareError);
}
if let Some(NameOrAddress::Name(ens_name)) = tx.to {
let addr = self
.inner
.resolve_name(&ens_name)
.await
.map_err(SignerMiddlewareError::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?;
@ -251,23 +246,14 @@ where
}
}
/// 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::LocalWallet};
use ethers_core::utils::{self, keccak256, Ganache};
use ethers_core::{
types::TransactionRequest,
utils::{self, keccak256, Ganache},
};
use std::convert::TryFrom;
#[tokio::test]
@ -287,7 +273,8 @@ mod tests {
nonce: Some(0.into()),
gas_price: Some(21_000_000_000u128.into()),
data: None,
};
}
.into();
let chain_id = 1u64;
let provider = Provider::try_from("http://localhost:8545").unwrap();

View File

@ -3,7 +3,9 @@ use factory::{CreatedFilter, DsProxyFactory, ADDRESS_BOOK};
use super::{Transformer, TransformerError};
use ethers_contract::{builders::ContractCall, BaseContract, ContractError};
use ethers_core::{abi::parse_abi, types::*, utils::id};
use ethers_core::{
abi::parse_abi, types::transaction::eip2718::TypedTransaction, types::*, utils::id,
};
use ethers_providers::Middleware;
use std::sync::Arc;
@ -178,29 +180,26 @@ impl DsProxy {
}
impl Transformer for DsProxy {
fn transform(&self, tx: TransactionRequest) -> Result<TransactionRequest, TransformerError> {
// clone the tx into a new proxy tx.
let mut proxy_tx = tx.clone();
fn transform(&self, tx: &mut TypedTransaction) -> Result<(), TransformerError> {
// the target address cannot be None.
let target = match tx.to {
Some(NameOrAddress::Address(addr)) => Ok(addr),
_ => Err(TransformerError::MissingField("to".into())),
}?;
let target = match tx.to() {
Some(NameOrAddress::Address(addr)) => addr,
_ => return Err(TransformerError::MissingField("to".into())),
};
// fetch the data field.
let data = tx.data.unwrap_or_else(|| vec![].into());
let data = tx.data().cloned().unwrap_or_else(|| vec![].into());
// encode data as the ABI encoded data for DSProxy's execute method.
let selector = id("execute(address,bytes)");
let encoded_data = self
.contract
.encode_with_selector(selector, (target, data))?;
.encode_with_selector(selector, (*target, data))?;
// update appropriate fields of the proxy tx.
proxy_tx.data = Some(encoded_data);
proxy_tx.to = Some(NameOrAddress::Address(self.address));
tx.set_data(encoded_data);
tx.set_to(self.address);
Ok(proxy_tx)
Ok(())
}
}

View File

@ -1,6 +1,6 @@
use super::{Transformer, TransformerError};
use async_trait::async_trait;
use ethers_core::types::*;
use ethers_core::types::{transaction::eip2718::TypedTransaction, *};
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use thiserror::Error;
@ -53,27 +53,20 @@ where
&self.inner
}
async fn send_transaction(
async fn send_transaction<Tx: Into<TypedTransaction> + Send + Sync>(
&self,
mut tx: TransactionRequest,
tx: Tx,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
// resolve the to field if that's an ENS name.
if let Some(NameOrAddress::Name(ens_name)) = tx.to {
let addr = self
.inner
.resolve_name(&ens_name)
.await
.map_err(TransformerMiddlewareError::MiddlewareError)?;
tx.to = Some(addr.into())
}
let mut tx = tx.into();
// construct the appropriate proxy tx.
let proxy_tx = self.transformer.transform(tx)?;
self.transformer.transform(&mut tx)?;
self.fill_transaction(&mut tx, block).await?;
// send the proxy tx.
self.inner
.send_transaction(proxy_tx, block)
.send_transaction(tx, block)
.await
.map_err(TransformerMiddlewareError::MiddlewareError)
}

View File

@ -5,7 +5,7 @@ mod middleware;
pub use middleware::TransformerMiddleware;
use ethers_contract::AbiError;
use ethers_core::{abi::ParseError, types::*};
use ethers_core::{abi::ParseError, types::transaction::eip2718::TypedTransaction};
use thiserror::Error;
#[derive(Error, Debug)]
@ -31,5 +31,5 @@ pub trait Transformer: Send + Sync + std::fmt::Debug {
/// proxy contract.
///
/// [`transaction request`]: struct@ethers_core::types::TransactionRequest
fn transform(&self, tx: TransactionRequest) -> Result<TransactionRequest, TransformerError>;
fn transform(&self, tx: &mut TypedTransaction) -> Result<(), TransformerError>;
}

View File

@ -33,6 +33,7 @@ async fn using_gas_oracle() {
#[tokio::test]
#[ignore]
// TODO: Re-enable, EthGasStation changed its response api @ https://ethgasstation.info/api/ethgasAPI.json
async fn eth_gas_station() {
// initialize and fetch gas estimates from EthGasStation
let eth_gas_station_oracle = EthGasStation::new(None);
@ -60,6 +61,8 @@ async fn etherscan() {
#[tokio::test]
#[ignore]
// TODO: Etherchain has Cloudflare DDOS protection which makes the request fail
// https://twitter.com/gakonst/status/1421796226316578816
async fn etherchain() {
// initialize and fetch gas estimates from Etherchain
let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);

View File

@ -83,7 +83,8 @@ pub use pubsub::{PubsubClient, SubscriptionStream};
use async_trait::async_trait;
use auto_impl::auto_impl;
use serde::{de::DeserializeOwned, Serialize};
use ethers_core::types::transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{error::Error, fmt::Debug, future::Future, pin::Pin};
pub use provider::{FilterKind, Provider, ProviderError};
@ -112,6 +113,18 @@ pub trait FromErr<T> {
fn from(src: T) -> Self;
}
/// Calls the future if `item` is None, otherwise returns a `futures::ok`
pub 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 {
futures_util::future::ok(item).await
} else {
f.await
}
}
#[async_trait]
#[auto_impl(&, Box, Arc)]
/// A middleware allows customizing requests send and received from an ethereum node.
@ -122,7 +135,7 @@ pub trait FromErr<T> {
/// 3. implementing any of the methods you want to override
///
/// ```rust
/// use ethers::{providers::{Middleware, FromErr}, types::{U64, TransactionRequest, U256}};
/// use ethers::{providers::{Middleware, FromErr}, types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction}};
/// use thiserror::Error;
/// use async_trait::async_trait;
///
@ -163,7 +176,7 @@ pub trait FromErr<T> {
///
/// /// Overrides the default `estimate_gas` method to log that it was called,
/// /// before forwarding the call to the next layer.
/// async fn estimate_gas(&self, tx: &TransactionRequest) -> Result<U256, Self::Error> {
/// async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
/// println!("Estimating gas...");
/// self.inner().estimate_gas(tx).await.map_err(FromErr::from)
/// }
@ -182,17 +195,90 @@ pub trait Middleware: Sync + Send + Debug {
self.inner().provider()
}
fn default_sender(&self) -> Option<Address> {
self.inner().default_sender()
}
async fn client_version(&self) -> Result<String, Self::Error> {
self.inner().client_version().await.map_err(FromErr::from)
}
/// Helper for filling a transaction
async fn fill_transaction(
&self,
tx: &mut TypedTransaction,
block: Option<BlockId>,
) -> Result<(), Self::Error> {
let tx_clone = tx.clone();
// TODO: Maybe deduplicate the code in a nice way
match tx {
TypedTransaction::Legacy(ref mut inner) => {
if let Some(NameOrAddress::Name(ref ens_name)) = inner.to {
let addr = self.resolve_name(ens_name).await?;
inner.to = Some(addr.into());
};
if inner.from.is_none() {
inner.from = self.default_sender();
}
let (gas_price, gas) = futures_util::try_join!(
maybe(inner.gas_price, self.get_gas_price()),
maybe(inner.gas, self.estimate_gas(&tx_clone)),
)?;
inner.gas = Some(gas);
inner.gas_price = Some(gas_price);
}
TypedTransaction::Eip2930(inner) => {
if let Ok(lst) = self.create_access_list(&tx_clone, block).await {
inner.access_list = lst.access_list;
}
if let Some(NameOrAddress::Name(ref ens_name)) = inner.tx.to {
let addr = self.resolve_name(ens_name).await?;
inner.tx.to = Some(addr.into());
};
let (gas_price, gas) = futures_util::try_join!(
maybe(inner.tx.gas_price, self.get_gas_price()),
maybe(inner.tx.gas, self.estimate_gas(&tx_clone)),
)?;
inner.tx.gas = Some(gas);
inner.tx.gas_price = Some(gas_price);
}
TypedTransaction::Eip1559(inner) => {
if let Ok(lst) = self.create_access_list(&tx_clone, block).await {
inner.access_list = lst.access_list;
}
if let Some(NameOrAddress::Name(ref ens_name)) = inner.to {
let addr = self.resolve_name(ens_name).await?;
inner.to = Some(addr.into());
};
let (max_priority_fee_per_gas, max_fee_per_gas, gas) = futures_util::try_join!(
// TODO: Replace with algorithms using eth_feeHistory
maybe(inner.max_priority_fee_per_gas, self.get_gas_price()),
maybe(inner.max_fee_per_gas, self.get_gas_price()),
maybe(inner.gas, self.estimate_gas(&tx_clone)),
)?;
inner.gas = Some(gas);
inner.max_fee_per_gas = Some(max_fee_per_gas);
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
}
};
Ok(())
}
async fn get_block_number(&self) -> Result<U64, Self::Error> {
self.inner().get_block_number().await.map_err(FromErr::from)
}
async fn send_transaction(
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
tx: TransactionRequest,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
self.inner()
@ -246,13 +332,13 @@ pub trait Middleware: Sync + Send + Debug {
.map_err(FromErr::from)
}
async fn estimate_gas(&self, tx: &TransactionRequest) -> Result<U256, Self::Error> {
async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
self.inner().estimate_gas(tx).await.map_err(FromErr::from)
}
async fn call(
&self,
tx: &TransactionRequest,
tx: &TypedTransaction,
block: Option<BlockId>,
) -> Result<Bytes, Self::Error> {
self.inner().call(tx, block).await.map_err(FromErr::from)
@ -427,9 +513,9 @@ pub trait Middleware: Sync + Send + Debug {
// Parity `trace` support
/// Executes the given call and returns a number of possible traces for it
async fn trace_call(
async fn trace_call<T: Into<TypedTransaction> + Send + Sync>(
&self,
req: TransactionRequest,
req: T,
trace_type: Vec<TraceType>,
block: Option<BlockNumber>,
) -> Result<BlockTrace, Self::Error> {
@ -574,6 +660,38 @@ pub trait Middleware: Sync + Send + Debug {
.await
.map_err(FromErr::from)
}
async fn fee_history(
&self,
block_count: u64,
last_block: BlockNumber,
reward_percentiles: &[f64],
) -> Result<FeeHistory, Self::Error> {
self.inner()
.fee_history(block_count, last_block, reward_percentiles)
.await
.map_err(FromErr::from)
}
async fn create_access_list(
&self,
tx: &TypedTransaction,
block: Option<BlockId>,
) -> Result<AccessListWithGasUsed, Self::Error> {
self.inner()
.create_access_list(tx, block)
.await
.map_err(FromErr::from)
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FeeHistory {
pub base_fee_per_gas: Vec<U256>,
pub gas_used_ratio: Vec<f64>,
pub oldest_block: u64,
pub reward: Vec<Vec<U256>>,
}
#[cfg(feature = "celo")]

View File

@ -2,15 +2,16 @@ use crate::{
ens,
pubsub::{PubsubClient, SubscriptionStream},
stream::{FilterWatcher, DEFAULT_POLL_INTERVAL},
FromErr, Http as HttpProvider, JsonRpcClient, MockProvider, PendingTransaction,
FeeHistory, FromErr, Http as HttpProvider, JsonRpcClient, MockProvider, PendingTransaction,
};
use ethers_core::{
abi::{self, Detokenize, ParamType},
types::{
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, Filter, Log, NameOrAddress,
Selector, Signature, Trace, TraceFilter, TraceType, Transaction, TransactionReceipt,
TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus, H256, U256, U64,
TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus, H256, U256, U64,
},
utils,
};
@ -192,6 +193,10 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
self
}
fn default_sender(&self) -> Option<Address> {
self.from
}
////// Blockchain Status
//
// Functions for querying the state of the blockchain
@ -307,7 +312,7 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
/// This is free, since it does not change any state on the blockchain.
async fn call(
&self,
tx: &TransactionRequest,
tx: &TypedTransaction,
block: Option<BlockId>,
) -> Result<Bytes, ProviderError> {
let tx = utils::serialize(tx);
@ -318,33 +323,29 @@ impl<P: JsonRpcClient> Middleware for 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).
async fn estimate_gas(&self, tx: &TransactionRequest) -> Result<U256, ProviderError> {
async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, ProviderError> {
self.request("eth_estimateGas", [tx]).await
}
async fn create_access_list(
&self,
tx: &TypedTransaction,
block: Option<BlockId>,
) -> Result<AccessListWithGasUsed, ProviderError> {
let tx = utils::serialize(tx);
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
self.request("eth_createAccessList", [tx, block]).await
}
/// 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.
async fn send_transaction(
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
mut tx: TransactionRequest,
_: Option<BlockId>,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, P>, ProviderError> {
if tx.from.is_none() {
tx.from = self.from;
}
if tx.gas.is_none() {
tx.gas = Some(self.estimate_gas(&tx).await?);
}
if let Some(NameOrAddress::Name(ref ens_name)) = tx.to {
// resolve to an address
let addr = self.resolve_name(ens_name).await?;
// set the value
tx.to = Some(addr.into())
}
let mut tx = tx.into();
self.fill_transaction(&mut tx, block).await?;
let tx_hash = self.request("eth_sendTransaction", [tx]).await?;
Ok(PendingTransaction::new(tx_hash, self).interval(self.get_interval()))
@ -557,12 +558,13 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
}
/// Executes the given call and returns a number of possible traces for it
async fn trace_call(
async fn trace_call<T: Into<TypedTransaction> + Send + Sync>(
&self,
req: TransactionRequest,
req: T,
trace_type: Vec<TraceType>,
block: Option<BlockNumber>,
) -> Result<BlockTrace, ProviderError> {
let req = req.into();
let req = utils::serialize(&req);
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
let trace_type = utils::serialize(&trace_type);
@ -694,6 +696,22 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
let filter = utils::serialize(filter);
self.subscribe([logs, filter]).await
}
async fn fee_history(
&self,
block_count: u64,
last_block: BlockNumber,
reward_percentiles: &[f64],
) -> Result<FeeHistory, Self::Error> {
let block_count = utils::serialize(&block_count);
let last_block = utils::serialize(&last_block);
let reward_percentiles = utils::serialize(&reward_percentiles);
self.request(
"eth_feeHistory",
[block_count, last_block, reward_percentiles],
)
.await
}
}
impl<P: JsonRpcClient> Provider<P> {
@ -709,7 +727,7 @@ impl<P: JsonRpcClient> Provider<P> {
// first get the resolver responsible for this name
// the call will return a Bytes array which we convert to an address
let data = self
.call(&ens::get_resolver(ens_addr, ens_name), None)
.call(&ens::get_resolver(ens_addr, ens_name).into(), None)
.await?;
let resolver_address: Address = decode_bytes(ParamType::Address, data);
@ -719,7 +737,10 @@ impl<P: JsonRpcClient> Provider<P> {
// resolve
let data = self
.call(&ens::resolve(resolver_address, selector, ens_name), None)
.call(
&ens::resolve(resolver_address, selector, ens_name).into(),
None,
)
.await?;
Ok(decode_bytes(param, data))
@ -885,7 +906,7 @@ mod ens_tests {
mod tests {
use super::*;
use crate::Http;
use ethers_core::types::H256;
use ethers_core::types::{TransactionRequest, H256};
use ethers_core::utils::Geth;
use futures_util::StreamExt;
@ -1020,4 +1041,19 @@ mod tests {
.await;
assert_eq!(blocks, vec![1, 2, 3]);
}
#[tokio::test]
#[cfg_attr(feature = "celo", ignore)]
async fn fee_history() {
let provider = Provider::<Http>::try_from(
"https://goerli.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778",
)
.unwrap();
let history = provider
.fee_history(10, BlockNumber::Latest, &[10.0, 40.0])
.await
.unwrap();
dbg!(&history);
}
}

View File

@ -6,6 +6,7 @@ mod eth_tests {
use super::*;
use ethers::{
middleware::SignerMiddleware,
prelude::transaction::eip2718::TypedTransaction,
signers::{LocalWallet, Signer},
types::{BlockId, TransactionRequest, H256},
utils::Ganache,
@ -148,6 +149,56 @@ mod eth_tests {
// got the correct receipt
assert_eq!(receipt.transaction_hash, tx_hash);
}
#[tokio::test]
async fn typed_txs() {
use ethers_core::types::Eip1559TransactionRequest;
let provider = Provider::<Http>::try_from(
"https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
)
.unwrap();
let chain_id = provider.get_chainid().await.unwrap();
let wallet = "59c37cb6b16fa2de30675f034c8008f890f4b2696c729d6267946d29736d73e4"
.parse::<LocalWallet>()
.unwrap()
.with_chain_id(chain_id.as_u64());
let address = wallet.address();
let provider = SignerMiddleware::new(provider, wallet);
async fn check_tx<M: Middleware>(provider: &M, tx: TypedTransaction, expected: u64) {
let receipt = provider
.send_transaction(tx, None)
.await
.unwrap()
.await
.unwrap()
.unwrap();
let tx = provider
.get_transaction(receipt.transaction_hash)
.await
.unwrap()
.unwrap();
assert_eq!(receipt.transaction_type, Some(expected.into()));
assert_eq!(tx.transaction_type, Some(expected.into()));
}
let tx: TypedTransaction = TransactionRequest::new().from(address).to(address).into();
check_tx(&provider, tx, 0).await;
let tx: TypedTransaction = TransactionRequest::new()
.from(address)
.to(address)
.with_access_list(vec![])
.into();
check_tx(&provider, tx, 1).await;
let tx: TypedTransaction = Eip1559TransactionRequest::new()
.from(address)
.to(address)
.into();
check_tx(&provider, tx, 2).await;
}
}
#[cfg(feature = "celo")]

View File

@ -2,7 +2,7 @@
use ethers_core::{
k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey},
types::{Address, Signature as EthSig, TransactionRequest, H256},
types::{Address, Signature as EthSig, H256, transaction::eip2718::TypedTransaction},
utils::hash_message,
};
use rusoto_core::RusotoError;
@ -240,7 +240,7 @@ impl<'a> super::Signer for AwsSigner<'a> {
}
#[instrument(err)]
async fn sign_transaction(&self, tx: &TransactionRequest) -> Result<EthSig, Self::Error> {
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<EthSig, Self::Error> {
let sighash = tx.sighash(self.chain_id);
self.sign_digest_with_eip155(sighash).await
}

View File

@ -8,7 +8,8 @@ use futures_util::lock::Mutex;
use ethers_core::{
types::{
Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256,
transaction::eip2718::TypedTransaction, Address, NameOrAddress, Signature, Transaction,
TransactionRequest, TxHash, H256, U256,
},
utils::keccak256,
};
@ -118,7 +119,7 @@ impl LedgerEthereum {
}
/// Signs an Ethereum transaction (requires confirmation on the ledger)
pub async fn sign_tx(&self, tx: &TransactionRequest) -> Result<Signature, LedgerError> {
pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> {
let mut payload = Self::path_to_bytes(&self.derivation);
payload.extend_from_slice(tx.rlp(self.chain_id).as_ref());
self.sign_payload(INS::SIGN, payload).await
@ -241,7 +242,8 @@ mod tests {
.gas_price(400e9 as u64)
.nonce(5)
.data(data)
.value(ethers_core::utils::parse_ether(100).unwrap());
.value(ethers_core::utils::parse_ether(100).unwrap())
.into();
let tx = ledger.sign_transaction(&tx_req).await.unwrap();
}

View File

@ -4,7 +4,7 @@ pub mod types;
use crate::Signer;
use app::LedgerEthereum;
use async_trait::async_trait;
use ethers_core::types::{Address, Signature, TransactionRequest};
use ethers_core::types::{transaction::eip2718::TypedTransaction, Address, Signature};
use types::LedgerError;
#[async_trait]
@ -20,10 +20,7 @@ impl Signer for LedgerEthereum {
}
/// Signs the transaction
async fn sign_transaction(
&self,
message: &TransactionRequest,
) -> Result<Signature, Self::Error> {
async fn sign_transaction(&self, message: &TypedTransaction) -> Result<Signature, Self::Error> {
self.sign_tx(message).await
}

View File

@ -25,7 +25,7 @@
//! // create a transaction
//! let tx = TransactionRequest::new()
//! .to("vitalik.eth") // this will use ENS
//! .value(10000);
//! .value(10000).into();
//!
//! // sign it
//! let signature = wallet.sign_transaction(&tx).await?;
@ -70,7 +70,7 @@ mod aws;
pub use aws::{AwsSigner, AwsSignerError};
use async_trait::async_trait;
use ethers_core::types::{Address, Signature, TransactionRequest};
use ethers_core::types::{transaction::eip2718::TypedTransaction, Address, Signature};
use std::error::Error;
/// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md)
@ -91,10 +91,7 @@ pub trait Signer: std::fmt::Debug + Send + Sync {
) -> Result<Signature, Self::Error>;
/// Signs the transaction
async fn sign_transaction(
&self,
message: &TransactionRequest,
) -> Result<Signature, Self::Error>;
async fn sign_transaction(&self, message: &TypedTransaction) -> Result<Signature, Self::Error>;
/// Returns the signer's Ethereum Address
fn address(&self) -> Address;

View File

@ -16,7 +16,7 @@ use ethers_core::{
elliptic_curve::FieldBytes,
Secp256k1,
},
types::{Address, Signature, TransactionRequest, H256},
types::{transaction::eip2718::TypedTransaction, Address, Signature, H256},
utils::hash_message,
};
use hash::Sha256Proxy;
@ -78,7 +78,7 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
Ok(self.sign_hash(message_hash, false))
}
async fn sign_transaction(&self, tx: &TransactionRequest) -> Result<Signature, Self::Error> {
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Self::Error> {
let sighash = tx.sighash(self.chain_id);
Ok(self.sign_hash(sighash, true))
}

View File

@ -217,7 +217,8 @@ mod tests {
nonce: Some(0.into()),
gas_price: Some(21_000_000_000u128.into()),
data: None,
};
}
.into();
let chain_id = 1u64;
let wallet: Wallet<SigningKey> =