Skip to main content

foundry_cli/opts/
rpc.rs

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    /// Common RPC options (URL, timeout, rate limiting, etc.).
23    #[command(flatten)]
24    pub common: RpcCommonOpts,
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    /// Specify custom headers for RPC requests.
47    #[arg(long, alias = "headers", env = "ETH_RPC_HEADERS", value_delimiter(','))]
48    pub rpc_headers: Option<Vec<String>>,
49
50    /// Print the equivalent curl command instead of making the RPC request.
51    #[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    /// Returns the RPC endpoint.
69    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    /// Returns the JWT secret.
78    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    /// The Etherscan (or equivalent) API key.
115    #[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    /// The chain name or EIP-155 chain ID.
120    #[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    /// Returns true if the Etherscan API key is set.
145    pub fn has_key(&self) -> bool {
146        self.key.as_ref().filter(|key| !key.trim().is_empty()).is_some()
147    }
148
149    /// Returns the Etherscan API key.
150    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
186// Make this args a `Figment` so that it can be merged into the `Config`
187impl 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}