Skip to content

Commit 0f530f2

Browse files
feat(cast): support websockets (#5571)
* feat(cast): support websockets * add tests and rework ipc path
1 parent b1c03fa commit 0f530f2

File tree

12 files changed

+373
-68
lines changed

12 files changed

+373
-68
lines changed

Cargo.lock

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

crates/anvil/src/lib.rs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ use eth::backend::fork::ClientFork;
1717
use ethers::{
1818
core::k256::ecdsa::SigningKey,
1919
prelude::Wallet,
20-
providers::{Http, Provider, Ws},
2120
signers::Signer,
2221
types::{Address, U256},
2322
};
23+
use foundry_common::{ProviderBuilder, RetryProvider};
2424
use foundry_evm::revm;
2525
use futures::{FutureExt, TryFutureExt};
2626
use parking_lot::Mutex;
@@ -267,27 +267,23 @@ impl NodeHandle {
267267
}
268268

269269
/// Returns a Provider for the http endpoint
270-
pub fn http_provider(&self) -> Provider<Http> {
271-
Provider::<Http>::try_from(self.http_endpoint())
272-
.unwrap()
270+
pub fn http_provider(&self) -> RetryProvider {
271+
ProviderBuilder::new(self.http_endpoint())
272+
.build()
273+
.expect("Failed to connect using http provider")
273274
.interval(Duration::from_millis(500))
274275
}
275276

276277
/// Connects to the websocket Provider of the node
277-
pub async fn ws_provider(&self) -> Provider<Ws> {
278-
Provider::new(
279-
Ws::connect(self.ws_endpoint()).await.expect("Failed to connect to node's websocket"),
280-
)
278+
pub async fn ws_provider(&self) -> RetryProvider {
279+
ProviderBuilder::new(self.ws_endpoint())
280+
.build()
281+
.expect("Failed to connect to node's websocket")
281282
}
282283

283284
/// Connects to the ipc endpoint of the node, if spawned
284-
pub async fn ipc_provider(&self) -> Option<Provider<ethers::providers::Ipc>> {
285-
let ipc_path = self.config.get_ipc_path()?;
286-
tracing::trace!(target: "ipc", ?ipc_path, "connecting ipc provider");
287-
let provider = Provider::connect_ipc(&ipc_path).await.unwrap_or_else(|err| {
288-
panic!("Failed to connect to node's ipc endpoint {ipc_path}: {err:?}")
289-
});
290-
Some(provider)
285+
pub async fn ipc_provider(&self) -> Option<RetryProvider> {
286+
ProviderBuilder::new(self.config.get_ipc_path()?).build().ok()
291287
}
292288

293289
/// Signer accounts that can sign messages/transactions from the EVM node

crates/cast/bin/cmd/call.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use foundry_cli::{
99
opts::{EthereumOpts, TransactionOpts},
1010
utils::{self, handle_traces, parse_ether_value, TraceResult},
1111
};
12+
use foundry_common::runtime_client::RuntimeClient;
1213
use foundry_config::{find_project_root_path, Config};
1314
use foundry_evm::{executor::opts::EvmOpts, trace::TracingExecutor};
1415
use std::str::FromStr;
1516

16-
type Provider =
17-
ethers::providers::Provider<ethers::providers::RetryClient<ethers::providers::Http>>;
17+
type Provider = ethers::providers::Provider<RuntimeClient>;
1818

1919
/// CLI arguments for `cast call`.
2020
#[derive(Debug, Parser)]

crates/cast/tests/cli/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use foundry_test_utils::{
44
casttest,
55
util::{OutputExt, TestCommand, TestProject},
66
};
7-
use foundry_utils::rpc::next_http_rpc_endpoint;
7+
use foundry_utils::rpc::{next_http_rpc_endpoint, next_ws_rpc_endpoint};
88
use std::{io::Write, path::Path};
99

1010
// tests `--help` is printed to std out
@@ -243,6 +243,16 @@ casttest!(cast_rpc_no_args, |_: TestProject, mut cmd: TestCommand| {
243243
assert_eq!(output.trim_end(), r#""0x1""#);
244244
});
245245

246+
// test for cast_rpc without arguments using websocket
247+
casttest!(cast_ws_rpc_no_args, |_: TestProject, mut cmd: TestCommand| {
248+
let eth_rpc_url = next_ws_rpc_endpoint();
249+
250+
// Call `cast rpc eth_chainId`
251+
cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]);
252+
let output = cmd.stdout_lossy();
253+
assert_eq!(output.trim_end(), r#""0x1""#);
254+
});
255+
246256
// test for cast_rpc with arguments
247257
casttest!(cast_rpc_with_args, |_: TestProject, mut cmd: TestCommand| {
248258
let eth_rpc_url = next_http_rpc_endpoint();

crates/common/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ tempfile = "3"
3333

3434
# misc
3535
auto_impl = "1.1.0"
36+
async-trait = "0.1"
3637
serde = "1"
3738
serde_json = "1"
3839
thiserror = "1"
@@ -43,8 +44,11 @@ once_cell = "1"
4344
dunce = "1"
4445
regex = "1"
4546
globset = "0.4"
47+
tokio = "1"
48+
url = "2"
4649
# Using const-hex instead of hex for speed
4750
hex.workspace = true
4851

52+
4953
[dev-dependencies]
5054
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

crates/common/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod fmt;
1414
pub mod fs;
1515
pub mod glob;
1616
pub mod provider;
17+
pub mod runtime_client;
1718
pub mod selectors;
1819
pub mod shell;
1920
pub mod term;

crates/common/src/provider.rs

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
//! Commonly used helpers to construct `Provider`s
22
3-
use crate::{ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT};
3+
use crate::{runtime_client::RuntimeClient, ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT};
44
use ethers_core::types::{Chain, U256};
55
use ethers_middleware::gas_oracle::{GasCategory, GasOracle, Polygon};
6-
use ethers_providers::{
7-
is_local_endpoint, Authorization, Http, HttpRateLimitRetryPolicy, JwtAuth, JwtKey, Middleware,
8-
Provider, RetryClient, RetryClientBuilder, DEFAULT_LOCAL_POLL_INTERVAL,
9-
};
6+
use ethers_providers::{is_local_endpoint, Middleware, Provider, DEFAULT_LOCAL_POLL_INTERVAL};
107
use eyre::WrapErr;
11-
use reqwest::{header::HeaderValue, IntoUrl, Url};
12-
use std::{borrow::Cow, time::Duration};
8+
use reqwest::{IntoUrl, Url};
9+
use std::{borrow::Cow, env, path::Path, time::Duration};
10+
use url::ParseError;
1311

1412
/// Helper type alias for a retry provider
15-
pub type RetryProvider = Provider<RetryClient<Http>>;
13+
pub type RetryProvider = Provider<RuntimeClient>;
1614

1715
/// Helper type alias for a rpc url
1816
pub type RpcUrl = String;
@@ -68,9 +66,38 @@ impl ProviderBuilder {
6866
// prefix
6967
return Self::new(format!("http://{url_str}"))
7068
}
71-
let err = format!("Invalid provider url: {url_str}");
69+
70+
let url = Url::parse(url_str)
71+
.or_else(|err| {
72+
match err {
73+
ParseError::RelativeUrlWithoutBase => {
74+
let path = Path::new(url_str);
75+
let absolute_path = if path.is_absolute() {
76+
path.to_path_buf()
77+
} else {
78+
// Assume the path is relative to the current directory.
79+
// Don't use `std::fs::canonicalize` as it requires the path to exist.
80+
// It should be possible to construct a provider and only
81+
// attempt to establish a connection later
82+
let current_dir =
83+
env::current_dir().expect("Current directory should exist");
84+
current_dir.join(path)
85+
};
86+
87+
let path_str =
88+
absolute_path.to_str().expect("Path should be a valid string");
89+
90+
// invalid url: non-prefixed URL scheme is not allowed, so we assume the URL
91+
// is for a local file
92+
Url::parse(format!("file://{path_str}").as_str())
93+
}
94+
_ => Err(err),
95+
}
96+
})
97+
.wrap_err(format!("Invalid provider url: {url_str}"));
98+
7299
Self {
73-
url: url.into_url().wrap_err(err),
100+
url,
74101
chain: Chain::Mainnet,
75102
max_retry: 100,
76103
timeout_retry: 5,
@@ -176,43 +203,18 @@ impl ProviderBuilder {
176203
} = self;
177204
let url = url?;
178205

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-
}
206+
let mut provider = Provider::new(RuntimeClient::new(
207+
url.clone(),
208+
max_retry,
209+
timeout_retry,
210+
initial_backoff,
211+
timeout,
212+
compute_units_per_second,
213+
jwt,
214+
));
200215

201-
let client = client_builder.build()?;
202216
let is_local = is_local_endpoint(url.as_str());
203217

204-
let provider = Http::new_with_client(url, client);
205-
206-
#[allow(clippy::box_default)]
207-
let mut provider = Provider::new(
208-
RetryClientBuilder::default()
209-
.initial_backoff(Duration::from_millis(initial_backoff))
210-
.rate_limit_retries(max_retry)
211-
.timeout_retries(timeout_retry)
212-
.compute_units_per_second(compute_units_per_second)
213-
.build(provider, Box::new(HttpRateLimitRetryPolicy)),
214-
);
215-
216218
if is_local {
217219
provider = provider.interval(DEFAULT_LOCAL_POLL_INTERVAL);
218220
} else if let Some(blocktime) = chain.average_blocktime_hint() {

0 commit comments

Comments
 (0)