1use crate::opts::ChainValueParser;
2use alloy_chains::ChainKind;
3use clap::Parser;
4use eyre::Result;
5use foundry_block_explorers::EtherscanApiVersion;
6use foundry_config::{
7 Chain, Config,
8 figment::{
9 self, Metadata, Profile,
10 value::{Dict, Map},
11 },
12 impl_figment_convert_cast,
13};
14use foundry_wallets::WalletOpts;
15use serde::Serialize;
16use std::borrow::Cow;
17
18const FLASHBOTS_URL: &str = "https://rpc.flashbots.net/fast";
19
20#[derive(Clone, Debug, Default, Parser)]
21pub struct RpcOpts {
22 #[arg(short = 'r', long = "rpc-url", env = "ETH_RPC_URL")]
24 pub url: Option<String>,
25
26 #[arg(short = 'k', long = "insecure", default_value = "false")]
31 pub accept_invalid_certs: bool,
32
33 #[arg(long)]
39 pub flashbots: bool,
40
41 #[arg(long, env = "ETH_RPC_JWT_SECRET")]
51 pub jwt_secret: Option<String>,
52
53 #[arg(long, env = "ETH_RPC_TIMEOUT")]
59 pub rpc_timeout: Option<u64>,
60
61 #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))]
63 pub rpc_headers: Option<Vec<String>>,
64}
65
66impl_figment_convert_cast!(RpcOpts);
67
68impl figment::Provider for RpcOpts {
69 fn metadata(&self) -> Metadata {
70 Metadata::named("RpcOpts")
71 }
72
73 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
74 Ok(Map::from([(Config::selected_profile(), self.dict())]))
75 }
76}
77
78impl RpcOpts {
79 pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
81 let url = match (self.flashbots, self.url.as_deref(), config) {
82 (true, ..) => Some(Cow::Borrowed(FLASHBOTS_URL)),
83 (false, Some(url), _) => Some(Cow::Borrowed(url)),
84 (false, None, Some(config)) => config.get_rpc_url().transpose()?,
85 (false, None, None) => None,
86 };
87 Ok(url)
88 }
89
90 pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
92 let jwt = match (self.jwt_secret.as_deref(), config) {
93 (Some(jwt), _) => Some(Cow::Borrowed(jwt)),
94 (None, Some(config)) => config.get_rpc_jwt_secret()?,
95 (None, None) => None,
96 };
97 Ok(jwt)
98 }
99
100 pub fn dict(&self) -> Dict {
101 let mut dict = Dict::new();
102 if let Ok(Some(url)) = self.url(None) {
103 dict.insert("eth_rpc_url".into(), url.into_owned().into());
104 }
105 if let Ok(Some(jwt)) = self.jwt(None) {
106 dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into());
107 }
108 if let Some(rpc_timeout) = self.rpc_timeout {
109 dict.insert("eth_rpc_timeout".into(), rpc_timeout.into());
110 }
111 if let Some(headers) = &self.rpc_headers {
112 dict.insert("eth_rpc_headers".into(), headers.clone().into());
113 }
114 if self.accept_invalid_certs {
115 dict.insert("eth_rpc_accept_invalid_certs".into(), true.into());
116 }
117 dict
118 }
119}
120
121#[derive(Clone, Debug, Default, Serialize, Parser)]
122pub struct EtherscanOpts {
123 #[arg(short = 'e', long = "etherscan-api-key", alias = "api-key", env = "ETHERSCAN_API_KEY")]
125 #[serde(rename = "etherscan_api_key", skip_serializing_if = "Option::is_none")]
126 pub key: Option<String>,
127
128 #[arg(
130 short,
131 long = "etherscan-api-version",
132 alias = "api-version",
133 env = "ETHERSCAN_API_VERSION"
134 )]
135 #[serde(rename = "etherscan_api_version", skip_serializing_if = "Option::is_none")]
136 pub api_version: Option<EtherscanApiVersion>,
137
138 #[arg(
140 short,
141 long,
142 alias = "chain-id",
143 env = "CHAIN",
144 value_parser = ChainValueParser::default(),
145 )]
146 #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none")]
147 pub chain: Option<Chain>,
148}
149
150impl_figment_convert_cast!(EtherscanOpts);
151
152impl figment::Provider for EtherscanOpts {
153 fn metadata(&self) -> Metadata {
154 Metadata::named("EtherscanOpts")
155 }
156
157 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
158 Ok(Map::from([(Config::selected_profile(), self.dict())]))
159 }
160}
161
162impl EtherscanOpts {
163 pub fn has_key(&self) -> bool {
165 self.key.as_ref().filter(|key| !key.trim().is_empty()).is_some()
166 }
167
168 pub fn key(&self) -> Option<String> {
170 self.key.as_ref().filter(|key| !key.trim().is_empty()).cloned()
171 }
172
173 pub fn dict(&self) -> Dict {
174 let mut dict = Dict::new();
175 if let Some(key) = self.key() {
176 dict.insert("etherscan_api_key".into(), key.into());
177 }
178
179 if let Some(api_version) = &self.api_version {
180 dict.insert("etherscan_api_version".into(), api_version.to_string().into());
181 }
182
183 if let Some(chain) = self.chain {
184 if let ChainKind::Id(id) = chain.kind() {
185 dict.insert("chain_id".into(), (*id).into());
186 } else {
187 dict.insert("chain_id".into(), chain.to_string().into());
188 }
189 }
190 dict
191 }
192}
193
194#[derive(Clone, Debug, Default, Parser)]
195#[command(next_help_heading = "Ethereum options")]
196pub struct EthereumOpts {
197 #[command(flatten)]
198 pub rpc: RpcOpts,
199
200 #[command(flatten)]
201 pub etherscan: EtherscanOpts,
202
203 #[command(flatten)]
204 pub wallet: WalletOpts,
205}
206
207impl_figment_convert_cast!(EthereumOpts);
208
209impl figment::Provider for EthereumOpts {
211 fn metadata(&self) -> Metadata {
212 Metadata::named("Ethereum Opts Provider")
213 }
214
215 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
216 let mut dict = self.etherscan.dict();
217 dict.extend(self.rpc.dict());
218
219 if let Some(from) = self.wallet.from {
220 dict.insert("sender".to_string(), from.to_string().into());
221 }
222
223 Ok(Map::from([(Config::selected_profile(), dict)]))
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn parse_etherscan_opts() {
233 let args: EtherscanOpts =
234 EtherscanOpts::parse_from(["foundry-cli", "--etherscan-api-key", "dummykey"]);
235 assert_eq!(args.key(), Some("dummykey".to_string()));
236
237 let args: EtherscanOpts =
238 EtherscanOpts::parse_from(["foundry-cli", "--etherscan-api-key", ""]);
239 assert!(!args.has_key());
240 }
241}