use crate::address::check_stealth_address;
use crate::error::Result;
use crate::keys::StealthMetaAddress;
use crate::spend::StealthKeypair;
use serde::{Deserialize, Serialize};
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DetectedPayment {
pub stealth_address: Pubkey,
pub ephemeral_pubkey: [u8; 32],
pub amount: Option<u64>,
pub timestamp: Option<i64>,
pub announcement_account: Option<Pubkey>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Announcement {
pub ephemeral_pubkey: [u8; 32],
pub stealth_address: Pubkey,
pub timestamp: i64,
}
#[derive(Clone, Debug)]
pub struct ScannerConfig {
pub program_id: Pubkey,
pub after_timestamp: Option<i64>,
pub max_announcements: usize,
}
impl Default for ScannerConfig {
fn default() -> Self {
Self {
program_id: Pubkey::default(), after_timestamp: None,
max_announcements: 1000,
}
}
}
pub struct Scanner {
meta: StealthMetaAddress,
config: ScannerConfig,
}
impl Scanner {
pub fn new(meta: &StealthMetaAddress) -> Self {
Self {
meta: meta.clone(),
config: ScannerConfig::default(),
}
}
pub fn with_config(meta: &StealthMetaAddress, config: ScannerConfig) -> Self {
Self {
meta: meta.clone(),
config,
}
}
pub fn program_id(mut self, program_id: Pubkey) -> Self {
self.config.program_id = program_id;
self
}
pub fn after_timestamp(mut self, timestamp: i64) -> Self {
self.config.after_timestamp = Some(timestamp);
self
}
pub fn scan_announcements_list(
&self,
announcements: &[Announcement],
) -> Result<Vec<DetectedPayment>> {
let mut detected = Vec::new();
for announcement in announcements {
if let Some(after) = self.config.after_timestamp {
if announcement.timestamp < after {
continue;
}
}
let is_ours = check_stealth_address(
self.meta.viewing_key(),
self.meta.spending_pubkey(),
&announcement.ephemeral_pubkey,
&announcement.stealth_address,
)?;
if is_ours {
detected.push(DetectedPayment {
stealth_address: announcement.stealth_address,
ephemeral_pubkey: announcement.ephemeral_pubkey,
amount: None, timestamp: Some(announcement.timestamp),
announcement_account: None,
});
}
}
Ok(detected)
}
pub async fn scan(&self, rpc_url: &str) -> Result<Vec<DetectedPayment>> {
let client = RpcClient::new(rpc_url.to_string());
let announcements = self.fetch_announcements(&client)?;
let mut detected = self.scan_announcements_list(&announcements)?;
for payment in &mut detected {
if let Ok(balance) = client.get_balance(&payment.stealth_address) {
payment.amount = Some(balance);
}
}
Ok(detected)
}
fn fetch_announcements(&self, _client: &RpcClient) -> Result<Vec<Announcement>> {
Ok(Vec::new())
}
pub fn derive_spend_keypair(&self, payment: &DetectedPayment) -> Result<StealthKeypair> {
StealthKeypair::derive(&self.meta, &payment.ephemeral_pubkey)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::address::StealthPayment;
#[test]
fn test_scan_detects_payment() {
let meta = StealthMetaAddress::generate();
let public_meta = meta.public_meta_address();
let payment = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
let announcement = Announcement {
ephemeral_pubkey: payment.ephemeral_pubkey,
stealth_address: payment.stealth_address,
timestamp: 12345,
};
let scanner = Scanner::new(&meta);
let detected = scanner.scan_announcements_list(&[announcement]).unwrap();
assert_eq!(detected.len(), 1);
assert_eq!(detected[0].stealth_address, payment.stealth_address);
}
#[test]
fn test_scan_ignores_other_payments() {
let alice = StealthMetaAddress::generate();
let bob = StealthMetaAddress::generate();
let payment = StealthPayment::create(&alice.public_meta_address(), 1_000_000_000).unwrap();
let announcement = Announcement {
ephemeral_pubkey: payment.ephemeral_pubkey,
stealth_address: payment.stealth_address,
timestamp: 12345,
};
let scanner = Scanner::new(&bob);
let detected = scanner.scan_announcements_list(&[announcement]).unwrap();
assert_eq!(detected.len(), 0, "Bob should not detect Alice's payment");
}
#[test]
fn test_scan_multiple_payments() {
let meta = StealthMetaAddress::generate();
let public_meta = meta.public_meta_address();
let payments: Vec<_> = (0..5)
.map(|i| StealthPayment::create(&public_meta, (i + 1) * 1_000_000_000).unwrap())
.collect();
let announcements: Vec<_> = payments
.iter()
.enumerate()
.map(|(i, p)| Announcement {
ephemeral_pubkey: p.ephemeral_pubkey,
stealth_address: p.stealth_address,
timestamp: i as i64,
})
.collect();
let scanner = Scanner::new(&meta);
let detected = scanner.scan_announcements_list(&announcements).unwrap();
assert_eq!(detected.len(), 5);
}
#[test]
fn test_timestamp_filtering() {
let meta = StealthMetaAddress::generate();
let public_meta = meta.public_meta_address();
let payments: Vec<_> = (0..5)
.map(|i| StealthPayment::create(&public_meta, (i + 1) * 1_000_000_000).unwrap())
.collect();
let announcements: Vec<_> = payments
.iter()
.enumerate()
.map(|(i, p)| Announcement {
ephemeral_pubkey: p.ephemeral_pubkey,
stealth_address: p.stealth_address,
timestamp: (i * 100) as i64, })
.collect();
let scanner = Scanner::new(&meta).after_timestamp(200);
let detected = scanner.scan_announcements_list(&announcements).unwrap();
assert_eq!(detected.len(), 3); }
}