diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index 17005bb598e0..134de3db3d0d 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -17,10 +17,10 @@ use eth::backend::fork::ClientFork; use ethers::{ core::k256::ecdsa::SigningKey, prelude::Wallet, - providers::{Http, Provider, Ws}, signers::Signer, types::{Address, U256}, }; +use foundry_common::{ProviderBuilder, RetryProvider}; use foundry_evm::revm; use futures::{FutureExt, TryFutureExt}; use parking_lot::Mutex; @@ -267,27 +267,23 @@ impl NodeHandle { } /// Returns a Provider for the http endpoint - pub fn http_provider(&self) -> Provider { - Provider::::try_from(self.http_endpoint()) - .unwrap() + pub fn http_provider(&self) -> RetryProvider { + ProviderBuilder::new(self.http_endpoint()) + .build() + .expect("Failed to connect using http provider") .interval(Duration::from_millis(500)) } /// Connects to the websocket Provider of the node - pub async fn ws_provider(&self) -> Provider { - Provider::new( - Ws::connect(self.ws_endpoint()).await.expect("Failed to connect to node's websocket"), - ) + pub async fn ws_provider(&self) -> RetryProvider { + ProviderBuilder::new(self.ws_endpoint()) + .build() + .expect("Failed to connect to node's websocket") } /// Connects to the ipc endpoint of the node, if spawned - pub async fn ipc_provider(&self) -> Option> { - let ipc_path = self.config.get_ipc_path()?; - tracing::trace!(target: "ipc", ?ipc_path, "connecting ipc provider"); - let provider = Provider::connect_ipc(&ipc_path).await.unwrap_or_else(|err| { - panic!("Failed to connect to node's ipc endpoint {ipc_path}: {err:?}") - }); - Some(provider) + pub async fn ipc_provider(&self) -> Option { + ProviderBuilder::new(self.config.get_ipc_path()?).build().ok() } /// Signer accounts that can sign messages/transactions from the EVM node diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 9eff595b6969..cbc999079262 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -4,7 +4,7 @@ use foundry_test_utils::{ casttest, util::{OutputExt, TestCommand, TestProject}, }; -use foundry_utils::rpc::next_http_rpc_endpoint; +use foundry_utils::rpc::{next_http_rpc_endpoint, next_ws_rpc_endpoint}; use std::{io::Write, path::Path}; // tests `--help` is printed to std out @@ -243,6 +243,16 @@ casttest!(cast_rpc_no_args, |_: TestProject, mut cmd: TestCommand| { assert_eq!(output.trim_end(), r#""0x1""#); }); +// test for cast_rpc without arguments using websocket +casttest!(cast_ws_rpc_no_args, |_: TestProject, mut cmd: TestCommand| { + let eth_rpc_url = next_ws_rpc_endpoint(); + + // Call `cast rpc eth_chainId` + cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]); + let output = cmd.stdout_lossy(); + assert_eq!(output.trim_end(), r#""0x1""#); +}); + // test for cast_rpc with arguments casttest!(cast_rpc_with_args, |_: TestProject, mut cmd: TestCommand| { let eth_rpc_url = next_http_rpc_endpoint(); diff --git a/crates/common/src/provider.rs b/crates/common/src/provider.rs index 888f673476b6..0af44f320ef1 100644 --- a/crates/common/src/provider.rs +++ b/crates/common/src/provider.rs @@ -6,7 +6,7 @@ use ethers_middleware::gas_oracle::{GasCategory, GasOracle, Polygon}; use ethers_providers::{is_local_endpoint, Middleware, Provider, DEFAULT_LOCAL_POLL_INTERVAL}; use eyre::WrapErr; use reqwest::{IntoUrl, Url}; -use std::{borrow::Cow, time::Duration}; +use std::{borrow::Cow, env, path::Path, time::Duration}; use url::ParseError; /// Helper type alias for a retry provider @@ -66,13 +66,35 @@ impl ProviderBuilder { // prefix return Self::new(format!("http://{url_str}")) } - let err = format!("Invalid provider url: {url_str}"); + let url = Url::parse(url_str) - .and_then(|url| match url.scheme() { - "http" | "https" | "wss" | "ws" | "file" => Ok(url), - _ => Err(ParseError::EmptyHost), + .or_else(|err| { + match err { + ParseError::RelativeUrlWithoutBase => { + let path = Path::new(url_str); + let absolute_path = if path.is_absolute() { + path.to_path_buf() + } else { + // Assume the path is relative to the current directory. + // Don't use `std::fs::canonicalize` as it requires the path to exist. + // It should be possible to construct a provider and only + // attempt to establish a connection later + let current_dir = + env::current_dir().expect("Current directory should exist"); + current_dir.join(path) + }; + + let path_str = + absolute_path.to_str().expect("Path should be a valid string"); + + // invalid url: non-prefixed URL scheme is not allowed, so we assume the URL + // is for a local file + Url::parse(format!("file://{path_str}").as_str()) + } + _ => Err(err), + } }) - .wrap_err(err); + .wrap_err(format!("Invalid provider url: {url_str}")); Self { url, diff --git a/crates/common/src/runtime_client.rs b/crates/common/src/runtime_client.rs index 425a22225874..cd89736aab6b 100644 --- a/crates/common/src/runtime_client.rs +++ b/crates/common/src/runtime_client.rs @@ -7,12 +7,11 @@ use ethers_providers::{ JsonRpcError, JwtAuth, JwtKey, ProviderError, PubsubClient, RetryClient, RetryClientBuilder, RpcError, Ws, }; -use reqwest::header::HeaderValue; +use reqwest::{header::HeaderValue, Url}; use serde::{de::DeserializeOwned, Serialize}; -use std::{fmt::Debug, path, sync::Arc, time::Duration}; +use std::{fmt::Debug, sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::RwLock; -use url::Url; /// Enum representing a the client types supported by the runtime provider #[derive(Debug)] @@ -33,7 +32,16 @@ pub enum RuntimeClientError { ProviderError(ProviderError), /// Failed to lock the client + #[error("Failed to lock the client")] LockError, + + /// Invalid URL scheme + #[error("URL scheme is not supported: {0}")] + BadScheme(String), + + /// Invalid file path + #[error("Invalid IPC file path: {0}")] + BadPath(String), } impl RpcError for RuntimeClientError { @@ -52,26 +60,18 @@ impl RpcError for RuntimeClientError { } } -impl std::fmt::Display for RuntimeClientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - impl From for ProviderError { fn from(src: RuntimeClientError) -> Self { match src { RuntimeClientError::ProviderError(err) => err, - RuntimeClientError::LockError => { - ProviderError::CustomError("Failed to lock the client".to_string()) - } + _ => ProviderError::JsonRpcClientError(Box::new(src)), } } } /// A provider that connects on first request allowing handling of different provider types at /// runtime -#[derive(Debug, Error)] +#[derive(Clone, Debug, Error)] pub struct RuntimeClient { client: Arc>>, url: Url, @@ -176,13 +176,18 @@ impl RuntimeClient { Ok(InnerClient::Ws(client)) } "file" => { - let client = Ipc::connect(path::Path::new(&self.url.to_string())) + let path = self + .url + .to_file_path() + .map_err(|_| RuntimeClientError::BadPath(self.url.to_string()))?; + + let client = Ipc::connect(path) .await .map_err(|e| RuntimeClientError::ProviderError(e.into()))?; Ok(InnerClient::Ipc(client)) } - _ => Err(RuntimeClientError::ProviderError(ProviderError::UnsupportedNodeClient)), + _ => Err(RuntimeClientError::BadScheme(self.url.to_string())), } } } diff --git a/crates/forge/tests/it/fork.rs b/crates/forge/tests/it/fork.rs index 0d52239ef9b8..9d10e8a5def8 100644 --- a/crates/forge/tests/it/fork.rs +++ b/crates/forge/tests/it/fork.rs @@ -76,6 +76,15 @@ async fn test_launch_fork() { TestConfig::with_filter(runner, filter).run().await; } +/// Smoke test that forking workings with websockets +#[tokio::test(flavor = "multi_thread")] +async fn test_launch_fork_ws() { + let rpc_url = foundry_utils::rpc::next_ws_archive_rpc_endpoint(); + let runner = forked_runner(&rpc_url).await; + let filter = Filter::new(".*", ".*", &format!(".*fork{RE_PATH_SEPARATOR}Launch")); + TestConfig::with_filter(runner, filter).run().await; +} + /// Tests that we can transact transactions in forking mode #[tokio::test(flavor = "multi_thread")] async fn test_transact_fork() { diff --git a/crates/utils/src/rpc.rs b/crates/utils/src/rpc.rs index c32cbe199f12..a41f2dddaba4 100644 --- a/crates/utils/src/rpc.rs +++ b/crates/utils/src/rpc.rs @@ -70,6 +70,13 @@ pub fn next_http_rpc_endpoint() -> String { next_rpc_endpoint("mainnet") } +/// Returns the next _mainnet_ rpc endpoint in inline +/// +/// This will rotate all available rpc endpoints +pub fn next_ws_rpc_endpoint() -> String { + next_ws_endpoint("mainnet") +} + pub fn next_rpc_endpoint(network: &str) -> String { let idx = next() % num_keys(); if idx < INFURA_KEYS.len() { @@ -80,12 +87,28 @@ pub fn next_rpc_endpoint(network: &str) -> String { } } +pub fn next_ws_endpoint(network: &str) -> String { + let idx = next() % num_keys(); + if idx < INFURA_KEYS.len() { + format!("wss://{network}.infura.io/v3/{}", INFURA_KEYS[idx]) + } else { + let idx = idx - INFURA_KEYS.len(); + format!("wss://eth-{network}.alchemyapi.io/v2/{}", ALCHEMY_MAINNET_KEYS[idx]) + } +} + /// Returns endpoint that has access to archive state pub fn next_http_archive_rpc_endpoint() -> String { let idx = next() % ALCHEMY_MAINNET_KEYS.len(); format!("https://eth-mainnet.alchemyapi.io/v2/{}", ALCHEMY_MAINNET_KEYS[idx]) } +/// Returns endpoint that has access to archive state +pub fn next_ws_archive_rpc_endpoint() -> String { + let idx = next() % ALCHEMY_MAINNET_KEYS.len(); + format!("wss://eth-mainnet.alchemyapi.io/v2/{}", ALCHEMY_MAINNET_KEYS[idx]) +} + #[cfg(test)] mod tests { use super::*;