1use crate::{signer::WalletSigner, utils, wallet_raw::RawWalletOpts};
2use alloy_primitives::Address;
3use clap::Parser;
4use eyre::Result;
5use serde::Serialize;
6
7#[derive(Clone, Debug, Default, Serialize, Parser)]
17#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
18pub struct WalletOpts {
19 #[arg(
21 long,
22 short,
23 value_name = "ADDRESS",
24 help_heading = "Wallet options - raw",
25 env = "ETH_FROM"
26 )]
27 pub from: Option<Address>,
28
29 #[command(flatten)]
30 pub raw: RawWalletOpts,
31
32 #[arg(
34 long = "keystore",
35 help_heading = "Wallet options - keystore",
36 value_name = "PATH",
37 env = "ETH_KEYSTORE"
38 )]
39 pub keystore_path: Option<String>,
40
41 #[arg(
43 long = "account",
44 help_heading = "Wallet options - keystore",
45 value_name = "ACCOUNT_NAME",
46 env = "ETH_KEYSTORE_ACCOUNT",
47 conflicts_with = "keystore_path"
48 )]
49 pub keystore_account_name: Option<String>,
50
51 #[arg(
55 long = "password",
56 help_heading = "Wallet options - keystore",
57 requires = "keystore_path",
58 value_name = "PASSWORD"
59 )]
60 pub keystore_password: Option<String>,
61
62 #[arg(
66 long = "password-file",
67 help_heading = "Wallet options - keystore",
68 requires = "keystore_path",
69 value_name = "PASSWORD_FILE",
70 env = "ETH_PASSWORD"
71 )]
72 pub keystore_password_file: Option<String>,
73
74 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
76 pub ledger: bool,
77
78 #[arg(long, short, help_heading = "Wallet options - hardware wallet")]
80 pub trezor: bool,
81
82 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "aws-kms"))]
86 pub aws: bool,
87
88 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
95 pub gcp: bool,
96
97 #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))]
104 pub turnkey: bool,
105
106 #[arg(long, help_heading = "Wallet options - browser")]
108 pub browser: bool,
109
110 #[arg(
112 long,
113 help_heading = "Wallet options - browser",
114 value_name = "PORT",
115 default_value = "9545",
116 requires = "browser"
117 )]
118 pub browser_port: u16,
119
120 #[arg(
122 long,
123 help_heading = "Wallet options - browser",
124 default_value_t = false,
125 requires = "browser"
126 )]
127 pub browser_disable_open: bool,
128
129 #[arg(long, help_heading = "Wallet options - browser", hide = true)]
134 pub browser_development: bool,
135}
136
137impl WalletOpts {
138 pub async fn signer(&self) -> Result<WalletSigner> {
139 trace!("start finding signer");
140
141 let get_env = |key: &str| {
142 std::env::var(key)
143 .map_err(|_| eyre::eyre!("{key} environment variable is required for signer"))
144 };
145
146 let signer = if self.ledger {
147 utils::create_ledger_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
148 .await?
149 } else if self.trezor {
150 utils::create_trezor_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
151 .await?
152 } else if self.aws {
153 let key_id = get_env("AWS_KMS_KEY_ID")?;
154 WalletSigner::from_aws(key_id).await?
155 } else if self.gcp {
156 let project_id = get_env("GCP_PROJECT_ID")?;
157 let location = get_env("GCP_LOCATION")?;
158 let keyring = get_env("GCP_KEY_RING")?;
159 let key_name = get_env("GCP_KEY_NAME")?;
160 let key_version = get_env("GCP_KEY_VERSION")?
161 .parse()
162 .map_err(|_| eyre::eyre!("GCP_KEY_VERSION could not be parsed into u64"))?;
163 WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
164 } else if self.turnkey {
165 let api_private_key = get_env("TURNKEY_API_PRIVATE_KEY")?;
166 let organization_id = get_env("TURNKEY_ORGANIZATION_ID")?;
167 let address_str = get_env("TURNKEY_ADDRESS")?;
168 let address = address_str.parse().map_err(|_| {
169 eyre::eyre!("TURNKEY_ADDRESS could not be parsed as an Ethereum address")
170 })?;
171 WalletSigner::from_turnkey(api_private_key, organization_id, address)?
172 } else if self.browser {
173 WalletSigner::from_browser(
174 self.browser_port,
175 !self.browser_disable_open,
176 self.browser_development,
177 )
178 .await?
179 } else if let Some(raw_wallet) = self.raw.signer()? {
180 raw_wallet
181 } else if let Some(path) = utils::maybe_get_keystore_path(
182 self.keystore_path.as_deref(),
183 self.keystore_account_name.as_deref(),
184 )? {
185 let (maybe_signer, maybe_pending) = utils::create_keystore_signer(
186 &path,
187 self.keystore_password.as_deref(),
188 self.keystore_password_file.as_deref(),
189 )?;
190 if let Some(pending) = maybe_pending {
191 pending.unlock()?
192 } else if let Some(signer) = maybe_signer {
193 signer
194 } else {
195 unreachable!()
196 }
197 } else {
198 eyre::bail!(
199 "\
200Error accessing local wallet. Did you pass a keystore, hardware wallet, private key or mnemonic?
201
202Run the command with --help flag for more information or use the corresponding CLI
203flag to set your key via:
204
205--keystore
206--interactive
207--private-key
208--mnemonic-path
209--aws
210--gcp
211--turnkey
212--trezor
213--ledger
214--browser
215
216Alternatively, when using the `cast send` or `cast mktx` commands with a local node
217or RPC that has unlocked accounts, the --unlocked or --ethsign flags can be used,
218respectively. The sender address can be specified by setting the `ETH_FROM` environment
219variable to the desired unlocked account address, or by providing the address directly
220using the --from flag."
221 )
222 };
223
224 Ok(signer)
225 }
226}
227
228impl From<RawWalletOpts> for WalletOpts {
229 fn from(options: RawWalletOpts) -> Self {
230 Self { raw: options, ..Default::default() }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use alloy_signer::Signer;
238 use std::{path::Path, str::FromStr};
239
240 #[tokio::test]
241 async fn find_keystore() {
242 let keystore =
243 Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore"));
244 let keystore_file = keystore
245 .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2");
246 let password_file = keystore.join("password-ec554");
247 let wallet: WalletOpts = WalletOpts::parse_from([
248 "foundry-cli",
249 "--from",
250 "560d246fcddc9ea98a8b032c9a2f474efb493c28",
251 "--keystore",
252 keystore_file.to_str().unwrap(),
253 "--password-file",
254 password_file.to_str().unwrap(),
255 ]);
256 let signer = wallet.signer().await.unwrap();
257 assert_eq!(
258 signer.address(),
259 Address::from_str("ec554aeafe75601aaab43bd4621a22284db566c2").unwrap()
260 );
261 }
262
263 #[tokio::test]
264 async fn illformed_private_key_generates_user_friendly_error() {
265 let wallet = WalletOpts {
266 raw: RawWalletOpts {
267 interactive: false,
268 private_key: Some("123".to_string()),
269 mnemonic: None,
270 mnemonic_passphrase: None,
271 hd_path: None,
272 mnemonic_index: 0,
273 },
274 from: None,
275 keystore_path: None,
276 keystore_account_name: None,
277 keystore_password: None,
278 keystore_password_file: None,
279 ledger: false,
280 trezor: false,
281 aws: false,
282 gcp: false,
283 turnkey: false,
284 browser: false,
285 browser_port: 9545,
286 browser_development: false,
287 browser_disable_open: false,
288 };
289 match wallet.signer().await {
290 Ok(_) => {
291 panic!("illformed private key shouldn't decode")
292 }
293 Err(x) => {
294 assert!(
295 x.to_string().contains("Failed to decode private key"),
296 "Error message is not user-friendly"
297 );
298 }
299 }
300 }
301}