Skip to main content

alloy_provider/fillers/
gas.rs

1use std::{
2    fmt::{self, Formatter},
3    future::IntoFuture,
4    sync::Arc,
5};
6
7use crate::{
8    fillers::{FillerControlFlow, TxFiller},
9    provider::SendableTx,
10    utils::{Eip1559Estimation, Eip1559Estimator},
11    Provider,
12};
13use alloy_eips::eip4844::BLOB_TX_MIN_BLOB_GASPRICE;
14use alloy_json_rpc::RpcError;
15use alloy_network::{Network, TransactionBuilder, TransactionBuilder4844};
16use alloy_rpc_types_eth::BlockNumberOrTag;
17use alloy_transport::TransportResult;
18use futures::FutureExt;
19
20/// An enum over the different types of gas fillable.
21#[doc(hidden)]
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum GasFillable {
24    Legacy { gas_limit: u64, gas_price: u128 },
25    Eip1559 { gas_limit: u64, estimate: Eip1559Estimation },
26}
27
28/// A [`TxFiller`] that populates gas related fields in transaction requests if
29/// unset.
30///
31/// Gas related fields are gas_price, gas_limit, max_fee_per_gas
32/// and max_priority_fee_per_gas. For EIP-4844 `max_fee_per_blob_gas`,
33/// see [`BlobGasFiller`].
34///
35/// The layer fetches the estimations for these via the
36/// [`Provider::get_gas_price`], [`Provider::estimate_gas`] and
37/// [`Provider::estimate_eip1559_fees`] methods.
38///
39/// ## Note:
40///
41/// The layer will populate gas fields based on the following logic:
42/// - if `gas_price` is set, it will process as a legacy tx (or EIP-2930 if `access_list` is also
43///   set) and populate the `gas_limit` field if unset, and `gas_price` if unset for EIP-2930.
44/// - if `access_list` is set but `gas_price` is not set, it will process as an EIP-1559 tx (which
45///   can also have an access_list) and populate the `gas_limit`, `max_fee_per_gas` and
46///   `max_priority_fee_per_gas` fields if unset.
47/// - if `blob_sidecar` is set, it will process as an EIP-4844 tx and populate the `gas_limit`,
48///   `max_fee_per_gas`, and `max_priority_fee_per_gas` fields if unset. The `max_fee_per_blob_gas`
49///   is populated by [`BlobGasFiller`].
50/// - Otherwise, it will process as a EIP-1559 tx and populate the `gas_limit`, `max_fee_per_gas`
51///   and `max_priority_fee_per_gas` fields if unset.
52/// - If the network does not support EIP-1559, it will fallback to the legacy tx and populate the
53///   `gas_limit` and `gas_price` fields if unset.
54///
55/// # Example
56///
57/// ```
58/// # use alloy_network::{Ethereum};
59/// # use alloy_rpc_types_eth::TransactionRequest;
60/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider};
61/// # use alloy_signer_local::PrivateKeySigner;
62/// # async fn test(url: url::Url) -> Result<(), Box<dyn std::error::Error>> {
63/// let pk: PrivateKeySigner = "0x...".parse()?;
64/// let provider = ProviderBuilder::<_, _, Ethereum>::default()
65///     .with_gas_estimation()
66///     .wallet(pk)
67///     .connect_http(url);
68///
69/// provider.send_transaction(TransactionRequest::default()).await;
70/// # Ok(())
71/// # }
72/// ```
73#[non_exhaustive]
74#[derive(Clone, Debug, Default)]
75pub struct GasFiller {
76    /// The eip1559 gas estimator to use.
77    pub estimator: Eip1559Estimator,
78}
79
80impl GasFiller {
81    async fn prepare_legacy<P, N>(
82        &self,
83        provider: &P,
84        tx: &N::TransactionRequest,
85    ) -> TransportResult<GasFillable>
86    where
87        P: Provider<N>,
88        N: Network,
89    {
90        let gas_price_fut = tx.gas_price().map_or_else(
91            || provider.get_gas_price().right_future(),
92            |gas_price| async move { Ok(gas_price) }.left_future(),
93        );
94
95        let gas_limit_fut = tx.gas_limit().map_or_else(
96            || provider.estimate_gas(tx.clone()).into_future().right_future(),
97            |gas_limit| async move { Ok(gas_limit) }.left_future(),
98        );
99
100        let (gas_price, gas_limit) = futures::try_join!(gas_price_fut, gas_limit_fut)?;
101
102        Ok(GasFillable::Legacy { gas_limit, gas_price })
103    }
104
105    async fn prepare_1559<P, N>(
106        &self,
107        provider: &P,
108        tx: &N::TransactionRequest,
109    ) -> TransportResult<GasFillable>
110    where
111        P: Provider<N>,
112        N: Network,
113    {
114        let gas_limit_fut = tx.gas_limit().map_or_else(
115            || provider.estimate_gas(tx.clone()).into_future().right_future(),
116            |gas_limit| async move { Ok(gas_limit) }.left_future(),
117        );
118
119        let eip1559_fees_fut = if let (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) =
120            (tx.max_fee_per_gas(), tx.max_priority_fee_per_gas())
121        {
122            async move { Ok(Eip1559Estimation { max_fee_per_gas, max_priority_fee_per_gas }) }
123                .left_future()
124        } else {
125            provider.estimate_eip1559_fees_with(self.estimator.clone()).right_future()
126        };
127
128        let (gas_limit, estimate) = futures::try_join!(gas_limit_fut, eip1559_fees_fut)?;
129
130        Ok(GasFillable::Eip1559 { gas_limit, estimate })
131    }
132}
133
134impl<N: Network> TxFiller<N> for GasFiller {
135    type Fillable = GasFillable;
136
137    fn status(&self, tx: &<N as Network>::TransactionRequest) -> FillerControlFlow {
138        // legacy and eip2930 tx
139        if tx.gas_price().is_some() && tx.gas_limit().is_some() {
140            return FillerControlFlow::Finished;
141        }
142
143        // eip1559
144        if tx.max_fee_per_gas().is_some()
145            && tx.max_priority_fee_per_gas().is_some()
146            && tx.gas_limit().is_some()
147        {
148            return FillerControlFlow::Finished;
149        }
150
151        FillerControlFlow::Ready
152    }
153
154    fn fill_sync(&self, _tx: &mut SendableTx<N>) {}
155
156    async fn prepare<P>(
157        &self,
158        provider: &P,
159        tx: &<N as Network>::TransactionRequest,
160    ) -> TransportResult<Self::Fillable>
161    where
162        P: Provider<N>,
163    {
164        if tx.gas_price().is_some() {
165            self.prepare_legacy(provider, tx).await
166        } else {
167            match self.prepare_1559(provider, tx).await {
168                // fallback to legacy
169                Ok(estimate) => Ok(estimate),
170                Err(RpcError::UnsupportedFeature(_)) => self.prepare_legacy(provider, tx).await,
171                Err(e) => Err(e),
172            }
173        }
174    }
175
176    async fn fill(
177        &self,
178        fillable: Self::Fillable,
179        mut tx: SendableTx<N>,
180    ) -> TransportResult<SendableTx<N>> {
181        if let Some(builder) = tx.as_mut_builder() {
182            match fillable {
183                GasFillable::Legacy { gas_limit, gas_price } => {
184                    builder.set_gas_limit(gas_limit);
185                    builder.set_gas_price(gas_price);
186                }
187                GasFillable::Eip1559 { gas_limit, estimate } => {
188                    builder.set_gas_limit(gas_limit);
189                    builder.set_max_fee_per_gas(estimate.max_fee_per_gas);
190                    builder.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas);
191                }
192            }
193        };
194        Ok(tx)
195    }
196}
197
198/// An estimator function for blob gas fees.
199pub type BlobGasEstimatorFunction = fn(u128, &[f64]) -> u128;
200
201/// A trait responsible for estimating blob gas values
202pub trait BlobGasEstimatorFn: Send + Sync + Unpin {
203    /// Estimates the blob gas fee given the base fee per blob gas
204    /// and the blob gas usage ratio.
205    fn estimate(&self, base_fee_per_blob_gas: u128, blob_gas_used_ratio: &[f64]) -> u128;
206}
207
208/// Blob Gas estimator variants
209#[derive(Default, Clone)]
210pub enum BlobGasEstimator {
211    /// Uses the builtin estimator
212    #[default]
213    Default,
214    /// Uses a custom estimator
215    Custom(Arc<dyn BlobGasEstimatorFn>),
216}
217
218impl BlobGasEstimator {
219    /// Creates a new estimator from a closure
220    pub fn new<F>(f: F) -> Self
221    where
222        F: Fn(u128, &[f64]) -> u128 + Send + Sync + Unpin + 'static,
223    {
224        Self::new_estimator(f)
225    }
226
227    /// Creates a new estimate fn
228    pub fn new_estimator<F: BlobGasEstimatorFn + 'static>(f: F) -> Self {
229        Self::Custom(Arc::new(f))
230    }
231
232    /// Create a custom estimator
233    pub fn custom<F>(f: F) -> Self
234    where
235        F: Fn(u128, &[f64]) -> u128 + Send + Sync + Unpin + 'static,
236    {
237        Self::Custom(Arc::new(f))
238    }
239
240    /// Create a scaled estimator
241    pub fn scaled(scale: u128) -> Self {
242        Self::custom(move |base_fee, _| base_fee.saturating_mul(scale))
243    }
244
245    /// Estimates the blob gas fee given the base fee per blob gas
246    /// and the blob gas usage ratio.
247    pub fn estimate(&self, base_fee_per_blob_gas: u128, blob_gas_used_ratio: &[f64]) -> u128 {
248        match self {
249            Self::Default => base_fee_per_blob_gas,
250            Self::Custom(val) => val.estimate(base_fee_per_blob_gas, blob_gas_used_ratio),
251        }
252    }
253}
254
255impl<F> BlobGasEstimatorFn for F
256where
257    F: Fn(u128, &[f64]) -> u128 + Send + Sync + Unpin,
258{
259    fn estimate(&self, base_fee_per_blob_gas: u128, blob_gas_used_ratio: &[f64]) -> u128 {
260        (self)(base_fee_per_blob_gas, blob_gas_used_ratio)
261    }
262}
263
264impl fmt::Debug for BlobGasEstimator {
265    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
266        f.debug_struct("BlobGasEstimator")
267            .field(
268                "estimator",
269                &match self {
270                    Self::Default => "default",
271                    Self::Custom(_) => "custom",
272                },
273            )
274            .finish()
275    }
276}
277
278/// Filler for the `max_fee_per_blob_gas` field in blob transactions.
279#[derive(Clone, Debug, Default)]
280pub struct BlobGasFiller {
281    /// The blob gas estimator to use.
282    pub estimator: BlobGasEstimator,
283}
284
285impl<N: Network> TxFiller<N> for BlobGasFiller
286where
287    N::TransactionRequest: TransactionBuilder4844,
288{
289    type Fillable = u128;
290
291    fn status(&self, tx: &<N as Network>::TransactionRequest) -> FillerControlFlow {
292        // Nothing to fill if no blob sidecar is present or `max_fee_per_blob_gas` is already set
293        // to a valid value.
294        if !tx.has_blob_sidecar()
295            || tx.max_fee_per_blob_gas().is_some_and(|gas| gas >= BLOB_TX_MIN_BLOB_GASPRICE)
296        {
297            return FillerControlFlow::Finished;
298        }
299
300        FillerControlFlow::Ready
301    }
302
303    fn fill_sync(&self, _tx: &mut SendableTx<N>) {}
304
305    async fn prepare<P>(
306        &self,
307        provider: &P,
308        tx: &<N as Network>::TransactionRequest,
309    ) -> TransportResult<Self::Fillable>
310    where
311        P: Provider<N>,
312    {
313        if let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() {
314            if max_fee_per_blob_gas >= BLOB_TX_MIN_BLOB_GASPRICE {
315                return Ok(max_fee_per_blob_gas);
316            }
317        }
318
319        // Fetch the latest fee_history
320        let fee_history = provider.get_fee_history(2, BlockNumberOrTag::Latest, &[]).await?;
321
322        let base_fee_per_blob_gas =
323            fee_history.base_fee_per_blob_gas.last().ok_or(RpcError::NullResp).copied()?;
324
325        let blob_gas_used_ratio = fee_history.blob_gas_used_ratio;
326
327        Ok(self.estimator.estimate(base_fee_per_blob_gas, &blob_gas_used_ratio))
328    }
329
330    async fn fill(
331        &self,
332        fillable: Self::Fillable,
333        mut tx: SendableTx<N>,
334    ) -> TransportResult<SendableTx<N>> {
335        if let Some(builder) = tx.as_mut_builder() {
336            builder.set_max_fee_per_blob_gas(fillable);
337        }
338        Ok(tx)
339    }
340}
341
342#[cfg(feature = "reqwest")]
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::ProviderBuilder;
347    use alloy_consensus::{SidecarBuilder, SimpleCoder, Transaction};
348    use alloy_eips::eip4844::DATA_GAS_PER_BLOB;
349    use alloy_network::Ethereum;
350    use alloy_primitives::{address, U256};
351    use alloy_rpc_types_eth::TransactionRequest;
352
353    #[tokio::test]
354    async fn no_gas_price_or_limit() {
355        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
356
357        // GasEstimationLayer requires chain_id to be set to handle EIP-1559 tx
358        let tx = TransactionRequest {
359            value: Some(U256::from(100)),
360            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
361            chain_id: Some(31337),
362            ..Default::default()
363        };
364
365        let tx = provider.send_transaction(tx).await.unwrap();
366
367        let receipt = tx.get_receipt().await.unwrap();
368
369        assert_eq!(receipt.effective_gas_price, 1_000_000_001);
370        assert_eq!(receipt.gas_used, 21000);
371    }
372
373    #[tokio::test]
374    async fn no_gas_limit() {
375        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
376
377        let gas_price = provider.get_gas_price().await.unwrap();
378        let tx = TransactionRequest {
379            value: Some(U256::from(100)),
380            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
381            gas_price: Some(gas_price),
382            ..Default::default()
383        };
384
385        let tx = provider.send_transaction(tx).await.unwrap();
386
387        let receipt = tx.get_receipt().await.unwrap();
388
389        assert_eq!(receipt.gas_used, 21000);
390    }
391
392    #[tokio::test]
393    async fn no_max_fee_per_blob_gas() {
394        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
395
396        let sidecar: SidecarBuilder<SimpleCoder> = SidecarBuilder::from_slice(b"Hello World");
397        let sidecar = sidecar.build_4844().unwrap();
398
399        let tx = TransactionRequest {
400            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
401            sidecar: Some(sidecar.into()),
402            ..Default::default()
403        };
404
405        let tx = provider.send_transaction(tx).await.unwrap();
406
407        let receipt = tx.get_receipt().await.unwrap();
408
409        let tx = provider.get_transaction_by_hash(receipt.transaction_hash).await.unwrap().unwrap();
410
411        assert!(tx.max_fee_per_blob_gas().unwrap() >= BLOB_TX_MIN_BLOB_GASPRICE);
412        assert_eq!(receipt.gas_used, 21000);
413        assert_eq!(
414            receipt.blob_gas_used.expect("Expected to be EIP-4844 transaction"),
415            DATA_GAS_PER_BLOB
416        );
417    }
418
419    #[tokio::test]
420    async fn zero_max_fee_per_blob_gas() {
421        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
422
423        let sidecar: SidecarBuilder<SimpleCoder> = SidecarBuilder::from_slice(b"Hello World");
424        let sidecar = sidecar.build_4844().unwrap();
425
426        let tx = TransactionRequest {
427            to: Some(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into()),
428            max_fee_per_blob_gas: Some(0),
429            sidecar: Some(sidecar.into()),
430            ..Default::default()
431        };
432
433        let tx = provider.send_transaction(tx).await.unwrap();
434
435        let receipt = tx.get_receipt().await.unwrap();
436
437        let tx = provider.get_transaction_by_hash(receipt.transaction_hash).await.unwrap().unwrap();
438
439        assert!(tx.max_fee_per_blob_gas().unwrap() >= BLOB_TX_MIN_BLOB_GASPRICE);
440        assert_eq!(receipt.gas_used, 21000);
441        assert_eq!(
442            receipt.blob_gas_used.expect("Expected to be EIP-4844 transaction"),
443            DATA_GAS_PER_BLOB
444        );
445    }
446
447    #[test]
448    fn eip7594_sidecar_marks_blob_gas_filler_ready() {
449        let sidecar =
450            SidecarBuilder::<SimpleCoder>::from_slice(b"Hello World").build_7594().unwrap();
451        let tx = TransactionRequest { sidecar: Some(sidecar.into()), ..Default::default() };
452
453        assert_eq!(
454            <BlobGasFiller as TxFiller<Ethereum>>::status(&BlobGasFiller::default(), &tx),
455            FillerControlFlow::Ready
456        );
457    }
458}