1use alloy_chains::Chain;
2use alloy_dyn_abi::TypedData;
3use alloy_primitives::{hex, Address, Signature, B256, U256};
4use alloy_provider::Provider;
5use alloy_rpc_types::Authorization;
6use alloy_signer::{
7 k256::{elliptic_curve::sec1::ToEncodedPoint, SecretKey},
8 Signer,
9};
10use alloy_signer_local::{
11 coins_bip39::{English, Entropy, Mnemonic},
12 MnemonicBuilder, PrivateKeySigner,
13};
14use clap::Parser;
15use eyre::{Context, Result};
16use foundry_cli::{opts::RpcOpts, utils, utils::LoadConfig};
17use foundry_common::{fs, sh_println, shell};
18use foundry_config::Config;
19use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner};
20use rand_08::thread_rng;
21use serde_json::json;
22use std::path::Path;
23use yansi::Paint;
24
25pub mod vanity;
26use vanity::VanityArgs;
27
28pub mod list;
29use list::ListArgs;
30
31#[derive(Debug, Parser)]
33pub enum WalletSubcommands {
34 #[command(visible_alias = "n")]
36 New {
37 path: Option<String>,
39
40 #[arg(value_name = "ACCOUNT_NAME")]
43 account_name: Option<String>,
44
45 #[arg(long, short, requires = "path", conflicts_with = "unsafe_password")]
49 password: bool,
50
51 #[arg(long, requires = "path", env = "CAST_PASSWORD", value_name = "PASSWORD")]
55 unsafe_password: Option<String>,
56
57 #[arg(long, short, default_value = "1")]
59 number: u32,
60 },
61
62 #[command(visible_alias = "nm")]
64 NewMnemonic {
65 #[arg(long, short, default_value = "12")]
67 words: usize,
68
69 #[arg(long, short, default_value = "1")]
71 accounts: u8,
72
73 #[arg(long, short, conflicts_with = "words")]
75 entropy: Option<String>,
76 },
77
78 #[command(visible_alias = "va")]
80 Vanity(VanityArgs),
81
82 #[command(visible_aliases = &["a", "addr"])]
84 Address {
85 #[arg(value_name = "PRIVATE_KEY")]
87 private_key_override: Option<String>,
88
89 #[command(flatten)]
90 wallet: WalletOpts,
91 },
92
93 #[command(visible_alias = "s")]
95 Sign {
96 message: String,
110
111 #[arg(long)]
113 data: bool,
114
115 #[arg(long, requires = "data")]
117 from_file: bool,
118
119 #[arg(long, conflicts_with = "data")]
121 no_hash: bool,
122
123 #[command(flatten)]
124 wallet: WalletOpts,
125 },
126
127 #[command(visible_alias = "sa")]
129 SignAuth {
130 address: Address,
132
133 #[command(flatten)]
134 rpc: RpcOpts,
135
136 #[arg(long)]
137 nonce: Option<u64>,
138
139 #[arg(long)]
140 chain: Option<Chain>,
141
142 #[command(flatten)]
143 wallet: WalletOpts,
144 },
145
146 #[command(visible_alias = "v")]
148 Verify {
149 message: String,
154
155 signature: Signature,
157
158 #[arg(long, short)]
160 address: Address,
161 },
162
163 #[command(visible_alias = "i")]
165 Import {
166 #[arg(value_name = "ACCOUNT_NAME")]
168 account_name: String,
169 #[arg(long, short)]
172 keystore_dir: Option<String>,
173 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
176 unsafe_password: Option<String>,
177 #[command(flatten)]
178 raw_wallet_options: RawWalletOpts,
179 },
180
181 #[command(visible_alias = "ls")]
183 List(ListArgs),
184
185 #[command(visible_aliases = &["rm"], override_usage = "cast wallet remove --name <NAME>")]
190 Remove {
191 #[arg(long, required = true)]
193 name: String,
194 #[arg(long)]
197 dir: Option<String>,
198 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
201 unsafe_password: Option<String>,
202 },
203
204 #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])]
206 PrivateKey {
207 #[arg(value_name = "MNEMONIC")]
209 mnemonic_override: Option<String>,
210
211 #[arg(value_name = "MNEMONIC_INDEX_OR_DERIVATION_PATH")]
214 mnemonic_index_or_derivation_path_override: Option<String>,
215
216 #[command(flatten)]
217 wallet: WalletOpts,
218 },
219 #[command(visible_aliases = &["pubkey"])]
221 PublicKey {
222 #[arg(long = "raw-private-key", value_name = "PRIVATE_KEY")]
224 private_key_override: Option<String>,
225
226 #[command(flatten)]
227 wallet: WalletOpts,
228 },
229 #[command(name = "decrypt-keystore", visible_alias = "dk")]
231 DecryptKeystore {
232 #[arg(value_name = "ACCOUNT_NAME")]
234 account_name: String,
235 #[arg(long, short)]
238 keystore_dir: Option<String>,
239 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
242 unsafe_password: Option<String>,
243 },
244
245 #[command(name = "change-password", visible_alias = "cp")]
247 ChangePassword {
248 #[arg(value_name = "ACCOUNT_NAME")]
250 account_name: String,
251 #[arg(long, short)]
254 keystore_dir: Option<String>,
255 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
258 unsafe_password: Option<String>,
259 #[arg(long, env = "CAST_UNSAFE_NEW_PASSWORD", value_name = "NEW_PASSWORD")]
262 unsafe_new_password: Option<String>,
263 },
264}
265
266impl WalletSubcommands {
267 pub async fn run(self) -> Result<()> {
268 match self {
269 Self::New { path, account_name, unsafe_password, number, .. } => {
270 let mut rng = thread_rng();
271
272 let mut json_values = if shell::is_json() { Some(vec![]) } else { None };
273 if let Some(path) = path {
274 let path = match dunce::canonicalize(path.clone()) {
275 Ok(path) => path,
276 Err(e) => {
279 eyre::bail!("If you specified a directory, please make sure it exists, or create it before running `cast wallet new <DIR>`.\n{path} is not a directory.\nError: {}", e);
280 }
281 };
282 if !path.is_dir() {
283 eyre::bail!("`{}` is not a directory", path.display());
285 }
286
287 let password = if let Some(password) = unsafe_password {
288 password
289 } else {
290 rpassword::prompt_password("Enter secret: ")?
292 };
293
294 for i in 0..number {
295 let account_name_ref = account_name.as_deref().map(|name| match number {
296 1 => name.to_string(),
297 _ => format!("{}_{}", name, i + 1),
298 });
299
300 let (wallet, uuid) = PrivateKeySigner::new_keystore(
301 &path,
302 &mut rng,
303 password.clone(),
304 account_name_ref.as_deref(),
305 )?;
306 let identifier = account_name_ref.as_deref().unwrap_or(&uuid);
307
308 if let Some(json) = json_values.as_mut() {
309 json.push(json!({
310 "address": wallet.address().to_checksum(None),
311 "path": format!("{}", path.join(identifier).display()),
312 }));
313 } else {
314 sh_println!(
315 "Created new encrypted keystore file: {}",
316 path.join(identifier).display()
317 )?;
318 sh_println!("Address: {}", wallet.address().to_checksum(None))?;
319 }
320 }
321
322 if let Some(json) = json_values.as_ref() {
323 sh_println!("{}", serde_json::to_string_pretty(json)?)?;
324 }
325 } else {
326 for _ in 0..number {
327 let wallet = PrivateKeySigner::random_with(&mut rng);
328
329 if let Some(json) = json_values.as_mut() {
330 json.push(json!({
331 "address": wallet.address().to_checksum(None),
332 "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
333 }))
334 } else {
335 sh_println!("Successfully created new keypair.")?;
336 sh_println!("Address: {}", wallet.address().to_checksum(None))?;
337 sh_println!(
338 "Private key: 0x{}",
339 hex::encode(wallet.credential().to_bytes())
340 )?;
341 }
342 }
343
344 if let Some(json) = json_values.as_ref() {
345 sh_println!("{}", serde_json::to_string_pretty(json)?)?;
346 }
347 }
348 }
349 Self::NewMnemonic { words, accounts, entropy } => {
350 let phrase = if let Some(entropy) = entropy {
351 let entropy = Entropy::from_slice(hex::decode(entropy)?)?;
352 Mnemonic::<English>::new_from_entropy(entropy).to_phrase()
353 } else {
354 let mut rng = thread_rng();
355 Mnemonic::<English>::new_with_count(&mut rng, words)?.to_phrase()
356 };
357
358 let format_json = shell::is_json();
359
360 if !format_json {
361 sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?;
362 }
363
364 let builder = MnemonicBuilder::<English>::default().phrase(phrase.as_str());
365 let derivation_path = "m/44'/60'/0'/0/";
366 let wallets = (0..accounts)
367 .map(|i| builder.clone().derivation_path(format!("{derivation_path}{i}")))
368 .collect::<Result<Vec<_>, _>>()?;
369 let wallets =
370 wallets.into_iter().map(|b| b.build()).collect::<Result<Vec<_>, _>>()?;
371
372 if !format_json {
373 sh_println!("{}", "Successfully generated a new mnemonic.".green())?;
374 sh_println!("Phrase:\n{phrase}")?;
375 sh_println!("\nAccounts:")?;
376 }
377
378 let mut accounts = json!([]);
379 for (i, wallet) in wallets.iter().enumerate() {
380 let private_key = hex::encode(wallet.credential().to_bytes());
381 if format_json {
382 accounts.as_array_mut().unwrap().push(json!({
383 "address": format!("{}", wallet.address()),
384 "private_key": format!("0x{}", private_key),
385 }));
386 } else {
387 sh_println!("- Account {i}:")?;
388 sh_println!("Address: {}", wallet.address())?;
389 sh_println!("Private key: 0x{private_key}\n")?;
390 }
391 }
392
393 if format_json {
394 let obj = json!({
395 "mnemonic": phrase,
396 "accounts": accounts,
397 });
398 sh_println!("{}", serde_json::to_string_pretty(&obj)?)?;
399 }
400 }
401 Self::Vanity(cmd) => {
402 cmd.run()?;
403 }
404 Self::Address { wallet, private_key_override } => {
405 let wallet = private_key_override
406 .map(|pk| WalletOpts {
407 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
408 ..Default::default()
409 })
410 .unwrap_or(wallet)
411 .signer()
412 .await?;
413 let addr = wallet.address();
414 sh_println!("{}", addr.to_checksum(None))?;
415 }
416 Self::PublicKey { wallet, private_key_override } => {
417 let wallet = private_key_override
418 .map(|pk| WalletOpts {
419 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
420 ..Default::default()
421 })
422 .unwrap_or(wallet)
423 .signer()
424 .await?;
425
426 let private_key_bytes = match wallet {
427 WalletSigner::Local(wallet) => wallet.credential().to_bytes(),
428 _ => eyre::bail!("Only local wallets are supported by this command"),
429 };
430
431 let secret_key = SecretKey::from_slice(&private_key_bytes)
432 .map_err(|e| eyre::eyre!("Invalid private key: {}", e))?;
433
434 let public_key = secret_key.public_key();
436
437 let pubkey_bytes = public_key.to_encoded_point(false);
439 let ethereum_pubkey = &pubkey_bytes.as_bytes()[1..];
441
442 sh_println!("0x{}", hex::encode(ethereum_pubkey))?;
443 }
444 Self::Sign { message, data, from_file, no_hash, wallet } => {
445 let wallet = wallet.signer().await?;
446 let sig = if data {
447 let typed_data: TypedData = if from_file {
448 foundry_common::fs::read_json_file(message.as_ref())?
450 } else {
451 serde_json::from_str(&message)?
453 };
454 wallet.sign_dynamic_typed_data(&typed_data).await?
455 } else if no_hash {
456 wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
457 } else {
458 wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
459 };
460
461 if shell::verbosity() > 0 {
462 if shell::is_json() {
463 sh_println!(
464 "{}",
465 serde_json::to_string_pretty(&json!({
466 "message": message,
467 "address": wallet.address(),
468 "signature": hex::encode(sig.as_bytes()),
469 }))?
470 )?;
471 } else {
472 sh_println!(
473 "Successfully signed!\n Message: {}\n Address: {}\n Signature: 0x{}",
474 message,
475 wallet.address(),
476 hex::encode(sig.as_bytes()),
477 )?;
478 }
479 } else {
480 sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
482 }
483 }
484 Self::SignAuth { rpc, nonce, chain, wallet, address } => {
485 let wallet = wallet.signer().await?;
486 let provider = utils::get_provider(&rpc.load_config()?)?;
487 let nonce = if let Some(nonce) = nonce {
488 nonce
489 } else {
490 provider.get_transaction_count(wallet.address()).await?
491 };
492 let chain_id = if let Some(chain) = chain {
493 chain.id()
494 } else {
495 provider.get_chain_id().await?
496 };
497 let auth = Authorization { chain_id: U256::from(chain_id), address, nonce };
498 let signature = wallet.sign_hash(&auth.signature_hash()).await?;
499 let auth = auth.into_signed(signature);
500
501 if shell::verbosity() > 0 {
502 if shell::is_json() {
503 sh_println!(
504 "{}",
505 serde_json::to_string_pretty(&json!({
506 "nonce": nonce,
507 "chain_id": chain_id,
508 "address": wallet.address(),
509 "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
510 }))?
511 )?;
512 } else {
513 sh_println!(
514 "Successfully signed!\n Nonce: {}\n Chain ID: {}\n Address: {}\n Signature: 0x{}",
515 nonce,
516 chain_id,
517 wallet.address(),
518 hex::encode_prefixed(alloy_rlp::encode(&auth)),
519 )?;
520 }
521 } else {
522 sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
524 }
525 }
526 Self::Verify { message, signature, address } => {
527 let recovered_address = Self::recover_address_from_message(&message, &signature)?;
528 if address == recovered_address {
529 sh_println!("Validation succeeded. Address {address} signed this message.")?;
530 } else {
531 eyre::bail!("Validation failed. Address {address} did not sign this message.");
532 }
533 }
534 Self::Import { account_name, keystore_dir, unsafe_password, raw_wallet_options } => {
535 let dir = if let Some(path) = keystore_dir {
537 Path::new(&path).to_path_buf()
538 } else {
539 Config::foundry_keystores_dir().ok_or_else(|| {
540 eyre::eyre!("Could not find the default keystore directory.")
541 })?
542 };
543
544 fs::create_dir_all(&dir)?;
545
546 let keystore_path = Path::new(&dir).join(&account_name);
548 if keystore_path.exists() {
549 eyre::bail!("Keystore file already exists at {}", keystore_path.display());
550 }
551
552 let wallet = raw_wallet_options
554 .signer()?
555 .and_then(|s| match s {
556 WalletSigner::Local(s) => Some(s),
557 _ => None,
558 })
559 .ok_or_else(|| {
560 eyre::eyre!(
561 "\
562Did you set a private key or mnemonic?
563Run `cast wallet import --help` and use the corresponding CLI
564flag to set your key via:
565--private-key, --mnemonic-path or --interactive."
566 )
567 })?;
568
569 let private_key = wallet.credential().to_bytes();
570 let password = if let Some(password) = unsafe_password {
571 password
572 } else {
573 rpassword::prompt_password("Enter password: ")?
575 };
576
577 let mut rng = thread_rng();
578 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
579 dir,
580 &mut rng,
581 private_key,
582 password,
583 Some(&account_name),
584 )?;
585 let address = wallet.address();
586 let success_message = format!(
587 "`{}` keystore was saved successfully. Address: {:?}",
588 &account_name, address,
589 );
590 sh_println!("{}", success_message.green())?;
591 }
592 Self::List(cmd) => {
593 cmd.run().await?;
594 }
595 Self::Remove { name, dir, unsafe_password } => {
596 let dir = if let Some(path) = dir {
597 Path::new(&path).to_path_buf()
598 } else {
599 Config::foundry_keystores_dir().ok_or_else(|| {
600 eyre::eyre!("Could not find the default keystore directory.")
601 })?
602 };
603
604 let keystore_path = Path::new(&dir).join(&name);
605 if !keystore_path.exists() {
606 eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
607 }
608
609 let password = if let Some(pwd) = unsafe_password {
610 pwd
611 } else {
612 rpassword::prompt_password("Enter password: ")?
613 };
614
615 if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
616 eyre::bail!("Invalid password - wallet removal cancelled");
617 }
618
619 std::fs::remove_file(&keystore_path).wrap_err_with(|| {
620 format!("Failed to remove keystore file at {}", keystore_path.display())
621 })?;
622
623 let success_message = format!("`{}` keystore was removed successfully.", &name);
624 sh_println!("{}", success_message.green())?;
625 }
626 Self::PrivateKey {
627 wallet,
628 mnemonic_override,
629 mnemonic_index_or_derivation_path_override,
630 } => {
631 let (index_override, derivation_path_override) =
632 match mnemonic_index_or_derivation_path_override {
633 Some(value) => match value.parse::<u32>() {
634 Ok(index) => (Some(index), None),
635 Err(_) => (None, Some(value)),
636 },
637 None => (None, None),
638 };
639 let wallet = WalletOpts {
640 raw: RawWalletOpts {
641 mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
642 mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
643 hd_path: derivation_path_override.or(wallet.raw.hd_path),
644 ..wallet.raw
645 },
646 ..wallet
647 }
648 .signer()
649 .await?;
650 match wallet {
651 WalletSigner::Local(wallet) => {
652 if shell::verbosity() > 0 {
653 sh_println!("Address: {}", wallet.address())?;
654 sh_println!(
655 "Private key: 0x{}",
656 hex::encode(wallet.credential().to_bytes())
657 )?;
658 } else {
659 sh_println!("0x{}", hex::encode(wallet.credential().to_bytes()))?;
660 }
661 }
662 _ => {
663 eyre::bail!("Only local wallets are supported by this command.");
664 }
665 }
666 }
667 Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
668 let dir = if let Some(path) = keystore_dir {
670 Path::new(&path).to_path_buf()
671 } else {
672 Config::foundry_keystores_dir().ok_or_else(|| {
673 eyre::eyre!("Could not find the default keystore directory.")
674 })?
675 };
676
677 let keypath = dir.join(&account_name);
678
679 if !keypath.exists() {
680 eyre::bail!("Keystore file does not exist at {}", keypath.display());
681 }
682
683 let password = if let Some(password) = unsafe_password {
684 password
685 } else {
686 rpassword::prompt_password("Enter password: ")?
688 };
689
690 let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
691
692 let private_key = B256::from_slice(&wallet.credential().to_bytes());
693
694 let success_message =
695 format!("{}'s private key is: {}", &account_name, private_key);
696
697 sh_println!("{}", success_message.green())?;
698 }
699 Self::ChangePassword {
700 account_name,
701 keystore_dir,
702 unsafe_password,
703 unsafe_new_password,
704 } => {
705 let dir = if let Some(path) = keystore_dir {
707 Path::new(&path).to_path_buf()
708 } else {
709 Config::foundry_keystores_dir().ok_or_else(|| {
710 eyre::eyre!("Could not find the default keystore directory.")
711 })?
712 };
713
714 let keypath = dir.join(&account_name);
715
716 if !keypath.exists() {
717 eyre::bail!("Keystore file does not exist at {}", keypath.display());
718 }
719
720 let current_password = if let Some(password) = unsafe_password {
721 password
722 } else {
723 rpassword::prompt_password("Enter current password: ")?
725 };
726
727 let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
729 .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
730
731 let new_password = if let Some(password) = unsafe_new_password {
732 password
733 } else {
734 rpassword::prompt_password("Enter new password: ")?
736 };
737
738 if current_password == new_password {
739 eyre::bail!("New password cannot be the same as the current password");
740 }
741
742 let private_key = wallet.credential().to_bytes();
744 let mut rng = thread_rng();
745 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
746 dir,
747 &mut rng,
748 private_key,
749 new_password,
750 Some(&account_name),
751 )?;
752
753 let success_message = format!(
754 "Password for keystore `{}` was changed successfully. Address: {:?}",
755 &account_name,
756 wallet.address(),
757 );
758 sh_println!("{}", success_message.green())?;
759 }
760 };
761
762 Ok(())
763 }
764
765 fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
769 let message = Self::hex_str_to_bytes(message)?;
770 Ok(signature.recover_address_from_msg(message)?)
771 }
772
773 fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
777 Ok(match s.strip_prefix("0x") {
778 Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
779 None => s.as_bytes().to_vec(),
780 })
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use alloy_primitives::address;
788 use std::str::FromStr;
789
790 #[test]
791 fn can_parse_wallet_sign_message() {
792 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
793 match args {
794 WalletSubcommands::Sign { message, data, from_file, .. } => {
795 assert_eq!(message, "deadbeef".to_string());
796 assert!(!data);
797 assert!(!from_file);
798 }
799 _ => panic!("expected WalletSubcommands::Sign"),
800 }
801 }
802
803 #[test]
804 fn can_parse_wallet_sign_hex_message() {
805 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
806 match args {
807 WalletSubcommands::Sign { message, data, from_file, .. } => {
808 assert_eq!(message, "0xdeadbeef".to_string());
809 assert!(!data);
810 assert!(!from_file);
811 }
812 _ => panic!("expected WalletSubcommands::Sign"),
813 }
814 }
815
816 #[test]
817 fn can_verify_signed_hex_message() {
818 let message = "hello";
819 let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
820 let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
821 let recovered_address =
822 WalletSubcommands::recover_address_from_message(message, &signature);
823 assert!(recovered_address.is_ok());
824 assert_eq!(address, recovered_address.unwrap());
825 }
826
827 #[test]
828 fn can_parse_wallet_sign_data() {
829 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
830 match args {
831 WalletSubcommands::Sign { message, data, from_file, .. } => {
832 assert_eq!(message, "{ ... }".to_string());
833 assert!(data);
834 assert!(!from_file);
835 }
836 _ => panic!("expected WalletSubcommands::Sign"),
837 }
838 }
839
840 #[test]
841 fn can_parse_wallet_sign_data_file() {
842 let args = WalletSubcommands::parse_from([
843 "foundry-cli",
844 "sign",
845 "--data",
846 "--from-file",
847 "tests/data/typed_data.json",
848 ]);
849 match args {
850 WalletSubcommands::Sign { message, data, from_file, .. } => {
851 assert_eq!(message, "tests/data/typed_data.json".to_string());
852 assert!(data);
853 assert!(from_file);
854 }
855 _ => panic!("expected WalletSubcommands::Sign"),
856 }
857 }
858
859 #[test]
860 fn can_parse_wallet_change_password() {
861 let args = WalletSubcommands::parse_from([
862 "foundry-cli",
863 "change-password",
864 "my_account",
865 "--unsafe-password",
866 "old_password",
867 "--unsafe-new-password",
868 "new_password",
869 ]);
870 match args {
871 WalletSubcommands::ChangePassword {
872 account_name,
873 keystore_dir,
874 unsafe_password,
875 unsafe_new_password,
876 } => {
877 assert_eq!(account_name, "my_account".to_string());
878 assert_eq!(unsafe_password, Some("old_password".to_string()));
879 assert_eq!(unsafe_new_password, Some("new_password".to_string()));
880 assert!(keystore_dir.is_none());
881 }
882 _ => panic!("expected WalletSubcommands::ChangePassword"),
883 }
884 }
885}