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 #[arg(long)]
60 force: bool,
61 },
62
63 #[command(visible_alias = "nm")]
65 NewMnemonic {
66 #[arg(long, short, default_value = "12")]
68 words: usize,
69
70 #[arg(long, short, default_value = "1")]
72 accounts: u8,
73
74 #[arg(long, short, conflicts_with = "words")]
76 entropy: Option<String>,
77 },
78
79 #[command(visible_alias = "va")]
81 Vanity(VanityArgs),
82
83 #[command(visible_aliases = &["a", "addr"])]
85 Address {
86 #[arg(value_name = "PRIVATE_KEY")]
88 private_key_override: Option<String>,
89
90 #[command(flatten)]
91 wallet: WalletOpts,
92 },
93
94 #[command(visible_alias = "d")]
96 Derive {
97 #[arg(value_name = "MNEMONIC")]
99 mnemonic: String,
100
101 #[arg(long, short, default_value = "1")]
103 accounts: Option<u8>,
104
105 #[arg(long, default_value = "false")]
107 insecure: bool,
108 },
109
110 #[command(visible_alias = "s")]
112 Sign {
113 message: String,
127
128 #[arg(long)]
130 data: bool,
131
132 #[arg(long, requires = "data")]
134 from_file: bool,
135
136 #[arg(long, conflicts_with = "data")]
138 no_hash: bool,
139
140 #[command(flatten)]
141 wallet: WalletOpts,
142 },
143
144 #[command(visible_alias = "sa")]
146 SignAuth {
147 address: Address,
149
150 #[command(flatten)]
151 rpc: RpcOpts,
152
153 #[arg(long)]
154 nonce: Option<u64>,
155
156 #[arg(long)]
157 chain: Option<Chain>,
158
159 #[arg(long, conflicts_with = "nonce")]
163 self_broadcast: bool,
164
165 #[command(flatten)]
166 wallet: WalletOpts,
167 },
168
169 #[command(visible_alias = "v")]
171 Verify {
172 message: String,
186
187 signature: Signature,
189
190 #[arg(long, short)]
192 address: Address,
193
194 #[arg(long)]
196 data: bool,
197
198 #[arg(long, requires = "data")]
200 from_file: bool,
201
202 #[arg(long, conflicts_with = "data")]
204 no_hash: bool,
205 },
206
207 #[command(visible_alias = "i")]
209 Import {
210 #[arg(value_name = "ACCOUNT_NAME")]
212 account_name: String,
213 #[arg(long, short)]
216 keystore_dir: Option<String>,
217 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
220 unsafe_password: Option<String>,
221 #[command(flatten)]
222 raw_wallet_options: RawWalletOpts,
223 },
224
225 #[command(visible_alias = "ls")]
227 List(ListArgs),
228
229 #[command(visible_aliases = &["rm"], override_usage = "cast wallet remove --name <NAME>")]
234 Remove {
235 #[arg(long, required = true)]
237 name: String,
238 #[arg(long)]
241 dir: Option<String>,
242 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
245 unsafe_password: Option<String>,
246 },
247
248 #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])]
250 PrivateKey {
251 #[arg(value_name = "MNEMONIC")]
253 mnemonic_override: Option<String>,
254
255 #[arg(value_name = "MNEMONIC_INDEX_OR_DERIVATION_PATH")]
258 mnemonic_index_or_derivation_path_override: Option<String>,
259
260 #[command(flatten)]
261 wallet: WalletOpts,
262 },
263 #[command(visible_aliases = &["pubkey"])]
265 PublicKey {
266 #[arg(long = "raw-private-key", value_name = "PRIVATE_KEY")]
268 private_key_override: Option<String>,
269
270 #[command(flatten)]
271 wallet: WalletOpts,
272 },
273 #[command(name = "decrypt-keystore", visible_alias = "dk")]
275 DecryptKeystore {
276 #[arg(value_name = "ACCOUNT_NAME")]
278 account_name: String,
279 #[arg(long, short)]
282 keystore_dir: Option<String>,
283 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
286 unsafe_password: Option<String>,
287 },
288
289 #[command(name = "change-password", visible_alias = "cp")]
291 ChangePassword {
292 #[arg(value_name = "ACCOUNT_NAME")]
294 account_name: String,
295 #[arg(long, short)]
298 keystore_dir: Option<String>,
299 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
302 unsafe_password: Option<String>,
303 #[arg(long, env = "CAST_UNSAFE_NEW_PASSWORD", value_name = "NEW_PASSWORD")]
306 unsafe_new_password: Option<String>,
307 },
308}
309
310impl WalletSubcommands {
311 pub async fn run(self) -> Result<()> {
312 match self {
313 Self::New { path, account_name, unsafe_password, number, password, force } => {
314 let mut rng = thread_rng();
315
316 let mut json_values = if shell::is_json() { Some(vec![]) } else { None };
317
318 let path = if let Some(path) = path {
319 match dunce::canonicalize(&path) {
320 Ok(path) => {
321 if !path.is_dir() {
322 eyre::bail!("`{}` is not a directory", path.display());
324 }
325 Some(path)
326 }
327 Err(e) => {
328 eyre::bail!(
329 "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: {}",
330 e
331 );
332 }
333 }
334 } else if unsafe_password.is_some() || password {
335 let path = Config::foundry_keystores_dir().ok_or_else(|| {
336 eyre::eyre!("Could not find the default keystore directory.")
337 })?;
338 fs::create_dir_all(&path)?;
339 Some(path)
340 } else {
341 None
342 };
343
344 match path {
345 Some(path) => {
346 let password = if let Some(password) = unsafe_password {
347 password
348 } else {
349 rpassword::prompt_password("Enter secret: ")?
351 };
352
353 if !force && let Some(ref acc_name) = account_name {
355 let mut existing_files = Vec::new();
356
357 for i in 0..number {
358 let name = match number {
359 1 => acc_name.to_string(),
360 _ => format!("{}_{}", acc_name, i + 1),
361 };
362 let file_path = path.join(&name);
363 if file_path.exists() {
364 existing_files.push(name);
365 }
366 }
367
368 if !existing_files.is_empty() {
369 use std::io::Write;
370
371 sh_eprintln!("The following keystore file(s) already exist:")?;
372 for file in &existing_files {
373 sh_eprintln!(" - {file}")?;
374 }
375 sh_print!(
376 "\nDo you want to overwrite all {} file(s)? [y/N]: ",
377 existing_files.len()
378 )?;
379 std::io::stdout().flush()?;
380
381 let mut input = String::new();
382 std::io::stdin().read_line(&mut input)?;
383
384 if !input.trim().eq_ignore_ascii_case("y") {
385 eyre::bail!("Operation cancelled. No keystores were modified.");
386 }
387 }
388 }
389 for i in 0..number {
390 let account_name_ref =
391 account_name.as_deref().map(|name| match number {
392 1 => name.to_string(),
393 _ => format!("{}_{}", name, i + 1),
394 });
395
396 let (wallet, uuid) = PrivateKeySigner::new_keystore(
397 &path,
398 &mut rng,
399 password.clone(),
400 account_name_ref.as_deref(),
401 )?;
402 let identifier = account_name_ref.as_deref().unwrap_or(&uuid);
403
404 if let Some(json) = json_values.as_mut() {
405 json.push(if shell::verbosity() > 0 {
406 json!({
407 "address": wallet.address().to_checksum(None),
408 "public_key": format!("0x{}", hex::encode(wallet.public_key())),
409 "path": format!("{}", path.join(identifier).display()),
410 })
411 } else {
412 json!({
413 "address": wallet.address().to_checksum(None),
414 "path": format!("{}", path.join(identifier).display()),
415 })
416 });
417 } else {
418 sh_println!(
419 "Created new encrypted keystore file: {}",
420 path.join(identifier).display()
421 )?;
422 sh_println!("Address: {}", wallet.address().to_checksum(None))?;
423 if shell::verbosity() > 0 {
424 sh_println!(
425 "Public key: 0x{}",
426 hex::encode(wallet.public_key())
427 )?;
428 }
429 }
430 }
431 }
432 None => {
433 for _ in 0..number {
434 let wallet = PrivateKeySigner::random_with(&mut rng);
435
436 if let Some(json) = json_values.as_mut() {
437 json.push(if shell::verbosity() > 0 {
438 json!({
439 "address": wallet.address().to_checksum(None),
440 "public_key": format!("0x{}", hex::encode(wallet.public_key())),
441 "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
442 })
443 } else {
444 json!({
445 "address": wallet.address().to_checksum(None),
446 "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
447 })
448 });
449 } else {
450 sh_println!("Successfully created new keypair.")?;
451 sh_println!("Address: {}", wallet.address().to_checksum(None))?;
452 if shell::verbosity() > 0 {
453 sh_println!(
454 "Public key: 0x{}",
455 hex::encode(wallet.public_key())
456 )?;
457 }
458 sh_println!(
459 "Private key: 0x{}",
460 hex::encode(wallet.credential().to_bytes())
461 )?;
462 }
463 }
464 }
465 }
466
467 if let Some(json) = json_values.as_ref() {
468 sh_println!("{}", serde_json::to_string_pretty(json)?)?;
469 }
470 }
471 Self::NewMnemonic { words, accounts, entropy } => {
472 let phrase = if let Some(entropy) = entropy {
473 let entropy = Entropy::from_slice(hex::decode(entropy)?)?;
474 Mnemonic::<English>::new_from_entropy(entropy).to_phrase()
475 } else {
476 let mut rng = thread_rng();
477 Mnemonic::<English>::new_with_count(&mut rng, words)?.to_phrase()
478 };
479
480 let format_json = shell::is_json();
481
482 if !format_json {
483 sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?;
484 }
485
486 let builder = MnemonicBuilder::<English>::default().phrase(phrase.as_str());
487 let derivation_path = "m/44'/60'/0'/0/";
488 let wallets = (0..accounts)
489 .map(|i| builder.clone().derivation_path(format!("{derivation_path}{i}")))
490 .collect::<Result<Vec<_>, _>>()?;
491 let wallets =
492 wallets.into_iter().map(|b| b.build()).collect::<Result<Vec<_>, _>>()?;
493
494 if !format_json {
495 sh_println!("{}", "Successfully generated a new mnemonic.".green())?;
496 sh_println!("Phrase:\n{phrase}")?;
497 sh_println!("\nAccounts:")?;
498 }
499
500 let mut accounts = json!([]);
501 for (i, wallet) in wallets.iter().enumerate() {
502 let public_key = hex::encode(wallet.public_key());
503 let private_key = hex::encode(wallet.credential().to_bytes());
504 if format_json {
505 accounts.as_array_mut().unwrap().push(if shell::verbosity() > 0 {
506 json!({
507 "address": format!("{}", wallet.address()),
508 "public_key": format!("0x{}", public_key),
509 "private_key": format!("0x{}", private_key),
510 })
511 } else {
512 json!({
513 "address": format!("{}", wallet.address()),
514 "private_key": format!("0x{}", private_key),
515 })
516 });
517 } else {
518 sh_println!("- Account {i}:")?;
519 sh_println!("Address: {}", wallet.address())?;
520 if shell::verbosity() > 0 {
521 sh_println!("Public key: 0x{}", public_key)?;
522 }
523 sh_println!("Private key: 0x{}\n", private_key)?;
524 }
525 }
526
527 if format_json {
528 let obj = json!({
529 "mnemonic": phrase,
530 "accounts": accounts,
531 });
532 sh_println!("{}", serde_json::to_string_pretty(&obj)?)?;
533 }
534 }
535 Self::Vanity(cmd) => {
536 cmd.run()?;
537 }
538 Self::Address { wallet, private_key_override } => {
539 let wallet = private_key_override
540 .map(|pk| WalletOpts {
541 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
542 ..Default::default()
543 })
544 .unwrap_or(wallet)
545 .signer()
546 .await?;
547 let addr = wallet.address();
548 sh_println!("{}", addr.to_checksum(None))?;
549 }
550 Self::Derive { mnemonic, accounts, insecure } => {
551 let format_json = shell::is_json();
552 let mut accounts_json = json!([]);
553 for i in 0..accounts.unwrap_or(1) {
554 let wallet = WalletOpts {
555 raw: RawWalletOpts {
556 mnemonic: Some(mnemonic.clone()),
557 mnemonic_index: i as u32,
558 ..Default::default()
559 },
560 ..Default::default()
561 }
562 .signer()
563 .await?;
564
565 match wallet {
566 WalletSigner::Local(local_wallet) => {
567 let address = local_wallet.address().to_checksum(None);
568 let private_key = hex::encode(local_wallet.credential().to_bytes());
569 if format_json {
570 if insecure {
571 accounts_json.as_array_mut().unwrap().push(json!({
572 "address": format!("{}", address),
573 "private_key": format!("0x{}", private_key),
574 }));
575 } else {
576 accounts_json.as_array_mut().unwrap().push(json!({
577 "address": format!("{}", address)
578 }));
579 }
580 } else {
581 sh_println!("- Account {i}:")?;
582 if insecure {
583 sh_println!("Address: {}", address)?;
584 sh_println!("Private key: 0x{}\n", private_key)?;
585 } else {
586 sh_println!("Address: {}\n", address)?;
587 }
588 }
589 }
590 _ => eyre::bail!("Only local wallets are supported by this command"),
591 }
592 }
593
594 if format_json {
595 sh_println!("{}", serde_json::to_string_pretty(&accounts_json)?)?;
596 }
597 }
598 Self::PublicKey { wallet, private_key_override } => {
599 let wallet = private_key_override
600 .map(|pk| WalletOpts {
601 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
602 ..Default::default()
603 })
604 .unwrap_or(wallet)
605 .signer()
606 .await?;
607
608 let public_key = match wallet {
609 WalletSigner::Local(wallet) => wallet.public_key(),
610 _ => eyre::bail!("Only local wallets are supported by this command"),
611 };
612
613 sh_println!("0x{}", hex::encode(public_key))?;
614 }
615 Self::Sign { message, data, from_file, no_hash, wallet } => {
616 let wallet = wallet.signer().await?;
617 let sig = if data {
618 let typed_data: TypedData = if from_file {
619 foundry_common::fs::read_json_file(message.as_ref())?
621 } else {
622 serde_json::from_str(&message)?
624 };
625 wallet.sign_dynamic_typed_data(&typed_data).await?
626 } else if no_hash {
627 wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
628 } else {
629 wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
630 };
631
632 if shell::verbosity() > 0 {
633 if shell::is_json() {
634 sh_println!(
635 "{}",
636 serde_json::to_string_pretty(&json!({
637 "message": message,
638 "address": wallet.address(),
639 "signature": hex::encode(sig.as_bytes()),
640 }))?
641 )?;
642 } else {
643 sh_println!(
644 "Successfully signed!\n Message: {}\n Address: {}\n Signature: 0x{}",
645 message,
646 wallet.address(),
647 hex::encode(sig.as_bytes()),
648 )?;
649 }
650 } else {
651 sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
653 }
654 }
655 Self::SignAuth { rpc, nonce, chain, wallet, address, self_broadcast } => {
656 let wallet = wallet.signer().await?;
657 let provider = utils::get_provider(&rpc.load_config()?)?;
658 let nonce = if let Some(nonce) = nonce {
659 nonce
660 } else {
661 let current_nonce = provider.get_transaction_count(wallet.address()).await?;
662 if self_broadcast {
663 current_nonce + 1
666 } else {
667 current_nonce
668 }
669 };
670 let chain_id = if let Some(chain) = chain {
671 chain.id()
672 } else {
673 provider.get_chain_id().await?
674 };
675 let auth = Authorization { chain_id: U256::from(chain_id), address, nonce };
676 let signature = wallet.sign_hash(&auth.signature_hash()).await?;
677 let auth = auth.into_signed(signature);
678
679 if shell::verbosity() > 0 {
680 if shell::is_json() {
681 sh_println!(
682 "{}",
683 serde_json::to_string_pretty(&json!({
684 "nonce": nonce,
685 "chain_id": chain_id,
686 "address": wallet.address(),
687 "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
688 }))?
689 )?;
690 } else {
691 sh_println!(
692 "Successfully signed!\n Nonce: {}\n Chain ID: {}\n Address: {}\n Signature: 0x{}",
693 nonce,
694 chain_id,
695 wallet.address(),
696 hex::encode_prefixed(alloy_rlp::encode(&auth)),
697 )?;
698 }
699 } else {
700 sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
702 }
703 }
704 Self::Verify { message, signature, address, data, from_file, no_hash } => {
705 let recovered_address = if data {
706 let typed_data: TypedData = if from_file {
707 foundry_common::fs::read_json_file(message.as_ref())?
709 } else {
710 serde_json::from_str(&message)?
712 };
713 Self::recover_address_from_typed_data(&typed_data, &signature)?
714 } else if no_hash {
715 Self::recover_address_from_message_no_hash(
716 &hex::decode(&message)?[..].try_into()?,
717 &signature,
718 )?
719 } else {
720 Self::recover_address_from_message(&message, &signature)?
721 };
722
723 if address == recovered_address {
724 sh_println!("Validation succeeded. Address {address} signed this message.")?;
725 } else {
726 eyre::bail!("Validation failed. Address {address} did not sign this message.");
727 }
728 }
729 Self::Import { account_name, keystore_dir, unsafe_password, raw_wallet_options } => {
730 let dir = if let Some(path) = keystore_dir {
732 Path::new(&path).to_path_buf()
733 } else {
734 Config::foundry_keystores_dir().ok_or_else(|| {
735 eyre::eyre!("Could not find the default keystore directory.")
736 })?
737 };
738
739 fs::create_dir_all(&dir)?;
740
741 let keystore_path = Path::new(&dir).join(&account_name);
743 if keystore_path.exists() {
744 eyre::bail!("Keystore file already exists at {}", keystore_path.display());
745 }
746
747 let wallet = raw_wallet_options
749 .signer()?
750 .and_then(|s| match s {
751 WalletSigner::Local(s) => Some(s),
752 _ => None,
753 })
754 .ok_or_else(|| {
755 eyre::eyre!(
756 "\
757Did you set a private key or mnemonic?
758Run `cast wallet import --help` and use the corresponding CLI
759flag to set your key via:
760--private-key, --mnemonic-path or --interactive."
761 )
762 })?;
763
764 let private_key = wallet.credential().to_bytes();
765 let password = if let Some(password) = unsafe_password {
766 password
767 } else {
768 rpassword::prompt_password("Enter password: ")?
770 };
771
772 let mut rng = thread_rng();
773 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
774 dir,
775 &mut rng,
776 private_key,
777 password,
778 Some(&account_name),
779 )?;
780 let address = wallet.address();
781 let success_message = format!(
782 "`{}` keystore was saved successfully. Address: {:?}",
783 &account_name, address,
784 );
785 sh_println!("{}", success_message.green())?;
786 }
787 Self::List(cmd) => {
788 cmd.run().await?;
789 }
790 Self::Remove { name, dir, unsafe_password } => {
791 let dir = if let Some(path) = dir {
792 Path::new(&path).to_path_buf()
793 } else {
794 Config::foundry_keystores_dir().ok_or_else(|| {
795 eyre::eyre!("Could not find the default keystore directory.")
796 })?
797 };
798
799 let keystore_path = Path::new(&dir).join(&name);
800 if !keystore_path.exists() {
801 eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
802 }
803
804 let password = if let Some(pwd) = unsafe_password {
805 pwd
806 } else {
807 rpassword::prompt_password("Enter password: ")?
808 };
809
810 if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
811 eyre::bail!("Invalid password - wallet removal cancelled");
812 }
813
814 std::fs::remove_file(&keystore_path).wrap_err_with(|| {
815 format!("Failed to remove keystore file at {}", keystore_path.display())
816 })?;
817
818 let success_message = format!("`{}` keystore was removed successfully.", &name);
819 sh_println!("{}", success_message.green())?;
820 }
821 Self::PrivateKey {
822 wallet,
823 mnemonic_override,
824 mnemonic_index_or_derivation_path_override,
825 } => {
826 let (index_override, derivation_path_override) =
827 match mnemonic_index_or_derivation_path_override {
828 Some(value) => match value.parse::<u32>() {
829 Ok(index) => (Some(index), None),
830 Err(_) => (None, Some(value)),
831 },
832 None => (None, None),
833 };
834 let wallet = WalletOpts {
835 raw: RawWalletOpts {
836 mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
837 mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
838 hd_path: derivation_path_override.or(wallet.raw.hd_path),
839 ..wallet.raw
840 },
841 ..wallet
842 }
843 .signer()
844 .await?;
845 match wallet {
846 WalletSigner::Local(wallet) => {
847 if shell::verbosity() > 0 {
848 sh_println!("Address: {}", wallet.address())?;
849 sh_println!(
850 "Private key: 0x{}",
851 hex::encode(wallet.credential().to_bytes())
852 )?;
853 } else {
854 sh_println!("0x{}", hex::encode(wallet.credential().to_bytes()))?;
855 }
856 }
857 _ => {
858 eyre::bail!("Only local wallets are supported by this command.");
859 }
860 }
861 }
862 Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
863 let dir = if let Some(path) = keystore_dir {
865 Path::new(&path).to_path_buf()
866 } else {
867 Config::foundry_keystores_dir().ok_or_else(|| {
868 eyre::eyre!("Could not find the default keystore directory.")
869 })?
870 };
871
872 let keypath = dir.join(&account_name);
873
874 if !keypath.exists() {
875 eyre::bail!("Keystore file does not exist at {}", keypath.display());
876 }
877
878 let password = if let Some(password) = unsafe_password {
879 password
880 } else {
881 rpassword::prompt_password("Enter password: ")?
883 };
884
885 let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
886
887 let private_key = B256::from_slice(&wallet.credential().to_bytes());
888
889 let success_message =
890 format!("{}'s private key is: {}", &account_name, private_key);
891
892 sh_println!("{}", success_message.green())?;
893 }
894 Self::ChangePassword {
895 account_name,
896 keystore_dir,
897 unsafe_password,
898 unsafe_new_password,
899 } => {
900 let dir = if let Some(path) = keystore_dir {
902 Path::new(&path).to_path_buf()
903 } else {
904 Config::foundry_keystores_dir().ok_or_else(|| {
905 eyre::eyre!("Could not find the default keystore directory.")
906 })?
907 };
908
909 let keypath = dir.join(&account_name);
910
911 if !keypath.exists() {
912 eyre::bail!("Keystore file does not exist at {}", keypath.display());
913 }
914
915 let current_password = if let Some(password) = unsafe_password {
916 password
917 } else {
918 rpassword::prompt_password("Enter current password: ")?
920 };
921
922 let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
924 .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
925
926 let new_password = if let Some(password) = unsafe_new_password {
927 password
928 } else {
929 rpassword::prompt_password("Enter new password: ")?
931 };
932
933 if current_password == new_password {
934 eyre::bail!("New password cannot be the same as the current password");
935 }
936
937 let private_key = wallet.credential().to_bytes();
939 let mut rng = thread_rng();
940 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
941 dir,
942 &mut rng,
943 private_key,
944 new_password,
945 Some(&account_name),
946 )?;
947
948 let success_message = format!(
949 "Password for keystore `{}` was changed successfully. Address: {:?}",
950 &account_name,
951 wallet.address(),
952 );
953 sh_println!("{}", success_message.green())?;
954 }
955 };
956
957 Ok(())
958 }
959
960 fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
964 let message = Self::hex_str_to_bytes(message)?;
965 Ok(signature.recover_address_from_msg(message)?)
966 }
967
968 fn recover_address_from_message_no_hash(
970 prehash: &B256,
971 signature: &Signature,
972 ) -> Result<Address> {
973 Ok(signature.recover_address_from_prehash(prehash)?)
974 }
975
976 fn recover_address_from_typed_data(
978 typed_data: &TypedData,
979 signature: &Signature,
980 ) -> Result<Address> {
981 Ok(signature.recover_address_from_prehash(&typed_data.eip712_signing_hash()?)?)
982 }
983
984 fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
988 Ok(match s.strip_prefix("0x") {
989 Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
990 None => s.as_bytes().to_vec(),
991 })
992 }
993}
994
995#[cfg(test)]
996mod tests {
997 use super::*;
998 use alloy_primitives::{address, keccak256};
999 use std::str::FromStr;
1000
1001 #[test]
1002 fn can_parse_wallet_sign_message() {
1003 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
1004 match args {
1005 WalletSubcommands::Sign { message, data, from_file, .. } => {
1006 assert_eq!(message, "deadbeef".to_string());
1007 assert!(!data);
1008 assert!(!from_file);
1009 }
1010 _ => panic!("expected WalletSubcommands::Sign"),
1011 }
1012 }
1013
1014 #[test]
1015 fn can_parse_wallet_sign_hex_message() {
1016 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
1017 match args {
1018 WalletSubcommands::Sign { message, data, from_file, .. } => {
1019 assert_eq!(message, "0xdeadbeef".to_string());
1020 assert!(!data);
1021 assert!(!from_file);
1022 }
1023 _ => panic!("expected WalletSubcommands::Sign"),
1024 }
1025 }
1026
1027 #[test]
1028 fn can_verify_signed_hex_message() {
1029 let message = "hello";
1030 let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
1031 let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
1032 let recovered_address =
1033 WalletSubcommands::recover_address_from_message(message, &signature);
1034 assert!(recovered_address.is_ok());
1035 assert_eq!(address, recovered_address.unwrap());
1036 }
1037
1038 #[test]
1039 fn can_verify_signed_hex_message_no_hash() {
1040 let prehash = keccak256("hello");
1041 let signature = Signature::from_str("433ec3d37e4f1253df15e2dea412fed8e915737730f74b3dfb1353268f932ef5557c9158e0b34bce39de28d11797b42e9b1acb2749230885fe075aedc3e491a41b").unwrap();
1042 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
1044 WalletSubcommands::recover_address_from_message_no_hash(&prehash, &signature);
1045 assert!(recovered_address.is_ok());
1046 assert_eq!(address, recovered_address.unwrap());
1047 }
1048
1049 #[test]
1050 fn can_verify_signed_typed_data() {
1051 let typed_data: TypedData = serde_json::from_str(r#"{"domain":{"name":"Test","version":"1","chainId":1,"verifyingContract":"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"},"message":{"value":123},"primaryType":"Data","types":{"Data":[{"name":"value","type":"uint256"}]}}"#).unwrap();
1052 let signature = Signature::from_str("0285ff83b93bd01c14e201943af7454fe2bc6c98be707a73888c397d6ae3b0b92f73ca559f81cbb19fe4e0f1dc4105bd7b647c6a84b033057977cf2ec982daf71b").unwrap();
1053 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
1055 WalletSubcommands::recover_address_from_typed_data(&typed_data, &signature);
1056 assert!(recovered_address.is_ok());
1057 assert_eq!(address, recovered_address.unwrap());
1058 }
1059
1060 #[test]
1061 fn can_parse_wallet_sign_data() {
1062 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
1063 match args {
1064 WalletSubcommands::Sign { message, data, from_file, .. } => {
1065 assert_eq!(message, "{ ... }".to_string());
1066 assert!(data);
1067 assert!(!from_file);
1068 }
1069 _ => panic!("expected WalletSubcommands::Sign"),
1070 }
1071 }
1072
1073 #[test]
1074 fn can_parse_wallet_sign_data_file() {
1075 let args = WalletSubcommands::parse_from([
1076 "foundry-cli",
1077 "sign",
1078 "--data",
1079 "--from-file",
1080 "tests/data/typed_data.json",
1081 ]);
1082 match args {
1083 WalletSubcommands::Sign { message, data, from_file, .. } => {
1084 assert_eq!(message, "tests/data/typed_data.json".to_string());
1085 assert!(data);
1086 assert!(from_file);
1087 }
1088 _ => panic!("expected WalletSubcommands::Sign"),
1089 }
1090 }
1091
1092 #[test]
1093 fn can_parse_wallet_change_password() {
1094 let args = WalletSubcommands::parse_from([
1095 "foundry-cli",
1096 "change-password",
1097 "my_account",
1098 "--unsafe-password",
1099 "old_password",
1100 "--unsafe-new-password",
1101 "new_password",
1102 ]);
1103 match args {
1104 WalletSubcommands::ChangePassword {
1105 account_name,
1106 keystore_dir,
1107 unsafe_password,
1108 unsafe_new_password,
1109 } => {
1110 assert_eq!(account_name, "my_account".to_string());
1111 assert_eq!(unsafe_password, Some("old_password".to_string()));
1112 assert_eq!(unsafe_new_password, Some("new_password".to_string()));
1113 assert!(keystore_dir.is_none());
1114 }
1115 _ => panic!("expected WalletSubcommands::ChangePassword"),
1116 }
1117 }
1118
1119 #[test]
1120 fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1121 let result = WalletSubcommands::try_parse_from([
1122 "foundry-cli",
1123 "sign-auth",
1124 "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1125 "--nonce",
1126 "42",
1127 "--self-broadcast",
1128 ]);
1129 assert!(
1130 result.is_err(),
1131 "expected error when both --nonce and --self-broadcast are provided"
1132 );
1133 }
1134}