Skip to main content

cast/cmd/vaddr/
watch.rs

1use alloy_primitives::{Address, B256, keccak256};
2use alloy_provider::Provider;
3use alloy_rpc_types::{BlockNumberOrTag, Filter};
4use eyre::Result;
5use foundry_cli::{opts::RpcOpts, utils::LoadConfig};
6use foundry_common::{provider::ProviderBuilder, shell};
7use serde_json::json;
8use std::sync::LazyLock;
9use tempo_alloy::TempoNetwork;
10use tempo_primitives::TempoAddressExt;
11
12static TRANSFER_TOPIC: LazyLock<B256> =
13    LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)"));
14
15pub(super) async fn run(
16    addr: Address,
17    token: Option<Address>,
18    from_block: Option<u64>,
19    rpc: RpcOpts,
20) -> Result<()> {
21    if !addr.is_virtual() {
22        eyre::bail!("{addr} is not a virtual address");
23    }
24
25    let config = rpc.load_config()?;
26    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
27
28    // Transfer(address indexed from, address indexed to, uint256 value)
29    // topic[0] = event sig, topic[1] = from, topic[2] = to
30    let to_topic: B256 = {
31        let mut buf = [0u8; 32];
32        buf[12..].copy_from_slice(addr.as_slice());
33        buf.into()
34    };
35
36    let start = from_block.map(BlockNumberOrTag::Number).unwrap_or(BlockNumberOrTag::Latest);
37
38    let mut filter =
39        Filter::new().event_signature(*TRANSFER_TOPIC).topic2(to_topic).from_block(start);
40
41    if let Some(tok) = token {
42        filter = filter.address(tok);
43    }
44
45    if !shell::is_json() {
46        sh_println!("Watching transfers to {addr}... (Ctrl-C to stop)")?;
47    }
48
49    // Fetch logs from the requested start block (historical when from_block is set)
50    let logs = provider.get_logs(&filter).await?;
51    for log in &logs {
52        print_transfer_log(log)?;
53    }
54
55    // Poll for new logs
56    let mut last_block = provider.get_block_number().await?;
57    loop {
58        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
59        let current = provider.get_block_number().await?;
60        if current > last_block {
61            let poll_filter = filter.clone().from_block(last_block + 1).to_block(current);
62            let new_logs = provider.get_logs(&poll_filter).await?;
63            for log in &new_logs {
64                print_transfer_log(log)?;
65            }
66            last_block = current;
67        }
68    }
69}
70
71fn print_transfer_log(log: &alloy_rpc_types::Log) -> Result<()> {
72    let block = log.block_number.unwrap_or(0);
73    let tx = log.transaction_hash.unwrap_or_default();
74    let token = log.address();
75
76    // Decode topics: topic[1]=from, topic[2]=to
77    let from = log.topics().get(1).map(|t| {
78        let mut addr = [0u8; 20];
79        addr.copy_from_slice(&t[12..]);
80        Address::from(addr)
81    });
82
83    // Decode amount from data
84    let amount = if log.data().data.len() >= 32 {
85        alloy_primitives::U256::from_be_slice(&log.data().data[..32])
86    } else {
87        alloy_primitives::U256::ZERO
88    };
89
90    if shell::is_json() {
91        sh_println!(
92            "{}",
93            serde_json::to_string(&json!({
94                "block": block,
95                "tx": format!("{tx}"),
96                "token": format!("{token}"),
97                "from": from.map(|a| format!("{a}")).unwrap_or_default(),
98                "amount": amount.to_string(),
99            }))?
100        )?;
101    } else {
102        sh_println!(
103            "block={block} tx={tx} token={token} from={} amount={amount}",
104            from.map(|a| a.to_string()).unwrap_or_default(),
105        )?;
106    }
107    Ok(())
108}