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)]
20pub struct RpcOpts {
21 #[arg(short = 'r', long = "rpc-url", env = "ETH_RPC_URL")]
23 pub url: Option<String>,
24
25 #[arg(short = 'k', long = "insecure", default_value = "false")]
30 pub accept_invalid_certs: bool,
31
32 #[arg(long)]
38 pub flashbots: bool,
39
40 #[arg(long, env = "ETH_RPC_JWT_SECRET")]
50 pub jwt_secret: Option<String>,
51
52 #[arg(long, env = "ETH_RPC_TIMEOUT")]
58 pub rpc_timeout: Option<u64>,
59
60 #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))]
62 pub rpc_headers: Option<Vec<String>>,
63}
64
65impl_figment_convert_cast!(RpcOpts);
66
67impl figment::Provider for RpcOpts {
68 fn metadata(&self) -> Metadata {
69 Metadata::named("RpcOpts")
70 }
71
72 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
73 Ok(Map::from([(Config::selected_profile(), self.dict())]))
74 }
75}
76
77impl RpcOpts {
78 pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
80 let url = match (self.flashbots, self.url.as_deref(), config) {
81 (true, ..) => Some(Cow::Borrowed(FLASHBOTS_URL)),
82 (false, Some(url), _) => Some(Cow::Borrowed(url)),
83 (false, None, Some(config)) => config.get_rpc_url().transpose()?,
84 (false, None, None) => None,
85 };
86 Ok(url)
87 }
88
89 pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
91 let jwt = match (self.jwt_secret.as_deref(), config) {
92 (Some(jwt), _) => Some(Cow::Borrowed(jwt)),
93 (None, Some(config)) => config.get_rpc_jwt_secret()?,
94 (None, None) => None,
95 };
96 Ok(jwt)
97 }
98
99 pub fn dict(&self) -> Dict {
100 let mut dict = Dict::new();
101 if let Ok(Some(url)) = self.url(None) {
102 dict.insert("eth_rpc_url".into(), url.into_owned().into());
103 }
104 if let Ok(Some(jwt)) = self.jwt(None) {
105 dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into());
106 }
107 if let Some(rpc_timeout) = self.rpc_timeout {
108 dict.insert("eth_rpc_timeout".into(), rpc_timeout.into());
109 }
110 if let Some(headers) = &self.rpc_headers {
111 dict.insert("eth_rpc_headers".into(), headers.clone().into());
112 }
113 if self.accept_invalid_certs {
114 dict.insert("eth_rpc_accept_invalid_certs".into(), true.into());
115 }
116 dict
117 }
118
119 pub fn into_figment(self, all: bool) -> Figment {
120 let root = find_project_root(None).expect("could not determine project root");
121 Config::with_root(&root)
122 .to_figment(if all { FigmentProviders::All } else { FigmentProviders::Cast })
123 .merge(self)
124 }
125}
126
127#[derive(Clone, Debug, Default, Serialize, Parser)]
128pub struct EtherscanOpts {
129 #[arg(short = 'e', long = "etherscan-api-key", alias = "api-key", env = "ETHERSCAN_API_KEY")]
131 #[serde(rename = "etherscan_api_key", skip_serializing_if = "Option::is_none")]
132 pub key: Option<String>,
133
134 #[arg(
136 short,
137 long,
138 alias = "chain-id",
139 env = "CHAIN",
140 value_parser = ChainValueParser::default(),
141 )]
142 #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none")]
143 pub chain: Option<Chain>,
144}
145
146impl_figment_convert_cast!(EtherscanOpts);
147
148impl figment::Provider for EtherscanOpts {
149 fn metadata(&self) -> Metadata {
150 Metadata::named("EtherscanOpts")
151 }
152
153 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
154 Ok(Map::from([(Config::selected_profile(), self.dict())]))
155 }
156}
157
158impl EtherscanOpts {
159 pub fn has_key(&self) -> bool {
161 self.key.as_ref().filter(|key| !key.trim().is_empty()).is_some()
162 }
163
164 pub fn key(&self) -> Option<String> {
166 self.key.as_ref().filter(|key| !key.trim().is_empty()).cloned()
167 }
168
169 pub fn dict(&self) -> Dict {
170 let mut dict = Dict::new();
171 if let Some(key) = self.key() {
172 dict.insert("etherscan_api_key".into(), key.into());
173 }
174
175 if let Some(chain) = self.chain {
176 if let ChainKind::Id(id) = chain.kind() {
177 dict.insert("chain_id".into(), (*id).into());
178 } else {
179 dict.insert("chain_id".into(), chain.to_string().into());
180 }
181 }
182 dict
183 }
184}
185
186#[derive(Clone, Debug, Default, Parser)]
187#[command(next_help_heading = "Ethereum options")]
188pub struct EthereumOpts {
189 #[command(flatten)]
190 pub rpc: RpcOpts,
191
192 #[command(flatten)]
193 pub etherscan: EtherscanOpts,
194
195 #[command(flatten)]
196 pub wallet: WalletOpts,
197}
198
199impl_figment_convert_cast!(EthereumOpts);
200
201impl figment::Provider for EthereumOpts {
203 fn metadata(&self) -> Metadata {
204 Metadata::named("Ethereum Opts Provider")
205 }
206
207 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
208 let mut dict = self.etherscan.dict();
209 dict.extend(self.rpc.dict());
210
211 if let Some(from) = self.wallet.from {
212 dict.insert("sender".to_string(), from.to_string().into());
213 }
214
215 Ok(Map::from([(Config::selected_profile(), dict)]))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn parse_etherscan_opts() {
225 let args: EtherscanOpts =
226 EtherscanOpts::parse_from(["foundry-cli", "--etherscan-api-key", "dummykey"]);
227 assert_eq!(args.key(), Some("dummykey".to_string()));
228
229 let args: EtherscanOpts =
230 EtherscanOpts::parse_from(["foundry-cli", "--etherscan-api-key", ""]);
231 assert!(!args.has_key());
232 }
233}