1use crate::{raw_wallet::RawWalletOpts, utils, wallet_signer::WalletSigner};
2use alloy_primitives::Address;
3use clap::Parser;
4use eyre::Result;
5use serde::Serialize;
6
7#[derive(Clone, Debug, Default, Serialize, Parser)]
15#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
16pub struct WalletOpts {
17 #[arg(
19 long,
20 short,
21 value_name = "ADDRESS",
22 help_heading = "Wallet options - raw",
23 env = "ETH_FROM"
24 )]
25 pub from: Option<Address>,
26
27 #[command(flatten)]
28 pub raw: RawWalletOpts,
29
30 #[arg(
32 long = "keystore",
33 help_heading = "Wallet options - keystore",
34 value_name = "PATH",
35 env = "ETH_KEYSTORE"
36 )]
37 pub keystore_path: Option<String>,
38
39 #[arg(
41 long = "account",
42 help_heading = "Wallet options - keystore",
43 value_name = "ACCOUNT_NAME",
44 env = "ETH_KEYSTORE_ACCOUNT",
45 conflicts_with = "keystore_path"
46 )]
47 pub keystore_account_name: Option<String>,
48
49 #[arg(
53 long = "password",
54 help_heading = "Wallet options - keystore",
55 requires = "keystore_path",
56 value_name = "PASSWORD"
57 )]
58 pub keystore_password: Option<String>,
59
60 #[arg(
64 long = "password-file",
65 help_heading = "Wallet options - keystore",
66 requires = "keystore_path",
67 value_name = "PASSWORD_FILE",
68 env = "ETH_PASSWORD"
69 )]
70 pub keystore_password_file: Option<String>,
71
72 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
74 pub ledger: bool,
75
76 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
78 pub trezor: bool,
79
80 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
82 pub aws: bool,
83
84 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
86 pub gcp: bool,
87}
88
89impl WalletOpts {
90 pub async fn signer(&self) -> Result<WalletSigner> {
91 trace!("start finding signer");
92
93 let signer = if self.ledger {
94 utils::create_ledger_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
95 .await?
96 } else if self.trezor {
97 utils::create_trezor_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
98 .await?
99 } else if self.aws {
100 let key_id = std::env::var("AWS_KMS_KEY_ID")?;
101 WalletSigner::from_aws(key_id).await?
102 } else if self.gcp {
103 let project_id = std::env::var("GCP_PROJECT_ID")?;
104 let location = std::env::var("GCP_LOCATION")?;
105 let keyring = std::env::var("GCP_KEYRING")?;
106 let key_name = std::env::var("GCP_KEY_NAME")?;
107 let key_version = std::env::var("GCP_KEY_VERSION")?.parse()?;
108 WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
109 } else if let Some(raw_wallet) = self.raw.signer()? {
110 raw_wallet
111 } else if let Some(path) = utils::maybe_get_keystore_path(
112 self.keystore_path.as_deref(),
113 self.keystore_account_name.as_deref(),
114 )? {
115 let (maybe_signer, maybe_pending) = utils::create_keystore_signer(
116 &path,
117 self.keystore_password.as_deref(),
118 self.keystore_password_file.as_deref(),
119 )?;
120 if let Some(pending) = maybe_pending {
121 pending.unlock()?
122 } else if let Some(signer) = maybe_signer {
123 signer
124 } else {
125 unreachable!()
126 }
127 } else {
128 eyre::bail!(
129 "\
130Error accessing local wallet. Did you set a private key, mnemonic or keystore?
131Run `cast send --help` or `forge create --help` and use the corresponding CLI
132flag to set your key via:
133--private-key, --mnemonic-path, --aws, --gcp, --interactive, --trezor or --ledger.
134Alternatively, if you're using a local node with unlocked accounts,
135use the --unlocked flag and either set the `ETH_FROM` environment variable to the address
136of the unlocked account you want to use, or provide the --from flag with the address directly."
137 )
138 };
139
140 Ok(signer)
141 }
142}
143
144impl From<RawWalletOpts> for WalletOpts {
145 fn from(options: RawWalletOpts) -> Self {
146 Self { raw: options, ..Default::default() }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use alloy_signer::Signer;
154 use std::{path::Path, str::FromStr};
155
156 #[tokio::test]
157 async fn find_keystore() {
158 let keystore =
159 Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore"));
160 let keystore_file = keystore
161 .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2");
162 let password_file = keystore.join("password-ec554");
163 let wallet: WalletOpts = WalletOpts::parse_from([
164 "foundry-cli",
165 "--from",
166 "560d246fcddc9ea98a8b032c9a2f474efb493c28",
167 "--keystore",
168 keystore_file.to_str().unwrap(),
169 "--password-file",
170 password_file.to_str().unwrap(),
171 ]);
172 let signer = wallet.signer().await.unwrap();
173 assert_eq!(
174 signer.address(),
175 Address::from_str("ec554aeafe75601aaab43bd4621a22284db566c2").unwrap()
176 );
177 }
178
179 #[tokio::test]
180 async fn illformed_private_key_generates_user_friendly_error() {
181 let wallet = WalletOpts {
182 raw: RawWalletOpts {
183 interactive: false,
184 private_key: Some("123".to_string()),
185 mnemonic: None,
186 mnemonic_passphrase: None,
187 hd_path: None,
188 mnemonic_index: 0,
189 },
190 from: None,
191 keystore_path: None,
192 keystore_account_name: None,
193 keystore_password: None,
194 keystore_password_file: None,
195 ledger: false,
196 trezor: false,
197 aws: false,
198 gcp: false,
199 };
200 match wallet.signer().await {
201 Ok(_) => {
202 panic!("illformed private key shouldn't decode")
203 }
204 Err(x) => {
205 assert!(
206 x.to_string().contains("Failed to decode private key"),
207 "Error message is not user-friendly"
208 );
209 }
210 }
211 }
212}