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