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
107impl WalletOpts {
108 pub async fn signer(&self) -> Result<WalletSigner> {
109 trace!("start finding signer");
110
111 let get_env = |key: &str| {
112 std::env::var(key)
113 .map_err(|_| eyre::eyre!("{key} environment variable is required for signer"))
114 };
115
116 let signer = if self.ledger {
117 utils::create_ledger_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
118 .await?
119 } else if self.trezor {
120 utils::create_trezor_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index)
121 .await?
122 } else if self.aws {
123 let key_id = get_env("AWS_KMS_KEY_ID")?;
124 WalletSigner::from_aws(key_id).await?
125 } else if self.gcp {
126 let project_id = get_env("GCP_PROJECT_ID")?;
127 let location = get_env("GCP_LOCATION")?;
128 let keyring = get_env("GCP_KEY_RING")?;
129 let key_name = get_env("GCP_KEY_NAME")?;
130 let key_version = get_env("GCP_KEY_VERSION")?
131 .parse()
132 .map_err(|_| eyre::eyre!("GCP_KEY_VERSION could not be parsed into u64"))?;
133 WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
134 } else if self.turnkey {
135 let api_private_key = get_env("TURNKEY_API_PRIVATE_KEY")?;
136 let organization_id = get_env("TURNKEY_ORGANIZATION_ID")?;
137 let address_str = get_env("TURNKEY_ADDRESS")?;
138 let address = address_str.parse().map_err(|_| {
139 eyre::eyre!("TURNKEY_ADDRESS could not be parsed as an Ethereum address")
140 })?;
141 WalletSigner::from_turnkey(api_private_key, organization_id, address)?
142 } else if let Some(raw_wallet) = self.raw.signer()? {
143 raw_wallet
144 } else if let Some(path) = utils::maybe_get_keystore_path(
145 self.keystore_path.as_deref(),
146 self.keystore_account_name.as_deref(),
147 )? {
148 let (maybe_signer, maybe_pending) = utils::create_keystore_signer(
149 &path,
150 self.keystore_password.as_deref(),
151 self.keystore_password_file.as_deref(),
152 )?;
153 if let Some(pending) = maybe_pending {
154 pending.unlock()?
155 } else if let Some(signer) = maybe_signer {
156 signer
157 } else {
158 unreachable!()
159 }
160 } else {
161 eyre::bail!(
162 "\
163Error accessing local wallet. Did you pass a keystore, hardware wallet, private key or mnemonic?
164
165Run the command with --help flag for more information or use the corresponding CLI
166flag to set your key via:
167
168--keystore
169--interactive
170--private-key
171--mnemonic-path
172--aws
173--gcp
174--turnkey
175--trezor
176--ledger
177--browser
178
179Alternatively, when using the `cast send` or `cast mktx` commands with a local node
180or RPC that has unlocked accounts, the --unlocked or --ethsign flags can be used,
181respectively. The sender address can be specified by setting the `ETH_FROM` environment
182variable to the desired unlocked account address, or by providing the address directly
183using the --from flag."
184 )
185 };
186
187 Ok(signer)
188 }
189}
190
191impl From<RawWalletOpts> for WalletOpts {
192 fn from(options: RawWalletOpts) -> Self {
193 Self { raw: options, ..Default::default() }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use alloy_signer::Signer;
201 use std::{path::Path, str::FromStr};
202
203 #[tokio::test]
204 async fn find_keystore() {
205 let keystore =
206 Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore"));
207 let keystore_file = keystore
208 .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2");
209 let password_file = keystore.join("password-ec554");
210 let wallet: WalletOpts = WalletOpts::parse_from([
211 "foundry-cli",
212 "--from",
213 "560d246fcddc9ea98a8b032c9a2f474efb493c28",
214 "--keystore",
215 keystore_file.to_str().unwrap(),
216 "--password-file",
217 password_file.to_str().unwrap(),
218 ]);
219 let signer = wallet.signer().await.unwrap();
220 assert_eq!(
221 signer.address(),
222 Address::from_str("ec554aeafe75601aaab43bd4621a22284db566c2").unwrap()
223 );
224 }
225
226 #[tokio::test]
227 async fn illformed_private_key_generates_user_friendly_error() {
228 let wallet = WalletOpts {
229 raw: RawWalletOpts {
230 interactive: false,
231 private_key: Some("123".to_string()),
232 mnemonic: None,
233 mnemonic_passphrase: None,
234 hd_path: None,
235 mnemonic_index: 0,
236 },
237 from: None,
238 keystore_path: None,
239 keystore_account_name: None,
240 keystore_password: None,
241 keystore_password_file: None,
242 ledger: false,
243 trezor: false,
244 aws: false,
245 gcp: false,
246 turnkey: false,
247 };
248 match wallet.signer().await {
249 Ok(_) => {
250 panic!("illformed private key shouldn't decode")
251 }
252 Err(x) => {
253 assert!(
254 x.to_string().contains("Failed to decode private key"),
255 "Error message is not user-friendly"
256 );
257 }
258 }
259 }
260}