From 5967d6c6cc602c040aeeed8643b366bcb4f94ebf Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Nov 2023 15:02:51 -0800 Subject: [PATCH 01/30] feature: new pubsub --- crates/json-rpc/src/packet.rs | 34 ++- crates/json-rpc/src/request.rs | 41 ++- crates/providers/src/provider.rs | 4 +- crates/transports/Cargo.toml | 10 +- crates/transports/src/batch.rs | 18 +- crates/transports/src/call.rs | 14 +- crates/transports/src/client.rs | 12 +- crates/transports/src/error.rs | 13 +- crates/transports/src/lib.rs | 11 +- crates/transports/src/pubsub/frontend.rs | 123 +++++++++ crates/transports/src/pubsub/handle.rs | 67 +++++ crates/transports/src/pubsub/ix.rs | 25 ++ .../src/pubsub/managers/active_sub.rs | 87 +++++++ .../src/pubsub/managers/in_flight.rs | 75 ++++++ crates/transports/src/pubsub/managers/mod.rs | 11 + crates/transports/src/pubsub/managers/req.rs | 40 +++ crates/transports/src/pubsub/managers/sub.rs | 97 +++++++ crates/transports/src/pubsub/mod.rs | 50 ++++ crates/transports/src/pubsub/service.rs | 245 ++++++++++++++++++ crates/transports/src/pubsub/trait.rs | 101 ++++++++ crates/transports/src/transports/boxed.rs | 8 +- crates/transports/src/transports/connect.rs | 14 +- .../transports/src/transports/http/hyper.rs | 20 +- .../transports/src/transports/http/reqwest.rs | 18 +- crates/transports/src/transports/json.rs | 143 ---------- crates/transports/src/transports/mod.rs | 7 +- .../src/transports/{transport.rs => trait.rs} | 14 +- 27 files changed, 1077 insertions(+), 225 deletions(-) create mode 100644 crates/transports/src/pubsub/frontend.rs create mode 100644 crates/transports/src/pubsub/handle.rs create mode 100644 crates/transports/src/pubsub/ix.rs create mode 100644 crates/transports/src/pubsub/managers/active_sub.rs create mode 100644 crates/transports/src/pubsub/managers/in_flight.rs create mode 100644 crates/transports/src/pubsub/managers/mod.rs create mode 100644 crates/transports/src/pubsub/managers/req.rs create mode 100644 crates/transports/src/pubsub/managers/sub.rs create mode 100644 crates/transports/src/pubsub/mod.rs create mode 100644 crates/transports/src/pubsub/service.rs create mode 100644 crates/transports/src/pubsub/trait.rs delete mode 100644 crates/transports/src/transports/json.rs rename crates/transports/src/transports/{transport.rs => trait.rs} (90%) diff --git a/crates/json-rpc/src/packet.rs b/crates/json-rpc/src/packet.rs index 75c6a2c717d..715ddb9a5da 100644 --- a/crates/json-rpc/src/packet.rs +++ b/crates/json-rpc/src/packet.rs @@ -31,11 +31,11 @@ impl Serialize for RequestPacket { S: serde::Serializer, { match self { - RequestPacket::Single(single) => single.request().serialize(serializer), + RequestPacket::Single(single) => single.serialized().serialize(serializer), RequestPacket::Batch(batch) => { let mut seq = serializer.serialize_seq(Some(batch.len()))?; for req in batch { - seq.serialize_element(req.request())?; + seq.serialize_element(req.serialized())?; } seq.end() } @@ -111,6 +111,36 @@ pub enum ResponsePacket, ErrData = Box> { Batch(Vec>), } +impl FromIterator> + for ResponsePacket +{ + fn from_iter>>(iter: T) -> Self { + let mut iter = iter.into_iter().peekable(); + // return single if iter has exactly one element, else make a batch + if let Some(first) = iter.next() { + if iter.peek().is_none() { + return Self::Single(first); + } else { + let mut batch = Vec::new(); + batch.push(first); + batch.extend(iter); + return Self::Batch(batch); + } + } + Self::Batch(vec![]) + } +} + +impl From>> for ResponsePacket { + fn from(value: Vec>) -> Self { + if value.len() == 1 { + Self::Single(value.into_iter().next().unwrap()) + } else { + Self::Batch(value) + } + } +} + use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; use std::fmt; use std::marker::PhantomData; diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index 5616e0254b8..13b03cde3f3 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -1,5 +1,6 @@ use crate::{common::Id, RpcParam}; +use alloy_primitives::{keccak256, B256}; use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize}; use serde_json::value::RawValue; @@ -150,7 +151,45 @@ impl SerializedRequest { self.meta.method } /// Get the serialized request. - pub fn request(&self) -> &RawValue { + pub fn serialized(&self) -> &RawValue { &self.request } + + /// Consumes the serialized request, returning the underlying [`RawValue`] + /// and the [`RequestMeta`]. + pub fn decompose(self) -> (RequestMeta, Box) { + (self.meta, self.request) + } + + /// Take the serialized request, consuming the [`SerializedRequest`]. + pub fn take_request(self) -> Box { + self.request + } + + /// Get a reference to the serialized request's params. + /// + /// This partially deserializes the request, and should be avoided if + /// possible. + pub fn params<'a>(&'a self) -> Option<&'a RawValue> { + #[derive(Deserialize)] + struct Req<'a> { + #[serde(borrow)] + params: Option<&'a RawValue>, + } + + let req: Req = serde_json::from_str(self.request.get()).unwrap(); + req.params + } + + /// Get the hash of the serialized request's params. + /// + /// This partially deserializes the request, and should be avoided if + /// possible. + pub fn params_hash(&self) -> B256 { + if let Some(params) = self.params() { + keccak256(params.get()) + } else { + keccak256("") + } + } } diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index d7e971cd24b..6583b250e1e 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -454,8 +454,8 @@ impl<'a> TryFrom<&'a String> for Provider> { #[cfg(test)] mod providers_test { use crate::{provider::Provider, utils}; - use alloy_primitives::{address, b256, Address, U256, U64}; - use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter}; + use alloy_primitives::{address, b256, U256, U64}; + use alloy_rpc_types::{BlockNumberOrTag, Filter}; use ethers_core::utils::Anvil; diff --git a/crates/transports/Cargo.toml b/crates/transports/Cargo.toml index 05221f2071b..35ad7035c59 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 } -tokio = { version = "1.33.0", features = ["sync"] } +tokio = { version = "1.33.0", features = ["sync", "macros"] } +alloy-primitives.workspace = true +bimap = "0.6.3" +tracing = "0.1.40" +futures = "0.3.29" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.hyper] version = "0.14.27" @@ -41,6 +45,10 @@ features = ["sync", "rt"] [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4.37" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio-tungstenite = "0.20.1" + + [features] default = ["reqwest", "hyper"] reqwest = ["dep:reqwest"] diff --git a/crates/transports/src/batch.rs b/crates/transports/src/batch.rs index 676b489ab60..45a237f7194 100644 --- a/crates/transports/src/batch.rs +++ b/crates/transports/src/batch.rs @@ -10,7 +10,7 @@ use std::{ use futures_channel::oneshot; use serde_json::value::RawValue; -use crate::{error::TransportError, transports::Transport, utils::to_json_raw_value, RpcClient}; +use crate::{error::TransportError, transports::Transport, RpcClient}; use alloy_json_rpc::{ Id, Request, RequestPacket, ResponsePacket, RpcParam, RpcResult, RpcReturn, SerializedRequest, }; @@ -185,14 +185,6 @@ where let channels = std::mem::replace(channels, HashMap::with_capacity(0)); let req = std::mem::replace(requests, RequestPacket::Batch(Vec::with_capacity(0))); - let req = match to_json_raw_value(&req) { - Ok(req) => req, - Err(e) => { - self.set(BatchFuture::Complete); - return Poll::Ready(Err(e)); - } - }; - let fut = transport.call(req); self.set(BatchFuture::AwaitingResponse { channels, fut }); cx.waker().wake_by_ref(); @@ -216,14 +208,6 @@ where } }; - let responses: ResponsePacket = match serde_json::from_str(responses.get()) { - Ok(responses) => responses, - Err(err) => { - self.set(BatchFuture::Complete); - return Poll::Ready(Err(TransportError::deser_err(err, responses.get()))); - } - }; - // Send all responses via channels match responses { ResponsePacket::Single(single) => { diff --git a/crates/transports/src/call.rs b/crates/transports/src/call.rs index fac74f6bc63..eec5245f076 100644 --- a/crates/transports/src/call.rs +++ b/crates/transports/src/call.rs @@ -1,8 +1,4 @@ -use crate::{ - error::TransportError, - transports::{JsonRpcLayer, JsonRpcService, Transport}, - RpcFut, -}; +use crate::{error::TransportError, transports::Transport, RpcFut}; use alloy_json_rpc::{Request, RequestPacket, ResponsePacket, RpcParam, RpcResult, RpcReturn}; use core::panic; @@ -13,7 +9,7 @@ use std::{ pin::Pin, task::{self, Poll::Ready}, }; -use tower::{Layer, Service}; +use tower::Service; /// The states of the [`RpcCall`] future. #[must_use = "futures do nothing unless you `.await` or poll them"] @@ -25,11 +21,11 @@ where { Prepared { request: Option>, - connection: JsonRpcService, + connection: Conn, }, AwaitingResponse { #[pin] - fut: as Service>::Future, + fut: >::Future, }, Complete, } @@ -152,7 +148,7 @@ where Self { state: CallState::Prepared { request: Some(req), - connection: JsonRpcLayer.layer(connection), + connection, }, _pd: PhantomData, } diff --git a/crates/transports/src/client.rs b/crates/transports/src/client.rs index 020efa2ff58..017ea9b9302 100644 --- a/crates/transports/src/client.rs +++ b/crates/transports/src/client.rs @@ -234,7 +234,7 @@ impl ClientBuilder { L: Layer, L::Service: Transport, { - let transport = connect.to_transport()?; + let transport = connect.get_transport()?; Ok(self.transport(transport, connect.is_local())) } @@ -253,7 +253,7 @@ impl ClientBuilder { #[cfg(all(test, feature = "reqwest"))] mod test { - use crate::transports::Http; + use crate::{pubsub::PubSubFrontend, transports::Http, BoxPubSub}; use super::RpcClient; @@ -263,4 +263,12 @@ mod test { assert!(h.is_local()); } + + fn __compile_check_a() -> RpcClient { + todo!() + } + + fn __compile_check_2() { + let _ = __compile_check_a().new_batch(); + } } diff --git a/crates/transports/src/error.rs b/crates/transports/src/error.rs index d0b119f87eb..cb4a4d52bc7 100644 --- a/crates/transports/src/error.rs +++ b/crates/transports/src/error.rs @@ -18,16 +18,25 @@ pub enum TransportError { #[error("Missing response in batch request")] MissingBatchResponse, - /// Hyper http transport + /// Reqwest http transport. #[error(transparent)] #[cfg(feature = "reqwest")] Reqwest(#[from] reqwest::Error), - /// Hyper http transport + /// Hyper http transport. #[error(transparent)] #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] Hyper(#[from] hyper::Error), + /// Tungstenite websocket transport. + #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] + Tungstenite(#[from] tokio_tungstenite::tungstenite::Error), + + /// PubSub backend connection task has stopped. + #[error("PubSub backend connection task has stopped.")] + BackendGone, + /// Custom error #[error(transparent)] Custom(Box), diff --git a/crates/transports/src/lib.rs b/crates/transports/src/lib.rs index 5f540b746b6..918b476141f 100644 --- a/crates/transports/src/lib.rs +++ b/crates/transports/src/lib.rs @@ -13,6 +13,9 @@ pub use client::{ClientBuilder, RpcClient}; mod error; pub use error::TransportError; +mod pubsub; +pub use pubsub::{BoxPubSub, ConnectionHandle, ConnectionInterface, PubSub}; + mod transports; pub use transports::{BoxTransport, BoxTransportConnect, Http, Transport, TransportConnect}; @@ -24,13 +27,13 @@ pub use type_aliases::*; #[cfg(not(target_arch = "wasm32"))] mod type_aliases { - use alloy_json_rpc::RpcResult; + use alloy_json_rpc::{ResponsePacket, RpcResult}; use serde_json::value::RawValue; use crate::TransportError; /// Future for Transport-level requests. - pub type TransportFut<'a, T = Box, E = TransportError> = + pub type TransportFut<'a, T = ResponsePacket, E = TransportError> = std::pin::Pin> + Send + 'a>>; /// Future for RPC-level requests. @@ -41,13 +44,13 @@ mod type_aliases { #[cfg(target_arch = "wasm32")] mod type_aliases { - use alloy_json_rpc::RpcResult; + use alloy_json_rpc::{ResponsePacket, RpcResult}; use serde_json::value::RawValue; use crate::TransportError; /// Future for Transport-level requests. - pub type TransportFut<'a, T = Box, E = TransportError> = + pub type TransportFut<'a, T = ResponsePacket, E = TransportError> = std::pin::Pin> + 'a>>; /// Future for RPC-level requests. diff --git a/crates/transports/src/pubsub/frontend.rs b/crates/transports/src/pubsub/frontend.rs new file mode 100644 index 00000000000..06e5ff1c55b --- /dev/null +++ b/crates/transports/src/pubsub/frontend.rs @@ -0,0 +1,123 @@ +use std::{future::Future, pin::Pin}; + +use alloy_json_rpc::{RequestPacket, Response, ResponsePacket, SerializedRequest}; +use alloy_primitives::U256; +use futures::future::try_join_all; +use serde_json::value::RawValue; +use tokio::sync::{broadcast, mpsc, oneshot}; + +use crate::{ + pubsub::{ix::PubSubInstruction, managers::InFlight}, + TransportError, +}; + +#[derive(Debug, Clone)] +pub struct PubSubFrontend { + tx: mpsc::UnboundedSender, +} + +impl PubSubFrontend { + /// Create a new frontend. + pub(crate) fn new(tx: mpsc::UnboundedSender) -> Self { + Self { tx } + } + + /// Get the subscription ID for a local ID. + pub async fn get_subscription( + &self, + id: U256, + ) -> Result>, TransportError> { + let (tx, rx) = oneshot::channel(); + self.tx + .send(PubSubInstruction::GetSub(id, tx)) + .map_err(|_| TransportError::BackendGone)?; + rx.await.map_err(|_| TransportError::BackendGone) + } + + /// Unsubscribe from a subscription. + pub async fn unsubscribe(&self, id: U256) -> Result<(), TransportError> { + self.tx + .send(PubSubInstruction::Unsubscribe(id)) + .map_err(|_| TransportError::BackendGone)?; + Ok(()) + } + + /// Send a request. + pub fn send( + &self, + req: SerializedRequest, + ) -> Pin> + Send>> { + let (in_flight, rx) = InFlight::new(req); + let ix = PubSubInstruction::Request(in_flight); + let tx = self.tx.clone(); + + Box::pin(async move { + tx.send(ix).map_err(|_| TransportError::BackendGone)?; + rx.await.map_err(|_| TransportError::BackendGone)? + }) + } + + /// Send a packet of requests, by breaking it up into individual requests. + /// + /// Once all responses are received, we return a single response packet. + /// This is a bit annoying + pub fn send_packet( + &self, + req: RequestPacket, + ) -> Pin> + Send>> { + match req { + RequestPacket::Single(req) => { + let fut = self.send(req); + Box::pin(async move { Ok(ResponsePacket::Single(fut.await?)) }) + } + RequestPacket::Batch(reqs) => { + let futs = try_join_all(reqs.into_iter().map(|req| self.send(req))); + Box::pin(async move { Ok(futs.await?.into()) }) + } + } + } +} + +impl tower::Service for PubSubFrontend { + type Response = ResponsePacket; + type Error = TransportError; + type Future = Pin> + Send>>; + + #[inline] + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if self.tx.is_closed() { + return std::task::Poll::Ready(Err(TransportError::BackendGone)); + } + std::task::Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: RequestPacket) -> Self::Future { + self.send_packet(req) + } +} + +impl tower::Service for &PubSubFrontend { + type Response = ResponsePacket; + type Error = TransportError; + type Future = Pin> + Send>>; + + #[inline] + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if self.tx.is_closed() { + return std::task::Poll::Ready(Err(TransportError::BackendGone)); + } + std::task::Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: RequestPacket) -> Self::Future { + self.send_packet(req) + } +} diff --git a/crates/transports/src/pubsub/handle.rs b/crates/transports/src/pubsub/handle.rs new file mode 100644 index 00000000000..6daf57b99dc --- /dev/null +++ b/crates/transports/src/pubsub/handle.rs @@ -0,0 +1,67 @@ +use alloy_json_rpc::PubSubItem; +use serde_json::value::RawValue; +use tokio::sync::{mpsc, oneshot}; + +#[derive(Debug)] +/// A handle to a backend. Communicates to a `ConnectionInterface` on the +/// backend. +/// +/// The backend SHOULD shut down when the handle is dropped (as indicated by +/// the shutdown channel). +pub struct ConnectionHandle { + /// Outbound channel to server. + pub(crate) to_socket: mpsc::UnboundedSender>, + + /// Inbound channel from remote server via WS. + pub(crate) from_socket: mpsc::UnboundedReceiver, + + /// Notification from the backend of a terminal error. + pub(crate) error: oneshot::Receiver<()>, + + /// Notify the backend of intentional shutdown. + pub(crate) shutdown: oneshot::Sender<()>, +} + +impl ConnectionHandle { + /// Create a new connection handle. + pub fn new() -> (Self, ConnectionInterface) { + let (to_socket, from_frontend) = mpsc::unbounded_channel(); + let (to_frontend, from_socket) = mpsc::unbounded_channel(); + let (error_tx, error_rx) = oneshot::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let handle = Self { + to_socket, + from_socket, + error: error_rx, + shutdown: shutdown_tx, + }; + let interface = ConnectionInterface { + from_frontend, + to_frontend, + error: error_tx, + shutdown: shutdown_rx, + }; + (handle, interface) + } + + /// Shutdown the backend. + pub fn shutdown(self) { + let _ = self.shutdown.send(()); + } +} + +/// The reciprocal of [`ConnectionHandle`]. +pub struct ConnectionInterface { + /// Inbound channel from frontend. + pub(crate) from_frontend: mpsc::UnboundedReceiver>, + + /// Channel of responses to the frontend + pub(crate) to_frontend: mpsc::UnboundedSender, + + /// Notifies the frontend of a terminal error. + pub(crate) error: oneshot::Sender<()>, + + /// Causes local shutdown when sender is triggered or dropped. + pub(crate) shutdown: oneshot::Receiver<()>, +} diff --git a/crates/transports/src/pubsub/ix.rs b/crates/transports/src/pubsub/ix.rs new file mode 100644 index 00000000000..88b2c96a401 --- /dev/null +++ b/crates/transports/src/pubsub/ix.rs @@ -0,0 +1,25 @@ +use alloy_primitives::U256; +use serde_json::value::RawValue; +use tokio::sync::{broadcast, oneshot}; + +use crate::pubsub::managers::InFlight; + +/// Instructions for the pubsub service. +pub enum PubSubInstruction { + /// Send a request. + Request(InFlight), + /// Get the subscription ID for a local ID. + GetSub(U256, oneshot::Sender>>), + /// Unsubscribe from a subscription. + Unsubscribe(U256), +} + +impl std::fmt::Debug for PubSubInstruction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Request(arg0) => f.debug_tuple("Request").field(arg0).finish(), + Self::GetSub(arg0, _) => f.debug_tuple("GetSub").field(arg0).finish(), + Self::Unsubscribe(arg0) => f.debug_tuple("Unsubscribe").field(arg0).finish(), + } + } +} diff --git a/crates/transports/src/pubsub/managers/active_sub.rs b/crates/transports/src/pubsub/managers/active_sub.rs new file mode 100644 index 00000000000..eb4b7aa2843 --- /dev/null +++ b/crates/transports/src/pubsub/managers/active_sub.rs @@ -0,0 +1,87 @@ +use std::hash::Hash; + +use alloy_json_rpc::SerializedRequest; +use alloy_primitives::B256; +use serde_json::value::RawValue; +use tokio::sync::broadcast; + +#[derive(Clone)] +/// An active subscription. +pub(crate) struct ActiveSubscription { + /// Cached hash of the request, used for sorting and equality. + pub local_id: B256, + /// The serialized subscription request. + pub request: SerializedRequest, + /// The channel via which notifications are broadcast. + pub tx: broadcast::Sender>, +} + +// NB: We implement this to prevent any incorrect future implementations. +// See: https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq +impl Hash for ActiveSubscription { + fn hash(&self, state: &mut H) { + self.local_id.hash(state); + } +} + +impl PartialEq for ActiveSubscription { + fn eq(&self, other: &Self) -> bool { + self.local_id == other.local_id + } +} + +impl Eq for ActiveSubscription {} + +impl PartialOrd for ActiveSubscription { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for ActiveSubscription { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.local_id.cmp(&other.local_id) + } +} + +impl std::fmt::Debug for ActiveSubscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let channel_desc = format!("Channel status: {} subscribers", self.tx.receiver_count()); + + f.debug_struct("ActiveSubscription") + .field("req", &self.request) + .field("tx", &channel_desc) + .finish() + } +} + +impl ActiveSubscription { + /// Create a new active subscription. + pub fn new(request: SerializedRequest) -> (Self, broadcast::Receiver>) { + let local_id = request.params_hash(); + let (tx, rx) = broadcast::channel(16); + ( + Self { + request, + local_id, + tx, + }, + rx, + ) + } + + /// Serialize the request as a boxed [`RawValue`]. + /// + /// This is used to (re-)send the request over the transport. + pub fn request(&self) -> &SerializedRequest { + &self.request + } + + /// Notify the subscription channel of a new value, if any receiver exists. + /// If no receiver exists, the notification is dropped. + pub fn notify(&mut self, notification: Box) { + if self.tx.receiver_count() > 0 { + let _ = self.tx.send(notification); + } + } +} diff --git a/crates/transports/src/pubsub/managers/in_flight.rs b/crates/transports/src/pubsub/managers/in_flight.rs new file mode 100644 index 00000000000..98ee431e60b --- /dev/null +++ b/crates/transports/src/pubsub/managers/in_flight.rs @@ -0,0 +1,75 @@ +use alloy_json_rpc::{Response, ResponsePayload, SerializedRequest}; +use alloy_primitives::U256; +use tokio::sync::oneshot; + +use crate::TransportError; + +/// An in-flight JSON-RPC request. +/// +/// This struct contains the request that was sent, as well as a channel to +/// receive the response on. +pub struct InFlight { + /// The request + pub request: SerializedRequest, + + /// The channel to send the response on. + pub tx: oneshot::Sender>, +} + +impl std::fmt::Debug for InFlight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let channel_desc = format!( + "Channel status: {}", + if self.tx.is_closed() { "closed" } else { "ok" } + ); + + f.debug_struct("InFlight") + .field("req", &self.request) + .field("tx", &channel_desc) + .finish() + } +} + +impl InFlight { + /// Create a new in-flight request. + pub fn new( + request: SerializedRequest, + ) -> (Self, oneshot::Receiver>) { + let (tx, rx) = oneshot::channel(); + + (Self { request, tx }, rx) + } + + /// Get the method + pub fn method(&self) -> &'static str { + self.request.method() + } + + /// Get a reference to the serialized request. + /// + /// This is used to (re-)send the request over the transport. + pub fn request(&self) -> &SerializedRequest { + &self.request + } + + /// Fulfill the request with a response. This consumes the in-flight + /// request. If the request is a subscription and the response is not an + /// error, the subscription ID and the in-flight request are returned. + pub fn fulfill(self, resp: Response) -> Option<(U256, Self)> { + if self.method() == "eth_subscribe" { + if let ResponsePayload::Success(val) = resp.payload { + let sub_id: serde_json::Result = serde_json::from_str(val.get()); + match sub_id { + Ok(alias) => return Some((alias, self)), + Err(e) => { + let _ = self.tx.send(Err(TransportError::deser_err(e, val.get()))); + return None; + } + } + } + } + + let _ = self.tx.send(Ok(resp)); + None + } +} diff --git a/crates/transports/src/pubsub/managers/mod.rs b/crates/transports/src/pubsub/managers/mod.rs new file mode 100644 index 00000000000..8c05bf38ac1 --- /dev/null +++ b/crates/transports/src/pubsub/managers/mod.rs @@ -0,0 +1,11 @@ +mod active_sub; +pub(crate) use active_sub::ActiveSubscription; + +mod in_flight; +pub(crate) use in_flight::InFlight; + +mod req; +pub(crate) use req::RequestManager; + +mod sub; +pub(crate) use sub::SubscriptionManager; diff --git a/crates/transports/src/pubsub/managers/req.rs b/crates/transports/src/pubsub/managers/req.rs new file mode 100644 index 00000000000..e39fba86f2f --- /dev/null +++ b/crates/transports/src/pubsub/managers/req.rs @@ -0,0 +1,40 @@ +use alloy_json_rpc::{Id, Response}; +use alloy_primitives::U256; +use std::collections::BTreeMap; + +use crate::pubsub::managers::InFlight; + +/// Manages in-flight requests. +#[derive(Debug, Default)] +pub struct RequestManager { + reqs: BTreeMap, +} + +impl RequestManager { + /// Get the number of in-flight requests. + pub fn len(&self) -> usize { + self.reqs.len() + } + + /// Get an iterator over the in-flight requests. + pub fn iter(&self) -> impl Iterator { + self.reqs.iter() + } + + /// Insert a new in-flight request. + pub fn insert(&mut self, in_flight: InFlight) { + self.reqs.insert(in_flight.request.id().clone(), in_flight); + } + + /// Handle a response by sending the payload to the waiter. + /// + /// If the request created a new subscription, this function returns the + /// subscription ID and the in-flight request for conversion to an + /// `ActiveSubscription`. + pub fn handle_response(&mut self, resp: Response) -> Option<(U256, InFlight)> { + if let Some(in_flight) = self.reqs.remove(&resp.id) { + return in_flight.fulfill(resp); + } + None + } +} diff --git a/crates/transports/src/pubsub/managers/sub.rs b/crates/transports/src/pubsub/managers/sub.rs new file mode 100644 index 00000000000..62ee79ca97a --- /dev/null +++ b/crates/transports/src/pubsub/managers/sub.rs @@ -0,0 +1,97 @@ +use alloy_json_rpc::{EthNotification, SerializedRequest}; +use alloy_primitives::{B256, U256}; +use bimap::BiBTreeMap; +use serde_json::value::RawValue; +use tokio::sync::broadcast; + +use crate::pubsub::managers::ActiveSubscription; + +#[derive(Default, Debug)] +pub(crate) struct SubscriptionManager { + /// The subscriptions. + local_to_sub: BiBTreeMap, + /// Tracks the CURRENT server id for a subscription. + local_to_server: BiBTreeMap, +} + +impl SubscriptionManager { + /// Get an iterator over the subscriptions. + pub fn iter(&self) -> impl Iterator { + self.local_to_sub.iter() + } + + /// Get the number of subscriptions. + pub fn len(&self) -> usize { + self.local_to_sub.len() + } + + /// Insert a subscription. + fn insert( + &mut self, + request: SerializedRequest, + server_id: U256, + ) -> broadcast::Receiver> { + let (sub, rx) = ActiveSubscription::new(request); + self.local_to_server.insert(sub.local_id, server_id); + self.local_to_sub.insert(sub.local_id, sub); + + rx + } + + /// Insert or update the server_id for a subscription. + pub fn upsert( + &mut self, + request: SerializedRequest, + server_id: U256, + ) -> broadcast::Receiver> { + let local_id = request.params_hash(); + + // If we already know a subscription with the exact params, + // we can just update the server_id and get a new listener. + if self.local_to_server.contains_left(&local_id) { + self.change_server_id(local_id, server_id); + self.get_rx(local_id).expect("checked existence") + } else { + self.insert(request, server_id) + } + } + + /// De-alias an alias, getting the original ID. + pub fn local_id_for(&self, server_id: U256) -> Option { + self.local_to_server.get_by_right(&server_id).copied() + } + + /// Drop all server_ids. + pub fn drop_server_ids(&mut self) { + self.local_to_server.clear(); + } + + /// Change the server_id of a subscription. + fn change_server_id(&mut self, local_id: B256, server_id: U256) { + self.local_to_server.insert(local_id, server_id); + } + + /// Remove a subscription by its local_id. + pub fn remove_sub(&mut self, local_id: B256) { + let _ = self.local_to_sub.remove_by_left(&local_id); + let _ = self.local_to_server.remove_by_left(&local_id); + } + + /// Notify the subscription channel of a new value, if the sub is known, + /// and if any receiver exists. If the sub id is unknown, or no receiver + /// exists, the notification is dropped. + pub fn notify(&mut self, notification: EthNotification) { + if let Some(local_id) = self.local_id_for(notification.subscription) { + if let Some((_, mut sub)) = self.local_to_sub.remove_by_left(&local_id) { + sub.notify(notification.result); + self.local_to_sub.insert(local_id, sub); + } + } + } + + /// Get a receiver for a subscription. + pub fn get_rx(&self, local_id: B256) -> Option>> { + let sub = self.local_to_sub.get_by_left(&local_id)?; + Some(sub.tx.subscribe()) + } +} diff --git a/crates/transports/src/pubsub/mod.rs b/crates/transports/src/pubsub/mod.rs new file mode 100644 index 00000000000..f875fba3a52 --- /dev/null +++ b/crates/transports/src/pubsub/mod.rs @@ -0,0 +1,50 @@ +//! PubSub services. +//! +//! # Overview +//! +//! PubSub services, unlike regular RPC services, are long-lived and +//! bidirectional. They are used to subscribe to events on the server, and +//! receive notifications when those events occur. +//! +//! The PubSub system here consists of 3 logical parts: +//! - The **frontend** is the part of the system that the user interacts with. +//! It exposes a simple API that allows the user to issue requests and manage +//! subscriptions. +//! - The **service** is an intermediate layer that manages request/response +//! mappings, subscription aliasing, and backend lifecycle events. Running +//! [`PubSubConnect::into_service`] will spawn a long-lived service task. +//! - The **backend** is an actively running connection to the server. Users +//! should NEVER instantiate a backend directly. Instead, they should use +//! [`PubSubConnect::into_service`] for some connection object. +//! +//! This module provides the following: +//! +//! - [PubSubConnect]: A trait for instantiating a PubSub service by connecting +//! to some **backend**. Implementors of this trait are responsible for +//! the precise connection details, and for spawning the **backend** task. +//! Users should ALWAYS call [`PubSubConnect::into_service`] to get a running +//! service with a running backend. +//! - [ConnectionHandle]: A handle to a running **backend**. This type is +//! returned by [PubSubConnect::connect], and owned by the **service**. +//! Dropping the handle will shut down the **backend**. +//! - [ConnectionInterface]: The reciprocal of [ConnectionHandle]. This type is +//! owned by the **backend**, and is used to communicate with the **service**. +//! Dropping the interface will notify the **service** of a terminal error. +//! - [PubSubFrontend]: The **frontend**. A handle to a running PubSub +//! **service**. It is used to issue requests and subscription lifecycle +//! instructions to the **service**. + +mod frontend; +pub use frontend::PubSubFrontend; + +mod ix; + +mod handle; + +mod managers; + +mod service; +pub use handle::{ConnectionHandle, ConnectionInterface}; + +mod r#trait; +pub use r#trait::{BoxPubSub, PubSub}; diff --git a/crates/transports/src/pubsub/service.rs b/crates/transports/src/pubsub/service.rs new file mode 100644 index 00000000000..dfcc88ef875 --- /dev/null +++ b/crates/transports/src/pubsub/service.rs @@ -0,0 +1,245 @@ +use alloy_json_rpc::{Id, PubSubItem, Request, RequestMeta, Response, ResponsePayload}; +use alloy_primitives::U256; +use serde_json::value::RawValue; +use tokio::{ + sync::{broadcast, oneshot}, + task::JoinHandle, +}; + +use crate::{ + pubsub::{ + handle::ConnectionHandle, + ix::PubSubInstruction, + managers::{InFlight, RequestManager, SubscriptionManager}, + }, + utils::to_json_raw_value, + TransportConnect, TransportError, +}; + +#[derive(Debug)] +/// The service contains the backend handle, a subscription manager, and the +/// configuration details required to reconnect. +pub struct PubSubService { + /// The backend handle. + pub(crate) handle: ConnectionHandle, + + /// The configuration details required to reconnect. + pub(crate) connector: T, + + /// The inbound requests. + pub(crate) reqs: tokio::sync::mpsc::UnboundedReceiver, + + /// The subscription manager. + pub(crate) subs: SubscriptionManager, + + /// The request manager. + pub(crate) in_flights: RequestManager, +} + +impl PubSubService +where + T: TransportConnect, +{ + /// Reconnect by dropping the backend and creating a new one. + async fn get_new_backend(&mut self) -> Result { + let mut handle = self.connector.get_transport()?; + std::mem::swap(&mut self.handle, &mut handle); + Ok(handle) + } + + /// Reconnect the backend, re-issue pending requests, and re-start active + /// subscriptions. + pub async fn reconnect(&mut self) -> Result<(), TransportError> { + tracing::info!("Reconnecting pubsub service backend."); + + let mut old_handle = self + .get_new_backend() + .await + .map_err(TransportError::custom)?; + + tracing::debug!("Draining old backend to_handle"); + + // Drain the old backend + while let Ok(item) = old_handle.from_socket.try_recv() { + self.handle_item(item)?; + } + + old_handle.shutdown(); + + // Re-issue pending requests. + tracing::debug!(count = self.in_flights.len(), "Reissuing pending requests"); + self.in_flights + .iter() + .map(|(_, in_flight)| in_flight.request().serialized().to_owned()) + .collect::>() + .into_iter() + .try_for_each(|brv| self.dispatch_request(brv))?; + + // Re-subscribe to all active subscriptions + tracing::debug!(count = self.subs.len(), "Re-starting active subscriptions"); + + // Drop all server IDs. We'll re-insert them as we get responses. + self.subs.drop_server_ids(); + // Dispatch all subscription requests + self.subs + .iter() + .map(|(_, sub)| sub.request().serialized().to_owned()) + .collect::>() + .into_iter() + .try_for_each(|brv| self.dispatch_request(brv))?; + + Ok(()) + } + + /// Dispatch a request to the socket. + fn dispatch_request(&mut self, brv: Box) -> Result<(), TransportError> { + self.handle + .to_socket + .send(brv) + .map(drop) + .map_err(|_| TransportError::BackendGone) + } + + /// Service a request. + fn service_request(&mut self, in_flight: InFlight) -> Result<(), TransportError> { + let brv = in_flight.request(); + + self.dispatch_request(brv.serialized().to_owned())?; + self.in_flights.insert(in_flight); + + Ok(()) + } + + /// Service a GetSub instruction. + /// + /// If the subscription exists, the waiter is sent a broadcast receiver. If + /// the subscription does not exist, the waiter is sent nothing, and the + /// `tx` is dropped. This notifies the waiter that the subscription does + /// not exist. + fn service_get_sub( + &mut self, + local_id: U256, + tx: oneshot::Sender>>, + ) -> Result<(), TransportError> { + let local_id = local_id.into(); + + if let Some(rx) = self.subs.get_rx(local_id) { + let _ = tx.send(rx); + } + + Ok(()) + } + + /// Service an unsubscribe instruction. + fn service_unsubscribe(&mut self, local_id: U256) -> Result<(), TransportError> { + let local_id = local_id.into(); + let req = Request { + meta: RequestMeta { + id: Id::None, + method: "eth_unsubscribe", + }, + params: [local_id], + }; + let brv = req.serialize().expect("no ser error").take_request(); + + self.dispatch_request(brv)?; + self.subs.remove_sub(local_id); + Ok(()) + } + + /// Service an instruction + fn service_ix(&mut self, ix: PubSubInstruction) -> Result<(), TransportError> { + tracing::trace!(?ix, "servicing instruction"); + match ix { + PubSubInstruction::Request(in_flight) => self.service_request(in_flight), + PubSubInstruction::GetSub(alias, tx) => self.service_get_sub(alias, tx), + PubSubInstruction::Unsubscribe(alias) => self.service_unsubscribe(alias), + } + } + + /// Handle an item from the backend. + fn handle_item(&mut self, item: PubSubItem) -> Result<(), TransportError> { + match item { + PubSubItem::Response(resp) => match self.in_flights.handle_response(resp) { + Some((server_id, in_flight)) => self.handle_sub_response(in_flight, server_id), + None => Ok(()), + }, + PubSubItem::Notification(notification) => { + self.subs.notify(notification); + Ok(()) + } + } + } + + /// Rewrite the subscription id and insert into the subscriptions manager + fn handle_sub_response( + &mut self, + in_flight: InFlight, + server_id: U256, + ) -> Result<(), TransportError> { + let request = in_flight.request; + let id = request.id().clone(); + + self.subs.upsert(request, server_id); + + // lie to the client about the sub id. + let local_id = self.subs.local_id_for(server_id).unwrap(); + // Serialized B256 is always a valid serialized U256 too. + let ser_alias = to_json_raw_value(&local_id)?; + + // We send back a success response with the new subscription ID. + // We don't care if the channel is dead. + let _ = in_flight.tx.send(Ok(Response { + id, + payload: ResponsePayload::Success(ser_alias), + })); + + Ok(()) + } + + /// Spawn the service. + pub fn spawn(mut self) -> JoinHandle<()> { + tokio::spawn(async move { + let result: Result<(), TransportError> = loop { + // We bias the loop so that we always handle new messages before + // reconnecting, and always reconnect before dispatching new + // requests. + tokio::select! { + biased; + + item_opt = self.handle.from_socket.recv() => { + if let Some(item) = item_opt { + if let Err(e) = self.handle_item(item) { + break Err(e) + } + } else if let Err(e) = self.reconnect().await { + break Err(TransportError::Custom(Box::new(e))) + } + } + + _ = &mut self.handle.error => { + tracing::error!("Pubsub service backend error."); + if let Err(e) = self.reconnect().await { + break Err(TransportError::Custom(Box::new(e))) + } + } + + req_opt = self.reqs.recv() => { + if let Some(req) = req_opt { + if let Err(e) = self.service_ix(req) { + break Err(e) + } + } else { + tracing::info!("Pubsub service request channel closed. Shutting down."); + break Ok(()) + } + } + } + }; + + if let Err(err) = result { + tracing::error!(%err, "pubsub service reconnection error"); + } + }) + } +} diff --git a/crates/transports/src/pubsub/trait.rs b/crates/transports/src/pubsub/trait.rs new file mode 100644 index 00000000000..b9ba911ab92 --- /dev/null +++ b/crates/transports/src/pubsub/trait.rs @@ -0,0 +1,101 @@ +use crate::{Transport, TransportError, TransportFut}; + +use alloy_json_rpc::{Id, RequestPacket, Response, ResponsePacket}; +use alloy_primitives::U256; +use serde_json::value::RawValue; +use tokio::sync::broadcast; +use tower::Service; + +/// A trait for transports supporting notifications. +/// +/// This trait models notifications bodies as a stream of [`RawValue`]s. It is +/// up to the recipient to deserialize the notification. +pub trait PubSub: Transport { + /// Reserve an ID for a subscription, based on the JSON-RPC request ID of + /// the subscription request. + /// + /// This is intended for internal use by RPC clients, and should not be + /// called directly. + fn reserve_id(&self, id: &Id) -> U256; + + /// Get a [`broadcast::Receiver`] for the given subscription ID. + fn get_watcher(&self, id: U256) -> broadcast::Receiver>; +} + +/// Helper trait for constructing [`BoxPubSub`]. +trait ClonePubSub: PubSub { + fn clone_box(&self) -> Box; +} + +impl ClonePubSub for T +where + T: PubSub + Clone + Send + Sync, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// A boxed, Clone-able [`PubSub`] trait object. +/// +/// This type allows [`RpcClient`] to use a type-erased transport. It is +/// [`Clone`] and [`Send`] + [`Sync`], and implementes [`PubSub`]. This +/// allows for complex behavior abstracting across several different clients +/// with different transport types. +/// +/// [`RpcClient`]: crate::client::RpcClient +#[repr(transparent)] +pub struct BoxPubSub { + inner: Box, +} + +impl std::fmt::Debug for BoxPubSub { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BoxPubSub").finish() + } +} + +impl Clone for BoxPubSub { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone_box(), + } + } +} + +impl PubSub for BoxPubSub { + fn reserve_id(&self, id: &Id) -> U256 { + self.inner.reserve_id(id) + } + + fn get_watcher(&self, id: U256) -> broadcast::Receiver> { + self.inner.get_watcher(id) + } +} + +impl Service for BoxPubSub { + type Response = ResponsePacket; + + type Error = TransportError; + + type Future = TransportFut<'static>; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Service::::poll_ready(&mut self.inner, cx) + } + + fn call(&mut self, req: RequestPacket) -> Self::Future { + Service::::call(&mut self.inner, req) + } +} + +/// checks trait + send + sync + 'static +fn __compile_check() { + fn inner() { + unimplemented!() + } + inner::(); +} diff --git a/crates/transports/src/transports/boxed.rs b/crates/transports/src/transports/boxed.rs index 99bd99ead0c..b0c7ad65617 100644 --- a/crates/transports/src/transports/boxed.rs +++ b/crates/transports/src/transports/boxed.rs @@ -1,4 +1,4 @@ -use serde_json::value::RawValue; +use alloy_json_rpc::{RequestPacket, ResponsePacket}; use std::fmt::Debug; use tower::Service; @@ -61,8 +61,8 @@ where } } -impl Service> for BoxTransport { - type Response = Box; +impl Service for BoxTransport { + type Response = ResponsePacket; type Error = TransportError; @@ -75,7 +75,7 @@ impl Service> for BoxTransport { self.inner.poll_ready(cx) } - fn call(&mut self, req: Box) -> Self::Future { + fn call(&mut self, req: RequestPacket) -> Self::Future { self.inner.call(req) } } diff --git a/crates/transports/src/transports/connect.rs b/crates/transports/src/transports/connect.rs index 89f19263150..b6f4e8bb1d7 100644 --- a/crates/transports/src/transports/connect.rs +++ b/crates/transports/src/transports/connect.rs @@ -13,21 +13,19 @@ use crate::{BoxTransport, RpcClient, Transport, TransportError}; /// provider. /// - You have implemented a custom [`Transport`]. /// - You require a specific websocket reconnection strategy. -pub trait TransportConnect { +pub trait TransportConnect: Sized + Send + Sync + 'static { /// The transport type that is returned by `connect`. type Transport: Transport + Clone; /// Returns `true`` if the transport is a local transport. - fn is_local(&self) -> bool { - false - } + fn is_local(&self) -> bool; /// Connect to the transport, returning a `Transport` instance. - fn to_transport(&self) -> Result; + fn get_transport(&self) -> Result; /// Connect to the transport, wrapping it into a `RpcClient` instance. fn connect(&self) -> Result, TransportError> { - self.to_transport() + self.get_transport() .map(|t| RpcClient::new(t, self.is_local())) } @@ -37,7 +35,7 @@ pub trait TransportConnect { /// will be used by PubSub connection managers in the event the connection /// fails. fn try_reconnect(&self) -> Result { - self.to_transport() + self.get_transport() } } @@ -71,7 +69,7 @@ where } fn to_boxed_transport(&self) -> Result { - self.to_transport().map(Transport::boxed) + self.get_transport().map(Transport::boxed) } fn connect_boxed(&self) -> Result, TransportError> { diff --git a/crates/transports/src/transports/http/hyper.rs b/crates/transports/src/transports/http/hyper.rs index 72032801eda..8f3f2528686 100644 --- a/crates/transports/src/transports/http/hyper.rs +++ b/crates/transports/src/transports/http/hyper.rs @@ -1,5 +1,5 @@ +use alloy_json_rpc::{RequestPacket, ResponsePacket}; use hyper::client::{connect::Connect, Client}; -use serde_json::value::RawValue; use std::task; use tower::Service; @@ -10,15 +10,17 @@ where C: Connect + Clone + Send + Sync + 'static, { /// Make a request. - fn request(&self, req: Box) -> TransportFut<'static> { + fn request(&self, req: RequestPacket) -> TransportFut<'static> { let this = self.clone(); Box::pin(async move { + let ser = req.serialize().map_err(TransportError::ser_err)?; + // convert the Box into a hyper request let req = hyper::Request::builder() .method(hyper::Method::POST) .uri(this.url.as_str()) .header("content-type", "application/json") - .body(hyper::Body::from(req.get().to_owned())) + .body(hyper::Body::from(ser.get().to_owned())) .expect("request parts are valid"); let resp = this.client.request(req).await?; @@ -36,11 +38,11 @@ where } } -impl Service> for &Http> +impl Service for &Http> where C: Connect + Clone + Send + Sync + 'static, { - type Response = Box; + type Response = ResponsePacket; type Error = TransportError; type Future = TransportFut<'static>; @@ -51,16 +53,16 @@ where } #[inline] - fn call(&mut self, req: Box) -> Self::Future { + fn call(&mut self, req: RequestPacket) -> Self::Future { self.request(req) } } -impl Service> for Http> +impl Service for Http> where C: Connect + Clone + Send + Sync + 'static, { - type Response = Box; + type Response = ResponsePacket; type Error = TransportError; type Future = TransportFut<'static>; @@ -71,7 +73,7 @@ where } #[inline] - fn call(&mut self, req: Box) -> Self::Future { + fn call(&mut self, req: RequestPacket) -> Self::Future { self.request(req) } } diff --git a/crates/transports/src/transports/http/reqwest.rs b/crates/transports/src/transports/http/reqwest.rs index 49286c4627e..07d534c15c1 100644 --- a/crates/transports/src/transports/http/reqwest.rs +++ b/crates/transports/src/transports/http/reqwest.rs @@ -1,4 +1,4 @@ -use serde_json::value::RawValue; +use alloy_json_rpc::{RequestPacket, ResponsePacket}; use std::task; use tower::Service; @@ -6,19 +6,19 @@ use crate::{Http, TransportError, TransportFut}; impl Http { /// Make a request. - fn request(&self, req: Box) -> TransportFut<'static> { + fn request(&self, req: RequestPacket) -> TransportFut<'static> { let this = self.clone(); Box::pin(async move { let resp = this.client.post(this.url).json(&req).send().await?; let json = resp.text().await?; - RawValue::from_string(json).map_err(|err| TransportError::deser_err(err, "")) + serde_json::from_str(&json).map_err(|err| TransportError::deser_err(err, &json)) }) } } -impl Service> for Http { - type Response = Box; +impl Service for Http { + type Response = ResponsePacket; type Error = TransportError; type Future = TransportFut<'static>; @@ -29,13 +29,13 @@ impl Service> for Http { } #[inline] - fn call(&mut self, req: Box) -> Self::Future { + fn call(&mut self, req: RequestPacket) -> Self::Future { self.request(req) } } -impl Service> for &Http { - type Response = Box; +impl Service for &Http { + type Response = ResponsePacket; type Error = TransportError; type Future = TransportFut<'static>; @@ -46,7 +46,7 @@ impl Service> for &Http { } #[inline] - fn call(&mut self, req: Box) -> Self::Future { + fn call(&mut self, req: RequestPacket) -> Self::Future { self.request(req) } } diff --git a/crates/transports/src/transports/json.rs b/crates/transports/src/transports/json.rs deleted file mode 100644 index 7168344b479..00000000000 --- a/crates/transports/src/transports/json.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::{utils::to_json_raw_value, Transport, TransportError}; - -use alloy_json_rpc::{RequestPacket, ResponsePacket}; -use serde::de::DeserializeOwned; -use serde_json::value::RawValue; -use std::{future::Future, pin::Pin, task}; -use tower::Service; - -/// A service layer that transforms [`RequestPacket`] into [`ResponsePacket`] -/// by wrapping an inner service that implements [`Transport`]. -#[derive(Debug, Clone)] -#[repr(transparent)] -pub(crate) struct JsonRpcService { - pub(crate) inner: S, -} - -impl From for JsonRpcService { - fn from(inner: S) -> Self { - JsonRpcService { inner } - } -} - -/// Layer for [`JsonRpcService`] -#[derive(Debug, Copy, Clone)] -pub(crate) struct JsonRpcLayer; - -impl tower::Layer for JsonRpcLayer { - type Service = JsonRpcService; - - fn layer(&self, inner: S) -> Self::Service { - JsonRpcService { inner } - } -} - -impl Service for JsonRpcService -where - S: Transport + Clone, -{ - type Response = ResponsePacket; - - type Error = TransportError; - - type Future = JsonRpcFuture; - - fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: RequestPacket) -> Self::Future { - let replacement = self.inner.clone(); - let mut client = std::mem::replace(&mut self.inner, replacement); - match to_json_raw_value(&req) { - Ok(raw) => JsonRpcFuture { - state: States::Pending { - fut: client.call(raw), - }, - _resp: std::marker::PhantomData, - }, - Err(e) => JsonRpcFuture { - state: States::Errored(Some(e)), - _resp: std::marker::PhantomData, - }, - } - } -} - -/// States for [`JsonRpcFuture`] -#[must_use = "futures do nothing unless you `.await` or poll them"] -#[pin_project::pin_project(project = StatesProj)] -enum States { - Errored(Option), - Pending { - #[pin] - fut: F, - }, - Complete, -} - -impl States -where - F: Future, TransportError>>, -{ - pub fn poll_errored(mut self: Pin<&mut Self>) -> task::Poll<::Output> { - let e = if let StatesProj::Errored(e) = self.as_mut().project() { - e.take().expect("No error. This is a bug.") - } else { - unreachable!("Called poll_ser_error in incorrect state") - }; - - self.set(States::Complete); - task::Poll::Ready(Err(e)) - } - - pub fn poll_pending( - mut self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll<::Output> { - let StatesProj::Pending { fut } = self.as_mut().project() else { - unreachable!("Called poll_pending in incorrect state") - }; - - fut.poll(cx) - } -} - -impl Future for States -where - F: Future, TransportError>>, -{ - type Output = F::Output; - - fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { - match self.as_mut().project() { - StatesProj::Errored(_) => self.poll_errored(), - StatesProj::Pending { .. } => self.poll_pending(cx), - StatesProj::Complete => panic!("polled after completion"), - } - } -} - -/// Wrapper future to do JSON ser and deser -#[pin_project::pin_project] -pub struct JsonRpcFuture { - #[pin] - state: States, - _resp: std::marker::PhantomData Resp>, -} - -impl Future for JsonRpcFuture -where - F: Future, TransportError>>, - Resp: DeserializeOwned, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { - let resp = task::ready!(self.project().state.poll(cx)); - - task::Poll::Ready(resp.and_then(|raw| { - serde_json::from_str(raw.get()).map_err(|err| TransportError::deser_err(err, raw.get())) - })) - } -} diff --git a/crates/transports/src/transports/mod.rs b/crates/transports/src/transports/mod.rs index 1f8a5ba3909..1afe4027c92 100644 --- a/crates/transports/src/transports/mod.rs +++ b/crates/transports/src/transports/mod.rs @@ -7,8 +7,5 @@ pub use connect::{BoxTransportConnect, TransportConnect}; mod http; pub use http::Http; -mod json; -pub(crate) use json::{JsonRpcLayer, JsonRpcService}; - -mod transport; -pub use transport::Transport; +mod r#trait; +pub use r#trait::Transport; diff --git a/crates/transports/src/transports/transport.rs b/crates/transports/src/transports/trait.rs similarity index 90% rename from crates/transports/src/transports/transport.rs rename to crates/transports/src/transports/trait.rs index ff8937dbad4..92e335c4d66 100644 --- a/crates/transports/src/transports/transport.rs +++ b/crates/transports/src/transports/trait.rs @@ -1,4 +1,4 @@ -use serde_json::value::RawValue; +use alloy_json_rpc::{RequestPacket, ResponsePacket}; use tower::Service; use crate::{BoxTransport, TransportError, TransportFut}; @@ -38,8 +38,8 @@ use crate::{BoxTransport, TransportError, TransportFut}; pub trait Transport: private::Sealed + Service< - Box, - Response = Box, + RequestPacket, + Response = ResponsePacket, Error = TransportError, Future = TransportFut<'static>, > + Send @@ -58,8 +58,8 @@ pub trait Transport: impl Transport for T where T: private::Sealed + Service< - Box, - Response = Box, + RequestPacket, + Response = ResponsePacket, Error = TransportError, Future = TransportFut<'static>, > + Send @@ -74,8 +74,8 @@ mod private { pub trait Sealed {} impl Sealed for T where T: Service< - Box, - Response = Box, + RequestPacket, + Response = ResponsePacket, Error = TransportError, Future = TransportFut<'static>, > + Send From cf385c7334d12d5013ca86a406572edad6a4743c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Nov 2023 15:08:05 -0800 Subject: [PATCH 02/30] fix: reconnect in pubsubservice --- crates/transports/src/pubsub/service.rs | 2 +- crates/transports/src/transports/connect.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/transports/src/pubsub/service.rs b/crates/transports/src/pubsub/service.rs index dfcc88ef875..0f212fb5830 100644 --- a/crates/transports/src/pubsub/service.rs +++ b/crates/transports/src/pubsub/service.rs @@ -42,7 +42,7 @@ where { /// Reconnect by dropping the backend and creating a new one. async fn get_new_backend(&mut self) -> Result { - let mut handle = self.connector.get_transport()?; + let mut handle = self.connector.try_reconnect()?; std::mem::swap(&mut self.handle, &mut handle); Ok(handle) } diff --git a/crates/transports/src/transports/connect.rs b/crates/transports/src/transports/connect.rs index b6f4e8bb1d7..c469b5628a9 100644 --- a/crates/transports/src/transports/connect.rs +++ b/crates/transports/src/transports/connect.rs @@ -23,12 +23,6 @@ pub trait TransportConnect: Sized + Send + Sync + 'static { /// Connect to the transport, returning a `Transport` instance. fn get_transport(&self) -> Result; - /// Connect to the transport, wrapping it into a `RpcClient` instance. - fn connect(&self) -> Result, TransportError> { - self.get_transport() - .map(|t| RpcClient::new(t, self.is_local())) - } - /// Attempt to reconnect the transport. /// /// Override this to add custom reconnection logic to your connector. This @@ -37,6 +31,12 @@ pub trait TransportConnect: Sized + Send + Sync + 'static { fn try_reconnect(&self) -> Result { self.get_transport() } + + /// Connect to the transport, wrapping it into a `RpcClient` instance. + fn connect(&self) -> Result, TransportError> { + self.get_transport() + .map(|t| RpcClient::new(t, self.is_local())) + } } /// Connection details for a transport that can be boxed. From 3f926f9fbd18862f3d468fdc080a624868f5371f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Nov 2023 17:42:20 -0800 Subject: [PATCH 03/30] feat: Ws --- crates/json-rpc/src/request.rs | 2 +- crates/transports/Cargo.toml | 4 +- crates/transports/src/client.rs | 16 +- crates/transports/src/lib.rs | 12 +- crates/transports/src/pubsub/connect.rs | 53 ++++++ crates/transports/src/pubsub/frontend.rs | 4 +- .../src/pubsub/managers/active_sub.rs | 2 +- crates/transports/src/pubsub/mod.rs | 16 +- crates/transports/src/pubsub/service.rs | 44 +++-- crates/transports/src/pubsub/trait.rs | 2 +- crates/transports/src/transports/connect.rs | 41 ++-- crates/transports/src/transports/http/mod.rs | 6 +- crates/transports/src/transports/mod.rs | 5 +- crates/transports/src/transports/ws/mod.rs | 47 +++++ crates/transports/src/transports/ws/native.rs | 177 ++++++++++++++++++ crates/transports/src/transports/ws/wasm.rs | 94 ++++++++++ crates/transports/src/utils.rs | 18 ++ crates/transports/tests/http.rs | 17 ++ crates/transports/tests/ws.rs | 24 +++ 19 files changed, 522 insertions(+), 62 deletions(-) create mode 100644 crates/transports/src/pubsub/connect.rs create mode 100644 crates/transports/src/transports/ws/mod.rs create mode 100644 crates/transports/src/transports/ws/native.rs create mode 100644 crates/transports/src/transports/ws/wasm.rs create mode 100644 crates/transports/tests/http.rs create mode 100644 crates/transports/tests/ws.rs diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index 13b03cde3f3..0cda2ee40eb 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -170,7 +170,7 @@ impl SerializedRequest { /// /// This partially deserializes the request, and should be avoided if /// possible. - pub fn params<'a>(&'a self) -> Option<&'a RawValue> { + pub fn params(&self) -> Option<&RawValue> { #[derive(Deserialize)] struct Req<'a> { #[serde(borrow)] diff --git a/crates/transports/Cargo.toml b/crates/transports/Cargo.toml index 35ad7035c59..b0f3afa889a 100644 --- a/crates/transports/Cargo.toml +++ b/crates/transports/Cargo.toml @@ -32,6 +32,7 @@ alloy-primitives.workspace = true bimap = "0.6.3" tracing = "0.1.40" futures = "0.3.29" +http = "0.2.9" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.hyper] version = "0.14.27" @@ -44,9 +45,10 @@ features = ["sync", "rt"] [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4.37" +ws_stream_wasm = "0.7.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio-tungstenite = "0.20.1" +tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"]} [features] diff --git a/crates/transports/src/client.rs b/crates/transports/src/client.rs index 017ea9b9302..269e7b743a9 100644 --- a/crates/transports/src/client.rs +++ b/crates/transports/src/client.rs @@ -54,11 +54,13 @@ impl RpcClient { } /// Connect to a transport via a [`TransportConnect`] implementor. - pub fn connect>(connect: C) -> Result + pub async fn connect>( + connect: C, + ) -> Result where C: Transport, { - connect.connect() + connect.connect().await } /// Build a `JsonRpcRequest` with the given method and params. @@ -228,32 +230,32 @@ impl ClientBuilder { /// Connect a transport, producing an [`RpcClient`] with the provided /// connection. - pub fn connect(self, connect: C) -> Result, TransportError> + pub async fn connect(self, connect: C) -> Result, TransportError> where C: TransportConnect, L: Layer, L::Service: Transport, { - let transport = connect.get_transport()?; + let transport = connect.get_transport().await?; Ok(self.transport(transport, connect.is_local())) } /// Connect a transport, producing an [`RpcClient`] with a [`BoxTransport`] /// connection. - pub fn connect_boxed(self, connect: C) -> Result, TransportError> + pub async fn connect_boxed(self, connect: C) -> Result, TransportError> where C: BoxTransportConnect, L: Layer, L::Service: Transport, { - let transport = connect.to_boxed_transport()?; + let transport = connect.get_boxed_transport().await?; Ok(self.transport(transport, connect.is_local())) } } #[cfg(all(test, feature = "reqwest"))] mod test { - use crate::{pubsub::PubSubFrontend, transports::Http, BoxPubSub}; + use crate::{pubsub::PubSubFrontend, transports::Http}; use super::RpcClient; diff --git a/crates/transports/src/lib.rs b/crates/transports/src/lib.rs index 918b476141f..bca5da42595 100644 --- a/crates/transports/src/lib.rs +++ b/crates/transports/src/lib.rs @@ -14,10 +14,12 @@ mod error; pub use error::TransportError; mod pubsub; -pub use pubsub::{BoxPubSub, ConnectionHandle, ConnectionInterface, PubSub}; +pub use pubsub::{BoxPubSub, PubSub}; mod transports; -pub use transports::{BoxTransport, BoxTransportConnect, Http, Transport, TransportConnect}; +pub use transports::{ + BoxTransport, BoxTransportConnect, Http, Transport, TransportConnect, WsBackend, WsConnect, +}; pub use alloy_json_rpc::RpcResult; @@ -32,6 +34,9 @@ mod type_aliases { use crate::TransportError; + pub type Pbf<'a, T, E> = + std::pin::Pin> + Send + 'a>>; + /// Future for Transport-level requests. pub type TransportFut<'a, T = ResponsePacket, E = TransportError> = std::pin::Pin> + Send + 'a>>; @@ -49,6 +54,9 @@ mod type_aliases { use crate::TransportError; + pub type Pbf<'a, T, E> = + std::pin::Pin> + 'a>>; + /// Future for Transport-level requests. pub type TransportFut<'a, T = ResponsePacket, E = TransportError> = std::pin::Pin> + 'a>>; diff --git a/crates/transports/src/pubsub/connect.rs b/crates/transports/src/pubsub/connect.rs new file mode 100644 index 00000000000..0dfa5cc35d3 --- /dev/null +++ b/crates/transports/src/pubsub/connect.rs @@ -0,0 +1,53 @@ +use crate::{ + pubsub::{handle::ConnectionHandle, service::PubSubService, PubSubFrontend}, + Pbf, TransportConnect, TransportError, +}; + +/// Configuration objects that contain connection details for a backend. +/// +/// Implementers should contain configuration options for the underlying +/// transport. +pub trait PubSubConnect: Sized + Send + Sync + 'static { + /// Returns `true`` if the transport connects to a local resource. + fn is_local(&self) -> bool; + + /// Spawn the backend, returning a handle to it. + /// + /// This function MUST create a long-lived task containing a + /// [`ConnectionInterface`], and return the corresponding handle. + /// + /// [`ConnectionInterface`]: crate::pubsub::ConnectionInterface + fn connect<'a: 'b, 'b>(&'a self) -> Pbf<'b, ConnectionHandle, TransportError>; + + /// Attempt to reconnect the transport. + /// + /// Override this to add custom reconnection logic to your connector. This + /// will be used by [`PubSubService`] connection managers in the event the + /// connection fails. + /// + /// [`PubSubService`]: crate::pubsub::service::PubSubService + fn try_reconnect<'a: 'b, 'b>(&'a self) -> Pbf<'b, ConnectionHandle, TransportError> { + self.connect() + } + + /// Convert the configuration object into a service with a running backend. + fn into_service(self) -> Pbf<'static, PubSubFrontend, TransportError> { + Box::pin(PubSubService::connect(self)) + } +} + +impl TransportConnect for T +where + T: PubSubConnect + Clone, +{ + type Transport = PubSubFrontend; + + fn is_local(&self) -> bool { + PubSubConnect::is_local(self) + } + + fn get_transport<'a: 'b, 'b>(&self) -> Pbf<'b, Self::Transport, TransportError> { + let this = self.clone(); + Box::pin(async move { this.into_service().await }) + } +} diff --git a/crates/transports/src/pubsub/frontend.rs b/crates/transports/src/pubsub/frontend.rs index 06e5ff1c55b..52dfe978188 100644 --- a/crates/transports/src/pubsub/frontend.rs +++ b/crates/transports/src/pubsub/frontend.rs @@ -8,7 +8,7 @@ use tokio::sync::{broadcast, mpsc, oneshot}; use crate::{ pubsub::{ix::PubSubInstruction, managers::InFlight}, - TransportError, + TransportError, TransportFut, }; #[derive(Debug, Clone)] @@ -81,7 +81,7 @@ impl PubSubFrontend { impl tower::Service for PubSubFrontend { type Response = ResponsePacket; type Error = TransportError; - type Future = Pin> + Send>>; + type Future = TransportFut<'static>; #[inline] fn poll_ready( diff --git a/crates/transports/src/pubsub/managers/active_sub.rs b/crates/transports/src/pubsub/managers/active_sub.rs index eb4b7aa2843..67cc45aad5f 100644 --- a/crates/transports/src/pubsub/managers/active_sub.rs +++ b/crates/transports/src/pubsub/managers/active_sub.rs @@ -34,7 +34,7 @@ impl Eq for ActiveSubscription {} impl PartialOrd for ActiveSubscription { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } diff --git a/crates/transports/src/pubsub/mod.rs b/crates/transports/src/pubsub/mod.rs index f875fba3a52..3172b44e3c6 100644 --- a/crates/transports/src/pubsub/mod.rs +++ b/crates/transports/src/pubsub/mod.rs @@ -24,27 +24,31 @@ //! the precise connection details, and for spawning the **backend** task. //! Users should ALWAYS call [`PubSubConnect::into_service`] to get a running //! service with a running backend. -//! - [ConnectionHandle]: A handle to a running **backend**. This type is +//! - [`ConnectionHandle`]: A handle to a running **backend**. This type is //! returned by [PubSubConnect::connect], and owned by the **service**. //! Dropping the handle will shut down the **backend**. -//! - [ConnectionInterface]: The reciprocal of [ConnectionHandle]. This type is -//! owned by the **backend**, and is used to communicate with the **service**. -//! Dropping the interface will notify the **service** of a terminal error. -//! - [PubSubFrontend]: The **frontend**. A handle to a running PubSub +//! - [`ConnectionInterface`]: The reciprocal of [ConnectionHandle]. This type +//! is owned by the **backend**, and is used to communicate with the +//! **service**. Dropping the interface will notify the **service** of a +//! terminal error. +//! - [`PubSubFrontend`]: The **frontend**. A handle to a running PubSub //! **service**. It is used to issue requests and subscription lifecycle //! instructions to the **service**. +mod connect; +pub use connect::PubSubConnect; + mod frontend; pub use frontend::PubSubFrontend; mod ix; mod handle; +pub use handle::{ConnectionHandle, ConnectionInterface}; mod managers; mod service; -pub use handle::{ConnectionHandle, ConnectionInterface}; mod r#trait; pub use r#trait::{BoxPubSub, PubSub}; diff --git a/crates/transports/src/pubsub/service.rs b/crates/transports/src/pubsub/service.rs index 0f212fb5830..02e4a2a6b74 100644 --- a/crates/transports/src/pubsub/service.rs +++ b/crates/transports/src/pubsub/service.rs @@ -1,10 +1,7 @@ use alloy_json_rpc::{Id, PubSubItem, Request, RequestMeta, Response, ResponsePayload}; use alloy_primitives::U256; use serde_json::value::RawValue; -use tokio::{ - sync::{broadcast, oneshot}, - task::JoinHandle, -}; +use tokio::sync::{broadcast, mpsc, oneshot}; use crate::{ pubsub::{ @@ -12,14 +9,16 @@ use crate::{ ix::PubSubInstruction, managers::{InFlight, RequestManager, SubscriptionManager}, }, - utils::to_json_raw_value, - TransportConnect, TransportError, + utils::{to_json_raw_value, Spawnable}, + TransportError, }; +use super::{PubSubConnect, PubSubFrontend}; + #[derive(Debug)] /// The service contains the backend handle, a subscription manager, and the /// configuration details required to reconnect. -pub struct PubSubService { +pub(crate) struct PubSubService { /// The backend handle. pub(crate) handle: ConnectionHandle, @@ -27,7 +26,7 @@ pub struct PubSubService { pub(crate) connector: T, /// The inbound requests. - pub(crate) reqs: tokio::sync::mpsc::UnboundedReceiver, + pub(crate) reqs: mpsc::UnboundedReceiver, /// The subscription manager. pub(crate) subs: SubscriptionManager, @@ -38,18 +37,34 @@ pub struct PubSubService { impl PubSubService where - T: TransportConnect, + T: PubSubConnect, { + /// Create a new service from a connector. + pub(crate) async fn connect(connector: T) -> Result { + let handle = connector.connect().await?; + + let (tx, reqs) = mpsc::unbounded_channel(); + let this = Self { + handle, + connector, + reqs, + subs: Default::default(), + in_flights: Default::default(), + }; + this.spawn(); + Ok(PubSubFrontend::new(tx)) + } + /// Reconnect by dropping the backend and creating a new one. async fn get_new_backend(&mut self) -> Result { - let mut handle = self.connector.try_reconnect()?; + let mut handle = self.connector.try_reconnect().await?; std::mem::swap(&mut self.handle, &mut handle); Ok(handle) } /// Reconnect the backend, re-issue pending requests, and re-start active /// subscriptions. - pub async fn reconnect(&mut self) -> Result<(), TransportError> { + async fn reconnect(&mut self) -> Result<(), TransportError> { tracing::info!("Reconnecting pubsub service backend."); let mut old_handle = self @@ -198,8 +213,8 @@ where } /// Spawn the service. - pub fn spawn(mut self) -> JoinHandle<()> { - tokio::spawn(async move { + pub fn spawn(mut self) { + let fut = async move { let result: Result<(), TransportError> = loop { // We bias the loop so that we always handle new messages before // reconnecting, and always reconnect before dispatching new @@ -240,6 +255,7 @@ where if let Err(err) = result { tracing::error!(%err, "pubsub service reconnection error"); } - }) + }; + fut.spawn_task(); } } diff --git a/crates/transports/src/pubsub/trait.rs b/crates/transports/src/pubsub/trait.rs index b9ba911ab92..17bfc839b71 100644 --- a/crates/transports/src/pubsub/trait.rs +++ b/crates/transports/src/pubsub/trait.rs @@ -1,6 +1,6 @@ use crate::{Transport, TransportError, TransportFut}; -use alloy_json_rpc::{Id, RequestPacket, Response, ResponsePacket}; +use alloy_json_rpc::{Id, RequestPacket, ResponsePacket}; use alloy_primitives::U256; use serde_json::value::RawValue; use tokio::sync::broadcast; diff --git a/crates/transports/src/transports/connect.rs b/crates/transports/src/transports/connect.rs index c469b5628a9..64652fa66d3 100644 --- a/crates/transports/src/transports/connect.rs +++ b/crates/transports/src/transports/connect.rs @@ -1,4 +1,4 @@ -use crate::{BoxTransport, RpcClient, Transport, TransportError}; +use crate::{BoxTransport, Pbf, RpcClient, Transport, TransportError}; /// Connection details for a transport. /// @@ -17,25 +17,19 @@ pub trait TransportConnect: Sized + Send + Sync + 'static { /// The transport type that is returned by `connect`. type Transport: Transport + Clone; - /// Returns `true`` if the transport is a local transport. + /// Returns `true`` if the transport connects to a local resource. fn is_local(&self) -> bool; /// Connect to the transport, returning a `Transport` instance. - fn get_transport(&self) -> Result; - - /// Attempt to reconnect the transport. - /// - /// Override this to add custom reconnection logic to your connector. This - /// will be used by PubSub connection managers in the event the connection - /// fails. - fn try_reconnect(&self) -> Result { - self.get_transport() - } + fn get_transport<'a: 'b, 'b>(&self) -> Pbf<'b, Self::Transport, TransportError>; /// Connect to the transport, wrapping it into a `RpcClient` instance. - fn connect(&self) -> Result, TransportError> { - self.get_transport() - .map(|t| RpcClient::new(t, self.is_local())) + fn connect<'a: 'b, 'b>(&'a self) -> Pbf<'b, RpcClient, TransportError> { + Box::pin(async move { + self.get_transport() + .await + .map(|t| RpcClient::new(t, self.is_local())) + }) } } @@ -54,10 +48,10 @@ pub trait BoxTransportConnect { fn is_local(&self) -> bool; /// Connect to a transport, and box it. - fn to_boxed_transport(&self) -> Result; + fn get_boxed_transport<'a: 'b, 'b>(&'a self) -> Pbf<'b, BoxTransport, TransportError>; /// Connect to a transport, and box it, wrapping it into a `RpcClient`. - fn connect_boxed(&self) -> Result, TransportError>; + fn connect_boxed<'a: 'b, 'b>(&'a self) -> Pbf<'b, RpcClient, TransportError>; } impl BoxTransportConnect for T @@ -68,13 +62,16 @@ where TransportConnect::is_local(self) } - fn to_boxed_transport(&self) -> Result { - self.get_transport().map(Transport::boxed) + fn get_boxed_transport<'a: 'b, 'b>(&'a self) -> Pbf<'b, BoxTransport, TransportError> { + Box::pin(async move { self.get_transport().await.map(Transport::boxed) }) } - fn connect_boxed(&self) -> Result, TransportError> { - self.to_boxed_transport() - .map(|boxed| RpcClient::new(boxed, self.is_local())) + fn connect_boxed<'a: 'b, 'b>(&'a self) -> Pbf<'b, RpcClient, TransportError> { + Box::pin(async move { + self.get_boxed_transport() + .await + .map(|boxed| RpcClient::new(boxed, self.is_local())) + }) } } diff --git a/crates/transports/src/transports/http/mod.rs b/crates/transports/src/transports/http/mod.rs index 0b20c42576b..3ddf4b4ffce 100644 --- a/crates/transports/src/transports/http/mod.rs +++ b/crates/transports/src/transports/http/mod.rs @@ -4,7 +4,7 @@ mod hyper; #[cfg(feature = "reqwest")] mod reqwest; -use crate::client::RpcClient; +use crate::{client::RpcClient, utils::guess_local_url}; use std::{str::FromStr, sync::atomic::AtomicU64}; use url::Url; @@ -57,9 +57,7 @@ impl Http { /// 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") + guess_local_url(&self.url) } /// Get a reference to the client. diff --git a/crates/transports/src/transports/mod.rs b/crates/transports/src/transports/mod.rs index 1afe4027c92..7705e2a2117 100644 --- a/crates/transports/src/transports/mod.rs +++ b/crates/transports/src/transports/mod.rs @@ -5,7 +5,10 @@ mod connect; pub use connect::{BoxTransportConnect, TransportConnect}; mod http; -pub use http::Http; +pub use self::http::Http; mod r#trait; pub use r#trait::Transport; + +mod ws; +pub use ws::{WsBackend, WsConnect}; diff --git a/crates/transports/src/transports/ws/mod.rs b/crates/transports/src/transports/ws/mod.rs new file mode 100644 index 00000000000..c58d6a4ae98 --- /dev/null +++ b/crates/transports/src/transports/ws/mod.rs @@ -0,0 +1,47 @@ +#[cfg(not(target_arch = "wasm32"))] +mod native; +#[cfg(not(target_arch = "wasm32"))] +pub use native::WsConnect; + +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::WsConnect; + +use crate::pubsub::ConnectionInterface; + +use tracing::{error, trace}; + +/// An ongoing connection to a backend. +/// +/// Users should NEVER instantiate a backend directly. Instead, they should use +/// [`PubSubConnect`] to get a running service with a running backend. +/// +/// [`PubSubConnect`]: crate::pubsub::PubSubConnect +pub struct WsBackend { + pub(crate) socket: T, + + pub(crate) interface: ConnectionInterface, +} + +impl WsBackend { + pub async fn handle_text(&mut self, t: String) -> Result<(), ()> { + trace!(text = t, "Received message"); + + match serde_json::from_str(&t) { + Ok(item) => { + trace!(?item, "Deserialized message"); + let res = self.interface.to_frontend.send(item); + if res.is_err() { + error!("Failed to send message to handler"); + return Err(()); + } + } + Err(e) => { + error!(e = %e, "Failed to deserialize message"); + return Err(()); + } + } + Ok(()) + } +} diff --git a/crates/transports/src/transports/ws/native.rs b/crates/transports/src/transports/ws/native.rs new file mode 100644 index 00000000000..3f714a975c8 --- /dev/null +++ b/crates/transports/src/transports/ws/native.rs @@ -0,0 +1,177 @@ +use crate::{pubsub::PubSubConnect, utils::Spawnable, TransportError}; + +use futures::{SinkExt, StreamExt}; +use serde_json::value::RawValue; +use std::{future::Future, pin::Pin, time::Duration}; +use tokio::time::sleep; +use tokio_tungstenite::{ + tungstenite::{self, client::IntoClientRequest, Message}, + MaybeTlsStream, WebSocketStream, +}; +use tracing::error; + +use super::WsBackend; + +type TungsteniteStream = WebSocketStream>; + +const KEEPALIVE: u64 = 10; + +impl WsBackend { + pub async fn handle(&mut self, msg: Message) -> Result<(), ()> { + match msg { + Message::Text(text) => self.handle_text(text).await, + Message::Close(frame) => { + if frame.is_some() { + error!(?frame, "Received close frame with data"); + } else { + error!("WS server has gone away"); + } + Err(()) + } + Message::Binary(_) => { + error!("Received binary message, expected text"); + Err(()) + } + Message::Ping(_) => Ok(()), + Message::Pong(_) => Ok(()), + Message::Frame(_) => Ok(()), + } + } + + pub async fn send(&mut self, msg: Box) -> Result<(), tungstenite::Error> { + self.socket.send(Message::Text(msg.get().to_owned())).await + } + + /// Spawn a new backend task. + pub fn spawn(mut self) { + let fut = async move { + let mut err = false; + let keepalive = sleep(Duration::from_secs(KEEPALIVE)); + tokio::pin!(keepalive); + loop { + // We bias the loop as follows + // 1. Shutdown channels. + // 2. New dispatch to server. + // 3. Keepalive. + // 4. Response or notification from server. + // This ensures that keepalive is sent only if no other messages + // have been sent in the last 10 seconds. And prioritizes new + // dispatches over responses from the server. This will fail if + // the client saturates the task with dispatches, but that's + // probably not a big deal. + tokio::select! { + biased; + // break on shutdown recv, or on shutdown recv error + _ = &mut self.interface.shutdown => { + self.interface.from_frontend.close(); + break + }, + // we've received a new dispatch, so we send it via + // websocket. We handle new work before processing any + // responses from the server. + inst = self.interface.from_frontend.recv() => { + match inst { + Some(msg) => { + // Reset the keepalive timer. + keepalive.set(sleep(Duration::from_secs(KEEPALIVE))); + if let Err(e) = self.send(msg).await { + error!(err = %e, "WS connection error"); + err = true; + break + } + }, + // dispatcher has gone away + None => { + break + }, + } + }, + // Send a ping to the server, if no other messages have been + // sent in the last 10 seconds. + _ = &mut keepalive => { + // Reset the keepalive timer. + keepalive.set(sleep(Duration::from_secs(KEEPALIVE))); + if let Err(e) = self.socket.send(Message::Ping(vec![])).await { + error!(err = %e, "WS connection error"); + err = true; + break + } + } + resp = self.socket.next() => { + match resp { + Some(Ok(item)) => { + err = self.handle(item).await.is_err(); + if err { break } + }, + Some(Err(e)) => { + error!(err = %e, "WS connection error"); + err = true; + break + } + None => { + error!("WS server has gone away"); + err = true; + break + }, + } + } + } + } + if err { + let _ = self.interface.error.send(()); + } + }; + fut.spawn_task() + } +} + +#[derive(Debug, Clone)] +pub struct WsConnect { + pub url: String, + pub auth: Option, +} + +impl IntoClientRequest for WsConnect { + fn into_client_request(self) -> tungstenite::Result { + let mut request: http::Request<()> = self.url.into_client_request()?; + if let Some(auth) = self.auth { + let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?; + auth_value.set_sensitive(true); + + request + .headers_mut() + .insert(http::header::AUTHORIZATION, auth_value); + } + + request.into_client_request() + } +} + +impl PubSubConnect for WsConnect { + fn is_local(&self) -> bool { + crate::utils::guess_local_url(&self.url) + } + + fn connect<'a: 'b, 'b>( + &'a self, + ) -> Pin< + Box< + dyn Future> + + Send + + 'b, + >, + > { + let request = self.clone().into_client_request(); + + Box::pin(async move { + let (socket, _) = tokio_tungstenite::connect_async(request?).await?; + + let (handle, interface) = crate::pubsub::ConnectionHandle::new(); + let backend = WsBackend { socket, interface }; + + backend.spawn(); + + Ok(handle) + }) + } +} diff --git a/crates/transports/src/transports/ws/wasm.rs b/crates/transports/src/transports/ws/wasm.rs new file mode 100644 index 00000000000..6a46e130a7c --- /dev/null +++ b/crates/transports/src/transports/ws/wasm.rs @@ -0,0 +1,94 @@ +use super::WsBackend; +use crate::utils::Spawnable; + +use futures::{ + sink::SinkExt, + stream::{Fuse, StreamExt}, +}; +use serde_json::value::RawValue; +use tracing::error; +use ws_stream_wasm::{WsErr, WsMessage, WsStream}; + +impl WsBackend> { + pub async fn handle(&mut self, item: WsMessage) -> Result<(), ()> { + match item { + WsMessage::Text(text) => self.handle_text(text).await, + WsMessage::Binary(_) => { + error!("Received binary message, expected text"); + Err(()) + } + } + } + + pub async fn send(&mut self, msg: Box) -> Result<(), WsErr> { + self.socket + .send(WsMessage::Text(msg.get().to_owned())) + .await + } + + pub fn spawn(mut self) { + let fut = async move { + let mut err = false; + loop { + // We bias the loop as follows + // 1. Shutdown channels. + // 2. New dispatch to server. + // 3. Keepalive. + // 4. Response or notification from server. + // This ensures that keepalive is sent only if no other messages + // have been sent in the last 10 seconds. And prioritizes new + // dispatches over responses from the server. This will fail if + // the client saturates the task with dispatches, but that's + // probably not a big deal. + tokio::select! { + biased; + // break on shutdown recv, or on shutdown recv error + _ = &mut self.interface.shutdown => { + self.interface.from_frontend.close(); + break + }, + // we've received a new dispatch, so we send it via + // websocket. We handle new work before processing any + // responses from the server. + inst = self.interface.from_frontend.recv() => { + match inst { + Some(msg) => { + if let Err(e) = self.send(msg).await { + error!(err = %e, "WS connection error"); + err = true; + break + } + }, + // dispatcher has gone away + None => { + break + }, + } + }, + resp = self.socket.next() => { + match resp { + Some(item) => { + err = self.handle(item).await.is_err(); + if err { break } + }, + None => { + error!("WS server has gone away"); + err = true; + break + }, + } + } + } + } + if err { + let _ = self.interface.error.send(()); + } + }; + fut.spawn_task(); + } +} + +#[derive(Debug, Clone)] +pub struct WsConnect { + pub url: String, +} diff --git a/crates/transports/src/utils.rs b/crates/transports/src/utils.rs index b211d5567a3..b9f80a261ee 100644 --- a/crates/transports/src/utils.rs +++ b/crates/transports/src/utils.rs @@ -1,10 +1,28 @@ use serde::Serialize; use serde_json::{self, value::RawValue}; +use url::Url; use std::future::Future; use crate::error::TransportError; +/// 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(crate) fn guess_local_url(s: impl AsRef) -> bool { + fn _guess_local_url(url: &str) -> bool { + if let Ok(url) = url.parse::() { + url.host_str() + .map_or(true, |host| host == "localhost" || host == "127.0.0.1") + } else { + false + } + } + _guess_local_url(s.as_ref()) +} + /// Convert to a `Box` from a `Serialize` type, mapping the error /// to a `TransportError`. pub(crate) fn to_json_raw_value(s: &S) -> Result, TransportError> diff --git a/crates/transports/tests/http.rs b/crates/transports/tests/http.rs new file mode 100644 index 00000000000..09c51f2fab7 --- /dev/null +++ b/crates/transports/tests/http.rs @@ -0,0 +1,17 @@ +use std::borrow::Cow; + +use alloy_primitives::U64; +use alloy_transports::{ClientBuilder, RpcCall}; + +#[tokio::test] +async fn it_makes_a_request() { + let infura = std::env::var("INFURA").unwrap(); + + let client = ClientBuilder::default().reqwest_http(infura.parse().unwrap()); + + let params: Cow<'static, _> = Cow::Owned(()); + + let req: RpcCall<_, Cow<'static, ()>, U64> = client.prepare("eth_blockNumber", params); + let res = req.await; + res.unwrap(); +} diff --git a/crates/transports/tests/ws.rs b/crates/transports/tests/ws.rs new file mode 100644 index 00000000000..7bc482bf1d0 --- /dev/null +++ b/crates/transports/tests/ws.rs @@ -0,0 +1,24 @@ +use std::borrow::Cow; + +use alloy_primitives::U64; +use alloy_transports::{ClientBuilder, RpcCall, WsConnect}; + +#[tokio::test] +async fn it_makes_a_request() { + let infura = std::env::var("INFURA_WS").unwrap(); + + let connector = WsConnect { + url: infura.parse().unwrap(), + auth: None, + }; + + let client = ClientBuilder::default().connect(connector).await.unwrap(); + + let params: Cow<'static, _> = Cow::Owned(()); + + let req: RpcCall<_, Cow<'static, ()>, U64> = client.prepare("eth_blockNumber", params); + let res = req.await; + + dbg!(&res); + res.unwrap(); +} From 98c3bb87e4c9775c16dfeee42534307b00e232f6 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Nov 2023 18:08:27 -0800 Subject: [PATCH 04/30] refactor: delete pubsub trait --- crates/transports/src/client.rs | 14 ++- crates/transports/src/lib.rs | 44 ++++++++- crates/transports/src/pubsub/connect.rs | 4 +- crates/transports/src/pubsub/mod.rs | 40 -------- crates/transports/src/pubsub/trait.rs | 101 --------------------- crates/transports/src/transports/ws/mod.rs | 2 +- 6 files changed, 57 insertions(+), 148 deletions(-) delete mode 100644 crates/transports/src/pubsub/trait.rs diff --git a/crates/transports/src/client.rs b/crates/transports/src/client.rs index 269e7b743a9..4bbd52346c8 100644 --- a/crates/transports/src/client.rs +++ b/crates/transports/src/client.rs @@ -1,16 +1,19 @@ use alloy_json_rpc::{Id, Request, RequestMeta, RpcParam, RpcReturn}; +use alloy_primitives::U256; +use serde_json::value::RawValue; use std::{ borrow::Cow, sync::atomic::{AtomicU64, Ordering}, }; +use tokio::sync::broadcast; use tower::{ layer::util::{Identity, Stack}, Layer, ServiceBuilder, }; use crate::{ - BatchRequest, BoxTransport, BoxTransportConnect, RpcCall, Transport, TransportConnect, - TransportError, + pubsub::PubSubFrontend, BatchRequest, BoxTransport, BoxTransportConnect, RpcCall, Transport, + TransportConnect, TransportError, }; /// A JSON-RPC client. @@ -158,6 +161,13 @@ where } } +impl RpcClient { + /// Get a [`broadcast::Receiver`] for the given subscription ID. + pub async fn get_watcher(&self, id: U256) -> broadcast::Receiver> { + self.transport.get_subscription(id).await.unwrap() + } +} + /// A builder for the transport [`RpcClient`]. /// /// This is a wrapper around [`tower::ServiceBuilder`]. It allows you to diff --git a/crates/transports/src/lib.rs b/crates/transports/src/lib.rs index bca5da42595..5a35f87ef8c 100644 --- a/crates/transports/src/lib.rs +++ b/crates/transports/src/lib.rs @@ -1,3 +1,45 @@ +//! Alloy Transports +//! +//! ## Transport +//! +//! +//! ## PubSub services. +//! +//! ### Overview +//! +//! PubSub services, unlike regular RPC services, are long-lived and +//! bidirectional. They are used to subscribe to events on the server, and +//! receive notifications when those events occur. +//! +//! The PubSub system here consists of 3 logical parts: +//! - The **frontend** is the part of the system that the user interacts with. +//! It exposes a simple API that allows the user to issue requests and manage +//! subscriptions. +//! - The **service** is an intermediate layer that manages request/response +//! mappings, subscription aliasing, and backend lifecycle events. Running +//! [`PubSubConnect::into_service`] will spawn a long-lived service task. +//! - The **backend** is an actively running connection to the server. Users +//! should NEVER instantiate a backend directly. Instead, they should use +//! [`PubSubConnect::into_service`] for some connection object. +//! +//! This module provides the following: +//! +//! - [PubSubConnect]: A trait for instantiating a PubSub service by connecting +//! to some **backend**. Implementors of this trait are responsible for +//! the precise connection details, and for spawning the **backend** task. +//! Users should ALWAYS call [`PubSubConnect::into_service`] to get a running +//! service with a running backend. +//! - [`ConnectionHandle`]: A handle to a running **backend**. This type is +//! returned by [PubSubConnect::connect], and owned by the **service**. +//! Dropping the handle will shut down the **backend**. +//! - [`ConnectionInterface`]: The reciprocal of [ConnectionHandle]. This type +//! is owned by the **backend**, and is used to communicate with the +//! **service**. Dropping the interface will notify the **service** of a +//! terminal error. +//! - [`PubSubFrontend`]: The **frontend**. A handle to a running PubSub +//! **service**. It is used to issue requests and subscription lifecycle +//! instructions to the **service**. + mod batch; pub use batch::BatchRequest; @@ -14,7 +56,7 @@ mod error; pub use error::TransportError; mod pubsub; -pub use pubsub::{BoxPubSub, PubSub}; +pub use pubsub::{ConnectionHandle, ConnectionInterface, PubSubConnect, PubSubFrontend}; mod transports; pub use transports::{ diff --git a/crates/transports/src/pubsub/connect.rs b/crates/transports/src/pubsub/connect.rs index 0dfa5cc35d3..01af43ed002 100644 --- a/crates/transports/src/pubsub/connect.rs +++ b/crates/transports/src/pubsub/connect.rs @@ -22,10 +22,8 @@ pub trait PubSubConnect: Sized + Send + Sync + 'static { /// Attempt to reconnect the transport. /// /// Override this to add custom reconnection logic to your connector. This - /// will be used by [`PubSubService`] connection managers in the event the + /// will be used by the internal pubsub connection managers in the event the /// connection fails. - /// - /// [`PubSubService`]: crate::pubsub::service::PubSubService fn try_reconnect<'a: 'b, 'b>(&'a self) -> Pbf<'b, ConnectionHandle, TransportError> { self.connect() } diff --git a/crates/transports/src/pubsub/mod.rs b/crates/transports/src/pubsub/mod.rs index 3172b44e3c6..bee0d145d97 100644 --- a/crates/transports/src/pubsub/mod.rs +++ b/crates/transports/src/pubsub/mod.rs @@ -1,40 +1,3 @@ -//! PubSub services. -//! -//! # Overview -//! -//! PubSub services, unlike regular RPC services, are long-lived and -//! bidirectional. They are used to subscribe to events on the server, and -//! receive notifications when those events occur. -//! -//! The PubSub system here consists of 3 logical parts: -//! - The **frontend** is the part of the system that the user interacts with. -//! It exposes a simple API that allows the user to issue requests and manage -//! subscriptions. -//! - The **service** is an intermediate layer that manages request/response -//! mappings, subscription aliasing, and backend lifecycle events. Running -//! [`PubSubConnect::into_service`] will spawn a long-lived service task. -//! - The **backend** is an actively running connection to the server. Users -//! should NEVER instantiate a backend directly. Instead, they should use -//! [`PubSubConnect::into_service`] for some connection object. -//! -//! This module provides the following: -//! -//! - [PubSubConnect]: A trait for instantiating a PubSub service by connecting -//! to some **backend**. Implementors of this trait are responsible for -//! the precise connection details, and for spawning the **backend** task. -//! Users should ALWAYS call [`PubSubConnect::into_service`] to get a running -//! service with a running backend. -//! - [`ConnectionHandle`]: A handle to a running **backend**. This type is -//! returned by [PubSubConnect::connect], and owned by the **service**. -//! Dropping the handle will shut down the **backend**. -//! - [`ConnectionInterface`]: The reciprocal of [ConnectionHandle]. This type -//! is owned by the **backend**, and is used to communicate with the -//! **service**. Dropping the interface will notify the **service** of a -//! terminal error. -//! - [`PubSubFrontend`]: The **frontend**. A handle to a running PubSub -//! **service**. It is used to issue requests and subscription lifecycle -//! instructions to the **service**. - mod connect; pub use connect::PubSubConnect; @@ -49,6 +12,3 @@ pub use handle::{ConnectionHandle, ConnectionInterface}; mod managers; mod service; - -mod r#trait; -pub use r#trait::{BoxPubSub, PubSub}; diff --git a/crates/transports/src/pubsub/trait.rs b/crates/transports/src/pubsub/trait.rs deleted file mode 100644 index 17bfc839b71..00000000000 --- a/crates/transports/src/pubsub/trait.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::{Transport, TransportError, TransportFut}; - -use alloy_json_rpc::{Id, RequestPacket, ResponsePacket}; -use alloy_primitives::U256; -use serde_json::value::RawValue; -use tokio::sync::broadcast; -use tower::Service; - -/// A trait for transports supporting notifications. -/// -/// This trait models notifications bodies as a stream of [`RawValue`]s. It is -/// up to the recipient to deserialize the notification. -pub trait PubSub: Transport { - /// Reserve an ID for a subscription, based on the JSON-RPC request ID of - /// the subscription request. - /// - /// This is intended for internal use by RPC clients, and should not be - /// called directly. - fn reserve_id(&self, id: &Id) -> U256; - - /// Get a [`broadcast::Receiver`] for the given subscription ID. - fn get_watcher(&self, id: U256) -> broadcast::Receiver>; -} - -/// Helper trait for constructing [`BoxPubSub`]. -trait ClonePubSub: PubSub { - fn clone_box(&self) -> Box; -} - -impl ClonePubSub for T -where - T: PubSub + Clone + Send + Sync, -{ - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - -/// A boxed, Clone-able [`PubSub`] trait object. -/// -/// This type allows [`RpcClient`] to use a type-erased transport. It is -/// [`Clone`] and [`Send`] + [`Sync`], and implementes [`PubSub`]. This -/// allows for complex behavior abstracting across several different clients -/// with different transport types. -/// -/// [`RpcClient`]: crate::client::RpcClient -#[repr(transparent)] -pub struct BoxPubSub { - inner: Box, -} - -impl std::fmt::Debug for BoxPubSub { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BoxPubSub").finish() - } -} - -impl Clone for BoxPubSub { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone_box(), - } - } -} - -impl PubSub for BoxPubSub { - fn reserve_id(&self, id: &Id) -> U256 { - self.inner.reserve_id(id) - } - - fn get_watcher(&self, id: U256) -> broadcast::Receiver> { - self.inner.get_watcher(id) - } -} - -impl Service for BoxPubSub { - type Response = ResponsePacket; - - type Error = TransportError; - - type Future = TransportFut<'static>; - - fn poll_ready( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - Service::::poll_ready(&mut self.inner, cx) - } - - fn call(&mut self, req: RequestPacket) -> Self::Future { - Service::::call(&mut self.inner, req) - } -} - -/// checks trait + send + sync + 'static -fn __compile_check() { - fn inner() { - unimplemented!() - } - inner::(); -} diff --git a/crates/transports/src/transports/ws/mod.rs b/crates/transports/src/transports/ws/mod.rs index c58d6a4ae98..eb9222c0431 100644 --- a/crates/transports/src/transports/ws/mod.rs +++ b/crates/transports/src/transports/ws/mod.rs @@ -17,7 +17,7 @@ use tracing::{error, trace}; /// Users should NEVER instantiate a backend directly. Instead, they should use /// [`PubSubConnect`] to get a running service with a running backend. /// -/// [`PubSubConnect`]: crate::pubsub::PubSubConnect +/// [`PubSubConnect`]: crate::PubSubConnect pub struct WsBackend { pub(crate) socket: T, From ed4de99befbafbe89b50d375fd2f858282760af1 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Nov 2023 18:26:35 -0800 Subject: [PATCH 05/30] refactor: disable batching for pubsub --- crates/transports/src/client.rs | 40 +++++++++------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/crates/transports/src/client.rs b/crates/transports/src/client.rs index 4bbd52346c8..ebcd79471d8 100644 --- a/crates/transports/src/client.rs +++ b/crates/transports/src/client.rs @@ -12,8 +12,8 @@ use tower::{ }; use crate::{ - pubsub::PubSubFrontend, BatchRequest, BoxTransport, BoxTransportConnect, RpcCall, Transport, - TransportConnect, TransportError, + pubsub::PubSubFrontend, BatchRequest, BoxTransport, BoxTransportConnect, Http, RpcCall, + Transport, TransportConnect, TransportError, }; /// A JSON-RPC client. @@ -118,12 +118,6 @@ impl RpcClient where T: Transport + Clone, { - /// Create a new [`BatchRequest`] builder. - #[inline] - pub fn new_batch(&self) -> BatchRequest { - BatchRequest::new(self) - } - /// Prepare an [`RpcCall`]. /// /// This function reserves an ID for the request, however the request @@ -168,6 +162,14 @@ impl RpcClient { } } +impl RpcClient> { + /// Create a new [`BatchRequest`] builder. + #[inline] + pub fn new_batch(&self) -> BatchRequest> { + BatchRequest::new(self) + } +} + /// A builder for the transport [`RpcClient`]. /// /// This is a wrapper around [`tower::ServiceBuilder`]. It allows you to @@ -262,25 +264,3 @@ impl ClientBuilder { Ok(self.transport(transport, connect.is_local())) } } - -#[cfg(all(test, feature = "reqwest"))] -mod test { - use crate::{pubsub::PubSubFrontend, transports::Http}; - - use super::RpcClient; - - #[test] - fn basic_instantiation() { - let h: RpcClient> = "http://localhost:8545".parse().unwrap(); - - assert!(h.is_local()); - } - - fn __compile_check_a() -> RpcClient { - todo!() - } - - fn __compile_check_2() { - let _ = __compile_check_a().new_batch(); - } -} From 5f77c63ee6b6c1f9a8e63a043e463a3b85bb76e7 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Nov 2023 08:03:28 -0800 Subject: [PATCH 06/30] nit: match tuple order --- crates/json-rpc/src/request.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index 0cda2ee40eb..9449e5b2cea 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -155,8 +155,8 @@ impl SerializedRequest { &self.request } - /// Consumes the serialized request, returning the underlying [`RawValue`] - /// and the [`RequestMeta`]. + /// Consumes the serialized request, returning the underlying + /// [`RequestMeta`] and the [`RawValue`]. pub fn decompose(self) -> (RequestMeta, Box) { (self.meta, self.request) } From 06e2606d0c088fa83fad25d777372b28120ae0fc Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 10:08:04 -0800 Subject: [PATCH 07/30] fix: manually impl deser of pubsubitem --- crates/json-rpc/src/notification.rs | 105 ++++++++++++++++++++- crates/transports/Cargo.toml | 5 + crates/transports/src/call.rs | 22 ++++- crates/transports/src/transports/ws/mod.rs | 5 +- crates/transports/tests/{ => it}/http.rs | 0 crates/transports/tests/it/main.rs | 2 + crates/transports/tests/{ => it}/ws.rs | 17 +++- 7 files changed, 145 insertions(+), 11 deletions(-) rename crates/transports/tests/{ => it}/http.rs (100%) create mode 100644 crates/transports/tests/it/main.rs rename crates/transports/tests/{ => it}/ws.rs (70%) diff --git a/crates/json-rpc/src/notification.rs b/crates/json-rpc/src/notification.rs index ecb8fe41f9b..eb6b226ca8b 100644 --- a/crates/json-rpc/src/notification.rs +++ b/crates/json-rpc/src/notification.rs @@ -1,5 +1,8 @@ use alloy_primitives::U256; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{MapAccess, Visitor}, + Deserialize, Serialize, +}; use crate::Response; @@ -14,9 +17,105 @@ pub struct EthNotification> { /// An item received over an Ethereum pubsub transport. Ethereum pubsub uses a /// non-standard JSON-RPC notification format. An item received over a pubsub /// transport may be a JSON-RPC response or an Ethereum-style notification. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone)] pub enum PubSubItem { Response(Response), Notification(EthNotification), } + +impl<'de> Deserialize<'de> for PubSubItem { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct PubSubItemVisitor; + + impl<'de> Visitor<'de> for PubSubItemVisitor { + type Value = PubSubItem; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a JSON-RPC response or an Ethereum-style notification") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut id = None; + let mut subscription = None; + let mut result = None; + let mut error = None; + + while let Ok(Some(key)) = map.next_key() { + match key { + "id" => { + if id.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id = Some(map.next_value()?); + } + "subscription" => { + if subscription.is_some() { + return Err(serde::de::Error::duplicate_field("subscription")); + } + subscription = Some(map.next_value()?); + } + "result" => { + if result.is_some() { + return Err(serde::de::Error::duplicate_field("result")); + } + result = Some(map.next_value()?); + } + "error" => { + if error.is_some() { + return Err(serde::de::Error::duplicate_field("error")); + } + error = Some(map.next_value()?); + } + _ => { + let _ = map.next_value::()?; + } + } + } + + if let Some(id) = id { + if subscription.is_some() { + return Err(serde::de::Error::custom( + "unexpected subscription in pubsub item", + )); + } + + let payload = if error.is_some() { + crate::ResponsePayload::Failure(error.unwrap()) + } else { + if result.is_none() { + return Err(serde::de::Error::missing_field("result")); + } + crate::ResponsePayload::Success(result.unwrap()) + }; + Ok(PubSubItem::Response(Response { id, payload })) + } else { + if error.is_some() { + return Err(serde::de::Error::custom( + "unexpected `error` field in subscription notification", + )); + } + if subscription.is_none() { + return Err(serde::de::Error::missing_field("subscription")); + } + + if result.is_none() { + return Err(serde::de::Error::missing_field("result")); + } + + Ok(PubSubItem::Notification(EthNotification { + subscription: subscription.unwrap(), + result: result.unwrap(), + })) + } + } + } + + deserializer.deserialize_any(PubSubItemVisitor) + } +} diff --git a/crates/transports/Cargo.toml b/crates/transports/Cargo.toml index b0f3afa889a..0784c8c2583 100644 --- a/crates/transports/Cargo.toml +++ b/crates/transports/Cargo.toml @@ -55,3 +55,8 @@ tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"] default = ["reqwest", "hyper"] reqwest = ["dep:reqwest"] hyper = ["dep:hyper", "hyper/client"] + +[dev-dependencies] +test-log = { version = "0.2.13", default-features = false, features = ["trace"] } +tracing-subscriber = "0.3.17" +tracing-test = "0.2.4" diff --git a/crates/transports/src/call.rs b/crates/transports/src/call.rs index eec5245f076..a1f91a92a05 100644 --- a/crates/transports/src/call.rs +++ b/crates/transports/src/call.rs @@ -4,20 +4,22 @@ use alloy_json_rpc::{Request, RequestPacket, ResponsePacket, RpcParam, RpcResult use core::panic; use serde_json::value::RawValue; use std::{ + fmt::Debug, future::Future, marker::PhantomData, pin::Pin, task::{self, Poll::Ready}, }; use tower::Service; +use tracing::{instrument, trace}; /// The states of the [`RpcCall`] future. #[must_use = "futures do nothing unless you `.await` or poll them"] #[pin_project::pin_project(project = CallStateProj)] enum CallState where - Conn: Transport + Clone, Params: RpcParam, + Conn: Transport + Clone, { Prepared { request: Option>, @@ -30,6 +32,20 @@ where Complete, } +impl Debug for CallState +where + Params: RpcParam, + Conn: Transport + Clone, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Prepared { .. } => f.debug_struct("Prepared").finish(), + Self::AwaitingResponse { .. } => f.debug_struct("AwaitingResponse").finish(), + Self::Complete => write!(f, "Complete"), + } + } +} + impl CallState where Conn: Transport + Clone, @@ -39,6 +55,7 @@ where mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, ) -> task::Poll<::Output> { + trace!("Polling prepared"); let fut = { let CallStateProj::Prepared { connection, @@ -76,6 +93,7 @@ where mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, ) -> task::Poll<::Output> { + trace!("Polling awaiting"); let CallStateProj::AwaitingResponse { fut } = self.as_mut().project() else { unreachable!("Called poll_awaiting in incorrect state") }; @@ -95,6 +113,7 @@ where { type Output = RpcResult, Box, TransportError>; + #[instrument(skip(self, cx))] fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { if matches!(*self.as_mut(), CallState::Prepared { .. }) { return self.poll_prepared(cx); @@ -191,6 +210,7 @@ where type Output = RpcResult, TransportError>; fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + tracing::trace!(?self.state, "Polling RpcCall"); let this = self.project(); let resp = task::ready!(this.state.poll(cx)); diff --git a/crates/transports/src/transports/ws/mod.rs b/crates/transports/src/transports/ws/mod.rs index eb9222c0431..c29fb0ea0d4 100644 --- a/crates/transports/src/transports/ws/mod.rs +++ b/crates/transports/src/transports/ws/mod.rs @@ -10,7 +10,7 @@ pub use wasm::WsConnect; use crate::pubsub::ConnectionInterface; -use tracing::{error, trace}; +use tracing::{debug, error, trace}; /// An ongoing connection to a backend. /// @@ -25,8 +25,9 @@ pub struct WsBackend { } impl WsBackend { + #[tracing::instrument(skip(self))] pub async fn handle_text(&mut self, t: String) -> Result<(), ()> { - trace!(text = t, "Received message"); + debug!(text = t, "Received message from websocket"); match serde_json::from_str(&t) { Ok(item) => { diff --git a/crates/transports/tests/http.rs b/crates/transports/tests/it/http.rs similarity index 100% rename from crates/transports/tests/http.rs rename to crates/transports/tests/it/http.rs diff --git a/crates/transports/tests/it/main.rs b/crates/transports/tests/it/main.rs new file mode 100644 index 00000000000..d4904218868 --- /dev/null +++ b/crates/transports/tests/it/main.rs @@ -0,0 +1,2 @@ +mod http; +mod ws; diff --git a/crates/transports/tests/ws.rs b/crates/transports/tests/it/ws.rs similarity index 70% rename from crates/transports/tests/ws.rs rename to crates/transports/tests/it/ws.rs index 7bc482bf1d0..0a90b0aaa3f 100644 --- a/crates/transports/tests/ws.rs +++ b/crates/transports/tests/it/ws.rs @@ -1,9 +1,9 @@ -use std::borrow::Cow; +use alloy_transports::{ClientBuilder, RpcCall, WsConnect}; use alloy_primitives::U64; -use alloy_transports::{ClientBuilder, RpcCall, WsConnect}; +use std::borrow::Cow; -#[tokio::test] +#[test_log::test(tokio::test)] async fn it_makes_a_request() { let infura = std::env::var("INFURA_WS").unwrap(); @@ -12,13 +12,20 @@ async fn it_makes_a_request() { auth: None, }; + dbg!("have connector"); + let client = ClientBuilder::default().connect(connector).await.unwrap(); + dbg!("have client"); + let params: Cow<'static, _> = Cow::Owned(()); let req: RpcCall<_, Cow<'static, ()>, U64> = client.prepare("eth_blockNumber", params); - let res = req.await; + + let timeout = tokio::time::timeout(std::time::Duration::from_secs(2), req); + + let res = timeout.await; dbg!(&res); - res.unwrap(); + res.unwrap().unwrap(); } From e762f8d891b0dcb6215274e149d7a571ef326219 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 10:18:10 -0800 Subject: [PATCH 08/30] fix: clippy --- crates/json-rpc/src/notification.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/json-rpc/src/notification.rs b/crates/json-rpc/src/notification.rs index eb6b226ca8b..450e185e036 100644 --- a/crates/json-rpc/src/notification.rs +++ b/crates/json-rpc/src/notification.rs @@ -4,7 +4,7 @@ use serde::{ Deserialize, Serialize, }; -use crate::Response; +use crate::{Response, ResponsePayload}; /// An ethereum-style notification, not to be confused with a JSON-RPC /// notification. @@ -85,13 +85,14 @@ impl<'de> Deserialize<'de> for PubSubItem { )); } - let payload = if error.is_some() { - crate::ResponsePayload::Failure(error.unwrap()) + let payload = if let Some(error) = error { + ResponsePayload::Failure(error) + } else if let Some(result) = result { + ResponsePayload::Success(result) } else { - if result.is_none() { - return Err(serde::de::Error::missing_field("result")); - } - crate::ResponsePayload::Success(result.unwrap()) + return Err(serde::de::Error::custom( + "missing `result` or `error` field in response", + )); }; Ok(PubSubItem::Response(Response { id, payload })) } else { From acbffe3ff130123bae66aad4d73a0b78b6f0cbd4 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 10:19:55 -0800 Subject: [PATCH 09/30] docs: comments for deser impl --- crates/json-rpc/src/notification.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/json-rpc/src/notification.rs b/crates/json-rpc/src/notification.rs index 450e185e036..ca1e060d392 100644 --- a/crates/json-rpc/src/notification.rs +++ b/crates/json-rpc/src/notification.rs @@ -46,6 +46,7 @@ impl<'de> Deserialize<'de> for PubSubItem { let mut result = None; let mut error = None; + // Drain the map into the appropriate fields. while let Ok(Some(key)) = map.next_key() { match key { "id" => { @@ -72,19 +73,21 @@ impl<'de> Deserialize<'de> for PubSubItem { } error = Some(map.next_value()?); } + // Discard unknown fields. _ => { let _ = map.next_value::()?; } } } + // If it has an ID, it is a response. if let Some(id) = id { if subscription.is_some() { return Err(serde::de::Error::custom( "unexpected subscription in pubsub item", )); } - + // We need to differentiate error vs result here. let payload = if let Some(error) = error { ResponsePayload::Failure(error) } else if let Some(result) = result { @@ -96,15 +99,16 @@ impl<'de> Deserialize<'de> for PubSubItem { }; Ok(PubSubItem::Response(Response { id, payload })) } else { + // Notifications cannot have an error. if error.is_some() { return Err(serde::de::Error::custom( "unexpected `error` field in subscription notification", )); } + // Notifications must have a subscription and a result. if subscription.is_none() { return Err(serde::de::Error::missing_field("subscription")); } - if result.is_none() { return Err(serde::de::Error::missing_field("result")); } From 751d68cf74e526ca467ad4e2f08991cb6b78856d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 10:22:08 -0800 Subject: [PATCH 10/30] chore: remove dbg from test --- crates/transports/tests/it/ws.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/transports/tests/it/ws.rs b/crates/transports/tests/it/ws.rs index 0a90b0aaa3f..5fd12651cee 100644 --- a/crates/transports/tests/it/ws.rs +++ b/crates/transports/tests/it/ws.rs @@ -12,12 +12,8 @@ async fn it_makes_a_request() { auth: None, }; - dbg!("have connector"); - let client = ClientBuilder::default().connect(connector).await.unwrap(); - dbg!("have client"); - let params: Cow<'static, _> = Cow::Owned(()); let req: RpcCall<_, Cow<'static, ()>, U64> = client.prepare("eth_blockNumber", params); From b628e0ac14d95f068440489842740e3eeb684c3c Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 10:22:39 -0800 Subject: [PATCH 11/30] chore: remove dbg from test --- crates/transports/tests/it/ws.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/transports/tests/it/ws.rs b/crates/transports/tests/it/ws.rs index 5fd12651cee..ce0f1fd936b 100644 --- a/crates/transports/tests/it/ws.rs +++ b/crates/transports/tests/it/ws.rs @@ -20,8 +20,5 @@ async fn it_makes_a_request() { let timeout = tokio::time::timeout(std::time::Duration::from_secs(2), req); - let res = timeout.await; - - dbg!(&res); - res.unwrap().unwrap(); + timeout.await.unwrap().unwrap(); } From 37ecfb6dc1d056e80653dc030512f7d23c39cc92 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 10:28:32 -0800 Subject: [PATCH 12/30] refactor: rename env vars --- crates/transports/tests/it/http.rs | 5 ++--- crates/transports/tests/it/ws.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/transports/tests/it/http.rs b/crates/transports/tests/it/http.rs index 09c51f2fab7..14d9b03b93f 100644 --- a/crates/transports/tests/it/http.rs +++ b/crates/transports/tests/it/http.rs @@ -5,13 +5,12 @@ use alloy_transports::{ClientBuilder, RpcCall}; #[tokio::test] async fn it_makes_a_request() { - let infura = std::env::var("INFURA").unwrap(); + let infura = std::env::var("HTTP_PROVIDER_URL").unwrap(); let client = ClientBuilder::default().reqwest_http(infura.parse().unwrap()); let params: Cow<'static, _> = Cow::Owned(()); let req: RpcCall<_, Cow<'static, ()>, U64> = client.prepare("eth_blockNumber", params); - let res = req.await; - res.unwrap(); + req.await.unwrap(); } diff --git a/crates/transports/tests/it/ws.rs b/crates/transports/tests/it/ws.rs index ce0f1fd936b..8d82b326bec 100644 --- a/crates/transports/tests/it/ws.rs +++ b/crates/transports/tests/it/ws.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; #[test_log::test(tokio::test)] async fn it_makes_a_request() { - let infura = std::env::var("INFURA_WS").unwrap(); + let infura = std::env::var("WS_PROVIDER_URL").unwrap(); let connector = WsConnect { url: infura.parse().unwrap(), From 6ce20b5bdf38882e06daeb526fb0315777875f40 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:02:36 -0800 Subject: [PATCH 13/30] refactor: break transports into several crates --- Cargo.toml | 13 +- crates/networks/Cargo.toml | 6 +- crates/providers/Cargo.toml | 5 +- crates/providers/src/builder.rs | 3 +- crates/providers/src/lib.rs | 3 +- crates/providers/src/provider.rs | 8 +- crates/pubsub/Cargo.toml | 25 ++++ .../src/pubsub => pubsub/src}/connect.rs | 22 +-- .../src/pubsub => pubsub/src}/frontend.rs | 6 +- .../src/pubsub => pubsub/src}/handle.rs | 56 +++++++ .../src/pubsub => pubsub/src}/ix.rs | 2 +- .../src/pubsub/mod.rs => pubsub/src/lib.rs} | 0 .../src}/managers/active_sub.rs | 0 .../src}/managers/in_flight.rs | 2 +- .../src/pubsub => pubsub/src}/managers/mod.rs | 0 .../src/pubsub => pubsub/src}/managers/req.rs | 2 +- .../src/pubsub => pubsub/src}/managers/sub.rs | 2 +- .../src/pubsub => pubsub/src}/service.rs | 16 +- crates/rpc-client/Cargo.toml | 41 ++++++ .../{transports => rpc-client}/src/batch.rs | 16 +- crates/rpc-client/src/builder.rs | 116 +++++++++++++++ crates/{transports => rpc-client}/src/call.rs | 3 +- .../{transports => rpc-client}/src/client.rs | 139 +++--------------- crates/rpc-client/src/lib.rs | 11 ++ .../tests/it/http.rs | 2 +- crates/rpc-client/tests/it/main.rs | 4 + .../{transports => rpc-client}/tests/it/ws.rs | 5 +- crates/transport-http/Cargo.toml | 33 +++++ .../http => transport-http/src}/backend.rs | 0 .../http => transport-http/src}/hyper.rs | 0 .../http/mod.rs => transport-http/src/lib.rs} | 31 +--- .../http => transport-http/src}/reqwest.rs | 13 +- crates/transport-ws/Cargo.toml | 34 +++++ .../ws/mod.rs => transport-ws/src/lib.rs} | 4 +- .../ws => transport-ws/src}/native.rs | 33 ++--- .../ws => transport-ws/src}/wasm.rs | 0 crates/transport/Cargo.toml | 27 ++++ crates/{transports => transport}/README.md | 0 .../src/transports => transport/src}/boxed.rs | 0 .../{transports => transport}/src/common.rs | 0 .../transports => transport/src}/connect.rs | 22 +-- crates/{transports => transport}/src/error.rs | 15 -- crates/{transports => transport}/src/lib.rs | 23 +-- .../src/transports => transport/src}/trait.rs | 0 crates/{transports => transport}/src/utils.rs | 7 +- crates/transports/Cargo.toml | 62 -------- crates/transports/src/transports/mod.rs | 14 -- crates/transports/tests/it/main.rs | 2 - 48 files changed, 465 insertions(+), 363 deletions(-) create mode 100644 crates/pubsub/Cargo.toml rename crates/{transports/src/pubsub => pubsub/src}/connect.rs (71%) rename crates/{transports/src/pubsub => pubsub/src}/frontend.rs (97%) rename crates/{transports/src/pubsub => pubsub/src}/handle.rs (57%) rename crates/{transports/src/pubsub => pubsub/src}/ix.rs (95%) rename crates/{transports/src/pubsub/mod.rs => pubsub/src/lib.rs} (100%) rename crates/{transports/src/pubsub => pubsub/src}/managers/active_sub.rs (100%) rename crates/{transports/src/pubsub => pubsub/src}/managers/in_flight.rs (98%) rename crates/{transports/src/pubsub => pubsub/src}/managers/mod.rs (100%) rename crates/{transports/src/pubsub => pubsub/src}/managers/req.rs (96%) rename crates/{transports/src/pubsub => pubsub/src}/managers/sub.rs (98%) rename crates/{transports/src/pubsub => pubsub/src}/service.rs (97%) create mode 100644 crates/rpc-client/Cargo.toml rename crates/{transports => rpc-client}/src/batch.rs (98%) create mode 100644 crates/rpc-client/src/builder.rs rename crates/{transports => rpc-client}/src/call.rs (98%) rename crates/{transports => rpc-client}/src/client.rs (53%) create mode 100644 crates/rpc-client/src/lib.rs rename crates/{transports => rpc-client}/tests/it/http.rs (89%) create mode 100644 crates/rpc-client/tests/it/main.rs rename crates/{transports => rpc-client}/tests/it/ws.rs (77%) create mode 100644 crates/transport-http/Cargo.toml rename crates/{transports/src/transports/http => transport-http/src}/backend.rs (100%) rename crates/{transports/src/transports/http => transport-http/src}/hyper.rs (100%) rename crates/{transports/src/transports/http/mod.rs => transport-http/src/lib.rs} (72%) rename crates/{transports/src/transports/http => transport-http/src}/reqwest.rs (78%) create mode 100644 crates/transport-ws/Cargo.toml rename crates/{transports/src/transports/ws/mod.rs => transport-ws/src/lib.rs} (92%) rename crates/{transports/src/transports/ws => transport-ws/src}/native.rs (87%) rename crates/{transports/src/transports/ws => transport-ws/src}/wasm.rs (100%) create mode 100644 crates/transport/Cargo.toml rename crates/{transports => transport}/README.md (100%) rename crates/{transports/src/transports => transport/src}/boxed.rs (100%) rename crates/{transports => transport}/src/common.rs (100%) rename crates/{transports/src/transports => transport/src}/connect.rs (72%) rename crates/{transports => transport}/src/error.rs (78%) rename crates/{transports => transport}/src/lib.rs (90%) rename crates/{transports/src/transports => transport/src}/trait.rs (100%) rename crates/{transports => transport}/src/utils.rs (89%) delete mode 100644 crates/transports/Cargo.toml delete mode 100644 crates/transports/src/transports/mod.rs delete mode 100644 crates/transports/tests/it/main.rs diff --git a/Cargo.toml b/Cargo.toml index c1b4109c732..1ec65586fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,18 +17,20 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [workspace.dependencies] - alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" } -alloy-transports = { version = "0.1.0", path = "crates/transports" } +alloy-transport = { version = "0.1.0", path = "crates/transport" } +alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" } +alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } +alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" } alloy-networks = { version = "0.1.0", path = "crates/networks" } alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } +alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } alloy-primitives = { version = "0.4.2", features = ["serde"] } alloy-rlp = "0.3.0" # futures -futures-channel = "0.3" -futures-util = "0.3" +futures = "0.3.29" # serde serde = { version = "1.0", default-features = false, features = ["alloc"] } @@ -40,4 +42,7 @@ thiserror = "1.0" # transports url = "2.4.0" pin-project = "1.1.2" +reqwest = "0.11.18" tower = { version = "0.4.13", features = ["util"] } +tokio = { version = "1.33.0", features = ["sync", "macros"] } +tracing = "0.1.40" diff --git a/crates/networks/Cargo.toml b/crates/networks/Cargo.toml index 70982c2425a..b1446341adb 100644 --- a/crates/networks/Cargo.toml +++ b/crates/networks/Cargo.toml @@ -14,6 +14,6 @@ exclude.workspace = true alloy-json-rpc.workspace = true alloy-primitives.workspace = true alloy-rlp.workspace = true -alloy-transports.workspace = true -pin-project = "1.1.2" -tower = "0.4.13" +alloy-transport.workspace = true +pin-project.workspace = true +tower.workspace = true diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 1c44afd7663..f22057d96e6 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -15,8 +15,11 @@ exclude.workspace = true alloy-json-rpc.workspace = true alloy-networks.workspace = true alloy-primitives.workspace = true -alloy-transports.workspace = true +alloy-transport.workspace = true +alloy-transport-http.workspace = true alloy-rpc-types.workspace = true +alloy-rpc-client.workspace = true + async-trait = "0.1.73" futures-util = "0.3.28" serde_json = { workspace = true, features = ["raw_value"] } diff --git a/crates/providers/src/builder.rs b/crates/providers/src/builder.rs index 946951d40e2..8ae31f667e3 100644 --- a/crates/providers/src/builder.rs +++ b/crates/providers/src/builder.rs @@ -1,7 +1,8 @@ use std::marker::PhantomData; use alloy_networks::Network; -use alloy_transports::{RpcClient, Transport}; +use alloy_rpc_client::RpcClient; +use alloy_transport::Transport; use crate::{NetworkRpcClient, Provider}; diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index d033c422195..30eb6df1ebb 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -4,7 +4,8 @@ pub use builder::{ProviderBuilder, ProviderLayer, Stack}; use alloy_json_rpc::RpcResult; use alloy_networks::{Network, Transaction}; use alloy_primitives::Address; -use alloy_transports::{BoxTransport, RpcClient, Transport, TransportError}; +use alloy_rpc_client::RpcClient; +use alloy_transport::{BoxTransport, Transport, TransportError}; use serde_json::value::RawValue; pub mod provider; diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index 6583b250e1e..470a6e3a72b 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -1,11 +1,13 @@ //! Alloy main Provider abstraction. use alloy_primitives::{Address, BlockHash, Bytes, TxHash, U256, U64}; +use alloy_rpc_client::{ClientBuilder, RpcClient}; use alloy_rpc_types::{ Block, BlockId, BlockNumberOrTag, FeeHistory, Filter, Log, RpcBlockHash, SyncStatus, Transaction, TransactionReceipt, TransactionRequest, }; -use alloy_transports::{BoxTransport, Http, RpcClient, RpcResult, Transport, TransportError}; +use alloy_transport::{BoxTransport, RpcResult, Transport, TransportError}; +use alloy_transport_http::Http; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -422,7 +424,9 @@ impl Provider { // HTTP Transport Provider implementation impl Provider> { pub fn new(url: &str) -> Result { - let inner: RpcClient> = url.parse().map_err(|_e| ClientError::ParseError)?; + let url = url.parse().map_err(|_e| ClientError::ParseError)?; + let inner = ClientBuilder::default().reqwest_http(url); + Ok(Self { inner, from: None }) } } diff --git a/crates/pubsub/Cargo.toml b/crates/pubsub/Cargo.toml new file mode 100644 index 00000000000..5e9f0babefd --- /dev/null +++ b/crates/pubsub/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "alloy-pubsub" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +alloy-json-rpc.workspace = true +alloy-transport.workspace = true + +alloy-primitives.workspace = true +futures.workspace = true +serde_json.workspace = true +tokio.workspace = true +tower.workspace = true +tracing.workspace = true + +bimap = "0.6.3" \ No newline at end of file diff --git a/crates/transports/src/pubsub/connect.rs b/crates/pubsub/src/connect.rs similarity index 71% rename from crates/transports/src/pubsub/connect.rs rename to crates/pubsub/src/connect.rs index 01af43ed002..015b75364bc 100644 --- a/crates/transports/src/pubsub/connect.rs +++ b/crates/pubsub/src/connect.rs @@ -1,7 +1,5 @@ -use crate::{ - pubsub::{handle::ConnectionHandle, service::PubSubService, PubSubFrontend}, - Pbf, TransportConnect, TransportError, -}; +use crate::{handle::ConnectionHandle, service::PubSubService, PubSubFrontend}; +use alloy_transport::{Pbf, TransportError}; /// Configuration objects that contain connection details for a backend. /// @@ -33,19 +31,3 @@ pub trait PubSubConnect: Sized + Send + Sync + 'static { Box::pin(PubSubService::connect(self)) } } - -impl TransportConnect for T -where - T: PubSubConnect + Clone, -{ - type Transport = PubSubFrontend; - - fn is_local(&self) -> bool { - PubSubConnect::is_local(self) - } - - fn get_transport<'a: 'b, 'b>(&self) -> Pbf<'b, Self::Transport, TransportError> { - let this = self.clone(); - Box::pin(async move { this.into_service().await }) - } -} diff --git a/crates/transports/src/pubsub/frontend.rs b/crates/pubsub/src/frontend.rs similarity index 97% rename from crates/transports/src/pubsub/frontend.rs rename to crates/pubsub/src/frontend.rs index 52dfe978188..5b49912ced6 100644 --- a/crates/transports/src/pubsub/frontend.rs +++ b/crates/pubsub/src/frontend.rs @@ -6,10 +6,8 @@ use futures::future::try_join_all; use serde_json::value::RawValue; use tokio::sync::{broadcast, mpsc, oneshot}; -use crate::{ - pubsub::{ix::PubSubInstruction, managers::InFlight}, - TransportError, TransportFut, -}; +use crate::{ix::PubSubInstruction, managers::InFlight}; +use alloy_transport::{TransportError, TransportFut}; #[derive(Debug, Clone)] pub struct PubSubFrontend { diff --git a/crates/transports/src/pubsub/handle.rs b/crates/pubsub/src/handle.rs similarity index 57% rename from crates/transports/src/pubsub/handle.rs rename to crates/pubsub/src/handle.rs index 6daf57b99dc..c183215e5c0 100644 --- a/crates/transports/src/pubsub/handle.rs +++ b/crates/pubsub/src/handle.rs @@ -1,4 +1,7 @@ +use std::task::Poll; + use alloy_json_rpc::PubSubItem; +use futures::{FutureExt, Stream}; use serde_json::value::RawValue; use tokio::sync::{mpsc, oneshot}; @@ -41,6 +44,7 @@ impl ConnectionHandle { to_frontend, error: error_tx, shutdown: shutdown_rx, + dead: false, }; (handle, interface) } @@ -52,6 +56,15 @@ impl ConnectionHandle { } /// The reciprocal of [`ConnectionHandle`]. +/// +/// [`ConnectionInterface`] implements [`Stream`] for receiving requests from +/// the frontend. The [`Stream`] implementation will return `None` permanently +/// when the shutdown channel from the frontend has resolved. +/// +/// It sends responses to the frontend via the `send_to_frontend` +/// method. It also notifies the frontend of a terminal error via the `error` +/// channel. + pub struct ConnectionInterface { /// Inbound channel from frontend. pub(crate) from_frontend: mpsc::UnboundedReceiver>, @@ -64,4 +77,47 @@ pub struct ConnectionInterface { /// Causes local shutdown when sender is triggered or dropped. pub(crate) shutdown: oneshot::Receiver<()>, + + /// True when the shutdown command has been received + dead: bool, +} + +impl ConnectionInterface { + /// Send a pubsub item to the frontend. + pub fn send_to_frontend( + &self, + item: PubSubItem, + ) -> Result<(), mpsc::error::SendError> { + self.to_frontend.send(item) + } + + /// Receive a request from the frontend. + pub async fn recv_from_frontend(&mut self) -> Option> { + self.from_frontend.recv().await + } + + /// Close the interface, sending an error to the frontend. + pub fn close_with_error(self) { + let _ = self.error.send(()); + } +} + +impl Stream for ConnectionInterface { + type Item = Box; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + if self.dead { + return Poll::Ready(None); + } + + if let Poll::Ready(_) = self.shutdown.poll_unpin(cx) { + self.dead = true; + return Poll::Ready(None); + } + + self.from_frontend.poll_recv(cx) + } } diff --git a/crates/transports/src/pubsub/ix.rs b/crates/pubsub/src/ix.rs similarity index 95% rename from crates/transports/src/pubsub/ix.rs rename to crates/pubsub/src/ix.rs index 88b2c96a401..530934c32f4 100644 --- a/crates/transports/src/pubsub/ix.rs +++ b/crates/pubsub/src/ix.rs @@ -2,7 +2,7 @@ use alloy_primitives::U256; use serde_json::value::RawValue; use tokio::sync::{broadcast, oneshot}; -use crate::pubsub::managers::InFlight; +use crate::managers::InFlight; /// Instructions for the pubsub service. pub enum PubSubInstruction { diff --git a/crates/transports/src/pubsub/mod.rs b/crates/pubsub/src/lib.rs similarity index 100% rename from crates/transports/src/pubsub/mod.rs rename to crates/pubsub/src/lib.rs diff --git a/crates/transports/src/pubsub/managers/active_sub.rs b/crates/pubsub/src/managers/active_sub.rs similarity index 100% rename from crates/transports/src/pubsub/managers/active_sub.rs rename to crates/pubsub/src/managers/active_sub.rs diff --git a/crates/transports/src/pubsub/managers/in_flight.rs b/crates/pubsub/src/managers/in_flight.rs similarity index 98% rename from crates/transports/src/pubsub/managers/in_flight.rs rename to crates/pubsub/src/managers/in_flight.rs index 98ee431e60b..0021b76f702 100644 --- a/crates/transports/src/pubsub/managers/in_flight.rs +++ b/crates/pubsub/src/managers/in_flight.rs @@ -2,7 +2,7 @@ use alloy_json_rpc::{Response, ResponsePayload, SerializedRequest}; use alloy_primitives::U256; use tokio::sync::oneshot; -use crate::TransportError; +use alloy_transport::TransportError; /// An in-flight JSON-RPC request. /// diff --git a/crates/transports/src/pubsub/managers/mod.rs b/crates/pubsub/src/managers/mod.rs similarity index 100% rename from crates/transports/src/pubsub/managers/mod.rs rename to crates/pubsub/src/managers/mod.rs diff --git a/crates/transports/src/pubsub/managers/req.rs b/crates/pubsub/src/managers/req.rs similarity index 96% rename from crates/transports/src/pubsub/managers/req.rs rename to crates/pubsub/src/managers/req.rs index e39fba86f2f..3d460f280da 100644 --- a/crates/transports/src/pubsub/managers/req.rs +++ b/crates/pubsub/src/managers/req.rs @@ -2,7 +2,7 @@ use alloy_json_rpc::{Id, Response}; use alloy_primitives::U256; use std::collections::BTreeMap; -use crate::pubsub::managers::InFlight; +use crate::managers::InFlight; /// Manages in-flight requests. #[derive(Debug, Default)] diff --git a/crates/transports/src/pubsub/managers/sub.rs b/crates/pubsub/src/managers/sub.rs similarity index 98% rename from crates/transports/src/pubsub/managers/sub.rs rename to crates/pubsub/src/managers/sub.rs index 62ee79ca97a..bd6a6d0485b 100644 --- a/crates/transports/src/pubsub/managers/sub.rs +++ b/crates/pubsub/src/managers/sub.rs @@ -4,7 +4,7 @@ use bimap::BiBTreeMap; use serde_json::value::RawValue; use tokio::sync::broadcast; -use crate::pubsub::managers::ActiveSubscription; +use crate::managers::ActiveSubscription; #[derive(Default, Debug)] pub(crate) struct SubscriptionManager { diff --git a/crates/transports/src/pubsub/service.rs b/crates/pubsub/src/service.rs similarity index 97% rename from crates/transports/src/pubsub/service.rs rename to crates/pubsub/src/service.rs index 02e4a2a6b74..2ff1b6f5a6d 100644 --- a/crates/transports/src/pubsub/service.rs +++ b/crates/pubsub/src/service.rs @@ -1,20 +1,20 @@ +use crate::{ + handle::ConnectionHandle, + ix::PubSubInstruction, + managers::{InFlight, RequestManager, SubscriptionManager}, + PubSubConnect, PubSubFrontend, +}; + use alloy_json_rpc::{Id, PubSubItem, Request, RequestMeta, Response, ResponsePayload}; use alloy_primitives::U256; use serde_json::value::RawValue; use tokio::sync::{broadcast, mpsc, oneshot}; -use crate::{ - pubsub::{ - handle::ConnectionHandle, - ix::PubSubInstruction, - managers::{InFlight, RequestManager, SubscriptionManager}, - }, +use alloy_transport::{ utils::{to_json_raw_value, Spawnable}, TransportError, }; -use super::{PubSubConnect, PubSubFrontend}; - #[derive(Debug)] /// The service contains the backend handle, a subscription manager, and the /// configuration details required to reconnect. diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml new file mode 100644 index 00000000000..1dfbe48f1da --- /dev/null +++ b/crates/rpc-client/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "alloy-rpc-client" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +alloy-json-rpc.workspace = true +alloy-transport.workspace = true +alloy-transport-http.workspace = true + +alloy-primitives.workspace = true + +futures.workspace = true +pin-project.workspace = true +serde_json.workspace = true +tracing.workspace = true +tokio.workspace = true +tower.workspace = true + +reqwest = { workspace = true, optional = true } +alloy-pubsub = { workspace = true, optional = true } +alloy-transport-ws = { workspace = true, optional = true } + +[dev-dependencies] +alloy-transport-ws.workspace = true +test-log = { version = "0.2.13", default-features = false, features = ["trace"] } +tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } + +[features] +default = ["reqwest", "ws"] +reqwest = ["dep:reqwest"] +pubsub = ["dep:alloy-pubsub"] +ws = ["pubsub", "dep:alloy-transport-ws"] \ No newline at end of file diff --git a/crates/transports/src/batch.rs b/crates/rpc-client/src/batch.rs similarity index 98% rename from crates/transports/src/batch.rs rename to crates/rpc-client/src/batch.rs index 45a237f7194..eca52b03e91 100644 --- a/crates/transports/src/batch.rs +++ b/crates/rpc-client/src/batch.rs @@ -1,3 +1,11 @@ +use crate::RpcClient; + +use alloy_json_rpc::{ + Id, Request, RequestPacket, ResponsePacket, RpcParam, RpcResult, RpcReturn, SerializedRequest, +}; +use alloy_transport::{Transport, TransportError}; +use futures::channel::oneshot; +use serde_json::value::RawValue; use std::{ borrow::Cow, collections::HashMap, @@ -7,14 +15,6 @@ use std::{ task::{self, ready, Poll}, }; -use futures_channel::oneshot; -use serde_json::value::RawValue; - -use crate::{error::TransportError, transports::Transport, RpcClient}; -use alloy_json_rpc::{ - Id, Request, RequestPacket, ResponsePacket, RpcParam, RpcResult, RpcReturn, SerializedRequest, -}; - pub(crate) type Channel = oneshot::Sender, Box, TransportError>>; pub(crate) type ChannelMap = HashMap; diff --git a/crates/rpc-client/src/builder.rs b/crates/rpc-client/src/builder.rs new file mode 100644 index 00000000000..67f7c88e5be --- /dev/null +++ b/crates/rpc-client/src/builder.rs @@ -0,0 +1,116 @@ +use crate::RpcClient; + +use alloy_transport::{ + BoxTransport, BoxTransportConnect, Transport, TransportConnect, TransportError, +}; +use tower::{ + layer::util::{Identity, Stack}, + Layer, ServiceBuilder, +}; + +/// A builder for the transport [`RpcClient`]. +/// +/// This is a wrapper around [`tower::ServiceBuilder`]. It allows you to +/// configure middleware layers that will be applied to the transport, and has +/// some shortcuts for common layers and transports. +/// +/// A builder accumulates Layers, and then is finished via the +/// [`ClientBuilder::connect`] method, which produces an RPC client. +pub struct ClientBuilder { + pub(crate) builder: ServiceBuilder, +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self { + builder: ServiceBuilder::new(), + } + } +} + +impl ClientBuilder { + /// Add a middleware layer to the stack. + /// + /// This is a wrapper around [`tower::ServiceBuilder::layer`]. Layers that + /// are added first will be called with the request first. + pub fn layer(self, layer: M) -> ClientBuilder> { + ClientBuilder { + builder: self.builder.layer(layer), + } + } + + /// Create a new [`RpcClient`] with the given transport and the configured + /// layers. + fn transport(self, transport: T, is_local: bool) -> RpcClient + where + L: Layer, + T: Transport, + L::Service: Transport, + { + RpcClient::new(self.builder.service(transport), is_local) + } + + /// Convenience function to create a new [`RpcClient`] with a [`reqwest`] + /// HTTP transport. + #[cfg(feature = "reqwest")] + pub fn reqwest_http(self, url: reqwest::Url) -> RpcClient + where + L: Layer>, + L::Service: Transport, + { + let transport = alloy_transport_http::Http::new(url); + let is_local = transport.guess_local(); + + self.transport(transport, is_local) + } + + /// Convenience function to create a new [`RpcClient`] with a [`hyper`] + /// HTTP transport. + #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] + pub fn hyper_http(self, url: url::Url) -> RpcClient + where + L: Layer>>, + L::Service: Transport, + { + let transport = alloy_transport_http::Http::new(url); + let is_local = transport.guess_local(); + + self.transport(transport, is_local) + } + + #[cfg(feature = "pubsub")] + pub async fn pubsub(self, pubsub_connect: C) -> Result, TransportError> + where + C: alloy_pubsub::PubSubConnect, + L: Layer, + L::Service: Transport, + { + let is_local = pubsub_connect.is_local(); + let transport = pubsub_connect.into_service().await?; + Ok(self.transport(transport, is_local)) + } + + /// Connect a transport, producing an [`RpcClient`] with the provided + /// connection. + pub async fn connect(self, connect: C) -> Result, TransportError> + where + C: TransportConnect, + L: Layer, + L::Service: Transport, + { + let transport = connect.get_transport().await?; + Ok(self.transport(transport, connect.is_local())) + } + + /// Connect a transport, producing an [`RpcClient`] with a [`BoxTransport`] + /// connection. + pub async fn connect_boxed(self, connect: C) -> Result, TransportError> + where + C: BoxTransportConnect, + L: Layer, + L::Service: Transport, + { + let transport = connect.get_boxed_transport().await?; + Ok(self.transport(transport, connect.is_local())) + } +} diff --git a/crates/transports/src/call.rs b/crates/rpc-client/src/call.rs similarity index 98% rename from crates/transports/src/call.rs rename to crates/rpc-client/src/call.rs index a1f91a92a05..2d8c7e44718 100644 --- a/crates/transports/src/call.rs +++ b/crates/rpc-client/src/call.rs @@ -1,6 +1,5 @@ -use crate::{error::TransportError, transports::Transport, RpcFut}; - use alloy_json_rpc::{Request, RequestPacket, ResponsePacket, RpcParam, RpcResult, RpcReturn}; +use alloy_transport::{RpcFut, Transport, TransportError}; use core::panic; use serde_json::value::RawValue; use std::{ diff --git a/crates/transports/src/client.rs b/crates/rpc-client/src/client.rs similarity index 53% rename from crates/transports/src/client.rs rename to crates/rpc-client/src/client.rs index ebcd79471d8..fbb7277537b 100644 --- a/crates/transports/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -1,20 +1,13 @@ +use crate::{BatchRequest, ClientBuilder, RpcCall}; + use alloy_json_rpc::{Id, Request, RequestMeta, RpcParam, RpcReturn}; -use alloy_primitives::U256; -use serde_json::value::RawValue; +use alloy_transport::{BoxTransport, Transport, TransportConnect, TransportError}; +use alloy_transport_http::Http; use std::{ borrow::Cow, sync::atomic::{AtomicU64, Ordering}, }; -use tokio::sync::broadcast; -use tower::{ - layer::util::{Identity, Stack}, - Layer, ServiceBuilder, -}; - -use crate::{ - pubsub::PubSubFrontend, BatchRequest, BoxTransport, BoxTransportConnect, Http, RpcCall, - Transport, TransportConnect, TransportError, -}; +use tower::{layer::util::Identity, ServiceBuilder}; /// A JSON-RPC client. /// @@ -57,13 +50,12 @@ impl RpcClient { } /// Connect to a transport via a [`TransportConnect`] implementor. - pub async fn connect>( - connect: C, - ) -> Result + pub async fn connect(connect: C) -> Result where - C: Transport, + T: Transport, + C: TransportConnect, { - connect.connect().await + ClientBuilder::default().connect(connect).await } /// Build a `JsonRpcRequest` with the given method and params. @@ -155,10 +147,20 @@ where } } -impl RpcClient { - /// Get a [`broadcast::Receiver`] for the given subscription ID. - pub async fn get_watcher(&self, id: U256) -> broadcast::Receiver> { - self.transport.get_subscription(id).await.unwrap() +#[cfg(feature = "pubsub")] +mod pubsub_impl { + use super::*; + use alloy_pubsub::PubSubFrontend; + use tokio::sync::broadcast; + + impl RpcClient { + /// Get a [`broadcast::Receiver`] for the given subscription ID. + pub async fn get_watcher( + &self, + id: alloy_primitives::U256, + ) -> broadcast::Receiver> { + self.transport.get_subscription(id).await.unwrap() + } } } @@ -169,98 +171,3 @@ impl RpcClient> { BatchRequest::new(self) } } - -/// A builder for the transport [`RpcClient`]. -/// -/// This is a wrapper around [`tower::ServiceBuilder`]. It allows you to -/// configure middleware layers that will be applied to the transport, and has -/// some shortcuts for common layers and transports. -/// -/// A builder accumulates Layers, and then is finished via the -/// [`ClientBuilder::connect`] method, which produces an RPC client. -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. - /// - /// This is a wrapper around [`tower::ServiceBuilder::layer`]. Layers that - /// are added first will be called with the request first. - pub fn layer(self, layer: M) -> ClientBuilder> { - ClientBuilder { - builder: self.builder.layer(layer), - } - } - - /// Create a new [`RpcClient`] with the given transport and the configured - /// layers. - fn transport(self, transport: T, is_local: bool) -> RpcClient - where - L: Layer, - T: Transport, - L::Service: Transport, - { - RpcClient::new(self.builder.service(transport), is_local) - } - - /// Convenience function to create a new [`RpcClient`] with a [`reqwest`] - /// HTTP transport. - #[cfg(feature = "reqwest")] - pub fn reqwest_http(self, url: reqwest::Url) -> RpcClient - where - L: Layer>, - L::Service: Transport, - { - let transport = crate::Http::new(url); - let is_local = transport.guess_local(); - - self.transport(transport, is_local) - } - - /// Convenience function to create a new [`RpcClient`] with a [`hyper`] - /// HTTP transport. - #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] - pub fn hyper_http(self, url: url::Url) -> RpcClient - where - L: Layer>>, - L::Service: Transport, - { - let transport = crate::Http::new(url); - let is_local = transport.guess_local(); - - self.transport(transport, is_local) - } - - /// Connect a transport, producing an [`RpcClient`] with the provided - /// connection. - pub async fn connect(self, connect: C) -> Result, TransportError> - where - C: TransportConnect, - L: Layer, - L::Service: Transport, - { - let transport = connect.get_transport().await?; - Ok(self.transport(transport, connect.is_local())) - } - - /// Connect a transport, producing an [`RpcClient`] with a [`BoxTransport`] - /// connection. - pub async fn connect_boxed(self, connect: C) -> Result, TransportError> - where - C: BoxTransportConnect, - L: Layer, - L::Service: Transport, - { - let transport = connect.get_boxed_transport().await?; - Ok(self.transport(transport, connect.is_local())) - } -} diff --git a/crates/rpc-client/src/lib.rs b/crates/rpc-client/src/lib.rs new file mode 100644 index 00000000000..454aadfe847 --- /dev/null +++ b/crates/rpc-client/src/lib.rs @@ -0,0 +1,11 @@ +mod batch; +pub use batch::BatchRequest; + +mod builder; +pub use builder::ClientBuilder; + +mod call; +pub use call::RpcCall; + +mod client; +pub use client::RpcClient; diff --git a/crates/transports/tests/it/http.rs b/crates/rpc-client/tests/it/http.rs similarity index 89% rename from crates/transports/tests/it/http.rs rename to crates/rpc-client/tests/it/http.rs index 14d9b03b93f..ee14596d6e4 100644 --- a/crates/transports/tests/it/http.rs +++ b/crates/rpc-client/tests/it/http.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use alloy_primitives::U64; -use alloy_transports::{ClientBuilder, RpcCall}; +use alloy_rpc_client::{ClientBuilder, RpcCall}; #[tokio::test] async fn it_makes_a_request() { diff --git a/crates/rpc-client/tests/it/main.rs b/crates/rpc-client/tests/it/main.rs new file mode 100644 index 00000000000..a293f4fe0c0 --- /dev/null +++ b/crates/rpc-client/tests/it/main.rs @@ -0,0 +1,4 @@ +mod http; + +#[cfg(feature = "pubsub")] +mod ws; diff --git a/crates/transports/tests/it/ws.rs b/crates/rpc-client/tests/it/ws.rs similarity index 77% rename from crates/transports/tests/it/ws.rs rename to crates/rpc-client/tests/it/ws.rs index 8d82b326bec..c8d7f017fcb 100644 --- a/crates/transports/tests/it/ws.rs +++ b/crates/rpc-client/tests/it/ws.rs @@ -1,4 +1,5 @@ -use alloy_transports::{ClientBuilder, RpcCall, WsConnect}; +use alloy_rpc_client::{ClientBuilder, RpcCall}; +use alloy_transport_ws::WsConnect; use alloy_primitives::U64; use std::borrow::Cow; @@ -12,7 +13,7 @@ async fn it_makes_a_request() { auth: None, }; - let client = ClientBuilder::default().connect(connector).await.unwrap(); + let client = ClientBuilder::default().pubsub(connector).await.unwrap(); let params: Cow<'static, _> = Cow::Owned(()); diff --git a/crates/transport-http/Cargo.toml b/crates/transport-http/Cargo.toml new file mode 100644 index 00000000000..a6c9411a276 --- /dev/null +++ b/crates/transport-http/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "alloy-transport-http" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +alloy-json-rpc.workspace = true +alloy-transport.workspace = true + +url.workspace = true +serde_json.workspace = true +tower.workspace = true + +reqwest = { workspace = true, features = ["serde_json", "json"], optional = true } + + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.hyper] +version = "0.14.27" +optional = true +features = ["full"] + +[features] +default = ["reqwest"] +reqwest = ["dep:reqwest"] +hyper = ["dep:hyper"] \ No newline at end of file diff --git a/crates/transports/src/transports/http/backend.rs b/crates/transport-http/src/backend.rs similarity index 100% rename from crates/transports/src/transports/http/backend.rs rename to crates/transport-http/src/backend.rs diff --git a/crates/transports/src/transports/http/hyper.rs b/crates/transport-http/src/hyper.rs similarity index 100% rename from crates/transports/src/transports/http/hyper.rs rename to crates/transport-http/src/hyper.rs diff --git a/crates/transports/src/transports/http/mod.rs b/crates/transport-http/src/lib.rs similarity index 72% rename from crates/transports/src/transports/http/mod.rs rename to crates/transport-http/src/lib.rs index 3ddf4b4ffce..6e611db3c8e 100644 --- a/crates/transports/src/transports/http/mod.rs +++ b/crates/transport-http/src/lib.rs @@ -4,9 +4,7 @@ mod hyper; #[cfg(feature = "reqwest")] mod reqwest; -use crate::{client::RpcClient, utils::guess_local_url}; - -use std::{str::FromStr, sync::atomic::AtomicU64}; +use alloy_transport::utils::guess_local_url; use url::Url; /// An Http transport. @@ -70,30 +68,3 @@ impl Http { self.url.as_ref() } } - -impl RpcClient> -where - T: Default, -{ - /// Create a new [`RpcClient`] from a URL. - pub fn new_http(url: Url) -> Self { - let transport = Http::new(url); - let is_local = transport.guess_local(); - Self { - transport, - is_local, - id: AtomicU64::new(0), - } - } -} - -impl FromStr for RpcClient> -where - T: Default, -{ - type Err = ::Err; - - fn from_str(s: &str) -> Result { - s.parse().map(Self::new_http) - } -} diff --git a/crates/transports/src/transports/http/reqwest.rs b/crates/transport-http/src/reqwest.rs similarity index 78% rename from crates/transports/src/transports/http/reqwest.rs rename to crates/transport-http/src/reqwest.rs index 07d534c15c1..53ce5dd39e1 100644 --- a/crates/transports/src/transports/http/reqwest.rs +++ b/crates/transport-http/src/reqwest.rs @@ -1,16 +1,23 @@ use alloy_json_rpc::{RequestPacket, ResponsePacket}; +use alloy_transport::{TransportError, TransportFut}; use std::task; use tower::Service; -use crate::{Http, TransportError, TransportFut}; +use crate::Http; impl Http { /// Make a request. fn request(&self, req: RequestPacket) -> TransportFut<'static> { let this = self.clone(); Box::pin(async move { - let resp = this.client.post(this.url).json(&req).send().await?; - let json = resp.text().await?; + let resp = this + .client + .post(this.url) + .json(&req) + .send() + .await + .map_err(TransportError::custom)?; + let json = resp.text().await.map_err(TransportError::custom)?; serde_json::from_str(&json).map_err(|err| TransportError::deser_err(err, &json)) }) diff --git a/crates/transport-ws/Cargo.toml b/crates/transport-ws/Cargo.toml new file mode 100644 index 00000000000..b458edfddac --- /dev/null +++ b/crates/transport-ws/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "alloy-transport-ws" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +alloy-json-rpc.workspace = true +alloy-pubsub.workspace = true +alloy-transport.workspace = true + +futures.workspace = true +http = "0.2.9" +serde_json.workspace = true +tracing.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] +version = "1.33.0" +features = ["sync", "rt"] + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4.37" +ws_stream_wasm = "0.7.4" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"]} + diff --git a/crates/transports/src/transports/ws/mod.rs b/crates/transport-ws/src/lib.rs similarity index 92% rename from crates/transports/src/transports/ws/mod.rs rename to crates/transport-ws/src/lib.rs index c29fb0ea0d4..a2940584db8 100644 --- a/crates/transports/src/transports/ws/mod.rs +++ b/crates/transport-ws/src/lib.rs @@ -8,7 +8,7 @@ mod wasm; #[cfg(target_arch = "wasm32")] pub use wasm::WsConnect; -use crate::pubsub::ConnectionInterface; +use alloy_pubsub::ConnectionInterface; use tracing::{debug, error, trace}; @@ -32,7 +32,7 @@ impl WsBackend { match serde_json::from_str(&t) { Ok(item) => { trace!(?item, "Deserialized message"); - let res = self.interface.to_frontend.send(item); + let res = self.interface.send_to_frontend(item); if res.is_err() { error!("Failed to send message to handler"); return Err(()); diff --git a/crates/transports/src/transports/ws/native.rs b/crates/transport-ws/src/native.rs similarity index 87% rename from crates/transports/src/transports/ws/native.rs rename to crates/transport-ws/src/native.rs index 3f714a975c8..742c28114b5 100644 --- a/crates/transports/src/transports/ws/native.rs +++ b/crates/transport-ws/src/native.rs @@ -1,4 +1,7 @@ -use crate::{pubsub::PubSubConnect, utils::Spawnable, TransportError}; +use crate::WsBackend; + +use alloy_pubsub::PubSubConnect; +use alloy_transport::{utils::Spawnable, Authorization, TransportError}; use futures::{SinkExt, StreamExt}; use serde_json::value::RawValue; @@ -10,8 +13,6 @@ use tokio_tungstenite::{ }; use tracing::error; -use super::WsBackend; - type TungsteniteStream = WebSocketStream>; const KEEPALIVE: u64 = 10; @@ -61,15 +62,10 @@ impl WsBackend { // probably not a big deal. tokio::select! { biased; - // break on shutdown recv, or on shutdown recv error - _ = &mut self.interface.shutdown => { - self.interface.from_frontend.close(); - break - }, // we've received a new dispatch, so we send it via // websocket. We handle new work before processing any // responses from the server. - inst = self.interface.from_frontend.recv() => { + inst = self.interface.recv_from_frontend() => { match inst { Some(msg) => { // Reset the keepalive timer. @@ -80,7 +76,7 @@ impl WsBackend { break } }, - // dispatcher has gone away + // dispatcher has gone away, or shutdown was received None => { break }, @@ -118,7 +114,7 @@ impl WsBackend { } } if err { - let _ = self.interface.error.send(()); + let _ = self.interface.close_with_error(); } }; fut.spawn_task() @@ -128,7 +124,7 @@ impl WsBackend { #[derive(Debug, Clone)] pub struct WsConnect { pub url: String, - pub auth: Option, + pub auth: Option, } impl IntoClientRequest for WsConnect { @@ -149,24 +145,25 @@ impl IntoClientRequest for WsConnect { impl PubSubConnect for WsConnect { fn is_local(&self) -> bool { - crate::utils::guess_local_url(&self.url) + alloy_transport::utils::guess_local_url(&self.url) } fn connect<'a: 'b, 'b>( &'a self, ) -> Pin< Box< - dyn Future> - + Send - + 'b, + dyn Future> + Send + 'b, >, > { let request = self.clone().into_client_request(); Box::pin(async move { - let (socket, _) = tokio_tungstenite::connect_async(request?).await?; + let req = request.map_err(TransportError::custom)?; + let (socket, _) = tokio_tungstenite::connect_async(req) + .await + .map_err(TransportError::custom)?; - let (handle, interface) = crate::pubsub::ConnectionHandle::new(); + let (handle, interface) = alloy_pubsub::ConnectionHandle::new(); let backend = WsBackend { socket, interface }; backend.spawn(); diff --git a/crates/transports/src/transports/ws/wasm.rs b/crates/transport-ws/src/wasm.rs similarity index 100% rename from crates/transports/src/transports/ws/wasm.rs rename to crates/transport-ws/src/wasm.rs diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml new file mode 100644 index 00000000000..835cac9d7e4 --- /dev/null +++ b/crates/transport/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "alloy-transport" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +alloy-json-rpc.workspace = true + +alloy-primitives.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["raw_value"] } +thiserror.workspace = true +url.workspace = true +tokio.workspace = true +tower.workspace = true +tracing.workspace = true + +base64 = "0.21.0" + diff --git a/crates/transports/README.md b/crates/transport/README.md similarity index 100% rename from crates/transports/README.md rename to crates/transport/README.md diff --git a/crates/transports/src/transports/boxed.rs b/crates/transport/src/boxed.rs similarity index 100% rename from crates/transports/src/transports/boxed.rs rename to crates/transport/src/boxed.rs diff --git a/crates/transports/src/common.rs b/crates/transport/src/common.rs similarity index 100% rename from crates/transports/src/common.rs rename to crates/transport/src/common.rs diff --git a/crates/transports/src/transports/connect.rs b/crates/transport/src/connect.rs similarity index 72% rename from crates/transports/src/transports/connect.rs rename to crates/transport/src/connect.rs index 64652fa66d3..9813b0675ca 100644 --- a/crates/transports/src/transports/connect.rs +++ b/crates/transport/src/connect.rs @@ -1,4 +1,4 @@ -use crate::{BoxTransport, Pbf, RpcClient, Transport, TransportError}; +use crate::{BoxTransport, Pbf, Transport, TransportError}; /// Connection details for a transport. /// @@ -22,15 +22,6 @@ pub trait TransportConnect: Sized + Send + Sync + 'static { /// Connect to the transport, returning a `Transport` instance. fn get_transport<'a: 'b, 'b>(&self) -> Pbf<'b, Self::Transport, TransportError>; - - /// Connect to the transport, wrapping it into a `RpcClient` instance. - fn connect<'a: 'b, 'b>(&'a self) -> Pbf<'b, RpcClient, TransportError> { - Box::pin(async move { - self.get_transport() - .await - .map(|t| RpcClient::new(t, self.is_local())) - }) - } } /// Connection details for a transport that can be boxed. @@ -49,9 +40,6 @@ pub trait BoxTransportConnect { /// Connect to a transport, and box it. fn get_boxed_transport<'a: 'b, 'b>(&'a self) -> Pbf<'b, BoxTransport, TransportError>; - - /// Connect to a transport, and box it, wrapping it into a `RpcClient`. - fn connect_boxed<'a: 'b, 'b>(&'a self) -> Pbf<'b, RpcClient, TransportError>; } impl BoxTransportConnect for T @@ -65,14 +53,6 @@ where fn get_boxed_transport<'a: 'b, 'b>(&'a self) -> Pbf<'b, BoxTransport, TransportError> { Box::pin(async move { self.get_transport().await.map(Transport::boxed) }) } - - fn connect_boxed<'a: 'b, 'b>(&'a self) -> Pbf<'b, RpcClient, TransportError> { - Box::pin(async move { - self.get_boxed_transport() - .await - .map(|boxed| RpcClient::new(boxed, self.is_local())) - }) - } } #[cfg(test)] diff --git a/crates/transports/src/error.rs b/crates/transport/src/error.rs similarity index 78% rename from crates/transports/src/error.rs rename to crates/transport/src/error.rs index cb4a4d52bc7..64685af9259 100644 --- a/crates/transports/src/error.rs +++ b/crates/transport/src/error.rs @@ -18,21 +18,6 @@ pub enum TransportError { #[error("Missing response in batch request")] MissingBatchResponse, - /// Reqwest http transport. - #[error(transparent)] - #[cfg(feature = "reqwest")] - Reqwest(#[from] reqwest::Error), - - /// Hyper http transport. - #[error(transparent)] - #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] - Hyper(#[from] hyper::Error), - - /// Tungstenite websocket transport. - #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Tungstenite(#[from] tokio_tungstenite::tungstenite::Error), - /// PubSub backend connection task has stopped. #[error("PubSub backend connection task has stopped.")] BackendGone, diff --git a/crates/transports/src/lib.rs b/crates/transport/src/lib.rs similarity index 90% rename from crates/transports/src/lib.rs rename to crates/transport/src/lib.rs index 5a35f87ef8c..c9373b5ff2a 100644 --- a/crates/transports/src/lib.rs +++ b/crates/transport/src/lib.rs @@ -39,33 +39,24 @@ //! - [`PubSubFrontend`]: The **frontend**. A handle to a running PubSub //! **service**. It is used to issue requests and subscription lifecycle //! instructions to the **service**. +mod boxed; +pub use boxed::BoxTransport; -mod batch; -pub use batch::BatchRequest; - -mod call; -pub use call::RpcCall; +mod connect; +pub use connect::{BoxTransportConnect, TransportConnect}; mod common; pub use common::Authorization; -mod client; -pub use client::{ClientBuilder, RpcClient}; - mod error; pub use error::TransportError; -mod pubsub; -pub use pubsub::{ConnectionHandle, ConnectionInterface, PubSubConnect, PubSubFrontend}; - -mod transports; -pub use transports::{ - BoxTransport, BoxTransportConnect, Http, Transport, TransportConnect, WsBackend, WsConnect, -}; +mod r#trait; +pub use r#trait::Transport; pub use alloy_json_rpc::RpcResult; -pub(crate) mod utils; +pub mod utils; pub use type_aliases::*; diff --git a/crates/transports/src/transports/trait.rs b/crates/transport/src/trait.rs similarity index 100% rename from crates/transports/src/transports/trait.rs rename to crates/transport/src/trait.rs diff --git a/crates/transports/src/utils.rs b/crates/transport/src/utils.rs similarity index 89% rename from crates/transports/src/utils.rs rename to crates/transport/src/utils.rs index b9f80a261ee..d05378f0f18 100644 --- a/crates/transports/src/utils.rs +++ b/crates/transport/src/utils.rs @@ -11,7 +11,7 @@ use crate::error::TransportError; /// 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(crate) fn guess_local_url(s: impl AsRef) -> bool { +pub fn guess_local_url(s: impl AsRef) -> bool { fn _guess_local_url(url: &str) -> bool { if let Ok(url) = url.parse::() { url.host_str() @@ -25,7 +25,7 @@ pub(crate) fn guess_local_url(s: impl AsRef) -> bool { /// Convert to a `Box` from a `Serialize` type, mapping the error /// to a `TransportError`. -pub(crate) fn to_json_raw_value(s: &S) -> Result, TransportError> +pub fn to_json_raw_value(s: &S) -> Result, TransportError> where S: Serialize, { @@ -33,7 +33,8 @@ where .map_err(TransportError::ser_err) } -pub(crate) trait Spawnable { +#[doc(hidden)] +pub trait Spawnable { /// Spawn the future as a task. /// /// In WASM this will be a `wasm-bindgen-futures::spawn_local` call, while diff --git a/crates/transports/Cargo.toml b/crates/transports/Cargo.toml deleted file mode 100644 index 0784c8c2583..00000000000 --- a/crates/transports/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "alloy-transports" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -exclude.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -alloy-json-rpc.workspace = true - -base64 = "0.21.0" - -futures-channel.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, features = ["raw_value"] } -thiserror.workspace = true - -url.workspace = true -tower.workspace = true -pin-project.workspace = true - -# feature deps -reqwest = { version = "0.11.18", features = ["serde_json", "json"], optional = true } -tokio = { version = "1.33.0", features = ["sync", "macros"] } -alloy-primitives.workspace = true -bimap = "0.6.3" -tracing = "0.1.40" -futures = "0.3.29" -http = "0.2.9" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.hyper] -version = "0.14.27" -optional = true -features = ["full"] - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] -version = "1.33.0" -features = ["sync", "rt"] - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-futures = "0.4.37" -ws_stream_wasm = "0.7.4" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"]} - - -[features] -default = ["reqwest", "hyper"] -reqwest = ["dep:reqwest"] -hyper = ["dep:hyper", "hyper/client"] - -[dev-dependencies] -test-log = { version = "0.2.13", default-features = false, features = ["trace"] } -tracing-subscriber = "0.3.17" -tracing-test = "0.2.4" diff --git a/crates/transports/src/transports/mod.rs b/crates/transports/src/transports/mod.rs deleted file mode 100644 index 7705e2a2117..00000000000 --- a/crates/transports/src/transports/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod boxed; -pub use boxed::BoxTransport; - -mod connect; -pub use connect::{BoxTransportConnect, TransportConnect}; - -mod http; -pub use self::http::Http; - -mod r#trait; -pub use r#trait::Transport; - -mod ws; -pub use ws::{WsBackend, WsConnect}; diff --git a/crates/transports/tests/it/main.rs b/crates/transports/tests/it/main.rs deleted file mode 100644 index d4904218868..00000000000 --- a/crates/transports/tests/it/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod http; -mod ws; From d07764a95777db9a76fd132c55ec39373f132627 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:05:35 -0800 Subject: [PATCH 14/30] fix: turn ws off by default --- crates/rpc-client/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 1dfbe48f1da..7067dab1c2e 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -35,7 +35,7 @@ test-log = { version = "0.2.13", default-features = false, features = ["trace"] tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } [features] -default = ["reqwest", "ws"] +default = ["reqwest"] reqwest = ["dep:reqwest"] pubsub = ["dep:alloy-pubsub"] ws = ["pubsub", "dep:alloy-transport-ws"] \ No newline at end of file From 61f311e79314b01cce8d9cc7c27c474c430582cc Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:05:52 -0800 Subject: [PATCH 15/30] nit: temporarily comment out tests --- crates/rpc-client/tests/it/http.rs | 2 +- crates/rpc-client/tests/it/ws.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rpc-client/tests/it/http.rs b/crates/rpc-client/tests/it/http.rs index ee14596d6e4..ccf026da0c4 100644 --- a/crates/rpc-client/tests/it/http.rs +++ b/crates/rpc-client/tests/it/http.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use alloy_primitives::U64; use alloy_rpc_client::{ClientBuilder, RpcCall}; -#[tokio::test] +// #[tokio::test] async fn it_makes_a_request() { let infura = std::env::var("HTTP_PROVIDER_URL").unwrap(); diff --git a/crates/rpc-client/tests/it/ws.rs b/crates/rpc-client/tests/it/ws.rs index c8d7f017fcb..035b74ce0c5 100644 --- a/crates/rpc-client/tests/it/ws.rs +++ b/crates/rpc-client/tests/it/ws.rs @@ -4,7 +4,7 @@ use alloy_transport_ws::WsConnect; use alloy_primitives::U64; use std::borrow::Cow; -#[test_log::test(tokio::test)] +// #[test_log::test(tokio::test)] async fn it_makes_a_request() { let infura = std::env::var("WS_PROVIDER_URL").unwrap(); From 2e47e93bcb1b5e35fb175c4b7b85d8a0e47ada45 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:06:39 -0800 Subject: [PATCH 16/30] nits: clippy --- crates/pubsub/src/handle.rs | 2 +- crates/rpc-client/tests/it/main.rs | 2 ++ crates/transport-ws/src/native.rs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/pubsub/src/handle.rs b/crates/pubsub/src/handle.rs index c183215e5c0..76a5426c3d7 100644 --- a/crates/pubsub/src/handle.rs +++ b/crates/pubsub/src/handle.rs @@ -113,7 +113,7 @@ impl Stream for ConnectionInterface { return Poll::Ready(None); } - if let Poll::Ready(_) = self.shutdown.poll_unpin(cx) { + if self.shutdown.poll_unpin(cx).is_ready() { self.dead = true; return Poll::Ready(None); } diff --git a/crates/rpc-client/tests/it/main.rs b/crates/rpc-client/tests/it/main.rs index a293f4fe0c0..aaefe735be9 100644 --- a/crates/rpc-client/tests/it/main.rs +++ b/crates/rpc-client/tests/it/main.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + mod http; #[cfg(feature = "pubsub")] diff --git a/crates/transport-ws/src/native.rs b/crates/transport-ws/src/native.rs index 742c28114b5..db1ef11f881 100644 --- a/crates/transport-ws/src/native.rs +++ b/crates/transport-ws/src/native.rs @@ -114,7 +114,7 @@ impl WsBackend { } } if err { - let _ = self.interface.close_with_error(); + self.interface.close_with_error(); } }; fut.spawn_task() From 2d2d85e079260ebbbfc8d66b40b89e963d75388d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:07:41 -0800 Subject: [PATCH 17/30] fix: clippy all-features --- crates/transport-http/src/hyper.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/transport-http/src/hyper.rs b/crates/transport-http/src/hyper.rs index 8f3f2528686..2951ada018b 100644 --- a/crates/transport-http/src/hyper.rs +++ b/crates/transport-http/src/hyper.rs @@ -1,9 +1,10 @@ use alloy_json_rpc::{RequestPacket, ResponsePacket}; +use alloy_transport::{TransportError, TransportFut}; use hyper::client::{connect::Connect, Client}; use std::task; use tower::Service; -use crate::{Http, TransportError, TransportFut}; +use crate::Http; impl Http> where @@ -23,10 +24,16 @@ where .body(hyper::Body::from(ser.get().to_owned())) .expect("request parts are valid"); - let resp = this.client.request(req).await?; + let resp = this + .client + .request(req) + .await + .map_err(TransportError::custom)?; // unpack json from the response body - let body = hyper::body::to_bytes(resp.into_body()).await?; + let body = hyper::body::to_bytes(resp.into_body()) + .await + .map_err(TransportError::custom)?; // Deser a Box from the body. If deser fails, return the // body as a string in the error. If the body is not UTF8, this will From acf091b12835edd2c1a5d3401a5e855fe39afa7e Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:15:21 -0800 Subject: [PATCH 18/30] fix: tests for provider --- crates/providers/src/provider.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index 470a6e3a72b..e63b4670727 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -544,12 +544,12 @@ mod providers_test { let anvil = Anvil::new().spawn(); let provider = Provider::new(&anvil.endpoint()).unwrap(); // Set the code - let addr = Address::with_last_byte(16); + let addr = alloy_primitives::Address::with_last_byte(16); provider.set_code(addr, "0xbeef").await.unwrap(); let _code = provider .get_code_at( addr, - BlockId::Number(alloy_rpc_types::BlockNumberOrTag::Latest), + crate::provider::BlockId::Number(alloy_rpc_types::BlockNumberOrTag::Latest), ) .await .unwrap(); From 21965976edafce3900a5cfef39edadd1a82675b0 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:20:44 -0800 Subject: [PATCH 19/30] chore: fix wasm --- crates/pubsub/src/handle.rs | 42 +++++++++++---------------------- crates/transport-ws/Cargo.toml | 3 ++- crates/transport-ws/src/wasm.rs | 11 +++------ crates/transport/Cargo.toml | 2 ++ 4 files changed, 21 insertions(+), 37 deletions(-) diff --git a/crates/pubsub/src/handle.rs b/crates/pubsub/src/handle.rs index 76a5426c3d7..284c3b48e52 100644 --- a/crates/pubsub/src/handle.rs +++ b/crates/pubsub/src/handle.rs @@ -1,9 +1,9 @@ -use std::task::Poll; - use alloy_json_rpc::PubSubItem; -use futures::{FutureExt, Stream}; use serde_json::value::RawValue; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::{ + mpsc, + oneshot::{self, error::TryRecvError}, +}; #[derive(Debug)] /// A handle to a backend. Communicates to a `ConnectionInterface` on the @@ -44,7 +44,6 @@ impl ConnectionHandle { to_frontend, error: error_tx, shutdown: shutdown_rx, - dead: false, }; (handle, interface) } @@ -77,9 +76,6 @@ pub struct ConnectionInterface { /// Causes local shutdown when sender is triggered or dropped. pub(crate) shutdown: oneshot::Receiver<()>, - - /// True when the shutdown command has been received - dead: bool, } impl ConnectionInterface { @@ -93,6 +89,16 @@ impl ConnectionInterface { /// Receive a request from the frontend. pub async fn recv_from_frontend(&mut self) -> Option> { + match self.shutdown.try_recv() { + Ok(_) => return None, + Err(TryRecvError::Closed) => return None, + Err(TryRecvError::Empty) => {} + } + + if self.shutdown.try_recv().is_ok() { + return None; + } + self.from_frontend.recv().await } @@ -101,23 +107,3 @@ impl ConnectionInterface { let _ = self.error.send(()); } } - -impl Stream for ConnectionInterface { - type Item = Box; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - if self.dead { - return Poll::Ready(None); - } - - if self.shutdown.poll_unpin(cx).is_ready() { - self.dead = true; - return Poll::Ready(None); - } - - self.from_frontend.poll_recv(cx) - } -} diff --git a/crates/transport-ws/Cargo.toml b/crates/transport-ws/Cargo.toml index b458edfddac..f381b7b36cb 100644 --- a/crates/transport-ws/Cargo.toml +++ b/crates/transport-ws/Cargo.toml @@ -19,14 +19,15 @@ alloy-transport.workspace = true futures.workspace = true http = "0.2.9" serde_json.workspace = true +tokio = { version = "1.33.0", features = ["sync", "rt"] } tracing.workspace = true + [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1.33.0" features = ["sync", "rt"] [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-futures = "0.4.37" ws_stream_wasm = "0.7.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/transport-ws/src/wasm.rs b/crates/transport-ws/src/wasm.rs index 6a46e130a7c..d7f747b8aab 100644 --- a/crates/transport-ws/src/wasm.rs +++ b/crates/transport-ws/src/wasm.rs @@ -1,5 +1,5 @@ use super::WsBackend; -use crate::utils::Spawnable; +use alloy_transport::utils::Spawnable; use futures::{ sink::SinkExt, @@ -42,15 +42,10 @@ impl WsBackend> { // probably not a big deal. tokio::select! { biased; - // break on shutdown recv, or on shutdown recv error - _ = &mut self.interface.shutdown => { - self.interface.from_frontend.close(); - break - }, // we've received a new dispatch, so we send it via // websocket. We handle new work before processing any // responses from the server. - inst = self.interface.from_frontend.recv() => { + inst = self.interface.recv_from_frontend() => { match inst { Some(msg) => { if let Err(e) = self.send(msg).await { @@ -81,7 +76,7 @@ impl WsBackend> { } } if err { - let _ = self.interface.error.send(()); + self.interface.close_with_error(); } }; fut.spawn_task(); diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index 835cac9d7e4..660f96581cd 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -25,3 +25,5 @@ tracing.workspace = true base64 = "0.21.0" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4.37" \ No newline at end of file From 48ee7e1229ecb55b665249bc107e48488f1dfd1d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:27:50 -0800 Subject: [PATCH 20/30] doc: resolve broken links --- crates/pubsub/src/connect.rs | 2 +- crates/pubsub/src/handle.rs | 13 +++-------- crates/pubsub/src/lib.rs | 37 +++++++++++++++++++++++++++++++ crates/transport-http/src/lib.rs | 6 +++-- crates/transport-ws/Cargo.toml | 2 +- crates/transport-ws/src/lib.rs | 2 +- crates/transport-ws/src/native.rs | 7 +++--- crates/transport-ws/src/wasm.rs | 6 ++--- crates/transport/src/boxed.rs | 2 +- crates/transport/src/connect.rs | 2 +- crates/transport/src/lib.rs | 36 ------------------------------ crates/transport/src/trait.rs | 7 +++--- 12 files changed, 57 insertions(+), 65 deletions(-) diff --git a/crates/pubsub/src/connect.rs b/crates/pubsub/src/connect.rs index 015b75364bc..39b100b74ec 100644 --- a/crates/pubsub/src/connect.rs +++ b/crates/pubsub/src/connect.rs @@ -14,7 +14,7 @@ pub trait PubSubConnect: Sized + Send + Sync + 'static { /// This function MUST create a long-lived task containing a /// [`ConnectionInterface`], and return the corresponding handle. /// - /// [`ConnectionInterface`]: crate::pubsub::ConnectionInterface + /// [`ConnectionInterface`]: crate::ConnectionInterface fn connect<'a: 'b, 'b>(&'a self) -> Pbf<'b, ConnectionHandle, TransportError>; /// Attempt to reconnect the transport. diff --git a/crates/pubsub/src/handle.rs b/crates/pubsub/src/handle.rs index 284c3b48e52..c5403db1820 100644 --- a/crates/pubsub/src/handle.rs +++ b/crates/pubsub/src/handle.rs @@ -55,15 +55,6 @@ impl ConnectionHandle { } /// The reciprocal of [`ConnectionHandle`]. -/// -/// [`ConnectionInterface`] implements [`Stream`] for receiving requests from -/// the frontend. The [`Stream`] implementation will return `None` permanently -/// when the shutdown channel from the frontend has resolved. -/// -/// It sends responses to the frontend via the `send_to_frontend` -/// method. It also notifies the frontend of a terminal error via the `error` -/// channel. - pub struct ConnectionInterface { /// Inbound channel from frontend. pub(crate) from_frontend: mpsc::UnboundedReceiver>, @@ -87,7 +78,9 @@ impl ConnectionInterface { self.to_frontend.send(item) } - /// Receive a request from the frontend. + /// Receive a request from the frontend. Ensures that if the frontend has + /// dropped or issued a shutdown instruction, the backend sees no more + /// requests. pub async fn recv_from_frontend(&mut self) -> Option> { match self.shutdown.try_recv() { Ok(_) => return None, diff --git a/crates/pubsub/src/lib.rs b/crates/pubsub/src/lib.rs index bee0d145d97..f9ba0f9b248 100644 --- a/crates/pubsub/src/lib.rs +++ b/crates/pubsub/src/lib.rs @@ -1,3 +1,40 @@ +//! # alloy-pubsub. +//! +//! ### Overview +//! +//! PubSub services, unlike regular RPC services, are long-lived and +//! bidirectional. They are used to subscribe to events on the server, and +//! receive notifications when those events occur. +//! +//! The PubSub system here consists of 3 logical parts: +//! - The **frontend** is the part of the system that the user interacts with. +//! It exposes a simple API that allows the user to issue requests and manage +//! subscriptions. +//! - The **service** is an intermediate layer that manages request/response +//! mappings, subscription aliasing, and backend lifecycle events. Running +//! [`PubSubConnect::into_service`] will spawn a long-lived service task. +//! - The **backend** is an actively running connection to the server. Users +//! should NEVER instantiate a backend directly. Instead, they should use +//! [`PubSubConnect::into_service`] for some connection object. +//! +//! This module provides the following: +//! +//! - [PubSubConnect]: A trait for instantiating a PubSub service by connecting +//! to some **backend**. Implementors of this trait are responsible for +//! the precise connection details, and for spawning the **backend** task. +//! Users should ALWAYS call [`PubSubConnect::into_service`] to get a running +//! service with a running backend. +//! - [`ConnectionHandle`]: A handle to a running **backend**. This type is +//! returned by [PubSubConnect::connect], and owned by the **service**. +//! Dropping the handle will shut down the **backend**. +//! - [`ConnectionInterface`]: The reciprocal of [ConnectionHandle]. This type +//! is owned by the **backend**, and is used to communicate with the +//! **service**. Dropping the interface will notify the **service** of a +//! terminal error. +//! - [`PubSubFrontend`]: The **frontend**. A handle to a running PubSub +//! **service**. It is used to issue requests and subscription lifecycle +//! instructions to the **service**. + mod connect; pub use connect::PubSubConnect; diff --git a/crates/transport-http/src/lib.rs b/crates/transport-http/src/lib.rs index 6e611db3c8e..9bf1caef1e7 100644 --- a/crates/transport-http/src/lib.rs +++ b/crates/transport-http/src/lib.rs @@ -10,8 +10,10 @@ use url::Url; /// An Http transport. /// /// The user must provide an internal http client and a URL to which to -/// connect. It implements `Service>`, and can be used directly -/// by an [`RpcClient`]. +/// connect. It implements `Service>`, and therefore +/// [`Transport`]. +/// +/// [`Transport`]: alloy_transport::Transport /// /// Currently supported clients are: #[cfg_attr(feature = "reqwest", doc = " - [`::reqwest::Client`]")] diff --git a/crates/transport-ws/Cargo.toml b/crates/transport-ws/Cargo.toml index f381b7b36cb..6e2c0e861f3 100644 --- a/crates/transport-ws/Cargo.toml +++ b/crates/transport-ws/Cargo.toml @@ -25,7 +25,7 @@ tracing.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1.33.0" -features = ["sync", "rt"] +features = ["sync", "rt", "time"] [target.'cfg(target_arch = "wasm32")'.dependencies] ws_stream_wasm = "0.7.4" diff --git a/crates/transport-ws/src/lib.rs b/crates/transport-ws/src/lib.rs index a2940584db8..e2246777652 100644 --- a/crates/transport-ws/src/lib.rs +++ b/crates/transport-ws/src/lib.rs @@ -17,7 +17,7 @@ use tracing::{debug, error, trace}; /// Users should NEVER instantiate a backend directly. Instead, they should use /// [`PubSubConnect`] to get a running service with a running backend. /// -/// [`PubSubConnect`]: crate::PubSubConnect +/// [`PubSubConnect`]: alloy_pubsub::PubSubConnect pub struct WsBackend { pub(crate) socket: T, diff --git a/crates/transport-ws/src/native.rs b/crates/transport-ws/src/native.rs index db1ef11f881..a4e4a4f9368 100644 --- a/crates/transport-ws/src/native.rs +++ b/crates/transport-ws/src/native.rs @@ -51,10 +51,9 @@ impl WsBackend { tokio::pin!(keepalive); loop { // We bias the loop as follows - // 1. Shutdown channels. - // 2. New dispatch to server. - // 3. Keepalive. - // 4. Response or notification from server. + // 1. New dispatch to server. + // 2. Keepalive. + // 3. Response or notification from server. // This ensures that keepalive is sent only if no other messages // have been sent in the last 10 seconds. And prioritizes new // dispatches over responses from the server. This will fail if diff --git a/crates/transport-ws/src/wasm.rs b/crates/transport-ws/src/wasm.rs index d7f747b8aab..9ae68b295ed 100644 --- a/crates/transport-ws/src/wasm.rs +++ b/crates/transport-ws/src/wasm.rs @@ -31,10 +31,8 @@ impl WsBackend> { let mut err = false; loop { // We bias the loop as follows - // 1. Shutdown channels. - // 2. New dispatch to server. - // 3. Keepalive. - // 4. Response or notification from server. + // 1. New dispatch to server. + // 2. Response or notification from server. // This ensures that keepalive is sent only if no other messages // have been sent in the last 10 seconds. And prioritizes new // dispatches over responses from the server. This will fail if diff --git a/crates/transport/src/boxed.rs b/crates/transport/src/boxed.rs index b0c7ad65617..8e27e939be7 100644 --- a/crates/transport/src/boxed.rs +++ b/crates/transport/src/boxed.rs @@ -6,7 +6,7 @@ use crate::{Transport, TransportError, TransportFut}; /// A boxed, Clone-able [`Transport`] trait object. /// -/// This type allows [`RpcClient`] to use a type-erased transport. It is +/// This type allows RPC clients 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. diff --git a/crates/transport/src/connect.rs b/crates/transport/src/connect.rs index 9813b0675ca..b7f45706330 100644 --- a/crates/transport/src/connect.rs +++ b/crates/transport/src/connect.rs @@ -8,7 +8,7 @@ use crate::{BoxTransport, Pbf, Transport, TransportError}; /// ## Why implement `TransportConnect`? /// /// Users may want to implement transport-connect for the following reasons: -/// - You want to customize a [`reqwest::Client`] before using it. +/// - You want to customize a `reqwest::Client` before using it. /// - You need to provide special authentication information to a remote /// provider. /// - You have implemented a custom [`Transport`]. diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs index c9373b5ff2a..275a14b45ef 100644 --- a/crates/transport/src/lib.rs +++ b/crates/transport/src/lib.rs @@ -3,42 +3,6 @@ //! ## Transport //! //! -//! ## PubSub services. -//! -//! ### Overview -//! -//! PubSub services, unlike regular RPC services, are long-lived and -//! bidirectional. They are used to subscribe to events on the server, and -//! receive notifications when those events occur. -//! -//! The PubSub system here consists of 3 logical parts: -//! - The **frontend** is the part of the system that the user interacts with. -//! It exposes a simple API that allows the user to issue requests and manage -//! subscriptions. -//! - The **service** is an intermediate layer that manages request/response -//! mappings, subscription aliasing, and backend lifecycle events. Running -//! [`PubSubConnect::into_service`] will spawn a long-lived service task. -//! - The **backend** is an actively running connection to the server. Users -//! should NEVER instantiate a backend directly. Instead, they should use -//! [`PubSubConnect::into_service`] for some connection object. -//! -//! This module provides the following: -//! -//! - [PubSubConnect]: A trait for instantiating a PubSub service by connecting -//! to some **backend**. Implementors of this trait are responsible for -//! the precise connection details, and for spawning the **backend** task. -//! Users should ALWAYS call [`PubSubConnect::into_service`] to get a running -//! service with a running backend. -//! - [`ConnectionHandle`]: A handle to a running **backend**. This type is -//! returned by [PubSubConnect::connect], and owned by the **service**. -//! Dropping the handle will shut down the **backend**. -//! - [`ConnectionInterface`]: The reciprocal of [ConnectionHandle]. This type -//! is owned by the **backend**, and is used to communicate with the -//! **service**. Dropping the interface will notify the **service** of a -//! terminal error. -//! - [`PubSubFrontend`]: The **frontend**. A handle to a running PubSub -//! **service**. It is used to issue requests and subscription lifecycle -//! instructions to the **service**. mod boxed; pub use boxed::BoxTransport; diff --git a/crates/transport/src/trait.rs b/crates/transport/src/trait.rs index 92e335c4d66..7cc4fdf96cd 100644 --- a/crates/transport/src/trait.rs +++ b/crates/transport/src/trait.rs @@ -27,14 +27,13 @@ use crate::{BoxTransport, TransportError, TransportFut}; /// /// [`Clone`] is not a bound on `Transport`, however, transports generally may /// not be used as expected unless they implement `Clone`. For example, only -/// cloneable transports may be used by the [`RpcClient::prepare`] to send RPC -/// requests, and [`BoxTransport`] may only be used to type-erase Cloneable -/// transports. +/// cloneable transports may be used by the `RpcClient` in `alloy-rpc-client` +/// to send RPC requests, and [`BoxTransport`] may only be used to type-erase +/// Cloneable transports. /// /// If you are implementing a transport, make sure it is [`Clone`]. /// /// [`TransportConnect`]: crate::TransportConnect -/// [`RpcClient::prepare`]: crate::RpcClient::prepare pub trait Transport: private::Sealed + Service< From f8f422ccca4a1699c2ebdcdb073d030711f4cd17 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:29:45 -0800 Subject: [PATCH 21/30] fix: tokio rt on non-wasm --- crates/transport/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index 660f96581cd..89f6be9eaec 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -26,4 +26,8 @@ tracing.workspace = true base64 = "0.21.0" [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-futures = "0.4.37" \ No newline at end of file +wasm-bindgen-futures = "0.4.37" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] +workspace = true +features = ["rt"] From 17218814b1731c6854572a3d2a9b688d926bf541 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:32:51 -0800 Subject: [PATCH 22/30] fix: cargo hack --- crates/rpc-client/tests/it/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rpc-client/tests/it/main.rs b/crates/rpc-client/tests/it/main.rs index aaefe735be9..c5c35e7bd39 100644 --- a/crates/rpc-client/tests/it/main.rs +++ b/crates/rpc-client/tests/it/main.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +#[cfg(feature = "reqwest")] mod http; #[cfg(feature = "pubsub")] From 0772226ec7f655d4a83bf045f84c958826eafab3 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:43:56 -0800 Subject: [PATCH 23/30] chore: add warns and denies to some lib files --- crates/json-rpc/src/common.rs | 5 ++++- crates/json-rpc/src/lib.rs | 16 ++++++++++++++++ crates/json-rpc/src/notification.rs | 6 +++++- crates/json-rpc/src/packet.rs | 6 +++++- crates/json-rpc/src/request.rs | 7 ++++++- crates/json-rpc/src/response/error.rs | 13 ++++++++----- crates/json-rpc/src/response/mod.rs | 12 +++++++----- crates/json-rpc/src/response/payload.rs | 2 ++ crates/pubsub/src/frontend.rs | 4 ++++ crates/pubsub/src/handle.rs | 1 + crates/pubsub/src/ix.rs | 2 +- crates/pubsub/src/lib.rs | 16 ++++++++++++++++ crates/pubsub/src/managers/active_sub.rs | 12 ++++++------ crates/pubsub/src/managers/in_flight.rs | 14 +++++++------- crates/pubsub/src/managers/req.rs | 10 +++++----- crates/pubsub/src/managers/sub.rs | 16 ++++++++-------- crates/pubsub/src/service.rs | 2 +- 17 files changed, 102 insertions(+), 42 deletions(-) diff --git a/crates/json-rpc/src/common.rs b/crates/json-rpc/src/common.rs index 1031fdea722..1e9d94683cd 100644 --- a/crates/json-rpc/src/common.rs +++ b/crates/json-rpc/src/common.rs @@ -23,8 +23,11 @@ use serde::{de::Visitor, Deserialize, Serialize}; /// [`HashSet`]: std::collections::HashSet #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Id { + /// A number. Number(u64), + /// A string. String(String), + /// Null. None, } @@ -48,7 +51,7 @@ impl<'de> Deserialize<'de> for Id { impl<'de> Visitor<'de> for IdVisitor { type Value = Id; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "a string, a number, or null") } diff --git a/crates/json-rpc/src/lib.rs b/crates/json-rpc/src/lib.rs index 8327d663332..fa8be7ae704 100644 --- a/crates/json-rpc/src/lib.rs +++ b/crates/json-rpc/src/lib.rs @@ -1,3 +1,19 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + //! Alloy JSON-RPC data types. //! //! This crate provides data types for use with the JSON-RPC 2.0 protocol. It diff --git a/crates/json-rpc/src/notification.rs b/crates/json-rpc/src/notification.rs index ca1e060d392..4bffe2f8bb4 100644 --- a/crates/json-rpc/src/notification.rs +++ b/crates/json-rpc/src/notification.rs @@ -10,7 +10,9 @@ use crate::{Response, ResponsePayload}; /// notification. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct EthNotification> { + /// The subscription ID. pub subscription: U256, + /// The notification payload. pub result: T, } @@ -19,7 +21,9 @@ pub struct EthNotification> { /// transport may be a JSON-RPC response or an Ethereum-style notification. #[derive(Debug, Clone)] pub enum PubSubItem { + /// A [`Response`] to a JSON-RPC request. Response(Response), + /// An Ethereum-style notification. Notification(EthNotification), } @@ -33,7 +37,7 @@ impl<'de> Deserialize<'de> for PubSubItem { impl<'de> Visitor<'de> for PubSubItemVisitor { type Value = PubSubItem; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a JSON-RPC response or an Ethereum-style notification") } diff --git a/crates/json-rpc/src/packet.rs b/crates/json-rpc/src/packet.rs index 715ddb9a5da..4205456f1e8 100644 --- a/crates/json-rpc/src/packet.rs +++ b/crates/json-rpc/src/packet.rs @@ -9,7 +9,9 @@ use crate::{Id, Response, SerializedRequest}; /// request. #[derive(Debug, Clone)] pub enum RequestPacket { + /// A single request. Single(SerializedRequest), + /// A batch of requests. Batch(Vec), } @@ -107,7 +109,9 @@ impl RequestPacket { /// A [`ResponsePacket`] is a [`Response`] or a batch of responses. #[derive(Debug, Clone)] pub enum ResponsePacket, ErrData = Box> { + /// A single response. Single(Response), + /// A batch of responses. Batch(Vec>), } @@ -165,7 +169,7 @@ where { type Value = ResponsePacket; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a single response or a batch of responses") } diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index 9449e5b2cea..79bc0d22eb3 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -4,9 +4,12 @@ use alloy_primitives::{keccak256, B256}; use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize}; use serde_json::value::RawValue; +/// `RequestMeta` contains the [`Id`] and method name of a request. #[derive(Debug, Clone)] pub struct RequestMeta { + /// The method name. pub method: &'static str, + /// The request ID. pub id: Id, } @@ -21,7 +24,9 @@ pub struct RequestMeta { /// The value of `method` should be known at compile time. #[derive(Debug, Clone)] pub struct Request { + /// The request metadata (ID and method). pub meta: RequestMeta, + /// The request parameters. pub params: Params, } @@ -177,7 +182,7 @@ impl SerializedRequest { params: Option<&'a RawValue>, } - let req: Req = serde_json::from_str(self.request.get()).unwrap(); + let req: Req<'_> = serde_json::from_str(self.request.get()).unwrap(); req.params } diff --git a/crates/json-rpc/src/response/error.rs b/crates/json-rpc/src/response/error.rs index ea348322c92..e856b4a9150 100644 --- a/crates/json-rpc/src/response/error.rs +++ b/crates/json-rpc/src/response/error.rs @@ -12,8 +12,11 @@ use std::{borrow::Borrow, fmt, marker::PhantomData}; /// included in the `message` field of the response payload. #[derive(Debug, Clone)] pub struct ErrorPayload> { + /// The error code. pub code: i64, + /// The error message (if any). pub message: String, + /// The error data (if any). pub data: Option, } @@ -60,7 +63,7 @@ impl<'de, ErrData: Deserialize<'de>> Deserialize<'de> for ErrorPayload impl<'de> serde::de::Visitor<'de> for FieldVisitor { type Value = Field; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("`code`, `message` and `data`") } @@ -88,7 +91,7 @@ impl<'de, ErrData: Deserialize<'de>> Deserialize<'de> for ErrorPayload { type Value = ErrorPayload; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "a JSON-RPC2.0 error object") } @@ -128,7 +131,7 @@ impl<'de, ErrData: Deserialize<'de>> Deserialize<'de> for ErrorPayload } Ok(ErrorPayload { code: code.ok_or_else(|| serde::de::Error::missing_field("code"))?, - message: message.ok_or_else(|| serde::de::Error::missing_field("message"))?, + message: message.unwrap_or_default(), data, }) } @@ -184,7 +187,7 @@ mod test { #[test] fn smooth_borrowing() { let json = r#"{ "code": -32000, "message": "b", "data": null }"#; - let payload: BorrowedErrorPayload = serde_json::from_str(json).unwrap(); + let payload: BorrowedErrorPayload<'_> = serde_json::from_str(json).unwrap(); assert_eq!(payload.code, -32000); assert_eq!(payload.message, "b"); @@ -201,7 +204,7 @@ mod test { let json = r#"{ "code": -32000, "message": "b", "data": { "a": 5, "b": null } }"#; - let payload: BorrowedErrorPayload = serde_json::from_str(json).unwrap(); + let payload: BorrowedErrorPayload<'_> = serde_json::from_str(json).unwrap(); let data: TestData = payload.try_data_as().unwrap().unwrap(); assert_eq!(data, TestData { a: 5, b: None }); } diff --git a/crates/json-rpc/src/response/mod.rs b/crates/json-rpc/src/response/mod.rs index 83a9fa14f10..96ea10c29fa 100644 --- a/crates/json-rpc/src/response/mod.rs +++ b/crates/json-rpc/src/response/mod.rs @@ -21,7 +21,9 @@ use crate::common::Id; /// mirrored from the response. #[derive(Debug, Clone)] pub struct Response, ErrData = Box> { + /// The ID of the request that this response is responding to. pub id: Id, + /// The response payload. pub payload: ResponsePayload, } @@ -149,7 +151,7 @@ where impl<'de> serde::de::Visitor<'de> for FieldVisitor { type Value = Field; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("`result`, `error` and `id`") } @@ -178,7 +180,7 @@ where { type Value = Response; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str( "a JSON-RPC response object, consisting of either a result or an error", ) @@ -243,7 +245,7 @@ where #[cfg(test)] mod test { #[test] - pub fn deser_success() { + fn deser_success() { let response = r#"{ "jsonrpc": "2.0", "result": "california", @@ -258,7 +260,7 @@ mod test { } #[test] - pub fn deser_err() { + fn deser_err() { let response = r#"{ "jsonrpc": "2.0", "error": { @@ -276,7 +278,7 @@ mod test { } #[test] - pub fn deser_complex_success() { + fn deser_complex_success() { let response = r#"{ "result": { "name": "california", diff --git a/crates/json-rpc/src/response/payload.rs b/crates/json-rpc/src/response/payload.rs index 4b7755b2d80..8b66daaf814 100644 --- a/crates/json-rpc/src/response/payload.rs +++ b/crates/json-rpc/src/response/payload.rs @@ -19,7 +19,9 @@ use crate::ErrorPayload; /// [`Response`]: crate::Response #[derive(Debug, Clone)] pub enum ResponsePayload, ErrData = Box> { + /// A successful response payload. Success(Payload), + /// An error response payload. Failure(ErrorPayload), } diff --git a/crates/pubsub/src/frontend.rs b/crates/pubsub/src/frontend.rs index 5b49912ced6..cdf61b2e4be 100644 --- a/crates/pubsub/src/frontend.rs +++ b/crates/pubsub/src/frontend.rs @@ -9,6 +9,10 @@ use tokio::sync::{broadcast, mpsc, oneshot}; use crate::{ix::PubSubInstruction, managers::InFlight}; use alloy_transport::{TransportError, TransportFut}; +/// A `PubSubFrontend` is [`Transport`] composed of a channel to a running +/// PubSub service. +/// +/// [`Transport`]: alloy_transport::Transport #[derive(Debug, Clone)] pub struct PubSubFrontend { tx: mpsc::UnboundedSender, diff --git a/crates/pubsub/src/handle.rs b/crates/pubsub/src/handle.rs index c5403db1820..1ce8422101a 100644 --- a/crates/pubsub/src/handle.rs +++ b/crates/pubsub/src/handle.rs @@ -55,6 +55,7 @@ impl ConnectionHandle { } /// The reciprocal of [`ConnectionHandle`]. +#[derive(Debug)] pub struct ConnectionInterface { /// Inbound channel from frontend. pub(crate) from_frontend: mpsc::UnboundedReceiver>, diff --git a/crates/pubsub/src/ix.rs b/crates/pubsub/src/ix.rs index 530934c32f4..ddcc1c1bddc 100644 --- a/crates/pubsub/src/ix.rs +++ b/crates/pubsub/src/ix.rs @@ -5,7 +5,7 @@ use tokio::sync::{broadcast, oneshot}; use crate::managers::InFlight; /// Instructions for the pubsub service. -pub enum PubSubInstruction { +pub(crate) enum PubSubInstruction { /// Send a request. Request(InFlight), /// Get the subscription ID for a local ID. diff --git a/crates/pubsub/src/lib.rs b/crates/pubsub/src/lib.rs index f9ba0f9b248..d437c93b935 100644 --- a/crates/pubsub/src/lib.rs +++ b/crates/pubsub/src/lib.rs @@ -1,3 +1,19 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + //! # alloy-pubsub. //! //! ### Overview diff --git a/crates/pubsub/src/managers/active_sub.rs b/crates/pubsub/src/managers/active_sub.rs index 67cc45aad5f..a532138b68a 100644 --- a/crates/pubsub/src/managers/active_sub.rs +++ b/crates/pubsub/src/managers/active_sub.rs @@ -9,11 +9,11 @@ use tokio::sync::broadcast; /// An active subscription. pub(crate) struct ActiveSubscription { /// Cached hash of the request, used for sorting and equality. - pub local_id: B256, + pub(crate) local_id: B256, /// The serialized subscription request. - pub request: SerializedRequest, + pub(crate) request: SerializedRequest, /// The channel via which notifications are broadcast. - pub tx: broadcast::Sender>, + pub(crate) tx: broadcast::Sender>, } // NB: We implement this to prevent any incorrect future implementations. @@ -57,7 +57,7 @@ impl std::fmt::Debug for ActiveSubscription { impl ActiveSubscription { /// Create a new active subscription. - pub fn new(request: SerializedRequest) -> (Self, broadcast::Receiver>) { + pub(crate) fn new(request: SerializedRequest) -> (Self, broadcast::Receiver>) { let local_id = request.params_hash(); let (tx, rx) = broadcast::channel(16); ( @@ -73,13 +73,13 @@ impl ActiveSubscription { /// Serialize the request as a boxed [`RawValue`]. /// /// This is used to (re-)send the request over the transport. - pub fn request(&self) -> &SerializedRequest { + pub(crate) fn request(&self) -> &SerializedRequest { &self.request } /// Notify the subscription channel of a new value, if any receiver exists. /// If no receiver exists, the notification is dropped. - pub fn notify(&mut self, notification: Box) { + pub(crate) fn notify(&mut self, notification: Box) { if self.tx.receiver_count() > 0 { let _ = self.tx.send(notification); } diff --git a/crates/pubsub/src/managers/in_flight.rs b/crates/pubsub/src/managers/in_flight.rs index 0021b76f702..1cf0b66437f 100644 --- a/crates/pubsub/src/managers/in_flight.rs +++ b/crates/pubsub/src/managers/in_flight.rs @@ -8,12 +8,12 @@ use alloy_transport::TransportError; /// /// This struct contains the request that was sent, as well as a channel to /// receive the response on. -pub struct InFlight { +pub(crate) struct InFlight { /// The request - pub request: SerializedRequest, + pub(crate) request: SerializedRequest, /// The channel to send the response on. - pub tx: oneshot::Sender>, + pub(crate) tx: oneshot::Sender>, } impl std::fmt::Debug for InFlight { @@ -32,7 +32,7 @@ impl std::fmt::Debug for InFlight { impl InFlight { /// Create a new in-flight request. - pub fn new( + pub(crate) fn new( request: SerializedRequest, ) -> (Self, oneshot::Receiver>) { let (tx, rx) = oneshot::channel(); @@ -41,21 +41,21 @@ impl InFlight { } /// Get the method - pub fn method(&self) -> &'static str { + pub(crate) fn method(&self) -> &'static str { self.request.method() } /// Get a reference to the serialized request. /// /// This is used to (re-)send the request over the transport. - pub fn request(&self) -> &SerializedRequest { + pub(crate) fn request(&self) -> &SerializedRequest { &self.request } /// Fulfill the request with a response. This consumes the in-flight /// request. If the request is a subscription and the response is not an /// error, the subscription ID and the in-flight request are returned. - pub fn fulfill(self, resp: Response) -> Option<(U256, Self)> { + pub(crate) fn fulfill(self, resp: Response) -> Option<(U256, Self)> { if self.method() == "eth_subscribe" { if let ResponsePayload::Success(val) = resp.payload { let sub_id: serde_json::Result = serde_json::from_str(val.get()); diff --git a/crates/pubsub/src/managers/req.rs b/crates/pubsub/src/managers/req.rs index 3d460f280da..7c26ae817bf 100644 --- a/crates/pubsub/src/managers/req.rs +++ b/crates/pubsub/src/managers/req.rs @@ -6,23 +6,23 @@ use crate::managers::InFlight; /// Manages in-flight requests. #[derive(Debug, Default)] -pub struct RequestManager { +pub(crate) struct RequestManager { reqs: BTreeMap, } impl RequestManager { /// Get the number of in-flight requests. - pub fn len(&self) -> usize { + pub(crate) fn len(&self) -> usize { self.reqs.len() } /// Get an iterator over the in-flight requests. - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.reqs.iter() } /// Insert a new in-flight request. - pub fn insert(&mut self, in_flight: InFlight) { + pub(crate) fn insert(&mut self, in_flight: InFlight) { self.reqs.insert(in_flight.request.id().clone(), in_flight); } @@ -31,7 +31,7 @@ impl RequestManager { /// If the request created a new subscription, this function returns the /// subscription ID and the in-flight request for conversion to an /// `ActiveSubscription`. - pub fn handle_response(&mut self, resp: Response) -> Option<(U256, InFlight)> { + pub(crate) fn handle_response(&mut self, resp: Response) -> Option<(U256, InFlight)> { if let Some(in_flight) = self.reqs.remove(&resp.id) { return in_flight.fulfill(resp); } diff --git a/crates/pubsub/src/managers/sub.rs b/crates/pubsub/src/managers/sub.rs index bd6a6d0485b..cac62c96b86 100644 --- a/crates/pubsub/src/managers/sub.rs +++ b/crates/pubsub/src/managers/sub.rs @@ -16,12 +16,12 @@ pub(crate) struct SubscriptionManager { impl SubscriptionManager { /// Get an iterator over the subscriptions. - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.local_to_sub.iter() } /// Get the number of subscriptions. - pub fn len(&self) -> usize { + pub(crate) fn len(&self) -> usize { self.local_to_sub.len() } @@ -39,7 +39,7 @@ impl SubscriptionManager { } /// Insert or update the server_id for a subscription. - pub fn upsert( + pub(crate) fn upsert( &mut self, request: SerializedRequest, server_id: U256, @@ -57,12 +57,12 @@ impl SubscriptionManager { } /// De-alias an alias, getting the original ID. - pub fn local_id_for(&self, server_id: U256) -> Option { + pub(crate) fn local_id_for(&self, server_id: U256) -> Option { self.local_to_server.get_by_right(&server_id).copied() } /// Drop all server_ids. - pub fn drop_server_ids(&mut self) { + pub(crate) fn drop_server_ids(&mut self) { self.local_to_server.clear(); } @@ -72,7 +72,7 @@ impl SubscriptionManager { } /// Remove a subscription by its local_id. - pub fn remove_sub(&mut self, local_id: B256) { + pub(crate) fn remove_sub(&mut self, local_id: B256) { let _ = self.local_to_sub.remove_by_left(&local_id); let _ = self.local_to_server.remove_by_left(&local_id); } @@ -80,7 +80,7 @@ impl SubscriptionManager { /// Notify the subscription channel of a new value, if the sub is known, /// and if any receiver exists. If the sub id is unknown, or no receiver /// exists, the notification is dropped. - pub fn notify(&mut self, notification: EthNotification) { + pub(crate) fn notify(&mut self, notification: EthNotification) { if let Some(local_id) = self.local_id_for(notification.subscription) { if let Some((_, mut sub)) = self.local_to_sub.remove_by_left(&local_id) { sub.notify(notification.result); @@ -90,7 +90,7 @@ impl SubscriptionManager { } /// Get a receiver for a subscription. - pub fn get_rx(&self, local_id: B256) -> Option>> { + pub(crate) fn get_rx(&self, local_id: B256) -> Option>> { let sub = self.local_to_sub.get_by_left(&local_id)?; Some(sub.tx.subscribe()) } diff --git a/crates/pubsub/src/service.rs b/crates/pubsub/src/service.rs index 2ff1b6f5a6d..e5364da3bfc 100644 --- a/crates/pubsub/src/service.rs +++ b/crates/pubsub/src/service.rs @@ -213,7 +213,7 @@ where } /// Spawn the service. - pub fn spawn(mut self) { + pub(crate) fn spawn(mut self) { let fut = async move { let result: Result<(), TransportError> = loop { // We bias the loop so that we always handle new messages before From f1e8a68d5fafc6db3bdb470bd27ff5541a9cfeeb Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 15:49:59 -0800 Subject: [PATCH 24/30] chore: add warns and denies to more lib files --- crates/rpc-client/Cargo.toml | 3 +-- crates/rpc-client/src/batch.rs | 5 ++++- crates/rpc-client/src/builder.rs | 1 + crates/rpc-client/src/call.rs | 1 + crates/rpc-client/src/client.rs | 3 ++- crates/rpc-client/src/lib.rs | 18 ++++++++++++++++++ crates/transport/Cargo.toml | 2 -- crates/transport/src/error.rs | 2 ++ crates/transport/src/lib.rs | 26 +++++++++++++++++++++----- 9 files changed, 50 insertions(+), 11 deletions(-) diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 7067dab1c2e..b5c612f81c6 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -16,13 +16,11 @@ alloy-json-rpc.workspace = true alloy-transport.workspace = true alloy-transport-http.workspace = true -alloy-primitives.workspace = true futures.workspace = true pin-project.workspace = true serde_json.workspace = true tracing.workspace = true -tokio.workspace = true tower.workspace = true reqwest = { workspace = true, optional = true } @@ -30,6 +28,7 @@ alloy-pubsub = { workspace = true, optional = true } alloy-transport-ws = { workspace = true, optional = true } [dev-dependencies] +alloy-primitives.workspace = true alloy-transport-ws.workspace = true test-log = { version = "0.2.13", default-features = false, features = ["trace"] } tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } diff --git a/crates/rpc-client/src/batch.rs b/crates/rpc-client/src/batch.rs index eca52b03e91..f4514f6c39e 100644 --- a/crates/rpc-client/src/batch.rs +++ b/crates/rpc-client/src/batch.rs @@ -35,9 +35,10 @@ pub struct BatchRequest<'a, T> { /// 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."] +#[derive(Debug)] pub struct Waiter { rx: oneshot::Receiver, Box, TransportError>>, - _resp: PhantomData, + _resp: PhantomData Resp>, } impl From, Box, TransportError>>> @@ -71,6 +72,7 @@ where } #[pin_project::pin_project(project = CallStateProj)] +#[derive(Debug)] pub enum BatchFuture where Conn: Transport, @@ -90,6 +92,7 @@ where } impl<'a, T> BatchRequest<'a, T> { + /// Create a new batch request. pub fn new(transport: &'a RpcClient) -> Self { Self { transport, diff --git a/crates/rpc-client/src/builder.rs b/crates/rpc-client/src/builder.rs index 67f7c88e5be..b62a8811569 100644 --- a/crates/rpc-client/src/builder.rs +++ b/crates/rpc-client/src/builder.rs @@ -16,6 +16,7 @@ use tower::{ /// /// A builder accumulates Layers, and then is finished via the /// [`ClientBuilder::connect`] method, which produces an RPC client. +#[derive(Debug)] pub struct ClientBuilder { pub(crate) builder: ServiceBuilder, } diff --git a/crates/rpc-client/src/call.rs b/crates/rpc-client/src/call.rs index 2d8c7e44718..76d7b10887a 100644 --- a/crates/rpc-client/src/call.rs +++ b/crates/rpc-client/src/call.rs @@ -146,6 +146,7 @@ where /// requests with different `Param` types, while the `RpcCall` may do so lazily. #[must_use = "futures do nothing unless you `.await` or poll them"] #[pin_project::pin_project] +#[derive(Debug)] pub struct RpcCall where Conn: Transport + Clone, diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index fbb7277537b..22931e4f779 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -32,6 +32,7 @@ pub struct RpcClient { } impl RpcClient { + /// Create a new [`ClientBuilder`]. pub fn builder() -> ClientBuilder { ClientBuilder { builder: ServiceBuilder::new(), @@ -167,7 +168,7 @@ mod pubsub_impl { impl RpcClient> { /// Create a new [`BatchRequest`] builder. #[inline] - pub fn new_batch(&self) -> BatchRequest> { + pub fn new_batch(&self) -> BatchRequest<'_, Http> { BatchRequest::new(self) } } diff --git a/crates/rpc-client/src/lib.rs b/crates/rpc-client/src/lib.rs index 454aadfe847..82820d31f91 100644 --- a/crates/rpc-client/src/lib.rs +++ b/crates/rpc-client/src/lib.rs @@ -1,3 +1,21 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +//! alloy-rpc-client + mod batch; pub use batch::BatchRequest; diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index 89f6be9eaec..f6526f0b4f6 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -14,14 +14,12 @@ exclude.workspace = true [dependencies] alloy-json-rpc.workspace = true -alloy-primitives.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["raw_value"] } thiserror.workspace = true url.workspace = true tokio.workspace = true tower.workspace = true -tracing.workspace = true base64 = "0.21.0" diff --git a/crates/transport/src/error.rs b/crates/transport/src/error.rs index 64685af9259..97e875658fd 100644 --- a/crates/transport/src/error.rs +++ b/crates/transport/src/error.rs @@ -9,8 +9,10 @@ pub enum TransportError { /// SerdeJson (de)ser #[error("{err}")] SerdeJson { + /// The underlying serde_json error. #[source] err: serde_json::Error, + /// For deser errors, the text that failed to deserialize. text: Option, }, diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs index 275a14b45ef..360ee653e53 100644 --- a/crates/transport/src/lib.rs +++ b/crates/transport/src/lib.rs @@ -1,8 +1,21 @@ -//! Alloy Transports -//! -//! ## Transport -//! -//! +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +//! alloy-transports + mod boxed; pub use boxed::BoxTransport; @@ -20,6 +33,7 @@ pub use r#trait::Transport; pub use alloy_json_rpc::RpcResult; +/// Misc. utilities for building transports. pub mod utils; pub use type_aliases::*; @@ -31,6 +45,7 @@ mod type_aliases { use crate::TransportError; + /// Pin-boxed future. pub type Pbf<'a, T, E> = std::pin::Pin> + Send + 'a>>; @@ -51,6 +66,7 @@ mod type_aliases { use crate::TransportError; + /// Pin-boxed future. pub type Pbf<'a, T, E> = std::pin::Pin> + 'a>>; From 61b3a2c17447f47017d6916c5e3d14a82d263d29 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 16:00:41 -0800 Subject: [PATCH 25/30] chore: more lints and warns and errors --- Cargo.toml | 1 + crates/json-rpc/src/common.rs | 8 ++++---- crates/json-rpc/src/request.rs | 10 ++++++---- crates/json-rpc/src/response/mod.rs | 4 ++-- crates/json-rpc/src/response/payload.rs | 8 ++++---- crates/json-rpc/src/result.rs | 9 ++++++--- crates/pubsub/src/frontend.rs | 2 +- crates/pubsub/src/managers/active_sub.rs | 2 +- crates/pubsub/src/managers/in_flight.rs | 4 ++-- crates/rpc-client/Cargo.toml | 11 ++++++++--- crates/rpc-client/src/builder.rs | 16 ++++++++++++++++ crates/rpc-client/src/client.rs | 4 ++-- crates/transport-http/Cargo.toml | 1 - crates/transport-http/src/lib.rs | 22 ++++++++++++++++++++-- crates/transport/src/error.rs | 2 +- 15 files changed, 74 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ec65586fb5..7af55b6cf69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ thiserror = "1.0" url = "2.4.0" pin-project = "1.1.2" reqwest = "0.11.18" +hyper = "0.14.27" tower = { version = "0.4.13", features = ["util"] } tokio = { version = "1.33.0", features = ["sync", "macros"] } tracing = "0.1.40" diff --git a/crates/json-rpc/src/common.rs b/crates/json-rpc/src/common.rs index 1e9d94683cd..08db96a5285 100644 --- a/crates/json-rpc/src/common.rs +++ b/crates/json-rpc/src/common.rs @@ -115,22 +115,22 @@ impl Ord for Id { impl Id { /// Returns `true` if the ID is a number. - pub fn is_number(&self) -> bool { + pub const fn is_number(&self) -> bool { matches!(self, Id::Number(_)) } /// Returns `true` if the ID is a string. - pub fn is_string(&self) -> bool { + pub const fn is_string(&self) -> bool { matches!(self, Id::String(_)) } /// Returns `true` if the ID is `None`. - pub fn is_none(&self) -> bool { + pub const fn is_none(&self) -> bool { matches!(self, Id::None) } /// Returns the ID as a number, if it is one. - pub fn as_number(&self) -> Option { + pub const fn as_number(&self) -> Option { match self { Id::Number(n) => Some(*n), _ => None, diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index 79bc0d22eb3..c25f0da6f7c 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -144,29 +144,31 @@ where impl SerializedRequest { /// Get the request metadata (ID and Method) - pub fn meta(&self) -> &RequestMeta { + pub const fn meta(&self) -> &RequestMeta { &self.meta } /// Get the request ID. - pub fn id(&self) -> &Id { + pub const fn id(&self) -> &Id { &self.meta.id } /// Get the request method. - pub fn method(&self) -> &'static str { + pub const fn method(&self) -> &'static str { self.meta.method } /// Get the serialized request. - pub fn serialized(&self) -> &RawValue { + pub const fn serialized(&self) -> &RawValue { &self.request } /// Consumes the serialized request, returning the underlying /// [`RequestMeta`] and the [`RawValue`]. + #[allow(clippy::missing_const_for_fn)] // erroneous lint pub fn decompose(self) -> (RequestMeta, Box) { (self.meta, self.request) } /// Take the serialized request, consuming the [`SerializedRequest`]. + #[allow(clippy::missing_const_for_fn)] // erroneous lint pub fn take_request(self) -> Box { self.request } diff --git a/crates/json-rpc/src/response/mod.rs b/crates/json-rpc/src/response/mod.rs index 96ea10c29fa..1b0c234ffbd 100644 --- a/crates/json-rpc/src/response/mod.rs +++ b/crates/json-rpc/src/response/mod.rs @@ -49,12 +49,12 @@ impl BorrowedResponse<'_> { impl Response { /// Returns `true` if the response is a success. - pub fn is_success(&self) -> bool { + pub const fn is_success(&self) -> bool { self.payload.is_success() } /// Returns `true` if the response is an error. - pub fn is_error(&self) -> bool { + pub const fn is_error(&self) -> bool { self.payload.is_error() } } diff --git a/crates/json-rpc/src/response/payload.rs b/crates/json-rpc/src/response/payload.rs index 8b66daaf814..7ee9c13bd02 100644 --- a/crates/json-rpc/src/response/payload.rs +++ b/crates/json-rpc/src/response/payload.rs @@ -47,7 +47,7 @@ impl BorrowedResponsePayload<'_> { impl ResponsePayload { /// Fallible conversion to the succesful payload. - pub fn as_success(&self) -> Option<&Payload> { + pub const fn as_success(&self) -> Option<&Payload> { match self { ResponsePayload::Success(payload) => Some(payload), _ => None, @@ -55,7 +55,7 @@ impl ResponsePayload { } /// Fallible conversion to the error object. - pub fn as_error(&self) -> Option<&ErrorPayload> { + pub const fn as_error(&self) -> Option<&ErrorPayload> { match self { ResponsePayload::Failure(payload) => Some(payload), _ => None, @@ -63,12 +63,12 @@ impl ResponsePayload { } /// Returns `true` if the response payload is a success. - pub fn is_success(&self) -> bool { + pub const fn is_success(&self) -> bool { matches!(self, ResponsePayload::Success(_)) } /// Returns `true` if the response payload is an error. - pub fn is_error(&self) -> bool { + pub const fn is_error(&self) -> bool { matches!(self, ResponsePayload::Failure(_)) } } diff --git a/crates/json-rpc/src/result.rs b/crates/json-rpc/src/result.rs index e87456f27fc..eff889b7ca6 100644 --- a/crates/json-rpc/src/result.rs +++ b/crates/json-rpc/src/result.rs @@ -50,17 +50,17 @@ impl<'a, E> BorrowedRpcResult<'a, E> { impl RpcResult { /// `true` if the result is an `Ok` value. - pub fn is_success(&self) -> bool { + pub const fn is_success(&self) -> bool { matches!(self, RpcResult::Success(_)) } /// `true` if the result is an `Failure` value. - pub fn is_failure(&self) -> bool { + pub const fn is_failure(&self) -> bool { matches!(self, RpcResult::Failure(_)) } /// `true` if the result is an `Err` value. - pub fn is_err(&self) -> bool { + pub const fn is_err(&self) -> bool { matches!(self, RpcResult::Err(_)) } @@ -158,6 +158,7 @@ impl RpcResult { } /// Converts from `RpcResult` to `Option`. + #[allow(clippy::missing_const_for_fn)] // erroneous lint pub fn success(self) -> Option { match self { RpcResult::Success(val) => Some(val), @@ -166,6 +167,7 @@ impl RpcResult { } /// Converts from `RpcResult` to `Option`. + #[allow(clippy::missing_const_for_fn)] // erroneous lint pub fn failure(self) -> Option> { match self { RpcResult::Failure(err) => Some(err), @@ -174,6 +176,7 @@ impl RpcResult { } /// Converts from `RpcResult` to `Option`. + #[allow(clippy::missing_const_for_fn)] // erroneous lint pub fn err(self) -> Option { match self { RpcResult::Err(err) => Some(err), diff --git a/crates/pubsub/src/frontend.rs b/crates/pubsub/src/frontend.rs index cdf61b2e4be..bc6dd2af33a 100644 --- a/crates/pubsub/src/frontend.rs +++ b/crates/pubsub/src/frontend.rs @@ -20,7 +20,7 @@ pub struct PubSubFrontend { impl PubSubFrontend { /// Create a new frontend. - pub(crate) fn new(tx: mpsc::UnboundedSender) -> Self { + pub(crate) const fn new(tx: mpsc::UnboundedSender) -> Self { Self { tx } } diff --git a/crates/pubsub/src/managers/active_sub.rs b/crates/pubsub/src/managers/active_sub.rs index a532138b68a..ccb501f0b0e 100644 --- a/crates/pubsub/src/managers/active_sub.rs +++ b/crates/pubsub/src/managers/active_sub.rs @@ -73,7 +73,7 @@ impl ActiveSubscription { /// Serialize the request as a boxed [`RawValue`]. /// /// This is used to (re-)send the request over the transport. - pub(crate) fn request(&self) -> &SerializedRequest { + pub(crate) const fn request(&self) -> &SerializedRequest { &self.request } diff --git a/crates/pubsub/src/managers/in_flight.rs b/crates/pubsub/src/managers/in_flight.rs index 1cf0b66437f..3ce5c3ccb1c 100644 --- a/crates/pubsub/src/managers/in_flight.rs +++ b/crates/pubsub/src/managers/in_flight.rs @@ -41,14 +41,14 @@ impl InFlight { } /// Get the method - pub(crate) fn method(&self) -> &'static str { + pub(crate) const fn method(&self) -> &'static str { self.request.method() } /// Get a reference to the serialized request. /// /// This is used to (re-)send the request over the transport. - pub(crate) fn request(&self) -> &SerializedRequest { + pub(crate) const fn request(&self) -> &SerializedRequest { &self.request } diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index b5c612f81c6..725c21468a6 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -23,9 +23,13 @@ serde_json.workspace = true tracing.workspace = true tower.workspace = true -reqwest = { workspace = true, optional = true } +alloy-primitives = { workspace = true, optional = true } alloy-pubsub = { workspace = true, optional = true } alloy-transport-ws = { workspace = true, optional = true } +hyper = { workspace = true, optional = true } +reqwest = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } +url = { workspace = true, optional = true } [dev-dependencies] alloy-primitives.workspace = true @@ -35,6 +39,7 @@ tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } [features] default = ["reqwest"] -reqwest = ["dep:reqwest"] -pubsub = ["dep:alloy-pubsub"] +reqwest = ["dep:reqwest", "alloy-transport-http/reqwest"] +hyper = ["dep:hyper", "alloy-transport-http/hyper"] +pubsub = ["dep:alloy-pubsub", "dep:alloy-primitives"] ws = ["pubsub", "dep:alloy-transport-ws"] \ No newline at end of file diff --git a/crates/rpc-client/src/builder.rs b/crates/rpc-client/src/builder.rs index b62a8811569..fb148069bcb 100644 --- a/crates/rpc-client/src/builder.rs +++ b/crates/rpc-client/src/builder.rs @@ -80,6 +80,8 @@ impl ClientBuilder { } #[cfg(feature = "pubsub")] + /// Connect a pubsub transport, producing an [`RpcClient`] with the provided + /// connection. pub async fn pubsub(self, pubsub_connect: C) -> Result, TransportError> where C: alloy_pubsub::PubSubConnect, @@ -91,6 +93,20 @@ impl ClientBuilder { Ok(self.transport(transport, is_local)) } + #[cfg(feature = "ws")] + /// Connect a WS transport, producing an [`RpcClient`] with the provided + /// connection + pub async fn ws( + self, + ws_connect: alloy_transport_ws::WsConnect, + ) -> Result, TransportError> + where + L: Layer, + L::Service: Transport, + { + self.pubsub(ws_connect).await + } + /// Connect a transport, producing an [`RpcClient`] with the provided /// connection. pub async fn connect(self, connect: C) -> Result, TransportError> diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index 22931e4f779..bd0ae4abaa3 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -42,7 +42,7 @@ impl RpcClient { impl RpcClient { /// Create a new [`RpcClient`] with the given transport. - pub fn new(t: T, is_local: bool) -> Self { + pub const fn new(t: T, is_local: bool) -> Self { Self { transport: t, is_local, @@ -85,7 +85,7 @@ impl RpcClient { /// a URL or other external input, this value is set on a best-efforts /// basis and may be incorrect. #[inline] - pub fn is_local(&self) -> bool { + pub const fn is_local(&self) -> bool { self.is_local } diff --git a/crates/transport-http/Cargo.toml b/crates/transport-http/Cargo.toml index a6c9411a276..3a0e9e99b2d 100644 --- a/crates/transport-http/Cargo.toml +++ b/crates/transport-http/Cargo.toml @@ -21,7 +21,6 @@ tower.workspace = true reqwest = { workspace = true, features = ["serde_json", "json"], optional = true } - [target.'cfg(not(target_arch = "wasm32"))'.dependencies.hyper] version = "0.14.27" optional = true diff --git a/crates/transport-http/src/lib.rs b/crates/transport-http/src/lib.rs index 9bf1caef1e7..0e0161bf9c3 100644 --- a/crates/transport-http/src/lib.rs +++ b/crates/transport-http/src/lib.rs @@ -1,3 +1,21 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +//! alloy-transport-http + #[cfg(all(not(target_arch = "wasm32"), feature = "hyper"))] mod hyper; @@ -37,7 +55,7 @@ impl Http { } /// Create a new [`Http`] transport with a custom client. - pub fn with_client(client: T, url: Url) -> Self { + pub const fn with_client(client: T, url: Url) -> Self { Self { client, url } } @@ -61,7 +79,7 @@ impl Http { } /// Get a reference to the client. - pub fn client(&self) -> &T { + pub const fn client(&self) -> &T { &self.client } diff --git a/crates/transport/src/error.rs b/crates/transport/src/error.rs index 97e875658fd..c06e9a86167 100644 --- a/crates/transport/src/error.rs +++ b/crates/transport/src/error.rs @@ -32,7 +32,7 @@ pub enum TransportError { impl TransportError { /// Instantiate a new `TransportError` from a [`serde_json::Error`]. This /// should be called when the error occurs during serialization. - pub fn ser_err(err: serde_json::Error) -> Self { + pub const fn ser_err(err: serde_json::Error) -> Self { Self::SerdeJson { err, text: None } } From 6ad5ddc098cbd41fa9b45d43850b1dbe05a26a47 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 16:11:24 -0800 Subject: [PATCH 26/30] fix: impl PubSubConnect for WsConnect in wasm --- crates/transport-ws/Cargo.toml | 11 ++- crates/transport-ws/src/lib.rs | 22 ++++++ crates/transport-ws/src/native.rs | 107 +++++++++++++++--------------- crates/transport-ws/src/wasm.rs | 43 ++++++++++-- crates/transport/Cargo.toml | 1 - 5 files changed, 116 insertions(+), 68 deletions(-) diff --git a/crates/transport-ws/Cargo.toml b/crates/transport-ws/Cargo.toml index 6e2c0e861f3..a2cf526be1a 100644 --- a/crates/transport-ws/Cargo.toml +++ b/crates/transport-ws/Cargo.toml @@ -12,24 +12,23 @@ exclude.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -alloy-json-rpc.workspace = true alloy-pubsub.workspace = true alloy-transport.workspace = true futures.workspace = true -http = "0.2.9" serde_json.workspace = true tokio = { version = "1.33.0", features = ["sync", "rt"] } tracing.workspace = true +# non-WASM only +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"]} +http = "0.2.9" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1.33.0" features = ["sync", "rt", "time"] +# WASM only [target.'cfg(target_arch = "wasm32")'.dependencies] ws_stream_wasm = "0.7.4" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio-tungstenite = { version = "0.20.1", features = ["rustls-tls-webpki-roots"]} - diff --git a/crates/transport-ws/src/lib.rs b/crates/transport-ws/src/lib.rs index e2246777652..124a2d468e5 100644 --- a/crates/transport-ws/src/lib.rs +++ b/crates/transport-ws/src/lib.rs @@ -1,3 +1,21 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +//! alloy-transports-ws + #[cfg(not(target_arch = "wasm32"))] mod native; #[cfg(not(target_arch = "wasm32"))] @@ -18,13 +36,17 @@ use tracing::{debug, error, trace}; /// [`PubSubConnect`] to get a running service with a running backend. /// /// [`PubSubConnect`]: alloy_pubsub::PubSubConnect +#[derive(Debug)] pub struct WsBackend { + /// The websocket connection. pub(crate) socket: T, + /// The interface to the connection. pub(crate) interface: ConnectionInterface, } impl WsBackend { + /// Handle inbound text from the websocket. #[tracing::instrument(skip(self))] pub async fn handle_text(&mut self, t: String) -> Result<(), ()> { debug!(text = t, "Received message from websocket"); diff --git a/crates/transport-ws/src/native.rs b/crates/transport-ws/src/native.rs index a4e4a4f9368..0112ab24d2e 100644 --- a/crates/transport-ws/src/native.rs +++ b/crates/transport-ws/src/native.rs @@ -1,11 +1,11 @@ use crate::WsBackend; use alloy_pubsub::PubSubConnect; -use alloy_transport::{utils::Spawnable, Authorization, TransportError}; +use alloy_transport::{utils::Spawnable, Authorization, Pbf, TransportError}; use futures::{SinkExt, StreamExt}; use serde_json::value::RawValue; -use std::{future::Future, pin::Pin, time::Duration}; +use std::time::Duration; use tokio::time::sleep; use tokio_tungstenite::{ tungstenite::{self, client::IntoClientRequest, Message}, @@ -17,7 +17,57 @@ type TungsteniteStream = WebSocketStream>; const KEEPALIVE: u64 = 10; +/// Simple connection details for a websocket connection. +#[derive(Debug, Clone)] +pub struct WsConnect { + /// The URL to connect to. + pub url: String, + /// The authorization header to use. + pub auth: Option, +} + +impl IntoClientRequest for WsConnect { + fn into_client_request(self) -> tungstenite::Result { + let mut request: http::Request<()> = self.url.into_client_request()?; + if let Some(auth) = self.auth { + let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?; + auth_value.set_sensitive(true); + + request + .headers_mut() + .insert(http::header::AUTHORIZATION, auth_value); + } + + request.into_client_request() + } +} + +impl PubSubConnect for WsConnect { + fn is_local(&self) -> bool { + alloy_transport::utils::guess_local_url(&self.url) + } + + fn connect<'a: 'b, 'b>(&'a self) -> Pbf<'b, alloy_pubsub::ConnectionHandle, TransportError> { + let request = self.clone().into_client_request(); + + Box::pin(async move { + let req = request.map_err(TransportError::custom)?; + let (socket, _) = tokio_tungstenite::connect_async(req) + .await + .map_err(TransportError::custom)?; + + let (handle, interface) = alloy_pubsub::ConnectionHandle::new(); + let backend = WsBackend { socket, interface }; + + backend.spawn(); + + Ok(handle) + }) + } +} + impl WsBackend { + /// Handle a message from the server. pub async fn handle(&mut self, msg: Message) -> Result<(), ()> { match msg { Message::Text(text) => self.handle_text(text).await, @@ -39,6 +89,7 @@ impl WsBackend { } } + /// Send a message to the server. pub async fn send(&mut self, msg: Box) -> Result<(), tungstenite::Error> { self.socket.send(Message::Text(msg.get().to_owned())).await } @@ -119,55 +170,3 @@ impl WsBackend { fut.spawn_task() } } - -#[derive(Debug, Clone)] -pub struct WsConnect { - pub url: String, - pub auth: Option, -} - -impl IntoClientRequest for WsConnect { - fn into_client_request(self) -> tungstenite::Result { - let mut request: http::Request<()> = self.url.into_client_request()?; - if let Some(auth) = self.auth { - let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?; - auth_value.set_sensitive(true); - - request - .headers_mut() - .insert(http::header::AUTHORIZATION, auth_value); - } - - request.into_client_request() - } -} - -impl PubSubConnect for WsConnect { - fn is_local(&self) -> bool { - alloy_transport::utils::guess_local_url(&self.url) - } - - fn connect<'a: 'b, 'b>( - &'a self, - ) -> Pin< - Box< - dyn Future> + Send + 'b, - >, - > { - let request = self.clone().into_client_request(); - - Box::pin(async move { - let req = request.map_err(TransportError::custom)?; - let (socket, _) = tokio_tungstenite::connect_async(req) - .await - .map_err(TransportError::custom)?; - - let (handle, interface) = alloy_pubsub::ConnectionHandle::new(); - let backend = WsBackend { socket, interface }; - - backend.spawn(); - - Ok(handle) - }) - } -} diff --git a/crates/transport-ws/src/wasm.rs b/crates/transport-ws/src/wasm.rs index 9ae68b295ed..af20dadbac8 100644 --- a/crates/transport-ws/src/wasm.rs +++ b/crates/transport-ws/src/wasm.rs @@ -1,15 +1,47 @@ use super::WsBackend; -use alloy_transport::utils::Spawnable; +use alloy_pubsub::PubSubConnect; +use alloy_transport::{utils::Spawnable, Pbf, TransportError}; use futures::{ sink::SinkExt, stream::{Fuse, StreamExt}, }; use serde_json::value::RawValue; use tracing::error; -use ws_stream_wasm::{WsErr, WsMessage, WsStream}; +use ws_stream_wasm::{WsErr, WsMessage, WsMeta, WsStream}; + +/// Simple connection info for the websocket. +#[derive(Debug, Clone)] +pub struct WsConnect { + /// The URL to connect to. + pub url: String, +} + +impl PubSubConnect for WsConnect { + fn is_local(&self) -> bool { + alloy_transport::utils::guess_local_url(&self.url) + } + + fn connect<'a: 'b, 'b>(&'a self) -> Pbf<'b, alloy_pubsub::ConnectionHandle, TransportError> { + Box::pin(async move { + let socket = WsMeta::connect(&self.url, None) + .await + .map_err(TransportError::custom)? + .1 + .fuse(); + + let (handle, interface) = alloy_pubsub::ConnectionHandle::new(); + let backend = WsBackend { socket, interface }; + + backend.spawn(); + + Ok(handle) + }) + } +} impl WsBackend> { + /// Handle a message from the websocket. pub async fn handle(&mut self, item: WsMessage) -> Result<(), ()> { match item { WsMessage::Text(text) => self.handle_text(text).await, @@ -20,12 +52,14 @@ impl WsBackend> { } } + /// Send a message to the websocket. pub async fn send(&mut self, msg: Box) -> Result<(), WsErr> { self.socket .send(WsMessage::Text(msg.get().to_owned())) .await } + /// Spawn this backend on a loop. pub fn spawn(mut self) { let fut = async move { let mut err = false; @@ -80,8 +114,3 @@ impl WsBackend> { fut.spawn_task(); } } - -#[derive(Debug, Clone)] -pub struct WsConnect { - pub url: String, -} diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index f6526f0b4f6..53993a8065a 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -18,7 +18,6 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["raw_value"] } thiserror.workspace = true url.workspace = true -tokio.workspace = true tower.workspace = true base64 = "0.21.0" From f59daa68989b956a36c5488f6852335133ab8ab4 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 16:14:43 -0800 Subject: [PATCH 27/30] docs: fix some backticks --- crates/json-rpc/src/response/mod.rs | 2 +- crates/pubsub/src/connect.rs | 2 +- crates/transport/src/connect.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/json-rpc/src/response/mod.rs b/crates/json-rpc/src/response/mod.rs index 1b0c234ffbd..811c8655038 100644 --- a/crates/json-rpc/src/response/mod.rs +++ b/crates/json-rpc/src/response/mod.rs @@ -75,7 +75,7 @@ where /// /// # Returns /// - /// - `Ok(Response)`` if the payload is a success and can be + /// - `Ok(Response)` if the payload is a success and can be /// deserialized as T, or if the payload is an error. /// - `Err(self)` if the payload is a success and can't be deserialized. pub fn deser_success(self) -> Result, Self> { diff --git a/crates/pubsub/src/connect.rs b/crates/pubsub/src/connect.rs index 39b100b74ec..5e74f08d9e0 100644 --- a/crates/pubsub/src/connect.rs +++ b/crates/pubsub/src/connect.rs @@ -6,7 +6,7 @@ use alloy_transport::{Pbf, TransportError}; /// Implementers should contain configuration options for the underlying /// transport. pub trait PubSubConnect: Sized + Send + Sync + 'static { - /// Returns `true`` if the transport connects to a local resource. + /// Returns `true` if the transport connects to a local resource. fn is_local(&self) -> bool; /// Spawn the backend, returning a handle to it. diff --git a/crates/transport/src/connect.rs b/crates/transport/src/connect.rs index b7f45706330..70689c5b88d 100644 --- a/crates/transport/src/connect.rs +++ b/crates/transport/src/connect.rs @@ -17,7 +17,7 @@ pub trait TransportConnect: Sized + Send + Sync + 'static { /// The transport type that is returned by `connect`. type Transport: Transport + Clone; - /// Returns `true`` if the transport connects to a local resource. + /// Returns `true` if the transport connects to a local resource. fn is_local(&self) -> bool; /// Connect to the transport, returning a `Transport` instance. @@ -33,9 +33,9 @@ pub trait TransportConnect: Sized + Send + Sync + 'static { /// This trait is separate from `TransportConnect`` to hide the associated type /// in when this trait is a trai object. It is intended to allow creation of /// several unlike transports or clients at once. E.g. -/// `Vec<&dyn BoxTransportConnect>.into_iter().map(|t| t.connect_boxed())`. +/// in something like `Vec<&dyn BoxTransportConnect>. pub trait BoxTransportConnect { - /// Returns `true`` if the transport is a local transport. + /// Returns `true` if the transport is a local transport. fn is_local(&self) -> bool; /// Connect to a transport, and box it. From 093d5ec31b1698193fbf36200ffd26dc0e874868 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 16:15:45 -0800 Subject: [PATCH 28/30] fix: url in deps --- crates/rpc-client/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 725c21468a6..123b17bc226 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -39,7 +39,7 @@ tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } [features] default = ["reqwest"] -reqwest = ["dep:reqwest", "alloy-transport-http/reqwest"] -hyper = ["dep:hyper", "alloy-transport-http/hyper"] +reqwest = ["dep:url", "dep:reqwest", "alloy-transport-http/reqwest"] +hyper = ["dep:url", "dep:hyper", "alloy-transport-http/hyper"] pubsub = ["dep:alloy-pubsub", "dep:alloy-primitives"] ws = ["pubsub", "dep:alloy-transport-ws"] \ No newline at end of file From 8dfb6754c59842e77b3206befa54068e214d7f3e Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 16:18:34 -0800 Subject: [PATCH 29/30] fix: 1 url type --- crates/rpc-client/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-client/src/builder.rs b/crates/rpc-client/src/builder.rs index fb148069bcb..318f842c543 100644 --- a/crates/rpc-client/src/builder.rs +++ b/crates/rpc-client/src/builder.rs @@ -54,7 +54,7 @@ impl ClientBuilder { /// Convenience function to create a new [`RpcClient`] with a [`reqwest`] /// HTTP transport. #[cfg(feature = "reqwest")] - pub fn reqwest_http(self, url: reqwest::Url) -> RpcClient + pub fn reqwest_http(self, url: url::Url) -> RpcClient where L: Layer>, L::Service: Transport, From 245ff6869925d171d393b94f490f49c523caade7 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Nov 2023 16:19:39 -0800 Subject: [PATCH 30/30] fix: dep tokio --- crates/rpc-client/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 123b17bc226..e33d07a3e30 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -41,5 +41,5 @@ tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } default = ["reqwest"] reqwest = ["dep:url", "dep:reqwest", "alloy-transport-http/reqwest"] hyper = ["dep:url", "dep:hyper", "alloy-transport-http/hyper"] -pubsub = ["dep:alloy-pubsub", "dep:alloy-primitives"] +pubsub = ["dep:tokio", "dep:alloy-pubsub", "dep:alloy-primitives"] ws = ["pubsub", "dep:alloy-transport-ws"] \ No newline at end of file