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