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