semioscan 0.10.0

Production-grade Rust library for blockchain analytics: gas calculation, price extraction, and block window calculations for EVM chains
Documentation
// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
//
// SPDX-License-Identifier: Apache-2.0

//! Receipt adapters for extracting gas data from different network types
//!
//! This module provides network-specific adapters for extracting gas cost information
//! from transaction receipts. Different blockchain networks have different receipt formats
//! and gas calculation rules, particularly regarding L1 data fees on L2 networks.
//!
//! # Network Types
//!
//! - **Ethereum**: L1 chains (Ethereum, Arbitrum, Polygon) with no L1 data fees
//! - **Optimism**: Optimism Stack chains (Base, Optimism, Mode) with L1 data fees
//!
//! # Example: Using the Ethereum adapter
//!
//! ```rust
//! use semioscan::{EthereumReceiptAdapter, ReceiptAdapter};
//! use alloy_network::Ethereum;
//!
//! // Create adapter for Ethereum-like chains (Ethereum, Arbitrum, Polygon)
//! let adapter = EthereumReceiptAdapter;
//!
//! // Adapter extracts gas_used, effective_gas_price, and l1_data_fee from receipts
//! // For Ethereum chains, l1_data_fee is always None
//! ```
//!
//! # Example: Using the Optimism adapter
//!
//! ```rust
//! use semioscan::{OptimismReceiptAdapter, ReceiptAdapter};
//! use op_alloy_network::Optimism;
//!
//! // Create adapter for Optimism Stack chains (Base, Optimism, Mode, Fraxtal)
//! let adapter = OptimismReceiptAdapter;
//!
//! // Adapter extracts gas_used, effective_gas_price, and l1_data_fee from receipts
//! // For Optimism chains, l1_data_fee is Some(U256) representing L1 posting costs
//! ```

use alloy_network::{Ethereum, Network};
use alloy_primitives::U256;
use op_alloy_network::Optimism;

/// Trait for network-specific receipt handling
///
/// Different blockchain networks use different receipt formats and have different
/// gas cost components. This trait abstracts over these differences to provide
/// a uniform interface for extracting gas data.
///
/// # Implementors
///
/// - [`EthereumReceiptAdapter`]: For L1 chains without L1 data fees
/// - [`OptimismReceiptAdapter`]: For Optimism Stack chains with L1 data fees
///
/// # Example: Generic function using ReceiptAdapter
///
/// ```rust,ignore
/// use semioscan::ReceiptAdapter;
/// use alloy_network::Network;
///
/// fn calculate_total_cost<N: Network>(
///     adapter: &impl ReceiptAdapter<N>,
///     receipt: &N::ReceiptResponse
/// ) -> alloy_primitives::U256 {
///     let gas_used = adapter.gas_used(receipt);
///     let price = adapter.effective_gas_price(receipt);
///     let l1_fee = adapter.l1_data_fee(receipt).unwrap_or_default();
///
///     gas_used.saturating_mul(price).saturating_add(l1_fee)
/// }
/// ```
pub trait ReceiptAdapter<N: Network> {
    /// Extract the amount of gas used by a transaction
    ///
    /// # Returns
    ///
    /// The gas consumed by the transaction execution
    fn gas_used(&self, receipt: &N::ReceiptResponse) -> U256;

    /// Extract the effective gas price paid for the transaction
    ///
    /// For EIP-1559 transactions, this is the actual price paid per gas unit,
    /// which may be lower than the max fee per gas.
    ///
    /// # Returns
    ///
    /// The effective gas price in wei per gas unit
    fn effective_gas_price(&self, receipt: &N::ReceiptResponse) -> U256;

    /// Extract the L1 data fee (for L2 chains only)
    ///
    /// On Optimism Stack chains, transactions pay an additional fee to cover
    /// the cost of posting transaction data to the L1 chain (Ethereum).
    ///
    /// # Returns
    ///
    /// - `Some(fee)`: L1 data fee in wei (for Optimism Stack chains)
    /// - `None`: No L1 data fee (for Ethereum, Arbitrum, Polygon)
    fn l1_data_fee(&self, receipt: &N::ReceiptResponse) -> Option<U256>;
}

/// Receipt adapter for Ethereum and Ethereum-like chains
///
/// Use this adapter for chains that don't have L1 data fees:
/// - Ethereum (L1)
/// - Arbitrum (L2 with different fee model)
/// - Polygon (L1)
/// - Avalanche (L1)
/// - BNB Chain (L1)
///
/// # Example
///
/// ```rust
/// use semioscan::{EthereumReceiptAdapter, ReceiptAdapter};
/// use alloy_network::Ethereum;
///
/// let adapter = EthereumReceiptAdapter;
/// // Use adapter with transaction receipts to extract gas data
/// ```
pub struct EthereumReceiptAdapter;

impl ReceiptAdapter<Ethereum> for EthereumReceiptAdapter {
    fn gas_used(&self, receipt: &<Ethereum as Network>::ReceiptResponse) -> U256 {
        U256::from(receipt.gas_used)
    }

    fn effective_gas_price(&self, receipt: &<Ethereum as Network>::ReceiptResponse) -> U256 {
        U256::from(receipt.effective_gas_price)
    }

    fn l1_data_fee(&self, _receipt: &<Ethereum as Network>::ReceiptResponse) -> Option<U256> {
        None // Ethereum L1 as well as chains like Arbitrum and Polygon don't have L1 data fees
    }
}

