Skip to content

Commit 6676e81

Browse files
RjectedEvalir
andauthored
feat(cast): add JWT secret configuration (foundry-rs#5501)
* feat(cast): add JWT secret configuration * set patches to branch * fix cli test * remove patches * change `jwt` to `jwt-secret` * change usages oops * fix rpc_jwt_secret docs, add usage docs * chore: use const-hex --------- Co-authored-by: Enrique Ortiz <hi@enriqueortiz.dev>
1 parent 5457cb7 commit 6676e81

File tree

7 files changed

+117
-28
lines changed

7 files changed

+117
-28
lines changed

Cargo.lock

Lines changed: 22 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/src/opts/ethereum.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ pub struct RpcOpts {
2323
/// Use the Flashbots RPC URL (https://rpc.flashbots.net).
2424
#[clap(long)]
2525
pub flashbots: bool,
26+
27+
/// JWT Secret for the RPC endpoint.
28+
///
29+
/// The JWT secret will be used to create a JWT for a RPC. For example, the following can be
30+
/// used to simulate a CL `engine_forkchoiceUpdated` call:
31+
///
32+
/// cast rpc --jwt-secret <JWT_SECRET> engine_forkchoiceUpdatedV2
33+
/// '["0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
34+
/// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
35+
/// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc"]'
36+
#[clap(long, env = "ETH_RPC_JWT_SECRET")]
37+
pub jwt_secret: Option<String>,
2638
}
2739

2840
impl_figment_convert_cast!(RpcOpts);
@@ -49,11 +61,24 @@ impl RpcOpts {
4961
Ok(url)
5062
}
5163

64+
/// Returns the JWT secret.
65+
pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
66+
let jwt = match (self.jwt_secret.as_deref(), config) {
67+
(Some(jwt), _) => Some(Cow::Borrowed(jwt)),
68+
(None, Some(config)) => config.get_rpc_jwt_secret()?,
69+
(None, None) => None,
70+
};
71+
Ok(jwt)
72+
}
73+
5274
pub fn dict(&self) -> Dict {
5375
let mut dict = Dict::new();
5476
if let Ok(Some(url)) = self.url(None) {
5577
dict.insert("eth_rpc_url".into(), url.into_owned().into());
5678
}
79+
if let Ok(Some(jwt)) = self.jwt(None) {
80+
dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into());
81+
}
5782
dict
5883
}
5984
}

crates/cli/src/utils/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,22 @@ pub fn parse_u256(s: &str) -> Result<U256> {
9090
pub fn get_provider(config: &Config) -> Result<foundry_common::RetryProvider> {
9191
get_provider_builder(config)?.build()
9292
}
93+
9394
/// Returns a [ProviderBuilder](foundry_common::ProviderBuilder) instantiated using [Config]'s RPC
9495
/// URL and chain.
9596
///
9697
/// Defaults to `http://localhost:8545` and `Mainnet`.
9798
pub fn get_provider_builder(config: &Config) -> Result<foundry_common::ProviderBuilder> {
9899
let url = config.get_rpc_url_or_localhost_http()?;
99100
let chain = config.chain_id.unwrap_or_default();
100-
Ok(foundry_common::ProviderBuilder::new(url.as_ref()).chain(chain))
101+
let mut builder = foundry_common::ProviderBuilder::new(url.as_ref()).chain(chain);
102+
103+
let jwt = config.get_rpc_jwt_secret()?;
104+
if let Some(jwt) = jwt {
105+
builder = builder.jwt(jwt.as_ref());
106+
}
107+
108+
Ok(builder)
101109
}
102110

103111
pub async fn get_chain<M>(chain: Option<Chain>, provider: M) -> Result<Chain>

crates/common/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ once_cell = "1"
4343
dunce = "1"
4444
regex = "1"
4545
globset = "0.4"
46+
# Using const-hex instead of hex for speed
47+
hex.workspace = true
4648

4749
[dev-dependencies]
4850
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

crates/common/src/provider.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ use crate::{ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT};
44
use ethers_core::types::{Chain, U256};
55
use ethers_middleware::gas_oracle::{GasCategory, GasOracle, Polygon};
66
use ethers_providers::{
7-
is_local_endpoint, Http, HttpRateLimitRetryPolicy, Middleware, Provider, RetryClient,
8-
RetryClientBuilder, DEFAULT_LOCAL_POLL_INTERVAL,
7+
is_local_endpoint, Authorization, Http, HttpRateLimitRetryPolicy, JwtAuth, JwtKey, Middleware,
8+
Provider, RetryClient, RetryClientBuilder, DEFAULT_LOCAL_POLL_INTERVAL,
99
};
1010
use eyre::WrapErr;
11-
use reqwest::{IntoUrl, Url};
11+
use reqwest::{header::HeaderValue, IntoUrl, Url};
1212
use std::{borrow::Cow, time::Duration};
1313

1414
/// Helper type alias for a retry provider
@@ -53,6 +53,8 @@ pub struct ProviderBuilder {
5353
timeout: Duration,
5454
/// available CUPS
5555
compute_units_per_second: u64,
56+
/// JWT Secret
57+
jwt: Option<String>,
5658
}
5759

5860
// === impl ProviderBuilder ===
@@ -76,6 +78,7 @@ impl ProviderBuilder {
7678
timeout: REQUEST_TIMEOUT,
7779
// alchemy max cpus <https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups>
7880
compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
81+
jwt: None,
7982
}
8083
}
8184

