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_block_explorers::EtherscanApiVersion;
6use foundry_config::{
7    Chain, Config, FigmentProviders,
8    figment::{
9        self, Figment, Metadata, Profile,
10        value::{Dict, Map},
11    },
12    find_project_root, impl_figment_convert_cast,
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    /// 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    /// Use the Flashbots RPC URL with fast mode (<https://rpc.flashbots.net/fast>).
34    ///
35    /// This shares the transaction privately with all registered builders.
36    ///
37    /// See: <https://docs.flashbots.net/flashbots-protect/quick-start#faster-transactions>
38    #[arg(long)]
39    pub flashbots: bool,
40
41    /// JWT Secret for the RPC endpoint.
42    ///
43    /// The JWT secret will be used to create a JWT for a RPC. For example, the following can be
44    /// used to simulate a CL `engine_forkchoiceUpdated` call:
45    ///
46    /// cast rpc --jwt-secret <JWT_SECRET> engine_forkchoiceUpdatedV2
47    /// '["0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
48    /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
49    /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc"]'
50    #[arg(long, env = "ETH_RPC_JWT_SECRET")]
51    pub jwt_secret: Option<String>,
52
53    /// Timeout for the RPC request in seconds.
54    ///
55    /// The specified timeout will be used to override the default timeout for RPC requests.
56    ///
57    /// Default value: 45
58    #[arg(long, env = "ETH_RPC_TIMEOUT")]
59    pub rpc_timeout: Option<u64>,
60
61    /// Specify custom headers for RPC requests.
62    #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))]
63    pub rpc_headers: Option<Vec<String>>,
64}
65
66impl_figment_convert_cast!(RpcOpts);
67
68impl figment::Provider for RpcOpts {
69    fn metadata(&self) -> Metadata {
70        Metadata::named("RpcOpts")
71    }
72
73    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
74        Ok(Map::from([(Config::selected_profile(), self.dict())]))
75    }
76}
77
78impl RpcOpts {
79    /// Returns the RPC endpoint.
80    pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
81        let url = match (self.flashbots, self.url.as_deref(), config) {
82            (true, ..) => Some(Cow::Borrowed(FLASHBOTS_URL)),
83            (false, Some(url), _) => Some(Cow::Borrowed(url)),
84            (false, None, Some(config)) => config.get_rpc_url().transpose()?,
85            (false, None, None) => None,
86        };
87        Ok(url)
88    }
89
90    /// Returns the JWT secret.
91    pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
92        let jwt = match (self.jwt_secret.as_deref(), config) {
93            (Some(jwt), _) => Some(Cow::Borrowed(jwt)),
94            (None, Some(config)) => config.get_rpc_jwt_secret()?,
95            (None, None) => None,
96        };
97        Ok(jwt)
98    }
99
100    pub fn dict(&self) -> Dict {
101        let mut dict = Dict::new();
102        if let Ok(Some(url)) = self.url(None) {
103            dict.insert("eth_rpc_url".into(), url.into_owned().into());
104        }
105        if let Ok(Some(jwt)) = self.jwt(None) {
106            dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into());
107        }
108        if let Some(rpc_timeout) = self.rpc_timeout {
109            dict.insert("eth_rpc_timeout".into(), rpc_timeout.into());
110        }
111        if let Some(headers) = &self.rpc_headers {
112            dict.insert("eth_rpc_headers".into(), headers.clone().into());
113        }
114        if self.accept_invalid_certs {
115            dict.insert("eth_rpc_accept_invalid_certs".into(), true.into());
116        }
117        dict
118    }
119
120    pub fn into_figment(self, all: bool) -> Figment {
121        let root = find_project_root(None).expect("could not determine project root");
122        Config::with_root(&root)
123            .to_figment(if all { FigmentProviders::All } else { FigmentProviders::Cast })
124            .merge(self)
125    }
126}
127
128#[derive(Clone, Debug, Default, Serialize, Parser)]
129pub struct EtherscanOpts {
130    /// The Etherscan (or equivalent) API key.
131    #[arg(short = 'e', long = "etherscan-api-key", alias = "api-key", env = "ETHERSCAN_API_KEY")]
132    #[serde(rename = "etherscan_api_key", skip_serializing_if = "Option::is_none")]
133    pub key: Option<String>,
134
135    /// The Etherscan API version.
136    #[arg(
137        short,
138        long = "etherscan-api-version",
139        alias = "api-version",
140        env = "ETHERSCAN_API_VERSION"
141    )]
142    #[serde(rename = "etherscan_api_version", skip_serializing_if = "Option::is_none")]
143    pub api_version: Option<EtherscanApiVersion>,
144
145    /// The chain name or EIP-155 chain ID.
146    #[arg(
147        short,
148        long,
149        alias = "chain-id",
150        env = "CHAIN",
151        value_parser = ChainValueParser::default(),
152    )]
153    #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none")]
154    pub chain: Option<Chain>,
155}
156
157impl_figment_convert_cast!(EtherscanOpts);
158
159impl figment::Provider for EtherscanOpts {
160    fn metadata(&self) -> Metadata {
161        Metadata::named("EtherscanOpts")
162    }
163
164    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
165        Ok(Map::from([(Config::selected_profile(), self.dict())]))
166    }
167}
168
169impl EtherscanOpts {
170    /// Returns true if the Etherscan API key is set.
171    pub fn has_key(&self) -> bool {
172        self.key.as_ref().filter(|key| !key.trim().is_empty()).is_some()
173    }
174
175    /// Returns the Etherscan API key.
176    pub fn key(&self) -> Option<String> {
177        self.key.as_ref().filter(|key| !key.trim().is_empty()).cloned()
178    }
179
180    pub fn dict(&self) -> Dict {
181        let mut dict = Dict::new();
182        if let Some(key) = self.key() {
183            dict.insert("etherscan_api_key".into(), key.into());
184        }
185
186        if let Some(api_version) = &self.api_version {
187            dict.insert("etherscan_api_version".into(), api_version.to_string().into());
188        }
189
190        if let Some(chain) = self.chain {
191            if let ChainKind::Id(id) = chain.kind() {
192                dict.insert("chain_id".into(), (*id).into());
193            } else {
194                dict.insert("chain_id".into(), chain.to_string().into());
195            }
196        }
197        dict
198    }
199}
200
201#[derive(Clone, Debug, Default, Parser)]
202#[command(next_help_heading = "Ethereum options")]
203pub struct EthereumOpts {
204    #[command(flatten)]
205    pub rpc: RpcOpts,
206
207    #[command(flatten)]
208    pub etherscan: EtherscanOpts,
209
210    #[command(flatten)]
211    pub wallet: WalletOpts,
212}
213
214impl_figment_convert_cast!(EthereumOpts);
215
216// Make this args a `Figment` so that it can be merged into the `Config`
217impl figment::Provider for EthereumOpts {
218    fn metadata(&self) -> Metadata {
219        Metadata::named("Ethereum Opts Provider")
220    }
221
222    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
223        let mut dict = self.etherscan.dict();
224        dict.extend(self.rpc.dict());
225
226        if let Some(from) = self.wallet.from {
227            dict.insert("sender".to_string(), from.to_string().into());
228        }
229
230        Ok(Map::from([(Config::selected_profile(), dict)]))
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn parse_etherscan_opts() {
240        let args: EtherscanOpts =
241            EtherscanOpts::parse_from(["foundry-cli", "--etherscan-api-key", "dummykey"]);
242        assert_eq!(args.key(), Some("dummykey".to_string()));
243
244        let args: EtherscanOpts =
245            EtherscanOpts::parse_from(["foundry-cli", "--etherscan-api-key", ""]);
246        assert!(!args.has_key());
247    }
248}