Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 33 additions & 34 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions crates/cli/src/opts/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ pub struct RpcOpts {
/// Use the Flashbots RPC URL (https://rpc.flashbots.net).
#[clap(long)]
pub flashbots: bool,

/// JWT Secret for the RPC endpoint.
///
/// The JWT secret will be used to create a JWT for a RPC. For example, the following can be
/// used to simulate a CL `engine_forkchoiceUpdated` call:
///
/// cast rpc --jwt-secret <JWT_SECRET> engine_forkchoiceUpdatedV2
/// '["0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
/// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc",
/// "0x6bb38c26db65749ab6e472080a3d20a2f35776494e72016d1e339593f21c59bc"]'
#[clap(long, env = "ETH_RPC_JWT_SECRET")]
pub jwt_secret: Option<String>,
}

impl_figment_convert_cast!(RpcOpts);
Expand All @@ -49,11 +61,24 @@ impl RpcOpts {
Ok(url)
}

/// Returns the JWT secret.
pub fn jwt<'a>(&'a self, config: Option<&'a Config>) -> Result<Option<Cow<'a, str>>> {
let jwt = match (self.jwt_secret.as_deref(), config) {
(Some(jwt), _) => Some(Cow::Borrowed(jwt)),
(None, Some(config)) => config.get_rpc_jwt_secret()?,
(None, None) => None,
};
Ok(jwt)
}

pub fn dict(&self) -> Dict {
let mut dict = Dict::new();
if let Ok(Some(url)) = self.url(None) {
dict.insert("eth_rpc_url".into(), url.into_owned().into());
}
if let Ok(Some(jwt)) = self.jwt(None) {
dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into());
}
dict
}
}
Expand Down
10 changes: 9 additions & 1 deletion crates/cli/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,22 @@ pub fn parse_u256(s: &str) -> Result<U256> {
pub fn get_provider(config: &Config) -> Result<foundry_common::RetryProvider> {
get_provider_builder(config)?.build()
}

/// Returns a [ProviderBuilder](foundry_common::ProviderBuilder) instantiated using [Config]'s RPC
/// URL and chain.
///
/// Defaults to `http://localhost:8545` and `Mainnet`.
pub fn get_provider_builder(config: &Config) -> Result<foundry_common::ProviderBuilder> {
let url = config.get_rpc_url_or_localhost_http()?;
let chain = config.chain_id.unwrap_or_default();
Ok(foundry_common::ProviderBuilder::new(url.as_ref()).chain(chain))
let mut builder = foundry_common::ProviderBuilder::new(url.as_ref()).chain(chain);

let jwt = config.get_rpc_jwt_secret()?;
if let Some(jwt) = jwt {
builder = builder.jwt(jwt.as_ref());
}

Ok(builder)
}

pub async fn get_chain<M>(chain: Option<Chain>, provider: M) -> Result<Chain>
Expand Down
2 changes: 2 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ once_cell = "1"
dunce = "1"
regex = "1"
globset = "0.4"
# Using const-hex instead of hex for speed
hex.workspace = true

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
40 changes: 36 additions & 4 deletions crates/common/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ use crate::{ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT};
use ethers_core::types::{Chain, U256};
use ethers_middleware::gas_oracle::{GasCategory, GasOracle, Polygon};
use ethers_providers::{
is_local_endpoint, Http, HttpRateLimitRetryPolicy, Middleware, Provider, RetryClient,
RetryClientBuilder, DEFAULT_LOCAL_POLL_INTERVAL,
is_local_endpoint, Authorization, Http, HttpRateLimitRetryPolicy, JwtAuth, JwtKey, Middleware,
Provider, RetryClient, RetryClientBuilder, DEFAULT_LOCAL_POLL_INTERVAL,
};
use eyre::WrapErr;
use reqwest::{IntoUrl, Url};
use reqwest::{header::HeaderValue, IntoUrl, Url};
use std::{borrow::Cow, time::Duration};

/// Helper type alias for a retry provider
Expand Down Expand Up @@ -53,6 +53,8 @@ pub struct ProviderBuilder {
timeout: Duration,
/// available CUPS
compute_units_per_second: u64,
/// JWT Secret
jwt: Option<String>,
}

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

Expand Down Expand Up @@ -141,6 +144,12 @@ impl ProviderBuilder {
self.max_retry(100).initial_backoff(100)
}

/// Sets the JWT secret
pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
self.jwt = Some(jwt.into());
self
}

/// Same as [`Self:build()`] but also retrieves the `chainId` in order to derive an appropriate
/// interval
pub async fn connect(self) -> eyre::Result<RetryProvider> {
Expand All @@ -163,10 +172,33 @@ impl ProviderBuilder {
initial_backoff,
timeout,
compute_units_per_second,
jwt,
} = self;
let url = url?;

let client = reqwest::Client::builder().timeout(timeout).build()?;
let mut client_builder = reqwest::Client::builder().timeout(timeout);

// Set the JWT auth as a header if present
if let Some(jwt) = jwt {
// Decode jwt from hex, then generate claims (iat with current timestamp)
let jwt = hex::decode(jwt)?;
let secret =
JwtKey::from_slice(&jwt).map_err(|err| eyre::eyre!("Invalid JWT: {}", err))?;
let auth = JwtAuth::new(secret, None, None);
let token = auth.generate_token()?;

// Essentially unrolled ethers-rs new_with_auth to accomodate the custom timeout
let auth = Authorization::Bearer(token);
let mut auth_value = HeaderValue::from_str(&auth.to_string())?;
auth_value.set_sensitive(true);

let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::AUTHORIZATION, auth_value);

client_builder = client_builder.default_headers(headers);
}

let client = client_builder.build()?;
let is_local = is_local_endpoint(url.as_str());

let provider = Http::new_with_client(url, client);
Expand Down
Loading