use alloy_chains::NamedChain;
use alloy_primitives::{Address, BlockNumber};
use alloy_provider::Provider;
use alloy_sol_types::SolEvent;
use tracing::{info, warn};
use crate::config::SemioscanConfig;
use crate::errors::EventProcessingError;
use crate::events::definitions::Transfer;
use crate::events::filter::TransferFilterBuilder;
use crate::events::scanner::EventScanner;
use crate::types::tokens::TokenAmount;
pub struct AmountResult {
pub chain: NamedChain,
pub to: Address,
pub token: Address,
pub amount: TokenAmount,
}
pub struct AmountCalculator<P> {
provider: P,
config: SemioscanConfig,
}
impl<P: Provider> AmountCalculator<P> {
pub fn new(provider: P, config: SemioscanConfig) -> Self {
Self { provider, config }
}
pub async fn calculate_transfer_amount_between_blocks(
&self,
chain: NamedChain,
from: Address,
to: Address,
token: Address,
from_block: BlockNumber,
to_block: BlockNumber,
) -> Result<AmountResult, EventProcessingError> {
let mut result = AmountResult {
chain,
to,
token,
amount: TokenAmount::ZERO,
};
let scanner = EventScanner::new(&self.provider, self.config.clone());
let filter = TransferFilterBuilder::new()
.with_token(token)
.with_sender(from)
.with_recipient(to)
.build();
let logs = scanner.scan(chain, filter, from_block, to_block).await?;
for log in logs {
match Transfer::decode_log(&log.into()) {
Ok(event) => {
info!(
chain = ?chain,
to = ?to,
token = ?token,
amount = ?event.value,
current_total_amount = ?result.amount,
"Adding transfer amount to result"
);
result.amount = result.amount + TokenAmount::from(event.value);
}
Err(e) => {
warn!(error = ?e, "Failed to decode Transfer log");
}
}
}
info!(
chain = ?chain,
to = ?to,
token = ?token,
total_amount = ?result.amount,
"Finished amount calculation"
);
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SemioscanConfigBuilder;
use alloy_primitives::address;
use std::time::Duration;
#[test]
fn test_amount_result_initialization() {
let chain = NamedChain::Arbitrum;
let to = address!("1111111111111111111111111111111111111111");
let token = address!("2222222222222222222222222222222222222222");
let result = AmountResult {
chain,
to,
token,
amount: TokenAmount::ZERO,
};
assert_eq!(result.chain, chain);
assert_eq!(result.to, to);
assert_eq!(result.token, token);
assert_eq!(result.amount, TokenAmount::ZERO);
}
#[test]
fn test_rate_limit_applied_for_sonic() {
let config = SemioscanConfig::default();
let sonic_delay = config.get_rate_limit_delay(NamedChain::Sonic);
assert_eq!(sonic_delay, Some(Duration::from_millis(250)));
}
#[test]
fn test_rate_limit_applied_for_base() {
let config = SemioscanConfig::default();
let base_delay = config.get_rate_limit_delay(NamedChain::Base);
assert_eq!(base_delay, Some(Duration::from_millis(250)));
}
#[test]
fn test_no_rate_limit_for_arbitrum_by_default() {
let config = SemioscanConfig::default();
let arb_delay = config.get_rate_limit_delay(NamedChain::Arbitrum);
assert_eq!(arb_delay, None);
}
#[test]
fn test_custom_rate_limit_overrides_default() {
let config = SemioscanConfigBuilder::with_defaults()
.chain_rate_limit(NamedChain::Arbitrum, Duration::from_millis(100))
.build();
let arb_delay = config.get_rate_limit_delay(NamedChain::Arbitrum);
assert_eq!(arb_delay, Some(Duration::from_millis(100)));
let base_delay = config.get_rate_limit_delay(NamedChain::Base);
assert_eq!(base_delay, Some(Duration::from_millis(250)));
}
#[test]
fn test_minimal_config_has_no_delays() {
let config = SemioscanConfig::minimal();
assert_eq!(config.get_rate_limit_delay(NamedChain::Sonic), None);
assert_eq!(config.get_rate_limit_delay(NamedChain::Base), None);
assert_eq!(config.get_rate_limit_delay(NamedChain::Arbitrum), None);
}
#[test]
fn test_amount_accumulation() {
let chain = NamedChain::Mainnet;
let to = address!("1111111111111111111111111111111111111111");
let token = address!("2222222222222222222222222222222222222222");
let mut result = AmountResult {
chain,
to,
token,
amount: TokenAmount::ZERO,
};
result.amount = result.amount + TokenAmount::from(1_000_000u64); result.amount = result.amount + TokenAmount::from(2_500_000u64);
assert_eq!(result.amount, TokenAmount::from(3_500_000u64)); }
#[test]
fn test_amount_overflow_protection() {
use alloy_primitives::U256;
let chain = NamedChain::Mainnet;
let to = address!("1111111111111111111111111111111111111111");
let token = address!("2222222222222222222222222222222222222222");
let mut result = AmountResult {
chain,
to,
token,
amount: TokenAmount::from(U256::MAX - U256::from(100u64)),
};
result.amount = result.amount + TokenAmount::from(200u64);
assert_eq!(result.amount, TokenAmount::from(U256::MAX));
}
#[test]
fn test_large_token_amounts() {
let chain = NamedChain::Mainnet;
let to = address!("1111111111111111111111111111111111111111");
let token = address!("2222222222222222222222222222222222222222");
let mut result = AmountResult {
chain,
to,
token,
amount: TokenAmount::ZERO,
};
let one_eth = TokenAmount::from(1_000_000_000_000_000_000u64);
result.amount = result.amount + one_eth;
result.amount = result.amount + one_eth;
assert_eq!(
result.amount,
TokenAmount::from(2_000_000_000_000_000_000u64)
); }
}