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