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