use alloy_chains::NamedChain;
use alloy_network::{Ethereum, Network};
use alloy_primitives::{Address, BlockNumber, B256, U256};
use alloy_provider::{network::eip2718::Typed2718, Provider};
use alloy_rpc_types::{Filter, Log, TransactionTrait};
use alloy_sol_types::SolEvent;
use op_alloy_network::Optimism;
use crate::config::policy::ScanPolicy;
use crate::errors::{GasCalculationError, RpcError};
use crate::events::definitions::{Approval, Transfer};
use crate::gas::adapter::{EthereumReceiptAdapter, OptimismReceiptAdapter, ReceiptAdapter};
use crate::gas::calculator::{GasCostCalculator, GasCostResult, GasForTx};
use crate::gas::transaction;
use crate::scan::LogScanner;
use crate::tracing::spans;
use tracing::{error, info, Instrument};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventType {
Transfer,
Approval,
}
impl EventType {
pub fn signature_hash(&self) -> B256 {
match self {
EventType::Transfer => Transfer::SIGNATURE_HASH,
EventType::Approval => Approval::SIGNATURE_HASH,
}
}
pub fn name(&self) -> &'static str {
match self {
EventType::Transfer => "Transfer",
EventType::Approval => "Approval",
}
}
fn decode_and_log(
&self,
log: &Log,
current_block: BlockNumber,
) -> Result<bool, GasCalculationError> {
let log_index = log.log_index.unwrap_or(0);
match self {
EventType::Transfer => match Transfer::decode_log(&log.inner) {
Ok(event) => {
info!(
?event,
current_block, "Processing Transfer event for gas cost"
);
Ok(true)
}
Err(e) => {
error!(error = ?e, "Failed to decode Transfer log for gas");
Err(GasCalculationError::event_decode_failed(log_index, e))
}
},
EventType::Approval => match Approval::decode_log(&log.inner) {
Ok(event) => {
info!(
?event,
current_block, "Processing Approval event for gas cost"
);
Ok(true)
}
Err(e) => {
error!(error = ?e, "Failed to decode Approval log for gas");
Err(GasCalculationError::event_decode_failed(log_index, e))
}
},
}
}
}
mod gas_calc_core {
use super::*;
pub(super) fn calculate_blob_gas_cost<N: Network>(
transaction: &N::TransactionResponse,
) -> U256 {
transaction::blob_gas_cost(transaction)
}
pub(super) fn calculate_effective_gas_price<N: Network>(
transaction: &N::TransactionResponse,
receipt_effective_gas_price: U256,
) -> U256 {
let effective_gas_price =
transaction::effective_gas_price(transaction, receipt_effective_gas_price);
if transaction::gas_price_override(transaction).is_none() {
info!("EIP-1559 or EIP-4844 transaction");
}
effective_gas_price
}
pub(super) fn create_event_filter(
event_type: EventType,
token: Address,
topic1: Address,
topic2: Address,
) -> Filter {
let event_topic = event_type.signature_hash();
Filter::new()
.address(token)
.event_signature(vec![event_topic])
.topic1(topic1)
.topic2(topic2)
}
}
impl<N: Network, P: Provider<N>> GasCostCalculator<N, P>
where
N::TransactionResponse: TransactionTrait + Typed2718,
{
async fn process_event_log<A: ReceiptAdapter<N>>(
&self,
log: &Log,
adapter: &A,
) -> Result<Option<GasForTx>, GasCalculationError> {
let tx_hash = log
.transaction_hash
.ok_or_else(GasCalculationError::missing_transaction_hash)?;
let span = spans::process_event_log(tx_hash);
let (transaction, receipt) = async {
let transaction = self
.provider
.get_transaction_by_hash(tx_hash)
.await
.map_err(|e| {
RpcError::request_failed(format!("get_transaction_by_hash({tx_hash})"), e)
})?
.ok_or_else(|| RpcError::TransactionNotFound { tx_hash })?;
let receipt = self
.provider
.get_transaction_receipt(tx_hash)
.await
.map_err(|e| {
RpcError::request_failed(format!("get_transaction_receipt({tx_hash})"), e)
})?
.ok_or_else(|| RpcError::ReceiptNotFound { tx_hash })?;
Ok::<_, GasCalculationError>((transaction, receipt))
}
.instrument(span)
.await?;
let gas_used = adapter.gas_used(&receipt);
let receipt_effective_gas_price = adapter.effective_gas_price(&receipt);
let effective_gas_price = gas_calc_core::calculate_effective_gas_price::<N>(
&transaction,
receipt_effective_gas_price,
);
info!(
?gas_used,
?effective_gas_price,
"Transaction details for gas calculation"
);
let base_gas_cost = gas_used.saturating_mul(effective_gas_price);
let blob_gas_cost = gas_calc_core::calculate_blob_gas_cost::<N>(&transaction);
let total_gas_cost = base_gas_cost.saturating_add(blob_gas_cost);
info!(
base_gas_cost = ?base_gas_cost,
blob_gas_cost = ?blob_gas_cost,
total_gas_cost = ?total_gas_cost,
"Calculated gas costs"
);
let gas_for_tx = match adapter.l1_data_fee(&receipt) {
Some(l1_fee) => {
GasForTx::from((gas_used, effective_gas_price, l1_fee))
}
None => {
GasForTx::from((gas_used, effective_gas_price))
}
};
info!(?gas_for_tx, "Gas for transaction");
Ok(Some(gas_for_tx))
}
#[allow(clippy::too_many_arguments)]
async fn process_logs_in_range<A: ReceiptAdapter<N>>(
&self,
event_type: EventType,
chain: NamedChain,
topic1_addr: Address,
topic2_addr: Address,
token: Address,
from_block: BlockNumber,
to_block: BlockNumber,
adapter: &A,
) -> Result<GasCostResult, GasCalculationError> {
let span = spans::process_logs_in_range(
event_type,
chain,
topic1_addr,
topic2_addr,
token,
from_block,
to_block,
);
async {
let mut result = GasCostResult::new(chain, topic1_addr, topic2_addr);
info!(
event_type = event_type.name(),
total_blocks = to_block.saturating_sub(from_block) + 1,
max_block_range = self.config.scan_config(chain).max_block_range.as_u64(),
"Starting log processing"
);
let filter_template =
gas_calc_core::create_event_filter(event_type, token, topic1_addr, topic2_addr);
let scanner = LogScanner::new(&self.provider, self.config.clone());
let event_name = event_type.name();
let logs = scanner
.scan::<GasCalculationError, _>(
chain,
filter_template,
from_block,
to_block,
|chunk_from, chunk_to, e| {
Some(GasCalculationError::from(RpcError::get_logs_failed(
format!("{event_name} events from block {chunk_from} to {chunk_to}"),
e,
)))
},
)
.await?;
for log in &logs {
let log_block = log.block_number.unwrap_or(from_block);
event_type.decode_and_log(log, log_block)?;
self.handle_log(log, &mut result, adapter).await?;
}
info!(
event_type = event_type.name(),
total_logs = logs.len(),
total_transactions = result.transaction_count.as_usize(),
total_gas_cost = %result.total_gas_cost,
"Completed log processing"
);
Ok(result)
}
.instrument(span)
.await
}
async fn handle_log<A: ReceiptAdapter<N>>(
&self,
log: &Log,
result: &mut GasCostResult,
adapter: &A,
) -> Result<(), GasCalculationError> {
match self.process_event_log(log, adapter).await {
Ok(Some(gas)) => {
result.add_transaction(gas);
}
Ok(None) => {
info!("No transfer event found");
}
Err(e) => {
error!(error = ?e, "Error processing transfer event for gas");
return Err(e);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn calculate_gas_cost_with_adapter<A: ReceiptAdapter<N>>(
&self,
event_type: EventType,
chain: NamedChain,
topic1_addr: Address,
topic2_addr: Address,
token: Address,
start_block: BlockNumber,
end_block: BlockNumber,
adapter: &A,
) -> Result<GasCostResult, GasCalculationError> {
let span = spans::calculate_gas_cost_with_adapter(
event_type,
chain,
topic1_addr,
topic2_addr,
start_block,
end_block,
);
async {
info!(
event_type = event_type.name(),
?chain,
topic1 = %topic1_addr,
topic2 = %topic2_addr,
start_block,
end_block,
block_count = end_block.saturating_sub(start_block) + 1,
"Starting gas cost calculation"
);
let (cached_result, gaps) = {
let cache = self.gas_cache.lock().await;
cache.calculate_gaps(chain, topic1_addr, topic2_addr, start_block, end_block)
};
if let Some(result) = cached_result.clone() {
if gaps.is_empty() {
info!(
event_type = event_type.name(),
?chain,
topic1 = %topic1_addr,
topic2 = %topic2_addr,
cached_tx_count = result.transaction_count.as_usize(),
cached_gas_cost = %result.total_gas_cost,
"Using complete cached result for gas cost block range"
);
return Ok(result);
}
}
let mut gas_data = cached_result
.unwrap_or_else(|| GasCostResult::new(chain, topic1_addr, topic2_addr));
info!(
event_type = event_type.name(),
gap_count = gaps.len(),
"Processing uncached block ranges"
);
for (gap_index, (gap_start, gap_end)) in gaps.iter().enumerate() {
info!(
event_type = event_type.name(),
?chain,
topic1 = %topic1_addr,
topic2 = %topic2_addr,
gap_start,
gap_end,
gap_index = gap_index + 1,
total_gaps = gaps.len(),
gap_blocks = gap_end.saturating_sub(*gap_start) + 1,
"Processing uncached block range for gas cost"
);
let gap_result = self
.process_logs_in_range(
event_type,
chain,
topic1_addr,
topic2_addr,
token,
*gap_start,
*gap_end,
adapter,
)
.await?;
{
let mut cache = self.gas_cache.lock().await;
cache.insert(
topic1_addr,
topic2_addr,
*gap_start,
*gap_end,
gap_result.clone(),
);
}
gas_data.merge(&gap_result);
info!(
event_type = event_type.name(),
gap_index = gap_index + 1,
gap_tx_count = gap_result.transaction_count.as_usize(),
gap_gas_cost = %gap_result.total_gas_cost,
cumulative_tx_count = gas_data.transaction_count.as_usize(),
cumulative_gas_cost = %gas_data.total_gas_cost,
"Completed gap processing"
);
}
{
let mut cache = self.gas_cache.lock().await;
cache.insert(
topic1_addr,
topic2_addr,
start_block,
end_block,
gas_data.clone(),
);
}
info!(
event_type = event_type.name(),
?chain,
topic1 = %topic1_addr,
topic2 = %topic2_addr,
total_gas_cost = %gas_data.total_gas_cost,
transaction_count = gas_data.transaction_count.as_usize(),
"Finished gas cost calculation"
);
Ok(gas_data)
}
.instrument(span)
.await
}
}
impl<P: Provider<Ethereum>> GasCostCalculator<Ethereum, P> {
pub async fn calculate_gas_cost_for_transfers_between_blocks(
&self,
chain: NamedChain,
from: Address,
to: Address,
token: Address,
start_block: BlockNumber,
end_block: BlockNumber,
) -> Result<GasCostResult, GasCalculationError> {
let adapter = EthereumReceiptAdapter;
self.calculate_gas_cost_with_adapter(
EventType::Transfer,
chain,
from,
to,
token,
start_block,
end_block,
&adapter,
)
.await
}
}
impl<P: Provider<Optimism>> GasCostCalculator<Optimism, P> {
pub async fn calculate_gas_cost_for_transfers_between_blocks(
&self,
chain: NamedChain,
from: Address,
to: Address,
token: Address,
start_block: BlockNumber,
end_block: BlockNumber,
) -> Result<GasCostResult, GasCalculationError> {
let adapter = OptimismReceiptAdapter;
self.calculate_gas_cost_with_adapter(
EventType::Transfer,
chain,
from,
to,
token,
start_block,
end_block,
&adapter,
)
.await
}
}
impl<P: Provider<Ethereum>> GasCostCalculator<Ethereum, P> {
pub async fn calculate_gas_cost_for_approvals_between_blocks(
&self,
chain: NamedChain,
owner: Address,
spender: Address,
token: Address,
start_block: BlockNumber,
end_block: BlockNumber,
) -> Result<GasCostResult, GasCalculationError> {
let adapter = EthereumReceiptAdapter;
self.calculate_gas_cost_with_adapter(
EventType::Approval,
chain,
owner,
spender,
token,
start_block,
end_block,
&adapter,
)
.await
}
}
impl<P: Provider<Optimism>> GasCostCalculator<Optimism, P> {
pub async fn calculate_gas_cost_for_approvals_between_blocks(
&self,
chain: NamedChain,
owner: Address,
spender: Address,
token: Address,
start_block: BlockNumber,
end_block: BlockNumber,
) -> Result<GasCostResult, GasCalculationError> {
let adapter = OptimismReceiptAdapter;
self.calculate_gas_cost_with_adapter(
EventType::Approval,
chain,
owner,
spender,
token,
start_block,
end_block,
&adapter,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eips::eip4844::DATA_GAS_PER_BLOB;
use alloy_primitives::B256;
use serde_json::json;
#[test]
fn test_blob_gas_per_blob_constant() {
assert_eq!(DATA_GAS_PER_BLOB, 131_072);
}
#[test]
fn test_create_transfer_filter_template_omits_block_range() {
let token = Address::ZERO;
let from = Address::from([0x11; 20]);
let to = Address::from([0x22; 20]);
let filter = gas_calc_core::create_event_filter(EventType::Transfer, token, from, to);
assert_eq!(filter.get_from_block(), None);
assert_eq!(filter.get_to_block(), None);
}
#[test]
fn test_create_approval_filter_template_omits_block_range() {
let token = Address::ZERO;
let owner = Address::from([0x11; 20]);
let spender = Address::from([0x22; 20]);
let filter = gas_calc_core::create_event_filter(EventType::Approval, token, owner, spender);
assert_eq!(filter.get_from_block(), None);
assert_eq!(filter.get_to_block(), None);
}
#[test]
fn test_event_type_signature_hash() {
assert_eq!(
EventType::Transfer.signature_hash(),
Transfer::SIGNATURE_HASH
);
assert_eq!(
EventType::Approval.signature_hash(),
Approval::SIGNATURE_HASH
);
}
#[test]
fn test_event_type_name() {
assert_eq!(EventType::Transfer.name(), "Transfer");
assert_eq!(EventType::Approval.name(), "Approval");
}
#[test]
fn test_calculate_effective_gas_price_uses_tx_gas_price_for_eip2930() {
let transaction: <Ethereum as Network>::TransactionResponse =
serde_json::from_value(json!({
"hash": B256::repeat_byte(0x11),
"nonce": "0x1",
"blockHash": B256::repeat_byte(0x22),
"blockNumber": "0x2a",
"transactionIndex": "0x0",
"from": Address::from([0x33; 20]),
"to": Address::from([0x44; 20]),
"value": "0x0",
"gasPrice": "0x539",
"gas": "0x5208",
"input": "0x",
"accessList": [],
"chainId": "0x1",
"r": B256::repeat_byte(0x55),
"s": B256::repeat_byte(0x66),
"v": "0x1",
"type": "0x1"
}))
.expect("valid access-list transaction response");
let effective_gas_price = gas_calc_core::calculate_effective_gas_price::<Ethereum>(
&transaction,
U256::from(999_u64),
);
assert_eq!(effective_gas_price, U256::from(0x539_u64));
}
}