alloy_provider/fillers/
gas.rs

1use std::future::IntoFuture;
2
3use crate::{
4    fillers::{FillerControlFlow, TxFiller},
5    provider::SendableTx,
6    utils::Eip1559Estimation,
7    Provider,
8};
9use alloy_eips::eip4844::BLOB_TX_MIN_BLOB_GASPRICE;
10use alloy_json_rpc::RpcError;
11use alloy_network::{Network, TransactionBuilder, TransactionBuilder4844};
12use alloy_rpc_types_eth::BlockNumberOrTag;
13use alloy_transport::TransportResult;
14use futures::FutureExt;
15
16/// An enum over the different types of gas fillable.
17#[doc(hidden)]
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum GasFillable {
20    Legacy { gas_limit: u64, gas_price: u128 },
21    Eip1559 { gas_limit: u64, estimate: Eip1559Estimation },
22}
23
24/// A [`TxFiller`] that populates gas related fields in transaction requests if
25/// unset.
26///
27/// Gas related fields are gas_price, gas_limit, max_fee_per_gas
28/// and max_priority_fee_per_gas. For EIP-4844 `max_fee_per_blob_gas`,
29/// see [`BlobGasFiller`].
30///
31/// The layer fetches the estimations for these via the
32/// [`Provider::get_gas_price`], [`Provider::estimate_gas`] and
33/// [`Provider::estimate_eip1559_fees`] methods.
34///
35/// ## Note:
36///
37/// The layer will populate gas fields based on the following logic:
38/// - if `gas_price` is set, it will process as a legacy tx and populate the `gas_limit` field if
39///   unset.
40/// - if `access_list` is set, it will process as a 2930 tx and populate the `gas_limit` and
41///   `gas_price` field if unset.
42/// - if `blob_sidecar` is set, it will process as an EIP-4844 tx and populate the `gas_limit`,
43///   `max_fee_per_gas`, and `max_priority_fee_per_gas` fields if unset. The `max_fee_per_blob_gas`
44///   is populated by [`BlobGasFiller`].
45/// - Otherwise, it will process as a EIP-1559 tx and populate the `gas_limit`, `max_fee_per_gas`
46///   and `max_priority_fee_per_gas` fields if unset.
47/// - If the network does not support EIP-1559, it will fallback to the legacy tx and populate the
48///   `gas_limit` and `gas_price` fields if unset.
49///
50/// # Example
51///
52/// ```
53/// # use alloy_network::{Ethereum};
54/// # use alloy_rpc_types_eth::TransactionRequest;
55/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider};
56/// # use alloy_signer_local::PrivateKeySigner;
57/// # async fn test(url: url::Url) -> Result<(), Box<dyn std::error::Error>> {
58/// let pk: PrivateKeySigner = "0x...".parse()?;
59/// let provider = ProviderBuilder::<_, _, Ethereum>::default()
60///     .with_gas_estimation()
61///     .wallet(pk)
62///     .connect_http(url);
63///
64/// provider.send_transaction(TransactionRequest::default()).await;
65/// # Ok(())
66/// # }
67/// ```
68#[derive(Clone, Copy, Debug, Default)]
69pub struct GasFiller;
70
71impl GasFiller {
72    async fn prepare_legacy<P, N>(
73        &self,
74        provider: &P,
75        tx: &N::TransactionRequest,
76    ) -> TransportResult<GasFillable>
77    where
78        P: Provider<N>,
79        N: Network,
80    {
81        let gas_price_fut = tx.gas_price().map_or_else(
82            || provider.get_gas_price().right_future(),
83            |gas_price| async move { Ok(gas_price) }.left_future(),
84        );
85
86        let gas_limit_fut = tx.gas_limit().map_or_else(
87            || provider.estimate_gas(tx.clone()).into_future().right_future(),
88            |gas_limit| async move { Ok(gas_limit) }.left_future(),
89        );
90
91        let (gas_price, gas_limit) = futures::try_join!(gas_price_fut, gas_limit_fut)?;
92
93        Ok(GasFillable::Legacy { gas_limit, gas_price })
94    }
95
96    async fn prepare_1559<P, N>(
97        &self,
98        provider: &P,
99        tx: &N::TransactionRequest,
100    ) -> TransportResult<GasFillable>
101    where
102        P: Provider<N>,
103        N: Network,
104    {
105        let gas_limit_fut = tx.gas_limit().map_or_else(
106            || provider.estimate_gas(tx.clone()).into_future().right_future(),
107            |gas_limit| async move { Ok(gas_limit) }.left_future(),
108        );
109
110        let eip1559_fees_fut = if let (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) =
111            (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas())
112        {
113            async move { Ok(Eip1559Estimation { max_fee_per_gas, max_priority_fee_per_gas }) }
114                .left_future()
115        } else {
116            provider.estimate_eip1559_fees().right_future()
117        };
118
119        let (gas_limit, estimate) = futures::try_join!(gas_limit_fut, eip1559_fees_fut)?;
120
121        Ok(GasFillable::Eip1559 { gas_limit, estimate })
122    }
123}
124
125impl<N: Network> TxFiller<N> for GasFiller {
126    type Fillable = GasFillable;
127
128    fn status(&self, tx: &<N as Network>::TransactionRequest) -> FillerControlFlow {
129        // legacy and eip2930 tx
130        if tx.gas_price().is_some() && tx.gas_limit().is_some() {
131            return FillerControlFlow::Finished;
132        }
133
134        // eip1559
135        if tx.max_fee_per_gas().is_some()
136            && tx.max_priority_fee_per_gas().is_some()
137            && tx.gas_limit().is_some()
138        {
139            return FillerControlFlow::Finished;
140        }
141
142        FillerControlFlow::Ready
143    }
144
145    fn fill_sync(&self, _tx: &mut SendableTx<N>) {}
146
147    async fn prepare<P>(
148        &self,
149        provider: &P,
150        tx: &<N as Network>::TransactionRequest,
151    ) -> TransportResult<Self::Fillable>
152    where
153        P: Provider<N>,
154    {
155        if tx.gas_price().is_some() {
156            self.prepare_legacy(provider, tx).await
157        } else {
158            match self.prepare_1559(provider, tx).await {
159                // fallback to legacy
160                Ok(estimate) => Ok(estimate),
161                Err(RpcError::UnsupportedFeature(_)) => self.prepare_legacy(provider, tx).await,
162                Err(e) => Err(e),
163            }
164        }
165    }
166
167    async fn fill(
168        &self,
169        fillable: Self::Fillable,
170        mut tx: SendableTx<N>,
171    ) -> TransportResult<SendableTx<N>> {
172        if let Some(builder) = tx.as_mut_builder() {
173            match fillable {
174                GasFillable::Legacy { gas_limit, gas_price } => {
175                    builder.set_gas_limit(gas_limit);
176                    builder.set_gas_price(gas_price);
177                }
178                GasFillable::Eip1559 { gas_limit, estimate } => {
179                    builder.set_gas_limit(gas_limit);
180                    builder.set_max_fee_per_gas(estimate.max_fee_per_gas);
181                    builder.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
182                }
183            }
184        };
185        Ok(tx)
186    }
187}
188
189/// Filler for the `max_fee_per_blob_gas` field in EIP-4844 transactions.
190#[derive(Clone, Copy, Debug, Default)]
191pub struct BlobGasFiller;
192
193impl<N: Network> TxFiller<N> for BlobGasFiller
194where
195    N::TransactionRequest: TransactionBuilder4844,
196{
197    type Fillable = u128;
198
199    fn status(&self, tx: &<N as Network>::TransactionRequest) -> FillerControlFlow {
200        // Nothing to fill if non-eip4844 tx or `max_fee_per_blob_gas` is already set to a valid
201        // value.
202        if tx.blob_sidecar().is_none()
203            || tx.max_fee_per_blob_gas().is_some_and(|gas| gas >= BLOB_TX_MIN_BLOB_GASPRICE)
204        {
205            return FillerControlFlow::Finished;
206        }
207
208        FillerControlFlow::Ready
209    }
210
211    fn fill_sync(&self, _tx: &mut SendableTx<N>) {}
212
213    async fn prepare<P>(
214        &self,
215        provider: &P,
216        tx: &<N as Network>::TransactionRequest,
217    ) -> TransportResult<Self::Fillable>
218    where
219        P: Provider<N>,
220    {
221        if let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() {
222            if max_fee_per_blob_gas >= BLOB_TX_MIN_BLOB_GASPRICE {
223                return Ok(max_fee_per_blob_gas);
224            }
225        }
226
227        provider
228            .get_fee_history(2, BlockNumberOrTag::Latest, &[])
229            .await?
230            .base_fee_per_blob_gas
231            .last()
232            .ok_or(RpcError::NullResp)
233            .copied()
234    }
235
236    async fn fill(
237        &self,
238        fillable: Self::Fillable,
239        mut tx: SendableTx<N>,
240    ) -> TransportResult<SendableTx<N>> {
241        if let Some(builder) = tx.as_mut_builder() {
242            builder.set_max_fee_per_blob_gas(fillable);
243        }
244        Ok(tx)
245    }
246}
247
248#[cfg(feature = "reqwest")]
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::ProviderBuilder;
253    use alloy_consensus::{SidecarBuilder, SimpleCoder, Transaction};
254    use alloy_eips::eip4844::DATA_GAS_PER_BLOB;
255    use alloy_primitives::{address, U256};
256    use alloy_rpc_types_eth::TransactionRequest;
257
258    #[tokio::test]
259    async fn no_gas_price_or_limit() {
260        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
261
262        // GasEstimationLayer requires chain_id to be set to handle EIP-1559 tx
263        let tx = TransactionRequest {
264            value: Some(U256::from(100)),
265            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
266            chain_id: Some(31337),
267            ..Default::default()
268        };
269
270        let tx = provider.send_transaction(tx).await.unwrap();
271
272        let receipt = tx.get_receipt().await.unwrap();
273
274        assert_eq!(receipt.effective_gas_price, 1_000_000_001);
275        assert_eq!(receipt.gas_used, 21000);
276    }
277
278    #[tokio::test]
279    async fn no_gas_limit() {
280        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
281
282        let gas_price = provider.get_gas_price().await.unwrap();
283        let tx = TransactionRequest {
284            value: Some(U256::from(100)),
285            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
286            gas_price: Some(gas_price),
287            ..Default::default()
288        };
289
290        let tx = provider.send_transaction(tx).await.unwrap();
291
292        let receipt = tx.get_receipt().await.unwrap();
293
294        assert_eq!(receipt.gas_used, 21000);
295    }
296
297    #[tokio::test]
298    async fn no_max_fee_per_blob_gas() {
299        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
300
301        let sidecar: SidecarBuilder<SimpleCoder> = SidecarBuilder::from_slice(b"Hello World");
302        let sidecar = sidecar.build().unwrap();
303
304        let tx = TransactionRequest {
305            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
306            sidecar: Some(sidecar),
307            ..Default::default()
308        };
309
310        let tx = provider.send_transaction(tx).await.unwrap();
311
312        let receipt = tx.get_receipt().await.unwrap();
313
314        let tx = provider.get_transaction_by_hash(receipt.transaction_hash).await.unwrap().unwrap();
315
316        assert!(tx.max_fee_per_blob_gas().unwrap() >= BLOB_TX_MIN_BLOB_GASPRICE);
317        assert_eq!(receipt.gas_used, 21000);
318        assert_eq!(
319            receipt.blob_gas_used.expect("Expected to be EIP-4844 transaction"),
320            DATA_GAS_PER_BLOB
321        );
322    }
323
324    #[tokio::test]
325    async fn zero_max_fee_per_blob_gas() {
326        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
327
328        let sidecar: SidecarBuilder<SimpleCoder> = SidecarBuilder::from_slice(b"Hello World");
329        let sidecar = sidecar.build().unwrap();
330
331        let tx = TransactionRequest {
332            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
333            max_fee_per_blob_gas: Some(0),
334            sidecar: Some(sidecar),
335            ..Default::default()
336        };
337
338        let tx = provider.send_transaction(tx).await.unwrap();
339
340        let receipt = tx.get_receipt().await.unwrap();
341
342        let tx = provider.get_transaction_by_hash(receipt.transaction_hash).await.unwrap().unwrap();
343
344        assert!(tx.max_fee_per_blob_gas().unwrap() >= BLOB_TX_MIN_BLOB_GASPRICE);
345        assert_eq!(receipt.gas_used, 21000);
346        assert_eq!(
347            receipt.blob_gas_used.expect("Expected to be EIP-4844 transaction"),
348            DATA_GAS_PER_BLOB
349        );
350    }
351}