cast/cmd/wallet/
mod.rs

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