Skip to content

Commit

Permalink
add tests and rework ipc path
Browse files Browse the repository at this point in the history
  • Loading branch information
bernard-wagner committed Aug 29, 2023
1 parent 32d5bc0 commit 815b601
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 37 deletions.
26 changes: 11 additions & 15 deletions crates/anvil/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -267,27 +267,23 @@ impl NodeHandle {
}

/// Returns a Provider for the http endpoint
pub fn http_provider(&self) -> Provider<Http> {
Provider::<Http>::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<Ws> {
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<Provider<ethers::providers::Ipc>> {
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<RetryProvider> {
ProviderBuilder::new(self.config.get_ipc_path()?).build().ok()
}

/// Signer accounts that can sign messages/transactions from the EVM node
Expand Down
12 changes: 11 additions & 1 deletion crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
34 changes: 28 additions & 6 deletions crates/common/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 20 additions & 15 deletions crates/common/src/runtime_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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 {
Expand All @@ -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<RuntimeClientError> 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<RwLock<Option<InnerClient>>>,
url: Url,
Expand Down Expand Up @@ -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())),
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions crates/forge/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
23 changes: 23 additions & 0 deletions crates/utils/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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::*;
Expand Down

0 comments on commit 815b601

Please sign in to comment.