Skip to content

Commit

Permalink
feat(cast): support websockets (#5571)
Browse files Browse the repository at this point in the history
* feat(cast): support websockets

* add tests and rework ipc path
  • Loading branch information
bernard-wagner authored Sep 4, 2023
1 parent b1c03fa commit 0f530f2
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 68 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

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
4 changes: 2 additions & 2 deletions crates/cast/bin/cmd/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use foundry_cli::{
opts::{EthereumOpts, TransactionOpts},
utils::{self, handle_traces, parse_ether_value, TraceResult},
};
use foundry_common::runtime_client::RuntimeClient;
use foundry_config::{find_project_root_path, Config};
use foundry_evm::{executor::opts::EvmOpts, trace::TracingExecutor};
use std::str::FromStr;

type Provider =
ethers::providers::Provider<ethers::providers::RetryClient<ethers::providers::Http>>;
type Provider = ethers::providers::Provider<RuntimeClient>;

/// CLI arguments for `cast call`.
#[derive(Debug, Parser)]
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
4 changes: 4 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tempfile = "3"

# misc
auto_impl = "1.1.0"
async-trait = "0.1"
serde = "1"
serde_json = "1"
thiserror = "1"
Expand All @@ -43,8 +44,11 @@ once_cell = "1"
dunce = "1"
regex = "1"
globset = "0.4"
tokio = "1"
url = "2"
# Using const-hex instead of hex for speed
hex.workspace = true


[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod fmt;
pub mod fs;
pub mod glob;
pub mod provider;
pub mod runtime_client;
pub mod selectors;
pub mod shell;
pub mod term;
Expand Down
90 changes: 46 additions & 44 deletions crates/common/src/provider.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
//! Commonly used helpers to construct `Provider`s
use crate::{ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT};
use crate::{runtime_client::RuntimeClient, 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, Authorization, Http, HttpRateLimitRetryPolicy, JwtAuth, JwtKey, Middleware,
Provider, RetryClient, RetryClientBuilder, DEFAULT_LOCAL_POLL_INTERVAL,
};
use ethers_providers::{is_local_endpoint, Middleware, Provider, DEFAULT_LOCAL_POLL_INTERVAL};
use eyre::WrapErr;
use reqwest::{header::HeaderValue, IntoUrl, Url};
use std::{borrow::Cow, time::Duration};
use reqwest::{IntoUrl, Url};
use std::{borrow::Cow, env, path::Path, time::Duration};
use url::ParseError;

/// Helper type alias for a retry provider
pub type RetryProvider = Provider<RetryClient<Http>>;
pub type RetryProvider = Provider<RuntimeClient>;

/// Helper type alias for a rpc url
pub type RpcUrl = String;
Expand Down Expand Up @@ -68,9 +66,38 @@ impl ProviderBuilder {
// prefix
return Self::new(format!("http://{url_str}"))
}
let err = format!("Invalid provider url: {url_str}");

let url = Url::parse(url_str)
.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(format!("Invalid provider url: {url_str}"));

Self {
url: url.into_url().wrap_err(err),
url,
chain: Chain::Mainnet,
max_retry: 100,
timeout_retry: 5,
Expand Down Expand Up @@ -176,43 +203,18 @@ impl ProviderBuilder {
} = self;
let url = url?;

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 mut provider = Provider::new(RuntimeClient::new(
url.clone(),
max_retry,
timeout_retry,
initial_backoff,
timeout,
compute_units_per_second,
jwt,
));

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

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

#[allow(clippy::box_default)]
let mut provider = Provider::new(
RetryClientBuilder::default()
.initial_backoff(Duration::from_millis(initial_backoff))
.rate_limit_retries(max_retry)
.timeout_retries(timeout_retry)
.compute_units_per_second(compute_units_per_second)
.build(provider, Box::new(HttpRateLimitRetryPolicy)),
);

if is_local {
provider = provider.interval(DEFAULT_LOCAL_POLL_INTERVAL);
} else if let Some(blocktime) = chain.average_blocktime_hint() {
Expand Down
Loading

0 comments on commit 0f530f2

Please sign in to comment.