From dfdaf0f7cbbfa583d96a990761791f2188fbb02a Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:29:51 -0400 Subject: [PATCH 1/2] feat(layers): FillTxLayer * Clone bound on Provider for ProviderBuilder * new method to nonce and gas provider layers --- crates/provider/src/builder.rs | 10 +- crates/provider/src/layers/fill_tx.rs | 213 ++++++++++++++++++++++++++ crates/provider/src/layers/gas.rs | 20 ++- crates/provider/src/layers/mod.rs | 3 + crates/provider/src/layers/nonce.rs | 16 +- crates/provider/src/layers/signer.rs | 8 +- 6 files changed, 250 insertions(+), 20 deletions(-) create mode 100644 crates/provider/src/layers/fill_tx.rs diff --git a/crates/provider/src/builder.rs b/crates/provider/src/builder.rs index 98c7591a5dd..5165042a254 100644 --- a/crates/provider/src/builder.rs +++ b/crates/provider/src/builder.rs @@ -7,9 +7,9 @@ use std::marker::PhantomData; /// A layering abstraction in the vein of [`tower::Layer`] /// /// [`tower::Layer`]: https://docs.rs/tower/latest/tower/trait.Layer.html -pub trait ProviderLayer, N: Network, T: Transport + Clone> { +pub trait ProviderLayer + Clone, N: Network, T: Transport + Clone> { /// The provider constructed by this layer. - type Provider: Provider; + type Provider: Provider + Clone; /// Wrap the given provider in the layer's provider. fn layer(&self, inner: P) -> Self::Provider; @@ -23,7 +23,7 @@ impl ProviderLayer for Identity where T: Transport + Clone, N: Network, - P: Provider, + P: Provider + Clone, { type Provider = P; @@ -50,7 +50,7 @@ impl ProviderLayer for Stack where T: Transport + Clone, N: Network, - P: Provider, + P: Provider + Clone, Inner: ProviderLayer, Outer: ProviderLayer, { @@ -128,7 +128,7 @@ impl ProviderBuilder { pub fn provider(self, provider: P) -> L::Provider where L: ProviderLayer, - P: Provider, + P: Provider + Clone, T: Transport + Clone, N: Network, { diff --git a/crates/provider/src/layers/fill_tx.rs b/crates/provider/src/layers/fill_tx.rs new file mode 100644 index 00000000000..248596667fe --- /dev/null +++ b/crates/provider/src/layers/fill_tx.rs @@ -0,0 +1,213 @@ +use crate::{ + layers::{GasEstimatorProvider, ManagedNonceProvider}, + PendingTransactionBuilder, Provider, ProviderLayer, RootProvider, +}; +use alloy_network::{Network, TransactionBuilder}; +use alloy_transport::{Transport, TransportError, TransportErrorKind, TransportResult}; +use async_trait::async_trait; +use futures::FutureExt; +use std::marker::PhantomData; + +/// A layer that fills in missing transaction fields. +#[derive(Debug, Clone, Copy)] +pub struct FillTxLayer; + +impl ProviderLayer for FillTxLayer +where + P: Provider + Clone, + N: Network, + T: Transport + Clone, +{ + type Provider = FillTxProvider; + + fn layer(&self, inner: P) -> Self::Provider { + let nonce_provider = ManagedNonceProvider::new(inner.clone()); + let gas_estimation_provider = GasEstimatorProvider::new(inner.clone()); + FillTxProvider { inner, nonce_provider, gas_estimation_provider, _phantom: PhantomData } + } +} + +/// A provider that fills in missing transaction fields. +#[derive(Debug, Clone)] +pub struct FillTxProvider +where + N: Network, + T: Transport + Clone, + P: Provider + Clone, +{ + inner: P, + nonce_provider: ManagedNonceProvider, + gas_estimation_provider: GasEstimatorProvider, + _phantom: PhantomData<(N, T)>, +} + +impl FillTxProvider +where + N: Network, + T: Transport + Clone, + P: Provider + Clone, +{ + /// Fills in missing transaction fields. + pub async fn fill_tx(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { + let chain_id_fut = if let Some(chain_id) = tx.chain_id() { + async move { Ok(chain_id) }.left_future() + } else { + async move { self.inner.get_chain_id().await.map(|ci| ci.to::()) }.right_future() + }; + + // Check if `from` is set + if tx.nonce().is_none() && tx.from().is_none() { + return Err(TransportError::Transport(TransportErrorKind::Custom( + "`from` field must be set in transaction request to populate the `nonce` field" + .into(), + ))); + } + + let nonce_fut = if let Some(nonce) = tx.nonce() { + async move { Ok(nonce) }.left_future() + } else { + let from = tx.from().unwrap(); + async move { self.nonce_provider.get_next_nonce(from).await }.right_future() + }; + + let gas_estimation_fut = if tx.gas_price().is_none() { + async { self.gas_estimation_provider.handle_eip1559_tx(tx).await }.left_future() + } else { + async { self.gas_estimation_provider.handle_legacy_tx(tx).await }.right_future() + }; + + match futures::try_join!(chain_id_fut, nonce_fut, gas_estimation_fut) { + Ok((chain_id, nonce, _)) => { + tx.set_chain_id(chain_id); + tx.set_nonce(nonce); + Ok(()) + } + Err(e) => Err(e), + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Provider for FillTxProvider +where + N: Network, + T: Transport + Clone, + P: Provider + Clone, +{ + #[inline] + fn root(&self) -> &RootProvider { + self.inner.root() + } + + async fn send_transaction( + &self, + mut tx: N::TransactionRequest, + ) -> TransportResult> { + self.fill_tx(&mut tx).await?; + self.inner.send_transaction(tx).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ProviderBuilder; + use alloy_network::EthereumSigner; + use alloy_node_bindings::Anvil; + use alloy_primitives::{address, U256}; + use alloy_rpc_client::RpcClient; + use alloy_rpc_types::TransactionRequest; + use alloy_transport_http::Http; + use reqwest::Client; + + #[tokio::test] + async fn test_1559_tx_no_nonce_no_chain_id() { + let anvil = Anvil::new().spawn(); + let url = anvil.endpoint_url(); + + let http = Http::::new(url); + + let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); + + let provider = ProviderBuilder::new() + .layer(FillTxLayer) + .signer(EthereumSigner::from(wallet)) + .provider(RootProvider::new(RpcClient::new(http, true))); + + let tx = TransactionRequest { + from: Some(anvil.addresses()[0]), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + ..Default::default() + }; + + let tx = provider.send_transaction(tx).await.unwrap(); + + let tx = provider.get_transaction_by_hash(tx.tx_hash().to_owned()).await.unwrap(); + + assert_eq!(tx.max_fee_per_gas, Some(U256::from(0x77359400))); + assert_eq!(tx.max_priority_fee_per_gas, Some(U256::from(0x0))); + assert_eq!(tx.gas, U256::from(21000)); + assert_eq!(tx.nonce, 0); + assert_eq!(tx.chain_id, Some(31337)); + } + + #[tokio::test] + async fn test_legacy_tx_no_nonce_chain_id() { + let anvil = Anvil::new().spawn(); + let url = anvil.endpoint_url(); + + let http = Http::::new(url); + + let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); + + let provider = ProviderBuilder::new() + .layer(FillTxLayer) + .signer(EthereumSigner::from(wallet)) + .provider(RootProvider::new(RpcClient::new(http, true))); + + let gas_price = provider.get_gas_price().await.unwrap(); + let tx = TransactionRequest { + from: Some(anvil.addresses()[0]), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + chain_id: Some(31337), + gas_price: Some(gas_price), + ..Default::default() + }; + + let tx = provider.send_transaction(tx).await.unwrap(); + + let tx = provider.get_transaction_by_hash(tx.tx_hash().to_owned()).await.unwrap(); + + assert_eq!(tx.gas_price, Some(gas_price)); + assert_eq!(tx.gas, U256::from(21000)); + assert_eq!(tx.nonce, 0); + assert_eq!(tx.chain_id, Some(31337)); + } + + #[tokio::test] + #[should_panic] + async fn test_no_from() { + let anvil = Anvil::new().spawn(); + let url = anvil.endpoint_url(); + + let http = Http::::new(url); + + let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); + + let provider = ProviderBuilder::new() + .layer(FillTxLayer) + .signer(EthereumSigner::from(wallet)) + .provider(RootProvider::new(RpcClient::new(http, true))); + + let tx = TransactionRequest { + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + ..Default::default() + }; + + let _ = provider.send_transaction(tx).await.unwrap(); + } +} diff --git a/crates/provider/src/layers/gas.rs b/crates/provider/src/layers/gas.rs index cfcd3bede01..e9c6854bd1c 100644 --- a/crates/provider/src/layers/gas.rs +++ b/crates/provider/src/layers/gas.rs @@ -44,7 +44,7 @@ pub struct GasEstimatorLayer; impl ProviderLayer for GasEstimatorLayer where - P: Provider, + P: Provider + Clone, N: Network, T: Transport + Clone, { @@ -67,7 +67,7 @@ pub struct GasEstimatorProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { inner: P, _phantom: PhantomData<(N, T)>, @@ -77,8 +77,13 @@ impl GasEstimatorProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { + /// Creates a new GasEstimatorProvider. + pub(crate) const fn new(inner: P) -> Self { + Self { inner, _phantom: PhantomData } + } + /// Gets the gas_price to be used in legacy txs. async fn get_gas_price(&self) -> TransportResult { self.inner.get_gas_price().await @@ -97,7 +102,7 @@ where /// Populates the gas_limit, max_fee_per_gas and max_priority_fee_per_gas fields if unset. /// Requires the chain_id to be set in the transaction request to be processed as a EIP-1559 tx. /// If the network does not support EIP-1559, it will process it as a legacy tx. - async fn handle_eip1559_tx( + pub async fn handle_eip1559_tx( &self, tx: &mut N::TransactionRequest, ) -> Result<(), TransportError> { @@ -130,7 +135,10 @@ where /// Populates the gas_price and only populates the gas_limit field if unset. /// This method always assumes that the gas_price is unset. - async fn handle_legacy_tx(&self, tx: &mut N::TransactionRequest) -> Result<(), TransportError> { + pub async fn handle_legacy_tx( + &self, + tx: &mut N::TransactionRequest, + ) -> Result<(), TransportError> { let gas_price_fut = self.get_gas_price(); let gas_limit_fut = if let Some(gas_limit) = tx.gas_limit() { async move { Ok(gas_limit) }.left_future() @@ -154,7 +162,7 @@ impl Provider for GasEstimatorProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { fn root(&self) -> &RootProvider { self.inner.root() diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index e26e50a6b46..2d26bebba37 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -10,3 +10,6 @@ pub use nonce::{ManagedNonceLayer, ManagedNonceProvider}; mod gas; pub use gas::{GasEstimatorLayer, GasEstimatorProvider}; + +mod fill_tx; +pub use fill_tx::{FillTxLayer, FillTxProvider}; diff --git a/crates/provider/src/layers/nonce.rs b/crates/provider/src/layers/nonce.rs index cb57419b74e..40b016fe2b8 100644 --- a/crates/provider/src/layers/nonce.rs +++ b/crates/provider/src/layers/nonce.rs @@ -41,7 +41,7 @@ pub struct ManagedNonceLayer; impl ProviderLayer for ManagedNonceLayer where - P: Provider, + P: Provider + Clone, N: Network, T: Transport + Clone, { @@ -69,7 +69,7 @@ pub struct ManagedNonceProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { inner: P, nonces: DashMap>>>, @@ -80,9 +80,15 @@ impl ManagedNonceProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { - async fn get_next_nonce(&self, from: Address) -> TransportResult { + /// Creates a new ManagedNonceProvider. + pub(crate) fn new(inner: P) -> Self { + Self { inner, nonces: DashMap::default(), _phantom: PhantomData } + } + + /// Gets the next nonce for the given account. + pub async fn get_next_nonce(&self, from: Address) -> TransportResult { // locks dashmap internally for a short duration to clone the `Arc` let mutex = Arc::clone(self.nonces.entry(from).or_default().value()); @@ -109,7 +115,7 @@ impl Provider for ManagedNonceProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { #[inline] fn root(&self) -> &RootProvider { diff --git a/crates/provider/src/layers/signer.rs b/crates/provider/src/layers/signer.rs index 7d67140df93..9c0e3f357e2 100644 --- a/crates/provider/src/layers/signer.rs +++ b/crates/provider/src/layers/signer.rs @@ -38,7 +38,7 @@ impl SignerLayer { impl ProviderLayer for SignerLayer where - P: Provider, + P: Provider + Clone, N: Network, T: Transport + Clone, S: NetworkSigner + Clone, @@ -59,12 +59,12 @@ where /// You cannot construct this provider directly. Use [`ProviderBuilder`] with a [`SignerLayer`]. /// /// [`ProviderBuilder`]: crate::ProviderBuilder -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SignerProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, { inner: P, signer: S, @@ -77,7 +77,7 @@ impl Provider for SignerProvider where N: Network, T: Transport + Clone, - P: Provider, + P: Provider + Clone, S: NetworkSigner, { #[inline] From 39d0cba0b45d1a3c36dae22a2b6843893ca1dcd2 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 22 Mar 2024 20:19:23 -0400 Subject: [PATCH 2/2] nits --- crates/provider/src/layers/fill_tx.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/provider/src/layers/fill_tx.rs b/crates/provider/src/layers/fill_tx.rs index 248596667fe..5bf83f3000d 100644 --- a/crates/provider/src/layers/fill_tx.rs +++ b/crates/provider/src/layers/fill_tx.rs @@ -76,14 +76,11 @@ where async { self.gas_estimation_provider.handle_legacy_tx(tx).await }.right_future() }; - match futures::try_join!(chain_id_fut, nonce_fut, gas_estimation_fut) { - Ok((chain_id, nonce, _)) => { - tx.set_chain_id(chain_id); - tx.set_nonce(nonce); - Ok(()) - } - Err(e) => Err(e), - } + let (chain_id, nonce, _) = futures::try_join!(chain_id_fut, nonce_fut, gas_estimation_fut)?; + + tx.set_chain_id(chain_id); + tx.set_nonce(nonce); + Ok(()) } }