1use super::*;
18use leo_ast::NetworkName;
19
20#[cfg(not(feature = "only_testnet"))]
21use snarkvm::prelude::{CanaryV0, MainnetV0};
22use snarkvm::{
23 console::program::{Signature, ToFields, Value},
24 prelude::{Address, Network, PrivateKey, TestnetV0, ViewKey},
25};
26
27use crossterm::ExecutableCommand;
28use itertools::Itertools;
29use rand::SeedableRng;
30use rand_chacha::ChaChaRng;
31use std::{
32 io::{self, Read, Write},
33 path::PathBuf,
34 str::FromStr,
35};
36
37#[derive(Parser, Debug)]
39pub enum Account {
40 New {
42 #[clap(short = 's', long)]
44 seed: Option<u64>,
45 #[clap(short = 'w', long)]
47 write: bool,
48 #[clap(long)]
50 discreet: bool,
51 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
52 network: NetworkName,
53 #[clap(
54 short = 'e',
55 long,
56 help = "Endpoint to record in the generated `.env` file. Only used with `--write`.",
57 default_value = "https://api.explorer.provable.com/v1"
58 )]
59 endpoint: String,
60 },
61 Import {
63 private_key: Option<String>,
65 #[clap(short = 'w', long)]
67 write: bool,
68 #[clap(long)]
70 discreet: bool,
71 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
72 network: NetworkName,
73 #[clap(
74 short = 'e',
75 long,
76 help = "Endpoint to record in the generated `.env` file. Only used with `--write`.",
77 default_value = "https://api.explorer.provable.com/v1"
78 )]
79 endpoint: String,
80 },
81 Sign {
83 #[clap(long = "private-key")]
85 private_key: Option<String>,
86 #[clap(long = "private-key-file")]
88 private_key_file: Option<String>,
89 #[clap(short = 'm', long)]
91 message: String,
92 #[clap(short = 'r', long)]
94 raw: bool,
95 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
96 network: NetworkName,
97 },
98 Verify {
100 #[clap(short = 'a', long)]
102 address: String,
103 #[clap(short = 's', long)]
105 signature: String,
106 #[clap(short = 'm', long)]
108 message: String,
109 #[clap(short = 'r', long)]
111 raw: bool,
112 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
113 network: NetworkName,
114 },
115 Decrypt {
117 #[clap(short = 'k', help = "Private key or view key to use for decryption")]
119 key: Option<String>,
120 #[clap(short = 'f', help = "Path to a file containing the private key or view key")]
122 key_file: Option<String>,
123 #[clap(short = 'c', long)]
125 ciphertext: String,
126 #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")]
127 network: NetworkName,
128 },
129}
130
131impl Command for Account {
132 type Input = ();
133 type Output = ();
134
135 fn prelude(&self, _: Context) -> Result<Self::Input>
136 where
137 Self: Sized,
138 {
139 Ok(())
140 }
141
142 fn apply(self, ctx: Context, _: Self::Input) -> Result<Self::Output>
143 where
144 Self: Sized,
145 {
146 match self {
147 Account::New { seed, write, discreet, network, endpoint } => match network {
148 NetworkName::TestnetV0 => {
149 generate_new_account::<TestnetV0>(network, seed, write, discreet, &ctx, endpoint)
150 }
151 NetworkName::MainnetV0 => {
152 #[cfg(feature = "only_testnet")]
153 panic!("Mainnet chosen with only_testnet feature");
154 #[cfg(not(feature = "only_testnet"))]
155 generate_new_account::<MainnetV0>(network, seed, write, discreet, &ctx, endpoint)
156 }
157 NetworkName::CanaryV0 => {
158 #[cfg(feature = "only_testnet")]
159 panic!("Canary chosen with only_testnet feature");
160 #[cfg(not(feature = "only_testnet"))]
161 generate_new_account::<CanaryV0>(network, seed, write, discreet, &ctx, endpoint)
162 }
163 }?,
164 Account::Import { private_key, write, discreet, network, endpoint } => match network {
165 NetworkName::TestnetV0 => {
166 import_account::<TestnetV0>(network, private_key, write, discreet, &ctx, endpoint)
167 }
168 NetworkName::MainnetV0 => {
169 #[cfg(feature = "only_testnet")]
170 panic!("Mainnet chosen with only_testnet feature");
171 #[cfg(not(feature = "only_testnet"))]
172 import_account::<MainnetV0>(network, private_key, write, discreet, &ctx, endpoint)
173 }
174 NetworkName::CanaryV0 => {
175 #[cfg(feature = "only_testnet")]
176 panic!("Canary chosen with only_testnet feature");
177 #[cfg(not(feature = "only_testnet"))]
178 import_account::<CanaryV0>(network, private_key, write, discreet, &ctx, endpoint)
179 }
180 }?,
181 Self::Sign { message, raw, private_key, private_key_file, network } => {
182 let result = match network {
183 NetworkName::TestnetV0 => sign_message::<TestnetV0>(message, raw, private_key, private_key_file),
184 NetworkName::MainnetV0 => {
185 #[cfg(feature = "only_testnet")]
186 panic!("Mainnet chosen with only_testnet feature");
187 #[cfg(not(feature = "only_testnet"))]
188 sign_message::<MainnetV0>(message, raw, private_key, private_key_file)
189 }
190 NetworkName::CanaryV0 => {
191 #[cfg(feature = "only_testnet")]
192 panic!("Canary chosen with only_testnet feature");
193 #[cfg(not(feature = "only_testnet"))]
194 sign_message::<CanaryV0>(message, raw, private_key, private_key_file)
195 }
196 }?;
197 println!("{result}")
198 }
199 Self::Verify { address, signature, message, raw, network } => {
200 let result = match network {
201 NetworkName::TestnetV0 => verify_message::<TestnetV0>(address, signature, message, raw),
202 NetworkName::MainnetV0 => {
203 #[cfg(feature = "only_testnet")]
204 panic!("Mainnet chosen with only_testnet feature");
205 #[cfg(not(feature = "only_testnet"))]
206 verify_message::<MainnetV0>(address, signature, message, raw)
207 }
208 NetworkName::CanaryV0 => {
209 #[cfg(feature = "only_testnet")]
210 panic!("Canary chosen with only_testnet feature");
211 #[cfg(not(feature = "only_testnet"))]
212 verify_message::<CanaryV0>(address, signature, message, raw)
213 }
214 }?;
215 println!("{result}")
216 }
217 Self::Decrypt { key, key_file, ciphertext, network } => {
218 let result = match network {
219 NetworkName::TestnetV0 => decrypt_ciphertext::<TestnetV0>(key, key_file, &ciphertext),
220 NetworkName::MainnetV0 => {
221 #[cfg(feature = "only_testnet")]
222 panic!("Mainnet chosen with only_testnet feature");
223 #[cfg(not(feature = "only_testnet"))]
224 decrypt_ciphertext::<MainnetV0>(key, key_file, &ciphertext)
225 }
226 NetworkName::CanaryV0 => {
227 #[cfg(feature = "only_testnet")]
228 panic!("Canary chosen with only_testnet feature");
229 #[cfg(not(feature = "only_testnet"))]
230 decrypt_ciphertext::<CanaryV0>(key, key_file, &ciphertext)
231 }
232 }?;
233 println!("{result}")
234 }
235 }
236 Ok(())
237 }
238}
239
240fn generate_new_account<N: Network>(
244 network: NetworkName,
245 seed: Option<u64>,
246 write: bool,
247 discreet: bool,
248 ctx: &Context,
249 endpoint: String,
250) -> Result<()> {
251 let private_key = match seed {
253 Some(seed) => PrivateKey::<N>::new(&mut ChaChaRng::seed_from_u64(seed)),
255 None => PrivateKey::new(&mut ChaChaRng::from_rng(&mut rand::rng())),
257 }
258 .map_err(crate::errors::failed_to_parse_seed)?;
259
260 print_keys(private_key, discreet)?;
262
263 if write {
265 write_to_env_file(network, private_key, ctx, endpoint)?;
266 }
267 Ok(())
268}
269
270fn import_account<N: Network>(
272 network: NetworkName,
273 private_key: Option<String>,
274 write: bool,
275 discreet: bool,
276 ctx: &Context,
277 endpoint: String,
278) -> Result<()> {
279 let priv_key = match discreet {
280 true => {
281 let private_key_input = rpassword::prompt_password("Please enter your private key: ").unwrap();
282 FromStr::from_str(&private_key_input).map_err(crate::errors::failed_to_parse_private_key)?
283 }
284 false => match private_key {
285 Some(private_key) => FromStr::from_str(&private_key).map_err(crate::errors::failed_to_parse_private_key)?,
286 None => {
287 return Err(crate::errors::failed_to_execute_account(
288 "Missing private key argument. Provide a private key or use the '--discreet' flag to enter it securely.",
289 )
290 .into());
291 }
292 },
293 };
294
295 print_keys::<N>(priv_key, discreet)?;
297
298 if write {
300 write_to_env_file::<N>(network, priv_key, ctx, endpoint)?;
301 }
302
303 Ok(())
304}
305
306fn print_keys<N: Network>(private_key: PrivateKey<N>, discreet: bool) -> Result<()> {
308 let view_key = ViewKey::try_from(&private_key)?;
309 let address = Address::<N>::try_from(&view_key)?;
310
311 if !discreet {
312 println!(
313 "\n {:>12} {private_key}\n {:>12} {view_key}\n {:>12} {address}\n",
314 "Private Key".cyan().bold(),
315 "View Key".cyan().bold(),
316 "Address".cyan().bold(),
317 );
318 return Ok(());
319 }
320 display_string_discreetly(
321 &private_key.to_string(),
322 "### Do not share or lose this private key! Press any key to complete. ###",
323 )?;
324 println!("\n {:>12} {view_key}\n {:>12} {address}\n", "View Key".cyan().bold(), "Address".cyan().bold(),);
325
326 Ok(())
327}
328
329pub(crate) fn sign_message<N: Network>(
331 message: String,
332 raw: bool,
333 private_key: Option<String>,
334 private_key_file: Option<String>,
335) -> Result<String> {
336 let private_key_string = get_key_string(private_key, private_key_file, &["PRIVATE_KEY"])?;
338
339 let private_key_string = private_key_string.trim();
341 let private_key = PrivateKey::<N>::from_str(private_key_string)
342 .map_err(|_| crate::errors::cli_invalid_input("Failed to parse a valid private key"))?;
343
344 let mut rng = ChaChaRng::from_rng(&mut rand::rng());
346
347 let signature = if raw {
349 private_key.sign_bytes(message.as_bytes(), &mut rng)
350 } else {
351 let fields = Value::<N>::from_str(&message)?
352 .to_fields()
353 .map_err(|_| crate::errors::cli_invalid_input("Failed to parse a valid Aleo value"))?;
354 private_key.sign(&fields, &mut rng)
355 }
356 .map_err(|_| crate::errors::cli_runtime_error("Failed to sign the message"))?
357 .to_string();
358 Ok(signature)
360}
361
362pub(crate) fn verify_message<N: Network>(
364 address: String,
365 signature: String,
366 message: String,
367 raw: bool,
368) -> Result<String> {
369 let address = Address::<N>::from_str(&address)?;
371
372 let signature = Signature::<N>::from_str(&signature)
373 .map_err(|e| crate::errors::cli_invalid_input(format!("Failed to parse a valid signature: {e}")))?;
374
375 let verified = if raw {
377 signature.verify_bytes(&address, message.as_bytes())
378 } else {
379 let fields = Value::<N>::from_str(&message)?
380 .to_fields()
381 .map_err(|_| crate::errors::cli_invalid_input("Failed to parse a valid Aleo value"))?;
382 signature.verify(&address, &fields)
383 };
384
385 match verified {
387 true => Ok("✅ The signature is valid".to_string()),
388 false => Err(crate::errors::cli_runtime_error("❌ The signature is invalid"))?,
389 }
390}
391
392pub(crate) fn decrypt_ciphertext<N: Network>(
394 key: Option<String>,
395 key_file: Option<String>,
396 ciphertext: &str,
397) -> Result<String> {
398 let key_string = get_key_string(key, key_file, &["PRIVATE_KEY", "VIEW_KEY"])?;
400
401 let key_string = key_string.trim();
403 let view_key = if key_string.starts_with("APrivateKey1") {
404 let private_key = PrivateKey::<N>::from_str(key_string)
406 .map_err(|_| crate::errors::cli_invalid_input("Failed to parse a valid private key"))?;
407 ViewKey::<N>::try_from(&private_key)
409 .map_err(|_| crate::errors::cli_invalid_input("Failed to convert private key to view key"))?
410 } else if key_string.starts_with("AViewKey1") {
411 ViewKey::<N>::from_str(key_string)
413 .map_err(|_| crate::errors::cli_invalid_input("Failed to parse a valid view key"))?
414 } else {
415 Err(crate::errors::cli_invalid_input("Invalid key format. Expected a private or view key."))?
417 };
418
419 let record_ciphertext = Record::<N, Ciphertext<N>>::from_str(ciphertext)
421 .map_err(|_| crate::errors::cli_invalid_input("Failed to parse a valid record ciphertext"))?;
422
423 let decrypted_value = record_ciphertext
425 .decrypt(&view_key)
426 .map_err(|_| crate::errors::cli_runtime_error("Failed to decrypt the record ciphertext"))?;
427
428 Ok(decrypted_value.to_string())
430}
431
432fn get_key_string(key: Option<String>, key_file: Option<String>, env_vars: &[&'static str]) -> Result<String> {
434 match (key, key_file) {
435 (Some(key), None) => Ok(key),
436 (None, Some(key_file)) => {
437 let path = key_file
438 .parse::<PathBuf>()
439 .map_err(|e| crate::errors::cli_invalid_input(format!("Invalid path - {e}")))?;
440 std::fs::read_to_string(path).map_err(|e| crate::errors::failed_to_read_file(e).into())
441 }
442 (None, None) => {
443 env_vars.iter().find_map(|&var| std::env::var(var).ok()).ok_or_else(|| {
445 crate::errors::cli_invalid_input(format!(
446 "Missing the '--key', '--key-file', or the following environment variables: '{}'",
447 env_vars.iter().format(",")
448 ))
449 .into()
450 })
451 }
452 (Some(_), Some(_)) => {
453 Err(crate::errors::cli_invalid_input("Cannot specify both the '--key' and '--key-file' flags").into())
454 }
455 }
456}
457
458fn write_to_env_file<N: Network>(
460 network: NetworkName,
461 private_key: PrivateKey<N>,
462 ctx: &Context,
463 endpoint: String,
464) -> Result<()> {
465 let program_dir = ctx.dir()?;
466 let env_path = program_dir.join(".env");
467 std::fs::write(env_path, format!("NETWORK={network}\nPRIVATE_KEY={private_key}\nENDPOINT={endpoint}\n"))
468 .map_err(crate::errors::io_error_env_file)?;
469 tracing::info!("✅ Private Key written to {}", program_dir.join(".env").display());
470 Ok(())
471}
472
473fn display_string_discreetly(discreet_string: &str, continue_message: &str) -> Result<()> {
475 use crossterm::{
476 style::Print,
477 terminal::{EnterAlternateScreen, LeaveAlternateScreen},
478 };
479 let mut stdout = io::stdout();
480 stdout.execute(EnterAlternateScreen).unwrap();
481 stdout.execute(Print(format!("{discreet_string}\n{continue_message}"))).unwrap();
483 stdout.flush().unwrap();
484 wait_for_keypress();
485 stdout.execute(LeaveAlternateScreen).unwrap();
486 Ok(())
487}
488
489fn wait_for_keypress() {
490 let mut single_key = [0u8];
491 std::io::stdin().read_exact(&mut single_key).unwrap();
492}
493
494#[cfg(test)]
495mod tests {
496 use super::{decrypt_ciphertext, sign_message, verify_message};
497 use snarkvm::{
498 prelude::{
499 Address,
500 Identifier,
501 Network,
502 Plaintext,
503 PrivateKey,
504 Process,
505 ProgramID,
506 Record,
507 Scalar,
508 TestRng,
509 U8,
510 Uniform,
511 ViewKey,
512 },
513 synthesizer::program::StackTrait,
514 };
515 use std::str::FromStr;
516
517 type CurrentNetwork = snarkvm::prelude::MainnetV0;
518
519 #[test]
520 fn test_signature_raw() {
521 let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
522 let message = "Hello, world!".to_string();
523 assert!(sign_message::<CurrentNetwork>(message, true, Some(key), None).is_ok());
524 }
525
526 #[test]
527 fn test_signature() {
528 let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
529 let message = "5field".to_string();
530 assert!(sign_message::<CurrentNetwork>(message, false, Some(key), None).is_ok());
531 }
532
533 #[test]
534 fn test_signature_fail() {
535 let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
536 let message = "not a literal value".to_string();
537 assert!(sign_message::<CurrentNetwork>(message, false, Some(key), None).is_err());
538 }
539
540 #[test]
541 fn test_verify_raw() {
542 let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j".to_string();
544 let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
545 let message = "Hello, world!".to_string();
546 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, true).is_ok());
547
548 let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
550 let message = "Different Message".to_string();
551 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, true).is_err());
552
553 let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
555 let message = "Hello, world!".to_string();
556 let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
557 assert!(verify_message::<CurrentNetwork>(wrong_address, signature, message, true).is_err());
558
559 let signature = "sign1424ztyt9hcm77nq450gvdszrvtg9kvhc4qadg4nzy9y0ah7wdqq7t36cxal42p9jj8e8pjpmc06lfev9nvffcpqv0cxwyr0a2j2tjqlesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk3yrr50".to_string();
561 let message = "Different Message".to_string();
562 assert!(verify_message::<CurrentNetwork>(address, signature, message, true).is_ok());
563 }
564
565 #[test]
566 fn test_verify() {
567 let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j".to_string();
569 let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
570 let message = "5field".to_string();
571 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, false).is_ok());
572
573 let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
575 let message = "10field".to_string();
576 assert!(verify_message::<CurrentNetwork>(address.clone(), signature, message, false).is_err());
577
578 let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
580 let message = "5field".to_string();
581 let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
582 assert!(verify_message::<CurrentNetwork>(wrong_address, signature, message, false).is_err());
583
584 let signature = "sign1t9v2t5tljk8pr5t6vkcqgkus0a3v69vryxmfrtwrwg0xtj7yv5qj2nz59e5zcyl50w23lhntxvt6vzeqfyu6dt56698zvfj2l6lz6q0esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk8rh9kt".to_string();
586 let message = "10field".to_string();
587 assert!(verify_message::<CurrentNetwork>(address, signature, message, false).is_ok());
588 }
589
590 #[test]
591 fn test_decrypt() -> anyhow::Result<()> {
592 let mut rng = &mut TestRng::default();
594
595 let private_key =
597 PrivateKey::<CurrentNetwork>::from_str("APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH")?;
598 let private_key_string = private_key.to_string();
599 let view_key = ViewKey::<CurrentNetwork>::try_from(&private_key)?;
600 let view_key_string = view_key.to_string();
601 let address = Address::<CurrentNetwork>::try_from(&view_key)?;
602
603 let process = Process::<CurrentNetwork>::load()?;
605 let stack = process.get_stack(ProgramID::from_str("credits.aleo")?)?;
606 let randomizer = Scalar::<CurrentNetwork>::rand(rng);
607 let nonce = CurrentNetwork::g_scalar_multiply(&randomizer);
608 let record = stack.sample_record(&address, &Identifier::from_str("credits").unwrap(), nonce, &mut rng)?;
609 let record = Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_plaintext(
610 record.owner().clone(),
611 record.data().clone(),
612 nonce,
613 U8::new(u8::rand(rng) % 2),
614 )?;
615 let record_string = record.to_string();
616 let ciphertext = record.encrypt(randomizer)?;
617 let ciphertext_string = ciphertext.to_string();
618
619 let candidate = decrypt_ciphertext::<CurrentNetwork>(Some(private_key_string), None, &ciphertext_string)?;
621 assert_eq!(candidate, record_string);
622
623 let candidate = decrypt_ciphertext::<CurrentNetwork>(Some(view_key_string), None, &ciphertext_string)?;
625 assert_eq!(candidate, record_string);
626
627 Ok(())
628 }
629}