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#[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#[non_exhaustive]
74#[derive(Clone, Debug, Default)]
75pub struct GasFiller {
76 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 if tx.gas_price().is_some() && tx.gas_limit().is_some() {
140 return FillerControlFlow::Finished;
141 }
142
143 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 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
198pub type BlobGasEstimatorFunction = fn(u128, &[f64]) -> u128;
200
201pub trait BlobGasEstimatorFn: Send + Sync + Unpin {
203 fn estimate(&self, base_fee_per_blob_gas: u128, blob_gas_used_ratio: &[f64]) -> u128;
206}
207
208#[derive(Default, Clone)]
210pub enum BlobGasEstimator {
211 #[default]
213 Default,
214 Custom(Arc<dyn BlobGasEstimatorFn>),
216}
217
218impl BlobGasEstimator {
219 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 pub fn new_estimator<F: BlobGasEstimatorFn + 'static>(f: F) -> Self {
229 Self::Custom(Arc::new(f))
230 }
231
232 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 pub fn scaled(scale: u128) -> Self {
242 Self::custom(move |base_fee, _| base_fee.saturating_mul(scale))
243 }
244
245 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#[derive(Clone, Debug, Default)]
280pub struct BlobGasFiller {
281 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 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 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 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}