Skip to content

Commit b1c968a

Browse files
add tests and rework ipc path
1 parent be348b7 commit b1c968a

File tree

6 files changed

+103
-37
lines changed

6 files changed

+103
-37
lines changed

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/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/src/provider.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use ethers_middleware::gas_oracle::{GasCategory, GasOracle, Polygon};
66
use ethers_providers::{is_local_endpoint, Middleware, Provider, DEFAULT_LOCAL_POLL_INTERVAL};
77
use eyre::WrapErr;
88
use reqwest::{IntoUrl, Url};
9-
use std::{borrow::Cow, time::Duration};
9+
use std::{borrow::Cow, env, path::Path, time::Duration};
1010
use url::ParseError;
1111

1212
/// Helper type alias for a retry provider
@@ -64,13 +64,35 @@ impl ProviderBuilder {
6464
// prefix
6565
return Self::new(format!("http://{url_str}"))
6666
}
67-
let err = format!("Invalid provider url: {url_str}");
67+
6868
let url = Url::parse(url_str)
69-
.and_then(|url| match url.scheme() {
70-
"http" | "https" | "wss" | "ws" | "file" => Ok(url),
71-
_ => Err(ParseError::EmptyHost),
69+
.or_else(|err| {
70+
match err {
71+
ParseError::RelativeUrlWithoutBase => {
72+
let path = Path::new(url_str);
73+
let absolute_path = if path.is_absolute() {
74+
path.to_path_buf()
75+
} else {
76+
// Assume the path is relative to the current directory.
77+
// Don't use `std::fs::canonicalize` as it requires the path to exist.
78+
// It should be possible to construct a provider and only
79+
// attempt to establish a connection later
80+
let current_dir =
81+
env::current_dir().expect("Current directory should exist");
82+
current_dir.join(path)
83+
};
84+
85+
let path_str =
86+
absolute_path.to_str().expect("Path should be a valid string");
87+
88+
// invalid url: non-prefixed URL scheme is not allowed, so we assume the URL
89+
// is for a local file
90+
Url::parse(format!("file://{path_str}").as_str())
91+
}
92+
_ => Err(err),
93+
}
7294
})
73-
.wrap_err(err);
95+
.wrap_err(format!("Invalid provider url: {url_str}"));
7496

7597
Self {
7698
url,

crates/common/src/runtime_client.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use ethers_providers::{
66
Http, HttpRateLimitRetryPolicy, Ipc, JsonRpcClient, JsonRpcError, ProviderError, PubsubClient,
77
RetryClient, RetryClientBuilder, RpcError, Ws,
88
};
9+
use reqwest::Url;
910
use serde::{de::DeserializeOwned, Serialize};
10-
use std::{fmt::Debug, path, sync::Arc, time::Duration};
11+
use std::{fmt::Debug, sync::Arc, time::Duration};
1112
use thiserror::Error;
1213
use tokio::sync::RwLock;
13-
use url::Url;
1414

1515
/// Enum representing a the client types supported by the runtime provider
1616
#[derive(Debug)]
@@ -31,7 +31,16 @@ pub enum RuntimeClientError {
3131
ProviderError(ProviderError),
3232

3333
/// Failed to lock the client
34+
#[error("Failed to lock the client")]
3435
LockError,
36+
37+
/// Invalid URL scheme
38+
#[error("URL scheme is not supported: {0}")]
39+
BadScheme(String),
40+
41+
/// Invalid file path
42+
#[error("Invalid IPC file path: {0}")]
43+
BadPath(String),
3544
}
3645

3746
impl RpcError for RuntimeClientError {
@@ -50,26 +59,18 @@ impl RpcError for RuntimeClientError {
5059
}
5160
}
5261

53-
impl std::fmt::Display for RuntimeClientError {
54-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55-
write!(f, "{self:?}")
56-
}
57-
}
58-
5962
impl From<RuntimeClientError> for ProviderError {
6063
fn from(src: RuntimeClientError) -> Self {
6164
match src {
6265
RuntimeClientError::ProviderError(err) => err,
63-
RuntimeClientError::LockError => {
64-
ProviderError::CustomError("Failed to lock the client".to_string())
65-
}
66+
_ => ProviderError::JsonRpcClientError(Box::new(src)),
6667
}
6768
}
6869
}
6970

7071
/// A provider that connects on first request allowing handling of different provider types at
7172
/// runtime
72-
#[derive(Debug, Error)]
73+
#[derive(Clone, Debug, Error)]
7374
pub struct RuntimeClient {
7475
client: Arc<RwLock<Option<InnerClient>>>,
7576
url: Url,
@@ -127,20 +128,25 @@ impl RuntimeClient {
127128
Ok(InnerClient::Http(provider))
128129
}
129130
"ws" | "wss" => {
130-
let client = Ws::connect(&self.url.to_string())
131+
let client = Ws::connect(self.url.as_str())
131132
.await
132133
.map_err(|e| RuntimeClientError::ProviderError(e.into()))?;
133134

134135
Ok(InnerClient::Ws(client))
135136
}
136137
"file" => {
137-
let client = Ipc::connect(path::Path::new(&self.url.to_string()))
138+
let path = self
139+
.url
140+
.to_file_path()
141+
.map_err(|_| RuntimeClientError::BadPath(self.url.to_string()))?;
142+
143+
let client = Ipc::connect(path)
138144
.await
139145
.map_err(|e| RuntimeClientError::ProviderError(e.into()))?;
140146

141147
Ok(InnerClient::Ipc(client))
142148
}
143-
_ => Err(RuntimeClientError::ProviderError(ProviderError::UnsupportedNodeClient)),
149+
_ => Err(RuntimeClientError::BadScheme(self.url.to_string())),
144150
}
145151
}
146152
}

