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