1use 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#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct DetectedPayment {
17 pub stealth_address: Pubkey,
19 pub ephemeral_pubkey: [u8; 32],
21 pub amount: Option<u64>,
23 pub timestamp: Option<i64>,
25 pub announcement_account: Option<Pubkey>,
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct Announcement {
32 pub ephemeral_pubkey: [u8; 32],
34 pub stealth_address: Pubkey,
36 pub timestamp: i64,
38}
39
40#[derive(Clone, Debug)]
42pub struct ScannerConfig {
43 pub program_id: Pubkey,
45 pub after_timestamp: Option<i64>,
47 pub max_announcements: usize,
49}
50
51impl Default for ScannerConfig {
52 fn default() -> Self {
53 Self {
54 program_id: Pubkey::default(), after_timestamp: None,
56 max_announcements: 1000,
57 }
58 }
59}
60
61pub struct Scanner {
63 meta: StealthMetaAddress,
65 config: ScannerConfig,
67}
68
69impl Scanner {
70 pub fn new(meta: &StealthMetaAddress) -> Self {
72 Self {
73 meta: meta.clone(),
74 config: ScannerConfig::default(),
75 }
76 }
77
78 pub fn with_config(meta: &StealthMetaAddress, config: ScannerConfig) -> Self {
80 Self {
81 meta: meta.clone(),
82 config,
83 }
84 }
85
86 pub fn program_id(mut self, program_id: Pubkey) -> Self {
88 self.config.program_id = program_id;
89 self
90 }
91
92 pub fn after_timestamp(mut self, timestamp: i64) -> Self {
94 self.config.after_timestamp = Some(timestamp);
95 self
96 }
97
98 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 if let Some(after) = self.config.after_timestamp {
108 if announcement.timestamp < after {
109 continue;
110 }
111 }
112
113 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, timestamp: Some(announcement.timestamp),
127 announcement_account: None,
128 });
129 }
130 }
131
132 Ok(detected)
133 }
134
135 pub async fn scan(&self, rpc_url: &str) -> Result<Vec<DetectedPayment>> {
140 let client = RpcClient::new(rpc_url.to_string());
141
142 let announcements = self.fetch_announcements(&client)?;
146
147 let mut detected = self.scan_announcements_list(&announcements)?;
149
150 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 fn fetch_announcements(&self, _client: &RpcClient) -> Result<Vec<Announcement>> {
162 Ok(Vec::new())
170 }
171
172 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 let payment = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();
190
191 let announcement = Announcement {
193 ephemeral_pubkey: payment.ephemeral_pubkey,
194 stealth_address: payment.stealth_address,
195 timestamp: 12345,
196 };
197
198 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 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 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 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, })
270 .collect();
271
272 let scanner = Scanner::new(&meta).after_timestamp(200);
274 let detected = scanner.scan_announcements_list(&announcements).unwrap();
275
276 assert_eq!(detected.len(), 3); }
278}