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                        _ => eyre::bail!("Only local wallets are supported by this command"),
597                    }
598                }
599
600                if format_json {
601                    print_json_success(accounts_json)?;
602                }
603            }
604            Self::PublicKey { wallet, private_key_override } => {
605                let wallet = private_key_override
606                    .map(|pk| WalletOpts {
607                        raw: RawWalletOpts { private_key: Some(pk), ..Default::default() },
608                        ..Default::default()
609                    })
610                    .unwrap_or(wallet)
611                    .signer()
612                    .await?;
613
614                let public_key = match wallet {
615                    WalletSigner::Local(wallet) => wallet.public_key(),
616                    _ => eyre::bail!("Only local wallets are supported by this command"),
617                };
618
619                print_scalar(format!("0x{}", hex::encode(public_key)))?;
620            }
621            Self::Sign { message, data, from_file, no_hash, wallet } => {
622                let wallet = wallet.signer().await?;
623                let sig = if data {
624                    let typed_data: TypedData = if from_file {
625                        // data is a file name, read json from file
626                        foundry_common::fs::read_json_file(message.as_ref())?
627                    } else {
628                        // data is a json string
629                        serde_json::from_str(&message)?
630                    };
631                    wallet.sign_dynamic_typed_data(&typed_data).await?
632                } else if no_hash {
633                    wallet.sign_hash(&hex::decode(&message)?[..].try_into()?).await?
634                } else {
635                    wallet.sign_message(&Self::hex_str_to_bytes(&message)?).await?
636                };
637
638                if shell::verbosity() > 0 {
639                    if shell::is_json() {
640                        print_json_success(json!({
641                            "message": message,
642                            "address": wallet.address(),
643                            "signature": hex::encode(sig.as_bytes()),
644                        }))?;
645                    } else {
646                        sh_status!("Successfully signed!")?;
647                        sh_status!("   Message: {message}")?;
648                        sh_status!("   Address: {}", wallet.address())?;
649                        sh_println!("0x{}", hex::encode(sig.as_bytes()))?;
650                    }
651                } else {
652                    print_scalar(format!("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                        print_json_success(json!({
682                            "nonce": nonce,
683                            "chain_id": chain_id,
684                            "address": wallet.address(),
685                            "signature": hex::encode_prefixed(alloy_rlp::encode(&auth)),
686                        }))?;
687                    } else {
688                        sh_status!("Successfully signed!")?;
689                        sh_status!("   Nonce: {nonce}")?;
690                        sh_status!("   Chain ID: {chain_id}")?;
691                        sh_status!("   Address: {}", wallet.address())?;
692                        sh_println!("{}", hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
693                    }
694                } else {
695                    print_scalar(hex::encode_prefixed(alloy_rlp::encode(&auth)))?;
696                }
697            }
698            Self::Verify { message, signature, address, data, from_file, no_hash } => {
699                let recovered_address = if data {
700                    let typed_data: TypedData = if from_file {
701                        // data is a file name, read json from file
702                        foundry_common::fs::read_json_file(message.as_ref())?
703                    } else {
704                        // data is a json string
705                        serde_json::from_str(&message)?
706                    };
707                    Self::recover_address_from_typed_data(&typed_data, &signature)?
708                } else if no_hash {
709                    Self::recover_address_from_message_no_hash(
710                        &hex::decode(&message)?[..].try_into()?,
711                        &signature,
712                    )?
713                } else {
714                    Self::recover_address_from_message(&message, &signature)?
715                };
716
717                if address == recovered_address {
718                    if shell::is_json() {
719                        print_json_success(json!({"address": address, "result": true}))?;
720                    } else {
721                        sh_println!(
722                            "Validation succeeded. Address {address} signed this message."
723                        )?;
724                    }
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                if shell::is_json() {
782                    print_json_success(json!({"account": account_name, "address": address}))?;
783                } else {
784                    sh_println!(
785                        "{}",
786                        format!(
787                            "`{account_name}` keystore was saved successfully. Address: {address:?}"
788                        )
789                        .green()
790                    )?;
791                }
792            }
793            Self::List(cmd) => {
794                cmd.run().await?;
795            }
796            Self::Session(args) => {
797                args.run().await?;
798            }
799            Self::Remove { name, dir, unsafe_password } => {
800                let dir = if let Some(path) = dir {
801                    Path::new(&path).to_path_buf()
802                } else {
803                    Config::foundry_keystores_dir().ok_or_else(|| {
804                        eyre::eyre!("Could not find the default keystore directory.")
805                    })?
806                };
807
808                let keystore_path = Path::new(&dir).join(&name);
809                if !keystore_path.exists() {
810                    eyre::bail!("Keystore file does not exist at {}", keystore_path.display());
811                }
812
813                let password = if let Some(pwd) = unsafe_password {
814                    pwd
815                } else {
816                    rpassword::prompt_password("Enter password: ")?
817                };
818
819                if PrivateKeySigner::decrypt_keystore(&keystore_path, password).is_err() {
820                    eyre::bail!("Invalid password - wallet removal cancelled");
821                }
822
823                std::fs::remove_file(&keystore_path).wrap_err_with(|| {
824                    format!("Failed to remove keystore file at {}", keystore_path.display())
825                })?;
826
827                if shell::is_json() {
828                    print_json_success(json!({"account": name, "removed": true}))?;
829                } else {
830                    sh_println!(
831                        "{}",
832                        format!("`{name}` keystore was removed successfully.").green()
833                    )?;
834                }
835            }
836            Self::PrivateKey {
837                wallet,
838                mnemonic_override,
839                mnemonic_index_or_derivation_path_override,
840            } => {
841                let (index_override, derivation_path_override) =
842                    match mnemonic_index_or_derivation_path_override {
843                        Some(value) => match value.parse::<u32>() {
844                            Ok(index) => (Some(index), None),
845                            Err(_) => (None, Some(value)),
846                        },
847                        None => (None, None),
848                    };
849                let wallet = WalletOpts {
850                    raw: RawWalletOpts {
851                        mnemonic: mnemonic_override.or(wallet.raw.mnemonic),
852                        mnemonic_index: index_override.unwrap_or(wallet.raw.mnemonic_index),
853                        hd_path: derivation_path_override.or(wallet.raw.hd_path),
854                        ..wallet.raw
855                    },
856                    ..wallet
857                }
858                .signer()
859                .await?;
860                match wallet {
861                    WalletSigner::Local(wallet) => {
862                        let private_key =
863                            format!("0x{}", hex::encode(wallet.credential().to_bytes()));
864                        if shell::verbosity() > 0 {
865                            if shell::is_json() {
866                                print_json_success(json!({
867                                    "address": wallet.address(),
868                                    "private_key": private_key,
869                                }))?;
870                            } else {
871                                sh_println!("Address:     {}", wallet.address())?;
872                                sh_println!("Private key: {private_key}")?;
873                            }
874                        } else {
875                            print_scalar(private_key)?;
876                        }
877                    }
878                    _ => {
879                        eyre::bail!("Only local wallets are supported by this command.");
880                    }
881                }
882            }
883            Self::DecryptKeystore { account_name, keystore_dir, unsafe_password } => {
884                // Set up keystore directory
885                let dir = if let Some(path) = keystore_dir {
886                    Path::new(&path).to_path_buf()
887                } else {
888                    Config::foundry_keystores_dir().ok_or_else(|| {
889                        eyre::eyre!("Could not find the default keystore directory.")
890                    })?
891                };
892
893                let keypath = dir.join(&account_name);
894
895                if !keypath.exists() {
896                    eyre::bail!("Keystore file does not exist at {}", keypath.display());
897                }
898
899                let password = if let Some(password) = unsafe_password {
900                    password
901                } else {
902                    // if no --unsafe-password was provided read via stdin
903                    rpassword::prompt_password("Enter password: ")?
904                };
905
906                let wallet = PrivateKeySigner::decrypt_keystore(keypath, password)?;
907
908                let private_key = B256::from_slice(&wallet.credential().to_bytes());
909                if shell::is_json() {
910                    print_json_success(
911                        json!({"account": account_name, "private_key": private_key}),
912                    )?;
913                } else {
914                    sh_println!(
915                        "{}",
916                        format!("{account_name}'s private key is: {private_key}").green()
917                    )?;
918                }
919            }
920            Self::ChangePassword {
921                account_name,
922                keystore_dir,
923                unsafe_password,
924                unsafe_new_password,
925            } => {
926                // Set up keystore directory
927                let dir = if let Some(path) = keystore_dir {
928                    Path::new(&path).to_path_buf()
929                } else {
930                    Config::foundry_keystores_dir().ok_or_else(|| {
931                        eyre::eyre!("Could not find the default keystore directory.")
932                    })?
933                };
934
935                let keypath = dir.join(&account_name);
936
937                if !keypath.exists() {
938                    eyre::bail!("Keystore file does not exist at {}", keypath.display());
939                }
940
941                let current_password = if let Some(password) = unsafe_password {
942                    password
943                } else {
944                    // if no --unsafe-password was provided read via stdin
945                    rpassword::prompt_password("Enter current password: ")?
946                };
947
948                // decrypt the keystore to verify the current password and get the private key
949                let wallet = PrivateKeySigner::decrypt_keystore(&keypath, current_password.clone())
950                    .map_err(|_| eyre::eyre!("Invalid password - password change cancelled"))?;
951
952                let new_password = if let Some(password) = unsafe_new_password {
953                    password
954                } else {
955                    // if no --unsafe-new-password was provided read via stdin
956                    rpassword::prompt_password("Enter new password: ")?
957                };
958
959                if current_password == new_password {
960                    eyre::bail!("New password cannot be the same as the current password");
961                }
962
963                // Create a new keystore with the new password
964                let private_key = wallet.credential().to_bytes();
965                let mut rng = thread_rng();
966                let (wallet, _) = PrivateKeySigner::encrypt_keystore(
967                    dir,
968                    &mut rng,
969                    private_key,
970                    new_password,
971                    Some(&account_name),
972                )?;
973
974                let address = wallet.address();
975                if shell::is_json() {
976                    print_json_success(json!({"account": account_name, "address": address}))?;
977                } else {
978                    sh_println!(
979                        "{}",
980                        format!(
981                            "Password for keystore `{account_name}` was changed successfully. Address: {address:?}"
982                        )
983                        .green()
984                    )?;
985                }
986            }
987        };
988
989        Ok(())
990    }
991
992    /// Recovers an address from the specified message and signature.
993    ///
994    /// Note: This attempts to decode the message as hex if it starts with 0x.
995    fn recover_address_from_message(message: &str, signature: &Signature) -> Result<Address> {
996        let message = Self::hex_str_to_bytes(message)?;
997        Ok(signature.recover_address_from_msg(message)?)
998    }
999
1000    /// Recovers an address from the specified message and signature.
1001    fn recover_address_from_message_no_hash(
1002        prehash: &B256,
1003        signature: &Signature,
1004    ) -> Result<Address> {
1005        Ok(signature.recover_address_from_prehash(prehash)?)
1006    }
1007
1008    /// Recovers an address from the specified EIP-712 typed data and signature.
1009    fn recover_address_from_typed_data(
1010        typed_data: &TypedData,
1011        signature: &Signature,
1012    ) -> Result<Address> {
1013        Ok(signature.recover_address_from_prehash(&typed_data.eip712_signing_hash()?)?)
1014    }
1015
1016    /// Strips the 0x prefix from a hex string and decodes it to bytes.
1017    ///
1018    /// Treats the string as raw bytes if it doesn't start with 0x.
1019    fn hex_str_to_bytes(s: &str) -> Result<Vec<u8>> {
1020        Ok(match s.strip_prefix("0x") {
1021            Some(data) => hex::decode(data).wrap_err("Could not decode 0x-prefixed string.")?,
1022            None => s.as_bytes().to_vec(),
1023        })
1024    }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::{session::SessionSubcommands, *};
1030    use alloy_primitives::{address, keccak256};
1031    use std::str::FromStr;
1032
1033    #[test]
1034    fn can_parse_wallet_sign_message() {
1035        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "deadbeef"]);
1036        match args {
1037            WalletSubcommands::Sign { message, data, from_file, .. } => {
1038                assert_eq!(message, "deadbeef".to_string());
1039                assert!(!data);
1040                assert!(!from_file);
1041            }
1042            _ => panic!("expected WalletSubcommands::Sign"),
1043        }
1044    }
1045
1046    #[test]
1047    fn can_parse_wallet_sign_hex_message() {
1048        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "0xdeadbeef"]);
1049        match args {
1050            WalletSubcommands::Sign { message, data, from_file, .. } => {
1051                assert_eq!(message, "0xdeadbeef".to_string());
1052                assert!(!data);
1053                assert!(!from_file);
1054            }
1055            _ => panic!("expected WalletSubcommands::Sign"),
1056        }
1057    }
1058
1059    #[test]
1060    fn can_verify_signed_hex_message() {
1061        let message = "hello";
1062        let signature = Signature::from_str("f2dd00eac33840c04b6fc8a5ec8c4a47eff63575c2bc7312ecb269383de0c668045309c423484c8d097df306e690c653f8e1ec92f7f6f45d1f517027771c3e801c").unwrap();
1063        let address = address!("0x28A4F420a619974a2393365BCe5a7b560078Cc13");
1064        let recovered_address =
1065            WalletSubcommands::recover_address_from_message(message, &signature);
1066        assert!(recovered_address.is_ok());
1067        assert_eq!(address, recovered_address.unwrap());
1068    }
1069
1070    #[test]
1071    fn can_verify_signed_hex_message_no_hash() {
1072        let prehash = keccak256("hello");
1073        let signature = Signature::from_str("433ec3d37e4f1253df15e2dea412fed8e915737730f74b3dfb1353268f932ef5557c9158e0b34bce39de28d11797b42e9b1acb2749230885fe075aedc3e491a41b").unwrap();
1074        let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); // private key = 1
1075        let recovered_address =
1076            WalletSubcommands::recover_address_from_message_no_hash(&prehash, &signature);
1077        assert!(recovered_address.is_ok());
1078        assert_eq!(address, recovered_address.unwrap());
1079    }
1080
1081    #[test]
1082    fn can_verify_signed_typed_data() {
1083        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();
1084        let signature = Signature::from_str("0285ff83b93bd01c14e201943af7454fe2bc6c98be707a73888c397d6ae3b0b92f73ca559f81cbb19fe4e0f1dc4105bd7b647c6a84b033057977cf2ec982daf71b").unwrap();
1085        let address = address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); // private key = 1
1086        let recovered_address =
1087            WalletSubcommands::recover_address_from_typed_data(&typed_data, &signature);
1088        assert!(recovered_address.is_ok());
1089        assert_eq!(address, recovered_address.unwrap());
1090    }
1091
1092    #[test]
1093    fn can_parse_wallet_sign_data() {
1094        let args = WalletSubcommands::parse_from(["foundry-cli", "sign", "--data", "{ ... }"]);
1095        match args {
1096            WalletSubcommands::Sign { message, data, from_file, .. } => {
1097                assert_eq!(message, "{ ... }".to_string());
1098                assert!(data);
1099                assert!(!from_file);
1100            }
1101            _ => panic!("expected WalletSubcommands::Sign"),
1102        }
1103    }
1104
1105    #[test]
1106    fn can_parse_wallet_sign_data_file() {
1107        let args = WalletSubcommands::parse_from([
1108            "foundry-cli",
1109            "sign",
1110            "--data",
1111            "--from-file",
1112            "tests/data/typed_data.json",
1113        ]);
1114        match args {
1115            WalletSubcommands::Sign { message, data, from_file, .. } => {
1116                assert_eq!(message, "tests/data/typed_data.json".to_string());
1117                assert!(data);
1118                assert!(from_file);
1119            }
1120            _ => panic!("expected WalletSubcommands::Sign"),
1121        }
1122    }
1123
1124    #[test]
1125    fn can_parse_wallet_change_password() {
1126        let args = WalletSubcommands::parse_from([
1127            "foundry-cli",
1128            "change-password",
1129            "my_account",
1130            "--unsafe-password",
1131            "old_password",
1132            "--unsafe-new-password",
1133            "new_password",
1134        ]);
1135        match args {
1136            WalletSubcommands::ChangePassword {
1137                account_name,
1138                keystore_dir,
1139                unsafe_password,
1140                unsafe_new_password,
1141            } => {
1142                assert_eq!(account_name, "my_account".to_string());
1143                assert_eq!(unsafe_password, Some("old_password".to_string()));
1144                assert_eq!(unsafe_new_password, Some("new_password".to_string()));
1145                assert!(keystore_dir.is_none());
1146            }
1147            _ => panic!("expected WalletSubcommands::ChangePassword"),
1148        }
1149    }
1150
1151    #[test]
1152    fn can_parse_wallet_session_create() {
1153        let args = WalletSubcommands::parse_from([
1154            "foundry-cli",
1155            "session",
1156            "create",
1157            "--root",
1158            "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
1159            "--chain-id",
1160            "4217",
1161            "--expires",
1162            "10m",
1163            "--scope",
1164            "0x20c0000000000000000000000000000000000001:transfer",
1165            "--spend-limit",
1166            "PathUSD=0",
1167            "--private-key",
1168            "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0",
1169        ]);
1170
1171        match args {
1172            WalletSubcommands::Session(args) => match args.command {
1173                Some(SessionSubcommands::Create {
1174                    root_account,
1175                    chain_id,
1176                    expires,
1177                    scope,
1178                    spend_limits,
1179                    wallet,
1180                }) => {
1181                    assert_eq!(
1182                        root_account,
1183                        address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf")
1184                    );
1185                    assert_eq!(chain_id, 4217);
1186                    assert_eq!(expires, 600);
1187                    assert_eq!(scope.len(), 1);
1188                    assert_eq!(spend_limits.len(), 1);
1189                    assert_eq!(
1190                        wallet.raw.private_key.as_deref(),
1191                        Some("0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0")
1192                    );
1193                }
1194                _ => panic!("expected WalletSubcommands::Session::Create"),
1195            },
1196            _ => panic!("expected WalletSubcommands::Session"),
1197        }
1198    }
1199
1200    #[test]
1201    fn can_parse_wallet_session_revoke() {
1202        for (extra_args, expected_local) in [([].as_slice(), false), (["--local"].as_slice(), true)]
1203        {
1204            let args = WalletSubcommands::parse_from(
1205                [
1206                    "foundry-cli",
1207                    "session",
1208                    "revoke",
1209                    "0x1111111111111111111111111111111111111111111111111111111111111111",
1210                ]
1211                .into_iter()
1212                .chain(extra_args.iter().copied()),
1213            );
1214
1215            match args {
1216                WalletSubcommands::Session(args) => match args.command {
1217                    Some(SessionSubcommands::Revoke { session_id, local, .. }) => {
1218                        assert_eq!(session_id, B256::from([0x11; 32]));
1219                        assert_eq!(local, expected_local);
1220                    }
1221                    _ => panic!("expected WalletSubcommands::Session::Revoke"),
1222                },
1223                _ => panic!("expected WalletSubcommands::Session"),
1224            }
1225        }
1226    }
1227
1228    #[test]
1229    fn can_parse_wallet_session_run_for_command() {
1230        let args = WalletSubcommands::parse_from([
1231            "foundry-cli",
1232            "session",
1233            "--root",
1234            "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
1235            "--chain-id",
1236            "4217",
1237            "--expires",
1238            "10m",
1239            "--target",
1240            "0x20c0000000000000000000000000000000000001",
1241            "--selector",
1242            "transfer(address,uint256)",
1243            "--spend-limit",
1244            "PathUSD=0",
1245            "--for",
1246            "forge script Deploy --broadcast",
1247            "--private-key",
1248            "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0",
1249        ]);
1250
1251        match args {
1252            WalletSubcommands::Session(args) => {
1253                assert!(args.command.is_none());
1254                assert_eq!(
1255                    args.root_account,
1256                    Some(address!("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"))
1257                );
1258                assert_eq!(args.send_tx.eth.etherscan.chain.map(|chain| chain.id()), Some(4217));
1259                assert_eq!(args.expires, Some(600));
1260                assert_eq!(
1261                    args.target,
1262                    Some(address!("0x20c0000000000000000000000000000000000001"))
1263                );
1264                assert_eq!(args.selectors.len(), 1);
1265                assert_eq!(args.spend_limits.len(), 1);
1266                assert_eq!(args.for_command.as_deref(), Some("forge script Deploy --broadcast"));
1267                assert_eq!(
1268                    args.send_tx.eth.wallet.raw.private_key.as_deref(),
1269                    Some("0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0")
1270                );
1271            }
1272            _ => panic!("expected WalletSubcommands::Session"),
1273        }
1274    }
1275
1276    #[test]
1277    fn wallet_sign_auth_nonce_and_self_broadcast_conflict() {
1278        let result = WalletSubcommands::try_parse_from([
1279            "foundry-cli",
1280            "sign-auth",
1281            "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
1282            "--nonce",
1283            "42",
1284            "--self-broadcast",
1285        ]);
1286        assert!(
1287            result.is_err(),
1288            "expected error when both --nonce and --self-broadcast are provided"
1289        );
1290    }
1291}