@@ -141,6 +144,12 @@ impl ProviderBuilder {
141144
self.max_retry(100).initial_backoff(100)
142145
}
143146

147+
/// Sets the JWT secret
148+
pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
149+
self.jwt = Some(jwt.into());
150+
self
151+
}
152+
144153
/// Same as [`Self:build()`] but also retrieves the `chainId` in order to derive an appropriate
145154
/// interval
146155
pub async fn connect(self) -> eyre::Result<RetryProvider> {
@@ -163,10 +172,33 @@ impl ProviderBuilder {
163172
initial_backoff,
164173
timeout,
165174
compute_units_per_second,
175+
jwt,
166176
} = self;
167177
let url = url?;
168178

169-
let client = reqwest::Client::builder().timeout(timeout).build()?;
179+
let mut client_builder = reqwest::Client::builder().timeout(timeout);
180+
181+
// Set the JWT auth as a header if present
182+
if let Some(jwt) = jwt {
183+
// Decode jwt from hex, then generate claims (iat with current timestamp)
184+
let jwt = hex::decode(jwt)?;
185+
let secret =
186+
JwtKey::from_slice(&jwt).map_err(|err| eyre::eyre!("Invalid JWT: {}", err))?;
187+
let auth = JwtAuth::new(secret, None, None);
188+
let token = auth.generate_token()?;
189+
190+
// Essentially unrolled ethers-rs new_with_auth to accomodate the custom timeout
191+
let auth = Authorization::Bearer(token);
192+
let mut auth_value = HeaderValue::from_str(&auth.to_string())?;
193+
auth_value.set_sensitive(true);
194+
195+
let mut headers = reqwest::header::HeaderMap::new();
196+
headers.insert(reqwest::header::AUTHORIZATION, auth_value);
197+
198+
client_builder = client_builder.default_headers(headers);
199+
}
200+
201+
let client = client_builder.build()?;
170202
let is_local = is_local_endpoint(url.as_str());
171203

172204
let provider = Http::new_with_client(url, client);

crates/config/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ pub struct Config {
205205
pub verbosity: u8,
206206
/// url of the rpc server that should be used for any rpc calls
207207
pub eth_rpc_url: Option<String>,
208+
/// JWT secret that should be used for any rpc calls
209+
pub eth_rpc_jwt: Option<String>,
208210
/// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
209211
pub etherscan_api_key: Option<String>,
210212
/// Multiple etherscan api configs and their aliases
@@ -752,6 +754,25 @@ impl Config {
752754
self.remappings.iter().map(|m| m.clone().into()).collect()
753755
}
754756

757+
/// Returns the configured rpc jwt secret
758+
///
759+
/// Returns:
760+
/// - The jwt secret, if configured
761+
///
762+
/// # Example
763+
///
764+
/// ```
765+
///
766+
/// use foundry_config::Config;
767+
/// # fn t() {
768+
/// let config = Config::with_root("./");
769+
/// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
770+
/// # }
771+
/// ```
772+
pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<str>>, UnresolvedEnvVarError> {
773+
Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
774+
}
775+
755776
/// Returns the configured rpc url
756777
///
757778
/// Returns:
@@ -1774,6 +1795,7 @@ impl Default for Config {
17741795
block_gas_limit: None,
17751796
memory_limit: 2u64.pow(25),
17761797
eth_rpc_url: None,
1798+
eth_rpc_jwt: None,
17771799
etherscan_api_key: None,
17781800
verbosity: 0,
17791801
remappings: vec![],

crates/forge/tests/cli/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ forgetest!(can_extract_config_values, |prj: TestProject, mut cmd: TestCommand| {
8484
block_gas_limit: Some(100u64.into()),
8585
memory_limit: 2u64.pow(25),
8686
eth_rpc_url: Some("localhost".to_string()),
87+
eth_rpc_jwt: None,
8788
etherscan_api_key: None,
8889
etherscan: Default::default(),
8990
verbosity: 4,

0 commit comments

Comments
 (0)