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