Skip to main content

onyx_sdk/
scanner.rs

1//! Scanner for detecting incoming stealth payments
2//!
3//! The scanner reads announcements from on-chain and checks if any
4//! of them correspond to payments for a given meta-address.
5
6use crate::address::check_stealth_address;
7use crate::error::Result;
8use crate::keys::StealthMetaAddress;
9use crate::spend::StealthKeypair;
10use serde::{Deserialize, Serialize};
11use solana_client::rpc_client::RpcClient;
12use solana_sdk::pubkey::Pubkey;
13
14/// A detected stealth payment
15#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct DetectedPayment {
17    /// The stealth address where funds are held
18    pub stealth_address: Pubkey,
19    /// The ephemeral public key (needed to derive spend key)
20    pub ephemeral_pubkey: [u8; 32],
21    /// Amount in lamports (if known)
22    pub amount: Option<u64>,
23    /// Timestamp of the announcement (if known)
24    pub timestamp: Option<i64>,
25    /// The announcement account (for reference)
26    pub announcement_account: Option<Pubkey>,
27}
28
29/// An announcement read from on-chain
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct Announcement {
32    /// The ephemeral public key
33    pub ephemeral_pubkey: [u8; 32],
34    /// The stealth address
35    pub stealth_address: Pubkey,
36    /// Timestamp when announced
37    pub timestamp: i64,
38}
39
40/// Scanner configuration
41#[derive(Clone, Debug)]
42pub struct ScannerConfig {
43    /// The program ID for the stealth announcements
44    pub program_id: Pubkey,
45    /// Only scan announcements after this timestamp (optional)
46    pub after_timestamp: Option<i64>,
47    /// Maximum number of announcements to scan
48    pub max_announcements: usize,
49}
50
51impl Default for ScannerConfig {
52    fn default() -> Self {
53        Self {
54            program_id: Pubkey::default(), // Should be set to actual program ID
55            after_timestamp: None,
56            max_announcements: 1000,
57        }
58    }
59}
60
61/// Scanner for detecting stealth payments
62pub struct Scanner {
63    /// The meta-address to scan for
64    meta: StealthMetaAddress,
65    /// Configuration
66    config: ScannerConfig,
67}
68
69impl Scanner {
70    /// Create a new scanner for a meta-address
71    pub fn new(meta: &StealthMetaAddress) -> Self {
72        Self {
73            meta: meta.clone(),
74            config: ScannerConfig::default(),
75        }
76    }
77
78    /// Create a scanner with custom configuration
79    pub fn with_config(meta: &StealthMetaAddress, config: ScannerConfig) -> Self {
80        Self {
81            meta: meta.clone(),
82            config,
83        }
84    }
85
86    /// Set the program ID
87    pub fn program_id(mut self, program_id: Pubkey) -> Self {
88        self.config.program_id = program_id;
89        self
90    }
91
92    /// Only scan announcements after this timestamp
93    pub fn after_timestamp(mut self, timestamp: i64) -> Self {
94        self.config.after_timestamp = Some(timestamp);
95        self
96    }
97
98    /// Scan a list of announcements for payments to this meta-address
99    pub fn scan_announcements_list(
100        &self,
101        announcements: &[Announcement],
102    ) -> Result<Vec<DetectedPayment>> {
103        let mut detected = Vec::new();
104
105        for announcement in announcements {
106            // Filter by timestamp if configured
107            if let Some(after) = self.config.after_timestamp {
108                if announcement.timestamp < after {
109                    continue;
110                }
111            }
112
113            // Check if this announcement is for us
114            let is_ours = check_stealth_address(
115                self.meta.viewing_key(),
116                self.meta.spending_pubkey(),
117                &announcement.ephemeral_pubkey,
118                &announcement.stealth_address,
119            )?;
120
121            if is_ours {
122                detected.push(DetectedPayment {
123                    stealth_address: announcement.stealth_address,
124                    ephemeral_pubkey: announcement.ephemeral_pubkey,
125                    amount: None, // Will be fetched separately
126                    timestamp: Some(announcement.timestamp),
127                    announcement_account: None,
128                });
129            }
130        }
131
132        Ok(detected)
133    }
134
135    /// Scan on-chain for payments (async version)
136    ///
137    /// This fetches announcements from the on-chain program and checks
138    /// which ones belong to this meta-address.
139    pub async fn scan(&self, rpc_url: &str) -> Result<Vec<DetectedPayment>> {
140        let client = RpcClient::new(rpc_url.to_string());
141
142        // Fetch announcements from on-chain
143        // This is a simplified version - actual implementation would
144        // use getProgramAccounts or similar
145        let announcements = self.fetch_announcements(&client)?;
146
147        // Scan for our payments
148        let mut detected = self.scan_announcements_list(&announcements)?;
149
150        // Fetch balances for detected payments
151        for payment in &mut detected {
152            if let Ok(balance) = client.get_balance(&payment.stealth_address) {
153                payment.amount = Some(balance);
154            }
155        }
156
157        Ok(detected)
158    }
159
160    /// Fetch announcements from on-chain
161    fn fetch_announcements(&self, _client: &RpcClient) -> Result<Vec<Announcement>> {
162        // TODO: Implement actual fetching from the program
163        // For now, return empty list
164        // In a real implementation, this would:
165        // 1. Use getProgramAccounts to fetch all announcement accounts
166        // 2. Deserialize them into Announcement structs
167        // 3. Return the list
168
169        Ok(Vec::new())
170    }
171
172    /// Derive the spend keypair for a detected payment
173    pub fn derive_spend_keypair(&self, payment: &DetectedPayment) -> Result<StealthKeypair> {
174        StealthKeypair::derive(&self.meta, &payment.ephemeral_pubkey)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::address::StealthPayment;
182
183    #[test]
184    fn test_scan_detects_payment() {
185        let meta = StealthMetaAddress::generate();
186        let public_meta = meta.public_meta_address();
187
188        // Create a payment
189        let payment = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
190
191        // Create an announcement from the payment
192        let announcement = Announcement {
193            ephemeral_pubkey: payment.ephemeral_pubkey,
194            stealth_address: payment.stealth_address,
195            timestamp: 12345,
196        };
197
198        // Scanner should detect this
199        let scanner = Scanner::new(&meta);
200        let detected = scanner.scan_announcements_list(&[announcement]).unwrap();
201
202        assert_eq!(detected.len(), 1);
203        assert_eq!(detected[0].stealth_address, payment.stealth_address);
204    }
205
206    #[test]
207    fn test_scan_ignores_other_payments() {
208        let alice = StealthMetaAddress::generate();
209        let bob = StealthMetaAddress::generate();
210
211        // Create a payment to Alice
212        let payment = StealthPayment::create(&alice.public_meta_address(), 1_000_000_000).unwrap();
213
214        let announcement = Announcement {
215            ephemeral_pubkey: payment.ephemeral_pubkey,
216            stealth_address: payment.stealth_address,
217            timestamp: 12345,
218        };
219
220        // Bob's scanner should NOT detect this
221        let scanner = Scanner::new(&bob);
222        let detected = scanner.scan_announcements_list(&[announcement]).unwrap();
223
224        assert_eq!(detected.len(), 0, "Bob should not detect Alice's payment");
225    }
226
227    #[test]
228    fn test_scan_multiple_payments() {
229        let meta = StealthMetaAddress::generate();
230        let public_meta = meta.public_meta_address();
231
232        // Create multiple payments
233        let payments: Vec<_> = (0..5)
234            .map(|i| StealthPayment::create(&public_meta, (i + 1) * 1_000_000_000).unwrap())
235            .collect();
236
237        let announcements: Vec<_> = payments
238            .iter()
239            .enumerate()
240            .map(|(i, p)| Announcement {
241                ephemeral_pubkey: p.ephemeral_pubkey,
242                stealth_address: p.stealth_address,
243                timestamp: i as i64,
244            })
245            .collect();
246
247        let scanner = Scanner::new(&meta);
248        let detected = scanner.scan_announcements_list(&announcements).unwrap();
249
250        assert_eq!(detected.len(), 5);
251    }
252
253    #[test]
254    fn test_timestamp_filtering() {
255        let meta = StealthMetaAddress::generate();
256        let public_meta = meta.public_meta_address();
257
258        let payments: Vec<_> = (0..5)
259            .map(|i| StealthPayment::create(&public_meta, (i + 1) * 1_000_000_000).unwrap())
260            .collect();
261
262        let announcements: Vec<_> = payments
263            .iter()
264            .enumerate()
265            .map(|(i, p)| Announcement {
266                ephemeral_pubkey: p.ephemeral_pubkey,
267                stealth_address: p.stealth_address,
268                timestamp: (i * 100) as i64, // 0, 100, 200, 300, 400
269            })
270            .collect();
271
272        // Only scan after timestamp 200
273        let scanner = Scanner::new(&meta).after_timestamp(200);
274        let detected = scanner.scan_announcements_list(&announcements).unwrap();
275
276        assert_eq!(detected.len(), 3); // 200, 300, 400
277    }
278}