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