use std::sync::Arc;
use alloy_chains::NamedChain;
use alloy_network::Network;
use alloy_primitives::{Address, U256};
use alloy_provider::Provider;
use serde::Serialize;
use tokio::sync::Mutex;
use crate::config::SemioscanConfig;
use crate::gas::cache::GasCache;
use crate::retrieval::DecimalPrecision;
use crate::types::config::TransactionCount;
use crate::types::fees::L1DataFee;
use crate::types::gas::{BlobCount, BlobGasPrice, GasAmount, GasBreakdown, GasPrice};
use crate::types::wei::WeiAmount;
#[derive(Debug, Clone)]
pub enum GasForTx {
L1(L1Gas),
L2(L2Gas),
}
impl From<(U256, U256)> for GasForTx {
fn from((gas_used, effective_gas_price): (U256, U256)) -> Self {
Self::L1(L1Gas::from((gas_used, effective_gas_price)))
}
}
impl From<(U256, U256, U256)> for GasForTx {
fn from((gas_used, effective_gas_price, l1_data_fee): (U256, U256, U256)) -> Self {
Self::L2(L2Gas::from((gas_used, effective_gas_price, l1_data_fee)))
}
}
#[derive(Debug, Clone)]
pub struct L1Gas {
pub gas_used: GasAmount,
pub effective_gas_price: GasPrice,
pub blob_count: BlobCount,
pub blob_gas_price: BlobGasPrice,
}
impl L1Gas {
pub fn execution_cost(&self) -> U256 {
self.gas_used * self.effective_gas_price
}
pub fn blob_cost(&self) -> U256 {
self.blob_gas_price.cost_for_blobs(self.blob_count)
}
pub fn total_cost(&self) -> U256 {
self.execution_cost().saturating_add(self.blob_cost())
}
pub fn to_breakdown(&self) -> GasBreakdown {
GasBreakdown::builder()
.execution_gas_cost(self.execution_cost())
.blob_gas_cost(self.blob_cost())
.blob_count(self.blob_count)
.blob_gas_price(self.blob_gas_price)
.build()
}
}
impl From<(U256, U256)> for L1Gas {
fn from((gas_used, effective_gas_price): (U256, U256)) -> Self {
Self {
gas_used: GasAmount::from(gas_used),
effective_gas_price: GasPrice::from(effective_gas_price),
blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}
}
}
impl From<(U256, U256, BlobCount, BlobGasPrice)> for L1Gas {
fn from(
(gas_used, effective_gas_price, blob_count, blob_gas_price): (
U256,
U256,
BlobCount,
BlobGasPrice,
),
) -> Self {
Self {
gas_used: GasAmount::from(gas_used),
effective_gas_price: GasPrice::from(effective_gas_price),
blob_count,
blob_gas_price,
}
}
}
#[derive(Debug, Clone)]
pub struct L2Gas {
pub gas_used: GasAmount,
pub effective_gas_price: GasPrice,
pub l1_data_fee: L1DataFee,
pub blob_count: BlobCount,
pub blob_gas_price: BlobGasPrice,
}
impl L2Gas {
pub fn execution_cost(&self) -> U256 {
self.gas_used * self.effective_gas_price
}
pub fn blob_cost(&self) -> U256 {
self.blob_gas_price.cost_for_blobs(self.blob_count)
}
pub fn total_cost(&self) -> U256 {
self.execution_cost()
.saturating_add(self.blob_cost())
.saturating_add(self.l1_data_fee.as_u256())
}
pub fn to_breakdown(&self) -> GasBreakdown {
GasBreakdown::builder()
.execution_gas_cost(self.execution_cost())
.blob_gas_cost(self.blob_cost())
.l1_data_fee(self.l1_data_fee.as_u256())
.blob_count(self.blob_count)
.blob_gas_price(self.blob_gas_price)
.build()
}
}
impl From<(U256, U256, U256)> for L2Gas {
fn from((gas_used, effective_gas_price, l1_data_fee): (U256, U256, U256)) -> Self {
Self {
gas_used: GasAmount::from(gas_used),
effective_gas_price: GasPrice::from(effective_gas_price),
l1_data_fee: L1DataFee::new(l1_data_fee),
blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}
}
}
impl From<(U256, U256, U256, BlobCount, BlobGasPrice)> for L2Gas {
fn from(
(gas_used, effective_gas_price, l1_data_fee, blob_count, blob_gas_price): (
U256,
U256,
U256,
BlobCount,
BlobGasPrice,
),
) -> Self {
Self {
gas_used: GasAmount::from(gas_used),
effective_gas_price: GasPrice::from(effective_gas_price),
l1_data_fee: L1DataFee::new(l1_data_fee),
blob_count,
blob_gas_price,
}
}
}
#[derive(Default, Debug, Clone, Serialize)]
pub struct GasCostResult {
pub chain: NamedChain,
pub from: Address,
pub to: Address,
pub total_gas_cost: WeiAmount,
pub transaction_count: TransactionCount,
pub breakdown: GasBreakdown,
}
impl GasCostResult {
pub fn new(chain: NamedChain, from: Address, to: Address) -> Self {
Self {
from,
to,
chain,
total_gas_cost: WeiAmount::ZERO,
transaction_count: TransactionCount::ZERO,
breakdown: GasBreakdown::new(),
}
}
pub fn add_l1_fee(&mut self, l1_fee: L1DataFee) {
self.total_gas_cost = self.total_gas_cost + WeiAmount::from(l1_fee.as_u256());
self.breakdown.l1_data_fee = self.breakdown.l1_data_fee.saturating_add(l1_fee.as_u256());
}
pub fn add_transaction(&mut self, gas: GasForTx) {
match gas {
GasForTx::L1(ref g) => {
let tx_breakdown = g.to_breakdown();
self.total_gas_cost =
self.total_gas_cost + WeiAmount::from(tx_breakdown.total_cost());
self.breakdown.merge(&tx_breakdown);
self.transaction_count.increment();
}
GasForTx::L2(ref g) => {
let tx_breakdown = g.to_breakdown();
self.total_gas_cost =
self.total_gas_cost + WeiAmount::from(tx_breakdown.total_cost());
self.breakdown.merge(&tx_breakdown);
self.transaction_count.increment();
}
}
}
pub fn merge(&mut self, other: &Self) {
self.total_gas_cost = self.total_gas_cost + other.total_gas_cost;
self.transaction_count += other.transaction_count;
self.breakdown.merge(&other.breakdown);
}
pub fn has_blob_transactions(&self) -> bool {
self.breakdown.has_blob_gas()
}
pub fn total_blob_gas_cost(&self) -> U256 {
self.breakdown.blob_gas_cost
}
pub fn total_execution_gas_cost(&self) -> U256 {
self.breakdown.execution_gas_cost
}
pub fn total_l1_data_fee(&self) -> U256 {
self.breakdown.l1_data_fee
}
pub fn total_blob_count(&self) -> BlobCount {
self.breakdown.blob_count
}
pub fn formatted_gas_cost(&self) -> String {
self.format_gas_cost()
}
fn format_gas_cost(&self) -> String {
let gas_cost = self.total_gas_cost.as_u256();
let decimals = DecimalPrecision::NativeToken.decimals();
let divisor = U256::from(10).pow(U256::from(decimals));
let whole = gas_cost / divisor;
let fractional = gas_cost % divisor;
let fractional_str = format!("{:0width$}", fractional, width = decimals as usize);
format!("{}.{}", whole, fractional_str.trim_end_matches('0'))
}
}
pub struct GasCostCalculator<N: Network, P: Provider<N>> {
pub(crate) provider: P,
pub(crate) gas_cache: Arc<Mutex<GasCache>>,
pub(crate) config: SemioscanConfig,
pub(crate) _phantom: std::marker::PhantomData<N>,
}
impl<N: Network, P: Provider<N>> GasCostCalculator<N, P> {
pub fn new(provider: P) -> Self {
Self::with_config(provider, SemioscanConfig::default())
}
pub fn with_config(provider: P, config: SemioscanConfig) -> Self {
Self {
provider,
gas_cache: Arc::new(Mutex::new(GasCache::default())),
config,
_phantom: std::marker::PhantomData,
}
}
pub fn with_cache_and_config(
provider: P,
gas_cache: Arc<Mutex<GasCache>>,
config: SemioscanConfig,
) -> Self {
Self {
provider,
gas_cache,
config,
_phantom: std::marker::PhantomData,
}
}
pub fn with_cache(provider: P, gas_cache: Arc<Mutex<GasCache>>) -> Self {
Self::with_cache_and_config(provider, gas_cache, SemioscanConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::address;
#[test]
fn test_gas_cost_result_add_transaction_l1() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult::new(NamedChain::Mainnet, from, to);
result.add_transaction(GasForTx::L1(L1Gas {
gas_used: GasAmount::new(21000),
effective_gas_price: GasPrice::from_gwei(50),
blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}));
assert_eq!(result.transaction_count, TransactionCount::new(1));
assert_eq!(
result.total_gas_cost,
WeiAmount::from(1_050_000_000_000_000u64)
);
result.add_transaction(GasForTx::L1(L1Gas {
gas_used: GasAmount::new(100000),
effective_gas_price: GasPrice::from_gwei(60),
blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}));
assert_eq!(result.transaction_count, TransactionCount::new(2));
assert_eq!(
result.total_gas_cost,
WeiAmount::from(7_050_000_000_000_000u64)
);
assert_eq!(
result.breakdown.execution_gas_cost,
U256::from(7_050_000_000_000_000u64)
);
assert_eq!(result.breakdown.blob_gas_cost, U256::ZERO);
}
#[test]
fn test_gas_cost_result_add_transaction_l1_with_blobs() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult::new(NamedChain::Mainnet, from, to);
result.add_transaction(GasForTx::L1(L1Gas {
gas_used: GasAmount::new(21000),
effective_gas_price: GasPrice::from_gwei(50),
blob_count: BlobCount::new(2),
blob_gas_price: BlobGasPrice::from_gwei(1),
}));
assert_eq!(result.transaction_count, TransactionCount::new(1));
assert_eq!(
result.total_gas_cost,
WeiAmount::from(1_312_144_000_000_000u64)
);
assert_eq!(
result.breakdown.execution_gas_cost,
U256::from(1_050_000_000_000_000u64)
);
assert_eq!(
result.breakdown.blob_gas_cost,
U256::from(262_144_000_000_000u64)
);
assert_eq!(result.breakdown.blob_count, BlobCount::new(2));
assert!(result.has_blob_transactions());
}
#[test]
fn test_gas_cost_result_add_transaction_l2() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult::new(NamedChain::Arbitrum, from, to);
result.add_transaction(GasForTx::L2(L2Gas {
gas_used: GasAmount::new(150000),
effective_gas_price: GasPrice::new(100_000_000), l1_data_fee: L1DataFee::new(U256::from(5_000_000_000_000_000u64)), blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}));
assert_eq!(result.transaction_count, TransactionCount::new(1));
assert_eq!(
result.total_gas_cost,
WeiAmount::from(5_015_000_000_000_000u64)
);
assert_eq!(
result.breakdown.l1_data_fee,
U256::from(5_000_000_000_000_000u64)
);
assert_eq!(
result.breakdown.execution_gas_cost,
U256::from(15_000_000_000_000u64)
);
}
#[test]
fn test_gas_cost_result_merge() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result1 = GasCostResult {
chain: NamedChain::Mainnet,
from,
to,
total_gas_cost: WeiAmount::from(1_000_000_000_000_000u64),
transaction_count: TransactionCount::new(5),
breakdown: GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000_000_000_000_000u64))
.build(),
};
let result2 = GasCostResult {
chain: NamedChain::Mainnet,
from,
to,
total_gas_cost: WeiAmount::from(500_000_000_000_000u64),
transaction_count: TransactionCount::new(3),
breakdown: GasBreakdown::builder()
.execution_gas_cost(U256::from(500_000_000_000_000u64))
.build(),
};
result1.merge(&result2);
assert_eq!(
result1.total_gas_cost,
WeiAmount::from(1_500_000_000_000_000u64)
);
assert_eq!(result1.transaction_count, TransactionCount::new(8));
assert_eq!(
result1.breakdown.execution_gas_cost,
U256::from(1_500_000_000_000_000u64)
);
}
#[test]
fn test_gas_cost_result_merge_with_zero() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult {
chain: NamedChain::Mainnet,
from,
to,
total_gas_cost: WeiAmount::from(1_000_000u64),
transaction_count: TransactionCount::new(5),
breakdown: GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000_000u64))
.build(),
};
let empty = GasCostResult::new(NamedChain::Mainnet, from, to);
result.merge(&empty);
assert_eq!(result.total_gas_cost, WeiAmount::from(1_000_000u64));
assert_eq!(result.transaction_count, TransactionCount::new(5));
}
#[test]
fn test_gas_cost_overflow_protection() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult::new(NamedChain::Mainnet, from, to);
result.total_gas_cost = WeiAmount::from(U256::MAX - U256::from(1000u64));
result.add_transaction(GasForTx::L1(L1Gas {
gas_used: GasAmount::new(1000000),
effective_gas_price: GasPrice::new(1000000),
blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}));
assert_eq!(result.total_gas_cost, WeiAmount::from(U256::MAX));
assert_eq!(result.transaction_count, TransactionCount::new(1));
}
#[test]
fn test_gas_cost_merge_overflow_protection() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result1 = GasCostResult {
chain: NamedChain::Mainnet,
from,
to,
total_gas_cost: WeiAmount::from(U256::MAX - U256::from(100u64)),
transaction_count: TransactionCount::new(5),
breakdown: GasBreakdown::new(),
};
let result2 = GasCostResult {
chain: NamedChain::Mainnet,
from,
to,
total_gas_cost: WeiAmount::from(500u64),
transaction_count: TransactionCount::new(3),
breakdown: GasBreakdown::new(),
};
result1.merge(&result2);
assert_eq!(result1.total_gas_cost, WeiAmount::from(U256::MAX));
assert_eq!(result1.transaction_count, TransactionCount::new(8));
}
#[test]
fn test_gas_cost_result_zero_transactions() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let result = GasCostResult::new(NamedChain::Mainnet, from, to);
assert_eq!(result.total_gas_cost, WeiAmount::ZERO);
assert_eq!(result.transaction_count, TransactionCount::ZERO);
assert_eq!(result.chain, NamedChain::Mainnet);
assert_eq!(result.from, from);
assert_eq!(result.to, to);
}
#[test]
fn test_add_l1_fee() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult::new(NamedChain::Arbitrum, from, to);
result.add_l1_fee(L1DataFee::new(U256::from(1_000_000_000_000_000u64)));
assert_eq!(
result.total_gas_cost,
WeiAmount::from(1_000_000_000_000_000u64)
);
assert_eq!(
result.breakdown.l1_data_fee,
U256::from(1_000_000_000_000_000u64)
);
result.add_l1_fee(L1DataFee::new(U256::from(500_000_000_000_000u64)));
assert_eq!(
result.total_gas_cost,
WeiAmount::from(1_500_000_000_000_000u64)
);
assert_eq!(
result.breakdown.l1_data_fee,
U256::from(1_500_000_000_000_000u64)
);
}
#[test]
fn test_formatted_gas_cost() {
let from = address!("1111111111111111111111111111111111111111");
let to = address!("2222222222222222222222222222222222222222");
let mut result = GasCostResult::new(NamedChain::Mainnet, from, to);
result.total_gas_cost = WeiAmount::from(1_500_000_000_000_000_000u64);
let formatted = result.formatted_gas_cost();
assert!(formatted.starts_with("1.5"));
}
}