Skip to main content

cast/cmd/vaddr/
watch.rs

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    // Transfer(address indexed from, address indexed to, uint256 value): topic2 is the recipient.
34    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    // Historical logs from the requested start block, then poll for new ones.
48    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}