/// Receipt adapter for Optimism Stack chains
///
/// Use this adapter for chains that have L1 data fees:
/// - Base
/// - Optimism
/// - Mode
/// - Fraxtal
/// - Sonic
///
/// These chains pay an additional L1 data fee to cover the cost of posting
/// transaction data to Ethereum L1.
///
/// # Example
///
/// ```rust
/// use semioscan::{OptimismReceiptAdapter, ReceiptAdapter};
/// use op_alloy_network::Optimism;
///
/// let adapter = OptimismReceiptAdapter;
/// // Use adapter with transaction receipts to extract gas data including L1 fees
/// ```
pub struct OptimismReceiptAdapter;

impl ReceiptAdapter<Optimism> for OptimismReceiptAdapter {
    fn gas_used(&self, receipt: &<Optimism as Network>::ReceiptResponse) -> U256 {
        U256::from(receipt.inner.gas_used)
    }

    fn effective_gas_price(&self, receipt: &<Optimism as Network>::ReceiptResponse) -> U256 {
        U256::from(receipt.inner.effective_gas_price)
    }

    fn l1_data_fee(&self, receipt: &<Optimism as Network>::ReceiptResponse) -> Option<U256> {
        Some(U256::from(receipt.l1_block_info.l1_fee.unwrap_or_default()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Create an Ethereum receipt with known gas values for testing
    fn create_ethereum_receipt(
        gas_used: u64,
        effective_gas_price: u128,
    ) -> <Ethereum as Network>::ReceiptResponse {
        let json = serde_json::json!({
            "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "blockNumber": "0x1",
            "transactionIndex": "0x0",
            "from": "0x0000000000000000000000000000000000000000",
            "to": "0x0000000000000000000000000000000000000000",
            "cumulativeGasUsed": format!("0x{:x}", gas_used),
            "gasUsed": format!("0x{:x}", gas_used),
            "effectiveGasPrice": format!("0x{:x}", effective_gas_price),
            "logs": [],
            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
            "status": "0x1",
            "type": "0x2"
        });

        serde_json::from_value(json).expect("Failed to create test Ethereum receipt")
    }

    /// Create an Optimism receipt with known gas values and L1 fee for testing
    fn create_optimism_receipt(
        gas_used: u64,
        effective_gas_price: u128,
        l1_fee: Option<u128>,
    ) -> <Optimism as Network>::ReceiptResponse {
        let json = serde_json::json!({
            "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "blockNumber": "0x1",
            "transactionIndex": "0x0",
            "from": "0x0000000000000000000000000000000000000000",
            "to": "0x0000000000000000000000000000000000000000",
            "cumulativeGasUsed": format!("0x{:x}", gas_used),
            "gasUsed": format!("0x{:x}", gas_used),
            "effectiveGasPrice": format!("0x{:x}", effective_gas_price),
            "logs": [],
            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
            "status": "0x1",
            "type": "0x2",
            "l1Fee": l1_fee.map(|fee| format!("0x{:x}", fee)),
            "l1GasUsed": "0x0",
            "l1GasPrice": "0x0",
            "l1FeeScalar": "1.0"
        });

        serde_json::from_value(json).expect("Failed to create test Optimism receipt")
    }

    #[test]
    fn ethereum_adapter_extracts_gas_used() {
        let adapter = EthereumReceiptAdapter;
        let receipt = create_ethereum_receipt(50_000, 30_000_000_000);

        let gas_used = adapter.gas_used(&receipt);

        assert_eq!(gas_used, U256::from(50_000));
    }

    #[test]
    fn ethereum_adapter_extracts_effective_gas_price() {
        let adapter = EthereumReceiptAdapter;
        let receipt = create_ethereum_receipt(50_000, 30_000_000_000);

        let price = adapter.effective_gas_price(&receipt);

        assert_eq!(price, U256::from(30_000_000_000_u128));
    }

    #[test]
    fn ethereum_adapter_returns_none_for_l1_fee() {
        let adapter = EthereumReceiptAdapter;
        let receipt = create_ethereum_receipt(50_000, 30_000_000_000);

        let l1_fee = adapter.l1_data_fee(&receipt);

        assert_eq!(l1_fee, None);
    }

    #[test]
    fn optimism_adapter_extracts_gas_used() {
        let adapter = OptimismReceiptAdapter;
        let receipt = create_optimism_receipt(75_000, 20_000_000_000, Some(1_000_000));

        let gas_used = adapter.gas_used(&receipt);

        assert_eq!(gas_used, U256::from(75_000));
    }

    #[test]
    fn optimism_adapter_extracts_effective_gas_price() {
        let adapter = OptimismReceiptAdapter;
        let receipt = create_optimism_receipt(75_000, 20_000_000_000, Some(1_000_000));

        let price = adapter.effective_gas_price(&receipt);

        assert_eq!(price, U256::from(20_000_000_000_u128));
    }

    #[test]
    fn optimism_adapter_extracts_l1_fee_when_present() {
        let adapter = OptimismReceiptAdapter;
        let receipt = create_optimism_receipt(75_000, 20_000_000_000, Some(1_500_000));

        let l1_fee = adapter.l1_data_fee(&receipt);

        assert_eq!(l1_fee, Some(U256::from(1_500_000)));
    }

    #[test]
    fn optimism_adapter_returns_zero_when_l1_fee_is_none() {
        let adapter = OptimismReceiptAdapter;
        let receipt = create_optimism_receipt(75_000, 20_000_000_000, None);

        let l1_fee = adapter.l1_data_fee(&receipt);

        // Implementation returns Some(0) when l1_fee is None in receipt
        assert_eq!(l1_fee, Some(U256::ZERO));
    }

    #[test]
    fn adapter_trait_object_safety() {
        // Verify that ReceiptAdapter can be used as a trait object (dynamic dispatch)
        let _ethereum_adapter: &dyn ReceiptAdapter<Ethereum> = &EthereumReceiptAdapter;
        let _optimism_adapter: &dyn ReceiptAdapter<Optimism> = &OptimismReceiptAdapter;
    }
}