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