foundry_cli/opts/
rpc.rs

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    /// The RPC endpoint, default value is http://localhost:8545.
22    #[arg(short = 'r', long = "rpc-url", env = "ETH_RPC_URL")]
23    pub url: Option<String>,
24
25    /// Use the Flashbots RPC URL with fast mode (<https://rpc.flashbots.net/fast>).
26    ///
27    /// This shares the transaction privately with all registered builders.
28    ///
29    /// See: <https://docs.flashbots.net/flashbots-protect/quick-start#faster-transactions>
30    #[arg(long)]
31    pub flashbots: bool,
32
33    /// JWT Secret for the RPC endpoint.
34    ///
35    /// The JWT secret will be used to create a JWT for a RPC. For example, the following can be
36    /// used to simulate a CL `engine_forkchoiceUpdated` call:
37    ///
38    /// cast rpc --jwt-secret <JWT_SECRET> engine_forkchoiceUpdatedV2
39    /// '["0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
40    /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
41    /// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc"]'
42    #[arg(long, env = "ETH_RPC_JWT_SECRET")]
43    pub jwt_secret: Option<String>,
44
45    /// Timeout for the RPC request in seconds.
46    ///
47    /// The specified timeout will be used to override the default timeout for RPC requests.
48    ///
49    /// Default value: 45
50    #[arg(long, env = "ETH_RPC_TIMEOUT")]
51    pub rpc_timeout: Option<u64>,
52
53    /// Specify custom headers for RPC requests.
54    #[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    /// Returns the RPC endpoint.
72    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    /// Returns the JWT secret.
83    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    /// The Etherscan (or equivalent) API key.
113    #[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    /// The chain name or EIP-155 chain ID.
118    #[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    /// Returns true if the Etherscan API key is set.
143    pub fn has_key(&self) -> bool {
144        self.key.as_ref().filter(|key| !key.trim().is_empty()).is_some()
145    }
146
147    /// Returns the Etherscan API key.
148    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
183// Make this args a `Figment` so that it can be merged into the `Config`
184impl 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}