Skip to main content

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, 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, 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        /// Overwrite existing keystore files without prompting.
59        #[arg(long)]
60        force: bool,
61    },
62
63    /// Generates a random BIP39 mnemonic phrase
64    #[command(visible_alias = "nm")]
65    NewMnemonic {
66        /// Number of words for the mnemonic
67        #[arg(long, short, default_value = "12")]
68        words: usize,
69
70        /// Number of accounts to display
71        #[arg(long, short, default_value = "1")]
72        accounts: u8,
73
74        /// Entropy to use for the mnemonic
75        #[arg(long, short, conflicts_with = "words")]
76        entropy: Option<String>,
77    },
78
79    /// Generate a vanity address.
80    #[command(visible_alias = "va")]
81    Vanity(VanityArgs),
82
83    /// Convert a private key to an address.
84    #[command(visible_aliases = &["a", "addr"])]
85    Address {
86        /// If provided, the address will be derived from the specified private key.
87        #[arg(value_name = "PRIVATE_KEY")]
88        private_key_override: Option<String>,
89
90        #[command(flatten)]
91        wallet: WalletOpts,
92    },
93
94    /// Derive accounts from a mnemonic
95    #[command(visible_alias = "d")]
96    Derive {
97        /// The accounts will be derived from the specified mnemonic phrase.
98        #[arg(value_name = "MNEMONIC")]
99        mnemonic: String,
100
101        /// Number of accounts to display.
102        #[arg(long, short, default_value = "1")]
103        accounts: Option<u8>,
104
105        /// Insecure mode: display private keys in the terminal.
106        #[arg(long, default_value = "false")]
107        insecure: bool,
108    },
109
110    /// Sign a message or typed data.
111    #[command(visible_alias = "s")]
112    Sign {
113        /// The message, typed data, or hash to sign.
114        ///
115        /// Messages starting with 0x are expected to be hex encoded, which get decoded before
116        /// being signed.
117        ///
118        /// The message will be prefixed with the Ethereum Signed Message header and hashed before
119        /// signing, unless `--no-hash` is provided.
120        ///
121        /// Typed data can be provided as a json string or a file name.
122        /// Use --data flag to denote the message is a string of typed data.
123        /// Use --data --from-file to denote the message is a file name containing typed data.
124        /// The data will be combined and hashed using the EIP712 specification before signing.
125        /// The data should be formatted as JSON.
126        message: String,
127
128        /// Treat the message as JSON typed data.
129        #[arg(long)]
130        data: bool,
131
132        /// Treat the message as a file containing JSON typed data. Requires `--data`.
133        #[arg(long, requires = "data")]
134        from_file: bool,
135
136        /// Treat the message as a raw 32-byte hash and sign it directly without hashing it again.
137        #[arg(long, conflicts_with = "data")]
138        no_hash: bool,
139
140        #[command(flatten)]
141        wallet: WalletOpts,
142    },
143
144    /// EIP-7702 sign authorization.
145    #[command(visible_alias = "sa")]
146    SignAuth {
147        /// Address to sign authorization for.
148        address: Address,
149
150        #[command(flatten)]
151        rpc: RpcOpts,
152
153        #[arg(long)]
154        nonce: Option<u64>,
155
156        #[arg(long)]
157        chain: Option<Chain>,
158
159        /// If set, indicates the authorization will be broadcast by the signing account itself.
160        /// This means the nonce used will be the current nonce + 1 (to account for the
161        /// transaction that will include this authorization).
162        #[arg(long, conflicts_with = "nonce")]
163        self_broadcast: bool,
164
165        #[command(flatten)]
166        wallet: WalletOpts,
167    },
168
169    /// Verify the signature of a message.
170    #[command(visible_alias = "v")]
171    Verify {
172        /// The original message.
173        ///
174        /// Treats 0x-prefixed strings as hex encoded bytes.
175        /// Non 0x-prefixed strings are treated as raw input message.
176        ///
177        /// The message will be prefixed with the Ethereum Signed Message header and hashed before
178        /// signing, unless `--no-hash` is provided.
179        ///
180        /// Typed data can be provided as a json string or a file name.
181        /// Use --data flag to denote the message is a string of typed data.
182        /// Use --data --from-file to denote the message is a file name containing typed data.
183        /// The data will be combined and hashed using the EIP712 specification before signing.
184        /// The data should be formatted as JSON.
185        message: String,
186
187        /// The signature to verify.
188        signature: Signature,
189
190        /// The address of the message signer.
191        #[arg(long, short)]
192        address: Address,
193
194        /// Treat the message as JSON typed data.
195        #[arg(long)]
196        data: bool,
197
198        /// Treat the message as a file containing JSON typed data. Requires `--data`.
199        #[arg(long, requires = "data")]
200        from_file: bool,
201
202        /// Treat the message as a raw 32-byte hash and sign it directly without hashing it again.
203        #[arg(long, conflicts_with = "data")]
204        no_hash: bool,
205    },
206
207    /// Import a private key into an encrypted keystore.
208    #[command(visible_alias = "i")]
209    Import {
210        /// The name for the account in the keystore.
211        #[arg(value_name = "ACCOUNT_NAME")]
212        account_name: String,
213        /// If provided, keystore will be saved here instead of the default keystores directory
214        /// (~/.foundry/keystores)
215        #[arg(long, short)]
216        keystore_dir: Option<String>,
217        /// Password for the JSON keystore in cleartext
218        /// This is unsafe, we recommend using the default hidden password prompt
219        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
220        unsafe_password: Option<String>,
221        #[command(flatten)]
222        raw_wallet_options: RawWalletOpts,
223    },
224
225    /// List all the accounts in the keystore default directory
226    #[command(visible_alias = "ls")]
227    List(ListArgs),
228
229    /// Remove a wallet from the keystore.
230    ///
231    /// This command requires the wallet alias and will prompt for a password to ensure that only
232    /// an authorized user can remove the wallet.
233    #[command(visible_aliases = &["rm"], override_usage = "cast wallet remove --name <NAME>")]
234    Remove {
235        /// The alias (or name) of the wallet to remove.
236        #[arg(long, required = true)]
237        name: String,
238        /// Optionally provide the keystore directory if not provided. default directory will be
239        /// used (~/.foundry/keystores).
240        #[arg(long)]
241        dir: Option<String>,
242        /// Password for the JSON keystore in cleartext
243        /// This is unsafe, we recommend using the default hidden password prompt
244        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
245        unsafe_password: Option<String>,
246    },
247
248    /// Derives private key from mnemonic
249    #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])]
250    PrivateKey {
251        /// If provided, the private key will be derived from the specified mnemonic phrase.
252        #[arg(value_name = "MNEMONIC")]
253        mnemonic_override: Option<String>,
254
255        /// If provided, the private key will be derived using the
256        /// specified mnemonic index (if integer) or derivation path.
257        #[arg(value_name = "MNEMONIC_INDEX_OR_DERIVATION_PATH")]
258        mnemonic_index_or_derivation_path_override: Option<String>,
259
260        #[command(flatten)]
261        wallet: WalletOpts,
262    },
263    /// Get the public key for the given private key.
264    #[command(visible_aliases = &["pubkey"])]
265    PublicKey {
266        /// If provided, the public key will be derived from the specified private key.
267        #[arg(long = "raw-private-key", value_name = "PRIVATE_KEY")]
268        private_key_override: Option<String>,
269
270        #[command(flatten)]
271        wallet: WalletOpts,
272    },
273    /// Decrypt a keystore file to get the private key
274    #[command(name = "decrypt-keystore", visible_alias = "dk")]
275    DecryptKeystore {
276        /// The name for the account in the keystore.
277        #[arg(value_name = "ACCOUNT_NAME")]
278        account_name: String,
279        /// If not provided, keystore will try to be located at the default keystores directory
280        /// (~/.foundry/keystores)
281        #[arg(long, short)]
282        keystore_dir: Option<String>,
283        /// Password for the JSON keystore in cleartext
284        /// This is unsafe, we recommend using the default hidden password prompt
285        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
286        unsafe_password: Option<String>,
287    },
288
289    /// Change the password of a keystore file
290    #[command(name = "change-password", visible_alias = "cp")]
291    ChangePassword {
292        /// The name for the account in the keystore.
293        #[arg(value_name = "ACCOUNT_NAME")]
294        account_name: String,
295        /// If not provided, keystore will try to be located at the default keystores directory
296        /// (~/.foundry/keystores)
297        #[arg(long, short)]
298        keystore_dir: Option<String>,
299        /// Current password for the JSON keystore in cleartext
300        /// This is unsafe, we recommend using the default hidden password prompt
301        #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
302        unsafe_password: Option<String>,
303        /// New password for the JSON keystore in cleartext
304        /// This is unsafe, we recommend using the default hidden password prompt
305        #[arg(long, env = "CAST_UNSAFE_NEW_PASSWORD", value_name = "NEW_PASSWORD")]
306        unsafe_new_password: Option<String>,
307    },
308}
309
310impl WalletSubcommands {
311    pub async fn run(self) -> Result<()> {
312        match self {
313            Self::New { path, account_name, unsafe_password, number, password, force } => {
314                let mut rng = thread_rng();
315
316                let mut json_values = if shell::is_json() { Some(vec![]) } else { None };
317
318                let path = if let Some(path) = path {
319                    match dunce::canonicalize(&path) {
320                        Ok(path) => {
321                            if !path.is_dir() {
322                                // we require path to be an existing directory
323                                eyre::bail!("`{}` is not a directory", path.display());
324                            }
325                            Some(path)
326                        }
327                        Err(e) => {
328                            eyre::bail!(
329                                "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: {}",
330                                e
331                            );
332                        }
333                    }
334                } else if unsafe_password.is_some() || password {
335                    let path = Config::foundry_keystores_dir().ok_or_else(|| {
336                        eyre::eyre!("Could not find the default keystore directory.")
337                    })?;
338                    fs::create_dir_all(&path)?;
339                    Some(path)
340                } else {
341                    None
342                };
343
344                match path {
345                    Some(path) => {
346                        let password = if let Some(password) = unsafe_password {
347                            password
348                        } else {
349                            // if no --unsafe-password was provided read via stdin
350                            rpassword::prompt_password("Enter secret: ")?
351                        };
352
353                        // Prevent accidental overwriting: check all target files upfront
354                        if !force && let Some(ref acc_name) = account_name {
355                            let mut existing_files = Vec::new();
356
357                            for i in 0..number {
358                                let name = match number {
359                                    1 => acc_name.to_string(),
360                                    _ => format!("{}_{}", acc_name, i + 1),
361                                };
362                                let file_path = path.join(&name);
363                                if file_path.exists() {
364                                    existing_files.push(name);
365                                }
366                            }
367
368                            if !existing_files.is_empty() {
369                                use std::io::Write;
370
371                                sh_eprintln!("The following keystore file(s) already exist:")?;
372                                for file in &existing_files {
373                                    sh_eprintln!("   - {file}")?;
374                                }
375                                sh_print!(
376                                    "\nDo you want to overwrite all {} file(s)? [y/N]: ",
377                                    existing_files.len()
378                                )?;
379                                std::io::stdout().flush()?;
380
381                                let mut input = String::new();
382                                std::io::stdin().read_line(&mut input)?;
383
384                                if !input.trim().eq_ignore_ascii_case("y") {
385                                    eyre::bail!("Operation cancelled. No keystores were modified.");
386                                }
387                            }
388                        }
389                        for i in 0..number {
390                            let account_name_ref =
391                                account_name.as_deref().map(|name| match number {
392                                    1 => name.to_string(),
393                                    _ => format!("{}_{}", name, i + 1),
394                                });
395
396                            let (wallet, uuid) = PrivateKeySigner::new_keystore(
397                                &path,
398                                &mut rng,
399                                password.clone(),
400                                account_name_ref.as_deref(),
401                            )?;
402                            let identifier = account_name_ref.as_deref().unwrap_or(&uuid);
403
404                            if let Some(json) = json_values.as_mut() {
405                                json.push(if shell::verbosity() > 0 {
406                                json!({
407                                    "address": wallet.address().to_checksum(None),
408                                    "public_key": format!("0x{}", hex::encode(wallet.public_key())),
409                                    "path": format!("{}", path.join(identifier).display()),
410                                })
411                            } else {
412                                json!({
413                                    "address": wallet.address().to_checksum(None),
414                                    "path": format!("{}", path.join(identifier).display()),
415                                })
416                            });
417                            } else {
418                                sh_println!(
419                                    "Created new encrypted keystore file: {}",
420                                    path.join(identifier).display()
421                                )?;
422                                sh_println!("Address:    {}", wallet.address().to_checksum(None))?;
423                                if shell::verbosity() > 0 {
424                                    sh_println!(
425                                        "Public key: 0x{}",
426                                        hex::encode(wallet.public_key())
427                                    )?;
428                                }
429                            }
430                        }
431                    }
432                    None => {
433                        for _ in 0..number {
434                            let wallet = PrivateKeySigner::random_with(&mut rng);
435
436                            if let Some(json) = json_values.as_mut() {
437                                json.push(if shell::verbosity() > 0 {
438                                json!({
439                                    "address": wallet.address().to_checksum(None),
440                                    "public_key": format!("0x{}", hex::encode(wallet.public_key())),
441                                    "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
442                                })
443                            } else {
444                                json!({
445                                    "address": wallet.address().to_checksum(None),
446                                    "private_key": format!("0x{}", hex::encode(wallet.credential().to_bytes())),
447                                })
448                            });
449                            } else {
450                                sh_println!("Successfully created new keypair.")?;
451                                sh_println!("Address:     {}", wallet.address().to_checksum(None))?;
452                                if shell::verbosity() > 0 {
453                                    sh_println!(
454                                        "Public key:  0x{}",
455                                        hex::encode(wallet.public_key())
456                                    )?;
457                                }
458                                sh_println!(
459                                    "Private key: 0x{}",
460                                    hex::encode(wallet.credential().to_bytes())
461                                )?;
462                            }
463                        }
464                    }
465                }
466
467                if let Some(json) = json_values.as_ref() {
468                    sh_println!("{}", serde_json::to_string_pretty(json)?)?;
469                }
470            }
471            Self::NewMnemonic { words, accounts, entropy } => {
472                let phrase = if let Some(entropy) = entropy {
473                    let entropy = Entropy::from_slice(hex::decode(entropy)?)?;
474                    Mnemonic::<English>::new_from_entropy(entropy).to_phrase()
475                } else {
476                    let mut rng = thread_rng();
477                    Mnemonic::<English>::new_with_count(&mut rng, words)?.to_phrase()
478                };
479
480                let format_json = shell::is_json();
481
482                if !format_json {
483                    sh_println!("{}", "Generating mnemonic from provided entropy...".yellow())?;
484                }
485
486                let builder = MnemonicBuilder::<English>::default().phrase(phrase.as_str());
487                let derivation_path = "m/44'/60'/0'/0/";
488                let wallets = (0..accounts)
489                    .map(|i| builder.clone().derivation_path(format!("{derivation_path}{i}")))
490                    .collect::<Result<Vec<_>, _>>()?;
491                let wallets =
492                    wallets.into_iter().map(|b| b.build()).collect::<Result<Vec<_>, _>>()?;
493
494                if !format_json {
495                    sh_println!("{}", "Successfully generated a new mnemonic.".green())?;
496                    sh_println!("Phrase:\n{phrase}")?;
497                    sh_println!("\nAccounts:")?;
498                }
499
500                let mut accounts = json!([]);
501                for (i, wallet) in wallets.iter().enumerate() {
502                    let public_key = hex::encode(wallet.public_key());
503                    let private_key = hex::encode(wallet.credential().to_bytes());
504                    if format_json {
505                        accounts.as_array_mut().unwrap().push(if shell::verbosity() > 0 {
506                            json!({
507                                "address": format!("{}", wallet.address()),
508                                "public_key": format!("0x{}", public_key),
509                                "private_key": format!("0x{}", private_key),
510                            })
511                        } else {
512                            json!({
513                                "address": format!("{}", wallet.address()),
514                                "private_key": format!("0x{}", private_key),
515                            })
516                        });
517                    } else {
518                        sh_println!("- Account {i}:")?;
519                        sh_println!("Address:     {}", wallet.address())?;
520                        if shell::verbosity() > 0 {
521                            sh_println!("Public key:  0x{}", public_key)?;
522                        }
523                        sh_println!("Private key: 0x{}\n", private_key)?;
524                    }
525                }
526
527                if format_json {
528                    let obj = json!({
529                        "mnemonic": phrase,
530                        "accounts": accounts,
531                    });
532                    sh_println!("{}", serde_json::to_string_pretty(&obj)?)?;
533                }
534            }
535            Self::Vanity(cmd) => {
536                cmd.run()?;
537            }
538            Self::Address { wallet, private_key_override } => {
539                let wallet = private_key_override
540                    .map(|pk| WalletOpts {
541                        raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
542                        ..Default::default()
543                    })
544                    .unwrap_or(wallet)
545                    .signer()
546                    .await?;
547                let addr = wallet.address();
548                sh_println!("{}", addr.to_checksum(None))?;
549            }
550            Self::Derive { mnemonic, accounts, insecure } => {
551                let format_json = shell::is_json();
552                let mut accounts_json = json!([]);
553                for i in 0..accounts.unwrap_or(1) {
554                    let wallet = WalletOpts {
555                        raw: RawWalletOpts {
556                            mnemonic: Some(mnemonic.clone()),
557                            mnemonic_index: i as u32,
558                            ..Default::default()
559                        },
560                        ..Default::default()
561                    }
562                    .signer()
563                    .await?;
564
565                    match wallet {
566                        WalletSigner::Local(local_wallet) => {
567                            let address = local_wallet.address().to_checksum(None);
568                            let private_key = hex::encode(local_wallet.credential().to_bytes());
569                            if format_json {
570                                if insecure {
571                                    accounts_json.as_array_mut().unwrap().push(json!({
572                                        "address": format!("{}", address),
573                                        "private_key": format!("0x{}", private_key),
574                                    }));
575                                } else {
576                                    accounts_json.as_array_mut().unwrap().push(json!({
577                                        "address": format!("{}", address)
578                                    }));
579                                }
580                            } else {
581                                sh_println!("- Account {i}:")?;
582                                if insecure {
583                                    sh_println!("Address:     {}", address)?;
584                                    sh_println!("Private key: 0x{}\n", private_key)?;
585                                } else {
586                                    sh_println!("Address:     {}\n", address)?;
587                                }
588                            }
589                        }
590                        _ => eyre::bail!("Only local wallets are supported by this command"),
591                    }
592                }
593
594                if format_json {
595                    sh_println!("{}", serde_json::to_string_pretty(&accounts_json)?)?;
596                }
597            }
598            Self::PublicKey { wallet, private_key_override } => {
599                let wallet = private_key_override
600                    .map(|pk| WalletOpts {
601                        raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
602                        ..Default::default()
603                    })
604                    .unwrap_or(wallet)
605                    .signer()
606                    .await?;
607
608                let public_key = match wallet {
609                    WalletSigner::Local(wallet) => wallet.public_key(),
610                    _ => eyre::bail!("Only local wallets are supported by this command"),
611                };
612
613                sh_println!("0x{}", hex::encode(public_key))?;
614            }
615            Self::Sign { message, data, from_file, no_hash, wallet } => {
616                let wallet = wallet.signer().await?;
617                let sig = if data {
618                    let typed_data: TypedData = if from_file {
619                        // data is a file name, read json from file
620                        foundry_common::fs::read_json_file(message.as_ref())?
621                    } else {
622                        // data is a json string
623                        serde_json::from_str(&message)?
624                    };
625                    wallet.sign_dynamic_typed_data(&typed_data).await?
626                } else if no_hash {
627                    wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
628                } else {
629                    wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
630                };
631
632                if shell::verbosity() > 0 {
633                    if shell::is_json() {
634                        sh_println!(
635                            "{}",
636                            serde_json::to_string_pretty(&json!({
637                                "message": message,
638                                "address": wallet.address(),
639                                "signature": hex::encode(sig.as_bytes()),
640                            }))?
641                        )?;
642                    } else {
643                        sh_println!(
644                            "Successfully signed!\n   Message: {}\n   Address: {}\n   Signature: 0x{}",
645                            message,
646                            wallet.address(),
647                            hex::encode(sig.as_bytes()),
648                        )?;
649                    }
650                } else {
651                    // Pipe friendly output
652                    sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
653                }
654            }
655            Self::SignAuth { rpc, nonce, chain, wallet, address, self_broadcast } => {
656                let wallet = wallet.signer().await?;
657                let provider = utils::get_provider(&rpc.load_config()?)?;
658                let nonce = if let Some(nonce) = nonce {
659                    nonce
660                } else {
661                    let current_nonce = provider.get_transaction_count(wallet.address()).await?;
662                    if self_broadcast {
663                        // When self-broadcasting, the authorization nonce needs to be +1
664                        // because the transaction itself will consume the current nonce
665                        current_nonce + 1
666                    } else {
667                        current_nonce
668                    }
669                };
670                let chain_id = if let Some(chain) = chain {
671                    chain.id()
672                } else {
673                    provider.get_chain_id().await?
674                };
675                let auth = Authorization { chain_id: U256::from(chain_id), address, nonce };
676                let signature = wallet.sign_hash(&auth.signature_hash()).await?;
677                let auth = auth.into_signed(signature);
678
679                if shell::verbosity() > 0 {
680                    if shell::is_json() {
681                        sh_println!(
682                            "{}",
683                            serde_json::to_string_pretty(&json!({
684                                "nonce": nonce,
685                                "chain_id": chain_id,
686                                "address": wallet.address(),
687                                "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
688                            }))?
689                        )?;
690                    } else {
691                        sh_println!(
692                            "Successfully signed!\n   Nonce: {}\n   Chain ID: {}\n   Address: {}\n   Signature: 0x{}",
693                            nonce,
694                            chain_id,
695                            wallet.address(),
696                            hex::encode_prefixed(alloy_rlp::encode(&auth)),
697                        )?;
698                    }
699                } else {
700                    // Pipe friendly output
701                    sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
702                }
703            }
704            Self::Verify { message, signature, address, data, from_file, no_hash } => {
705                let recovered_address = if data {
706                    let typed_data: TypedData = if from_file {
707                        // data is a file name, read json from file
708                        foundry_common::fs::read_json_file(message.as_ref())?
709                    } else {
710                        // data is a json string
711                        serde_json::from_str(&message)?
712                    };
713                    Self::recover_address_from_typed_data(&typed_data, &signature)?
714                } else if no_hash {
715                    Self::recover_address_from_message_no_hash(
716                        &hex::decode(&message)?[..].try_into()?,
717                        &signature,
718                    )?
719                } else {
720                    Self::recover_address_from_message(&message, &signature)?
721                };
722
723                if address == recovered_address {
724                    sh_println!("Validation succeeded. Address {address} signed this message.")?;
725                } else {
726                    eyre::bail!("Validation failed. Address {address} did not sign this message.");
727                }
728            }
729            Self::Import { account_name, keystore_dir, unsafe_password, raw_wallet_options } => {
730                // Set up keystore directory
731                let dir = if let Some(path) = keystore_dir {
732                    Path::new(&path).to_path_buf()
733                } else {
734                    Config::foundry_keystores_dir().ok_or_else(|| {
735                        eyre::eyre!("Could not find the default keystore directory.")
736                    })?
737                };
738
739                fs::create_dir_all(&dir)?;
740
741                // check if account exists already
742                let keystore_path = Path::new(&dir).join(&account_name);
743                if keystore_path.exists() {
744                    eyre::bail!("Keystore file already exists at {}", keystore_path.display());
745                }
746
747                // get wallet
748                let wallet = raw_wallet_options
749                    .signer()?
750                    .and_then(|s| match s {
751                        WalletSigner::Local(s) => Some(s),
752                        _ => None,
753                    })
754                    .ok_or_else(|| {
755                        eyre::eyre!(
756                            "\
757Did you set a private key or mnemonic?
758Run `cast wallet import --help` and use the corresponding CLI
759flag to set your key via:
760--private-key, --mnemonic-path or --interactive."
761                        )
762                    })?;
763
764                let private_key = wallet.credential().to_bytes();
765                let password = if let Some(password) = unsafe_password {
766                    password
767                } else {
768                    // if no --unsafe-password was provided read via stdin
769                    rpassword::prompt_password("Enter password: ")?
770                };
771
772                let mut rng = thread_rng();
773                let (wallet, _) = PrivateKeySigner::encrypt_keystore(
774                    dir,
775                    &mut rng,
776                    private_key,
777                    password,
778                    Some(&account_name),
779                )?;
780                let address = wallet.address();
781                let success_message = format!(
782                    "`{}` keystore was saved successfully. Address: {:?}",
783                    &account_name, address,
784                );
785                sh_println!("{}", success_message.green())?;
786            }
787            Self::List(cmd) => {
788                cmd.run().await?;
789            }
790            Self::Remove { name, dir, unsafe_password } => {
791                let dir = if let Some(path) = dir {
792                    Path::new(&path).to_path_buf()
793                } else {
794                    Config::foundry_keystores_dir().ok_or_else(|| {
795                        eyre::eyre!("Could not find the default keystore directory.")
796                    })?
797                };
798
799                let keystore_path = Path::new(&dir).join(&name);
800                if !keystore_path.exists() {
801                    eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
802                }
803
804                let password = if let Some(pwd) = unsafe_password {
805                    pwd
806                } else {
807                    rpassword::prompt_password("Enter password: ")?
808                };
809
810                if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
811                    eyre::bail!("Invalid password - wallet removal cancelled");
812                }
813
814                std::fs::remove_file(&keystore_path).wrap_err_with(|| {
815                    format!("Failed to remove keystore file at {}", keystore_path.display())
816                })?;
817
818                let success_message = format!("`{}` keystore was removed successfully.", &name);
819                sh_println!("{}", success_message.green())?;
820            }
821            Self::PrivateKey {
822                wallet,
823                mnemonic_override,
824                mnemonic_index_or_derivation_path_override,
825            } => {
826                let (index_override, derivation_path_override) =
827                    match mnemonic_index_or_derivation_path_override {
828                        Some(value) => match value.parse::<u32>() {
829                            Ok(index) => (Some(index), None),
830                            Err(_) => (None, Some(value)),
831                        },
832                        None => (None, None),
833                    };
834                let wallet = WalletOpts {
835                    raw: RawWalletOpts {
836                        mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
837                        mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
838                        hd_path: derivation_path_override.or(wallet.raw.hd_path),
839                        ..wallet.raw
840                    },
841                    ..wallet
842                }
843                .signer()
844                .await?;
845                match wallet {
846                    WalletSigner::Local(wallet) => {
847                        if shell::verbosity() > 0 {
848                            sh_println!("Address:     {}", wallet.address())?;
849                            sh_println!(
850                                "Private key: 0x{}",
851                                hex::encode(wallet.credential().to_bytes())
852                            )?;
853                        } else {
854                            sh_println!("0x{}", hex::encode(wallet.credential().to_bytes()))?;
855                        }
856                    }
857                    _ => {
858                        eyre::bail!("Only local wallets are supported by this command.");
859                    }
860                }
861            }
862            Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
863                // Set up keystore directory
864                let dir = if let Some(path) = keystore_dir {
865                    Path::new(&path).to_path_buf()
866                } else {
867                    Config::foundry_keystores_dir().ok_or_else(|| {
868                        eyre::eyre!("Could not find the default keystore directory.")
869                    })?
870                };
871
872                let keypath = dir.join(&account_name);
873
874                if !keypath.exists() {
875                    eyre::bail!("Keystore file does not exist at {}", keypath.display());
876                }
877
878                let password = if let Some(password) = unsafe_password {
879                    password
880                } else {
881                    // if no --unsafe-password was provided read via stdin
882                    rpassword::prompt_password("Enter password: ")?
883                };
884
885                let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
886
887                let private_key = B256::from_slice(&wallet.credential().to_bytes());
888
889                let success_message =
890                    format!("{}'s private key is: {}", &account_name, private_key);
891
892                sh_println!("{}", success_message.green())?;
893            }
894            Self::ChangePassword {
895                account_name,
896                keystore_dir,
897                unsafe_password,
898                unsafe_new_password,
899            } => {
900                // Set up keystore directory
901                let dir = if let Some(path) = keystore_dir {
902                    Path::new(&path).to_path_buf()
903                } else {
904                    Config::foundry_keystores_dir().ok_or_else(|| {
905                        eyre::eyre!("Could not find the default keystore directory.")
906                    })?
907                };
908
909                let keypath = dir.join(&account_name);
910
911                if !keypath.exists() {
912                    eyre::bail!("Keystore file does not exist at {}", keypath.display());
913                }
914
915                let current_password = if let Some(password) = unsafe_password {
916                    password
917                } else {
918                    // if no --unsafe-password was provided read via stdin
919                    rpassword::prompt_password("Enter current password: ")?
920                };
921
922                // decrypt the keystore to verify the current password and get the private key
923                let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
924                    .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
925
926                let new_password = if let Some(password) = unsafe_new_password {
927                    password
928                } else {
929                    // if no --unsafe-new-password was provided read via stdin
930                    rpassword::prompt_password("Enter new password: ")?
931                };
932
933                if current_password == new_password {
934                    eyre::bail!("New password cannot be the same as the current password");
935                }
936
937                // Create a new keystore with the new password
938                let private_key = wallet.credential().to_bytes();
939                let mut rng = thread_rng();
940                let (wallet, _) = PrivateKeySigner::encrypt_keystore(
941                    dir,
942                    &mut rng,
943                    private_key,
944                    new_password,
945                    Some(&account_name),
946                )?;
947
948                let success_message = format!(
949                    "Password for keystore `{}` was changed successfully. Address: {:?}",
950                    &account_name,
951                    wallet.address(),
952                );
953                sh_println!("{}", success_message.green())?;
954            }
955        };
956
957        Ok(())
958    }
959
960    /// Recovers an address from the specified message and signature.
961    ///
962    /// Note: This attempts to decode the message as hex if it starts with 0x.
963    fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
964        let message = Self::hex_str_to_bytes(message)?;
965        Ok(signature.recover_address_from_msg(message)?)
966    }
967
968    /// Recovers an address from the specified message and signature.
969    fn recover_address_from_message_no_hash(
970        prehash: &B256,
971        signature: &Signature,
972    ) -> Result<Address> {
973        Ok(signature.recover_address_from_prehash(prehash)?)
974    }
975
976    /// Recovers an address from the specified EIP-712 typed data and signature.
977    fn recover_address_from_typed_data(
978        typed_data: &TypedData,
979        signature: &Signature,
980    ) -> Result<Address> {
981        Ok(signature.recover_address_from_prehash(&typed_data.eip712_signing_hash()?)?)
982    }
983
984    /// Strips the 0x prefix from a hex string and decodes it to bytes.
985    ///
986    /// Treats the string as raw bytes if it doesn't start with 0x.
987    fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
988        Ok(match s.strip_prefix("0x") {
989            Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
990            None => s.as_bytes().to_vec(),
991        })
992    }
993}
994
995#[cfg(test)]
996mod tests {
997    use super::*;
998    use alloy_primitives::{address, keccak256};
999    use std::str::FromStr;
1000
1001    #[test]
1002    fn can_parse_wallet_sign_message() {
1003        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
1004        match args {
1005            WalletSubcommands::Sign { message, data, from_file, .. } => {
1006                assert_eq!(message, "deadbeef".to_string());
1007                assert!(!data);
1008                assert!(!from_file);
1009            }
1010            _ => panic!("expected WalletSubcommands::Sign"),
1011        }
1012    }
1013
1014    #[test]
1015    fn can_parse_wallet_sign_hex_message() {
1016        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
1017        match args {
1018            WalletSubcommands::Sign { message, data, from_file, .. } => {
1019                assert_eq!(message, "0xdeadbeef".to_string());
1020                assert!(!data);
1021                assert!(!from_file);
1022            }
1023            _ => panic!("expected WalletSubcommands::Sign"),
1024        }
1025    }
1026
1027    #[test]
1028    fn can_verify_signed_hex_message() {
1029        let message = "hello";
1030        let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
1031        let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
1032        let recovered_address =
1033            WalletSubcommands::recover_address_from_message(message, &signature);
1034        assert!(recovered_address.is_ok());
1035        assert_eq!(address, recovered_address.unwrap());
1036    }
1037
1038    #[test]
1039    fn can_verify_signed_hex_message_no_hash() {
1040        let prehash = keccak256("hello");
1041        let signature = Signature::from_str("433ec3d37e4f1253df15e2dea412fed8e915737730f74b3dfb1353268f932ef5557c9158e0b34bce39de28d11797b42e9b1acb2749230885fe075aedc3e491a41b").unwrap();
1042        let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); // private key = 1
1043        let recovered_address =
1044            WalletSubcommands::recover_address_from_message_no_hash(&prehash, &signature);
1045        assert!(recovered_address.is_ok());
1046        assert_eq!(address, recovered_address.unwrap());
1047    }
1048
1049    #[test]
1050    fn can_verify_signed_typed_data() {
1051        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();
1052        let signature = Signature::from_str("0285ff83b93bd01c14e201943af7454fe2bc6c98be707a73888c397d6ae3b0b92f73ca559f81cbb19fe4e0f1dc4105bd7b647c6a84b033057977cf2ec982daf71b").unwrap();
1053        let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); // private key = 1
1054        let recovered_address =
1055            WalletSubcommands::recover_address_from_typed_data(&typed_data, &signature);
1056        assert!(recovered_address.is_ok());
1057        assert_eq!(address, recovered_address.unwrap());
1058    }
1059
1060    #[test]
1061    fn can_parse_wallet_sign_data() {
1062        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
1063        match args {
1064            WalletSubcommands::Sign { message, data, from_file, .. } => {
1065                assert_eq!(message, "{ ... }".to_string());
1066                assert!(data);
1067                assert!(!from_file);
1068            }
1069            _ => panic!("expected WalletSubcommands::Sign"),
1070        }
1071    }
1072
1073    #[test]
1074    fn can_parse_wallet_sign_data_file() {
1075        let args = WalletSubcommands::parse_from([
1076            "foundry-cli",
1077            "sign",
1078            "--data",
1079            "--from-file",
1080            "tests/data/typed_data.json",
1081        ]);
1082        match args {
1083            WalletSubcommands::Sign { message, data, from_file, .. } => {
1084                assert_eq!(message, "tests/data/typed_data.json".to_string());
1085                assert!(data);
1086                assert!(from_file);
1087            }
1088            _ => panic!("expected WalletSubcommands::Sign"),
1089        }
1090    }
1091
1092    #[test]
1093    fn can_parse_wallet_change_password() {
1094        let args = WalletSubcommands::parse_from([
1095            "foundry-cli",
1096            "change-password",
1097            "my_account",
1098            "--unsafe-password",
1099            "old_password",
1100            "--unsafe-new-password",
1101            "new_password",
1102        ]);
1103        match args {
1104            WalletSubcommands::ChangePassword {
1105                account_name,
1106                keystore_dir,
1107                unsafe_password,
1108                unsafe_new_password,
1109            } => {
1110                assert_eq!(account_name, "my_account".to_string());
1111                assert_eq!(unsafe_password, Some("old_password".to_string()));
1112                assert_eq!(unsafe_new_password, Some("new_password".to_string()));
1113                assert!(keystore_dir.is_none());
1114            }
1115            _ => panic!("expected WalletSubcommands::ChangePassword"),
1116        }
1117    }
1118
1119    #[test]
1120    fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1121        let result = WalletSubcommands::try_parse_from([
1122            "foundry-cli",
1123            "sign-auth",
1124            "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1125            "--nonce",
1126            "42",
1127            "--self-broadcast",
1128        ]);
1129        assert!(
1130            result.is_err(),
1131            "expected error when both --nonce and --self-broadcast are provided"
1132        );
1133    }
1134}