use alloy_eips::eip4844::{
DATA_GAS_PER_BLOB, MAX_BLOBS_PER_BLOCK_DENCUN, TARGET_BLOBS_PER_BLOCK_DENCUN,
};
use alloy_primitives::U256;
use serde::{Deserialize, Serialize};
use std::ops::{Add, Mul};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct GasAmount(U256);
impl GasAmount {
pub const fn new(amount: u64) -> Self {
Self(U256::from_limbs([amount, 0, 0, 0]))
}
pub const fn as_u256(&self) -> U256 {
self.0
}
pub fn as_u64(&self) -> Option<u64> {
self.0.try_into().ok()
}
pub fn cost(&self, price: GasPrice) -> U256 {
self.0.saturating_mul(price.0)
}
}
impl From<u64> for GasAmount {
fn from(value: u64) -> Self {
Self::new(value)
}
}
impl From<U256> for GasAmount {
fn from(value: U256) -> Self {
Self(value)
}
}
impl Add for GasAmount {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0.saturating_add(rhs.0))
}
}
impl std::fmt::Display for GasAmount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct GasPrice(U256);
impl GasPrice {
pub const fn new(price_wei: u64) -> Self {
Self(U256::from_limbs([price_wei, 0, 0, 0]))
}
pub fn from_gwei(gwei: u64) -> Self {
Self(U256::from(gwei).saturating_mul(U256::from(1_000_000_000u64)))
}
pub const fn as_u256(&self) -> U256 {
self.0
}
pub fn as_gwei_f64(&self) -> f64 {
let gwei_divisor = 1_000_000_000u64;
let whole_gwei = self.0 / U256::from(gwei_divisor);
whole_gwei.to_string().parse::<f64>().unwrap_or(0.0)
}
pub fn total_cost(&self, amount: GasAmount) -> U256 {
self.0.saturating_mul(amount.0)
}
}
impl From<u64> for GasPrice {
fn from(value: u64) -> Self {
Self::new(value)
}
}
impl From<U256> for GasPrice {
fn from(value: U256) -> Self {
Self(value)
}
}
impl std::fmt::Display for GasPrice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let gwei = self.as_gwei_f64();
if gwei >= 1.0 {
write!(f, "{:.2} gwei", gwei)
} else {
write!(f, "{} wei", self.0)
}
}
}
impl Mul<GasPrice> for GasAmount {
type Output = U256;
fn mul(self, rhs: GasPrice) -> Self::Output {
self.cost(rhs)
}
}
impl Mul<GasAmount> for GasPrice {
type Output = U256;
fn mul(self, rhs: GasAmount) -> Self::Output {
self.total_cost(rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gas_amount_creation() {
let gas = GasAmount::new(21000);
assert_eq!(gas.as_u256(), U256::from(21000));
assert_eq!(gas.as_u64(), Some(21000));
}
#[test]
fn test_gas_price_creation() {
let price = GasPrice::new(50_000_000_000); assert_eq!(price.as_u256(), U256::from(50_000_000_000u64));
}
#[test]
fn test_gas_price_from_gwei() {
let price = GasPrice::from_gwei(50);
assert_eq!(price.as_u256(), U256::from(50_000_000_000u64));
}
#[test]
fn test_gas_cost_calculation() {
let gas = GasAmount::new(21000);
let price = GasPrice::from_gwei(50);
let cost = gas.cost(price);
assert_eq!(cost, U256::from(1_050_000_000_000_000u64));
}
#[test]
fn test_type_safe_multiplication() {
let gas = GasAmount::new(100000);
let price = GasPrice::from_gwei(10);
let cost1 = gas * price;
let cost2 = price * gas;
assert_eq!(cost1, cost2);
assert_eq!(cost1, U256::from(1_000_000_000_000_000u64));
}
#[test]
fn test_gas_amount_addition() {
let gas1 = GasAmount::new(21000);
let gas2 = GasAmount::new(50000);
let total = gas1 + gas2;
assert_eq!(total.as_u256(), U256::from(71000));
}
#[test]
fn test_saturating_arithmetic() {
let max_gas = GasAmount::from(U256::MAX);
let price = GasPrice::from_gwei(1);
let cost = max_gas.cost(price);
assert_eq!(cost, U256::MAX);
}
#[test]
fn test_display() {
let gas = GasAmount::new(21000);
assert_eq!(format!("{}", gas), "21000");
let price = GasPrice::new(50_000_000_000); assert_eq!(format!("{}", price), "50.00 gwei");
let small_price = GasPrice::new(100); assert_eq!(format!("{}", small_price), "100 wei");
}
#[test]
fn test_serialization() {
let gas = GasAmount::new(21000);
let json = serde_json::to_string(&gas).unwrap();
let deserialized: GasAmount = serde_json::from_str(&json).unwrap();
assert_eq!(gas, deserialized);
}
#[test]
fn test_conversions() {
let value = U256::from(12345u64);
let gas: GasAmount = value.into();
let back: U256 = gas.as_u256();
assert_eq!(value, back);
let price: GasPrice = value.into();
let back: U256 = price.as_u256();
assert_eq!(value, back);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BlobCount(usize);
impl BlobCount {
pub const ZERO: Self = Self(0);
pub const TARGET: Self = Self(TARGET_BLOBS_PER_BLOCK_DENCUN as usize);
pub const MAX: Self = Self(MAX_BLOBS_PER_BLOCK_DENCUN);
pub const fn new(count: usize) -> Self {
Self(count)
}
pub fn new_checked(count: usize) -> Option<Self> {
if count <= MAX_BLOBS_PER_BLOCK_DENCUN {
Some(Self(count))
} else {
None
}
}
pub const fn as_usize(&self) -> usize {
self.0
}
pub fn to_blob_gas_amount(&self) -> BlobGasAmount {
BlobGasAmount::new(U256::from(self.0 * DATA_GAS_PER_BLOB as usize))
}
pub fn is_valid(&self) -> bool {
self.0 <= MAX_BLOBS_PER_BLOCK_DENCUN
}
}
impl From<usize> for BlobCount {
fn from(value: usize) -> Self {
Self(value)
}
}
impl std::fmt::Display for BlobCount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0 == 1 {
write!(f, "1 blob")
} else {
write!(f, "{} blobs", self.0)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BlobGasAmount(U256);
impl BlobGasAmount {
pub const ZERO: Self = Self(U256::ZERO);
pub const fn new(gas: U256) -> Self {
Self(gas)
}
pub const fn as_u256(&self) -> U256 {
self.0
}
}
impl From<U256> for BlobGasAmount {
fn from(value: U256) -> Self {
Self(value)
}
}
impl Add for BlobGasAmount {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0.saturating_add(rhs.0))
}
}
impl std::fmt::Display for BlobGasAmount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} blob gas", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct BlobGasPrice(U256);
impl BlobGasPrice {
pub const ZERO: Self = Self(U256::ZERO);
pub const fn new(price_wei: U256) -> Self {
Self(price_wei)
}
pub fn from_gwei(gwei: u64) -> Self {
Self(U256::from(gwei).saturating_mul(U256::from(1_000_000_000u64)))
}
pub const fn as_u256(&self) -> U256 {
self.0
}
pub fn cost_for_gas(&self, blob_gas: U256) -> U256 {
self.0.saturating_mul(blob_gas)
}
pub fn cost_for_blobs(&self, count: BlobCount) -> U256 {
self.cost_for_gas(count.to_blob_gas_amount().as_u256())
}
pub fn as_gwei_f64(&self) -> f64 {
let gwei_divisor = 1_000_000_000u64;
let whole_gwei = self.0 / U256::from(gwei_divisor);
whole_gwei.to_string().parse::<f64>().unwrap_or(0.0)
}
}
impl From<u64> for BlobGasPrice {
fn from(value: u64) -> Self {
Self(U256::from(value))
}
}
impl From<u128> for BlobGasPrice {
fn from(value: u128) -> Self {
Self(U256::from(value))
}
}
impl From<U256> for BlobGasPrice {
fn from(value: U256) -> Self {
Self(value)
}
}
impl std::fmt::Display for BlobGasPrice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let gwei = self.as_gwei_f64();
if gwei >= 1.0 {
write!(f, "{gwei:.4} gwei (blob)")
} else {
write!(f, "{} wei (blob)", self.0)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct GasBreakdown {
pub execution_gas_cost: U256,
pub blob_gas_cost: U256,
pub l1_data_fee: U256,
pub blob_count: BlobCount,
pub blob_gas_price: BlobGasPrice,
}
impl GasBreakdown {
pub const fn new() -> Self {
Self {
execution_gas_cost: U256::ZERO,
blob_gas_cost: U256::ZERO,
l1_data_fee: U256::ZERO,
blob_count: BlobCount::ZERO,
blob_gas_price: BlobGasPrice::ZERO,
}
}
pub fn builder() -> GasBreakdownBuilder {
GasBreakdownBuilder::new()
}
pub fn total_cost(&self) -> U256 {
self.execution_gas_cost
.saturating_add(self.blob_gas_cost)
.saturating_add(self.l1_data_fee)
}
pub fn has_blob_gas(&self) -> bool {
self.blob_count.as_usize() > 0
}
pub fn has_l1_data_fee(&self) -> bool {
self.l1_data_fee > U256::ZERO
}
pub fn merge(&mut self, other: &Self) {
self.execution_gas_cost = self
.execution_gas_cost
.saturating_add(other.execution_gas_cost);
self.blob_gas_cost = self.blob_gas_cost.saturating_add(other.blob_gas_cost);
self.l1_data_fee = self.l1_data_fee.saturating_add(other.l1_data_fee);
self.blob_count = BlobCount::new(
self.blob_count
.as_usize()
.saturating_add(other.blob_count.as_usize()),
);
if other.blob_gas_price.as_u256() > U256::ZERO {
self.blob_gas_price = other.blob_gas_price;
}
}
}
impl Add for GasBreakdown {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let mut result = self;
result.merge(&rhs);
result
}
}
impl std::fmt::Display for GasBreakdown {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "execution: {} wei", self.execution_gas_cost)?;
if self.has_blob_gas() {
write!(
f,
", blob: {} wei ({} @ {})",
self.blob_gas_cost, self.blob_count, self.blob_gas_price
)?;
}
if self.has_l1_data_fee() {
write!(f, ", L1 data: {} wei", self.l1_data_fee)?;
}
write!(f, ", total: {} wei", self.total_cost())
}
}
#[derive(Debug, Default)]
pub struct GasBreakdownBuilder {
execution_gas_cost: U256,
blob_gas_cost: U256,
l1_data_fee: U256,
blob_count: BlobCount,
blob_gas_price: BlobGasPrice,
}
impl GasBreakdownBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn execution_gas_cost(mut self, cost: U256) -> Self {
self.execution_gas_cost = cost;
self
}
pub fn blob_gas_cost(mut self, cost: U256) -> Self {
self.blob_gas_cost = cost;
self
}
pub fn l1_data_fee(mut self, fee: U256) -> Self {
self.l1_data_fee = fee;
self
}
pub fn blob_count(mut self, count: BlobCount) -> Self {
self.blob_count = count;
self
}
pub fn blob_gas_price(mut self, price: BlobGasPrice) -> Self {
self.blob_gas_price = price;
self
}
pub fn build(self) -> GasBreakdown {
GasBreakdown {
execution_gas_cost: self.execution_gas_cost,
blob_gas_cost: self.blob_gas_cost,
l1_data_fee: self.l1_data_fee,
blob_count: self.blob_count,
blob_gas_price: self.blob_gas_price,
}
}
}
#[cfg(test)]
mod blob_tests {
use super::*;
#[test]
fn test_blob_count_constants() {
assert_eq!(BlobCount::ZERO.as_usize(), 0);
assert_eq!(BlobCount::TARGET.as_usize(), 3);
assert_eq!(BlobCount::MAX.as_usize(), 6);
}
#[test]
fn test_blob_count_new() {
let count = BlobCount::new(3);
assert_eq!(count.as_usize(), 3);
}
#[test]
fn test_blob_count_new_checked_valid() {
assert!(BlobCount::new_checked(0).is_some());
assert!(BlobCount::new_checked(1).is_some());
assert!(BlobCount::new_checked(3).is_some());
assert!(BlobCount::new_checked(6).is_some());
}
#[test]
fn test_blob_count_new_checked_invalid() {
assert!(BlobCount::new_checked(7).is_none());
assert!(BlobCount::new_checked(10).is_none());
assert!(BlobCount::new_checked(100).is_none());
}
#[test]
fn test_blob_count_is_valid() {
assert!(BlobCount::new(0).is_valid());
assert!(BlobCount::new(3).is_valid());
assert!(BlobCount::new(6).is_valid());
assert!(!BlobCount::new(7).is_valid());
assert!(!BlobCount::new(100).is_valid());
}
#[test]
fn test_blob_count_to_blob_gas_amount() {
let count = BlobCount::new(0);
let gas = count.to_blob_gas_amount();
assert_eq!(gas.as_u256(), U256::ZERO);
let count = BlobCount::new(1);
let gas = count.to_blob_gas_amount();
assert_eq!(gas.as_u256(), U256::from(131_072));
let count = BlobCount::new(2);
let gas = count.to_blob_gas_amount();
assert_eq!(gas.as_u256(), U256::from(262_144));
let count = BlobCount::new(6);
let gas = count.to_blob_gas_amount();
assert_eq!(gas.as_u256(), U256::from(786_432)); }
#[test]
fn test_blob_count_display() {
assert_eq!(format!("{}", BlobCount::new(1)), "1 blob");
assert_eq!(format!("{}", BlobCount::new(2)), "2 blobs");
assert_eq!(format!("{}", BlobCount::new(6)), "6 blobs");
}
#[test]
fn test_blob_count_from() {
let count: BlobCount = 3usize.into();
assert_eq!(count.as_usize(), 3);
}
#[test]
fn test_blob_gas_amount_creation() {
let gas = BlobGasAmount::new(U256::from(131_072));
assert_eq!(gas.as_u256(), U256::from(131_072));
}
#[test]
fn test_blob_gas_amount_zero() {
assert_eq!(BlobGasAmount::ZERO.as_u256(), U256::ZERO);
}
#[test]
fn test_blob_gas_amount_addition() {
let gas1 = BlobGasAmount::new(U256::from(131_072));
let gas2 = BlobGasAmount::new(U256::from(131_072));
let total = gas1 + gas2;
assert_eq!(total.as_u256(), U256::from(262_144));
}
#[test]
fn test_blob_gas_amount_saturating_add() {
let max_gas = BlobGasAmount::from(U256::MAX);
let gas = BlobGasAmount::new(U256::from(1));
let result = max_gas + gas;
assert_eq!(result.as_u256(), U256::MAX);
}
#[test]
fn test_blob_gas_amount_display() {
let gas = BlobGasAmount::new(U256::from(131_072));
assert_eq!(format!("{}", gas), "131072 blob gas");
}
#[test]
fn test_blob_gas_amount_from() {
let gas: BlobGasAmount = U256::from(262_144).into();
assert_eq!(gas.as_u256(), U256::from(262_144));
}
#[test]
fn test_eip4844_constants() {
assert_eq!(DATA_GAS_PER_BLOB, 131_072);
assert_eq!(MAX_BLOBS_PER_BLOCK_DENCUN, 6);
assert_eq!(TARGET_BLOBS_PER_BLOCK_DENCUN, 3);
}
#[test]
fn test_blob_count_serialization() {
let count = BlobCount::new(3);
let json = serde_json::to_string(&count).unwrap();
let deserialized: BlobCount = serde_json::from_str(&json).unwrap();
assert_eq!(count, deserialized);
}
#[test]
fn test_blob_gas_amount_serialization() {
let gas = BlobGasAmount::new(U256::from(131_072));
let json = serde_json::to_string(&gas).unwrap();
let deserialized: BlobGasAmount = serde_json::from_str(&json).unwrap();
assert_eq!(gas, deserialized);
}
#[test]
fn test_blob_gas_price_creation() {
let price = BlobGasPrice::new(U256::from(1_000_000_000)); assert_eq!(price.as_u256(), U256::from(1_000_000_000));
}
#[test]
fn test_blob_gas_price_from_gwei() {
let price = BlobGasPrice::from_gwei(25);
assert_eq!(price.as_u256(), U256::from(25_000_000_000u64));
}
#[test]
fn test_blob_gas_price_cost_for_gas() {
let price = BlobGasPrice::from_gwei(1); let blob_gas = U256::from(131_072); let cost = price.cost_for_gas(blob_gas);
assert_eq!(cost, U256::from(131_072_000_000_000u64));
}
#[test]
fn test_blob_gas_price_cost_for_blobs() {
let price = BlobGasPrice::from_gwei(1);
let cost = price.cost_for_blobs(BlobCount::new(2));
assert_eq!(cost, U256::from(262_144_000_000_000u64));
}
#[test]
fn test_blob_gas_price_display() {
let price = BlobGasPrice::from_gwei(25);
let display = format!("{price}");
assert!(display.contains("gwei"));
assert!(display.contains("blob"));
let small_price = BlobGasPrice::new(U256::from(100));
let display = format!("{small_price}");
assert!(display.contains("wei"));
}
#[test]
fn test_blob_gas_price_serialization() {
let price = BlobGasPrice::from_gwei(10);
let json = serde_json::to_string(&price).unwrap();
let deserialized: BlobGasPrice = serde_json::from_str(&json).unwrap();
assert_eq!(price, deserialized);
}
#[test]
fn test_gas_breakdown_new() {
let breakdown = GasBreakdown::new();
assert_eq!(breakdown.execution_gas_cost, U256::ZERO);
assert_eq!(breakdown.blob_gas_cost, U256::ZERO);
assert_eq!(breakdown.l1_data_fee, U256::ZERO);
assert_eq!(breakdown.blob_count, BlobCount::ZERO);
assert_eq!(breakdown.total_cost(), U256::ZERO);
}
#[test]
fn test_gas_breakdown_builder() {
let breakdown = GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000_000u64))
.blob_gas_cost(U256::from(500_000u64))
.l1_data_fee(U256::from(200_000u64))
.blob_count(BlobCount::new(2))
.blob_gas_price(BlobGasPrice::from_gwei(1))
.build();
assert_eq!(breakdown.execution_gas_cost, U256::from(1_000_000u64));
assert_eq!(breakdown.blob_gas_cost, U256::from(500_000u64));
assert_eq!(breakdown.l1_data_fee, U256::from(200_000u64));
assert_eq!(breakdown.blob_count, BlobCount::new(2));
assert_eq!(breakdown.total_cost(), U256::from(1_700_000u64));
}
#[test]
fn test_gas_breakdown_has_blob_gas() {
let without_blobs = GasBreakdown::new();
assert!(!without_blobs.has_blob_gas());
let with_blobs = GasBreakdown::builder()
.blob_count(BlobCount::new(1))
.build();
assert!(with_blobs.has_blob_gas());
}
#[test]
fn test_gas_breakdown_has_l1_data_fee() {
let without_l1 = GasBreakdown::new();
assert!(!without_l1.has_l1_data_fee());
let with_l1 = GasBreakdown::builder()
.l1_data_fee(U256::from(100u64))
.build();
assert!(with_l1.has_l1_data_fee());
}
#[test]
fn test_gas_breakdown_merge() {
let mut breakdown1 = GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000u64))
.blob_gas_cost(U256::from(500u64))
.blob_count(BlobCount::new(1))
.build();
let breakdown2 = GasBreakdown::builder()
.execution_gas_cost(U256::from(2_000u64))
.blob_gas_cost(U256::from(1_000u64))
.blob_count(BlobCount::new(2))
.build();
breakdown1.merge(&breakdown2);
assert_eq!(breakdown1.execution_gas_cost, U256::from(3_000u64));
assert_eq!(breakdown1.blob_gas_cost, U256::from(1_500u64));
assert_eq!(breakdown1.blob_count, BlobCount::new(3));
}
#[test]
fn test_gas_breakdown_add() {
let breakdown1 = GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000u64))
.build();
let breakdown2 = GasBreakdown::builder()
.execution_gas_cost(U256::from(2_000u64))
.build();
let result = breakdown1 + breakdown2;
assert_eq!(result.execution_gas_cost, U256::from(3_000u64));
}
#[test]
fn test_gas_breakdown_display() {
let breakdown = GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000_000u64))
.blob_gas_cost(U256::from(500_000u64))
.blob_count(BlobCount::new(2))
.blob_gas_price(BlobGasPrice::from_gwei(1))
.l1_data_fee(U256::from(200_000u64))
.build();
let display = format!("{breakdown}");
assert!(display.contains("execution"));
assert!(display.contains("blob"));
assert!(display.contains("L1 data"));
assert!(display.contains("total"));
}
#[test]
fn test_gas_breakdown_serialization() {
let breakdown = GasBreakdown::builder()
.execution_gas_cost(U256::from(1_000_000u64))
.blob_gas_cost(U256::from(500_000u64))
.build();
let json = serde_json::to_string(&breakdown).unwrap();
let deserialized: GasBreakdown = serde_json::from_str(&json).unwrap();
assert_eq!(breakdown, deserialized);
}
}