Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cast): support websockets #5571

Merged
merged 2 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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