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