1use crate::{cmd::erc20::IERC20, tempo::tempo_provider};
2use alloy_primitives::{Address, U256};
3use alloy_provider::Provider;
4use alloy_rpc_types::{BlockNumberOrTag, Filter, Log};
5use alloy_sol_types::SolEvent;
6use eyre::Result;
7use foundry_cli::opts::RpcOpts;
8use foundry_common::shell;
9use serde_json::json;
10use std::time::Duration;
11use tempo_primitives::TempoAddressExt;
12
13fn historical_filter(filter: &Filter, anchor: u64) -> Filter {
14 filter.clone().from_block(filter.get_from_block().unwrap_or(anchor)).to_block(anchor)
15}
16
17fn poll_filter(filter: &Filter, last_block: u64, current: u64) -> Option<Filter> {
18 (current > last_block).then(|| filter.clone().from_block(last_block + 1).to_block(current))
19}
20
21pub(super) async fn run(
22 addr: Address,
23 token: Option<Address>,
24 from_block: Option<u64>,
25 rpc: RpcOpts,
26) -> Result<()> {
27 if !addr.is_virtual() {
28 eyre::bail!("{addr} is not a virtual address");
29 }
30
31 let (_, provider) = tempo_provider(&rpc)?;
32
33 let start = from_block.map_or(BlockNumberOrTag::Latest, BlockNumberOrTag::Number);
35 let mut filter = Filter::new()
36 .event_signature(IERC20::Transfer::SIGNATURE_HASH)
37 .topic2(addr.into_word())
38 .from_block(start);
39 if let Some(token) = token {
40 filter = filter.address(token);
41 }
42
43 if !shell::is_json() {
44 sh_status!("Watching transfers to {addr}... (Ctrl-C to stop)")?;
45 }
46
47 let mut last_block = provider.get_block_number().await?;
49 for log in provider.get_logs(&historical_filter(&filter, last_block)).await? {
50 print_transfer_log(&log)?;
51 }
52 loop {
53 tokio::time::sleep(Duration::from_secs(2)).await;
54 let current = provider.get_block_number().await?;
55 if let Some(filter) = poll_filter(&filter, last_block, current) {
56 for log in provider.get_logs(&filter).await? {
57 print_transfer_log(&log)?;
58 }
59 last_block = current;
60 }
61 }
62}
63
64fn print_transfer_log(log: &Log) -> Result<()> {
65 let block = log.block_number.unwrap_or(0);
66 let tx = log.transaction_hash.unwrap_or_default();
67 let token = log.address();
68 let from = log.topics().get(1).map(|t| Address::from_word(*t).to_string()).unwrap_or_default();
69 let data = &log.data().data;
70 let amount = if data.len() >= 32 { U256::from_be_slice(&data[..32]) } else { U256::ZERO };
71
72 if shell::is_json() {
73 let payload = json!({
74 "block": block,
75 "tx": format!("{tx}"),
76 "token": format!("{token}"),
77 "from": from,
78 "amount": amount.to_string(),
79 });
80 sh_println!("{payload}")
81 } else {
82 sh_println!("block={block} tx={tx} token={token} from={from} amount={amount}")
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn history_and_poll_ranges_are_contiguous() {
92 for start in [BlockNumberOrTag::Number(0), BlockNumberOrTag::Latest] {
93 let filter = Filter::new().from_block(start);
94 let historical = historical_filter(&filter, 100);
95 let poll = poll_filter(&filter, 100, 106).unwrap();
96
97 assert_eq!(historical.get_from_block(), start.as_number().or(Some(100)));
98 assert_eq!(historical.get_to_block(), Some(100));
99 assert_eq!(poll.get_from_block(), Some(101));
100 assert_eq!(poll.get_to_block(), Some(106));
101 }
102
103 assert!(poll_filter(&Filter::new(), 100, 100).is_none());
104 }
105}