diff --git a/crates/alloy/Cargo.toml b/crates/alloy/Cargo.toml index f7d055d4e4e..5d072e7fc6a 100644 --- a/crates/alloy/Cargo.toml +++ b/crates/alloy/Cargo.toml @@ -94,7 +94,7 @@ contract = ["dep:alloy-contract", "dyn-abi", "json-abi", "json", "sol-types"] eips = ["dep:alloy-eips"] genesis = ["dep:alloy-genesis"] network = ["dep:alloy-network"] -node-bindings = ["dep:alloy-node-bindings"] +node-bindings = ["dep:alloy-node-bindings", "alloy-provider?/anvil"] # providers providers = ["dep:alloy-provider"] diff --git a/crates/contract/README.md b/crates/contract/README.md index 5cd97ea8e51..dd830b68f2f 100644 --- a/crates/contract/README.md +++ b/crates/contract/README.md @@ -32,7 +32,7 @@ sol! { } // Build a provider. -let provider = ProviderBuilder::new().with_recommended_layers().on_builtin("http://localhost:8545").await?; +let provider = ProviderBuilder::new().with_recommended_fillers().on_builtin("http://localhost:8545").await?; // If `#[sol(bytecode = "0x...")]` is provided, the contract can be deployed with `MyContract::deploy`, // and a new instance will be created. diff --git a/crates/eips/src/eip2930.rs b/crates/eips/src/eip2930.rs index d653138ddc3..ec655092b49 100644 --- a/crates/eips/src/eip2930.rs +++ b/crates/eips/src/eip2930.rs @@ -58,6 +58,18 @@ pub struct AccessList( pub Vec, ); +impl From> for AccessList { + fn from(list: Vec) -> Self { + Self(list) + } +} + +impl From for Vec { + fn from(this: AccessList) -> Self { + this.0 + } +} + impl AccessList { /// Converts the list into a vec, expected by revm pub fn flattened(&self) -> Vec<(Address, Vec)> { diff --git a/crates/json-rpc/src/error.rs b/crates/json-rpc/src/error.rs index 238d6bf5184..7514a590834 100644 --- a/crates/json-rpc/src/error.rs +++ b/crates/json-rpc/src/error.rs @@ -16,6 +16,11 @@ pub enum RpcError> { #[error("unsupported feature: {0}")] UnsupportedFeature(&'static str), + /// Returned when a local pre-processing step fails. This allows custom + /// errors from local signers or request pre-processors. + #[error("local usage error: {0}")] + LocalUsageError(#[source] Box), + /// JSON serialization error. #[error("serialization error: {0}")] SerError( @@ -52,12 +57,22 @@ impl RpcError where ErrResp: RpcReturn, { - /// Instantiate a new `TransportError` from an error response. + /// Instantiate a new `ErrorResp` from an error response. pub const fn err_resp(err: ErrorPayload) -> Self { Self::ErrorResp(err) } - /// Instantiate a new `TransportError` from a [`serde_json::Error`] and the + /// Instantiate a new `LocalUsageError` from a custom error. + pub fn local_usage(err: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::LocalUsageError(Box::new(err)) + } + + /// Instantiate a new `LocalUsageError` from a custom error message. + pub fn local_usage_str(err: &str) -> Self { + Self::LocalUsageError(err.into()) + } + + /// Instantiate a new `DeserError` from a [`serde_json::Error`] and the /// text. This should be called when the error occurs during /// deserialization. /// @@ -76,7 +91,7 @@ where } impl RpcError { - /// Instantiate a new `TransportError` from a [`serde_json::Error`]. This + /// Instantiate a new `SerError` from a [`serde_json::Error`]. This /// should be called when the error occurs during serialization. pub const fn ser_err(err: serde_json::Error) -> Self { Self::SerError(err) @@ -107,6 +122,16 @@ impl RpcError { matches!(self, Self::NullResp) } + /// Check if the error is an unsupported feature error. + pub const fn is_unsupported_feature(&self) -> bool { + matches!(self, Self::UnsupportedFeature(_)) + } + + /// Check if the error is a local usage error. + pub const fn is_local_usage_error(&self) -> bool { + matches!(self, Self::LocalUsageError(_)) + } + /// Fallible conversion to an error response. pub const fn as_error_resp(&self) -> Option<&ErrorPayload> { match self { diff --git a/crates/network/src/any/builder.rs b/crates/network/src/any/builder.rs index 065d114fe00..18a589b1d27 100644 --- a/crates/network/src/any/builder.rs +++ b/crates/network/src/any/builder.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; use alloy_consensus::BlobTransactionSidecar; -use alloy_rpc_types::{TransactionRequest, WithOtherFields}; +use alloy_rpc_types::{AccessList, TransactionRequest, WithOtherFields}; use crate::{ any::AnyNetwork, ethereum::build_unsigned, BuilderResult, Network, TransactionBuilder, @@ -96,18 +96,36 @@ impl TransactionBuilder for WithOtherFields { self.deref_mut().set_gas_limit(gas_limit); } - fn build_unsigned(self) -> BuilderResult<::UnsignedTx> { - build_unsigned::(self.inner) + /// Get the EIP-2930 access list for the transaction. + fn access_list(&self) -> Option<&AccessList> { + self.deref().access_list() + } + + /// Sets the EIP-2930 access list. + fn set_access_list(&mut self, access_list: AccessList) { + self.deref_mut().set_access_list(access_list) } - fn get_blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { - self.deref().get_blob_sidecar() + fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { + self.deref().blob_sidecar() } fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecar) { self.deref_mut().set_blob_sidecar(sidecar) } + fn can_build(&self) -> bool { + self.deref().can_build() + } + + fn can_submit(&self) -> bool { + self.deref().can_submit() + } + + fn build_unsigned(self) -> BuilderResult<::UnsignedTx> { + build_unsigned::(self.inner) + } + async fn build>( self, signer: &S, diff --git a/crates/network/src/ethereum/builder.rs b/crates/network/src/ethereum/builder.rs index f33c96d82f0..faa221f986c 100644 --- a/crates/network/src/ethereum/builder.rs +++ b/crates/network/src/ethereum/builder.rs @@ -3,16 +3,17 @@ use crate::{ }; use alloy_consensus::{ BlobTransactionSidecar, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxLegacy, + TypedTransaction, }; -use alloy_primitives::{Address, TxKind}; -use alloy_rpc_types::request::TransactionRequest; +use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256}; +use alloy_rpc_types::{request::TransactionRequest, AccessList}; -impl TransactionBuilder for alloy_rpc_types::TransactionRequest { - fn chain_id(&self) -> Option { +impl TransactionBuilder for TransactionRequest { + fn chain_id(&self) -> Option { self.chain_id } - fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { + fn set_chain_id(&mut self, chain_id: ChainId) { self.chain_id = Some(chain_id); } @@ -24,11 +25,11 @@ impl TransactionBuilder for alloy_rpc_types::TransactionRequest { self.nonce = Some(nonce); } - fn input(&self) -> Option<&alloy_primitives::Bytes> { + fn input(&self) -> Option<&Bytes> { self.input.input() } - fn set_input(&mut self, input: alloy_primitives::Bytes) { + fn set_input(&mut self, input: Bytes) { self.input.input = Some(input); } @@ -40,22 +41,22 @@ impl TransactionBuilder for alloy_rpc_types::TransactionRequest { self.from = Some(from); } - fn to(&self) -> Option { + fn to(&self) -> Option { self.to.map(TxKind::Call).or(Some(TxKind::Create)) } - fn set_to(&mut self, to: alloy_primitives::TxKind) { + fn set_to(&mut self, to: TxKind) { match to { TxKind::Create => self.to = None, TxKind::Call(to) => self.to = Some(to), } } - fn value(&self) -> Option { + fn value(&self) -> Option { self.value } - fn set_value(&mut self, value: alloy_primitives::U256) { + fn set_value(&mut self, value: U256) { self.value = Some(value) } @@ -99,11 +100,15 @@ impl TransactionBuilder for alloy_rpc_types::TransactionRequest { self.gas = Some(gas_limit); } - fn build_unsigned(self) -> BuilderResult<::UnsignedTx> { - build_unsigned::(self) + fn access_list(&self) -> Option<&AccessList> { + self.access_list.as_ref() + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.access_list = Some(access_list); } - fn get_blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { + fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { self.sidecar.as_ref() } @@ -112,6 +117,33 @@ impl TransactionBuilder for alloy_rpc_types::TransactionRequest { self.sidecar = Some(sidecar); } + fn can_submit(&self) -> bool { + // value and data may be None. If they are, they will be set to default. + // gas fields and nonce may be None, if they are, they will be populated + // with default values by the RPC server + self.to.is_some() && self.from.is_some() + } + + fn can_build(&self) -> bool { + // value and data may be none. If they are, they will be set to default + // values. + + // chain_id and from may be none. + let common = self.to.is_some() && self.gas.is_some() && self.nonce.is_some(); + + let legacy = self.gas_price.is_some(); + let eip2930 = legacy && self.access_list().is_some(); + + let eip1559 = self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some(); + + let eip4844 = eip1559 && self.sidecar.is_some(); + common && (legacy || eip2930 || eip1559 || eip4844) + } + + fn build_unsigned(self) -> BuilderResult { + build_unsigned::(self) + } + async fn build>( self, signer: &S, diff --git a/crates/network/src/ethereum/signer.rs b/crates/network/src/ethereum/signer.rs index 05a818971a7..c330f15e4ce 100644 --- a/crates/network/src/ethereum/signer.rs +++ b/crates/network/src/ethereum/signer.rs @@ -33,7 +33,7 @@ impl EthereumSigner { Self(Arc::new(signer)) } - async fn sign_transaction( + async fn sign_transaction_inner( &self, tx: &mut dyn SignableTransaction, ) -> alloy_signer::Result { @@ -47,19 +47,19 @@ impl NetworkSigner for EthereumSigner { async fn sign_transaction(&self, tx: TypedTransaction) -> alloy_signer::Result { match tx { TypedTransaction::Legacy(mut t) => { - let sig = self.sign_transaction(&mut t).await?; + let sig = self.sign_transaction_inner(&mut t).await?; Ok(t.into_signed(sig).into()) } TypedTransaction::Eip2930(mut t) => { - let sig = self.sign_transaction(&mut t).await?; + let sig = self.sign_transaction_inner(&mut t).await?; Ok(t.into_signed(sig).into()) } TypedTransaction::Eip1559(mut t) => { - let sig = self.sign_transaction(&mut t).await?; + let sig = self.sign_transaction_inner(&mut t).await?; Ok(t.into_signed(sig).into()) } TypedTransaction::Eip4844(mut t) => { - let sig = self.sign_transaction(&mut t).await?; + let sig = self.sign_transaction_inner(&mut t).await?; Ok(t.into_signed(sig).into()) } } diff --git a/crates/network/src/lib.rs b/crates/network/src/lib.rs index adb861f1bf1..1eb0c3024e8 100644 --- a/crates/network/src/lib.rs +++ b/crates/network/src/lib.rs @@ -48,11 +48,11 @@ pub trait ReceiptResponse { /// Networks are only containers for types, so it is recommended to use ZSTs for their definition. // todo: block responses are ethereum only, so we need to include this in here too, or make `Block` // generic over tx/header type -pub trait Network: Clone + Copy + Sized + Send + Sync + 'static { +pub trait Network: std::fmt::Debug + Clone + Copy + Sized + Send + Sync + 'static { // -- Consensus types -- /// The network transaction envelope type. - type TxEnvelope: Eip2718Envelope; + type TxEnvelope: Eip2718Envelope + std::fmt::Debug; /// An enum over the various transaction types. type UnsignedTx; diff --git a/crates/network/src/transaction/builder.rs b/crates/network/src/transaction/builder.rs index 8ac8c566c2f..98045eaa5cd 100644 --- a/crates/network/src/transaction/builder.rs +++ b/crates/network/src/transaction/builder.rs @@ -2,6 +2,7 @@ use super::signer::NetworkSigner; use crate::Network; use alloy_consensus::BlobTransactionSidecar; use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256}; +use alloy_rpc_types::AccessList; use futures_utils_wasm::impl_future; /// Error type for transaction builders. @@ -191,8 +192,20 @@ pub trait TransactionBuilder: Default + Sized + Send + Sync + 'stati self } + /// Get the EIP-2930 access list for the transaction. + fn access_list(&self) -> Option<&AccessList>; + + /// Sets the EIP-2930 access list. + fn set_access_list(&mut self, access_list: AccessList); + + /// Builder-pattern method for setting the access list. + fn with_access_list(mut self, access_list: AccessList) -> Self { + self.set_access_list(access_list); + self + } + /// Gets the EIP-4844 blob sidecar of the transaction. - fn get_blob_sidecar(&self) -> Option<&BlobTransactionSidecar>; + fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar>; /// Sets the EIP-4844 blob sidecar of the transaction. /// @@ -206,6 +219,14 @@ pub trait TransactionBuilder: Default + Sized + Send + Sync + 'stati self } + /// True if the builder contains all necessary information to be submitted + /// to the `eth_sendTransaction` endpoint. + fn can_submit(&self) -> bool; + + /// True if the builder contains all necessary information to be built into + /// a valid transaction. + fn can_build(&self) -> bool; + /// Build an unsigned, but typed, transaction. fn build_unsigned(self) -> BuilderResult; diff --git a/crates/network/src/transaction/signer.rs b/crates/network/src/transaction/signer.rs index 6024008a032..155b530579f 100644 --- a/crates/network/src/transaction/signer.rs +++ b/crates/network/src/transaction/signer.rs @@ -1,4 +1,4 @@ -use crate::Network; +use crate::{Network, TransactionBuilder}; use alloy_consensus::SignableTransaction; use async_trait::async_trait; @@ -9,9 +9,18 @@ use async_trait::async_trait; /// [`TxSigner`] to signify signing capability for specific signature types. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait NetworkSigner: Send + Sync { +pub trait NetworkSigner: std::fmt::Debug + Send + Sync { /// Asynchronously sign an unsigned transaction. async fn sign_transaction(&self, tx: N::UnsignedTx) -> alloy_signer::Result; + + /// Asynchronously sign a transaction request. + async fn sign_request( + &self, + request: N::TransactionRequest, + ) -> alloy_signer::Result { + let tx = request.build_unsigned().map_err(alloy_signer::Error::other)?; + self.sign_transaction(tx).await + } } /// Asynchronous transaction signer, capable of signing any [`SignableTransaction`] for the given diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index f0bff807187..4df2701a0f7 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -12,8 +12,11 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-eips.workspace = true alloy-json-rpc.workspace = true alloy-network.workspace = true +alloy-node-bindings = { workspace = true, optional = true } +alloy-signer-wallet = { workspace = true, optional = true } alloy-rpc-client.workspace = true alloy-rpc-types-trace.workspace = true alloy-rpc-types.workspace = true @@ -35,6 +38,7 @@ serde_json.workspace = true tokio = { workspace = true, features = ["sync", "macros"] } tracing.workspace = true url = { workspace = true, optional = true } +futures-utils-wasm.workspace = true [dev-dependencies] alloy-consensus = { workspace = true, features = ["std"] } @@ -59,3 +63,4 @@ reqwest = [ hyper = ["dep:alloy-transport-http", "dep:url", "alloy-rpc-client/hyper"] ws = ["pubsub", "alloy-rpc-client/ws", "alloy-transport-ws"] ipc = ["pubsub", "alloy-rpc-client/ipc", "alloy-transport-ipc"] +anvil = ["reqwest", "dep:alloy-node-bindings", "dep:alloy-signer-wallet"] \ No newline at end of file diff --git a/crates/provider/src/builder.rs b/crates/provider/src/builder.rs index d80befc5fc5..a5890bae247 100644 --- a/crates/provider/src/builder.rs +++ b/crates/provider/src/builder.rs @@ -1,12 +1,19 @@ use crate::{ - layers::{GasEstimatorLayer, NonceManagerLayer, SignerLayer}, + fillers::{ + ChainIdFiller, FillerControlFlow, GasFiller, JoinFill, NonceFiller, SignerFiller, TxFiller, + }, + provider::SendableTx, Provider, RootProvider, }; use alloy_network::{Ethereum, Network}; use alloy_rpc_client::{BuiltInConnectionString, ClientBuilder, RpcClient}; -use alloy_transport::{BoxTransport, Transport, TransportError}; +use alloy_transport::{BoxTransport, Transport, TransportError, TransportResult}; use std::marker::PhantomData; +/// The recommended filler. +type RecommendFiller = + JoinFill, NonceFiller>, ChainIdFiller>; + /// A layering abstraction in the vein of [`tower::Layer`] /// /// [`tower::Layer`]: https://docs.rs/tower/latest/tower/trait.Layer.html @@ -22,6 +29,33 @@ pub trait ProviderLayer, T: Transport + Clone, N: Network = Et #[derive(Debug, Clone, Copy)] pub struct Identity; +impl TxFiller for Identity +where + N: Network, +{ + type Fillable = (); + + fn status(&self, _tx: &::TransactionRequest) -> FillerControlFlow { + FillerControlFlow::Finished + } + + async fn prepare( + &self, + _provider: &P, + _tx: &N::TransactionRequest, + ) -> TransportResult { + Ok(()) + } + + async fn fill( + &self, + _to_fill: Self::Fillable, + tx: SendableTx, + ) -> TransportResult> { + Ok(tx) + } +} + impl ProviderLayer for Identity where T: Transport + Clone, @@ -73,25 +107,66 @@ where /// /// [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html #[derive(Debug)] -pub struct ProviderBuilder { +pub struct ProviderBuilder { layer: L, - network: PhantomData, + filler: F, + network: PhantomData N>, } -impl ProviderBuilder { +impl ProviderBuilder { /// Create a new [`ProviderBuilder`]. pub const fn new() -> Self { - ProviderBuilder { layer: Identity, network: PhantomData } + ProviderBuilder { layer: Identity, filler: Identity, network: PhantomData } } } -impl Default for ProviderBuilder { +impl Default for ProviderBuilder { fn default() -> Self { - ProviderBuilder { layer: Identity, network: PhantomData } + ProviderBuilder { layer: Identity, filler: Identity, network: PhantomData } } } -impl ProviderBuilder { +impl ProviderBuilder { + /// Add preconfigured set of layers handling gas estimation, nonce + /// management, and chain-id fetching. + pub fn with_recommended_fillers(self) -> ProviderBuilder { + self.filler(GasFiller).filler(NonceFiller::default()).filler(ChainIdFiller::default()) + } + + /// Add gas estimation to the stack being built. + /// + /// See [`GasFiller`] + pub fn with_gas_estimation(self) -> ProviderBuilder, N> { + self.filler(GasFiller) + } + + /// Add nonce management to the stack being built. + /// + /// See [`NonceFiller`] + pub fn with_nonce_management(self) -> ProviderBuilder, N> { + self.filler(NonceFiller::default()) + } + + /// Add a chain ID filler to the stack being built. The filler will attempt + /// to fetch the chain ID from the provider using + /// [`Provider::get_chain_id`]. the first time a transaction is prepared, + /// and will cache it for future transactions. + pub fn fetch_chain_id(self) -> ProviderBuilder, N> { + self.filler(ChainIdFiller::default()) + } + + /// Add a specific chain ID to the stack being built. The filler will + /// fill transactions with the provided chain ID, regardless of the chain ID + /// that the provider reports via [`Provider::get_chain_id`]. + pub fn with_chain_id( + self, + chain_id: u64, + ) -> ProviderBuilder, N> { + self.filler(ChainIdFiller::new(Some(chain_id))) + } +} + +impl ProviderBuilder { /// Add a layer to the stack being built. This is similar to /// [`tower::ServiceBuilder::layer`]. /// @@ -103,36 +178,30 @@ impl ProviderBuilder { /// /// [`tower::ServiceBuilder::layer`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html#method.layer /// [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html - pub fn layer(self, layer: Inner) -> ProviderBuilder, N> { - ProviderBuilder { layer: Stack::new(layer, self.layer), network: PhantomData } + pub fn layer(self, layer: Inner) -> ProviderBuilder, F, N> { + ProviderBuilder { + layer: Stack::new(layer, self.layer), + filler: self.filler, + network: PhantomData, + } } - /// Add a signer layer to the stack being built. - /// - /// See [`SignerLayer`]. - pub fn signer(self, signer: S) -> ProviderBuilder, L>, N> { - self.layer(SignerLayer::new(signer)) + /// Add a transaction filler to the stack being built. Transaction fillers + /// are used to fill in missing fields on transactions before they are sent, + /// and are all joined to form the outermost layer of the stack. + pub fn filler(self, filler: F2) -> ProviderBuilder, N> { + ProviderBuilder { + layer: self.layer, + filler: JoinFill::new(self.filler, filler), + network: PhantomData, + } } - /// Add gas estimation to the stack being built. - /// - /// See [`GasEstimatorLayer`] - pub fn with_gas_estimation(self) -> ProviderBuilder, N> { - self.layer(GasEstimatorLayer) - } - - /// Add nonce management to the stack being built. + /// Add a signer layer to the stack being built. /// - /// See [`NonceManagerLayer`] - pub fn with_nonce_management(self) -> ProviderBuilder, N> { - self.layer(NonceManagerLayer) - } - - /// Add preconfigured set of layers handling gas estimation and nonce management - pub fn with_recommended_layers( - self, - ) -> ProviderBuilder>, N> { - self.with_gas_estimation().with_nonce_management() + /// See [`SignerFiller`]. + pub fn signer(self, signer: S) -> ProviderBuilder>, N> { + self.filler(SignerFiller::new(signer)) } /// Change the network. @@ -143,20 +212,23 @@ impl ProviderBuilder { /// ```ignore /// builder.network::() /// ``` - pub fn network(self) -> ProviderBuilder { - ProviderBuilder { layer: self.layer, network: PhantomData } + pub fn network(self) -> ProviderBuilder { + ProviderBuilder { layer: self.layer, filler: self.filler, network: PhantomData } } /// Finish the layer stack by providing a root [`Provider`], outputting /// the final [`Provider`] type with all stack components. - pub fn provider(self, provider: P) -> L::Provider + pub fn on_provider(self, provider: P) -> F::Provider where L: ProviderLayer, + F: TxFiller + ProviderLayer, P: Provider, T: Transport + Clone, N: Network, { - self.layer.layer(provider) + let Self { layer, filler, .. } = self; + let stack = Stack::new(layer, filler); + stack.layer(provider) } /// Finish the layer stack by providing a root [`RpcClient`], outputting @@ -164,21 +236,23 @@ impl ProviderBuilder { /// /// This is a convenience function for /// `ProviderBuilder::provider`. - pub fn on_client(self, client: RpcClient) -> L::Provider + pub fn on_client(self, client: RpcClient) -> F::Provider where L: ProviderLayer, T, N>, + F: TxFiller + ProviderLayer, T: Transport + Clone, N: Network, { - self.provider(RootProvider::new(client)) + self.on_provider(RootProvider::new(client)) } /// Finish the layer stack by providing a connection string for a built-in /// transport type, outputting the final [`Provider`] type with all stack /// components. - pub async fn on_builtin(self, s: &str) -> Result + pub async fn on_builtin(self, s: &str) -> Result where L: ProviderLayer, BoxTransport, N>, + F: TxFiller + ProviderLayer, N: Network, { let connect: BuiltInConnectionString = s.parse()?; @@ -191,13 +265,14 @@ impl ProviderBuilder { pub async fn on_ws( self, connect: alloy_transport_ws::WsConnect, - ) -> Result + ) -> Result where L: ProviderLayer< RootProvider, alloy_pubsub::PubSubFrontend, N, >, + F: TxFiller + ProviderLayer, N: Network, { let client = ClientBuilder::default().ws(connect).await?; @@ -209,7 +284,7 @@ impl ProviderBuilder { pub async fn on_ipc( self, connect: alloy_transport_ipc::IpcConnect, - ) -> Result + ) -> Result where alloy_transport_ipc::IpcConnect: alloy_pubsub::PubSubConnect, L: ProviderLayer< @@ -217,6 +292,7 @@ impl ProviderBuilder { alloy_pubsub::PubSubFrontend, N, >, + F: TxFiller + ProviderLayer, N: Network, { let client = ClientBuilder::default().ipc(connect).await?; @@ -225,9 +301,10 @@ impl ProviderBuilder { /// Build this provider with an Reqwest HTTP transport. #[cfg(feature = "reqwest")] - pub fn on_http(self, url: url::Url) -> Result + pub fn on_http(self, url: url::Url) -> Result where L: ProviderLayer, alloy_transport_http::Http, N>, + F: TxFiller + ProviderLayer, N>, N: Network, { let client = ClientBuilder::default().http(url); @@ -236,13 +313,19 @@ impl ProviderBuilder { /// Build this provider with an Hyper HTTP transport. #[cfg(feature = "hyper")] - pub fn on_hyper_http(self, url: url::Url) -> Result + pub fn on_hyper_http(self, url: url::Url) -> Result where L: ProviderLayer< crate::HyperProvider, alloy_transport_http::Http, N, >, + F: TxFiller + + ProviderLayer< + L::Provider, + alloy_transport_http::Http, + N, + >, N: Network, { let client = ClientBuilder::default().hyper_http(url); @@ -250,6 +333,60 @@ impl ProviderBuilder { } } +// Enabled when the `anvil` feature is enabled, or when both in test and the +// `reqwest` feature is enabled. +#[cfg(any(all(test, feature = "reqwest"), feature = "anvil"))] +impl ProviderBuilder { + /// Build this provider with anvil, using an Reqwest HTTP transport. + pub fn on_anvil(self) -> (F::Provider, alloy_node_bindings::AnvilInstance) + where + F: TxFiller + + ProviderLayer, Ethereum>, + L: ProviderLayer< + crate::ReqwestProvider, + alloy_transport_http::Http, + Ethereum, + >, + { + let anvil = alloy_node_bindings::Anvil::new().spawn(); + let url = anvil.endpoint().parse().unwrap(); + + (self.on_http(url).unwrap(), anvil) + } + + /// Build this provider with anvil, using an Reqwest HTTP transport. This + /// function configures a signer backed by anvil keys, and is intended for + /// use in tests. + #[allow(clippy::type_complexity)] + pub fn on_anvil_with_signer( + self, + ) -> ( + > as ProviderLayer< + L::Provider, + alloy_transport_http::Http, + >>::Provider, + alloy_node_bindings::AnvilInstance, + ) + where + L: ProviderLayer< + crate::ReqwestProvider, + alloy_transport_http::Http, + Ethereum, + >, + F: TxFiller + + ProviderLayer, Ethereum>, + { + let anvil = alloy_node_bindings::Anvil::new().spawn(); + let url = anvil.endpoint().parse().unwrap(); + + let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); + + let this = self.signer(crate::network::EthereumSigner::from(wallet)); + + (this.on_http(url).unwrap(), anvil) + } +} + // Copyright (c) 2019 Tower Contributors // Permission is hereby granted, free of charge, to any diff --git a/crates/provider/src/fillers/chain_id.rs b/crates/provider/src/fillers/chain_id.rs new file mode 100644 index 00000000000..cb66ccc3cf0 --- /dev/null +++ b/crates/provider/src/fillers/chain_id.rs @@ -0,0 +1,97 @@ +use std::sync::{Arc, OnceLock}; + +use alloy_network::{Network, TransactionBuilder}; +use alloy_transport::TransportResult; + +use crate::{ + fillers::{FillerControlFlow, TxFiller}, + provider::SendableTx, +}; + +/// A [`TxFiller`] that populates the chain ID of a transaction. +/// +/// If a chain ID is provided, it will be used for filling. If a chain ID +/// is not provided, the filler will attempt to fetch the chain ID from the +/// provider the first time a transaction is prepared, and will cache it for +/// future transactions. +/// +/// Transactions that already have a chain_id set by the user will not be +/// modified. +/// +/// # Example +/// +/// ``` +/// # use alloy_network::{NetworkSigner, EthereumSigner, Ethereum}; +/// # use alloy_rpc_types::TransactionRequest; +/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider}; +/// # async fn test + Clone>(url: url::Url, signer: S) -> Result<(), Box> { +/// let provider = ProviderBuilder::new() +/// .with_chain_id(1) +/// .signer(signer) +/// .on_http(url)?; +/// +/// provider.send_transaction(TransactionRequest::default()).await; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ChainIdFiller(Arc>); + +impl ChainIdFiller { + /// Create a new [`ChainIdFiller`] with an optional chain ID. + /// + /// If a chain ID is provided, it will be used for filling. If a chain ID + /// is not provided, the filler will attempt to fetch the chain ID from the + /// provider the first time a transaction is prepared. + pub fn new(chain_id: Option) -> Self { + let lock = OnceLock::new(); + if let Some(chain_id) = chain_id { + lock.set(chain_id).expect("brand new"); + } + Self(Arc::new(lock)) + } +} + +impl TxFiller for ChainIdFiller { + type Fillable = u64; + + fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow { + if tx.chain_id().is_some() { + FillerControlFlow::Finished + } else { + FillerControlFlow::Ready + } + } + + async fn prepare( + &self, + provider: &P, + _tx: &N::TransactionRequest, + ) -> TransportResult + where + P: crate::Provider, + T: alloy_transport::Transport + Clone, + { + match self.0.get().copied() { + Some(chain_id) => Ok(chain_id), + None => { + let chain_id = provider.get_chain_id().await?; + let chain_id = *self.0.get_or_init(|| chain_id); + Ok(chain_id) + } + } + } + + async fn fill( + &self, + fillable: Self::Fillable, + mut tx: SendableTx, + ) -> TransportResult> { + if let Some(builder) = tx.as_mut_builder() { + if builder.chain_id().is_none() { + builder.set_chain_id(fillable) + } + }; + Ok(tx) + } +} diff --git a/crates/provider/src/fillers/gas.rs b/crates/provider/src/fillers/gas.rs new file mode 100644 index 00000000000..95bba241776 --- /dev/null +++ b/crates/provider/src/fillers/gas.rs @@ -0,0 +1,326 @@ +use crate::{ + fillers::{FillerControlFlow, TxFiller}, + provider::SendableTx, + utils::Eip1559Estimation, + Provider, +}; +use alloy_json_rpc::RpcError; +use alloy_network::{Network, TransactionBuilder}; +use alloy_rpc_types::BlockNumberOrTag; +use alloy_transport::{Transport, TransportResult}; +use futures::FutureExt; + +/// An enum over the different types of gas fillable. +#[allow(unreachable_pub)] +#[doc(hidden)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GasFillable { + Legacy { gas_limit: u128, gas_price: u128 }, + Eip1559 { gas_limit: u128, estimate: Eip1559Estimation }, + Eip4844 { gas_limit: u128, estimate: Eip1559Estimation, max_fee_per_blob_gas: u128 }, +} + +/// A [`TxFiller`] that populates gas related fields in transaction requests if +/// unset. +/// +/// Gas related fields are gas_price, gas_limit, max_fee_per_gas +/// max_priority_fee_per_gas and max_fee_per_blob_gas. +/// +/// The layer fetches the estimations for these via the +/// [`Provider::get_gas_price`], [`Provider::estimate_gas`] and +/// [`Provider::estimate_eip1559_fees`] methods. +/// +/// ## Note: +/// +/// The layer will populate gas fields based on the following logic: +/// - if `gas_price` is set, it will process as a legacy tx and populate the +/// `gas_limit` field if unset. +/// - if `access_list` is set, it will process as a 2930 tx and populate the +/// `gas_limit` and `gas_price` field if unset. +/// - if `blob_sidecar` is set, it will process as a 4844 tx and populate the +/// `gas_limit`, `max_fee_per_gas`, `max_priority_fee_per_gas` and +/// `max_fee_per_blob_gas` fields if unset. +/// - Otherwise, it will process as a EIP-1559 tx and populate the `gas_limit`, +/// `max_fee_per_gas` and `max_priority_fee_per_gas` fields if unset. +/// - If the network does not support EIP-1559, it will fallback to the legacy +/// tx and populate the `gas_limit` and `gas_price` fields if unset. +/// +/// # Example +/// +/// ``` +/// # use alloy_network::{NetworkSigner, EthereumSigner, Ethereum}; +/// # use alloy_rpc_types::TransactionRequest; +/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider}; +/// # async fn test + Clone>(url: url::Url, signer: S) -> Result<(), Box> { +/// let provider = ProviderBuilder::new() +/// .with_gas_estimation() +/// .signer(signer) +/// .on_http(url)?; +/// +/// provider.send_transaction(TransactionRequest::default()).await; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct GasFiller; + +impl GasFiller { + async fn prepare_legacy( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + N: Network, + { + let gas_price_fut = if let Some(gas_price) = tx.gas_price() { + async move { Ok(gas_price) }.left_future() + } else { + async { provider.get_gas_price().await }.right_future() + }; + + let gas_limit_fut = if let Some(gas_limit) = tx.gas_limit() { + async move { Ok(gas_limit) }.left_future() + } else { + async { provider.estimate_gas(tx, None).await }.right_future() + }; + + let (gas_price, gas_limit) = futures::try_join!(gas_price_fut, gas_limit_fut)?; + + Ok(GasFillable::Legacy { gas_limit, gas_price }) + } + + async fn prepare_1559( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + N: Network, + { + let gas_limit_fut = if let Some(gas_limit) = tx.gas_limit() { + async move { Ok(gas_limit) }.left_future() + } else { + async { provider.estimate_gas(tx, None).await }.right_future() + }; + + let eip1559_fees_fut = if let (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) = + (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) + { + async move { Ok(Eip1559Estimation { max_fee_per_gas, max_priority_fee_per_gas }) } + .left_future() + } else { + async { provider.estimate_eip1559_fees(None).await }.right_future() + }; + + let (gas_limit, estimate) = futures::try_join!(gas_limit_fut, eip1559_fees_fut)?; + + Ok(GasFillable::Eip1559 { gas_limit, estimate }) + } + + async fn prepare_4844( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + N: Network, + { + let gas_limit_fut = if let Some(gas_limit) = tx.gas_limit() { + async move { Ok(gas_limit) }.left_future() + } else { + async { provider.estimate_gas(tx, None).await }.right_future() + }; + + let eip1559_fees_fut = if let (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) = + (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) + { + async move { Ok(Eip1559Estimation { max_fee_per_gas, max_priority_fee_per_gas }) } + .left_future() + } else { + async { provider.estimate_eip1559_fees(None).await }.right_future() + }; + + let max_fee_per_blob_gas_fut = if let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() + { + async move { Ok(max_fee_per_blob_gas) }.left_future() + } else { + async { + provider + .get_block_by_number(BlockNumberOrTag::Latest, false) + .await? + .ok_or(RpcError::NullResp)? + .header + .next_block_blob_fee() + .ok_or(RpcError::UnsupportedFeature("eip4844")) + } + .right_future() + }; + + let (gas_limit, estimate, max_fee_per_blob_gas) = + futures::try_join!(gas_limit_fut, eip1559_fees_fut, max_fee_per_blob_gas_fut)?; + + Ok(GasFillable::Eip4844 { gas_limit, estimate, max_fee_per_blob_gas }) + } +} + +impl TxFiller for GasFiller { + type Fillable = GasFillable; + + fn status(&self, tx: &::TransactionRequest) -> FillerControlFlow { + // legacy and eip2930 tx + if tx.gas_price().is_some() && tx.gas_limit().is_some() { + return FillerControlFlow::Finished; + } + + // 4844 + if tx.max_fee_per_blob_gas().is_some() + && tx.max_fee_per_gas().is_some() + && tx.max_priority_fee_per_gas().is_some() + { + return FillerControlFlow::Finished; + } + + // eip1559 + if tx.blob_sidecar().is_none() + && tx.max_fee_per_gas().is_some() + && tx.max_priority_fee_per_gas().is_some() + { + return FillerControlFlow::Finished; + } + + FillerControlFlow::Ready + } + + async fn prepare( + &self, + provider: &P, + tx: &::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + { + if tx.gas_price().is_some() || tx.access_list().is_some() { + self.prepare_legacy(provider, tx).await + } else if tx.blob_sidecar().is_some() { + self.prepare_4844(provider, tx).await + } else { + match self.prepare_1559(provider, tx).await { + // fallback to legacy + Ok(estimate) => Ok(estimate), + Err(RpcError::UnsupportedFeature(_)) => self.prepare_legacy(provider, tx).await, + Err(e) => Err(e), + } + } + } + + async fn fill( + &self, + fillable: Self::Fillable, + mut tx: SendableTx, + ) -> TransportResult> { + if let Some(builder) = tx.as_mut_builder() { + match fillable { + GasFillable::Legacy { gas_limit, gas_price } => { + builder.set_gas_limit(gas_limit); + builder.set_gas_price(gas_price); + } + GasFillable::Eip1559 { gas_limit, estimate } => { + builder.set_gas_limit(gas_limit); + builder.set_max_fee_per_gas(estimate.max_fee_per_gas); + builder.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas); + } + GasFillable::Eip4844 { gas_limit, estimate, max_fee_per_blob_gas } => { + builder.set_gas_limit(gas_limit); + builder.set_max_fee_per_gas(estimate.max_fee_per_gas); + builder.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas); + builder.set_max_fee_per_blob_gas(max_fee_per_blob_gas); + } + } + }; + Ok(tx) + } +} + +#[cfg(feature = "reqwest")] +#[cfg(test)] +mod tests { + use super::*; + use crate::ProviderBuilder; + use alloy_primitives::{address, U256}; + use alloy_rpc_types::TransactionRequest; + + #[tokio::test] + async fn no_gas_price_or_limit() { + let (provider, anvil) = + ProviderBuilder::new().with_recommended_fillers().on_anvil_with_signer(); + + // GasEstimationLayer requires chain_id to be set to handle EIP-1559 tx + let tx = TransactionRequest { + from: Some(anvil.addresses()[0]), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + chain_id: Some(31337), + ..Default::default() + }; + + let tx = provider.send_transaction(tx).await.unwrap(); + + let tx = tx.get_receipt().await.unwrap(); + + assert_eq!(tx.effective_gas_price, 0x3b9aca00); + assert_eq!(tx.gas_used, Some(0x5208)); + } + + #[tokio::test] + async fn no_gas_limit() { + let (provider, anvil) = + ProviderBuilder::new().with_recommended_fillers().on_anvil_with_signer(); + + 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(), + gas_price: Some(gas_price), + ..Default::default() + }; + + let tx = provider.send_transaction(tx).await.unwrap(); + + let receipt = tx.get_receipt().await.unwrap(); + + assert_eq!(receipt.gas_used, Some(0x5208)); + } + + #[tokio::test] + async fn non_eip1559_network() { + let (provider, _anvil) = ProviderBuilder::new() + .filler(crate::fillers::GasFiller) + .filler(crate::fillers::NonceFiller::default()) + .filler(crate::fillers::ChainIdFiller::default()) + .on_anvil(); + + let tx = TransactionRequest { + from: Some(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + // access list forces legacy gassing + access_list: Some(vec![Default::default()].into()), + ..Default::default() + }; + + let tx = provider.send_transaction(tx).await.unwrap(); + + let receipt = tx.get_receipt().await.unwrap(); + + assert_eq!(receipt.effective_gas_price, 2000000000); + } +} diff --git a/crates/provider/src/fillers/join_fill.rs b/crates/provider/src/fillers/join_fill.rs new file mode 100644 index 00000000000..9c4805d7d6e --- /dev/null +++ b/crates/provider/src/fillers/join_fill.rs @@ -0,0 +1,119 @@ +use crate::{ + fillers::{FillProvider, FillerControlFlow, TxFiller}, + provider::SendableTx, + Provider, ProviderLayer, +}; +use alloy_network::Network; +use alloy_transport::{Transport, TransportResult}; +use futures::try_join; + +/// A layer that can fill in a [`TransactionRequest`] with additional +/// information by joining two [`TxFiller`]s. This struct is itself a +/// [`TxFiller`], and can be nested to compose any number of fill layers. +/// +/// [`TransactionRequest`]: alloy_rpc_types::TransactionRequest +#[derive(Debug, Clone, Copy)] +pub struct JoinFill { + left: L, + right: R, +} + +impl JoinFill { + /// Creates a new `JoinFill` with the given layers. + pub const fn new(left: L, right: R) -> Self { + Self { left, right } + } +} + +impl JoinFill { + /// Get a request for the left filler, if the left filler is ready. + async fn prepare_left( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult> + where + P: Provider, + T: Transport + Clone, + L: TxFiller, + N: Network, + { + if self.left.ready(tx) { + self.left.prepare(provider, tx).await.map(Some) + } else { + Ok(None) + } + } + + /// Get a prepare for the right filler, if the right filler is ready. + async fn prepare_right( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult> + where + P: Provider, + T: Transport + Clone, + R: TxFiller, + N: Network, + { + if self.right.ready(tx) { + self.right.prepare(provider, tx).await.map(Some) + } else { + Ok(None) + } + } +} + +impl TxFiller for JoinFill +where + L: TxFiller, + R: TxFiller, + N: Network, +{ + type Fillable = (Option, Option); + + fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow { + self.left.status(tx).absorb(self.right.status(tx)) + } + + async fn prepare( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + { + try_join!(self.prepare_left(provider, tx), self.prepare_right(provider, tx)) + } + + async fn fill( + &self, + to_fill: Self::Fillable, + mut tx: SendableTx, + ) -> TransportResult> { + if let Some(to_fill) = to_fill.0 { + tx = self.left.fill(to_fill, tx).await?; + }; + if let Some(to_fill) = to_fill.1 { + tx = self.right.fill(to_fill, tx).await?; + }; + Ok(tx) + } +} + +impl ProviderLayer for JoinFill +where + L: TxFiller, + R: TxFiller, + P: Provider, + T: alloy_transport::Transport + Clone, + N: Network, +{ + type Provider = FillProvider, P, T, N>; + fn layer(&self, inner: P) -> Self::Provider { + FillProvider::new(inner, self.clone()) + } +} diff --git a/crates/provider/src/fillers/mod.rs b/crates/provider/src/fillers/mod.rs new file mode 100644 index 00000000000..bf4eaa7e333 --- /dev/null +++ b/crates/provider/src/fillers/mod.rs @@ -0,0 +1,284 @@ +//! Transaction Fillers +//! +//! Fillers decorate a [`Provider`], filling transaction details before they +//! are sent to the network. Fillers are used to set the nonce, gas price, gas +//! limit, and other transaction details, and are called before any other layer. +//! +//! [`Provider`]: crate::Provider + +mod chain_id; +pub use chain_id::ChainIdFiller; + +mod signer; +pub use signer::SignerFiller; + +mod nonce; +pub use nonce::NonceFiller; + +mod gas; +pub use gas::GasFiller; + +mod join_fill; +pub use join_fill::JoinFill; + +use crate::{ + provider::SendableTx, PendingTransactionBuilder, Provider, ProviderLayer, RootProvider, +}; +use alloy_json_rpc::RpcError; +use alloy_network::{Ethereum, Network}; +use alloy_transport::{Transport, TransportResult}; +use async_trait::async_trait; +use futures_utils_wasm::impl_future; +use std::marker::PhantomData; + +/// The control flow for a filler. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FillerControlFlow { + /// The filler is missing a required property. + /// + /// To allow joining fillers while preserving their associated missing + /// lists, this variant contains a list of `(name, missing)` tuples. When + /// absorbing another control flow, if both are missing, the missing lists + /// are combined. + Missing(Vec<(&'static str, &'static [&'static str])>), + /// The filler is ready to fill in the transaction request. + Ready, + /// The filler has filled in all properties that it can fill. + Finished, +} + +impl FillerControlFlow { + /// Absorb the control flow of another filler. + /// + /// # Behavior: + /// - If either is finished, return the unfinished one + /// - If either is ready, return ready. + /// - If both are missing, return missing. + pub fn absorb(self, other: Self) -> Self { + if other.is_finished() { + return self; + } + + if self.is_finished() { + return other; + } + + if other.is_ready() || self.is_ready() { + return Self::Ready; + } + + if let (Self::Missing(mut a), Self::Missing(b)) = (self, other) { + a.extend(b); + return Self::Missing(a); + } + + unreachable!() + } + + /// Creates a new `Missing` control flow. + pub fn missing(name: &'static str, missing: &'static [&'static str]) -> Self { + Self::Missing(vec![(name, missing)]) + } + + /// Returns true if the filler is missing a required property. + pub fn as_missing(&self) -> Option<&[(&'static str, &'static [&'static str])]> { + match self { + Self::Missing(missing) => Some(missing), + _ => None, + } + } + + /// Returns `true` if the filler is missing information required to fill in + /// the transaction request. + pub const fn is_missing(&self) -> bool { + matches!(self, Self::Missing(_)) + } + + /// Returns `true` if the filler is ready to fill in the transaction + /// request. + pub const fn is_ready(&self) -> bool { + matches!(self, Self::Ready) + } + + /// Returns `true` if the filler is finished filling in the transaction + /// request. + pub const fn is_finished(&self) -> bool { + matches!(self, Self::Finished) + } +} + +/// A layer that can fill in a `TransactionRequest` with additional information. +/// +/// ## Lifecycle Notes +/// +/// The [`FillerControlFlow`] determines the lifecycle of a filler. Fillers +/// may be in one of three states: +/// - **Missing**: The filler is missing a required property to fill in the +/// transaction request. [`TxFiller::status`] should return +/// [`FillerControlFlow::Missing`]. +/// with a list of the missing properties. +/// - **Ready**: The filler is ready to fill in the transaction request. +/// [`TxFiller::status`] should return [`FillerControlFlow::Ready`]. +/// - **Finished**: The filler has filled in all properties that it can fill. +/// [`TxFiller::status`] should return [`FillerControlFlow::Finished`]. +pub trait TxFiller: Clone + Send + Sync + std::fmt::Debug { + /// The properties that this filler retrieves from the RPC. to fill in the + /// TransactionRequest. + type Fillable: Send + Sync + 'static; + + /// Joins this filler with another filler to compose multiple fillers. + fn join_with(self, other: T) -> JoinFill + where + T: TxFiller, + { + JoinFill::new(self, other) + } + + /// Return a control-flow enum indicating whether the filler is ready to + /// fill in the transaction request, or if it is missing required + /// properties. + fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow; + + /// Returns `true` if the filler is should continnue filling. + fn continue_filling(&self, tx: &SendableTx) -> bool { + tx.as_builder().map(|tx| self.status(tx).is_ready()).unwrap_or_default() + } + + /// Returns `true` if the filler is ready to fill in the transaction request. + fn ready(&self, tx: &N::TransactionRequest) -> bool { + self.status(tx).is_ready() + } + + /// Returns `true` if the filler is finished filling in the transaction request. + fn finished(&self, tx: &N::TransactionRequest) -> bool { + self.status(tx).is_finished() + } + + /// Prepares fillable properties, potentially by making an RPC request. + fn prepare( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> impl_future!(>) + where + P: Provider, + T: Transport + Clone; + + /// Fills in the transaction request with the fillable properties. + fn fill( + &self, + fillable: Self::Fillable, + tx: SendableTx, + ) -> impl_future!(>>); + + /// Prepares and fills the transaction request with the fillable properties. + fn prepare_and_fill( + &self, + provider: &P, + tx: SendableTx, + ) -> impl_future!(>>) + where + P: Provider, + T: Transport + Clone, + { + async move { + if tx.is_envelope() { + return Ok(tx); + } + + let fillable = + self.prepare(provider, tx.as_builder().expect("checked by is_envelope")).await?; + + self.fill(fillable, tx).await + } + } +} + +/// A [`Provider`] that applies one or more [`TxFiller`]s. +/// +/// Fills arbitrary properties in a transaction request by composing multiple +/// fill layers. This struct should always be the outermost layer in a provider +/// stack, and this is enforced when using [`ProviderBuilder::filler`] to +/// construct this layer. +/// +/// Users should NOT use this struct directly. Instead, use +/// [`ProviderBuilder::filler`] to construct and apply it to a stack. +/// +/// [`ProviderBuilder::filler`]: crate::ProviderBuilder::filler +#[derive(Debug, Clone)] +pub struct FillProvider +where + F: TxFiller, + P: Provider, + T: Transport + Clone, + N: Network, +{ + inner: P, + filler: F, + _pd: PhantomData (T, N)>, +} + +impl FillProvider +where + F: TxFiller, + P: Provider, + T: Transport + Clone, + N: Network, +{ + /// Creates a new `FillProvider` with the given filler and inner provider. + pub fn new(inner: P, filler: F) -> Self { + Self { inner, filler, _pd: PhantomData } + } + + /// Joins a filler to this provider + pub fn join_with>( + self, + other: Other, + ) -> FillProvider, P, T, N> { + self.filler.join_with(other).layer(self.inner) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Provider for FillProvider +where + F: TxFiller, + P: Provider, + T: Transport + Clone, + N: Network, +{ + fn root(&self) -> &RootProvider { + self.inner.root() + } + + async fn send_transaction_internal( + &self, + mut tx: SendableTx, + ) -> TransportResult> { + let mut count = 0; + + while self.filler.continue_filling(&tx) { + tx = self.filler.prepare_and_fill(&self.inner, tx).await?; + + count += 1; + if count >= 20 { + panic!( + "Tx filler loop detected. This indicates a bug in some filler implementation. Please file an issue containing your tx filler set." + ); + } + } + + if let Some(builder) = tx.as_builder() { + if let FillerControlFlow::Missing(missing) = self.filler.status(builder) { + // TODO: improve this. + // blocked by #431 + let message = format!("missing properties: {:?}", missing); + return Err(RpcError::local_usage_str(&message)); + } + } + + // Errors in tx building happen further down the stack. + self.inner.send_transaction_internal(tx).await + } +} diff --git a/crates/provider/src/fillers/nonce.rs b/crates/provider/src/fillers/nonce.rs new file mode 100644 index 00000000000..ebe174f58c5 --- /dev/null +++ b/crates/provider/src/fillers/nonce.rs @@ -0,0 +1,163 @@ +use crate::{ + fillers::{FillerControlFlow, TxFiller}, + provider::SendableTx, + Provider, +}; +use alloy_network::{Network, TransactionBuilder}; +use alloy_primitives::Address; +use alloy_transport::{Transport, TransportResult}; +use dashmap::DashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// A [`TxFiller`] that fills nonces on transactions. +/// +/// The filler will fetch the transaction count for any new account it sees, +/// store it locally and increment the locally stored nonce as transactions are +/// sent via [`Provider::send_transaction`]. +/// +/// # Note +/// +/// - If the transaction request does not have a sender set, this layer will +/// not fill nonces. +/// - Using two providers with their own nonce layer can potentially fill +/// invalid nonces if transactions are sent from the same address, as the next +/// nonce to be used is cached internally in the layer. +/// +/// # Example +/// +/// ``` +/// # use alloy_network::{NetworkSigner, EthereumSigner, Ethereum}; +/// # use alloy_rpc_types::TransactionRequest; +/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider}; +/// # async fn test + Clone>(url: url::Url, signer: S) -> Result<(), Box> { +/// let provider = ProviderBuilder::new() +/// .with_nonce_management() +/// .signer(signer) +/// .on_http(url)?; +/// +/// provider.send_transaction(TransactionRequest::default()).await; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Default)] +pub struct NonceFiller { + nonces: DashMap>>>, +} + +impl TxFiller for NonceFiller { + type Fillable = u64; + + fn status(&self, tx: &::TransactionRequest) -> FillerControlFlow { + if tx.nonce().is_some() { + return FillerControlFlow::Finished; + } + if tx.from().is_none() { + return FillerControlFlow::missing("NonceManager", &["from"]); + } + FillerControlFlow::Ready + } + + async fn prepare( + &self, + provider: &P, + tx: &N::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + { + let from = tx.from().expect("checked by 'ready()'"); + self.get_next_nonce(provider, from).await + } + + async fn fill( + &self, + nonce: Self::Fillable, + mut tx: SendableTx, + ) -> TransportResult> { + if let Some(builder) = tx.as_mut_builder() { + builder.set_nonce(nonce); + } + Ok(tx) + } +} + +impl NonceFiller { + /// Get the next nonce for the given account. + async fn get_next_nonce(&self, provider: &P, from: Address) -> TransportResult + where + P: Provider, + N: Network, + T: Transport + Clone, + { + // locks dashmap internally for a short duration to clone the `Arc` + let mutex = Arc::clone(self.nonces.entry(from).or_default().value()); + + // locks the value (does not lock dashmap) + let mut nonce = mutex.lock().await; + match *nonce { + Some(ref mut nonce) => { + *nonce += 1; + Ok(*nonce) + } + None => { + // initialize the nonce if we haven't seen this account before + let initial_nonce = provider.get_transaction_count(from, None).await?; + *nonce = Some(initial_nonce); + Ok(initial_nonce) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ProviderBuilder; + use alloy_primitives::{address, U256}; + use alloy_rpc_types::TransactionRequest; + + #[tokio::test] + async fn no_nonce_if_sender_unset() { + let (provider, _anvil) = + ProviderBuilder::new().with_nonce_management().on_anvil_with_signer(); + + let tx = TransactionRequest { + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + gas_price: Some(20e9 as u128), + gas: Some(21000), + ..Default::default() + }; + + // errors because signer layer expects nonce to be set, which it is not + assert!(provider.send_transaction(tx).await.is_err()); + } + + #[tokio::test] + async fn increments_nonce() { + let (provider, anvil) = + ProviderBuilder::new().with_nonce_management().on_anvil_with_signer(); + + let from = anvil.addresses()[0]; + let tx = TransactionRequest { + from: Some(from), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + gas_price: Some(20e9 as u128), + gas: Some(21000), + ..Default::default() + }; + + let pending = provider.send_transaction(tx.clone()).await.unwrap(); + let tx_hash = pending.watch().await.unwrap(); + let mined_tx = provider.get_transaction_by_hash(tx_hash).await.expect("tx didn't finalize"); + assert_eq!(mined_tx.nonce, 0); + + let pending = provider.send_transaction(tx).await.unwrap(); + let tx_hash = pending.watch().await.unwrap(); + let mined_tx = provider.get_transaction_by_hash(tx_hash).await.expect("tx didn't finalize"); + assert_eq!(mined_tx.nonce, 1); + } +} diff --git a/crates/provider/src/fillers/signer.rs b/crates/provider/src/fillers/signer.rs new file mode 100644 index 00000000000..e1fb14079f6 --- /dev/null +++ b/crates/provider/src/fillers/signer.rs @@ -0,0 +1,125 @@ +use crate::{provider::SendableTx, Provider}; +use alloy_json_rpc::RpcError; +use alloy_network::{Network, NetworkSigner, TransactionBuilder}; +use alloy_transport::{Transport, TransportResult}; + +use super::{FillerControlFlow, TxFiller}; + +/// A layer that signs transactions locally. +/// +/// The layer uses a [`NetworkSigner`] to sign transactions sent using +/// [`Provider::send_transaction`] locally before passing them to the node with +/// [`Provider::send_raw_transaction`]. +/// +/// # Example +/// +/// ``` +/// # use alloy_network::{NetworkSigner, EthereumSigner, Ethereum}; +/// # use alloy_rpc_types::TransactionRequest; +/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider}; +/// # async fn test + Clone>(url: url::Url, signer: S) -> Result<(), Box> { +/// let provider = ProviderBuilder::new() +/// .signer(signer) +/// .on_http(url)?; +/// +/// provider.send_transaction(TransactionRequest::default()).await; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct SignerFiller { + signer: S, +} + +impl SignerFiller { + /// Creates a new signing layer with the given signer. + pub const fn new(signer: S) -> Self { + Self { signer } + } +} + +impl TxFiller for SignerFiller +where + N: Network, + S: NetworkSigner + Clone, +{ + type Fillable = (); + + fn status(&self, tx: &::TransactionRequest) -> FillerControlFlow { + if tx.can_build() { + FillerControlFlow::Ready + } else { + // Blocked by #431 + // https://github.com/alloy-rs/alloy/pull/431 + FillerControlFlow::Missing(vec![("Signer", &["TODO"])]) + } + } + + async fn prepare( + &self, + _provider: &P, + _tx: &::TransactionRequest, + ) -> TransportResult + where + P: Provider, + T: Transport + Clone, + { + Ok(()) + } + + async fn fill( + &self, + _fillable: Self::Fillable, + tx: SendableTx, + ) -> TransportResult> { + let builder = match tx { + SendableTx::Builder(builder) => builder, + _ => return Ok(tx), + }; + + let envelope = builder.build(&self.signer).await.map_err(RpcError::local_usage)?; + + Ok(SendableTx::Envelope(envelope)) + } +} + +#[cfg(feature = "reqwest")] +#[cfg(test)] +mod tests { + use crate::{Provider, ProviderBuilder}; + use alloy_primitives::{address, b256, U256}; + use alloy_rpc_types::TransactionRequest; + + #[tokio::test] + async fn poc() { + let (provider, _anvil) = ProviderBuilder::new().on_anvil_with_signer(); + + let tx = TransactionRequest { + nonce: Some(0), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + gas_price: Some(20e9 as u128), + gas: Some(21000), + ..Default::default() + }; + + let builder = provider.send_transaction(tx).await.unwrap(); + let node_hash = *builder.tx_hash(); + assert_eq!( + node_hash, + b256!("eb56033eab0279c6e9b685a5ec55ea0ff8d06056b62b7f36974898d4fbb57e64") + ); + + let pending = builder.register().await.unwrap(); + let local_hash = *pending.tx_hash(); + assert_eq!(local_hash, node_hash); + + let local_hash2 = pending.await.unwrap(); + assert_eq!(local_hash2, node_hash); + + let receipt = + provider.get_transaction_receipt(local_hash2).await.unwrap().expect("no receipt"); + let receipt_hash = receipt.transaction_hash; + assert_eq!(receipt_hash, node_hash); + } +} diff --git a/crates/provider/src/layers/gas.rs b/crates/provider/src/layers/gas.rs deleted file mode 100644 index 0b4d15949d3..00000000000 --- a/crates/provider/src/layers/gas.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::{ - utils::Eip1559Estimation, PendingTransactionBuilder, Provider, ProviderLayer, RootProvider, -}; -use alloy_json_rpc::RpcError; -use alloy_network::{Network, TransactionBuilder}; -use alloy_rpc_types::BlockNumberOrTag; -use alloy_transport::{Transport, TransportError, TransportResult}; -use async_trait::async_trait; -use futures::FutureExt; -use std::marker::PhantomData; - -/// A layer that populates gas related fields in transaction requests if unset. -/// -/// Gas related fields are gas_price, gas_limit, max_fee_per_gas and max_priority_fee_per_gas. -/// -/// The layer fetches the estimations for these via the [`Provider::get_gas_price`], -/// [`Provider::estimate_gas`] and [`Provider::estimate_eip1559_fees`] methods. -/// -/// If you use layers that redirect the behavior of [`Provider::send_transaction`] (e.g. -/// [`crate::layers::SignerLayer`]), you should add this layer before those. -/// -/// Note: -/// - If none of the gas related fields are set, the layer first assumes it's a EIP-1559 tx and -/// populates the gas_limit, max_fee_per_gas and max_priority_fee_per_gas fields. -/// - If the network does not support EIP-1559, it will process as a legacy tx and populate the -/// gas_limit and gas_price fields. -/// - If the gas_price is already set by the user, it will process as a legacy tx and populate -/// the gas_limit field if unset. -/// -/// # Example -/// -/// ```rs -/// # async fn test>(transport: T, signer: S) { -/// let provider = ProviderBuilder::new() -/// .with_nonce_management() -/// .with_gas_estimation() -/// .signer(EthereumSigner::from(signer)) // note the order! -/// .provider(RootProvider::new(transport)); -/// -/// provider.send_transaction(TransactionRequest::default()).await; -/// # } -#[derive(Debug, Clone, Copy, Default)] -pub struct GasEstimatorLayer; - -impl ProviderLayer for GasEstimatorLayer -where - P: Provider, - N: Network, - T: Transport + Clone, -{ - type Provider = GasEstimatorProvider; - fn layer(&self, inner: P) -> Self::Provider { - GasEstimatorProvider { inner, _phantom: PhantomData } - } -} - -/// A provider that estimates gas for transactions. -/// -/// Note: This provider requires the chain_id to be set in the transaction request if it's a -/// EIP1559. -/// -/// You cannot construct this directly, use [`ProviderBuilder`] with a [`GasEstimatorLayer`]. -/// -/// [`ProviderBuilder`]: crate::ProviderBuilder -#[derive(Debug, Clone)] -pub struct GasEstimatorProvider -where - N: Network, - T: Transport + Clone, - P: Provider, -{ - inner: P, - _phantom: PhantomData<(N, T)>, -} - -impl GasEstimatorProvider -where - N: Network, - T: Transport + Clone, - P: Provider, -{ - /// Gets the gas_price to be used in legacy txs. - async fn get_gas_price(&self) -> TransportResult { - self.inner.get_gas_price().await - } - - /// Gets the gas_limit to be used in txs. - async fn get_gas_estimate(&self, tx: &N::TransactionRequest) -> TransportResult { - self.inner.estimate_gas(tx, None).await - } - - /// Gets the max_fee_per_gas and max_priority_fee_per_gas to be used in EIP-1559 txs. - async fn get_eip1559_fees_estimate(&self) -> TransportResult { - self.inner.estimate_eip1559_fees(None).await - } - - /// 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( - &self, - tx: &mut N::TransactionRequest, - ) -> Result<(), TransportError> { - let gas_estimate_fut = if let Some(gas_limit) = tx.gas_limit() { - async move { Ok(gas_limit) }.left_future() - } else { - async { self.get_gas_estimate(tx).await }.right_future() - }; - - let eip1559_fees_fut = if let (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) = - (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas()) - { - async move { Ok(Eip1559Estimation { max_fee_per_gas, max_priority_fee_per_gas }) } - .left_future() - } else { - async { self.get_eip1559_fees_estimate().await }.right_future() - }; - - match futures::try_join!(gas_estimate_fut, eip1559_fees_fut) { - Ok((gas_limit, eip1559_fees)) => { - tx.set_gas_limit(gas_limit); - tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); - tx.set_max_priority_fee_per_gas(eip1559_fees.max_priority_fee_per_gas); - Ok(()) - } - Err(RpcError::UnsupportedFeature("eip1559")) => self.handle_legacy_tx(tx).await, - Err(e) => Err(e), - } - } - - /// 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> { - 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() - } else { - async { self.get_gas_estimate(tx).await }.right_future() - }; - - futures::try_join!(gas_price_fut, gas_limit_fut).map(|(gas_price, gas_limit)| { - tx.set_gas_price(gas_price); - tx.set_gas_limit(gas_limit); - tx - })?; - - Ok(()) - } - - /// There are a few ways to obtain the blob base fee for an EIP-4844 transaction: - /// - /// * `eth_blobBaseFee`: Returns the fee for the next block directly. - /// * `eth_feeHistory`: Returns the same info as for the EIP-1559 fees. - /// * retrieving it from the "pending" block directly. - /// - /// At the time of this writing support for EIP-4844 fees is lacking, hence we're defaulting to - /// requesting the fee from the "pending" block. - async fn handle_eip4844_tx( - &self, - tx: &mut N::TransactionRequest, - ) -> Result<(), TransportError> { - // TODO this can be optimized together with 1559 dynamic fees once blob fee support on - // eth_feeHistory is more widely supported - if tx.get_blob_sidecar().is_some() && tx.max_fee_per_blob_gas().is_none() { - let next_blob_fee = self - .inner - .get_block_by_number(BlockNumberOrTag::Latest, false) - .await? - .ok_or(RpcError::NullResp)? - .header - .next_block_blob_fee() - .ok_or(RpcError::UnsupportedFeature("eip4844"))?; - tx.set_max_fee_per_blob_gas(next_blob_fee); - } - - Ok(()) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Provider for GasEstimatorProvider -where - N: Network, - T: Transport + Clone, - P: Provider, -{ - fn root(&self) -> &RootProvider { - self.inner.root() - } - - async fn send_transaction( - &self, - mut tx: N::TransactionRequest, - ) -> TransportResult> { - if tx.gas_price().is_none() { - // Assume its a EIP1559 tx - // Populate the following gas_limit, max_fee_per_gas and max_priority_fee_per_gas fields - // if unset. - self.handle_eip1559_tx(&mut tx).await?; - // TODO: this can be done more elegantly once we can set EIP-1559 and EIP-4844 fields - // with a single eth_feeHistory request - self.handle_eip4844_tx(&mut tx).await?; - } else { - // Assume its a legacy tx - // Populate only the gas_limit field if unset. - self.handle_legacy_tx(&mut tx).await?; - } - self.inner.send_transaction(tx).await - } -} - -#[cfg(feature = "reqwest")] -#[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 no_gas_price_or_limit() { - let anvil = Anvil::new().spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - - let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); - - let provider = ProviderBuilder::new() - .with_nonce_management() - .with_gas_estimation() - .signer(EthereumSigner::from(wallet)) - .provider(RootProvider::new(RpcClient::new(http, true))); - - // GasEstimationLayer requires chain_id to be set to handle EIP-1559 tx - let tx = TransactionRequest { - from: Some(anvil.addresses()[0]), - value: Some(U256::from(100)), - to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), - chain_id: Some(31337), - ..Default::default() - }; - - let tx = provider.send_transaction(tx).await.unwrap(); - - let tx = tx.get_receipt().await.unwrap(); - - assert_eq!(tx.effective_gas_price, 0x3b9aca00); - assert_eq!(tx.gas_used, Some(0x5208)); - } - - #[tokio::test] - async fn no_gas_limit() { - let anvil = Anvil::new().spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - - let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); - - let provider = ProviderBuilder::new() - .with_nonce_management() - .with_gas_estimation() - .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(), - gas_price: Some(gas_price), - ..Default::default() - }; - - let tx = provider.send_transaction(tx).await.unwrap(); - - let tx = tx.get_receipt().await.unwrap(); - - assert_eq!(tx.gas_used, Some(0x5208)); - } - - #[tokio::test] - async fn non_eip1559_network() { - let anvil = Anvil::new().arg("--hardfork").arg("frontier").spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - - let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); - - let provider = ProviderBuilder::new() - .with_nonce_management() - .with_gas_estimation() - .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(), - // chain_id: Some(31337), Not required as this will fallback to legacy_tx - ..Default::default() - }; - - let tx = provider.send_transaction(tx).await.unwrap(); - - let tx = tx.get_receipt().await.unwrap(); - - assert_eq!(tx.effective_gas_price, 0x6fc23ac0); - } -} diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs deleted file mode 100644 index 5091f232e4f..00000000000 --- a/crates/provider/src/layers/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Provider layers. -//! -//! Layers decorate a `Provider`, transforming various inputs and outputs of the root provider, -//! depending on the layers used. -mod signer; -pub use signer::{SignerLayer, SignerProvider}; - -mod nonce; -pub use nonce::{ManagedNonceProvider, NonceManagerLayer}; - -mod gas; -pub use gas::{GasEstimatorLayer, GasEstimatorProvider}; diff --git a/crates/provider/src/layers/nonce.rs b/crates/provider/src/layers/nonce.rs deleted file mode 100644 index f00a68c3f52..00000000000 --- a/crates/provider/src/layers/nonce.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::{PendingTransactionBuilder, Provider, ProviderLayer, RootProvider}; -use alloy_network::{Ethereum, Network, TransactionBuilder}; -use alloy_primitives::Address; -use alloy_transport::{Transport, TransportResult}; -use async_trait::async_trait; -use dashmap::DashMap; -use std::{marker::PhantomData, sync::Arc}; -use tokio::sync::Mutex; - -/// A layer that fills nonces on transactions. -/// -/// The layer will fetch the transaction count for any new account it sees, store it locally and -/// increment the locally stored nonce as transactions are sent via [`Provider::send_transaction`]. -/// -/// If you use layers that redirect the behavior of [`Provider::send_transaction`] (e.g. -/// [`SignerLayer`]), you should add this layer before those. -/// -/// # Note -/// -/// - If the transaction request does not have a sender set, this layer will not fill nonces. -/// - Using two providers with their own nonce layer can potentially fill invalid nonces if -/// transactions are sent from the same address, as the next nonce to be used is cached internally -/// in the layer. -/// -/// # Example -/// -/// ```rs -/// # async fn test>(transport: T, signer: S) { -/// let provider = ProviderBuilder::new() -/// .with_nonce_management() -/// .signer(EthereumSigner::from(signer)) // note the order! -/// .provider(RootProvider::new(transport)); -/// -/// provider.send_transaction(TransactionRequest::default()).await; -/// # } -/// ``` -/// -/// [`SignerLayer`]: crate::layers::SignerLayer -#[derive(Debug, Clone, Copy)] -pub struct NonceManagerLayer; - -impl ProviderLayer for NonceManagerLayer -where - P: Provider, - T: Transport + Clone, - N: Network, -{ - type Provider = ManagedNonceProvider; - - fn layer(&self, inner: P) -> Self::Provider { - ManagedNonceProvider { inner, nonces: DashMap::default(), _phantom: PhantomData } - } -} - -/// A provider that manages account nonces. -/// -/// Fills nonces for transaction requests if unset. -/// -/// # Note -/// -/// If the transaction requests do not have a sender set, this provider will not set nonces. -/// -/// You cannot construct this provider directly. Use [`ProviderBuilder`] with a -/// [`NonceManagerLayer`]. -/// -/// [`ProviderBuilder`]: crate::ProviderBuilder -#[derive(Debug, Clone)] -pub struct ManagedNonceProvider -where - T: Transport + Clone, - P: Provider, - N: Network, -{ - inner: P, - nonces: DashMap>>>, - _phantom: PhantomData<(T, N)>, -} - -impl ManagedNonceProvider -where - N: Network, - T: Transport + Clone, - P: Provider, -{ - 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()); - - // locks the value (does not lock dashmap) - let mut nonce = mutex.lock().await; - match *nonce { - Some(ref mut nonce) => { - *nonce += 1; - Ok(*nonce) - } - None => { - // initialize the nonce if we haven't seen this account before - let initial_nonce = self.inner.get_transaction_count(from, None).await?; - *nonce = Some(initial_nonce); - Ok(initial_nonce) - } - } - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Provider for ManagedNonceProvider -where - T: Transport + Clone, - P: Provider, - N: Network, -{ - #[inline] - fn root(&self) -> &RootProvider { - self.inner.root() - } - - async fn send_transaction( - &self, - mut tx: N::TransactionRequest, - ) -> TransportResult> { - if tx.nonce().is_none() { - if let Some(from) = tx.from() { - tx.set_nonce(self.get_next_nonce(from).await?); - } - } - - self.inner.send_transaction(tx).await - } -} - -#[cfg(feature = "reqwest")] -#[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 no_nonce_if_sender_unset() { - let anvil = Anvil::new().spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - - let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); - - let provider = ProviderBuilder::new() - .with_nonce_management() - .signer(EthereumSigner::from(wallet)) - .provider(RootProvider::new(RpcClient::new(http, true))); - - let tx = TransactionRequest { - value: Some(U256::from(100)), - to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), - gas_price: Some(20e9 as u128), - gas: Some(21000), - ..Default::default() - }; - - // errors because signer layer expects nonce to be set, which it is not - assert!(provider.send_transaction(tx.clone()).await.is_err()); - } - - #[tokio::test] - async fn increments_nonce() { - let anvil = Anvil::new().spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - - let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); - - let provider = ProviderBuilder::new() - .with_nonce_management() - .signer(EthereumSigner::from(wallet)) - .provider(RootProvider::new(RpcClient::new(http, true))); - - let from = anvil.addresses()[0]; - let tx = TransactionRequest { - from: Some(from), - value: Some(U256::from(100)), - to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), - gas_price: Some(20e9 as u128), - gas: Some(21000), - ..Default::default() - }; - - let pending = provider.send_transaction(tx.clone()).await.unwrap(); - let tx_hash = pending.watch().await.unwrap(); - let mined_tx = provider.get_transaction_by_hash(tx_hash).await.expect("tx didn't finalize"); - assert_eq!(mined_tx.nonce, 0); - - let pending = provider.send_transaction(tx).await.unwrap(); - let tx_hash = pending.watch().await.unwrap(); - let mined_tx = provider.get_transaction_by_hash(tx_hash).await.expect("tx didn't finalize"); - assert_eq!(mined_tx.nonce, 1); - } -} diff --git a/crates/provider/src/layers/signer.rs b/crates/provider/src/layers/signer.rs deleted file mode 100644 index 78d0013ca7f..00000000000 --- a/crates/provider/src/layers/signer.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::{PendingTransactionBuilder, Provider, ProviderLayer, RootProvider}; -use alloy_network::{eip2718::Encodable2718, Ethereum, Network, NetworkSigner, TransactionBuilder}; -use alloy_transport::{Transport, TransportErrorKind, TransportResult}; -use async_trait::async_trait; -use std::marker::PhantomData; - -/// A layer that signs transactions locally. -/// -/// The layer uses a [`NetworkSigner`] to sign transactions sent using -/// [`Provider::send_transaction`] locally before passing them to the node with -/// [`Provider::send_raw_transaction`]. -/// -/// If you have other layers that depend on [`Provider::send_transaction`] being invoked, add those -/// first. -/// -/// # Example -/// -/// ```rs -/// # async fn test>(transport: T, signer: S) { -/// let provider = ProviderBuilder::new() -/// .signer(EthereumSigner::from(signer)) -/// .provider(RootProvider::new(transport)); -/// -/// provider.send_transaction(TransactionRequest::default()).await; -/// # } -/// ``` -#[derive(Debug)] -pub struct SignerLayer { - signer: S, -} - -impl SignerLayer { - /// Creates a new signing layer with the given signer. - pub const fn new(signer: S) -> Self { - Self { signer } - } -} - -impl ProviderLayer for SignerLayer -where - P: Provider, - T: Transport + Clone, - S: NetworkSigner + Clone, - N: Network, -{ - type Provider = SignerProvider; - - fn layer(&self, inner: P) -> Self::Provider { - SignerProvider { inner, signer: self.signer.clone(), _phantom: PhantomData } - } -} - -/// A locally-signing provider. -/// -/// Signs transactions locally using a [`NetworkSigner`]. -/// -/// # Note -/// -/// You cannot construct this provider directly. Use [`ProviderBuilder`] with a [`SignerLayer`]. -/// -/// [`ProviderBuilder`]: crate::ProviderBuilder -#[derive(Debug)] -pub struct SignerProvider -where - T: Transport + Clone, - P: Provider, - N: Network, -{ - inner: P, - signer: S, - _phantom: PhantomData<(T, N)>, -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Provider for SignerProvider -where - T: Transport + Clone, - P: Provider, - S: NetworkSigner, - N: Network, -{ - #[inline] - fn root(&self) -> &RootProvider { - self.inner.root() - } - - async fn send_transaction( - &self, - tx: N::TransactionRequest, - ) -> TransportResult> { - let envelope = tx.build(&self.signer).await.map_err(TransportErrorKind::custom)?; - let rlp = envelope.encoded_2718(); - - self.inner.send_raw_transaction(&rlp).await - } -} - -#[cfg(feature = "reqwest")] -#[cfg(test)] -mod tests { - use crate::{Provider, ProviderBuilder, RootProvider}; - use alloy_network::EthereumSigner; - use alloy_node_bindings::Anvil; - use alloy_primitives::{address, b256, U256}; - use alloy_rpc_client::RpcClient; - use alloy_rpc_types::TransactionRequest; - use alloy_transport_http::Http; - use reqwest::Client; - - #[tokio::test] - async fn poc() { - let anvil = Anvil::new().spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - - let wallet = alloy_signer_wallet::Wallet::from(anvil.keys()[0].clone()); - - let provider = ProviderBuilder::new() - .signer(EthereumSigner::from(wallet)) - .provider(RootProvider::new(RpcClient::new(http, true))); - - let tx = TransactionRequest { - nonce: Some(0), - value: Some(U256::from(100)), - to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), - gas_price: Some(20e9 as u128), - gas: Some(21000), - ..Default::default() - }; - - let builder = provider.send_transaction(tx).await.unwrap(); - let node_hash = *builder.tx_hash(); - assert_eq!( - node_hash, - b256!("eb56033eab0279c6e9b685a5ec55ea0ff8d06056b62b7f36974898d4fbb57e64") - ); - - let pending = builder.register().await.unwrap(); - let local_hash = *pending.tx_hash(); - assert_eq!(local_hash, node_hash); - - let local_hash2 = pending.await.unwrap(); - assert_eq!(local_hash2, node_hash); - - let receipt = - provider.get_transaction_receipt(local_hash2).await.unwrap().expect("no receipt"); - let receipt_hash = receipt.transaction_hash; - assert_eq!(receipt_hash, node_hash); - } -} diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index f4e7a5ab352..7eb241d4fff 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -39,7 +39,7 @@ extern crate tracing; mod builder; pub use builder::{Identity, ProviderBuilder, ProviderLayer, Stack}; -pub mod layers; +pub mod fillers; mod chain; diff --git a/crates/provider/src/provider.rs b/crates/provider/src/provider.rs index 66d4714f767..d9e9b065c81 100644 --- a/crates/provider/src/provider.rs +++ b/crates/provider/src/provider.rs @@ -6,8 +6,9 @@ use crate::{ utils::{self, Eip1559Estimation, EstimatorFunction}, PendingTransactionBuilder, }; +use alloy_eips::eip2718::Encodable2718; use alloy_json_rpc::{RpcError, RpcParam, RpcReturn}; -use alloy_network::{Ethereum, Network, TransactionBuilder}; +use alloy_network::{Ethereum, Network}; use alloy_primitives::{ hex, Address, BlockHash, BlockNumber, Bytes, StorageKey, StorageValue, TxHash, B256, U128, U256, U64, @@ -46,6 +47,58 @@ use alloy_pubsub::{PubSubFrontend, Subscription}; /// See [`PollerBuilder`] for more details. pub type FilterPollerBuilder = PollerBuilder>; +/// A transaction that can be sent. This is either a builder or an envelope. +/// +/// This type is used to allow for fillers to convert a builder into an envelope +/// without changing the user-facing API. +/// +/// Users should NOT use this type directly. It should only be used as an +/// implementation detail of [`Provider::send_transaction_internal`]. +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SendableTx { + /// A transaction that is not yet signed. + Builder(N::TransactionRequest), + /// A transaction that is signed and fully constructed. + Envelope(N::TxEnvelope), +} + +impl SendableTx { + /// Fallible cast to an unbuilt transaction request. + pub fn as_mut_builder(&mut self) -> Option<&mut N::TransactionRequest> { + match self { + Self::Builder(tx) => Some(tx), + _ => None, + } + } + + /// Fallible cast to an unbuilt transaction request. + pub const fn as_builder(&self) -> Option<&N::TransactionRequest> { + match self { + Self::Builder(tx) => Some(tx), + _ => None, + } + } + + /// Checks if the transaction is a builder. + pub const fn is_builder(&self) -> bool { + matches!(self, Self::Builder(_)) + } + + /// Check if the transaction is an envelope. + pub const fn is_envelope(&self) -> bool { + matches!(self, Self::Envelope(_)) + } + + /// Fallible cast to a built transaction envelope. + pub const fn as_envelope(&self) -> Option<&N::TxEnvelope> { + match self { + Self::Envelope(tx) => Some(tx), + _ => None, + } + } +} + /// The root provider manages the RPC client and the heartbeat. It is at the /// base of every provider stack. pub struct RootProvider { @@ -595,35 +648,10 @@ pub trait Provider: self.client().request("eth_getBlockByNumber", (number, hydrate)).await } - /// Populates the legacy gas price field of the given transaction request. - async fn populate_gas( - &self, - tx: &mut N::TransactionRequest, - block: Option, - ) -> TransportResult<()> { - let gas = self.estimate_gas(&*tx, block).await; - - gas.map(|gas| tx.set_gas_limit(gas)) - } - - /// Populates the EIP-1559 gas price fields of the given transaction request. - async fn populate_gas_eip1559( - &self, - tx: &mut N::TransactionRequest, - estimator: Option, - ) -> TransportResult<()> { - let gas = self.estimate_eip1559_fees(estimator).await; - - gas.map(|estimate| { - tx.set_max_fee_per_gas(estimate.max_fee_per_gas); - tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas); - }) - } - /// Broadcasts a transaction to the network. /// - /// Returns a type that can be used to configure how and when to await the transaction's - /// confirmation. + /// Returns a type that can be used to configure how and when to await the + /// transaction's confirmation. /// /// # Examples /// @@ -644,8 +672,33 @@ pub trait Provider: &self, tx: N::TransactionRequest, ) -> TransportResult> { - let tx_hash = self.client().request("eth_sendTransaction", (tx,)).await?; - Ok(PendingTransactionBuilder::new(self.root(), tx_hash)) + self.send_transaction_internal(SendableTx::Builder(tx)).await + } + + /// + /// This method allows [`ProviderLayer`] and [`TxFiller`] to bulid the + /// transaction and send it to the network without changing user-facing + /// APIs. Generally implementors should NOT override this method. + /// + /// [`send_transaction`]: Self::send_transaction + /// [`ProviderLayer`]: crate::ProviderLayer + /// [`TxFiller`]: crate::TxFiller + #[doc(hidden)] + async fn send_transaction_internal( + &self, + tx: SendableTx, + ) -> TransportResult> { + match tx { + SendableTx::Builder(tx) => { + let tx_hash = self.client().request("eth_sendTransaction", (tx,)).await?; + Ok(PendingTransactionBuilder::new(self.root(), tx_hash)) + } + SendableTx::Envelope(tx) => { + let mut encoded_tx = vec![]; + tx.encode_2718(&mut encoded_tx); + self.send_raw_transaction(&encoded_tx).await + } + } } /// Broadcasts a raw transaction RLP bytes to the network. @@ -653,9 +706,9 @@ pub trait Provider: /// See [`send_transaction`](Self::send_transaction) for more details. async fn send_raw_transaction( &self, - rlp_bytes: &[u8], + encoded_tx: &[u8], ) -> TransportResult> { - let rlp_hex = hex::encode_prefixed(rlp_bytes); + let rlp_hex = hex::encode_prefixed(encoded_tx); let tx_hash = self.client().request("eth_sendRawTransaction", (rlp_hex,)).await?; Ok(PendingTransactionBuilder::new(self.root(), tx_hash)) } diff --git a/crates/rpc-types/src/eth/transaction/request.rs b/crates/rpc-types/src/eth/transaction/request.rs index 8d3a568407f..fb51393af01 100644 --- a/crates/rpc-types/src/eth/transaction/request.rs +++ b/crates/rpc-types/src/eth/transaction/request.rs @@ -220,6 +220,12 @@ impl TransactionInput { } } +impl From> for TransactionInput { + fn from(input: Vec) -> Self { + Self { input: Some(input.into()), data: None } + } +} + impl From for TransactionInput { fn from(input: Bytes) -> Self { Self { input: Some(input), data: None } diff --git a/crates/transport/src/common.rs b/crates/transport/src/common.rs index 5018a696a33..9e56a8fe93b 100644 --- a/crates/transport/src/common.rs +++ b/crates/transport/src/common.rs @@ -23,8 +23,6 @@ impl Authorization { return None; } - dbg!(username, password); - (!username.is_empty() || !password.is_empty()).then(|| Self::basic(username, password)) }