use alloy_chains::NamedChain;
use alloy_primitives::{Address, BlockNumber, TxHash, U256};
use serde::{Deserialize, Serialize};
use crate::types::config::TransactionCount;
use crate::types::gas::{GasAmount, GasPrice};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GasAndAmountForTx {
pub tx_hash: TxHash,
pub block_number: BlockNumber,
pub gas_used: GasAmount,
pub effective_gas_price: GasPrice,
pub l1_fee: Option<U256>,
pub blob_gas_cost: U256,
pub transferred_amount: U256,
}
impl GasAndAmountForTx {
#[must_use]
pub fn total_gas_cost(&self) -> U256 {
let l2_execution_cost = self.gas_used * self.effective_gas_price;
let total_cost = l2_execution_cost.saturating_add(self.blob_gas_cost);
total_cost.saturating_add(self.l1_fee.unwrap_or_default())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CombinedDataLookupStage {
Transaction,
Receipt,
}
impl CombinedDataLookupStage {
#[must_use]
pub const fn operation_name(self) -> &'static str {
match self {
Self::Transaction => "get_transaction_by_hash",
Self::Receipt => "get_transaction_receipt",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CombinedDataLookupPass {
Batch,
SerialFallback,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CombinedDataLookupAttempt {
pub pass: CombinedDataLookupPass,
pub stage: CombinedDataLookupStage,
pub error: String,
pub error_chain: Vec<String>,
pub transport_error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CombinedDataLookupFailure {
pub tx_hash: TxHash,
pub block_number: BlockNumber,
pub transfer_value: U256,
pub attempts: Vec<CombinedDataLookupAttempt>,
}
impl CombinedDataLookupFailure {
#[must_use]
pub fn final_attempt(&self) -> Option<&CombinedDataLookupAttempt> {
self.attempts.last()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CombinedDataRetrievalMetadata {
pub skipped_logs: usize,
pub fallback_attempts: usize,
pub fallback_recovered: usize,
pub partial_failures: Vec<CombinedDataLookupFailure>,
}
impl CombinedDataRetrievalMetadata {
#[must_use]
pub fn has_partial_failures(&self) -> bool {
!self.partial_failures.is_empty()
}
#[must_use]
pub fn skipped_tx_hashes(&self) -> Vec<TxHash> {
self.partial_failures
.iter()
.map(|failure| failure.tx_hash)
.collect()
}
pub fn record_fallback_attempts(&mut self, attempts: usize) {
self.fallback_attempts += attempts;
}
pub fn record_fallback_recovery(&mut self) {
self.fallback_recovered += 1;
}
pub fn record_partial_failure(&mut self, failure: CombinedDataLookupFailure) {
self.skipped_logs += 1;
self.partial_failures.push(failure);
}
pub fn merge(&mut self, other: &CombinedDataRetrievalMetadata) {
self.skipped_logs += other.skipped_logs;
self.fallback_attempts += other.fallback_attempts;
self.fallback_recovered += other.fallback_recovered;
self.partial_failures
.extend(other.partial_failures.iter().cloned());
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CombinedDataResult {
pub chain: NamedChain,
pub from_address: Address,
pub to_address: Address,
pub token_address: Address,
pub total_l2_execution_cost: U256,
pub total_blob_gas_cost: U256,
pub total_l1_fee: U256,
pub overall_total_gas_cost: U256,
pub total_amount_transferred: U256,
pub transaction_count: TransactionCount,
pub transactions_data: Vec<GasAndAmountForTx>,
#[serde(default)]
pub retrieval_metadata: CombinedDataRetrievalMetadata,
}
impl CombinedDataResult {
#[must_use]
pub fn new(
chain: NamedChain,
from_address: Address,
to_address: Address,
token_address: Address,
) -> Self {
Self {
chain,
from_address,
to_address,
token_address,
total_l2_execution_cost: U256::ZERO,
total_blob_gas_cost: U256::ZERO,
total_l1_fee: U256::ZERO,
overall_total_gas_cost: U256::ZERO,
total_amount_transferred: U256::ZERO,
transaction_count: TransactionCount::new(0),
transactions_data: Vec::new(),
retrieval_metadata: CombinedDataRetrievalMetadata::default(),
}
}
pub fn add_transaction_data(&mut self, data: GasAndAmountForTx) {
let l2_execution_cost = data.gas_used * data.effective_gas_price;
self.total_l2_execution_cost = self
.total_l2_execution_cost
.saturating_add(l2_execution_cost);
self.total_blob_gas_cost = self.total_blob_gas_cost.saturating_add(data.blob_gas_cost);
self.total_l1_fee = self
.total_l1_fee
.saturating_add(data.l1_fee.unwrap_or_default());
self.overall_total_gas_cost = self
.overall_total_gas_cost
.saturating_add(data.total_gas_cost());
self.total_amount_transferred = self
.total_amount_transferred
.saturating_add(data.transferred_amount);
self.transaction_count += TransactionCount::new(1);
self.transactions_data.push(data);
}
pub fn merge(&mut self, other: &CombinedDataResult) {
self.total_l2_execution_cost = self
.total_l2_execution_cost
.saturating_add(other.total_l2_execution_cost);
self.total_blob_gas_cost = self
.total_blob_gas_cost
.saturating_add(other.total_blob_gas_cost);
self.total_l1_fee = self.total_l1_fee.saturating_add(other.total_l1_fee);
self.overall_total_gas_cost = self
.overall_total_gas_cost
.saturating_add(other.overall_total_gas_cost);
self.total_amount_transferred = self
.total_amount_transferred
.saturating_add(other.total_amount_transferred);
self.transaction_count += other.transaction_count;
self.transactions_data
.extend(other.transactions_data.iter().cloned());
self.retrieval_metadata.merge(&other.retrieval_metadata);
}
#[must_use]
pub fn is_partial(&self) -> bool {
self.retrieval_metadata.has_partial_failures()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::TxHash;
fn create_test_tx(
gas_used: u64,
gas_price: u64,
l1_fee: Option<u64>,
blob_gas_cost: u64,
transferred_amount: u64,
) -> GasAndAmountForTx {
GasAndAmountForTx {
tx_hash: TxHash::ZERO,
block_number: 1000,
gas_used: GasAmount::from(gas_used),
effective_gas_price: GasPrice::from(gas_price),
l1_fee: l1_fee.map(U256::from),
blob_gas_cost: U256::from(blob_gas_cost),
transferred_amount: U256::from(transferred_amount),
}
}
#[test]
fn test_total_gas_cost_basic() {
let tx = create_test_tx(
21000, 50, None, 0, 1000, );
let total = tx.total_gas_cost();
let expected = U256::from(21000 * 50);
assert_eq!(total, expected, "Should calculate L2 gas cost correctly");
}
#[test]
fn test_total_gas_cost_with_l1_fee() {
let tx = create_test_tx(
50000, 100, Some(200000), 0, 5000,
);
let total = tx.total_gas_cost();
let expected = U256::from(50000 * 100 + 200000);
assert_eq!(
total, expected,
"Should include L1 fee in total gas cost calculation"
);
}
#[test]
fn test_total_gas_cost_with_blob_gas() {
let tx = create_test_tx(
30000, 75, None, 100000, 2500,
);
let total = tx.total_gas_cost();
let expected = U256::from(30000 * 75 + 100000);
assert_eq!(
total, expected,
"Should include blob gas cost in total calculation"
);
}
#[test]
fn test_total_gas_cost_all_components() {
let tx = create_test_tx(
100000, 200, Some(500000), 300000, 10000,
);
let total = tx.total_gas_cost();
let expected = U256::from(100000 * 200 + 500000 + 300000);
assert_eq!(
total, expected,
"Should correctly sum all gas cost components"
);
}
#[test]
fn test_total_gas_cost_large_values() {
let large_gas = u64::MAX / 2;
let large_price = 100;
let tx = create_test_tx(large_gas, large_price, Some(1_000_000), 500_000, 100_000);
let total = tx.total_gas_cost();
let expected_l2_cost = U256::from(large_gas) * U256::from(large_price);
let expected = expected_l2_cost + U256::from(1_000_000) + U256::from(500_000);
assert_eq!(total, expected, "Should handle large values correctly");
}
#[test]
fn test_total_gas_cost_saturating_arithmetic() {
let max_gas = u64::MAX;
let max_price = u64::MAX;
let tx = create_test_tx(max_gas, max_price, Some(u64::MAX), u64::MAX, 1000);
let total = tx.total_gas_cost();
assert!(
total > U256::ZERO,
"Should produce non-zero result even with overflow"
);
}
#[test]
fn test_clone_and_equality() {
let tx1 = create_test_tx(21000, 50, None, 0, 1000);
let tx2 = tx1.clone();
assert_eq!(tx1, tx2, "Cloned transactions should be equal");
assert_eq!(
tx1.total_gas_cost(),
tx2.total_gas_cost(),
"Total costs should match"
);
}
#[test]
fn test_debug_representation() {
let tx = create_test_tx(21000, 50, Some(1000), 500, 2000);
let debug_str = format!("{:?}", tx);
assert!(
debug_str.contains("gas_used"),
"Debug output should include gas_used"
);
assert!(
debug_str.contains("tx_hash"),
"Debug output should include tx_hash"
);
}
#[test]
fn test_combined_result_is_partial_when_metadata_has_failures() {
let mut result = CombinedDataResult::new(
NamedChain::Mainnet,
Address::ZERO,
Address::ZERO,
Address::ZERO,
);
result
.retrieval_metadata
.record_partial_failure(CombinedDataLookupFailure {
tx_hash: TxHash::repeat_byte(0x11),
block_number: 123,
transfer_value: U256::from(42_u64),
attempts: vec![CombinedDataLookupAttempt {
pass: CombinedDataLookupPass::Batch,
stage: CombinedDataLookupStage::Transaction,
error: "RPC error".to_string(),
error_chain: vec!["RPC error".to_string(), "inner transport".to_string()],
transport_error: Some("inner transport".to_string()),
}],
});
assert!(result.is_partial());
assert!(result.retrieval_metadata.has_partial_failures());
assert_eq!(
result.retrieval_metadata.skipped_tx_hashes(),
vec![TxHash::repeat_byte(0x11)]
);
}
#[test]
fn test_metadata_has_partial_failures_tracks_failure_entries_not_skip_counter() {
let metadata = CombinedDataRetrievalMetadata {
skipped_logs: 0,
fallback_attempts: 0,
fallback_recovered: 0,
partial_failures: vec![CombinedDataLookupFailure {
tx_hash: TxHash::repeat_byte(0x22),
block_number: 456,
transfer_value: U256::from(7_u64),
attempts: vec![CombinedDataLookupAttempt {
pass: CombinedDataLookupPass::Batch,
stage: CombinedDataLookupStage::Receipt,
error: "missing receipt".to_string(),
error_chain: vec!["missing receipt".to_string()],
transport_error: None,
}],
}],
};
assert!(metadata.has_partial_failures());
}
#[test]
fn test_combined_result_merge_includes_retrieval_metadata() {
let mut left = CombinedDataResult::new(
NamedChain::Mainnet,
Address::ZERO,
Address::ZERO,
Address::ZERO,
);
let mut right = CombinedDataResult::new(
NamedChain::Mainnet,
Address::ZERO,
Address::ZERO,
Address::ZERO,
);
left.retrieval_metadata.fallback_attempts = 1;
left.retrieval_metadata.fallback_recovered = 1;
right
.retrieval_metadata
.record_partial_failure(CombinedDataLookupFailure {
tx_hash: TxHash::repeat_byte(0x22),
block_number: 456,
transfer_value: U256::from(99_u64),
attempts: vec![CombinedDataLookupAttempt {
pass: CombinedDataLookupPass::SerialFallback,
stage: CombinedDataLookupStage::Receipt,
error: "missing receipt".to_string(),
error_chain: vec!["missing receipt".to_string()],
transport_error: None,
}],
});
left.merge(&right);
assert_eq!(left.retrieval_metadata.fallback_attempts, 1);
assert_eq!(left.retrieval_metadata.fallback_recovered, 1);
assert_eq!(left.retrieval_metadata.skipped_logs, 1);
assert_eq!(left.retrieval_metadata.partial_failures.len(), 1);
assert_eq!(
left.retrieval_metadata.skipped_tx_hashes(),
vec![TxHash::repeat_byte(0x22)]
);
}
}