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