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