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 = "s")]
92 Sign {
93 message: String,
107
108 #[arg(long)]
110 data: bool,
111
112 #[arg(long, requires = "data")]
114 from_file: bool,
115
116 #[arg(long, conflicts_with = "data")]
118 no_hash: bool,
119
120 #[command(flatten)]
121 wallet: WalletOpts,
122 },
123
124 #[command(visible_alias = "sa")]
126 SignAuth {
127 address: Address,
129
130 #[command(flatten)]
131 rpc: RpcOpts,
132
133 #[arg(long)]
134 nonce: Option<u64>,
135
136 #[arg(long)]
137 chain: Option<Chain>,
138
139 #[arg(long, conflicts_with = "nonce")]
143 self_broadcast: bool,
144
145 #[command(flatten)]
146 wallet: WalletOpts,
147 },
148
149 #[command(visible_alias = "v")]
151 Verify {
152 message: String,
166
167 signature: Signature,
169
170 #[arg(long, short)]
172 address: Address,
173
174 #[arg(long)]
176 data: bool,
177
178 #[arg(long, requires = "data")]
180 from_file: bool,
181
182 #[arg(long, conflicts_with = "data")]
184 no_hash: bool,
185 },
186
187 #[command(visible_alias = "i")]
189 Import {
190 #[arg(value_name = "ACCOUNT_NAME")]
192 account_name: String,
193 #[arg(long, short)]
196 keystore_dir: Option<String>,
197 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
200 unsafe_password: Option<String>,
201 #[command(flatten)]
202 raw_wallet_options: RawWalletOpts,
203 },
204
205 #[command(visible_alias = "ls")]
207 List(ListArgs),
208
209 #[command(visible_aliases = &["rm"], override_usage = "cast wallet remove --name <NAME>")]
214 Remove {
215 #[arg(long, required = true)]
217 name: String,
218 #[arg(long)]
221 dir: Option<String>,
222 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
225 unsafe_password: Option<String>,
226 },
227
228 #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])]
230 PrivateKey {
231 #[arg(value_name = "MNEMONIC")]
233 mnemonic_override: Option<String>,
234
235 #[arg(value_name = "MNEMONIC_INDEX_OR_DERIVATION_PATH")]
238 mnemonic_index_or_derivation_path_override: Option<String>,
239
240 #[command(flatten)]
241 wallet: WalletOpts,
242 },
243 #[command(visible_aliases = &["pubkey"])]
245 PublicKey {
246 #[arg(long = "raw-private-key", value_name = "PRIVATE_KEY")]
248 private_key_override: Option<String>,
249
250 #[command(flatten)]
251 wallet: WalletOpts,
252 },
253 #[command(name = "decrypt-keystore", visible_alias = "dk")]
255 DecryptKeystore {
256 #[arg(value_name = "ACCOUNT_NAME")]
258 account_name: String,
259 #[arg(long, short)]
262 keystore_dir: Option<String>,
263 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
266 unsafe_password: Option<String>,
267 },
268
269 #[command(name = "change-password", visible_alias = "cp")]
271 ChangePassword {
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 #[arg(long, env = "CAST_UNSAFE_NEW_PASSWORD", value_name = "NEW_PASSWORD")]
286 unsafe_new_password: Option<String>,
287 },
288}
289
290impl WalletSubcommands {
291 pub async fn run(self) -> Result<()> {
292 match self {
293 Self::New { path, account_name, unsafe_password, number, password } => {
294 let mut rng = thread_rng();
295
296 let mut json_values = if shell::is_json() { Some(vec![]) } else { None };
297
298 let path = if let Some(path) = path {
299 match dunce::canonicalize(&path) {
300 Ok(path) => {
301 if !path.is_dir() {
302 eyre::bail!("`{}` is not a directory", path.display());
304 }
305 Some(path)
306 }
307 Err(e) => {
308 eyre::bail!(
309 "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: {}",
310 e
311 );
312 }
313 }
314 } else if unsafe_password.is_some() || password {
315 let path = Config::foundry_keystores_dir().ok_or_else(|| {
316 eyre::eyre!("Could not find the default keystore directory.")
317 })?;
318 fs::create_dir_all(&path)?;
319 Some(path)
320 } else {
321 None
322 };
323
324 match path {
325 Some(path) => {
326 let password = if let Some(password) = unsafe_password {
327 password
328 } else {
329 rpassword::prompt_password("Enter secret: ")?
331 };
332
333 for i in 0..number {
334 let account_name_ref =
335 account_name.as_deref().map(|name| match number {
336 1 => name.to_string(),
337 _ => format!("{}_{}", name, i + 1),
338 });
339
340 let (wallet, uuid) = PrivateKeySigner::new_keystore(
341 &path,
342 &mut rng,
343 password.clone(),
344 account_name_ref.as_deref(),
345 )?;
346 let identifier = account_name_ref.as_deref().unwrap_or(&uuid);
347
348 if let Some(json) = json_values.as_mut() {
349 json.push(if shell::verbosity() > 0 {
350 json!({
351 "address": wallet.address().to_checksum(None),
352 "public_key": format!("0x{}", hex::encode(wallet.public_key())),
353 "path": format!("{}", path.join(identifier).display()),
354 })
355 } else {
356 json!({
357 "address": wallet.address().to_checksum(None),
358 "path": format!("{}", path.join(identifier).display()),
359 })
360 });
361 } else {
362 sh_println!(
363 "Created new encrypted keystore file: {}",
364 path.join(identifier).display()
365 )?;
366 sh_println!("Address: {}", wallet.address().to_checksum(None))?;
367 if shell::verbosity() > 0 {
368 sh_println!(
369 "Public key: 0x{}",
370 hex::encode(wallet.public_key())
371 )?;
372 }
373 }
374 }
375 }
376 None => {
377 for _ in 0..number {
378 let wallet = PrivateKeySigner::random_with(&mut rng);
379
380 if let Some(json) = json_values.as_mut() {
381 json.push(if shell::verbosity() > 0 {
382 json!({
383 "address": wallet.address().to_checksum(None),
384 "public_key": format!("0x{}", hex::encode(wallet.public_key())),
385 "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
386 })
387 } else {
388 json!({
389 "address": wallet.address().to_checksum(None),
390 "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
391 })
392 });
393 } else {
394 sh_println!("Successfully created new keypair.")?;
395 sh_println!("Address: {}", wallet.address().to_checksum(None))?;
396 if shell::verbosity() > 0 {
397 sh_println!(
398 "Public key: 0x{}",
399 hex::encode(wallet.public_key())
400 )?;
401 }
402 sh_println!(
403 "Private key: 0x{}",
404 hex::encode(wallet.credential().to_bytes())
405 )?;
406 }
407 }
408 }
409 }
410
411 if let Some(json) = json_values.as_ref() {
412 sh_println!("{}", serde_json::to_string_pretty(json)?)?;
413 }
414 }
415 Self::NewMnemonic { words, accounts, entropy } => {
416 let phrase = if let Some(entropy) = entropy {
417 let entropy = Entropy::from_slice(hex::decode(entropy)?)?;
418 Mnemonic::<English>::new_from_entropy(entropy).to_phrase()
419 } else {
420 let mut rng = thread_rng();
421 Mnemonic::<English>::new_with_count(&mut rng, words)?.to_phrase()
422 };
423
424 let format_json = shell::is_json();
425
426 if !format_json {
427 sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?;
428 }
429
430 let builder = MnemonicBuilder::<English>::default().phrase(phrase.as_str());
431 let derivation_path = "m/44'/60'/0'/0/";
432 let wallets = (0..accounts)
433 .map(|i| builder.clone().derivation_path(format!("{derivation_path}{i}")))
434 .collect::<Result<Vec<_>, _>>()?;
435 let wallets =
436 wallets.into_iter().map(|b| b.build()).collect::<Result<Vec<_>, _>>()?;
437
438 if !format_json {
439 sh_println!("{}", "Successfully generated a new mnemonic.".green())?;
440 sh_println!("Phrase:\n{phrase}")?;
441 sh_println!("\nAccounts:")?;
442 }
443
444 let mut accounts = json!([]);
445 for (i, wallet) in wallets.iter().enumerate() {
446 let public_key = hex::encode(wallet.public_key());
447 let private_key = hex::encode(wallet.credential().to_bytes());
448 if format_json {
449 accounts.as_array_mut().unwrap().push(if shell::verbosity() > 0 {
450 json!({
451 "address": format!("{}", wallet.address()),
452 "public_key": format!("0x{}", public_key),
453 "private_key": format!("0x{}", private_key),
454 })
455 } else {
456 json!({
457 "address": format!("{}", wallet.address()),
458 "private_key": format!("0x{}", private_key),
459 })
460 });
461 } else {
462 sh_println!("- Account {i}:")?;
463 sh_println!("Address: {}", wallet.address())?;
464 if shell::verbosity() > 0 {
465 sh_println!("Public key: 0x{}", public_key)?;
466 }
467 sh_println!("Private key: 0x{}\n", private_key)?;
468 }
469 }
470
471 if format_json {
472 let obj = json!({
473 "mnemonic": phrase,
474 "accounts": accounts,
475 });
476 sh_println!("{}", serde_json::to_string_pretty(&obj)?)?;
477 }
478 }
479 Self::Vanity(cmd) => {
480 cmd.run()?;
481 }
482 Self::Address { wallet, private_key_override } => {
483 let wallet = private_key_override
484 .map(|pk| WalletOpts {
485 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
486 ..Default::default()
487 })
488 .unwrap_or(wallet)
489 .signer()
490 .await?;
491 let addr = wallet.address();
492 sh_println!("{}", addr.to_checksum(None))?;
493 }
494 Self::PublicKey { wallet, private_key_override } => {
495 let wallet = private_key_override
496 .map(|pk| WalletOpts {
497 raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
498 ..Default::default()
499 })
500 .unwrap_or(wallet)
501 .signer()
502 .await?;
503
504 let public_key = match wallet {
505 WalletSigner::Local(wallet) => wallet.public_key(),
506 _ => eyre::bail!("Only local wallets are supported by this command"),
507 };
508
509 sh_println!("0x{}", hex::encode(public_key))?;
510 }
511 Self::Sign { message, data, from_file, no_hash, wallet } => {
512 let wallet = wallet.signer().await?;
513 let sig = if data {
514 let typed_data: TypedData = if from_file {
515 foundry_common::fs::read_json_file(message.as_ref())?
517 } else {
518 serde_json::from_str(&message)?
520 };
521 wallet.sign_dynamic_typed_data(&typed_data).await?
522 } else if no_hash {
523 wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
524 } else {
525 wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
526 };
527
528 if shell::verbosity() > 0 {
529 if shell::is_json() {
530 sh_println!(
531 "{}",
532 serde_json::to_string_pretty(&json!({
533 "message": message,
534 "address": wallet.address(),
535 "signature": hex::encode(sig.as_bytes()),
536 }))?
537 )?;
538 } else {
539 sh_println!(
540 "Successfully signed!\n Message: {}\n Address: {}\n Signature: 0x{}",
541 message,
542 wallet.address(),
543 hex::encode(sig.as_bytes()),
544 )?;
545 }
546 } else {
547 sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
549 }
550 }
551 Self::SignAuth { rpc, nonce, chain, wallet, address, self_broadcast } => {
552 let wallet = wallet.signer().await?;
553 let provider = utils::get_provider(&rpc.load_config()?)?;
554 let nonce = if let Some(nonce) = nonce {
555 nonce
556 } else {
557 let current_nonce = provider.get_transaction_count(wallet.address()).await?;
558 if self_broadcast {
559 current_nonce + 1
562 } else {
563 current_nonce
564 }
565 };
566 let chain_id = if let Some(chain) = chain {
567 chain.id()
568 } else {
569 provider.get_chain_id().await?
570 };
571 let auth = Authorization { chain_id: U256::from(chain_id), address, nonce };
572 let signature = wallet.sign_hash(&auth.signature_hash()).await?;
573 let auth = auth.into_signed(signature);
574
575 if shell::verbosity() > 0 {
576 if shell::is_json() {
577 sh_println!(
578 "{}",
579 serde_json::to_string_pretty(&json!({
580 "nonce": nonce,
581 "chain_id": chain_id,
582 "address": wallet.address(),
583 "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
584 }))?
585 )?;
586 } else {
587 sh_println!(
588 "Successfully signed!\n Nonce: {}\n Chain ID: {}\n Address: {}\n Signature: 0x{}",
589 nonce,
590 chain_id,
591 wallet.address(),
592 hex::encode_prefixed(alloy_rlp::encode(&auth)),
593 )?;
594 }
595 } else {
596 sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
598 }
599 }
600 Self::Verify { message, signature, address, data, from_file, no_hash } => {
601 let recovered_address = if data {
602 let typed_data: TypedData = if from_file {
603 foundry_common::fs::read_json_file(message.as_ref())?
605 } else {
606 serde_json::from_str(&message)?
608 };
609 Self::recover_address_from_typed_data(&typed_data, &signature)?
610 } else if no_hash {
611 Self::recover_address_from_message_no_hash(
612 &hex::decode(&message)?[..].try_into()?,
613 &signature,
614 )?
615 } else {
616 Self::recover_address_from_message(&message, &signature)?
617 };
618
619 if address == recovered_address {
620 sh_println!("Validation succeeded. Address {address} signed this message.")?;
621 } else {
622 eyre::bail!("Validation failed. Address {address} did not sign this message.");
623 }
624 }
625 Self::Import { account_name, keystore_dir, unsafe_password, raw_wallet_options } => {
626 let dir = if let Some(path) = keystore_dir {
628 Path::new(&path).to_path_buf()
629 } else {
630 Config::foundry_keystores_dir().ok_or_else(|| {
631 eyre::eyre!("Could not find the default keystore directory.")
632 })?
633 };
634
635 fs::create_dir_all(&dir)?;
636
637 let keystore_path = Path::new(&dir).join(&account_name);
639 if keystore_path.exists() {
640 eyre::bail!("Keystore file already exists at {}", keystore_path.display());
641 }
642
643 let wallet = raw_wallet_options
645 .signer()?
646 .and_then(|s| match s {
647 WalletSigner::Local(s) => Some(s),
648 _ => None,
649 })
650 .ok_or_else(|| {
651 eyre::eyre!(
652 "\
653Did you set a private key or mnemonic?
654Run `cast wallet import --help` and use the corresponding CLI
655flag to set your key via:
656--private-key, --mnemonic-path or --interactive."
657 )
658 })?;
659
660 let private_key = wallet.credential().to_bytes();
661 let password = if let Some(password) = unsafe_password {
662 password
663 } else {
664 rpassword::prompt_password("Enter password: ")?
666 };
667
668 let mut rng = thread_rng();
669 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
670 dir,
671 &mut rng,
672 private_key,
673 password,
674 Some(&account_name),
675 )?;
676 let address = wallet.address();
677 let success_message = format!(
678 "`{}` keystore was saved successfully. Address: {:?}",
679 &account_name, address,
680 );
681 sh_println!("{}", success_message.green())?;
682 }
683 Self::List(cmd) => {
684 cmd.run().await?;
685 }
686 Self::Remove { name, dir, unsafe_password } => {
687 let dir = if let Some(path) = dir {
688 Path::new(&path).to_path_buf()
689 } else {
690 Config::foundry_keystores_dir().ok_or_else(|| {
691 eyre::eyre!("Could not find the default keystore directory.")
692 })?
693 };
694
695 let keystore_path = Path::new(&dir).join(&name);
696 if !keystore_path.exists() {
697 eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
698 }
699
700 let password = if let Some(pwd) = unsafe_password {
701 pwd
702 } else {
703 rpassword::prompt_password("Enter password: ")?
704 };
705
706 if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
707 eyre::bail!("Invalid password - wallet removal cancelled");
708 }
709
710 std::fs::remove_file(&keystore_path).wrap_err_with(|| {
711 format!("Failed to remove keystore file at {}", keystore_path.display())
712 })?;
713
714 let success_message = format!("`{}` keystore was removed successfully.", &name);
715 sh_println!("{}", success_message.green())?;
716 }
717 Self::PrivateKey {
718 wallet,
719 mnemonic_override,
720 mnemonic_index_or_derivation_path_override,
721 } => {
722 let (index_override, derivation_path_override) =
723 match mnemonic_index_or_derivation_path_override {
724 Some(value) => match value.parse::<u32>() {
725 Ok(index) => (Some(index), None),
726 Err(_) => (None, Some(value)),
727 },
728 None => (None, None),
729 };
730 let wallet = WalletOpts {
731 raw: RawWalletOpts {
732 mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
733 mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
734 hd_path: derivation_path_override.or(wallet.raw.hd_path),
735 ..wallet.raw
736 },
737 ..wallet
738 }
739 .signer()
740 .await?;
741 match wallet {
742 WalletSigner::Local(wallet) => {
743 if shell::verbosity() > 0 {
744 sh_println!("Address: {}", wallet.address())?;
745 sh_println!(
746 "Private key: 0x{}",
747 hex::encode(wallet.credential().to_bytes())
748 )?;
749 } else {
750 sh_println!("0x{}", hex::encode(wallet.credential().to_bytes()))?;
751 }
752 }
753 _ => {
754 eyre::bail!("Only local wallets are supported by this command.");
755 }
756 }
757 }
758 Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
759 let dir = if let Some(path) = keystore_dir {
761 Path::new(&path).to_path_buf()
762 } else {
763 Config::foundry_keystores_dir().ok_or_else(|| {
764 eyre::eyre!("Could not find the default keystore directory.")
765 })?
766 };
767
768 let keypath = dir.join(&account_name);
769
770 if !keypath.exists() {
771 eyre::bail!("Keystore file does not exist at {}", keypath.display());
772 }
773
774 let password = if let Some(password) = unsafe_password {
775 password
776 } else {
777 rpassword::prompt_password("Enter password: ")?
779 };
780
781 let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
782
783 let private_key = B256::from_slice(&wallet.credential().to_bytes());
784
785 let success_message =
786 format!("{}'s private key is: {}", &account_name, private_key);
787
788 sh_println!("{}", success_message.green())?;
789 }
790 Self::ChangePassword {
791 account_name,
792 keystore_dir,
793 unsafe_password,
794 unsafe_new_password,
795 } => {
796 let dir = if let Some(path) = keystore_dir {
798 Path::new(&path).to_path_buf()
799 } else {
800 Config::foundry_keystores_dir().ok_or_else(|| {
801 eyre::eyre!("Could not find the default keystore directory.")
802 })?
803 };
804
805 let keypath = dir.join(&account_name);
806
807 if !keypath.exists() {
808 eyre::bail!("Keystore file does not exist at {}", keypath.display());
809 }
810
811 let current_password = if let Some(password) = unsafe_password {
812 password
813 } else {
814 rpassword::prompt_password("Enter current password: ")?
816 };
817
818 let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
820 .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
821
822 let new_password = if let Some(password) = unsafe_new_password {
823 password
824 } else {
825 rpassword::prompt_password("Enter new password: ")?
827 };
828
829 if current_password == new_password {
830 eyre::bail!("New password cannot be the same as the current password");
831 }
832
833 let private_key = wallet.credential().to_bytes();
835 let mut rng = thread_rng();
836 let (wallet, _) = PrivateKeySigner::encrypt_keystore(
837 dir,
838 &mut rng,
839 private_key,
840 new_password,
841 Some(&account_name),
842 )?;
843
844 let success_message = format!(
845 "Password for keystore `{}` was changed successfully. Address: {:?}",
846 &account_name,
847 wallet.address(),
848 );
849 sh_println!("{}", success_message.green())?;
850 }
851 };
852
853 Ok(())
854 }
855
856 fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
860 let message = Self::hex_str_to_bytes(message)?;
861 Ok(signature.recover_address_from_msg(message)?)
862 }
863
864 fn recover_address_from_message_no_hash(
866 prehash: &B256,
867 signature: &Signature,
868 ) -> Result<Address> {
869 Ok(signature.recover_address_from_prehash(prehash)?)
870 }
871
872 fn recover_address_from_typed_data(
874 typed_data: &TypedData,
875 signature: &Signature,
876 ) -> Result<Address> {
877 Ok(signature.recover_address_from_prehash(&typed_data.eip712_signing_hash()?)?)
878 }
879
880 fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
884 Ok(match s.strip_prefix("0x") {
885 Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
886 None => s.as_bytes().to_vec(),
887 })
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use alloy_primitives::{address, keccak256};
895 use std::str::FromStr;
896
897 #[test]
898 fn can_parse_wallet_sign_message() {
899 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
900 match args {
901 WalletSubcommands::Sign { message, data, from_file, .. } => {
902 assert_eq!(message, "deadbeef".to_string());
903 assert!(!data);
904 assert!(!from_file);
905 }
906 _ => panic!("expected WalletSubcommands::Sign"),
907 }
908 }
909
910 #[test]
911 fn can_parse_wallet_sign_hex_message() {
912 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
913 match args {
914 WalletSubcommands::Sign { message, data, from_file, .. } => {
915 assert_eq!(message, "0xdeadbeef".to_string());
916 assert!(!data);
917 assert!(!from_file);
918 }
919 _ => panic!("expected WalletSubcommands::Sign"),
920 }
921 }
922
923 #[test]
924 fn can_verify_signed_hex_message() {
925 let message = "hello";
926 let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
927 let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
928 let recovered_address =
929 WalletSubcommands::recover_address_from_message(message, &signature);
930 assert!(recovered_address.is_ok());
931 assert_eq!(address, recovered_address.unwrap());
932 }
933
934 #[test]
935 fn can_verify_signed_hex_message_no_hash() {
936 let prehash = keccak256("hello");
937 let signature = Signature::from_str("433ec3d37e4f1253df15e2dea412fed8e915737730f74b3dfb1353268f932ef5557c9158e0b34bce39de28d11797b42e9b1acb2749230885fe075aedc3e491a41b").unwrap();
938 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
940 WalletSubcommands::recover_address_from_message_no_hash(&prehash, &signature);
941 assert!(recovered_address.is_ok());
942 assert_eq!(address, recovered_address.unwrap());
943 }
944
945 #[test]
946 fn can_verify_signed_typed_data() {
947 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();
948 let signature = Signature::from_str("0285ff83b93bd01c14e201943af7454fe2bc6c98be707a73888c397d6ae3b0b92f73ca559f81cbb19fe4e0f1dc4105bd7b647c6a84b033057977cf2ec982daf71b").unwrap();
949 let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); let recovered_address =
951 WalletSubcommands::recover_address_from_typed_data(&typed_data, &signature);
952 assert!(recovered_address.is_ok());
953 assert_eq!(address, recovered_address.unwrap());
954 }
955
956 #[test]
957 fn can_parse_wallet_sign_data() {
958 let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
959 match args {
960 WalletSubcommands::Sign { message, data, from_file, .. } => {
961 assert_eq!(message, "{ ... }".to_string());
962 assert!(data);
963 assert!(!from_file);
964 }
965 _ => panic!("expected WalletSubcommands::Sign"),
966 }
967 }
968
969 #[test]
970 fn can_parse_wallet_sign_data_file() {
971 let args = WalletSubcommands::parse_from([
972 "foundry-cli",
973 "sign",
974 "--data",
975 "--from-file",
976 "tests/data/typed_data.json",
977 ]);
978 match args {
979 WalletSubcommands::Sign { message, data, from_file, .. } => {
980 assert_eq!(message, "tests/data/typed_data.json".to_string());
981 assert!(data);
982 assert!(from_file);
983 }
984 _ => panic!("expected WalletSubcommands::Sign"),
985 }
986 }
987
988 #[test]
989 fn can_parse_wallet_change_password() {
990 let args = WalletSubcommands::parse_from([
991 "foundry-cli",
992 "change-password",
993 "my_account",
994 "--unsafe-password",
995 "old_password",
996 "--unsafe-new-password",
997 "new_password",
998 ]);
999 match args {
1000 WalletSubcommands::ChangePassword {
1001 account_name,
1002 keystore_dir,
1003 unsafe_password,
1004 unsafe_new_password,
1005 } => {
1006 assert_eq!(account_name, "my_account".to_string());
1007 assert_eq!(unsafe_password, Some("old_password".to_string()));
1008 assert_eq!(unsafe_new_password, Some("new_password".to_string()));
1009 assert!(keystore_dir.is_none());
1010 }
1011 _ => panic!("expected WalletSubcommands::ChangePassword"),
1012 }
1013 }
1014
1015 #[test]
1016 fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1017 let result = WalletSubcommands::try_parse_from([
1018 "foundry-cli",
1019 "sign-auth",
1020 "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1021 "--nonce",
1022 "42",
1023 "--self-broadcast",
1024 ]);
1025 assert!(
1026 result.is_err(),
1027 "expected error when both --nonce and --self-broadcast are provided"
1028 );
1029 }
1030}