diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 301528791f2..f0e64cd59ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,18 +27,18 @@ jobs: - name: test run: cargo test --workspace ${{ matrix.flags }} - # wasm: - # name: check WASM - # runs-on: ubuntu-latest - # timeout-minutes: 30 - # steps: - # - uses: actions/checkout@v3 - # - uses: dtolnay/rust-toolchain@stable - # with: - # targets: wasm32-unknown-unknown - # - uses: Swatinem/rust-cache@v2 - # - name: check - # run: cargo check --workspace --target wasm32-unknown-unknown + wasm: + name: check WASM + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - name: check + run: cargo check --workspace --target wasm32-unknown-unknown feature-checks: name: feature checks diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index d6bc55c9119..045923096c4 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -54,7 +54,8 @@ where } } -#[async_trait::async_trait] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] /// Provider is parameterized with a network and a transport. The default /// transport is type-erased, but you can do `Provider`. pub trait Provider: Send + Sync { @@ -105,7 +106,8 @@ pub trait Provider: Send + Sync { } } -#[async_trait::async_trait] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for NetworkRpcClient { fn client(&self) -> &NetworkRpcClient { self diff --git a/crates/transports/Cargo.toml b/crates/transports/Cargo.toml index 0adfd4b2565..b6586121ba5 100644 --- a/crates/transports/Cargo.toml +++ b/crates/transports/Cargo.toml @@ -27,7 +27,11 @@ pin-project.workspace = true # feature deps reqwest = { version = "0.11.18", features = ["serde_json", "json"], optional = true } -hyper = { version = "0.14.27", optional = true, features = ["full"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.hyper] +version = "0.14.27" +optional = true +features = ["full"] [features] default = ["reqwest", "hyper"] diff --git a/crates/transports/README.md b/crates/transports/README.md index f2554df12c5..6de5e3b0c88 100644 --- a/crates/transports/README.md +++ b/crates/transports/README.md @@ -58,4 +58,4 @@ let balance = balance_fut.await.unwrap(); ### Features - `reqwest`: Enables the `reqwest` transport implementation. -- `hyper`: Enables the `hyper` transport implementation. +- `hyper`: Enables the `hyper` transport implementation (not available in WASM). diff --git a/crates/transports/src/batch.rs b/crates/transports/src/batch.rs index 3215cebfb00..2d0aae22b87 100644 --- a/crates/transports/src/batch.rs +++ b/crates/transports/src/batch.rs @@ -21,14 +21,18 @@ type ChannelMap = HashMap; /// call. #[derive(Debug)] pub struct BatchRequest<'a, T> { + /// The transport via which the batch will be sent. transport: &'a RpcClient, + /// The requests to be sent. requests: Vec>, + /// The channels to send the responses through. channels: ChannelMap, } /// Awaits a single response for a request that has been included in a batch. +#[must_use = "A Waiter does nothing unless the corresponding BatchRequest is sent via `send_batch` and `.await`, AND the Waiter is awaited."] pub struct Waiter { rx: oneshot::Receiver, TransportError>>, _resp: PhantomData, @@ -62,8 +66,7 @@ where #[pin_project::pin_project(project = CallStateProj)] pub enum BatchFuture where - Conn: Transport + Clone, - Conn::Future: Send, + Conn: Transport, { Prepared { transport: Conn, @@ -250,7 +253,6 @@ where impl Future for BatchFuture where T: Transport + Clone, - T::Future: Send, { type Output = Result<(), TransportError>; diff --git a/crates/transports/src/call.rs b/crates/transports/src/call.rs index 854e2f420af..d21d45f63e0 100644 --- a/crates/transports/src/call.rs +++ b/crates/transports/src/call.rs @@ -1,6 +1,7 @@ use crate::{ error::TransportError, transports::{JsonRpcLayer, JsonRpcService, Transport}, + RpcFut, }; use alloy_json_rpc::{Request, RpcParam, RpcResult, RpcReturn}; @@ -15,7 +16,6 @@ use tower::{Layer, Service}; enum CallState where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam, { Prepared { @@ -32,7 +32,6 @@ where impl CallState where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam, { fn poll_prepared( @@ -79,7 +78,6 @@ where impl Future for CallState where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam, { type Output = RpcResult, TransportError>; @@ -120,7 +118,6 @@ where pub struct RpcCall where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam, { #[pin] @@ -131,7 +128,6 @@ where impl RpcCall where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam, { #[doc(hidden)] @@ -164,14 +160,11 @@ where impl<'a, Conn, Params, Resp> RpcCall where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam + 'a, Resp: RpcReturn, { /// Convert this future into a boxed, pinned future, erasing its type. - pub fn boxed( - self, - ) -> Pin> + Send + 'a>> { + pub fn boxed(self) -> RpcFut<'a, Resp> { Box::pin(self) } } @@ -179,7 +172,6 @@ where impl Future for RpcCall where Conn: Transport + Clone, - Conn::Future: Send, Params: RpcParam, Resp: RpcReturn, { diff --git a/crates/transports/src/client.rs b/crates/transports/src/client.rs index 39a949a2364..67f821e88d0 100644 --- a/crates/transports/src/client.rs +++ b/crates/transports/src/client.rs @@ -1,6 +1,8 @@ use alloy_json_rpc::{Id, Request, RpcParam, RpcReturn}; -use serde_json::value::RawValue; -use tower::{layer::util::Stack, Layer, ServiceBuilder}; +use tower::{ + layer::util::{Identity, Stack}, + Layer, ServiceBuilder, +}; use std::{ borrow::Cow, @@ -31,6 +33,14 @@ pub struct RpcClient { pub(crate) id: AtomicU64, } +impl RpcClient { + pub fn builder() -> ClientBuilder { + ClientBuilder { + builder: ServiceBuilder::new(), + } + } +} + impl RpcClient { /// Create a new [`RpcClient`] with the given transport. pub fn new(t: T, is_local: bool) -> Self { @@ -41,6 +51,23 @@ impl RpcClient { } } + /// Build a `JsonRpcRequest` with the given method and params. + /// + /// This function reserves an ID for the request, however the request + /// is not sent. To send a request, use [`RpcClient::prepare`] and await + /// the returned [`RpcCall`]. + pub fn make_request<'a, Params: RpcParam>( + &self, + method: &'static str, + params: Cow<'a, Params>, + ) -> Request> { + Request { + method, + params, + id: self.next_id(), + } + } + /// `true` if the client believes the transport is local. /// /// This can be used to optimize remote API usage, or to change program @@ -73,7 +100,6 @@ impl RpcClient { impl RpcClient where T: Transport + Clone, - T::Future: Send, { /// Create a new [`BatchRequest`] builder. #[inline] @@ -81,23 +107,6 @@ where BatchRequest::new(self) } - /// Build a `JsonRpcRequest` with the given method and params. - /// - /// This function reserves an ID for the request, however the request - /// is not sent. To send a request, use [`RpcClient::prepare`] and await - /// the returned [`RpcCall`]. - pub fn make_request<'a, Params: RpcParam>( - &self, - method: &'static str, - params: Cow<'a, Params>, - ) -> Request> { - Request { - method, - params, - id: self.next_id(), - } - } - /// Prepare an [`RpcCall`]. /// /// This function reserves an ID for the request, however the request @@ -144,6 +153,14 @@ pub struct ClientBuilder { builder: ServiceBuilder, } +impl Default for ClientBuilder { + fn default() -> Self { + Self { + builder: ServiceBuilder::new(), + } + } +} + impl ClientBuilder { /// Add a middleware layer to the stack. /// @@ -162,37 +179,34 @@ impl ClientBuilder { L: Layer, T: Transport, L::Service: Transport, - >>::Future: Send, { RpcClient::new(self.builder.service(transport), is_local) } - #[cfg(feature = "reqwest")] /// Create a new [`RpcClient`] with a [`reqwest`] HTTP transport connecting /// to the given URL and the configured layers. + #[cfg(feature = "reqwest")] pub fn reqwest_http(self, url: reqwest::Url) -> RpcClient where L: Layer>, L::Service: Transport, - >>::Future: Send, { let transport = Http::new(url); - let is_local = transport.is_local(); + let is_local = transport.guess_local(); self.transport(transport, is_local) } - #[cfg(feature = "hyper")] /// Create a new [`RpcClient`] with a [`hyper`] HTTP transport connecting /// to the given URL and the configured layers. + #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] pub fn hyper_http(self, url: url::Url) -> RpcClient where L: Layer>>, L::Service: Transport, - >>::Future: Send, { let transport = Http::new(url); - let is_local = transport.is_local(); + let is_local = transport.guess_local(); self.transport(transport, is_local) } diff --git a/crates/transports/src/error.rs b/crates/transports/src/error.rs index 313570d5bbc..b08545781ef 100644 --- a/crates/transports/src/error.rs +++ b/crates/transports/src/error.rs @@ -28,7 +28,7 @@ pub enum TransportError { /// Hyper http transport #[error(transparent)] - #[cfg(feature = "hyper")] + #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] Hyper(#[from] hyper::Error), } diff --git a/crates/transports/src/lib.rs b/crates/transports/src/lib.rs index b489f06999a..e011e6ccb66 100644 --- a/crates/transports/src/lib.rs +++ b/crates/transports/src/lib.rs @@ -20,3 +20,35 @@ pub use transports::{BoxTransport, Http, Transport}; pub use alloy_json_rpc::RpcResult; pub(crate) mod utils; + +pub use type_aliases::*; + +#[cfg(not(target_arch = "wasm32"))] +mod type_aliases { + use alloy_json_rpc::RpcResult; + + use crate::TransportError; + + /// Future for Transport-level requests. + pub type TransportFut<'a, T = Box, E = TransportError> = + std::pin::Pin> + Send + 'a>>; + + /// Future for RPC-level requests. + pub type RpcFut<'a, T, E = TransportError> = + std::pin::Pin> + Send + 'a>>; +} + +#[cfg(target_arch = "wasm32")] +mod type_aliases { + use alloy_json_rpc::RpcResult; + + use crate::TransportError; + + /// Future for Transport-level requests. + pub type TransportFut<'a, T = Box, E = TransportError> = + std::pin::Pin> + 'a>>; + + /// Future for RPC-level requests. + pub type RpcFut<'a, T, E = TransportError> = + std::pin::Pin> + 'a>>; +} diff --git a/crates/transports/src/transports/http/hyper.rs b/crates/transports/src/transports/http/hyper.rs index 219ee985cf1..72032801eda 100644 --- a/crates/transports/src/transports/http/hyper.rs +++ b/crates/transports/src/transports/http/hyper.rs @@ -1,18 +1,16 @@ use hyper::client::{connect::Connect, Client}; use serde_json::value::RawValue; -use std::{future::Future, pin::Pin, task}; +use std::task; use tower::Service; -use crate::{Http, TransportError}; +use crate::{Http, TransportError, TransportFut}; impl Http> where C: Connect + Clone + Send + Sync + 'static, { - pub fn request( - &self, - req: Box, - ) -> Pin, TransportError>> + Send + 'static>> { + /// Make a request. + fn request(&self, req: Box) -> TransportFut<'static> { let this = self.clone(); Box::pin(async move { // convert the Box into a hyper request @@ -44,8 +42,7 @@ where { type Response = Box; type Error = TransportError; - type Future = - Pin> + Send + 'static>>; + type Future = TransportFut<'static>; #[inline] fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll> { @@ -65,8 +62,7 @@ where { type Response = Box; type Error = TransportError; - type Future = - Pin> + Send + 'static>>; + type Future = TransportFut<'static>; #[inline] fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll> { diff --git a/crates/transports/src/transports/http/mod.rs b/crates/transports/src/transports/http/mod.rs index a79b2edc1a0..0b20c42576b 100644 --- a/crates/transports/src/transports/http/mod.rs +++ b/crates/transports/src/transports/http/mod.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "hyper")] +#[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] mod hyper; #[cfg(feature = "reqwest")] @@ -41,13 +41,36 @@ impl Http { Self { client, url } } - /// True if the connection has no hostname, or the hostname is `localhost` - /// or `127.0.0.1`. - pub fn is_local(&self) -> bool { + /// Set the URL. + pub fn set_url(&mut self, url: Url) { + self.url = url; + } + + /// Set the client. + pub fn set_client(&mut self, client: T) { + self.client = client; + } + + /// Guess whether the URL is local, based on the hostname. + /// + /// The ouput of this function is best-efforts, and should be checked if + /// possible. It simply returns `true` if the connection has no hostname, + /// or the hostname is `localhost` or `127.0.0.1`. + pub fn guess_local(&self) -> bool { self.url .host_str() .map_or(true, |host| host == "localhost" || host == "127.0.0.1") } + + /// Get a reference to the client. + pub fn client(&self) -> &T { + &self.client + } + + /// Get a reference to the URL. + pub fn url(&self) -> &str { + self.url.as_ref() + } } impl RpcClient> @@ -57,7 +80,7 @@ where /// Create a new [`RpcClient`] from a URL. pub fn new_http(url: Url) -> Self { let transport = Http::new(url); - let is_local = transport.is_local(); + let is_local = transport.guess_local(); Self { transport, is_local, diff --git a/crates/transports/src/transports/http/reqwest.rs b/crates/transports/src/transports/http/reqwest.rs index f0c306ad227..49286c4627e 100644 --- a/crates/transports/src/transports/http/reqwest.rs +++ b/crates/transports/src/transports/http/reqwest.rs @@ -1,14 +1,12 @@ use serde_json::value::RawValue; -use std::{future::Future, pin::Pin, task}; +use std::task; use tower::Service; -use crate::{Http, TransportError}; +use crate::{Http, TransportError, TransportFut}; impl Http { - fn request( - &self, - req: Box, - ) -> Pin, TransportError>> + Send + 'static>> { + /// Make a request. + fn request(&self, req: Box) -> TransportFut<'static> { let this = self.clone(); Box::pin(async move { let resp = this.client.post(this.url).json(&req).send().await?; @@ -22,8 +20,7 @@ impl Http { impl Service> for Http { type Response = Box; type Error = TransportError; - type Future = - Pin> + Send + 'static>>; + type Future = TransportFut<'static>; #[inline] fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll> { @@ -40,8 +37,7 @@ impl Service> for Http { impl Service> for &Http { type Response = Box; type Error = TransportError; - type Future = - Pin> + Send + 'static>>; + type Future = TransportFut<'static>; #[inline] fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll> { diff --git a/crates/transports/src/transports/mod.rs b/crates/transports/src/transports/mod.rs index a92f9fa2601..7262fb5b117 100644 --- a/crates/transports/src/transports/mod.rs +++ b/crates/transports/src/transports/mod.rs @@ -5,27 +5,30 @@ mod json_service; pub(crate) use json_service::{JsonRpcLayer, JsonRpcService}; use serde_json::value::RawValue; -use std::{future::Future, pin::Pin}; +use std::fmt::Debug; use tower::Service; -use crate::TransportError; +use crate::{TransportError, TransportFut}; /// A marker trait for transports. /// +/// # Implementing `Transport` +/// /// This trait is blanket implemented for all appropriate types. To implement /// this trait, you must implement the [`tower::Service`] trait with the -/// appropriate associated types. +/// appropriate associated types. It cannot be implemented directly. pub trait Transport: private::Sealed + Service< Box, Response = Box, Error = TransportError, - Future = Pin, TransportError>> + Send>>, + Future = TransportFut<'static>, > + Send + Sync + 'static { + /// Convert this transport into a boxed trait object. fn boxed(self) -> BoxTransport where Self: Sized + Clone + Send + Sync + 'static, @@ -42,17 +45,36 @@ impl Transport for T where Box, Response = Box, Error = TransportError, - Future = Pin, TransportError>> + Send>>, + Future = TransportFut<'static>, > + Send + Sync + 'static { } +/// A boxed, Clone-able [`Transport`] trait object. +/// +/// This type allows [`RpcClient`] to use a type-erased transport. It is +/// [`Clone`] and [`Send`] + [`Sync`], and implementes [`Transport`]. This +/// allows for complex behavior abstracting across several different clients +/// with different transport types. +/// +/// Most higher-level types will be generic over `T: Transport = BoxTransport`. +/// This allows paramterization with a concrete type, while hiding this +/// complexity from the library consumer. +/// +/// [`RpcClient`]: crate::client::RpcClient +#[repr(transparent)] pub struct BoxTransport { inner: Box, } +impl Debug for BoxTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BoxTransport").finish() + } +} + impl Clone for BoxTransport { fn clone(&self) -> Self { Self { @@ -61,6 +83,7 @@ impl Clone for BoxTransport { } } +/// Helper trait for constructing [`BoxTransport`]. trait CloneTransport: Transport { fn clone_box(&self) -> Box; } @@ -79,7 +102,7 @@ impl Service> for BoxTransport { type Error = TransportError; - type Future = Pin, TransportError>> + Send>>; + type Future = TransportFut<'static>; fn poll_ready( &mut self, @@ -110,9 +133,7 @@ mod private { Box, Response = Box, Error = TransportError, - Future = Pin< - Box, TransportError>> + Send>, - >, + Future = TransportFut<'static>, > + Send + Sync + 'static