Skip to main content

foundry_cli/opts/
rpc.rs

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    /// The RPC endpoint, default value is http://localhost:8545.
23    #[arg(short = 'r', long = "rpc-url", env = "ETH_RPC_URL")]
24    pub url: Option<String>,
25
26    /// Allow insecure RPC connections (accept invalid HTTPS certificates).
27    ///
28    /// When the provider's inner runtime transport variant is HTTP, this configures the reqwest
29    /// client to accept invalid certificates.
30    #[arg(short = 'k', long = "insecure", default_value = "false")]
31    pub accept_invalid_certs: bool,
32
33    /// Disable automatic proxy detection.
34    ///
35    /// Use this in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox) where
36    /// system proxy detection causes crashes. When enabled, HTTP_PROXY/HTTPS_PROXY environment
37    /// variables and system proxy settings will be ignored.
38    #[arg(long = "no-proxy", alias = "disable-proxy", default_value = "false")]
39    pub no_proxy: bool,
40
41    /// Use the Flashbots RPC URL with fast mode (<https://rpc.flashbots.net/fast>).
42    ///
43    /// This shares the transaction privately with all registered builders.
44    ///
45    /// See: <https://docs.flashbots.net/flashbots-protect/quick-start#faster-transactions>
46    #[arg(long)]
47    pub flashbots: bool,
48
49    /// JWT Secret for the RPC endpoint.
50    ///
51    /// The JWT secret will be used to create a JWT for a RPC. For example, the following can be
52    /// used to simulate a CL `engine_forkchoiceUpdated` call:
53    ///
54    /// cast rpc --jwt-secret <JWT_SECRET> engine_forkchoiceUpdatedV2
55    /// '["0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
56    /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
57    /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc"]'
58    #[arg(long, env = "ETH_RPC_JWT_SECRET")]
59    pub jwt_secret: Option<String>,
60
61    /// Timeout for the RPC request in seconds.
62    ///
63    /// The specified timeout will be used to override the default timeout for RPC requests.
64    ///
65    /// Default value: 45
66    #[arg(long, env = "ETH_RPC_TIMEOUT")]
67    pub rpc_timeout: Option<u64>,
68
69    /// Specify custom headers for RPC requests.
70    #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))]
71    pub rpc_headers: Option<Vec<String>>,
72
73    /// Print the equivalent curl command instead of making the RPC request.
74    #[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    /// Returns the RPC endpoint.
92    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    /// Returns the JWT secret.
103    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    /// The Etherscan (or equivalent) API key.
149    #[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    /// The chain name or EIP-155 chain ID.
154    #[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    /// Returns true if the Etherscan API key is set.
179    pub fn has_key(&self) -> bool {
180        self.key.as_ref().filter(|key| !key.trim().is_empty()).is_some()
181    }
182
183    /// Returns the Etherscan API key.
184    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
220// Make this args a `Figment` so that it can be merged into the `Config`
221impl 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}