crates/forge/tests/it/fork.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ async fn test_launch_fork() {
5050
TestConfig::with_filter(runner, filter).run().await;
5151
}
5252

53+
/// Smoke test that forking workings with websockets
54+
#[tokio::test(flavor = "multi_thread")]
55+
async fn test_launch_fork_ws() {
56+
let rpc_url = foundry_utils::rpc::next_ws_archive_rpc_endpoint();
57+
let runner = forked_runner(&rpc_url).await;
58+
let filter = Filter::new(".*", ".*", &format!(".*fork{RE_PATH_SEPARATOR}Launch"));
59+
TestConfig::with_filter(runner, filter).run().await;
60+
}
61+
5362
/// Tests that we can transact transactions in forking mode
5463
#[tokio::test(flavor = "multi_thread")]
5564
async fn test_transact_fork() {

crates/utils/src/rpc.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ pub fn next_http_rpc_endpoint() -> String {
7070
next_rpc_endpoint("mainnet")
7171
}
7272

73+
/// Returns the next _mainnet_ rpc endpoint in inline
74+
///
75+
/// This will rotate all available rpc endpoints
76+
pub fn next_ws_rpc_endpoint() -> String {
77+
next_ws_endpoint("mainnet")
78+
}
79+
7380
pub fn next_rpc_endpoint(network: &str) -> String {
7481
let idx = next() % num_keys();
7582
if idx < INFURA_KEYS.len() {
@@ -80,12 +87,28 @@ pub fn next_rpc_endpoint(network: &str) -> String {
8087
}
8188
}
8289

90+
pub fn next_ws_endpoint(network: &str) -> String {
91+
let idx = next() % num_keys();
92+
if idx < INFURA_KEYS.len() {
93+
format!("wss://{network}.infura.io/v3/{}", INFURA_KEYS[idx])
94+
} else {
95+
let idx = idx - INFURA_KEYS.len();
96+
format!("wss://eth-{network}.alchemyapi.io/v2/{}", ALCHEMY_MAINNET_KEYS[idx])
97+
}
98+
}
99+
83100
/// Returns endpoint that has access to archive state
84101
pub fn next_http_archive_rpc_endpoint() -> String {
85102
let idx = next() % ALCHEMY_MAINNET_KEYS.len();
86103
format!("https://eth-mainnet.alchemyapi.io/v2/{}", ALCHEMY_MAINNET_KEYS[idx])
87104
}
88105

106+
/// Returns endpoint that has access to archive state
107+
pub fn next_ws_archive_rpc_endpoint() -> String {
108+
let idx = next() % ALCHEMY_MAINNET_KEYS.len();
109+
format!("wss://eth-mainnet.alchemyapi.io/v2/{}", ALCHEMY_MAINNET_KEYS[idx])
110+
}
111+
89112
#[cfg(test)]
90113
mod tests {
91114
use super::*;

0 commit comments

Comments
 (0)