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::{
14 json::{print_json_success, print_scalar},
15 opts::RpcOpts,
16 utils,
17 utils::LoadConfig,
18};
19use foundry_common::{fs, sh_println, shell};
20use foundry_config::Config;
21use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner};
22use rand_08::thread_rng;
23use serde_json::json;
24use std::path::Path;
25use yansi::Paint;
26
27pub mod vanity;
28use vanity::VanityArgs;
29
30pub mod list;
31use list::ListArgs;
32
33mod process_tree;
34
35pub mod session;
36use session::SessionArgs;
37
38#[derive(Debug, Parser)]
40pub enum WalletSubcommands {
41 #[command(visible_alias = "n")]
43 New {
44 path: Option<String>,
46
47 #[arg(value_name = "ACCOUNT_NAME")]
50 account_name: Option<String>,
51
52 #[arg(long, short, conflicts_with = "unsafe_password")]
56 password: bool,
57
58 #[arg(long, env = "CAST_PASSWORD", value_name = "PASSWORD")]
62 unsafe_password: Option<String>,
63
64 #[arg(long, short, default_value = "1")]
66 number: u32,
67
68 #[arg(long)]
70 force: bool,
71 },
72
73 #[command(visible_alias = "nm")]
75 NewMnemonic {
76 #[arg(long, short, default_value = "12")]
78 words: usize,
79
80 #[arg(long, short, default_value = "1")]
82 accounts: u8,
83
84 #[arg(long, short, conflicts_with = "words")]
86 entropy: Option<String>,
87 },
88
89 #[command(visible_alias = "va")]
91 Vanity(VanityArgs),
92
93 #[command(visible_aliases = &["a", "addr"])]
95 Address {
96 #[arg(value_name = "PRIVATE_KEY")]
98 private_key_override: Option<String>,
99
100 #[command(flatten)]
101 wallet: WalletOpts,
102 },
103
104 #[command(visible_alias = "d")]
106 Derive {
107 #[arg(value_name = "MNEMONIC")]
109 mnemonic: String,
110
111 #[arg(long, short, default_value = "1")]
113 accounts: Option<u8>,
114
115 #[arg(long, default_value = "false")]
117 insecure: bool,
118 },
119
120 #[command(visible_alias = "s")]
122 Sign {
123 message: String,
137
138 #[arg(long)]
140 data: bool,
141
142 #[arg(long, requires = "data")]
144 from_file: bool,
145
146 #[arg(long, conflicts_with = "data")]
148 no_hash: bool,
149
150 #[command(flatten)]
151 wallet: WalletOpts,
152 },
153
154 #[command(visible_alias = "sa")]
156 SignAuth {
157 address: Address,
159
160 #[command(flatten)]
161 rpc: RpcOpts,
162
163 #[arg(long)]
164 nonce: Option<u64>,
165
166 #[arg(long)]
167 chain: Option<Chain>,
168
169 #[arg(long, conflicts_with = "nonce")]
173 self_broadcast: bool,
174
175 #[command(flatten)]
176 wallet: WalletOpts,
177 },
178
179 #[command(visible_alias = "v")]
181 Verify {
182 message: String,
196
197 signature: Signature,
199
200 #[arg(long, short)]
202 address: Address,
203
204 #[arg(long)]
206 data: bool,
207
208 #[arg(long, requires = "data")]
210 from_file: bool,
211
212 #[arg(long, conflicts_with = "data")]
214 no_hash: bool,
215 },
216
217 #[command(visible_alias = "i")]
219 Import {
220 #[arg(value_name = "ACCOUNT_NAME")]
222 account_name: String,
223 #[arg(long, short)]
226 keystore_dir: Option<String>,
227 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
230 unsafe_password: Option<String>,
231 #[command(flatten)]
232 raw_wallet_options: RawWalletOpts,
233 },
234
235 #[command(visible_alias = "ls")]
237 List(ListArgs),
238
239 Session(SessionArgs),
241
242 #[command(visible_aliases = &["rm"], override_usage = "cast wallet remove --name <NAME>")]
247 Remove {
248 #[arg(long, required = true)]
250 name: String,
251 #[arg(long)]
254 dir: Option<String>,
255 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
258 unsafe_password: Option<String>,
259 },
260
261 #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])]
263 PrivateKey {
264 #[arg(value_name = "MNEMONIC")]
266 mnemonic_override: Option<String>,
267
268 #[arg(value_name = "MNEMONIC_INDEX_OR_DERIVATION_PATH")]
271 mnemonic_index_or_derivation_path_override: Option<String>,
272
273 #[command(flatten)]
274 wallet: WalletOpts,
275 },
276 #[command(visible_aliases = &["pubkey"])]
278 PublicKey {
279 #[arg(long = "raw-private-key", value_name = "PRIVATE_KEY")]
281 private_key_override: Option<String>,
282
283 #[command(flatten)]
284 wallet: WalletOpts,
285 },
286 #[command(name = "decrypt-keystore", visible_alias = "dk")]
288 DecryptKeystore {
289 #[arg(value_name = "ACCOUNT_NAME")]
291 account_name: String,
292 #[arg(long, short)]
295 keystore_dir: Option<String>,
296 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
299 unsafe_password: Option<String>,
300 },
301
302 #[command(name = "change-password", visible_alias = "cp")]
304 ChangePassword {
305 #[arg(value_name = "ACCOUNT_NAME")]
307 account_name: String,
308 #[arg(long, short)]
311 keystore_dir: Option<String>,
312 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
315 unsafe_password: Option<String>,
316 #[arg(long, env = "CAST_UNSAFE_NEW_PASSWORD", value_name = "NEW_PASSWORD")]
319 unsafe_new_password: Option<String>,
320 },
321}
322
323impl WalletSubcommands {
324 pub async fn run(self) -> Result<()> {
327 match self {
328 Self::New { path, account_name, unsafe_password, number, password, force } => {
329 let mut rng = thread_rng();
330
331 let mut json_values = shell::is_json().then(std::vec::Vec::new);
332
333 let path = if let Some(path) = path {
334 match dunce::canonicalize(&path) {
335 Ok(path) => {
336 if !path.is_dir() {
337 eyre::bail!("`{}` is not a directory", path.display());
339 }
340 Some(path)
341 }
342 Err(e) => {
343 eyre::bail!(
344 "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: {}",
345 e
346 );
347 }
348 }
349 } else if unsafe_password.is_some() || password {
350 let path = Config::foundry_keystores_dir().ok_or_else(|| {
351 eyre::eyre!("Could not find the default keystore directory.")
352 })?;
353 fs::create_dir_all(&path)?;
354 Some(path)
355 } else {
356 None
357 };
358
359 match path {
360 Some(path) => {
361 let password = if let Some(password) = unsafe_password {
362 password
363 } else {
364 rpassword::prompt_password("Enter secret: ")?
366 };
367
368 if !force && let Some(ref acc_name) = account_name {
370 let mut existing_files = Vec::new();
371
372 for i in 0..number {
373 let name = match number {
374 1 => acc_name.clone(),
375 _ => format!("{}_{}", acc_name, i + 1),
376 };
377 let file_path = path.join(&name);
378 if file_path.exists() {
379 existing_files.push(name);
380 }
381 }
382
383 if !existing_files.is_empty() {
384 use std::io::Write;
385
386 sh_eprintln!("The following keystore file(s) already exist:")?;
387 for file in &existing_files {
388 sh_eprintln!(" - {file}")?;
389 }
390 sh_eprint!(
391 "\nDo you want to overwrite all {} file(s)? [y/N]: ",
392 existing_files.len()
393 )?;
394 std::io::stderr().flush()?;
395
396 let mut input = String::new();
397 std::io::stdin().read_line(&mut input)?;
398
399 if !input.trim().eq_ignore_ascii_case("y") {
400 eyre::bail!("Operation cancelled. No keystores were modified.");
401 }
402 }
403 }
404 for i in 0..number {
405 let account_name_ref =
406 account_name.as_deref().map(|name| match number {
407 1 => name.to_string(),
408 _ => format!("{}_{}", name, i + 1),
409 });
410
411 let (wallet, uuid) = PrivateKeySigner::new_keystore(
412 &path,
413 &mut rng,
414 password.clone(),
415 account_name_ref.as_deref(),
416 )?;
417 let identifier = account_name_ref.as_deref().unwrap_or(&uuid);
418
419 if let Some(json) = json_values.as_mut() {
420 json.push(json!({
421 "address": wallet.address().to_checksum(None),
422 "public_key": format!("0x{}", hex::encode(wallet.public_key())),
423 "path": format!("{}", path.join(identifier).display()),
424 }));
425 } else {
426 sh_status!(
427 "Created new encrypted keystore file: {}",
428 path.join(identifier).display()
429 )?;
430 sh_status!("Address: {}", wallet.address().to_checksum(None))?;
431 if shell::verbosity() > 0 {
432 sh_status!(
433 "Public key: 0x{}",
434 hex::encode(wallet.public_key())
435 )?;
436 }
437 sh_println!("{}", wallet.address().to_checksum(None))?;
438 }
439 }
440 }
441 None => {
442 for _ in 0..number {
443 let wallet = PrivateKeySigner::random_with(&mut rng);
444
445 if let Some(json) = json_values.as_mut() {
446 json.push(json!({
447 "address": wallet.address().to_checksum(None),
448 "public_key": format!("0x{}", hex::encode(wallet.public_key())),
449 "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
450 }));
451 } else {
452 sh_status!("Successfully created new keypair.")?;
453 sh_status!("Address: {}", wallet.address().to_checksum(None))?;
454 if shell::verbosity() > 0 {
455 sh_status!(
456 "Public key: 0x{}",
457 hex::encode(wallet.public_key())
458 )?;
459 }
460 sh_status!(
461 "Private key: 0x{}",
462 hex::encode(wallet.credential().to_bytes())
463 )?;
464 sh_println!(
465 "{}\t0x{}",
466 wallet.address().to_checksum(None),
467 hex::encode(wallet.credential().to_bytes())
468 )?;
469 }
470 }
471 }
472 }
473
474 if let Some(json) = json_values {
475 print_json_success(json)?;
476 }
477 }
478 Self::NewMnemonic { words, accounts, entropy } => {
479 let phrase = if let Some(entropy) = entropy {
480 let entropy = Entropy::from_slice(hex::decode(entropy)?)?;
481 Mnemonic::<English>::new_from_entropy(entropy).to_phrase()
482 } else {
483 let mut rng = thread_rng();
484 Mnemonic::<English>::new_with_count(&mut rng, words)?.to_phrase()
485 };
486
487 let format_json = shell::is_json();
488
489 if !format_json {
490 sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?;
491 }
492
493 let builder = MnemonicBuilder::<English>::default().phrase(phrase.as_str());
494 let derivation_path = "m/44'/60'/0'/0/";
495 let wallets = (0..accounts)
496 .map(|i| builder.clone().derivation_path(format!("{derivation_path}{i}")))
497 .collect::<Result<Vec<_>, _>>()?;
498 let wallets =
499 wallets.into_iter().map(|b| b.build()).collect::<Result<Vec<_>, _>>()?;
500
501 if !format_json {
502 sh_println!("{}", "Successfully generated a new mnemonic.".green())?;
503 sh_println!("Phrase:\n{phrase}")?;
504 sh_println!("\nAccounts:")?;
505 }
506
507 let mut accounts = json!([]);
508 for (i, wallet) in wallets.iter().enumerate() {
509 let public_key = hex::encode(wallet.public_key());
510 let private_key = hex::encode(wallet.credential().to_bytes());
511 if format_json {
512 accounts.as_array_mut().unwrap().push(if shell::verbosity() > 0 {
513 json!({
514 "address": format!("{}", wallet.address()),
515 "public_key": format!("0x{}", public_key),
516 "private_key": format!("0x{}", private_key),
517 })
518 } else {
519 json!({
520 "address": format!("{}", wallet.address()),
521 "private_key": format!("0x{}", private_key),
522 })
523 });
524 } else {
525 sh_println!("- Account {i}:")?;
526 sh_println!("Address: {}", wallet.address())?;
527 if shell::verbosity() > 0 {
528 sh_println!("Public key: 0x{}", public_key)?;
529 }
530 sh_println!("Private key: 0x{}\n", private_key)?;
531 }
532 }
533
534 if format_json {
535 print_json_success(json!({
536 "mnemonic": phrase,
537 "accounts": accounts,
538 }))?;
539 }
540 }
541 Self::Vanity(cmd) => {
542 cmd.run()?;
543 }
544 Self::Address { wallet, private_key_override } => {
545 let wallet = private_key_override
546 .map(|pk| WalletOpts {
547 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
548 ..Default::default()
549 })
550 .unwrap_or(wallet)
551 .signer()
552 .await?;
553 let addr = wallet.address();
554 print_scalar(addr.to_checksum(None))?;
555 }
556 Self::Derive { mnemonic, accounts, insecure } => {
557 let format_json = shell::is_json();
558 let mut accounts_json = json!([]);
559 for i in 0..accounts.unwrap_or(1) {
560 let wallet = WalletOpts {
561 raw: RawWalletOpts {
562 mnemonic: Some(mnemonic.clone()),
563 mnemonic_index: i as u32,
564 ..Default::default()
565 },
566 ..Default::default()
567 }
568 .signer()
569 .await?;
570
571 match wallet {
572 WalletSigner::Local(local_wallet) => {
573 let address = local_wallet.address().to_checksum(None);
574 let private_key = hex::encode(local_wallet.credential().to_bytes());
575 if format_json {
576 if insecure {
577 accounts_json.as_array_mut().unwrap().push(json!({
578 "address": address.clone(),
579 "private_key": format!("0x{}", private_key),
580 }));
581 } else {
582 accounts_json.as_array_mut().unwrap().push(json!({
583 "address": address.clone()
584 }));
585 }
586 } else {
587 sh_println!("- Account {i}:")?;
588 if insecure {
589 sh_println!("Address: {}", address)?;
590 sh_println!("Private key: 0x{}\n", private_key)?;
591 } else {
592 sh_println!("Address: {}\n", address)?;
593 }
594 }
595 }
596 _ => {
597 eyre::bail!("Only local wallets are supported by this command");
598 }
599 }
600 }
601
602 if format_json {
603 print_json_success(accounts_json)?;
604 }
605 }
606 Self::PublicKey { wallet, private_key_override } => {
607 let wallet = private_key_override
608 .map(|pk| WalletOpts {
609 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
610 ..Default::default()
611 })
612 .unwrap_or(wallet)
613 .signer()
614 .await?;
615
616 let public_key = match wallet {
617 WalletSigner::Local(wallet) => wallet.public_key(),
618 _ => {
619 eyre::bail!("Only local wallets are supported by this command");
620 }
621 };
622
623 print_scalar(format!("0x{}", hex::encode(public_key)))?;
624 }
625 Self::Sign { message, data, from_file, no_hash, wallet } => {
626 let wallet = wallet.signer().await?;
627 let sig = if data {
628 let typed_data: TypedData = if from_file {
629 foundry_common::fs::read_json_file(message.as_ref())?
631 } else {
632 serde_json::from_str(&message)?
634 };
635 wallet.sign_dynamic_typed_data(&typed_data).await?
636 } else if no_hash {
637 wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
638 } else {
639 wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
640 };
641
642 if shell::verbosity() > 0 {
643 if shell::is_json() {
644 print_json_success(json!({
645 "message": message,
646 "address": wallet.address(),
647 "signature": hex::encode(sig.as_bytes()),
648 }))?;
649 } else {
650 sh_status!("Successfully signed!")?;
651 sh_status!(" Message: {message}")?;
652 sh_status!(" Address: {}", wallet.address())?;
653 sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
654 }
655 } else {
656 print_scalar(format!("0x{}", hex::encode(sig.as_bytes())))?;
657 }
658 }
659 Self::SignAuth { rpc, nonce, chain, wallet, address, self_broadcast } => {
660 let wallet = wallet.signer().await?;
661 let provider = utils::get_provider(&rpc.load_config()?)?;
662 let nonce = if let Some(nonce) = nonce {
663 nonce
664 } else {
665 let current_nonce = provider.get_transaction_count(wallet.address()).await?;
666 if self_broadcast {
667 current_nonce + 1
670 } else {
671 current_nonce
672 }
673 };
674 let chain_id = if let Some(chain) = chain {
675 chain.id()
676 } else {
677 provider.get_chain_id().await?
678 };
679 let auth = Authorization { chain_id: U256::from(chain_id), address, nonce };
680 let signature = wallet.sign_hash(&auth.signature_hash()).await?;
681 let auth = auth.into_signed(signature);
682
683 if shell::verbosity() > 0 {
684 if shell::is_json() {
685 print_json_success(json!({
686 "nonce": nonce,
687 "chain_id": chain_id,
688 "address": wallet.address(),
689 "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
690 }))?;
691 } else {
692 sh_status!("Successfully signed!")?;
693 sh_status!(" Nonce: {nonce}")?;
694 sh_status!(" Chain ID: {chain_id}")?;
695 sh_status!(" Address: {}", wallet.address())?;
696 sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
697 }
698 } else {
699 print_scalar(hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
700 }
701 }
702 Self::Verify { message, signature, address, data, from_file, no_hash } => {
703 let recovered_address = if data {
704 let typed_data: TypedData = if from_file {
705 foundry_common::fs::read_json_file(message.as_ref())?
707 } else {
708 serde_json::from_str(&message)?
710 };
711 Self::recover_address_from_typed_data(&typed_data, &signature)?
712 } else if no_hash {
713 Self::recover_address_from_message_no_hash(
714 &hex::decode(&message)?[..].try_into()?,
715 &signature,
716 )?
717 } else {
718 Self::recover_address_from_message(&message, &signature)?
719 };
720
721 if address == recovered_address {
722 if shell::is_json() {
723 print_json_success(json!({"address": address, "result": true}))?;
724 } else {
725 sh_println!(
726 "Validation succeeded. Address {address} signed this message."
727 )?;
728 }
729 } else {
730 eyre::bail!("Validation failed. Address {address} did not sign this message.");
731 }
732 }
733 Self::Import { account_name, keystore_dir, unsafe_password, raw_wallet_options } => {
734 let dir = if let Some(path) = keystore_dir {
736 Path::new(&path).to_path_buf()
737 } else {
738 Config::foundry_keystores_dir().ok_or_else(|| {
739 eyre::eyre!("Could not find the default keystore directory.")
740 })?
741 };
742
743 fs::create_dir_all(&dir)?;
744
745 let keystore_path = Path::new(&dir).join(&account_name);
747 if keystore_path.exists() {
748 eyre::bail!("Keystore file already exists at {}", keystore_path.display());
749 }
750
751 let wallet = raw_wallet_options
753 .signer()?
754 .and_then(|s| match s {
755 WalletSigner::Local(s) => Some(s),
756 _ => None,
757 })
758 .ok_or_else(|| {
759 eyre::eyre!(
760 "\
761Did you set a private key or mnemonic?
762Run `cast wallet import --help` and use the corresponding CLI
763flag to set your key via:
764--private-key, --mnemonic-path or --interactive."
765 )
766 })?;
767
768 let private_key = wallet.credential().to_bytes();
769 let password = if let Some(password) = unsafe_password {
770 password
771 } else {
772 rpassword::prompt_password("Enter password: ")?
774 };
775
776 let mut rng = thread_rng();
777 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
778 dir,
779 &mut rng,
780 private_key,
781 password,
782 Some(&account_name),
783 )?;
784 let address = wallet.address();
785 if shell::is_json() {
786 print_json_success(json!({"account": account_name, "address": address}))?;
787 } else {
788 sh_println!(
789 "{}",
790 format!(
791 "`{account_name}` keystore was saved successfully. Address: {address:?}"
792 )
793 .green()
794 )?;
795 }
796 }
797 Self::List(cmd) => {
798 cmd.run().await?;
799 }
800 Self::Session(args) => {
801 args.run().await?;
802 }
803 Self::Remove { name, dir, unsafe_password } => {
804 let dir = if let Some(path) = dir {
805 Path::new(&path).to_path_buf()
806 } else {
807 Config::foundry_keystores_dir().ok_or_else(|| {
808 eyre::eyre!("Could not find the default keystore directory.")
809 })?
810 };
811
812 let keystore_path = Path::new(&dir).join(&name);
813 if !keystore_path.exists() {
814 eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
815 }
816
817 let password = if let Some(pwd) = unsafe_password {
818 pwd
819 } else {
820 rpassword::prompt_password("Enter password: ")?
821 };
822
823 if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
824 eyre::bail!("Invalid password - wallet removal cancelled");
825 }
826
827 std::fs::remove_file(&keystore_path).wrap_err_with(|| {
828 format!("Failed to remove keystore file at {}", keystore_path.display())
829 })?;
830
831 if shell::is_json() {
832 print_json_success(json!({"account": name, "removed": true}))?;
833 } else {
834 sh_println!(
835 "{}",
836 format!("`{name}` keystore was removed successfully.").green()
837 )?;
838 }
839 }
840 Self::PrivateKey {
841 wallet,
842 mnemonic_override,
843 mnemonic_index_or_derivation_path_override,
844 } => {
845 let (index_override, derivation_path_override) =
846 match mnemonic_index_or_derivation_path_override {
847 Some(value) => match value.parse::<u32>() {
848 Ok(index) => (Some(index), None),
849 Err(_) => (None, Some(value)),
850 },
851 None => (None, None),
852 };
853 let wallet = WalletOpts {
854 raw: RawWalletOpts {
855 mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
856 mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
857 hd_path: derivation_path_override.or(wallet.raw.hd_path),
858 ..wallet.raw
859 },
860 ..wallet
861 }
862 .signer()
863 .await?;
864 match wallet {
865 WalletSigner::Local(wallet) => {
866 let private_key =
867 format!("0x{}", hex::encode(wallet.credential().to_bytes()));
868 if shell::verbosity() > 0 {
869 if shell::is_json() {
870 print_json_success(json!({
871 "address": wallet.address(),
872 "private_key": private_key,
873 }))?;
874 } else {
875 sh_println!("Address: {}", wallet.address())?;
876 sh_println!("Private key: {private_key}")?;
877 }
878 } else {
879 print_scalar(private_key)?;
880 }
881 }
882 _ => {
883 eyre::bail!("Only local wallets are supported by this command.");
884 }
885 }
886 }
887 Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
888 let dir = if let Some(path) = keystore_dir {
890 Path::new(&path).to_path_buf()
891 } else {
892 Config::foundry_keystores_dir().ok_or_else(|| {
893 eyre::eyre!("Could not find the default keystore directory.")
894 })?
895 };
896
897 let keypath = dir.join(&account_name);
898
899 if !keypath.exists() {
900 eyre::bail!("Keystore file does not exist at {}", keypath.display());
901 }
902
903 let password = if let Some(password) = unsafe_password {
904 password
905 } else {
906 rpassword::prompt_password("Enter password: ")?
908 };
909
910 let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
911
912 let private_key = B256::from_slice(&wallet.credential().to_bytes());
913 if shell::is_json() {
914 print_json_success(
915 json!({"account": account_name, "private_key": private_key}),
916 )?;
917 } else {
918 sh_println!(
919 "{}",
920 format!("{account_name}'s private key is: {private_key}").green()
921 )?;
922 }
923 }
924 Self::ChangePassword {
925 account_name,
926 keystore_dir,
927 unsafe_password,
928 unsafe_new_password,
929 } => {
930 let dir = if let Some(path) = keystore_dir {
932 Path::new(&path).to_path_buf()
933 } else {
934 Config::foundry_keystores_dir().ok_or_else(|| {
935 eyre::eyre!("Could not find the default keystore directory.")
936 })?
937 };
938
939 let keypath = dir.join(&account_name);
940
941 if !keypath.exists() {
942 eyre::bail!("Keystore file does not exist at {}", keypath.display());
943 }
944
945 let current_password = if let Some(password) = unsafe_password {
946 password
947 } else {
948 rpassword::prompt_password("Enter current password: ")?
950 };
951
952 let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
954 .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
955
956 let new_password = if let Some(password) = unsafe_new_password {
957 password
958 } else {
959 rpassword::prompt_password("Enter new password: ")?
961 };
962
963 if current_password == new_password {
964 eyre::bail!("New password cannot be the same as the current password");
965 }
966
967 let private_key = wallet.credential().to_bytes();
969 let mut rng = thread_rng();
970 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
971 dir,
972 &mut rng,
973 private_key,
974 new_password,
975 Some(&account_name),
976 )?;
977
978 let address = wallet.address();
979 if shell::is_json() {
980 print_json_success(json!({"account": account_name, "address": address}))?;
981 } else {
982 sh_println!(
983 "{}",
984 format!(
985 "Password for keystore `{account_name}` was changed successfully. Address: {address:?}"
986 )
987 .green()
988 )?;
989 }
990 }
991 };
992
993 Ok(())
994 }
995
996 fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
1000 let message = Self::hex_str_to_bytes(message)?;
1001 Ok(signature.recover_address_from_msg(message)?)
1002 }
1003
1004 fn recover_address_from_message_no_hash(
1006 prehash: &B256,
1007 signature: &Signature,
1008 ) -> Result<Address> {
1009 Ok(signature.recover_address_from_prehash(prehash)?)
1010 }
1011
1012 fn recover_address_from_typed_data(
1014 typed_data: &TypedData,
1015 signature: &Signature,
1016 ) -> Result<Address> {
1017 Ok(signature.recover_address_from_prehash(&typed_data.eip712_signing_hash()?)?)
1018 }
1019
1020 fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
1024 Ok(match s.strip_prefix("0x") {
1025 Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
1026 None => s.as_bytes().to_vec(),
1027 })
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::{session::SessionSubcommands, *};
1034 use alloy_primitives::{address, keccak256};
1035 use std::str::FromStr;
1036
1037 #[test]
1038 fn can_parse_wallet_sign_message() {
1039 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
1040 match args {
1041 WalletSubcommands::Sign { message, data, from_file, .. } => {
1042 assert_eq!(message, "deadbeef".to_string());
1043 assert!(!data);
1044 assert!(!from_file);
1045 }
1046 _ => panic!("expected WalletSubcommands::Sign"),
1047 }
1048 }
1049
1050 #[test]
1051 fn can_parse_wallet_sign_hex_message() {
1052 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
1053 match args {
1054 WalletSubcommands::Sign { message, data, from_file, .. } => {
1055 assert_eq!(message, "0xdeadbeef".to_string());
1056 assert!(!data);
1057 assert!(!from_file);
1058 }
1059 _ => panic!("expected WalletSubcommands::Sign"),
1060 }
1061 }
1062
1063 #[test]
1064 fn can_verify_signed_hex_message() {
1065 let message = "hello";
1066 let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
1067 let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
1068 let recovered_address =
1069 WalletSubcommands::recover_address_from_message(message, &signature);
1070 assert!(recovered_address.is_ok());
1071 assert_eq!(address, recovered_address.unwrap());
1072 }
1073
1074 #[test]
1075 fn can_verify_signed_hex_message_no_hash() {
1076 let prehash = keccak256("hello");
1077 let signature = Signature::from_str("433ec3d37e4f1253df15e2dea412fed8e915737730f74b3dfb1353268f932ef5557c9158e0b34bce39de28d11797b42e9b1acb2749230885fe075aedc3e491a41b").unwrap();
1078 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
1080 WalletSubcommands::recover_address_from_message_no_hash(&prehash, &signature);
1081 assert!(recovered_address.is_ok());
1082 assert_eq!(address, recovered_address.unwrap());
1083 }
1084
1085 #[test]
1086 fn can_verify_signed_typed_data() {
1087 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();
1088 let signature = Signature::from_str("0285ff83b93bd01c14e201943af7454fe2bc6c98be707a73888c397d6ae3b0b92f73ca559f81cbb19fe4e0f1dc4105bd7b647c6a84b033057977cf2ec982daf71b").unwrap();
1089 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
1091 WalletSubcommands::recover_address_from_typed_data(&typed_data, &signature);
1092 assert!(recovered_address.is_ok());
1093 assert_eq!(address, recovered_address.unwrap());
1094 }
1095
1096 #[test]
1097 fn can_parse_wallet_sign_data() {
1098 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
1099 match args {
1100 WalletSubcommands::Sign { message, data, from_file, .. } => {
1101 assert_eq!(message, "{ ... }".to_string());
1102 assert!(data);
1103 assert!(!from_file);
1104 }
1105 _ => panic!("expected WalletSubcommands::Sign"),
1106 }
1107 }
1108
1109 #[test]
1110 fn can_parse_wallet_sign_data_file() {
1111 let args = WalletSubcommands::parse_from([
1112 "foundry-cli",
1113 "sign",
1114 "--data",
1115 "--from-file",
1116 "tests/data/typed_data.json",
1117 ]);
1118 match args {
1119 WalletSubcommands::Sign { message, data, from_file, .. } => {
1120 assert_eq!(message, "tests/data/typed_data.json".to_string());
1121 assert!(data);
1122 assert!(from_file);
1123 }
1124 _ => panic!("expected WalletSubcommands::Sign"),
1125 }
1126 }
1127
1128 #[test]
1129 fn can_parse_wallet_change_password() {
1130 let args = WalletSubcommands::parse_from([
1131 "foundry-cli",
1132 "change-password",
1133 "my_account",
1134 "--unsafe-password",
1135 "old_password",
1136 "--unsafe-new-password",
1137 "new_password",
1138 ]);
1139 match args {
1140 WalletSubcommands::ChangePassword {
1141 account_name,
1142 keystore_dir,
1143 unsafe_password,
1144 unsafe_new_password,
1145 } => {
1146 assert_eq!(account_name, "my_account".to_string());
1147 assert_eq!(unsafe_password, Some("old_password".to_string()));
1148 assert_eq!(unsafe_new_password, Some("new_password".to_string()));
1149 assert!(keystore_dir.is_none());
1150 }
1151 _ => panic!("expected WalletSubcommands::ChangePassword"),
1152 }
1153 }
1154
1155 #[test]
1156 fn can_parse_wallet_session_create() {
1157 let args = WalletSubcommands::parse_from([
1158 "foundry-cli",
1159 "session",
1160 "create",
1161 "--root",
1162 "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
1163 "--chain-id",
1164 "4217",
1165 "--expires",
1166 "10m",
1167 "--scope",
1168 "0x20c0000000000000000000000000000000000001:transfer",
1169 "--spend-limit",
1170 "PathUSD=0",
1171 "--private-key",
1172 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0",
1173 ]);
1174
1175 match args {
1176 WalletSubcommands::Session(args) => match args.command {
1177 Some(SessionSubcommands::Create {
1178 root_account,
1179 chain_id,
1180 expires,
1181 scope,
1182 spend_limits,
1183 wallet,
1184 }) => {
1185 assert_eq!(
1186 root_account,
1187 address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf")
1188 );
1189 assert_eq!(chain_id, 4217);
1190 assert_eq!(expires, 600);
1191 assert_eq!(scope.len(), 1);
1192 assert_eq!(spend_limits.len(), 1);
1193 assert_eq!(
1194 wallet.raw.private_key.as_deref(),
1195 Some("0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0")
1196 );
1197 }
1198 _ => panic!("expected WalletSubcommands::Session::Create"),
1199 },
1200 _ => panic!("expected WalletSubcommands::Session"),
1201 }
1202 }
1203
1204 #[test]
1205 fn can_parse_wallet_session_revoke() {
1206 for (extra_args, expected_local) in [([].as_slice(), false), (["--local"].as_slice(), true)]
1207 {
1208 let args = WalletSubcommands::parse_from(
1209 [
1210 "foundry-cli",
1211 "session",
1212 "revoke",
1213 "0x1111111111111111111111111111111111111111111111111111111111111111",
1214 ]
1215 .into_iter()
1216 .chain(extra_args.iter().copied()),
1217 );
1218
1219 match args {
1220 WalletSubcommands::Session(args) => match args.command {
1221 Some(SessionSubcommands::Revoke { session_id, local, .. }) => {
1222 assert_eq!(session_id, B256::from([0x11; 32]));
1223 assert_eq!(local, expected_local);
1224 }
1225 _ => panic!("expected WalletSubcommands::Session::Revoke"),
1226 },
1227 _ => panic!("expected WalletSubcommands::Session"),
1228 }
1229 }
1230 }
1231
1232 #[test]
1233 fn can_parse_wallet_session_run_for_command() {
1234 let args = WalletSubcommands::parse_from([
1235 "foundry-cli",
1236 "session",
1237 "--root",
1238 "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
1239 "--chain-id",
1240 "4217",
1241 "--expires",
1242 "10m",
1243 "--target",
1244 "0x20c0000000000000000000000000000000000001",
1245 "--selector",
1246 "transfer(address,uint256)",
1247 "--spend-limit",
1248 "PathUSD=0",
1249 "--for",
1250 "forge script Deploy --broadcast",
1251 "--private-key",
1252 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0",
1253 ]);
1254
1255 match args {
1256 WalletSubcommands::Session(args) => {
1257 assert!(args.command.is_none());
1258 assert_eq!(
1259 args.root_account,
1260 Some(address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"))
1261 );
1262 assert_eq!(args.send_tx.eth.etherscan.chain.map(|chain| chain.id()), Some(4217));
1263 assert_eq!(args.expires, Some(600));
1264 assert_eq!(
1265 args.target,
1266 Some(address!("0x20c0000000000000000000000000000000000001"))
1267 );
1268 assert_eq!(args.selectors.len(), 1);
1269 assert_eq!(args.spend_limits.len(), 1);
1270 assert_eq!(args.for_command.as_deref(), Some("forge script Deploy --broadcast"));
1271 assert_eq!(
1272 args.send_tx.eth.wallet.raw.private_key.as_deref(),
1273 Some("0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0")
1274 );
1275 }
1276 _ => panic!("expected WalletSubcommands::Session"),
1277 }
1278 }
1279
1280 #[test]
1281 fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1282 let result = WalletSubcommands::try_parse_from([
1283 "foundry-cli",
1284 "sign-auth",
1285 "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1286 "--nonce",
1287 "42",
1288 "--self-broadcast",
1289 ]);
1290 assert!(
1291 result.is_err(),
1292 "expected error when both --nonce and --self-broadcast are provided"
1293 );
1294 }
1295}