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