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 _ => eyre::bail!("Only local wallets are supported by this command"),
597 }
598 }
599
600 if format_json {
601 print_json_success(accounts_json)?;
602 }
603 }
604 Self::PublicKey { wallet, private_key_override } => {
605 let wallet = private_key_override
606 .map(|pk| WalletOpts {
607 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
608 ..Default::default()
609 })
610 .unwrap_or(wallet)
611 .signer()
612 .await?;
613
614 let public_key = match wallet {
615 WalletSigner::Local(wallet) => wallet.public_key(),
616 _ => eyre::bail!("Only local wallets are supported by this command"),
617 };
618
619 print_scalar(format!("0x{}", hex::encode(public_key)))?;
620 }
621 Self::Sign { message, data, from_file, no_hash, wallet } => {
622 let wallet = wallet.signer().await?;
623 let sig = if data {
624 let typed_data: TypedData = if from_file {
625 foundry_common::fs::read_json_file(message.as_ref())?
627 } else {
628 serde_json::from_str(&message)?
630 };
631 wallet.sign_dynamic_typed_data(&typed_data).await?
632 } else if no_hash {
633 wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
634 } else {
635 wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
636 };
637
638 if shell::verbosity() > 0 {
639 if shell::is_json() {
640 print_json_success(json!({
641 "message": message,
642 "address": wallet.address(),
643 "signature": hex::encode(sig.as_bytes()),
644 }))?;
645 } else {
646 sh_status!("Successfully signed!")?;
647 sh_status!(" Message: {message}")?;
648 sh_status!(" Address: {}", wallet.address())?;
649 sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
650 }
651 } else {
652 print_scalar(format!("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 print_json_success(json!({
682 "nonce": nonce,
683 "chain_id": chain_id,
684 "address": wallet.address(),
685 "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
686 }))?;
687 } else {
688 sh_status!("Successfully signed!")?;
689 sh_status!(" Nonce: {nonce}")?;
690 sh_status!(" Chain ID: {chain_id}")?;
691 sh_status!(" Address: {}", wallet.address())?;
692 sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
693 }
694 } else {
695 print_scalar(hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
696 }
697 }
698 Self::Verify { message, signature, address, data, from_file, no_hash } => {
699 let recovered_address = if data {
700 let typed_data: TypedData = if from_file {
701 foundry_common::fs::read_json_file(message.as_ref())?
703 } else {
704 serde_json::from_str(&message)?
706 };
707 Self::recover_address_from_typed_data(&typed_data, &signature)?
708 } else if no_hash {
709 Self::recover_address_from_message_no_hash(
710 &hex::decode(&message)?[..].try_into()?,
711 &signature,
712 )?
713 } else {
714 Self::recover_address_from_message(&message, &signature)?
715 };
716
717 if address == recovered_address {
718 if shell::is_json() {
719 print_json_success(json!({"address": address, "result": true}))?;
720 } else {
721 sh_println!(
722 "Validation succeeded. Address {address} signed this message."
723 )?;
724 }
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 if shell::is_json() {
782 print_json_success(json!({"account": account_name, "address": address}))?;
783 } else {
784 sh_println!(
785 "{}",
786 format!(
787 "`{account_name}` keystore was saved successfully. Address: {address:?}"
788 )
789 .green()
790 )?;
791 }
792 }
793 Self::List(cmd) => {
794 cmd.run().await?;
795 }
796 Self::Session(args) => {
797 args.run().await?;
798 }
799 Self::Remove { name, dir, unsafe_password } => {
800 let dir = if let Some(path) = dir {
801 Path::new(&path).to_path_buf()
802 } else {
803 Config::foundry_keystores_dir().ok_or_else(|| {
804 eyre::eyre!("Could not find the default keystore directory.")
805 })?
806 };
807
808 let keystore_path = Path::new(&dir).join(&name);
809 if !keystore_path.exists() {
810 eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
811 }
812
813 let password = if let Some(pwd) = unsafe_password {
814 pwd
815 } else {
816 rpassword::prompt_password("Enter password: ")?
817 };
818
819 if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
820 eyre::bail!("Invalid password - wallet removal cancelled");
821 }
822
823 std::fs::remove_file(&keystore_path).wrap_err_with(|| {
824 format!("Failed to remove keystore file at {}", keystore_path.display())
825 })?;
826
827 if shell::is_json() {
828 print_json_success(json!({"account": name, "removed": true}))?;
829 } else {
830 sh_println!(
831 "{}",
832 format!("`{name}` keystore was removed successfully.").green()
833 )?;
834 }
835 }
836 Self::PrivateKey {
837 wallet,
838 mnemonic_override,
839 mnemonic_index_or_derivation_path_override,
840 } => {
841 let (index_override, derivation_path_override) =
842 match mnemonic_index_or_derivation_path_override {
843 Some(value) => match value.parse::<u32>() {
844 Ok(index) => (Some(index), None),
845 Err(_) => (None, Some(value)),
846 },
847 None => (None, None),
848 };
849 let wallet = WalletOpts {
850 raw: RawWalletOpts {
851 mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
852 mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
853 hd_path: derivation_path_override.or(wallet.raw.hd_path),
854 ..wallet.raw
855 },
856 ..wallet
857 }
858 .signer()
859 .await?;
860 match wallet {
861 WalletSigner::Local(wallet) => {
862 let private_key =
863 format!("0x{}", hex::encode(wallet.credential().to_bytes()));
864 if shell::verbosity() > 0 {
865 if shell::is_json() {
866 print_json_success(json!({
867 "address": wallet.address(),
868 "private_key": private_key,
869 }))?;
870 } else {
871 sh_println!("Address: {}", wallet.address())?;
872 sh_println!("Private key: {private_key}")?;
873 }
874 } else {
875 print_scalar(private_key)?;
876 }
877 }
878 _ => {
879 eyre::bail!("Only local wallets are supported by this command.");
880 }
881 }
882 }
883 Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
884 let dir = if let Some(path) = keystore_dir {
886 Path::new(&path).to_path_buf()
887 } else {
888 Config::foundry_keystores_dir().ok_or_else(|| {
889 eyre::eyre!("Could not find the default keystore directory.")
890 })?
891 };
892
893 let keypath = dir.join(&account_name);
894
895 if !keypath.exists() {
896 eyre::bail!("Keystore file does not exist at {}", keypath.display());
897 }
898
899 let password = if let Some(password) = unsafe_password {
900 password
901 } else {
902 rpassword::prompt_password("Enter password: ")?
904 };
905
906 let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
907
908 let private_key = B256::from_slice(&wallet.credential().to_bytes());
909 if shell::is_json() {
910 print_json_success(
911 json!({"account": account_name, "private_key": private_key}),
912 )?;
913 } else {
914 sh_println!(
915 "{}",
916 format!("{account_name}'s private key is: {private_key}").green()
917 )?;
918 }
919 }
920 Self::ChangePassword {
921 account_name,
922 keystore_dir,
923 unsafe_password,
924 unsafe_new_password,
925 } => {
926 let dir = if let Some(path) = keystore_dir {
928 Path::new(&path).to_path_buf()
929 } else {
930 Config::foundry_keystores_dir().ok_or_else(|| {
931 eyre::eyre!("Could not find the default keystore directory.")
932 })?
933 };
934
935 let keypath = dir.join(&account_name);
936
937 if !keypath.exists() {
938 eyre::bail!("Keystore file does not exist at {}", keypath.display());
939 }
940
941 let current_password = if let Some(password) = unsafe_password {
942 password
943 } else {
944 rpassword::prompt_password("Enter current password: ")?
946 };
947
948 let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
950 .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
951
952 let new_password = if let Some(password) = unsafe_new_password {
953 password
954 } else {
955 rpassword::prompt_password("Enter new password: ")?
957 };
958
959 if current_password == new_password {
960 eyre::bail!("New password cannot be the same as the current password");
961 }
962
963 let private_key = wallet.credential().to_bytes();
965 let mut rng = thread_rng();
966 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
967 dir,
968 &mut rng,
969 private_key,
970 new_password,
971 Some(&account_name),
972 )?;
973
974 let address = wallet.address();
975 if shell::is_json() {
976 print_json_success(json!({"account": account_name, "address": address}))?;
977 } else {
978 sh_println!(
979 "{}",
980 format!(
981 "Password for keystore `{account_name}` was changed successfully. Address: {address:?}"
982 )
983 .green()
984 )?;
985 }
986 }
987 };
988
989 Ok(())
990 }
991
992 fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
996 let message = Self::hex_str_to_bytes(message)?;
997 Ok(signature.recover_address_from_msg(message)?)
998 }
999
1000 fn recover_address_from_message_no_hash(
1002 prehash: &B256,
1003 signature: &Signature,
1004 ) -> Result<Address> {
1005 Ok(signature.recover_address_from_prehash(prehash)?)
1006 }
1007
1008 fn recover_address_from_typed_data(
1010 typed_data: &TypedData,
1011 signature: &Signature,
1012 ) -> Result<Address> {
1013 Ok(signature.recover_address_from_prehash(&typed_data.eip712_signing_hash()?)?)
1014 }
1015
1016 fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
1020 Ok(match s.strip_prefix("0x") {
1021 Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
1022 None => s.as_bytes().to_vec(),
1023 })
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::{session::SessionSubcommands, *};
1030 use alloy_primitives::{address, keccak256};
1031 use std::str::FromStr;
1032
1033 #[test]
1034 fn can_parse_wallet_sign_message() {
1035 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
1036 match args {
1037 WalletSubcommands::Sign { message, data, from_file, .. } => {
1038 assert_eq!(message, "deadbeef".to_string());
1039 assert!(!data);
1040 assert!(!from_file);
1041 }
1042 _ => panic!("expected WalletSubcommands::Sign"),
1043 }
1044 }
1045
1046 #[test]
1047 fn can_parse_wallet_sign_hex_message() {
1048 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
1049 match args {
1050 WalletSubcommands::Sign { message, data, from_file, .. } => {
1051 assert_eq!(message, "0xdeadbeef".to_string());
1052 assert!(!data);
1053 assert!(!from_file);
1054 }
1055 _ => panic!("expected WalletSubcommands::Sign"),
1056 }
1057 }
1058
1059 #[test]
1060 fn can_verify_signed_hex_message() {
1061 let message = "hello";
1062 let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
1063 let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
1064 let recovered_address =
1065 WalletSubcommands::recover_address_from_message(message, &signature);
1066 assert!(recovered_address.is_ok());
1067 assert_eq!(address, recovered_address.unwrap());
1068 }
1069
1070 #[test]
1071 fn can_verify_signed_hex_message_no_hash() {
1072 let prehash = keccak256("hello");
1073 let signature = Signature::from_str("433ec3d37e4f1253df15e2dea412fed8e915737730f74b3dfb1353268f932ef5557c9158e0b34bce39de28d11797b42e9b1acb2749230885fe075aedc3e491a41b").unwrap();
1074 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
1076 WalletSubcommands::recover_address_from_message_no_hash(&prehash, &signature);
1077 assert!(recovered_address.is_ok());
1078 assert_eq!(address, recovered_address.unwrap());
1079 }
1080
1081 #[test]
1082 fn can_verify_signed_typed_data() {
1083 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();
1084 let signature = Signature::from_str("0285ff83b93bd01c14e201943af7454fe2bc6c98be707a73888c397d6ae3b0b92f73ca559f81cbb19fe4e0f1dc4105bd7b647c6a84b033057977cf2ec982daf71b").unwrap();
1085 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
1087 WalletSubcommands::recover_address_from_typed_data(&typed_data, &signature);
1088 assert!(recovered_address.is_ok());
1089 assert_eq!(address, recovered_address.unwrap());
1090 }
1091
1092 #[test]
1093 fn can_parse_wallet_sign_data() {
1094 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
1095 match args {
1096 WalletSubcommands::Sign { message, data, from_file, .. } => {
1097 assert_eq!(message, "{ ... }".to_string());
1098 assert!(data);
1099 assert!(!from_file);
1100 }
1101 _ => panic!("expected WalletSubcommands::Sign"),
1102 }
1103 }
1104
1105 #[test]
1106 fn can_parse_wallet_sign_data_file() {
1107 let args = WalletSubcommands::parse_from([
1108 "foundry-cli",
1109 "sign",
1110 "--data",
1111 "--from-file",
1112 "tests/data/typed_data.json",
1113 ]);
1114 match args {
1115 WalletSubcommands::Sign { message, data, from_file, .. } => {
1116 assert_eq!(message, "tests/data/typed_data.json".to_string());
1117 assert!(data);
1118 assert!(from_file);
1119 }
1120 _ => panic!("expected WalletSubcommands::Sign"),
1121 }
1122 }
1123
1124 #[test]
1125 fn can_parse_wallet_change_password() {
1126 let args = WalletSubcommands::parse_from([
1127 "foundry-cli",
1128 "change-password",
1129 "my_account",
1130 "--unsafe-password",
1131 "old_password",
1132 "--unsafe-new-password",
1133 "new_password",
1134 ]);
1135 match args {
1136 WalletSubcommands::ChangePassword {
1137 account_name,
1138 keystore_dir,
1139 unsafe_password,
1140 unsafe_new_password,
1141 } => {
1142 assert_eq!(account_name, "my_account".to_string());
1143 assert_eq!(unsafe_password, Some("old_password".to_string()));
1144 assert_eq!(unsafe_new_password, Some("new_password".to_string()));
1145 assert!(keystore_dir.is_none());
1146 }
1147 _ => panic!("expected WalletSubcommands::ChangePassword"),
1148 }
1149 }
1150
1151 #[test]
1152 fn can_parse_wallet_session_create() {
1153 let args = WalletSubcommands::parse_from([
1154 "foundry-cli",
1155 "session",
1156 "create",
1157 "--root",
1158 "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
1159 "--chain-id",
1160 "4217",
1161 "--expires",
1162 "10m",
1163 "--scope",
1164 "0x20c0000000000000000000000000000000000001:transfer",
1165 "--spend-limit",
1166 "PathUSD=0",
1167 "--private-key",
1168 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0",
1169 ]);
1170
1171 match args {
1172 WalletSubcommands::Session(args) => match args.command {
1173 Some(SessionSubcommands::Create {
1174 root_account,
1175 chain_id,
1176 expires,
1177 scope,
1178 spend_limits,
1179 wallet,
1180 }) => {
1181 assert_eq!(
1182 root_account,
1183 address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf")
1184 );
1185 assert_eq!(chain_id, 4217);
1186 assert_eq!(expires, 600);
1187 assert_eq!(scope.len(), 1);
1188 assert_eq!(spend_limits.len(), 1);
1189 assert_eq!(
1190 wallet.raw.private_key.as_deref(),
1191 Some("0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0")
1192 );
1193 }
1194 _ => panic!("expected WalletSubcommands::Session::Create"),
1195 },
1196 _ => panic!("expected WalletSubcommands::Session"),
1197 }
1198 }
1199
1200 #[test]
1201 fn can_parse_wallet_session_revoke() {
1202 for (extra_args, expected_local) in [([].as_slice(), false), (["--local"].as_slice(), true)]
1203 {
1204 let args = WalletSubcommands::parse_from(
1205 [
1206 "foundry-cli",
1207 "session",
1208 "revoke",
1209 "0x1111111111111111111111111111111111111111111111111111111111111111",
1210 ]
1211 .into_iter()
1212 .chain(extra_args.iter().copied()),
1213 );
1214
1215 match args {
1216 WalletSubcommands::Session(args) => match args.command {
1217 Some(SessionSubcommands::Revoke { session_id, local, .. }) => {
1218 assert_eq!(session_id, B256::from([0x11; 32]));
1219 assert_eq!(local, expected_local);
1220 }
1221 _ => panic!("expected WalletSubcommands::Session::Revoke"),
1222 },
1223 _ => panic!("expected WalletSubcommands::Session"),
1224 }
1225 }
1226 }
1227
1228 #[test]
1229 fn can_parse_wallet_session_run_for_command() {
1230 let args = WalletSubcommands::parse_from([
1231 "foundry-cli",
1232 "session",
1233 "--root",
1234 "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
1235 "--chain-id",
1236 "4217",
1237 "--expires",
1238 "10m",
1239 "--target",
1240 "0x20c0000000000000000000000000000000000001",
1241 "--selector",
1242 "transfer(address,uint256)",
1243 "--spend-limit",
1244 "PathUSD=0",
1245 "--for",
1246 "forge script Deploy --broadcast",
1247 "--private-key",
1248 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0",
1249 ]);
1250
1251 match args {
1252 WalletSubcommands::Session(args) => {
1253 assert!(args.command.is_none());
1254 assert_eq!(
1255 args.root_account,
1256 Some(address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"))
1257 );
1258 assert_eq!(args.send_tx.eth.etherscan.chain.map(|chain| chain.id()), Some(4217));
1259 assert_eq!(args.expires, Some(600));
1260 assert_eq!(
1261 args.target,
1262 Some(address!("0x20c0000000000000000000000000000000000001"))
1263 );
1264 assert_eq!(args.selectors.len(), 1);
1265 assert_eq!(args.spend_limits.len(), 1);
1266 assert_eq!(args.for_command.as_deref(), Some("forge script Deploy --broadcast"));
1267 assert_eq!(
1268 args.send_tx.eth.wallet.raw.private_key.as_deref(),
1269 Some("0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0")
1270 );
1271 }
1272 _ => panic!("expected WalletSubcommands::Session"),
1273 }
1274 }
1275
1276 #[test]
1277 fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1278 let result = WalletSubcommands::try_parse_from([
1279 "foundry-cli",
1280 "sign-auth",
1281 "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1282 "--nonce",
1283 "42",
1284 "--self-broadcast",
1285 ]);
1286 assert!(
1287 result.is_err(),
1288 "expected error when both --nonce and --self-broadcast are provided"
1289 );
1290 }
1291}