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