cast/cmd/wallet/
mod.rs

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/// CLI arguments for `cast wallet`.
29#[derive(Debug, Parser)]
30pub enum WalletSubcommands {
31    /// Create a new random keypair.
32    #[command(visible_alias = "n")]
33    New {
34        /// If provided, then keypair will be written to an encrypted JSON keystore.
35        path: Option<String>,
36
37        /// Account name for the keystore file. If provided, the keystore file
38        /// will be named using this account name.
39        #[arg(value_name = "ACCOUNT_NAME")]
40        account_name: Option<String>,
41
42        /// Triggers a hidden password prompt for the JSON keystore.
43        ///
44        /// Deprecated: prompting for a hidden password is now the default.
45        #[arg(long, short, requires = "path", conflicts_with = "unsafe_password")]
46        password: bool,
47
48        /// Password for the JSON keystore in cleartext.
49        ///
50        /// This is UNSAFE to use and we recommend using the --password.
51        #[arg(long, requires = "path", env = "CAST_PASSWORD", value_name = "PASSWORD")]
52        unsafe_password: Option<String>,
53
54        /// Number of wallets to generate.
55        #[arg(long, short, default_value = "1")]
56        number: u32,
57    },
58
59    /// Generates a random BIP39 mnemonic phrase
60    #[command(visible_alias = "nm")]
61    NewMnemonic {
62        /// Number of words for the mnemonic
63        #[arg(long, short, default_value = "12")]
64        words: usize,
65
66        /// Number of accounts to display
67        #[arg(long, short, default_value = "1")]
68        accounts: u8,
69
70        /// Entropy to use for the mnemonic
71        #[arg(long, short, conflicts_with = "words")]
72        entropy: Option<String>,
73    },
74
75    /// Generate a vanity address.
76    #[command(visible_alias = "va")]
77    Vanity(VanityArgs),
78
79    /// Convert a private key to an address.
80    #[command(visible_aliases = &["a", "addr"])]
81    Address {
82        /// If provided, the address will be derived from the specified private key.
83        #[arg(value_name = "PRIVATE_KEY")]
84        private_key_override: Option<String>,
85
86        #[command(flatten)]
87        wallet: WalletOpts,
88    },
89
90    /// Sign a message or typed data.
91    #[command(visible_alias = "s")]
92    Sign {
93        /// The message, typed data, or hash to sign.
94        ///
95        /// Messages starting with 0x are expected to be hex encoded, which get decoded before
96        /// being signed.
97        ///
98        /// The message will be prefixed with the Ethereum Signed Message header and hashed before
99        /// signing, unless `--no-hash` is provided.
100        ///
101        /// Typed data can be provided as a json string or a file name.
102        /// Use --data flag to denote the message is a string of typed data.
103        /// Use --data --from-file to denote the message is a file name containing typed data.
104        /// The data will be combined and hashed using the EIP712 specification before signing.
105        /// The data should be formatted as JSON.
106        message: String,
107
108        /// Treat the message as JSON typed data.
109        #[arg(long)]
110        data: bool,
111
112        /// Treat the message as a file containing JSON typed data. Requires `--data`.
113        #[arg(long, requires = "data")]
114        from_file: bool,
115
116        /// Treat the message as a raw 32-byte hash and sign it directly without hashing it again.
117        #[arg(long, conflicts_with = "data")]
118        no_hash: bool,
119
120        #[command(flatten)]
121        wallet: WalletOpts,
122    },
123
124    /// EIP-7702 sign authorization.
125    #[command(visible_alias = "sa")]
126    SignAuth {
127        /// Address to sign authorization for.
128        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    /// Verify the signature of a message.
144    #[command(visible_alias = "v")]
145    Verify {
146        /// The original message.
147        ///
148        /// Treats 0x-prefixed strings as hex encoded bytes.
149        /// Non 0x-prefixed strings are treated as raw input message.
150        message: String,
151
152        /// The signature to verify.
153        signature: Signature,
154
155        /// The address of the message signer.
156        #[arg(long, short)]
157        address: Address,
158    },
159
160    /// Import a private key into an encrypted keystore.
161    #[command(visible_alias = "i")]
162    Import {
163        /// The name for the account in the keystore.
164        #[arg(value_name = "ACCOUNT_NAME")]
165        account_name: String,
166        /// If provided, keystore will be saved here instead of the default keystores directory
167        /// (~/.foundry/keystores)
168        #[arg(long, short)]
169        keystore_dir: Option<String>,
170        /// Password for the JSON keystore in cleartext
171        /// This is unsafe, we recommend using the default hidden password prompt
172        #[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    /// List all the accounts in the keystore default directory
179    #[command(visible_alias = "ls")]
180    List(ListArgs),
181
182    /// Remove a wallet from the keystore.
183    ///
184    /// This command requires the wallet alias and will prompt for a password to ensure that only
185    /// an authorized user can remove the wallet.
186    #[command(visible_aliases = &["rm"], override_usage = "cast wallet remove --name <NAME>")]
187    Remove {
188        /// The alias (or name) of the wallet to remove.
189        #[arg(long, required = true)]
190        name: String,
191        /// Optionally provide the keystore directory if not provided. default directory will be
192        /// used (~/.foundry/keystores).
193        #[arg(long)]
194        dir: Option<String>,
195        /// Password for the JSON keystore in cleartext
196        /// This is unsafe, we recommend using the default hidden password prompt
197        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
198        unsafe_password: Option<String>,
199    },
200
201    /// Derives private key from mnemonic
202    #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])]
203    PrivateKey {
204        /// If provided, the private key will be derived from the specified menomonic phrase.
205        #[arg(value_name = "MNEMONIC")]
206        mnemonic_override: Option<String>,
207
208        /// If provided, the private key will be derived using the
209        /// specified mnemonic index (if integer) or derivation path.
210        #[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    /// Get the public key for the given private key.
217    #[command(visible_aliases = &["pubkey"])]
218    PublicKey {
219        /// If provided, the public key will be derived from the specified private key.
220        #[arg(long = "raw-private-key", value_name = "PRIVATE_KEY")]
221        private_key_override: Option<String>,
222
223        #[command(flatten)]
224        wallet: WalletOpts,
225    },
226    /// Decrypt a keystore file to get the private key
227    #[command(name = "decrypt-keystore", visible_alias = "dk")]
228    DecryptKeystore {
229        /// The name for the account in the keystore.
230        #[arg(value_name = "ACCOUNT_NAME")]
231        account_name: String,
232        /// If not provided, keystore will try to be located at the default keystores directory
233        /// (~/.foundry/keystores)
234        #[arg(long, short)]
235        keystore_dir: Option<String>,
236        /// Password for the JSON keystore in cleartext
237        /// This is unsafe, we recommend using the default hidden password prompt
238        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
239        unsafe_password: Option<String>,
240    },
241
242    /// Change the password of a keystore file
243    #[command(name = "change-password", visible_alias = "cp")]
244    ChangePassword {
245        /// The name for the account in the keystore.
246        #[arg(value_name = "ACCOUNT_NAME")]
247        account_name: String,
248        /// If not provided, keystore will try to be located at the default keystores directory
249        /// (~/.foundry/keystores)
250        #[arg(long, short)]
251        keystore_dir: Option<String>,
252        /// Current password for the JSON keystore in cleartext
253        /// This is unsafe, we recommend using the default hidden password prompt
254        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
255        unsafe_password: Option<String>,
256        /// New password for the JSON keystore in cleartext
257        /// This is unsafe, we recommend using the default hidden password prompt
258        #[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                        // If the path doesn't exist, it will fail to be canonicalized,
274                        // so we attach more context to the error message.
275                        Err(e) => {
276                            eyre::bail!(
277                                "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: {}",
278                                e
279                            );
280                        }
281                    };
282                    if !path.is_dir() {
283                        // we require path to be an existing directory
284                        eyre::bail!("`{}` is not a directory", path.display());
285                    }
286
287                    let password = if let Some(password) = unsafe_password {
288                        password
289                    } else {
290                        // if no --unsafe-password was provided read via stdin
291                        rpassword::prompt_password("Enter secret: ")?
292                    };
293
294                    for i in 0..number {
295                        let account_name_ref = account_name.as_deref().map(|name| match number {
296                            1 => name.to_string(),
297                            _ => format!("{}_{}", name, i + 1),
298                        });
299
300                        let (wallet, uuid) = PrivateKeySigner::new_keystore(
301                            &path,
302                            &mut rng,
303                            password.clone(),
304                            account_name_ref.as_deref(),
305                        )?;
306                        let identifier = account_name_ref.as_deref().unwrap_or(&uuid);
307
308                        if let Some(json) = json_values.as_mut() {
309                            json.push(if shell::verbosity() > 0 {
310                                json!({
311                                    "address": wallet.address().to_checksum(None),
312                                    "public_key": format!("0x{}", hex::encode(wallet.public_key())),
313                                    "path": format!("{}", path.join(identifier).display()),
314                                })
315                            } else {
316                                json!({
317                                    "address": wallet.address().to_checksum(None),
318                                    "path": format!("{}", path.join(identifier).display()),
319                                })
320                            });
321                        } else {
322                            sh_println!(
323                                "Created new encrypted keystore file: {}",
324                                path.join(identifier).display()
325                            )?;
326                            sh_println!("Address:    {}", wallet.address().to_checksum(None))?;
327                            if shell::verbosity() > 0 {
328                                sh_println!("Public key: 0x{}", hex::encode(wallet.public_key()))?;
329                            }
330                        }
331                    }
332                } else {
333                    for _ in 0..number {
334                        let wallet = PrivateKeySigner::random_with(&mut rng);
335
336                        if let Some(json) = json_values.as_mut() {
337                            json.push(if shell::verbosity() > 0 {
338                                json!({
339                                    "address": wallet.address().to_checksum(None),
340                                    "public_key": format!("0x{}", hex::encode(wallet.public_key())),
341                                    "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
342                                })
343                            } else {
344                                json!({
345                                    "address": wallet.address().to_checksum(None),
346                                    "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
347                                })
348                            });
349                        } else {
350                            sh_println!("Successfully created new keypair.")?;
351                            sh_println!("Address:     {}", wallet.address().to_checksum(None))?;
352                            if shell::verbosity() > 0 {
353                                sh_println!("Public key:  0x{}", hex::encode(wallet.public_key()))?;
354                            }
355                            sh_println!(
356                                "Private key: 0x{}",
357                                hex::encode(wallet.credential().to_bytes())
358                            )?;
359                        }
360                    }
361                }
362                if let Some(json) = json_values.as_ref() {
363                    sh_println!("{}", serde_json::to_string_pretty(json)?)?;
364                }
365            }
366            Self::NewMnemonic { words, accounts, entropy } => {
367                let phrase = if let Some(entropy) = entropy {
368                    let entropy = Entropy::from_slice(hex::decode(entropy)?)?;
369                    Mnemonic::<English>::new_from_entropy(entropy).to_phrase()
370                } else {
371                    let mut rng = thread_rng();
372                    Mnemonic::<English>::new_with_count(&mut rng, words)?.to_phrase()
373                };
374
375                let format_json = shell::is_json();
376
377                if !format_json {
378                    sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?;
379                }
380
381                let builder = MnemonicBuilder::<English>::default().phrase(phrase.as_str());
382                let derivation_path = "m/44'/60'/0'/0/";
383                let wallets = (0..accounts)
384                    .map(|i| builder.clone().derivation_path(format!("{derivation_path}{i}")))
385                    .collect::<Result<Vec<_>, _>>()?;
386                let wallets =
387                    wallets.into_iter().map(|b| b.build()).collect::<Result<Vec<_>, _>>()?;
388
389                if !format_json {
390                    sh_println!("{}", "Successfully generated a new mnemonic.".green())?;
391                    sh_println!("Phrase:\n{phrase}")?;
392                    sh_println!("\nAccounts:")?;
393                }
394
395                let mut accounts = json!([]);
396                for (i, wallet) in wallets.iter().enumerate() {
397                    let public_key = hex::encode(wallet.public_key());
398                    let private_key = hex::encode(wallet.credential().to_bytes());
399                    if format_json {
400                        accounts.as_array_mut().unwrap().push(if shell::verbosity() > 0 {
401                            json!({
402                                "address": format!("{}", wallet.address()),
403                                "public_key": format!("0x{}", public_key),
404                                "private_key": format!("0x{}", private_key),
405                            })
406                        } else {
407                            json!({
408                                "address": format!("{}", wallet.address()),
409                                "private_key": format!("0x{}", private_key),
410                            })
411                        });
412                    } else {
413                        sh_println!("- Account {i}:")?;
414                        sh_println!("Address:     {}", wallet.address())?;
415                        if shell::verbosity() > 0 {
416                            sh_println!("Public key:  0x{}", public_key)?;
417                        }
418                        sh_println!("Private key: 0x{}\n", private_key)?;
419                    }
420                }
421
422                if format_json {
423                    let obj = json!({
424                        "mnemonic": phrase,
425                        "accounts": accounts,
426                    });
427                    sh_println!("{}", serde_json::to_string_pretty(&obj)?)?;
428                }
429            }
430            Self::Vanity(cmd) => {
431                cmd.run()?;
432            }
433            Self::Address { wallet, private_key_override } => {
434                let wallet = private_key_override
435                    .map(|pk| WalletOpts {
436                        raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
437                        ..Default::default()
438                    })
439                    .unwrap_or(wallet)
440                    .signer()
441                    .await?;
442                let addr = wallet.address();
443                sh_println!("{}", addr.to_checksum(None))?;
444            }
445            Self::PublicKey { wallet, private_key_override } => {
446                let wallet = private_key_override
447                    .map(|pk| WalletOpts {
448                        raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
449                        ..Default::default()
450                    })
451                    .unwrap_or(wallet)
452                    .signer()
453                    .await?;
454
455                let public_key = match wallet {
456                    WalletSigner::Local(wallet) => wallet.public_key(),
457                    _ => eyre::bail!("Only local wallets are supported by this command"),
458                };
459
460                sh_println!("0x{}", hex::encode(public_key))?;
461            }
462            Self::Sign { message, data, from_file, no_hash, wallet } => {
463                let wallet = wallet.signer().await?;
464                let sig = if data {
465                    let typed_data: TypedData = if from_file {
466                        // data is a file name, read json from file
467                        foundry_common::fs::read_json_file(message.as_ref())?
468                    } else {
469                        // data is a json string
470                        serde_json::from_str(&message)?
471                    };
472                    wallet.sign_dynamic_typed_data(&typed_data).await?
473                } else if no_hash {
474                    wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
475                } else {
476                    wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
477                };
478
479                if shell::verbosity() > 0 {
480                    if shell::is_json() {
481                        sh_println!(
482                            "{}",
483                            serde_json::to_string_pretty(&json!({
484                                "message": message,
485                                "address": wallet.address(),
486                                "signature": hex::encode(sig.as_bytes()),
487                            }))?
488                        )?;
489                    } else {
490                        sh_println!(
491                            "Successfully signed!\n   Message: {}\n   Address: {}\n   Signature: 0x{}",
492                            message,
493                            wallet.address(),
494                            hex::encode(sig.as_bytes()),
495                        )?;
496                    }
497                } else {
498                    // Pipe friendly output
499                    sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
500                }
501            }
502            Self::SignAuth { rpc, nonce, chain, wallet, address } => {
503                let wallet = wallet.signer().await?;
504                let provider = utils::get_provider(&rpc.load_config()?)?;
505                let nonce = if let Some(nonce) = nonce {
506                    nonce
507                } else {
508                    provider.get_transaction_count(wallet.address()).await?
509                };
510                let chain_id = if let Some(chain) = chain {
511                    chain.id()
512                } else {
513                    provider.get_chain_id().await?
514                };
515                let auth = Authorization { chain_id: U256::from(chain_id), address, nonce };
516                let signature = wallet.sign_hash(&auth.signature_hash()).await?;
517                let auth = auth.into_signed(signature);
518
519                if shell::verbosity() > 0 {
520                    if shell::is_json() {
521                        sh_println!(
522                            "{}",
523                            serde_json::to_string_pretty(&json!({
524                                "nonce": nonce,
525                                "chain_id": chain_id,
526                                "address": wallet.address(),
527                                "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
528                            }))?
529                        )?;
530                    } else {
531                        sh_println!(
532                            "Successfully signed!\n   Nonce: {}\n   Chain ID: {}\n   Address: {}\n   Signature: 0x{}",
533                            nonce,
534                            chain_id,
535                            wallet.address(),
536                            hex::encode_prefixed(alloy_rlp::encode(&auth)),
537                        )?;
538                    }
539                } else {
540                    // Pipe friendly output
541                    sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
542                }
543            }
544            Self::Verify { message, signature, address } => {
545                let recovered_address = Self::recover_address_from_message(&message, &signature)?;
546                if address == recovered_address {
547                    sh_println!("Validation succeeded. Address {address} signed this message.")?;
548                } else {
549                    eyre::bail!("Validation failed. Address {address} did not sign this message.");
550                }
551            }
552            Self::Import { account_name, keystore_dir, unsafe_password, raw_wallet_options } => {
553                // Set up keystore directory
554                let dir = if let Some(path) = keystore_dir {
555                    Path::new(&path).to_path_buf()
556                } else {
557                    Config::foundry_keystores_dir().ok_or_else(|| {
558                        eyre::eyre!("Could not find the default keystore directory.")
559                    })?
560                };
561
562                fs::create_dir_all(&dir)?;
563
564                // check if account exists already
565                let keystore_path = Path::new(&dir).join(&account_name);
566                if keystore_path.exists() {
567                    eyre::bail!("Keystore file already exists at {}", keystore_path.display());
568                }
569
570                // get wallet
571                let wallet = raw_wallet_options
572                    .signer()?
573                    .and_then(|s| match s {
574                        WalletSigner::Local(s) => Some(s),
575                        _ => None,
576                    })
577                    .ok_or_else(|| {
578                        eyre::eyre!(
579                            "\
580Did you set a private key or mnemonic?
581Run `cast wallet import --help` and use the corresponding CLI
582flag to set your key via:
583--private-key, --mnemonic-path or --interactive."
584                        )
585                    })?;
586
587                let private_key = wallet.credential().to_bytes();
588                let password = if let Some(password) = unsafe_password {
589                    password
590                } else {
591                    // if no --unsafe-password was provided read via stdin
592                    rpassword::prompt_password("Enter password: ")?
593                };
594
595                let mut rng = thread_rng();
596                let (wallet, _) = PrivateKeySigner::encrypt_keystore(
597                    dir,
598                    &mut rng,
599                    private_key,
600                    password,
601                    Some(&account_name),
602                )?;
603                let address = wallet.address();
604                let success_message = format!(
605                    "`{}` keystore was saved successfully. Address: {:?}",
606                    &account_name, address,
607                );
608                sh_println!("{}", success_message.green())?;
609            }
610            Self::List(cmd) => {
611                cmd.run().await?;
612            }
613            Self::Remove { name, dir, unsafe_password } => {
614                let dir = if let Some(path) = 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                let keystore_path = Path::new(&dir).join(&name);
623                if !keystore_path.exists() {
624                    eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
625                }
626
627                let password = if let Some(pwd) = unsafe_password {
628                    pwd
629                } else {
630                    rpassword::prompt_password("Enter password: ")?
631                };
632
633                if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
634                    eyre::bail!("Invalid password - wallet removal cancelled");
635                }
636
637                std::fs::remove_file(&keystore_path).wrap_err_with(|| {
638                    format!("Failed to remove keystore file at {}", keystore_path.display())
639                })?;
640
641                let success_message = format!("`{}` keystore was removed successfully.", &name);
642                sh_println!("{}", success_message.green())?;
643            }
644            Self::PrivateKey {
645                wallet,
646                mnemonic_override,
647                mnemonic_index_or_derivation_path_override,
648            } => {
649                let (index_override, derivation_path_override) =
650                    match mnemonic_index_or_derivation_path_override {
651                        Some(value) => match value.parse::<u32>() {
652                            Ok(index) => (Some(index), None),
653                            Err(_) => (None, Some(value)),
654                        },
655                        None => (None, None),
656                    };
657                let wallet = WalletOpts {
658                    raw: RawWalletOpts {
659                        mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
660                        mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
661                        hd_path: derivation_path_override.or(wallet.raw.hd_path),
662                        ..wallet.raw
663                    },
664                    ..wallet
665                }
666                .signer()
667                .await?;
668                match wallet {
669                    WalletSigner::Local(wallet) => {
670                        if shell::verbosity() > 0 {
671                            sh_println!("Address:     {}", wallet.address())?;
672                            sh_println!(
673                                "Private key: 0x{}",
674                                hex::encode(wallet.credential().to_bytes())
675                            )?;
676                        } else {
677                            sh_println!("0x{}", hex::encode(wallet.credential().to_bytes()))?;
678                        }
679                    }
680                    _ => {
681                        eyre::bail!("Only local wallets are supported by this command.");
682                    }
683                }
684            }
685            Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
686                // Set up keystore directory
687                let dir = if let Some(path) = keystore_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 keypath = dir.join(&account_name);
696
697                if !keypath.exists() {
698                    eyre::bail!("Keystore file does not exist at {}", keypath.display());
699                }
700
701                let password = if let Some(password) = unsafe_password {
702                    password
703                } else {
704                    // if no --unsafe-password was provided read via stdin
705                    rpassword::prompt_password("Enter password: ")?
706                };
707
708                let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
709
710                let private_key = B256::from_slice(&wallet.credential().to_bytes());
711
712                let success_message =
713                    format!("{}'s private key is: {}", &account_name, private_key);
714
715                sh_println!("{}", success_message.green())?;
716            }
717            Self::ChangePassword {
718                account_name,
719                keystore_dir,
720                unsafe_password,
721                unsafe_new_password,
722            } => {
723                // Set up keystore directory
724                let dir = if let Some(path) = keystore_dir {
725                    Path::new(&path).to_path_buf()
726                } else {
727                    Config::foundry_keystores_dir().ok_or_else(|| {
728                        eyre::eyre!("Could not find the default keystore directory.")
729                    })?
730                };
731
732                let keypath = dir.join(&account_name);
733
734                if !keypath.exists() {
735                    eyre::bail!("Keystore file does not exist at {}", keypath.display());
736                }
737
738                let current_password = if let Some(password) = unsafe_password {
739                    password
740                } else {
741                    // if no --unsafe-password was provided read via stdin
742                    rpassword::prompt_password("Enter current password: ")?
743                };
744
745                // decrypt the keystore to verify the current password and get the private key
746                let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
747                    .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
748
749                let new_password = if let Some(password) = unsafe_new_password {
750                    password
751                } else {
752                    // if no --unsafe-new-password was provided read via stdin
753                    rpassword::prompt_password("Enter new password: ")?
754                };
755
756                if current_password == new_password {
757                    eyre::bail!("New password cannot be the same as the current password");
758                }
759
760                // Create a new keystore with the new password
761                let private_key = wallet.credential().to_bytes();
762                let mut rng = thread_rng();
763                let (wallet, _) = PrivateKeySigner::encrypt_keystore(
764                    dir,
765                    &mut rng,
766                    private_key,
767                    new_password,
768                    Some(&account_name),
769                )?;
770
771                let success_message = format!(
772                    "Password for keystore `{}` was changed successfully. Address: {:?}",
773                    &account_name,
774                    wallet.address(),
775                );
776                sh_println!("{}", success_message.green())?;
777            }
778        };
779
780        Ok(())
781    }
782
783    /// Recovers an address from the specified message and signature.
784    ///
785    /// Note: This attempts to decode the message as hex if it starts with 0x.
786    fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
787        let message = Self::hex_str_to_bytes(message)?;
788        Ok(signature.recover_address_from_msg(message)?)
789    }
790
791    /// Strips the 0x prefix from a hex string and decodes it to bytes.
792    ///
793    /// Treats the string as raw bytes if it doesn't start with 0x.
794    fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
795        Ok(match s.strip_prefix("0x") {
796            Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
797            None => s.as_bytes().to_vec(),
798        })
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use alloy_primitives::address;
806    use std::str::FromStr;
807
808    #[test]
809    fn can_parse_wallet_sign_message() {
810        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
811        match args {
812            WalletSubcommands::Sign { message, data, from_file, .. } => {
813                assert_eq!(message, "deadbeef".to_string());
814                assert!(!data);
815                assert!(!from_file);
816            }
817            _ => panic!("expected WalletSubcommands::Sign"),
818        }
819    }
820
821    #[test]
822    fn can_parse_wallet_sign_hex_message() {
823        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
824        match args {
825            WalletSubcommands::Sign { message, data, from_file, .. } => {
826                assert_eq!(message, "0xdeadbeef".to_string());
827                assert!(!data);
828                assert!(!from_file);
829            }
830            _ => panic!("expected WalletSubcommands::Sign"),
831        }
832    }
833
834    #[test]
835    fn can_verify_signed_hex_message() {
836        let message = "hello";
837        let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
838        let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
839        let recovered_address =
840            WalletSubcommands::recover_address_from_message(message, &signature);
841        assert!(recovered_address.is_ok());
842        assert_eq!(address, recovered_address.unwrap());
843    }
844
845    #[test]
846    fn can_parse_wallet_sign_data() {
847        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
848        match args {
849            WalletSubcommands::Sign { message, data, from_file, .. } => {
850                assert_eq!(message, "{ ... }".to_string());
851                assert!(data);
852                assert!(!from_file);
853            }
854            _ => panic!("expected WalletSubcommands::Sign"),
855        }
856    }
857
858    #[test]
859    fn can_parse_wallet_sign_data_file() {
860        let args = WalletSubcommands::parse_from([
861            "foundry-cli",
862            "sign",
863            "--data",
864            "--from-file",
865            "tests/data/typed_data.json",
866        ]);
867        match args {
868            WalletSubcommands::Sign { message, data, from_file, .. } => {
869                assert_eq!(message, "tests/data/typed_data.json".to_string());
870                assert!(data);
871                assert!(from_file);
872            }
873            _ => panic!("expected WalletSubcommands::Sign"),
874        }
875    }
876
877    #[test]
878    fn can_parse_wallet_change_password() {
879        let args = WalletSubcommands::parse_from([
880            "foundry-cli",
881            "change-password",
882            "my_account",
883            "--unsafe-password",
884            "old_password",
885            "--unsafe-new-password",
886            "new_password",
887        ]);
888        match args {
889            WalletSubcommands::ChangePassword {
890                account_name,
891                keystore_dir,
892                unsafe_password,
893                unsafe_new_password,
894            } => {
895                assert_eq!(account_name, "my_account".to_string());
896                assert_eq!(unsafe_password, Some("old_password".to_string()));
897                assert_eq!(unsafe_new_password, Some("new_password".to_string()));
898                assert!(keystore_dir.is_none());
899            }
900            _ => panic!("expected WalletSubcommands::ChangePassword"),
901        }
902    }
903}