1#![allow(clippy::type_complexity)]
17
18use crate::commands::CDN_BASE_URL;
19use snarkvm::{
20 console::network::{CanaryV0, MainnetV0, Network, TestnetV0},
21 prelude::{Ciphertext, Field, FromBytes, Plaintext, PrivateKey, Record, ViewKey, block::Block},
22};
23
24use anyhow::{Result, bail, ensure};
25use clap::Parser;
26use parking_lot::RwLock;
27use std::{
28 io::{Write, stdout},
29 str::FromStr,
30 sync::Arc,
31};
32use zeroize::Zeroize;
33
34const MAX_BLOCK_RANGE: u32 = 50;
35
36#[derive(Debug, Parser, Zeroize)]
38pub struct Scan {
39 #[clap(default_value = "0", long = "network")]
41 pub network: u16,
42
43 #[clap(short, long)]
45 private_key: Option<String>,
46
47 #[clap(short, long)]
49 view_key: Option<String>,
50
51 #[clap(long, conflicts_with = "last")]
53 start: Option<u32>,
54
55 #[clap(long, conflicts_with = "last")]
57 end: Option<u32>,
58
59 #[clap(long)]
61 last: Option<u32>,
62
63 #[clap(long)]
65 endpoint: String,
66}
67
68impl Scan {
69 pub fn parse(self) -> Result<String> {
70 match self.network {
72 MainnetV0::ID => self.scan_records::<MainnetV0>(),
73 TestnetV0::ID => self.scan_records::<TestnetV0>(),
74 CanaryV0::ID => self.scan_records::<CanaryV0>(),
75 unknown_id => bail!("Unknown network ID ({unknown_id})"),
76 }
77 }
78
79 fn scan_records<N: Network>(&self) -> Result<String> {
81 let (private_key, view_key) = self.parse_account::<N>()?;
83
84 let (start_height, end_height) = self.parse_block_range()?;
86
87 let records = Self::fetch_records::<N>(private_key, &view_key, &self.endpoint, start_height, end_height)?;
89
90 if records.is_empty() {
92 Ok("No records found".to_string())
93 } else {
94 if private_key.is_none() {
95 println!("⚠️ This list may contain records that have already been spent.\n");
96 }
97
98 Ok(serde_json::to_string_pretty(&records)?.replace("\\n", ""))
99 }
100 }
101
102 fn parse_account<N: Network>(&self) -> Result<(Option<PrivateKey<N>>, ViewKey<N>)> {
104 match (&self.private_key, &self.view_key) {
105 (Some(private_key), Some(view_key)) => {
106 let private_key = PrivateKey::<N>::from_str(private_key)?;
108 let expected_view_key = ViewKey::<N>::try_from(private_key)?;
110 let view_key = ViewKey::<N>::from_str(view_key)?;
112
113 ensure!(
114 expected_view_key == view_key,
115 "The provided private key does not correspond to the provided view key."
116 );
117
118 Ok((Some(private_key), view_key))
119 }
120 (Some(private_key), _) => {
121 let private_key = PrivateKey::<N>::from_str(private_key)?;
123 let view_key = ViewKey::<N>::try_from(private_key)?;
125
126 Ok((Some(private_key), view_key))
127 }
128 (None, Some(view_key)) => Ok((None, ViewKey::<N>::from_str(view_key)?)),
129 (None, None) => bail!("Missing private key or view key."),
130 }
131 }
132
133 fn parse_block_range(&self) -> Result<(u32, u32)> {
135 let network = match self.network {
137 MainnetV0::ID => "mainnet",
138 TestnetV0::ID => "testnet",
139 CanaryV0::ID => "canary",
140 unknown_id => bail!("Unknown network ID ({unknown_id})"),
141 };
142
143 match (self.start, self.end, self.last) {
144 (Some(start), Some(end), None) => {
145 ensure!(end > start, "The given scan range is invalid (start = {start}, end = {end})");
146
147 Ok((start, end))
148 }
149 (Some(start), None, None) => {
150 let endpoint = format!("{}/{network}/block/height/latest", self.endpoint);
152 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_string()?)?;
153
154 if start == 0 {
156 println!("⚠️ Attention - Scanning the entire chain. This may take a while...\n");
157 }
158
159 Ok((start, latest_height))
160 }
161 (None, Some(end), None) => Ok((0, end)),
162 (None, None, Some(last)) => {
163 let endpoint = format!("{}/{network}/block/height/latest", self.endpoint);
165 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_string()?)?;
166
167 Ok((latest_height.saturating_sub(last), latest_height))
168 }
169 (None, None, None) => bail!("Missing data about block range."),
170 _ => bail!("`last` flags can't be used with `start` or `end`"),
171 }
172 }
173
174 fn parse_cdn<N: Network>() -> Result<String> {
176 match N::ID {
177 MainnetV0::ID => Ok(format!("{CDN_BASE_URL}/mainnet/v0")),
178 TestnetV0::ID => Ok(format!("{CDN_BASE_URL}/testnet/v0")),
179 CanaryV0::ID => Ok(format!("{CDN_BASE_URL}/canary/v0")),
180 _ => bail!("Unknown network ID ({})", N::ID),
181 }
182 }
183
184 fn fetch_records<N: Network>(
186 private_key: Option<PrivateKey<N>>,
187 view_key: &ViewKey<N>,
188 endpoint: &str,
189 start_height: u32,
190 end_height: u32,
191 ) -> Result<Vec<Record<N, Plaintext<N>>>> {
192 if start_height > end_height {
194 bail!("Invalid block range");
195 }
196
197 let network = match N::ID {
199 MainnetV0::ID => "mainnet",
200 TestnetV0::ID => "testnet",
201 CanaryV0::ID => "canary",
202 unknown_id => bail!("Unknown network ID ({unknown_id})"),
203 };
204
205 let address_x_coordinate = view_key.to_address().to_x_coordinate();
207
208 let records = Arc::new(RwLock::new(Vec::new()));
210
211 let total_blocks = end_height.saturating_sub(start_height);
213
214 print!("\rScanning {total_blocks} blocks for records (0% complete)...");
216 stdout().flush()?;
217
218 let genesis_block: Block<N> = ureq::get(&format!("{endpoint}/{network}/block/0")).call()?.into_json()?;
220 let is_development_network = genesis_block != Block::from_bytes_le(N::genesis_bytes())?;
222
223 let mut request_start = match is_development_network {
225 true => start_height,
226 false => {
227 let cdn_endpoint = Self::parse_cdn::<N>()?;
229 Self::scan_from_cdn(
231 start_height,
232 end_height,
233 cdn_endpoint,
234 endpoint.to_string(),
235 private_key,
236 *view_key,
237 address_x_coordinate,
238 records.clone(),
239 )?;
240
241 end_height.saturating_sub(start_height % MAX_BLOCK_RANGE)
243 }
244 };
245
246 while request_start <= end_height {
248 let percentage_complete = request_start.saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
250 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
251 stdout().flush()?;
252
253 let num_blocks_to_request =
254 std::cmp::min(MAX_BLOCK_RANGE, end_height.saturating_sub(request_start).saturating_add(1));
255 let request_end = request_start.saturating_add(num_blocks_to_request);
256
257 let blocks_endpoint = format!("{endpoint}/{network}/blocks?start={request_start}&end={request_end}");
259 let blocks: Vec<Block<N>> = ureq::get(&blocks_endpoint).call()?.into_json()?;
261
262 for block in &blocks {
264 Self::scan_block(block, endpoint, private_key, view_key, &address_x_coordinate, records.clone())?;
265 }
266
267 request_start = request_start.saturating_add(num_blocks_to_request);
268 }
269
270 println!("\rScanning {total_blocks} blocks for records (100% complete)... \n");
272 stdout().flush()?;
273
274 let result = records.read().clone();
275 Ok(result)
276 }
277
278 #[allow(clippy::too_many_arguments)]
280 fn scan_from_cdn<N: Network>(
281 start_height: u32,
282 end_height: u32,
283 cdn: String,
284 endpoint: String,
285 private_key: Option<PrivateKey<N>>,
286 view_key: ViewKey<N>,
287 address_x_coordinate: Field<N>,
288 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
289 ) -> Result<()> {
290 let total_blocks = end_height.saturating_sub(start_height);
292
293 let cdn_request_start = start_height.saturating_sub(start_height % MAX_BLOCK_RANGE);
295 let cdn_request_end = end_height.saturating_sub(start_height % MAX_BLOCK_RANGE);
296
297 let rt = tokio::runtime::Runtime::new()?;
299
300 let _shutdown = Default::default();
302
303 rt.block_on(async move {
305 let _ = snarkos_node_cdn::load_blocks(
306 &cdn,
307 cdn_request_start,
308 Some(cdn_request_end),
309 _shutdown,
310 move |block| {
311 if block.height() < start_height || block.height() > end_height {
313 return Ok(());
314 }
315
316 let percentage_complete =
318 block.height().saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
319 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
320 stdout().flush()?;
321
322 Self::scan_block(
324 &block,
325 &endpoint,
326 private_key,
327 &view_key,
328 &address_x_coordinate,
329 records.clone(),
330 )?;
331
332 Ok(())
333 },
334 )
335 .await;
336 });
337
338 Ok(())
339 }
340
341 fn scan_block<N: Network>(
343 block: &Block<N>,
344 endpoint: &str,
345 private_key: Option<PrivateKey<N>>,
346 view_key: &ViewKey<N>,
347 address_x_coordinate: &Field<N>,
348 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
349 ) -> Result<()> {
350 for (commitment, ciphertext_record) in block.records() {
351 if ciphertext_record.is_owner_with_address_x_coordinate(view_key, address_x_coordinate) {
353 if let Some(record) =
355 Self::decrypt_record(private_key, view_key, endpoint, *commitment, ciphertext_record)?
356 {
357 records.write().push(record);
358 }
359 }
360 }
361
362 Ok(())
363 }
364
365 fn decrypt_record<N: Network>(
367 private_key: Option<PrivateKey<N>>,
368 view_key: &ViewKey<N>,
369 endpoint: &str,
370 commitment: Field<N>,
371 ciphertext_record: &Record<N, Ciphertext<N>>,
372 ) -> Result<Option<Record<N, Plaintext<N>>>> {
373 if let Some(private_key) = private_key {
375 let serial_number = Record::<N, Plaintext<N>>::serial_number(private_key, commitment)?;
377
378 let network = match N::ID {
380 MainnetV0::ID => "mainnet",
381 TestnetV0::ID => "testnet",
382 CanaryV0::ID => "canary",
383 unknown_id => bail!("Unknown network ID ({unknown_id})"),
384 };
385
386 let endpoint = format!("{endpoint}/{network}/find/transitionID/{serial_number}");
388
389 match ureq::get(&endpoint).call() {
391 Ok(_) => Ok(None),
393 Err(_error) => {
395 Ok(Some(ciphertext_record.decrypt(view_key)?))
400 }
401 }
402 } else {
403 Ok(Some(ciphertext_record.decrypt(view_key)?))
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use snarkvm::prelude::{MainnetV0, TestRng};
413
414 type CurrentNetwork = MainnetV0;
415
416 #[test]
417 fn test_parse_account() {
418 let rng = &mut TestRng::default();
419
420 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
422 let view_key = ViewKey::try_from(private_key).unwrap();
423
424 let unassociated_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
426 let unassociated_view_key = ViewKey::try_from(unassociated_private_key).unwrap();
427
428 let config = Scan::try_parse_from(
429 [
430 "snarkos",
431 "--private-key",
432 &format!("{private_key}"),
433 "--view-key",
434 &format!("{view_key}"),
435 "--last",
436 "10",
437 "--endpoint",
438 "",
439 ]
440 .iter(),
441 )
442 .unwrap();
443 assert!(config.parse_account::<CurrentNetwork>().is_ok());
444
445 let config = Scan::try_parse_from(
446 [
447 "snarkos",
448 "--private-key",
449 &format!("{private_key}"),
450 "--view-key",
451 &format!("{unassociated_view_key}"),
452 "--last",
453 "10",
454 "--endpoint",
455 "",
456 ]
457 .iter(),
458 )
459 .unwrap();
460 assert!(config.parse_account::<CurrentNetwork>().is_err());
461 }
462
463 #[test]
464 fn test_parse_block_range() {
465 let config =
466 Scan::try_parse_from(["snarkos", "--view-key", "", "--start", "0", "--end", "10", "--endpoint", ""].iter())
467 .unwrap();
468 assert!(config.parse_block_range().is_ok());
469
470 let config =
472 Scan::try_parse_from(["snarkos", "--view-key", "", "--start", "10", "--end", "5", "--endpoint", ""].iter())
473 .unwrap();
474 assert!(config.parse_block_range().is_err());
475
476 assert!(
478 Scan::try_parse_from(
479 ["snarkos", "--view-key", "", "--start", "0", "--last", "10", "--endpoint", ""].iter(),
480 )
481 .is_err()
482 );
483
484 assert!(
486 Scan::try_parse_from(["snarkos", "--view-key", "", "--end", "10", "--last", "10", "--endpoint", ""].iter())
487 .is_err()
488 );
489
490 assert!(
492 Scan::try_parse_from(
493 ["snarkos", "--view-key", "", "--start", "0", "--end", "01", "--last", "10", "--endpoint", ""].iter(),
494 )
495 .is_err()
496 );
497 }
498}