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