From fa95d24acfb8d778fe18cf4a2afc655eac055988 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Feb 2024 11:13:39 -0800 Subject: [PATCH 01/27] wip: weak and ref clients --- crates/contract/src/instance.rs | 2 +- crates/providers/src/builder.rs | 3 +- crates/providers/src/heart.rs | 1 + crates/providers/src/lib.rs | 151 +------------------ crates/providers/src/parameterized.rs | 148 ++++++++++++++++++ crates/providers/src/{provider.rs => tmp.rs} | 6 +- crates/rpc-client/Cargo.toml | 2 + crates/rpc-client/src/batch.rs | 6 +- crates/rpc-client/src/call.rs | 26 ++++ crates/rpc-client/src/client.rs | 93 ++++++++---- crates/rpc-client/src/lib.rs | 5 +- crates/rpc-client/src/poller.rs | 54 +++++++ 12 files changed, 313 insertions(+), 184 deletions(-) create mode 100644 crates/providers/src/heart.rs create mode 100644 crates/providers/src/parameterized.rs rename crates/providers/src/{provider.rs => tmp.rs} (99%) create mode 100644 crates/rpc-client/src/poller.rs diff --git a/crates/contract/src/instance.rs b/crates/contract/src/instance.rs index b0a05bb9c32..e94ee9d9cae 100644 --- a/crates/contract/src/instance.rs +++ b/crates/contract/src/instance.rs @@ -2,7 +2,7 @@ use crate::{CallBuilder, Interface, Result}; use alloy_dyn_abi::DynSolValue; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Selector}; -use alloy_providers::provider::TempProvider; +use alloy_providers::tmp::TempProvider; /// A handle to an Ethereum contract at a specific address. /// diff --git a/crates/providers/src/builder.rs b/crates/providers/src/builder.rs index d69a7ad359a..0acb85db0f4 100644 --- a/crates/providers/src/builder.rs +++ b/crates/providers/src/builder.rs @@ -1,4 +1,5 @@ -use crate::{NetworkRpcClient, Provider}; +use crate::parameterized::{NetworkRpcClient, Provider}; + use alloy_network::Network; use alloy_rpc_client::RpcClient; use alloy_transport::Transport; diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs new file mode 100644 index 00000000000..77072727ec5 --- /dev/null +++ b/crates/providers/src/heart.rs @@ -0,0 +1 @@ +//! WIP diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 9001de90dac..88ed135cf1d 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -16,157 +16,10 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use alloy_network::{Network, Transaction}; -use alloy_primitives::Address; -use alloy_rpc_client::RpcClient; -use alloy_transport::{BoxTransport, Transport, TransportResult}; -use std::{borrow::Cow, marker::PhantomData}; - mod builder; pub use builder::{ProviderBuilder, ProviderLayer, Stack}; -pub mod provider; +pub mod tmp; pub mod utils; -/// A network-wrapped RPC client. -/// -/// This type allows you to specify (at the type-level) that the RPC client is -/// for a specific network. This helps avoid accidentally using the wrong -/// connection to access a network. -#[derive(Debug)] -pub struct NetworkRpcClient { - pub network: PhantomData N>, - pub client: RpcClient, -} - -impl std::ops::Deref for NetworkRpcClient -where - N: Network, - T: Transport, -{ - type Target = RpcClient; - - fn deref(&self) -> &Self::Target { - &self.client - } -} - -impl From> for NetworkRpcClient -where - N: Network, - T: Transport, -{ - fn from(client: RpcClient) -> Self { - Self { network: PhantomData, client } - } -} - -impl From> for RpcClient -where - N: Network, - T: Transport, -{ - fn from(client: NetworkRpcClient) -> Self { - client.client - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -/// Provider is parameterized with a network and a transport. The default -/// transport is type-erased, but you can do `Provider`. -pub trait Provider: Send + Sync { - fn raw_client(&self) -> &RpcClient { - &self.client().client - } - - /// Return a reference to the inner RpcClient. - fn client(&self) -> &NetworkRpcClient; - - /// Return a reference to the inner Provider. - /// - /// Providers are object safe now :) - fn inner(&self) -> &dyn Provider; - - async fn estimate_gas( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult { - self.inner().estimate_gas(tx).await - } - - /// Get the transaction count for an address. Used for finding the - /// appropriate nonce. - /// - /// TODO: block number/hash/tag - async fn get_transaction_count( - &self, - address: Address, - ) -> TransportResult { - self.inner().get_transaction_count(address).await - } - - /// Send a transaction to the network. - /// - /// The transaction type is defined by the network. - async fn send_transaction( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult { - self.inner().send_transaction(tx).await - } - - async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { - let gas = self.estimate_gas(&*tx).await; - - gas.map(|gas| tx.set_gas_limit(gas.try_into().unwrap())) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl Provider for NetworkRpcClient { - fn client(&self) -> &NetworkRpcClient { - self - } - - fn inner(&self) -> &dyn Provider { - panic!("called inner on ") - } - - async fn estimate_gas( - &self, - tx: &::TransactionRequest, - ) -> TransportResult { - self.prepare("eth_estimateGas", Cow::Borrowed(tx)).await - } - - async fn get_transaction_count( - &self, - address: Address, - ) -> TransportResult { - self.prepare( - "eth_getTransactionCount", - Cow::<(Address, String)>::Owned((address, "latest".to_string())), - ) - .await - } - - async fn send_transaction( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult { - self.prepare("eth_sendTransaction", Cow::Borrowed(tx)).await - } -} - -#[cfg(test)] -mod test { - use crate::Provider; - use alloy_network::Network; - - // checks that `Provider` is object-safe - fn __compile_check() -> Box> { - unimplemented!() - } -} +pub mod parameterized; diff --git a/crates/providers/src/parameterized.rs b/crates/providers/src/parameterized.rs new file mode 100644 index 00000000000..41c4096abfe --- /dev/null +++ b/crates/providers/src/parameterized.rs @@ -0,0 +1,148 @@ +use alloy_network::{Network, Transaction}; +use alloy_primitives::Address; +use alloy_rpc_client::RpcClient; +use alloy_transport::{BoxTransport, Transport, TransportResult}; +use std::{borrow::Cow, marker::PhantomData}; + +/// A network-wrapped RPC client. +/// +/// This type allows you to specify (at the type-level) that the RPC client is +/// for a specific network. This helps avoid accidentally using the wrong +/// connection to access a network. +#[derive(Debug)] +pub struct NetworkRpcClient { + pub network: PhantomData N>, + pub client: RpcClient, +} + +impl std::ops::Deref for NetworkRpcClient +where + N: Network, + T: Transport, +{ + type Target = RpcClient; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl From> for NetworkRpcClient +where + N: Network, + T: Transport, +{ + fn from(client: RpcClient) -> Self { + Self { network: PhantomData, client } + } +} + +impl From> for RpcClient +where + N: Network, + T: Transport, +{ + fn from(client: NetworkRpcClient) -> Self { + client.client + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +/// Provider is parameterized with a network and a transport. The default +/// transport is type-erased, but you can do `Provider`. +pub trait Provider: Send + Sync { + fn raw_client(&self) -> &RpcClient { + &self.client().client + } + + /// Return a reference to the inner RpcClient. + fn client(&self) -> &NetworkRpcClient; + + /// Return a reference to the inner Provider. + /// + /// Providers are object safe now :) + fn inner(&self) -> &dyn Provider; + + async fn estimate_gas( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult { + self.inner().estimate_gas(tx).await + } + + /// Get the transaction count for an address. Used for finding the + /// appropriate nonce. + /// + /// TODO: block number/hash/tag + async fn get_transaction_count( + &self, + address: Address, + ) -> TransportResult { + self.inner().get_transaction_count(address).await + } + + /// Send a transaction to the network. + /// + /// The transaction type is defined by the network. + async fn send_transaction( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult { + self.inner().send_transaction(tx).await + } + + async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { + let gas = self.estimate_gas(&*tx).await; + + gas.map(|gas| tx.set_gas_limit(gas.try_into().unwrap())) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Provider for NetworkRpcClient { + fn client(&self) -> &NetworkRpcClient { + self + } + + fn inner(&self) -> &dyn Provider { + panic!("called inner on ") + } + + async fn estimate_gas( + &self, + tx: &::TransactionRequest, + ) -> TransportResult { + self.prepare("eth_estimateGas", Cow::Borrowed(tx)).await + } + + async fn get_transaction_count( + &self, + address: Address, + ) -> TransportResult { + self.prepare( + "eth_getTransactionCount", + Cow::<(Address, String)>::Owned((address, "latest".to_string())), + ) + .await + } + + async fn send_transaction( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult { + self.prepare("eth_sendTransaction", Cow::Borrowed(tx)).await + } +} + +#[cfg(test)] +mod test { + use super::Provider; + use alloy_network::Network; + + // checks that `Provider` is object-safe + fn __compile_check() -> Box> { + unimplemented!() + } +} diff --git a/crates/providers/src/provider.rs b/crates/providers/src/tmp.rs similarity index 99% rename from crates/providers/src/provider.rs rename to crates/providers/src/tmp.rs index 934c5c99cdc..7a23b4885a7 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/tmp.rs @@ -33,7 +33,7 @@ pub type HttpProvider = Provider>; /// An abstract provider for interacting with the [Ethereum JSON RPC /// API](https://github.com/ethereum/wiki/wiki/JSON-RPC). Must be instantiated /// with a transport which implements the [Transport] trait. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Provider { inner: RpcClient, from: Option
, @@ -552,7 +552,7 @@ impl<'a> TryFrom<&'a String> for Provider> { #[cfg(test)] mod tests { use crate::{ - provider::{Provider, TempProvider}, + tmp::{Provider, TempProvider}, utils, }; use alloy_node_bindings::Anvil; @@ -676,7 +676,7 @@ mod tests { let _code = provider .get_code_at( addr, - crate::provider::BlockId::Number(alloy_rpc_types::BlockNumberOrTag::Latest), + crate::tmp::BlockId::Number(alloy_rpc_types::BlockNumberOrTag::Latest), ) .await .unwrap(); diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index add716b4b99..77ccdb4c4de 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -30,6 +30,8 @@ reqwest = { workspace = true, optional = true } url = { workspace = true, optional = true } serde = { workspace = true, optional = true } +tokio = { workspace = true, features = ["sync"] } +tokio-stream = { version = "0.1.14", features = ["sync"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] alloy-transport-ipc = { workspace = true, optional = true } diff --git a/crates/rpc-client/src/batch.rs b/crates/rpc-client/src/batch.rs index c1512efa63d..26cbb7ea510 100644 --- a/crates/rpc-client/src/batch.rs +++ b/crates/rpc-client/src/batch.rs @@ -1,4 +1,4 @@ -use crate::RpcClient; +use crate::client::RpcClientInner; use alloy_json_rpc::{ transform_response, try_deserialize_ok, Id, Request, RequestPacket, ResponsePacket, RpcParam, RpcReturn, SerializedRequest, @@ -24,7 +24,7 @@ pub(crate) type ChannelMap = HashMap; #[must_use = "A BatchRequest does nothing unless sent via `send_batch` and `.await`"] pub struct BatchRequest<'a, T> { /// The transport via which the batch will be sent. - transport: &'a RpcClient, + transport: &'a RpcClientInner, /// The requests to be sent. requests: RequestPacket, @@ -86,7 +86,7 @@ where impl<'a, T> BatchRequest<'a, T> { /// Create a new batch request. - pub fn new(transport: &'a RpcClient) -> Self { + pub fn new(transport: &'a RpcClientInner) -> Self { Self { transport, requests: RequestPacket::Batch(Vec::with_capacity(10)), diff --git a/crates/rpc-client/src/call.rs b/crates/rpc-client/src/call.rs index aaf1839dc87..c2f1560abb8 100644 --- a/crates/rpc-client/src/call.rs +++ b/crates/rpc-client/src/call.rs @@ -33,6 +33,21 @@ where Complete, } +impl Clone for CallState +where + Params: RpcParam, + Conn: Transport + Clone, +{ + fn clone(&self) -> Self { + match self { + Self::Prepared { request, connection } => { + Self::Prepared { request: request.clone(), connection: connection.clone() } + } + _ => panic!("cloned after dispatch"), + } + } +} + impl fmt::Debug for CallState where Params: RpcParam, @@ -152,6 +167,17 @@ where _pd: PhantomData Resp>, } +impl Clone for RpcCall +where + Conn: Transport + Clone, + Params: RpcParam, + Resp: RpcReturn, +{ + fn clone(&self) -> Self { + Self { state: self.state.clone(), _pd: PhantomData } + } +} + impl RpcCall where Conn: Transport + Clone, diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index 0cddf0251a8..6fc8c577415 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -2,9 +2,67 @@ use crate::{BatchRequest, ClientBuilder, RpcCall}; use alloy_json_rpc::{Id, Request, RpcParam, RpcReturn}; use alloy_transport::{BoxTransport, Transport, TransportConnect, TransportError}; use alloy_transport_http::Http; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::{ + ops::Deref, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, Weak, + }, +}; use tower::{layer::util::Identity, ServiceBuilder}; +/// An [`RpcClient`] in a [`Weak`] reference. +pub type WeakClient = Weak>; + +/// A borrowed [`RpcClient`]. +pub type ClientRef<'a, T> = &'a RpcClientInner; + +/// A JSON-RPC client. +#[derive(Debug, Clone)] +pub struct RpcClient(Arc>); + +impl RpcClient { + /// Create a new [`RpcClient`] with the given transport. + pub fn new(t: T, is_local: bool) -> Self { + Self(Arc::new(RpcClientInner::new(t, is_local))) + } + + /// Connect to a transport via a [`TransportConnect`] implementor. + pub async fn connect(connect: C) -> Result + where + T: Transport, + C: TransportConnect, + { + ClientBuilder::default().connect(connect).await + } + + /// Get a [`Weak`] reference to the client. + pub fn get_weak(&self) -> WeakClient { + Arc::downgrade(&self.0) + } + + /// Borrow the client. + pub fn get_ref(&self) -> ClientRef<'_, T> { + &self.0 + } +} + +impl RpcClient> { + /// Create a new [`BatchRequest`] builder. + #[inline] + pub fn new_batch(&self) -> BatchRequest<'_, Http> { + BatchRequest::new(&self.0) + } +} + +impl Deref for RpcClient { + type Target = RpcClientInner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// A JSON-RPC client. /// /// This struct manages a [`Transport`] and a request ID counter. It is used to @@ -18,7 +76,7 @@ use tower::{layer::util::Identity, ServiceBuilder}; /// no guarantee that a prepared [`RpcCall`] will be sent, or that a sent call /// will receive a response. #[derive(Debug)] -pub struct RpcClient { +pub struct RpcClientInner { /// The underlying transport. pub(crate) transport: T, /// `true` if the transport is local. @@ -27,28 +85,19 @@ pub struct RpcClient { pub(crate) id: AtomicU64, } -impl RpcClient { +impl RpcClientInner { /// Create a new [`ClientBuilder`]. pub fn builder() -> ClientBuilder { ClientBuilder { builder: ServiceBuilder::new() } } } -impl RpcClient { +impl RpcClientInner { /// Create a new [`RpcClient`] with the given transport. pub const fn new(t: T, is_local: bool) -> Self { Self { transport: t, is_local, id: AtomicU64::new(0) } } - /// Connect to a transport via a [`TransportConnect`] implementor. - pub async fn connect(connect: C) -> Result - where - T: Transport, - C: TransportConnect, - { - ClientBuilder::default().connect(connect).await - } - /// Build a `JsonRpcRequest` with the given method and params. /// /// This function reserves an ID for the request, however the request @@ -91,7 +140,7 @@ impl RpcClient { } } -impl RpcClient +impl RpcClientInner where T: Transport + Clone, { @@ -123,8 +172,8 @@ where /// erasing each type. E.g. if you have `RpcClient` and /// `RpcClient` you can put both into a `Vec>`. #[inline] - pub fn boxed(self) -> RpcClient { - RpcClient { transport: self.transport.boxed(), is_local: self.is_local, id: self.id } + pub fn boxed(self) -> RpcClientInner { + RpcClientInner { transport: self.transport.boxed(), is_local: self.is_local, id: self.id } } } @@ -133,7 +182,7 @@ mod pubsub_impl { use super::*; use alloy_pubsub::{PubSubConnect, PubSubFrontend, RawSubscription, Subscription}; - impl RpcClient { + impl RpcClientInner { /// Get a [`RawSubscription`] for the given subscription ID. pub async fn get_raw_subscription(&self, id: alloy_primitives::U256) -> RawSubscription { self.transport.get_subscription(id).await.unwrap() @@ -150,7 +199,7 @@ mod pubsub_impl { /// Connect to a transport via a [`PubSubConnect`] implementor. pub async fn connect_pubsub( connect: C, - ) -> Result, TransportError> + ) -> Result, TransportError> where C: PubSubConnect, { @@ -178,11 +227,3 @@ mod pubsub_impl { } } } - -impl RpcClient> { - /// Create a new [`BatchRequest`] builder. - #[inline] - 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 c0a148c4e8c..6fba02466bf 100644 --- a/crates/rpc-client/src/lib.rs +++ b/crates/rpc-client/src/lib.rs @@ -28,7 +28,10 @@ mod call; pub use call::RpcCall; mod client; -pub use client::RpcClient; +pub use client::{ClientRef, RpcClient, WeakClient}; + +mod poller; +pub use poller::PollStream; #[cfg(feature = "ws")] pub use alloy_transport_ws::WsConnect; diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs new file mode 100644 index 00000000000..d8aa5152170 --- /dev/null +++ b/crates/rpc-client/src/poller.rs @@ -0,0 +1,54 @@ +use std::marker::PhantomData; + +use alloy_json_rpc::{RpcParam, RpcReturn}; +use alloy_transport::{utils::Spawnable, Transport, TransportResult}; +use reqwest::Request; +use tokio::sync::broadcast; + +use crate::WeakClient; + +pub struct PollTask +where + Conn: Transport + Clone, + Params: RpcParam, + Resp: RpcReturn, +{ + client: WeakClient, + params: Params, + method: &'static str, + tx: broadcast::Sender>, + + duration: std::time::Duration, +} + +impl PollTask +where + Conn: Transport + Clone, + Params: RpcParam + 'static, + Resp: RpcReturn, +{ + /// Spawn the poller task. + pub fn spawn(self) { + let fut = async move { + loop { + tokio::time::sleep(self.duration).await; + + let client = match self.client.upgrade() { + Some(client) => client, + None => break, + }; + let resp = client.prepare(self.method, &self.params).await; + if self.tx.send(resp).is_err() { + break; + } + } + }; + fut.spawn_task() + } +} + +#[derive(Debug)] +pub struct PollStream { + _pd: PhantomData Resp>, + rx: tokio_stream::wrappers::BroadcastStream, +} From 52be2b88f1d3d9c49abe9102b878ebc59f539b84 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 3 Feb 2024 08:45:06 -0800 Subject: [PATCH 02/27] nit: use type alias --- crates/rpc-client/src/batch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rpc-client/src/batch.rs b/crates/rpc-client/src/batch.rs index 26cbb7ea510..58800dcb57d 100644 --- a/crates/rpc-client/src/batch.rs +++ b/crates/rpc-client/src/batch.rs @@ -1,4 +1,4 @@ -use crate::client::RpcClientInner; +use crate::{client::RpcClientInner, ClientRef}; use alloy_json_rpc::{ transform_response, try_deserialize_ok, Id, Request, RequestPacket, ResponsePacket, RpcParam, RpcReturn, SerializedRequest, @@ -24,7 +24,7 @@ pub(crate) type ChannelMap = HashMap; #[must_use = "A BatchRequest does nothing unless sent via `send_batch` and `.await`"] pub struct BatchRequest<'a, T> { /// The transport via which the batch will be sent. - transport: &'a RpcClientInner, + transport: ClientRef<'a, T>, /// The requests to be sent. requests: RequestPacket, From 70ecd4b5d7fda66b998da44b17c1cf2090625244 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 3 Feb 2024 10:15:47 -0800 Subject: [PATCH 03/27] feat: poller and subscription streams --- crates/contract/src/call.rs | 2 +- crates/contract/src/lib.rs | 2 +- crates/pubsub/Cargo.toml | 1 + crates/pubsub/src/sub.rs | 143 ++++++++++++++++++++++++++++++++ crates/rpc-client/src/client.rs | 46 +++++++--- crates/rpc-client/src/lib.rs | 2 +- crates/rpc-client/src/poller.rs | 126 ++++++++++++++++++++++++---- crates/transport/src/trait.rs | 8 ++ 8 files changed, 297 insertions(+), 33 deletions(-) diff --git a/crates/contract/src/call.rs b/crates/contract/src/call.rs index 45bfb11f38f..6f4737fcfb4 100644 --- a/crates/contract/src/call.rs +++ b/crates/contract/src/call.rs @@ -2,7 +2,7 @@ use crate::{Error, Result}; use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::Function; use alloy_primitives::{Address, Bytes, U256, U64}; -use alloy_providers::provider::TempProvider; +use alloy_providers::tmp::TempProvider; use alloy_rpc_types::{ request::{TransactionInput, TransactionRequest}, state::StateOverride, diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 5d12208e72d..64561fdce9b 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -34,5 +34,5 @@ pub use call::*; // NOTE: please avoid changing the API of this module due to its use in the `sol!` macro. #[doc(hidden)] pub mod private { - pub use alloy_providers::provider::TempProvider as Provider; + pub use alloy_providers::tmp::TempProvider as Provider; } diff --git a/crates/pubsub/Cargo.toml b/crates/pubsub/Cargo.toml index 264138c1d2c..c4f0263e915 100644 --- a/crates/pubsub/Cargo.toml +++ b/crates/pubsub/Cargo.toml @@ -21,5 +21,6 @@ futures.workspace = true serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["macros", "sync"] } +tokio-stream = { version = "0.1.14", features = ["sync"] } tower.workspace = true tracing.workspace = true diff --git a/crates/pubsub/src/sub.rs b/crates/pubsub/src/sub.rs index e01855a2569..802337ddae7 100644 --- a/crates/pubsub/src/sub.rs +++ b/crates/pubsub/src/sub.rs @@ -1,7 +1,11 @@ +use std::{pin::Pin, task}; + use alloy_primitives::B256; +use futures::{ready, Stream, StreamExt}; use serde::de::DeserializeOwned; use serde_json::value::RawValue; use tokio::sync::broadcast; +use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; /// A Subscription is a feed of notifications from the server, identified by a /// local ID. @@ -72,6 +76,11 @@ impl RawSubscription { pub fn try_recv(&mut self) -> Result, broadcast::error::TryRecvError> { self.rx.try_recv() } + + /// Convert the subscription into a stream. + pub fn into_stream(self) -> BroadcastStream> { + self.rx.into() + } } #[derive(Debug)] @@ -206,6 +215,15 @@ impl Subscription { self.inner.try_recv().map(Into::into) } + /// Convert the subscription into a stream that may yield unexpected types. + pub fn into_any_stream(self) -> SubAnyStream { + SubAnyStream { + id: self.local_id(), + inner: self.inner.into_stream(), + _pd: std::marker::PhantomData, + } + } + /// Wrapper for [`blocking_recv`]. Block the current thread until a message /// of the expected type is available. /// @@ -245,6 +263,15 @@ impl Subscription { } } + /// Convert the subscription into a stream. + pub fn into_stream(self) -> SubscriptionStream { + SubscriptionStream { + id: self.local_id(), + inner: self.inner.into_stream(), + _pd: std::marker::PhantomData, + } + } + /// Wrapper for [`blocking_recv`]. Block the current thread until a message /// is available, deserializing the message and returning the result. /// @@ -274,4 +301,120 @@ impl Subscription { ) -> Result, broadcast::error::TryRecvError> { self.inner.try_recv().map(|value| serde_json::from_str(value.get())) } + + /// Convert the subscription into a stream that returns deserialization + /// results. + pub fn into_result_stream(self) -> SubResultStream { + SubResultStream { + id: self.local_id(), + inner: self.inner.into_stream(), + _pd: std::marker::PhantomData, + } + } +} + +/// A stream of notifications from the server, identified by a local ID. This +/// stream may yield unexpected types. +#[derive(Debug)] +pub struct SubAnyStream { + id: B256, + inner: BroadcastStream>, + _pd: std::marker::PhantomData T>, +} + +impl SubAnyStream { + /// Get the local ID of the subscription. + pub fn id(&self) -> B256 { + self.id + } +} + +impl Stream for SubAnyStream { + type Item = Result, BroadcastStreamRecvError>; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + match ready!(self.inner.poll_next_unpin(cx)) { + Some(value) => task::Poll::Ready(Some(value.map(Into::into))), + None => task::Poll::Ready(None), + } + } +} + +/// A stream of notifications from the server, identified by a local ID. This/ +/// stream will yield only the expected type, discarding any notifications of +/// unexpected types. +#[derive(Debug)] +pub struct SubscriptionStream { + id: B256, + inner: BroadcastStream>, + _pd: std::marker::PhantomData T>, +} + +impl SubscriptionStream { + /// Get the local ID of the subscription. + pub fn id(&self) -> B256 { + self.id + } +} + +impl Stream for SubscriptionStream { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + loop { + match ready!(self.inner.poll_next_unpin(cx)) { + Some(Ok(value)) => match serde_json::from_str(value.get()) { + Ok(item) => return task::Poll::Ready(Some(Ok(item))), + Err(e) => { + trace!(value = value.get(), error = ?e, "Received unexpected value in subscription."); + continue; + } + }, + Some(Err(e)) => return task::Poll::Ready(Some(Err(e))), + None => return task::Poll::Ready(None), + } + } + } +} + +/// A stream of notifications from the server, identified by a local ID. This +/// stream will attempt to deserialize the notifications and yield the +/// `serde_json::Result` of the deserialization. +#[derive(Debug)] +pub struct SubResultStream { + id: B256, + inner: BroadcastStream>, + _pd: std::marker::PhantomData T>, +} + +impl SubResultStream { + /// Get the local ID of the subscription. + pub fn id(&self) -> B256 { + self.id + } +} + +impl Stream for SubResultStream { + type Item = Result, BroadcastStreamRecvError>; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + loop { + match ready!(self.inner.poll_next_unpin(cx)) { + Some(Ok(value)) => { + return task::Poll::Ready(Some(Ok(serde_json::from_str(value.get())))) + } + Some(Err(e)) => return task::Poll::Ready(Some(Err(e))), + None => return task::Poll::Ready(None), + } + } + } } diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index 6fc8c577415..5d53dda2c4e 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -1,4 +1,4 @@ -use crate::{BatchRequest, ClientBuilder, RpcCall}; +use crate::{poller::PollTask, BatchRequest, ClientBuilder, RpcCall}; use alloy_json_rpc::{Id, Request, RpcParam, RpcReturn}; use alloy_transport::{BoxTransport, Transport, TransportConnect, TransportError}; use alloy_transport_http::Http; @@ -27,15 +27,6 @@ impl RpcClient { Self(Arc::new(RpcClientInner::new(t, is_local))) } - /// Connect to a transport via a [`TransportConnect`] implementor. - pub async fn connect(connect: C) -> Result - where - T: Transport, - C: TransportConnect, - { - ClientBuilder::default().connect(connect).await - } - /// Get a [`Weak`] reference to the client. pub fn get_weak(&self) -> WeakClient { Arc::downgrade(&self.0) @@ -47,6 +38,36 @@ impl RpcClient { } } +impl RpcClient +where + T: Transport, +{ + /// Connect to a transport via a [`TransportConnect`] implementor. + pub async fn connect(connect: C) -> Result + where + C: TransportConnect, + { + ClientBuilder::default().connect(connect).await + } + + /// Poll a method with the given parameters. + /// + /// A [`PollTask`] + pub fn prepare_poller( + &self, + method: &'static str, + params: Params, + ) -> PollTask + where + T: Clone, + Params: RpcParam + 'static, + Resp: RpcReturn + Clone, + { + let request = self.make_request(method, params); + PollTask::new(self.get_weak(), method, request.params) + } +} + impl RpcClient> { /// Create a new [`BatchRequest`] builder. #[inline] @@ -171,7 +192,6 @@ where /// This is for abstracting over `RpcClient` for multiple `T` by /// erasing each type. E.g. if you have `RpcClient` and /// `RpcClient` you can put both into a `Vec>`. - #[inline] pub fn boxed(self) -> RpcClientInner { RpcClientInner { transport: self.transport.boxed(), is_local: self.is_local, id: self.id } } @@ -195,11 +215,13 @@ mod pubsub_impl { ) -> Subscription { Subscription::from(self.get_raw_subscription(id).await) } + } + impl RpcClient { /// Connect to a transport via a [`PubSubConnect`] implementor. pub async fn connect_pubsub( connect: C, - ) -> Result, TransportError> + ) -> Result, TransportError> where C: PubSubConnect, { diff --git a/crates/rpc-client/src/lib.rs b/crates/rpc-client/src/lib.rs index 6fba02466bf..1f3b6ad5f71 100644 --- a/crates/rpc-client/src/lib.rs +++ b/crates/rpc-client/src/lib.rs @@ -31,7 +31,7 @@ mod client; pub use client::{ClientRef, RpcClient, WeakClient}; mod poller; -pub use poller::PollStream; +pub use poller::{PollChannel, PollTask}; #[cfg(feature = "ws")] pub use alloy_transport_ws::WsConnect; diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index d8aa5152170..94055995f02 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -1,54 +1,144 @@ -use std::marker::PhantomData; +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, + time::Duration, +}; use alloy_json_rpc::{RpcParam, RpcReturn}; -use alloy_transport::{utils::Spawnable, Transport, TransportResult}; -use reqwest::Request; +use alloy_transport::{utils::Spawnable, Transport}; use tokio::sync::broadcast; +use tokio_stream::wrappers::BroadcastStream; use crate::WeakClient; +/// A Poller task. +#[derive(Debug)] pub struct PollTask where Conn: Transport + Clone, Params: RpcParam, Resp: RpcReturn, { + /// The client to poll with. client: WeakClient, - params: Params, + + /// Request Method method: &'static str, - tx: broadcast::Sender>, + /// Request Params + params: Params, + + // config options + channel_size: usize, + poll_interval: Duration, - duration: std::time::Duration, + _pd: PhantomData Resp>, } impl PollTask where Conn: Transport + Clone, Params: RpcParam + 'static, - Resp: RpcReturn, + Resp: RpcReturn + Clone, { - /// Spawn the poller task. - pub fn spawn(self) { + /// Create a new poller task. + pub(crate) fn new(client: WeakClient, method: &'static str, params: Params) -> Self { + Self { + client, + params, + method, + channel_size: 16, + poll_interval: Duration::from_secs(10), + _pd: PhantomData, + } + } + + /// Set the duration between polls. + pub fn set_poll_interval(&mut self, poll_interval: Duration) { + self.poll_interval = poll_interval; + } + + /// Set the duration between polls. + pub fn withpoll_interval(mut self, poll_interval: Duration) -> Self { + self.set_poll_interval(poll_interval); + self + } + + /// Spawn the poller task, producing a stream of responses. + pub fn spawn(self) -> PollChannel { + let (tx, rx) = broadcast::channel(self.channel_size); + let fut = async move { loop { - tokio::time::sleep(self.duration).await; - let client = match self.client.upgrade() { Some(client) => client, None => break, }; - let resp = client.prepare(self.method, &self.params).await; - if self.tx.send(resp).is_err() { - break; + + match client.prepare(self.method, &self.params).await { + Ok(resp) => { + if tx.send(resp).is_err() { + break; + } + } + Err(e) => { + debug!(%e, "Error response in polling request."); + } } + + tokio::time::sleep(self.poll_interval).await; } }; - fut.spawn_task() + fut.spawn_task(); + rx.into() } } +/// A stream of responses from a poller task. +/// +/// This stream is backed by a coroutine, and will continue to produce responses +/// until the poller task is dropped. The poller task is dropped when all +/// [`RpcClient`] instances are dropped, or when all listening PollStream are +/// dropped. +/// +/// The poller task also ignores errors from the server and deserialization +/// errors, and will continue to poll until the client is dropped. +/// +/// [`RpcClient`]: crate::RpcClient #[derive(Debug)] -pub struct PollStream { - _pd: PhantomData Resp>, - rx: tokio_stream::wrappers::BroadcastStream, +pub struct PollChannel { + rx: broadcast::Receiver, +} + +impl From> for PollChannel { + fn from(rx: broadcast::Receiver) -> Self { + Self { rx } + } +} + +impl Deref for PollChannel { + type Target = broadcast::Receiver; + + fn deref(&self) -> &Self::Target { + &self.rx + } +} +impl DerefMut for PollChannel { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.rx + } +} + +impl PollChannel +where + Resp: RpcReturn + Clone, +{ + /// Resubscribe to the poller task. + pub fn resubscribe(&self) -> Self { + Self { rx: self.rx.resubscribe() } + } + + /// Convert the poll_channel into a stream. + pub fn into_stream(self) -> BroadcastStream { + BroadcastStream::from(self.rx) + } } diff --git a/crates/transport/src/trait.rs b/crates/transport/src/trait.rs index db928a2893e..007a8c6b1b6 100644 --- a/crates/transport/src/trait.rs +++ b/crates/transport/src/trait.rs @@ -51,6 +51,14 @@ pub trait Transport: { BoxTransport::new(self) } + + /// Make a boxed trait object by cloning this transport. + fn as_boxed(&self) -> BoxTransport + where + Self: Sized + Clone + Send + Sync + 'static, + { + BoxTransport::new(self.clone()) + } } impl Transport for T where From b724da0e2f9f76d3f4556dc39a8539e10eb0b2cb Mon Sep 17 00:00:00 2001 From: James Date: Sat, 3 Feb 2024 10:17:45 -0800 Subject: [PATCH 04/27] lint: clippy --- crates/pubsub/src/sub.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/crates/pubsub/src/sub.rs b/crates/pubsub/src/sub.rs index 802337ddae7..d660549f6d0 100644 --- a/crates/pubsub/src/sub.rs +++ b/crates/pubsub/src/sub.rs @@ -324,7 +324,7 @@ pub struct SubAnyStream { impl SubAnyStream { /// Get the local ID of the subscription. - pub fn id(&self) -> B256 { + pub const fn id(&self) -> B256 { self.id } } @@ -355,7 +355,7 @@ pub struct SubscriptionStream { impl SubscriptionStream { /// Get the local ID of the subscription. - pub fn id(&self) -> B256 { + pub const fn id(&self) -> B256 { self.id } } @@ -395,7 +395,7 @@ pub struct SubResultStream { impl SubResultStream { /// Get the local ID of the subscription. - pub fn id(&self) -> B256 { + pub const fn id(&self) -> B256 { self.id } } @@ -407,14 +407,10 @@ impl Stream for SubResultStream { mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, ) -> task::Poll> { - loop { - match ready!(self.inner.poll_next_unpin(cx)) { - Some(Ok(value)) => { - return task::Poll::Ready(Some(Ok(serde_json::from_str(value.get())))) - } - Some(Err(e)) => return task::Poll::Ready(Some(Err(e))), - None => return task::Poll::Ready(None), - } + match ready!(self.inner.poll_next_unpin(cx)) { + Some(Ok(value)) => task::Poll::Ready(Some(Ok(serde_json::from_str(value.get())))), + Some(Err(e)) => task::Poll::Ready(Some(Err(e))), + None => task::Poll::Ready(None), } } } From 0c494b624b4cde385a35d63d4a12236a647aba03 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 3 Feb 2024 10:19:08 -0800 Subject: [PATCH 05/27] doc: fix links --- crates/rpc-client/src/client.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index 5d53dda2c4e..c651a79d3fd 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -93,9 +93,9 @@ impl Deref for RpcClient { /// ### Note /// /// IDs are allocated sequentially, starting at 0. IDs are reserved via -/// [`RpcClient::next_id`]. Note that allocated IDs may not be used. There is -/// no guarantee that a prepared [`RpcCall`] will be sent, or that a sent call -/// will receive a response. +/// [`RpcClientInner::next_id`]. Note that allocated IDs may not be used. There +/// is no guarantee that a prepared [`RpcCall`] will be sent, or that a sent +/// call will receive a response. #[derive(Debug)] pub struct RpcClientInner { /// The underlying transport. @@ -122,8 +122,8 @@ impl RpcClientInner { /// Build a `JsonRpcRequest` with the given method and params. /// /// This function reserves an ID for the request, however the request - /// is not sent. To send a request, use [`RpcClient::prepare`] and await - /// the returned [`RpcCall`]. + /// is not sent. To send a request, use [`RpcClientInner::prepare`] and + /// await the returned [`RpcCall`]. pub fn make_request( &self, method: &'static str, From 2a1ad274ee36e30ee502b08b5ff9c33a42cb3c7a Mon Sep 17 00:00:00 2001 From: James Date: Sat, 3 Feb 2024 12:06:59 -0800 Subject: [PATCH 06/27] feat: polling limit --- crates/rpc-client/src/client.rs | 4 ++-- crates/rpc-client/src/poller.rs | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index c651a79d3fd..0f7ddf8d04d 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -53,7 +53,7 @@ where /// Poll a method with the given parameters. /// /// A [`PollTask`] - pub fn prepare_poller( + pub fn prepare_static_poller( &self, method: &'static str, params: Params, @@ -63,7 +63,7 @@ where Params: RpcParam + 'static, Resp: RpcReturn + Clone, { - let request = self.make_request(method, params); + let request: Request = self.make_request(method, params); PollTask::new(self.get_weak(), method, request.params) } } diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index 94055995f02..8d4351695b2 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -24,12 +24,12 @@ where /// Request Method method: &'static str, - /// Request Params params: Params, // config options channel_size: usize, poll_interval: Duration, + limit: Option, _pd: PhantomData Resp>, } @@ -40,18 +40,24 @@ where Params: RpcParam + 'static, Resp: RpcReturn + Clone, { - /// Create a new poller task. - pub(crate) fn new(client: WeakClient, method: &'static str, params: Params) -> Self { + /// Create a new poller task with cloneable params. + pub fn new(client: WeakClient, method: &'static str, params: Params) -> Self { Self { client, - params, method, + params, channel_size: 16, poll_interval: Duration::from_secs(10), + limit: None, _pd: PhantomData, } } + /// Set a limit on the number of succesful polls. + pub fn set_limit(&mut self, limit: Option) { + self.limit = limit; + } + /// Set the duration between polls. pub fn set_poll_interval(&mut self, poll_interval: Duration) { self.poll_interval = poll_interval; @@ -68,7 +74,8 @@ where let (tx, rx) = broadcast::channel(self.channel_size); let fut = async move { - loop { + let limit = self.limit.unwrap_or(usize::MAX); + for _ in 0..limit { let client = match self.client.upgrade() { Some(client) => client, None => break, From babf0ef829e7140cd3d9533ca917bb8d8d433f63 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 3 Feb 2024 17:46:51 -0800 Subject: [PATCH 07/27] fix: name --- crates/rpc-client/src/poller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index 8d4351695b2..8ffc37fef1b 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -64,7 +64,7 @@ where } /// Set the duration between polls. - pub fn withpoll_interval(mut self, poll_interval: Duration) -> Self { + pub fn with_poll_interval(mut self, poll_interval: Duration) -> Self { self.set_poll_interval(poll_interval); self } From 51824bfeb993e2f6175cc4bb7e7748365252afc2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 5 Feb 2024 10:05:38 -0800 Subject: [PATCH 08/27] feat: block heartbeat --- crates/providers/Cargo.toml | 2 + crates/providers/src/heart.rs | 255 +++++++++++++++++++++++++++++++- crates/providers/src/lib.rs | 2 + crates/rpc-client/src/poller.rs | 9 +- 4 files changed, 265 insertions(+), 3 deletions(-) diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 24aceeaa6f1..e4e7e3d3dec 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -24,6 +24,8 @@ serde.workspace = true thiserror.workspace = true reqwest.workspace = true auto_impl = "1.1.0" +tokio = { version = "1.33.0", features = ["sync"] } +futures.workspace = true [dev-dependencies] alloy-node-bindings.workspace = true diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index 77072727ec5..a6c47917054 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -1 +1,254 @@ -//! WIP +#![allow(dead_code, unreachable_pub)] // TODO: remove +//! Block Hearbeat and Transaction Watcher + +use std::{ + collections::{BTreeMap, HashMap}, + future::Future, + time::Duration, +}; + +use alloy_primitives::{B256, U256}; +use alloy_rpc_types::Block; +use alloy_transport::utils::Spawnable; +use futures::{stream::StreamExt, FutureExt, Stream}; +use tokio::{ + select, + sync::{mpsc, oneshot, watch}, +}; + +/// A configuration object for watching for transaction confirmation. +pub struct WatchConfig { + /// The transaction hash to watch for. + tx_hash: B256, + + /// Require a number of confirmations. + confirmations: u64, + + /// Optional timeout for the transaction. + timeout: Option, + + /// Notify the waiter. + tx: oneshot::Sender<()>, +} + +impl WatchConfig { + /// Create a new watch for a transaction. + pub fn new(tx_hash: B256, tx: oneshot::Sender<()>) -> Self { + Self { tx_hash, confirmations: 0, timeout: Default::default(), tx } + } + + /// Set the number of confirmations to wait for. + pub fn with_confirmations(mut self, confirmations: u64) -> Self { + self.confirmations = confirmations; + self + } + + /// Set the timeout for the transaction. + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + /// Notify the waiter. + fn notify(self) { + let _ = self.tx.send(()); + } +} + +/// A pending transaction that can be awaited. +pub struct PendingTransaction { + /// The transaction hash. + tx_hash: B256, + /// The receiver for the notification. + // TODO: send a receipt? + rx: oneshot::Receiver<()>, +} + +impl PendingTransaction { + pub fn tx_hash(&self) -> B256 { + self.tx_hash + } +} + +impl Future for PendingTransaction { + type Output = (); + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + self.rx.poll_unpin(cx).map(|res| res.unwrap()) + } +} + +/// A handle to the heartbeat task. +pub struct HeartbeatHandle { + tx: mpsc::Sender, + latest: watch::Receiver, +} + +impl HeartbeatHandle { + /// Get a new watcher that always sees the latest block. + pub fn latest(&self) -> watch::Receiver { + self.latest.clone() + } +} + +// TODO: Parameterize with `Network` +/// A heartbeat task that receives blocks and watches for transactions. +pub(crate) struct Heartbeat { + /// The stream of incoming blocks to watch. + stream: futures::stream::Fuse, + + /// Transactions to watch for. + unconfirmed: HashMap, + + /// Ordered map of transactions waiting for confirmations. + waiting_confs: BTreeMap, + + /// Ordered map of transactions to reap at a certain time. + reap_at: BTreeMap, +} + +impl Heartbeat +where + St: Stream + Unpin + Send + 'static, +{ + /// Create a new heartbeat task. + pub fn new(stream: St) -> Self { + Self { + stream: stream.fuse(), + unconfirmed: Default::default(), + waiting_confs: Default::default(), + reap_at: Default::default(), + } + } +} + +impl Heartbeat { + /// Check if any transactions have enough confirmations to notify. + fn check_confirmations(&mut self, latest: &watch::Sender) { + if let Some(current_height) = { latest.borrow().header.number } { + let to_keep = self.waiting_confs.split_off(¤t_height); + let to_notify = std::mem::replace(&mut self.waiting_confs, to_keep); + + for (_, watcher) in to_notify.into_iter() { + watcher.notify(); + } + } + } + + /// Get the next time to reap a transaction. If no reaps, this is a very + /// long time from now (i.e. will not be woken). + fn next_reap(&self) -> std::time::Instant { + self.reap_at + .keys() + .next() + .copied() + .unwrap_or_else(|| std::time::Instant::now() + Duration::from_secs(60_000)) + } + + /// Reap any timeout + fn reap_timeouts(&mut self) { + let now = std::time::Instant::now(); + let to_keep = self.reap_at.split_off(&now); + let to_reap = std::mem::replace(&mut self.reap_at, to_keep); + + for tx_hash in to_reap.values() { + self.unconfirmed.remove(tx_hash); + } + } + + /// Handle a watch instruction by adding it to the watch list, and + /// potentially adding it to our `reap_at` list. + fn handle_watch_ix(&mut self, to_watch: WatchConfig) { + // start watching for the tx + if let Some(timeout) = to_watch.timeout { + self.reap_at.insert(std::time::Instant::now() + timeout, to_watch.tx_hash); + } + self.unconfirmed.insert(to_watch.tx_hash, to_watch); + } + + /// Handle a new block by checking if any of the transactions we're + /// watching are in it, and if so, notifying the watcher. Also updates + /// the latest block. + fn handle_new_block(&mut self, block: Block, latest: &watch::Sender) { + // Blocks without numbers are ignored, as they're not part of the chain. + let block_height = match block.header.number { + Some(height) => height, + None => return, + }; + + // check if we are watching for any of the txns in this block + let to_check = block + .transactions + .hashes() + .copied() + .filter_map(|tx_hash| self.unconfirmed.remove(&tx_hash)); + + // If confirmations is 1 or less, notify the watcher + for watcher in to_check { + if watcher.confirmations <= 1 { + watcher.notify(); + continue; + } + // Otherwise add it to the waiting list + self.waiting_confs.insert(block_height + U256::from(watcher.confirmations), watcher); + } + + // update the latest block. We use `send_replace` here to ensure the + // latest block is always up to date, even if no receivers exist + // C.f. + // https://docs.rs/tokio/latest/tokio/sync/watch/struct.Sender.html#method.send + let _ = latest.send_replace(block); + + self.check_confirmations(latest); + } +} + +impl Heartbeat +where + St: Stream + Unpin + Send + 'static, +{ + /// Spawn the heartbeat task, returning a [`HeartbeatHandle`] + pub(crate) fn spawn(mut self, from: Block) -> HeartbeatHandle { + let (latest, latest_rx) = watch::channel(from); + let (ix_tx, mut ixns) = mpsc::channel(16); + + let fut = async move { + 'shutdown: loop { + { + // We bias the select so that we always handle new messages + // before checking blocks, and reap timeouts are last. + let next_reap = self.next_reap(); + select! { + biased; + + // Watch for new transactions. + ix_opt = ixns.recv() => { + match ix_opt { + Some(to_watch) => self.handle_watch_ix(to_watch), + None => break 'shutdown, // ix channel is closed + } + }, + + // Wake up to handle new blocks + block = self.stream.select_next_some() => { + self.handle_new_block(block, &latest); + }, + + // This arm ensures we always wake up to reap timeouts, + // even if there are no other events. + _ = tokio::time::sleep_until(next_reap.into()) => {}, + } + } + + // Always reap timeouts + self.reap_timeouts(); + } + }; + + fut.spawn_task(); + HeartbeatHandle { tx: ix_tx, latest: latest_rx } + } +} diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 88ed135cf1d..a28e45c4bbb 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -23,3 +23,5 @@ pub mod tmp; pub mod utils; pub mod parameterized; + +mod heart; diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index 8ffc37fef1b..09752205335 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -100,11 +100,11 @@ where } } -/// A stream of responses from a poller task. +/// A channel yeildiing responses from a poller task. /// /// This stream is backed by a coroutine, and will continue to produce responses /// until the poller task is dropped. The poller task is dropped when all -/// [`RpcClient`] instances are dropped, or when all listening PollStream are +/// [`RpcClient`] instances are dropped, or when all listening `PollChannel` are /// dropped. /// /// The poller task also ignores errors from the server and deserialization @@ -149,3 +149,8 @@ where BroadcastStream::from(self.rx) } } + +fn __assert_unpin() { + fn _assert() {} + _assert::>(); +} From 39a148fd21c791229fed08b7dae5d9c19da9c971 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 6 Feb 2024 09:35:21 -0800 Subject: [PATCH 09/27] wip: chaintask --- crates/providers/src/builder.rs | 57 ++++----- crates/providers/src/chain.rs | 52 ++++++++ crates/providers/src/heart.rs | 1 + crates/providers/src/lib.rs | 13 +- crates/providers/src/new.rs | 173 ++++++++++++++++++++++++++ crates/providers/src/parameterized.rs | 148 ---------------------- crates/rpc-client/src/client.rs | 8 +- 7 files changed, 268 insertions(+), 184 deletions(-) create mode 100644 crates/providers/src/chain.rs create mode 100644 crates/providers/src/new.rs delete mode 100644 crates/providers/src/parameterized.rs diff --git a/crates/providers/src/builder.rs b/crates/providers/src/builder.rs index 0acb85db0f4..2ee1f99c377 100644 --- a/crates/providers/src/builder.rs +++ b/crates/providers/src/builder.rs @@ -1,4 +1,4 @@ -use crate::parameterized::{NetworkRpcClient, Provider}; +use crate::new::{Provider, RootProvider, RootProviderInner}; use alloy_network::Network; use alloy_rpc_client::RpcClient; @@ -14,20 +14,19 @@ pub trait ProviderLayer, N: Network, T: Transport> { fn layer(&self, inner: P) -> Self::Provider; } -pub struct Stack { +pub struct Stack { inner: Inner, outer: Outer, - _pd: std::marker::PhantomData T>, } -impl Stack { +impl Stack { /// Create a new `Stack`. pub fn new(inner: Inner, outer: Outer) -> Self { - Stack { inner, outer, _pd: std::marker::PhantomData } + Stack { inner, outer } } } -impl ProviderLayer for Stack +impl ProviderLayer for Stack where T: Transport, N: Network, @@ -50,14 +49,13 @@ where /// around maintaining the network and transport types. /// /// [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html -pub struct ProviderBuilder { +pub struct ProviderBuilder { layer: L, - transport: PhantomData, network: PhantomData, } -impl ProviderBuilder { +impl ProviderBuilder { /// Add a layer to the stack being built. This is similar to /// [`tower::ServiceBuilder::layer`]. /// @@ -71,12 +69,8 @@ impl ProviderBuilder { /// [`tower::ServiceBuilder::layer`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html#method.layer /// [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html - pub fn layer(self, layer: Inner) -> ProviderBuilder> { - ProviderBuilder { - layer: Stack::new(layer, self.layer), - transport: PhantomData, - network: PhantomData, - } + pub fn layer(self, layer: Inner) -> ProviderBuilder> { + ProviderBuilder { layer: Stack::new(layer, self.layer), network: PhantomData } } /// Change the network. @@ -88,34 +82,35 @@ impl ProviderBuilder { /// ```rust,ignore /// builder.network::() /// ``` - pub fn network(self) -> ProviderBuilder { - ProviderBuilder { layer: self.layer, transport: self.transport, network: PhantomData } + pub fn network(self) -> ProviderBuilder { + ProviderBuilder { layer: self.layer, network: PhantomData } } - /// Finish the layer stack by providing a root [`RpcClient`], outputting + /// Finish the layer stack by providing a root [`Provider`], outputting /// the final [`Provider`] type with all stack components. - /// - /// This is a convenience function for - /// `ProviderBuilder::provider`. - pub fn client(self, client: RpcClient) -> L::Provider + pub fn provider(self, provider: P) -> L::Provider where - L: ProviderLayer, N, T>, - T: Transport + Clone, + L: ProviderLayer, + P: Provider, + T: Transport, N: Network, { - self.provider(NetworkRpcClient::from(client)) + self.layer.layer(provider) } - /// Finish the layer stack by providing a root [`Provider`], outputting + /// Finish the layer stack by providing a root [`RpcClient`], outputting /// the final [`Provider`] type with all stack components. - pub fn provider

(self, provider: P) -> L::Provider + /// + /// This is a convenience function for + /// `ProviderBuilder::provider`. + pub fn on_client(self, client: RpcClient) -> L::Provider where - L: ProviderLayer, - P: Provider, - T: Transport, + L: ProviderLayer, N, T>, + T: Transport + Clone, N: Network, { - self.layer.layer(provider) + let root = RootProviderInner::new(client); + self.provider(root.into()) } } diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs new file mode 100644 index 00000000000..07b337c3c3f --- /dev/null +++ b/crates/providers/src/chain.rs @@ -0,0 +1,52 @@ +use std::{marker::PhantomData, sync::Arc}; + +use alloy_network::Network; +use alloy_rpc_types::Block; +use alloy_transport::{utils::Spawnable, Transport}; +use tokio::sync::broadcast; + +use crate::{Provider, WeakProvider}; + +/// Task that emits an ordered set of blocks. +pub(crate) struct ChainTask { + provider: WeakProvider

, + + _transport: PhantomData (N, T)>, +} + +impl ChainTask +where + P: Provider, + N: Network, + T: Transport, +{ + pub fn new(client: WeakProvider

) -> Self { + Self { provider: client, _transport: PhantomData } + } + + /// Get the provider, if it still exists. + pub async fn provider(&self) -> Option> { + self.provider.upgrade() + } + + pub async fn get_height(&self) -> Option { + let provider = self.provider.upgrade()?; + + provider.get_block_number().await.ok() + } +} + +pub struct ChainListener { + rx: broadcast::Receiver, +} + +impl ChainTask +where + P: Provider, + N: Network, + T: Transport, +{ + pub fn spawn(self) -> ChainListener { + todo!() + } +} diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index a6c47917054..1a0121bad53 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -82,6 +82,7 @@ impl Future for PendingTransaction { } /// A handle to the heartbeat task. +#[derive(Debug, Clone)] pub struct HeartbeatHandle { tx: mpsc::Sender, latest: watch::Receiver, diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index a28e45c4bbb..90125238348 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -19,9 +19,14 @@ mod builder; pub use builder::{ProviderBuilder, ProviderLayer, Stack}; -pub mod tmp; -pub mod utils; - -pub mod parameterized; +mod chain; mod heart; + +pub mod new; +pub use new::{ProviderRef, RootProvider, WeakProvider, Provider}; + +pub mod utils; + +// TODO: remove +pub mod tmp; diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs new file mode 100644 index 00000000000..3c8191b0468 --- /dev/null +++ b/crates/providers/src/new.rs @@ -0,0 +1,173 @@ +use alloy_network::{Network, Transaction}; +use alloy_primitives::{Address, BlockNumber, U64}; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types::Block; +use alloy_transport::{BoxTransport, Transport, TransportResult}; +use auto_impl::auto_impl; +use std::{ + borrow::Cow, + marker::PhantomData, + ops::Deref, + sync::{Arc, Weak}, +}; + +use crate::heart::HeartbeatHandle; + +/// A [`Provider`] in a [`Weak`] reference. +pub type WeakProvider

= Weak

; + +/// A borrowed [`Provider`]. +pub type ProviderRef<'a, P> = &'a P; + +/// The root provider manages the RPC client and the heartbeat. It is at the +/// base of every provider stack. +pub struct RootProviderInner { + client: RpcClient, + + heart: Option, + + _network: PhantomData N>, +} + +impl RootProviderInner { + pub(crate) fn new(client: RpcClient) -> Self { + let this = Self { client, heart: None, _network: PhantomData }; + this + } + + /// Get a reference to the RPC client. + pub fn client_ref(&self) -> &RpcClient { + &self.client + } + + /// Get a clone of the RPC client. + pub fn client(&self) -> RpcClient { + self.client.clone() + } + + /// Init the heartbeat + async fn init_heartbeat(&mut self) { + if self.heart.is_some() { + return; + } + todo!() + } +} + +pub struct RootProvider { + pub inner: Arc>, +} + +impl From> for RootProvider { + fn from(inner: RootProviderInner) -> Self { + Self { inner: Arc::new(inner) } + } +} + +impl Clone for RootProviderInner { + fn clone(&self) -> Self { + Self { client: self.client.clone(), heart: self.heart.clone(), _network: PhantomData } + } +} + +impl Deref for RootProvider { + type Target = RootProviderInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +/// Provider is parameterized with a network and a transport. The default +/// transport is type-erased, but you can do `Provider`. +pub trait Provider: Send + Sync { + /// Get a reference to the RPC client. + fn client(&self) -> &RpcClient; + + async fn estimate_gas( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult; + + /// Get the last block number available. + async fn get_block_number(&self) -> TransportResult; + + /// Get the transaction count for an address. Used for finding the + /// appropriate nonce. + /// + /// TODO: block number/hash/tag + async fn get_transaction_count( + &self, + address: Address, + ) -> TransportResult; + + /// Get a block by its number. + /// + /// TODO: Network associate + async fn get_block_by_number( + &self, + number: BlockNumber, + hydrate: bool, + ) -> TransportResult; + + /// Populate the gas limit for a transaction. + async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { + let gas = self.estimate_gas(&*tx).await; + + gas.map(|gas| tx.set_gas_limit(gas.try_into().unwrap())) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Provider for RootProvider { + fn client(&self) -> &RpcClient { + &self.client_ref() + } + + async fn estimate_gas( + &self, + tx: &::TransactionRequest, + ) -> TransportResult { + self.client.prepare("eth_estimateGas", Cow::Borrowed(tx)).await + } + + async fn get_block_number(&self) -> TransportResult { + self.client.prepare("eth_blockNumber", ()).await.map(|num: U64| num.to::()) + } + + async fn get_block_by_number( + &self, + number: BlockNumber, + hydrate: bool, + ) -> TransportResult { + self.client + .prepare("eth_getBlockByNumber", Cow::<(BlockNumber, bool)>::Owned((number, hydrate))) + .await + } + + async fn get_transaction_count( + &self, + address: Address, + ) -> TransportResult { + self.client + .prepare( + "eth_getTransactionCount", + Cow::<(Address, String)>::Owned((address, "latest".to_string())), + ) + .await + } +} + +#[cfg(test)] +mod test { + use super::Provider; + use alloy_network::Network; + + // checks that `Provider` is object-safe + fn __compile_check() -> Box> { + unimplemented!() + } +} diff --git a/crates/providers/src/parameterized.rs b/crates/providers/src/parameterized.rs deleted file mode 100644 index 41c4096abfe..00000000000 --- a/crates/providers/src/parameterized.rs +++ /dev/null @@ -1,148 +0,0 @@ -use alloy_network::{Network, Transaction}; -use alloy_primitives::Address; -use alloy_rpc_client::RpcClient; -use alloy_transport::{BoxTransport, Transport, TransportResult}; -use std::{borrow::Cow, marker::PhantomData}; - -/// A network-wrapped RPC client. -/// -/// This type allows you to specify (at the type-level) that the RPC client is -/// for a specific network. This helps avoid accidentally using the wrong -/// connection to access a network. -#[derive(Debug)] -pub struct NetworkRpcClient { - pub network: PhantomData N>, - pub client: RpcClient, -} - -impl std::ops::Deref for NetworkRpcClient -where - N: Network, - T: Transport, -{ - type Target = RpcClient; - - fn deref(&self) -> &Self::Target { - &self.client - } -} - -impl From> for NetworkRpcClient -where - N: Network, - T: Transport, -{ - fn from(client: RpcClient) -> Self { - Self { network: PhantomData, client } - } -} - -impl From> for RpcClient -where - N: Network, - T: Transport, -{ - fn from(client: NetworkRpcClient) -> Self { - client.client - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -/// Provider is parameterized with a network and a transport. The default -/// transport is type-erased, but you can do `Provider`. -pub trait Provider: Send + Sync { - fn raw_client(&self) -> &RpcClient { - &self.client().client - } - - /// Return a reference to the inner RpcClient. - fn client(&self) -> &NetworkRpcClient; - - /// Return a reference to the inner Provider. - /// - /// Providers are object safe now :) - fn inner(&self) -> &dyn Provider; - - async fn estimate_gas( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult { - self.inner().estimate_gas(tx).await - } - - /// Get the transaction count for an address. Used for finding the - /// appropriate nonce. - /// - /// TODO: block number/hash/tag - async fn get_transaction_count( - &self, - address: Address, - ) -> TransportResult { - self.inner().get_transaction_count(address).await - } - - /// Send a transaction to the network. - /// - /// The transaction type is defined by the network. - async fn send_transaction( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult { - self.inner().send_transaction(tx).await - } - - async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { - let gas = self.estimate_gas(&*tx).await; - - gas.map(|gas| tx.set_gas_limit(gas.try_into().unwrap())) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl Provider for NetworkRpcClient { - fn client(&self) -> &NetworkRpcClient { - self - } - - fn inner(&self) -> &dyn Provider { - panic!("called inner on ") - } - - async fn estimate_gas( - &self, - tx: &::TransactionRequest, - ) -> TransportResult { - self.prepare("eth_estimateGas", Cow::Borrowed(tx)).await - } - - async fn get_transaction_count( - &self, - address: Address, - ) -> TransportResult { - self.prepare( - "eth_getTransactionCount", - Cow::<(Address, String)>::Owned((address, "latest".to_string())), - ) - .await - } - - async fn send_transaction( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult { - self.prepare("eth_sendTransaction", Cow::Borrowed(tx)).await - } -} - -#[cfg(test)] -mod test { - use super::Provider; - use alloy_network::Network; - - // checks that `Provider` is object-safe - fn __compile_check() -> Box> { - unimplemented!() - } -} diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index 0f7ddf8d04d..ba5bf853023 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -18,9 +18,15 @@ pub type WeakClient = Weak>; pub type ClientRef<'a, T> = &'a RpcClientInner; /// A JSON-RPC client. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct RpcClient(Arc>); +impl Clone for RpcClient { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + impl RpcClient { /// Create a new [`RpcClient`] with the given transport. pub fn new(t: T, is_local: bool) -> Self { From 0188a78149e352df1ed8c6acfcdcec29236804e2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Feb 2024 10:17:20 -0800 Subject: [PATCH 10/27] feat: chain stream poller take 1 --- crates/providers/Cargo.toml | 3 + crates/providers/src/chain.rs | 107 +++++++++++++++++++++++----------- crates/providers/src/lib.rs | 2 +- crates/providers/src/new.rs | 27 ++++++--- crates/transport/src/error.rs | 9 +++ crates/transport/src/lib.rs | 2 +- 6 files changed, 107 insertions(+), 43 deletions(-) diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index e4e7e3d3dec..986254c929f 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -26,6 +26,9 @@ reqwest.workspace = true auto_impl = "1.1.0" tokio = { version = "1.33.0", features = ["sync"] } futures.workspace = true +async-stream = "0.3.5" +lru = "0.12.2" +tracing.workspace = true [dev-dependencies] alloy-node-bindings.workspace = true diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index 07b337c3c3f..f80f14f33b4 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -1,52 +1,91 @@ -use std::{marker::PhantomData, sync::Arc}; +use std::{num::NonZeroUsize, time::Duration}; use alloy_network::Network; +use alloy_primitives::BlockNumber; +use alloy_rpc_client::PollTask; use alloy_rpc_types::Block; -use alloy_transport::{utils::Spawnable, Transport}; -use tokio::sync::broadcast; +use alloy_transport::{RpcError, Transport}; +use async_stream::stream; +use futures::{Stream, StreamExt}; +use lru::LruCache; use crate::{Provider, WeakProvider}; -/// Task that emits an ordered set of blocks. -pub(crate) struct ChainTask { - provider: WeakProvider

, - - _transport: PhantomData (N, T)>, -} +/// The size of the block cache. +pub const BLOCK_CACHE_SIZE: NonZeroUsize = match NonZeroUsize::new(10) { + Some(size) => size, + None => panic!("BLOCK_CACHE_SIZE must be non-zero"), +}; -impl ChainTask +fn chain_stream_poller( + provider: WeakProvider

, + from_height: BlockNumber, + poll_interval: Duration, +) -> impl Stream where P: Provider, N: Network, - T: Transport, + T: Transport + Clone, { - pub fn new(client: WeakProvider

) -> Self { - Self { provider: client, _transport: PhantomData } - } + let mut poll_stream = provider + .upgrade() + .map(|provider| { + PollTask::new((&*provider).weak_client(), "eth_blockNumber", ()) + .with_poll_interval(poll_interval) + .spawn() + .into_stream() + }) + .expect("provider dropped before poller started"); - /// Get the provider, if it still exists. - pub async fn provider(&self) -> Option> { - self.provider.upgrade() - } + let mut next_yield = from_height; + let mut known_blocks: LruCache = LruCache::new(BLOCK_CACHE_SIZE); - pub async fn get_height(&self) -> Option { - let provider = self.provider.upgrade()?; + stream! { + 'task: loop { + // first clear any buffered blocks + if known_blocks.contains(&next_yield) { + next_yield += 1; + yield known_blocks.get(&next_yield).unwrap().clone(); + continue; + } - provider.get_block_number().await.ok() - } -} + let block_number = match poll_stream.next().await { + Some(Ok(block_number)) => block_number, + Some(Err(err)) => { + tracing::error!(%err, "polling stream lagged"); + continue; + }, + None => { + tracing::debug!("polling stream ended"); + break; + }, + }; -pub struct ChainListener { - rx: broadcast::Receiver, -} + let provider = match provider.upgrade() { + Some(provider) => provider, + None => { + tracing::debug!("provider dropped"); + break 'task; + }, + }; -impl ChainTask -where - P: Provider, - N: Network, - T: Transport, -{ - pub fn spawn(self) -> ChainListener { - todo!() + // Then try to fill as many blocks as possible + while !known_blocks.contains(&block_number) { + let block = provider.get_block_by_number(block_number, false).await; + match block { + Ok(block) => { + known_blocks.put(block_number, block); + }, + Err(RpcError::Transport(err)) if err.recoverable() => { + continue 'task; + }, + Err(err) => { + tracing::error!(block_number, %err, "failed to fetch block"); + break 'task; + }, + } + } + + } } } diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 90125238348..6f2d82cf7bd 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -24,7 +24,7 @@ mod chain; mod heart; pub mod new; -pub use new::{ProviderRef, RootProvider, WeakProvider, Provider}; +pub use new::{Provider, ProviderRef, RootProvider, WeakProvider}; pub mod utils; diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 3c8191b0468..ef764d2c444 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -1,9 +1,8 @@ use alloy_network::{Network, Transaction}; use alloy_primitives::{Address, BlockNumber, U64}; -use alloy_rpc_client::RpcClient; +use alloy_rpc_client::{ClientRef, RpcClient, WeakClient}; use alloy_rpc_types::Block; use alloy_transport::{BoxTransport, Transport, TransportResult}; -use auto_impl::auto_impl; use std::{ borrow::Cow, marker::PhantomData, @@ -35,9 +34,14 @@ impl RootProviderInner { this } + /// Get a weak reference to the RPC client. + pub fn weak_client(&self) -> WeakClient { + self.client.get_weak() + } + /// Get a reference to the RPC client. - pub fn client_ref(&self) -> &RpcClient { - &self.client + pub fn client_ref(&self) -> ClientRef<'_, T> { + self.client.get_ref() } /// Get a clone of the RPC client. @@ -54,6 +58,8 @@ impl RootProviderInner { } } +/// The root provider manages the RPC client and the heartbeat. It is at the +/// base of every provider stack. pub struct RootProvider { pub inner: Arc>, } @@ -84,7 +90,10 @@ impl Deref for RootProvider { /// transport is type-erased, but you can do `Provider`. pub trait Provider: Send + Sync { /// Get a reference to the RPC client. - fn client(&self) -> &RpcClient; + fn client_ref(&self) -> ClientRef<'_, T>; + + /// Get a weak reference to the RPC client. + fn weak_client(&self) -> WeakClient; async fn estimate_gas( &self, @@ -123,8 +132,12 @@ pub trait Provider: Send + Sync { #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for RootProvider { - fn client(&self) -> &RpcClient { - &self.client_ref() + fn client_ref(&self) -> ClientRef<'_, T> { + self.inner.client_ref() + } + + fn weak_client(&self) -> WeakClient { + self.inner.weak_client() } async fn estimate_gas( diff --git a/crates/transport/src/error.rs b/crates/transport/src/error.rs index b7ecb329705..d6cecffc149 100644 --- a/crates/transport/src/error.rs +++ b/crates/transport/src/error.rs @@ -32,6 +32,15 @@ pub enum TransportErrorKind { } impl TransportErrorKind { + /// Whether the error is potentially recoverable. This is a naive heuristic + /// and should be used with caution. + pub fn recoverable(&self) -> bool { + match self { + Self::MissingBatchResponse(_) => true, + _ => false, + } + } + /// Instantiate a new `TransportError` from a custom error. pub fn custom_str(err: &str) -> TransportError { RpcError::Transport(Self::Custom(err.into())) diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs index 1cca6314114..b57c1c21477 100644 --- a/crates/transport/src/lib.rs +++ b/crates/transport/src/lib.rs @@ -32,7 +32,7 @@ pub use error::{TransportError, TransportResult}; mod r#trait; pub use r#trait::Transport; -pub use alloy_json_rpc::RpcResult; +pub use alloy_json_rpc::{RpcError, RpcResult}; /// Misc. utilities for building transports. pub mod utils; From 698f57a0cfe21660077326a9311cd78a292ad96e Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Feb 2024 10:22:47 -0800 Subject: [PATCH 11/27] fix: pubsub --- crates/providers/src/chain.rs | 3 +++ crates/rpc-client/src/client.rs | 12 +----------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index f80f14f33b4..364f393d5ac 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -49,9 +49,11 @@ where continue; } + // then get the tip/ let block_number = match poll_stream.next().await { Some(Ok(block_number)) => block_number, Some(Err(err)) => { + // this is fine. tracing::error!(%err, "polling stream lagged"); continue; }, @@ -61,6 +63,7 @@ where }, }; + // then upgrade the provider let provider = match provider.upgrade() { Some(provider) => provider, None => { diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index ba5bf853023..c7349317e0f 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -240,18 +240,8 @@ mod pubsub_impl { /// behavior. /// /// [`tokio::sync::broadcast`]: https://docs.rs/tokio/latest/tokio/sync/broadcast/index.html - pub const fn channel_size(&self) -> usize { + pub fn channel_size(&self) -> usize { self.transport.channel_size() } - - /// Set the channel size. This is the number of items to buffer in new - /// subscription channels. Defaults to 16. See - /// [`tokio::sync::broadcast`] for a description of relevant - /// behavior. - /// - /// [`tokio::sync::broadcast`]: https://docs.rs/tokio/latest/tokio/sync/broadcast/index.html - pub fn set_channel_size(&mut self, size: usize) { - self.transport.set_channel_size(size); - } } } From 9854584bdcaa75e5b3265f6e0655191a8a813f75 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:59:08 +0100 Subject: [PATCH 12/27] chore: misc cleanups, nfc --- Cargo.toml | 2 +- crates/providers/src/chain.rs | 63 ++++++++++++---------------- crates/providers/src/heart.rs | 43 +++++++++++-------- crates/providers/src/lib.rs | 3 ++ crates/providers/src/new.rs | 3 +- crates/pubsub/src/service.rs | 17 +++----- crates/pubsub/src/sub.rs | 34 +++++++-------- crates/rpc-client/Cargo.toml | 5 +-- crates/rpc-client/src/poller.rs | 74 +++++++++++++++++++++++---------- crates/signer-aws/src/signer.rs | 2 +- crates/transport/src/error.rs | 11 ++--- crates/transport/src/trait.rs | 6 +-- 12 files changed, 141 insertions(+), 122 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bdbaa76ce43..cd5ea466a78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,4 +101,4 @@ tempfile = "3.8" # TODO: Remove once alloy-contract is stable. This is only used in tests for `sol!`. [patch.crates-io] -alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "18b0509950c90d9ec38f25913b692ae4cdd6f227" } +# alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "18b0509950c90d9ec38f25913b692ae4cdd6f227" } diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index 58c2c3d4854..13fd7afb950 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -10,10 +10,7 @@ use lru::LruCache; use std::{num::NonZeroUsize, time::Duration}; /// The size of the block cache. -pub const BLOCK_CACHE_SIZE: NonZeroUsize = match NonZeroUsize::new(10) { - Some(size) => size, - None => panic!("BLOCK_CACHE_SIZE must be non-zero"), -}; +const BLOCK_CACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(10) }; fn chain_stream_poller( provider: WeakProvider

, @@ -25,52 +22,47 @@ where N: Network, T: Transport + Clone, { - let mut poll_stream = provider - .upgrade() - .map(|provider| { - PollTask::new((&*provider).weak_client(), "eth_blockNumber", ()) - .with_poll_interval(poll_interval) - .spawn() - .into_stream() - }) - .expect("provider dropped before poller started"); + let mut poll_stream = { + let upgraded_provider = provider.upgrade().expect("provider dropped before poller started"); + PollTask::new(upgraded_provider.weak_client(), "eth_blockNumber", ()) + .with_poll_interval(poll_interval) + .spawn() + .into_stream() + }; let mut next_yield = from_height; - let mut known_blocks: LruCache = LruCache::new(BLOCK_CACHE_SIZE); + let mut known_blocks = LruCache::::new(BLOCK_CACHE_SIZE); stream! { 'task: loop { - // first clear any buffered blocks - if known_blocks.contains(&next_yield) { + // Clear any buffered blocks. + while let Some(known_block) = known_blocks.pop(&next_yield) { next_yield += 1; - yield known_blocks.get(&next_yield).unwrap().clone(); - continue; + yield known_block; } - // then get the tip/ + // Get the tip. let block_number = match poll_stream.next().await { Some(Ok(block_number)) => block_number, Some(Err(err)) => { - // this is fine. - tracing::error!(%err, "polling stream lagged"); - continue; - }, + // This is fine. + debug!(%err, "polling stream lagged"); + continue 'task; + } None => { - tracing::debug!("polling stream ended"); - break; - }, + debug!("polling stream ended"); + break 'task; + } }; - // then upgrade the provider - let provider = match provider.upgrade() { - Some(provider) => provider, - None => { - tracing::debug!("provider dropped"); - break 'task; - }, + // Upgrade the provider. + let Some(provider) = provider.upgrade() else { + debug!("provider dropped"); + break 'task; }; - // Then try to fill as many blocks as possible + // Then try to fill as many blocks as possible. + // TODO: Maybe use `join_all` while !known_blocks.contains(&block_number) { let block = provider.get_block_by_number(block_number, false).await; match block { @@ -81,12 +73,11 @@ where continue 'task; }, Err(err) => { - tracing::error!(block_number, %err, "failed to fetch block"); + error!(block_number, %err, "failed to fetch block"); break 'task; }, } } - } } } diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index 0f0120ec913..2b6cba9598e 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -8,7 +8,7 @@ use futures::{stream::StreamExt, FutureExt, Stream}; use std::{ collections::{BTreeMap, HashMap}, future::Future, - time::Duration, + time::{Duration, Instant}, }; use tokio::{ select, @@ -33,7 +33,12 @@ pub struct WatchConfig { impl WatchConfig { /// Create a new watch for a transaction. pub fn new(tx_hash: B256, tx: oneshot::Sender<()>) -> Self { - Self { tx_hash, confirmations: 0, timeout: Default::default(), tx } + Self { tx_hash, confirmations: 0, timeout: None, tx } + } + + /// Set the number of confirmations to wait for. + pub fn set_confirmations(&mut self, confirmations: u64) { + self.confirmations = confirmations; } /// Set the number of confirmations to wait for. @@ -42,6 +47,11 @@ impl WatchConfig { self } + /// Set the timeout for the transaction. + pub fn set_timeout(&mut self, timeout: Duration) { + self.timeout = Some(timeout); + } + /// Set the timeout for the transaction. pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); @@ -64,8 +74,9 @@ pub struct PendingTransaction { } impl PendingTransaction { - pub fn tx_hash(&self) -> B256 { - self.tx_hash + /// Returns this transaction's hash. + pub const fn tx_hash(&self) -> &B256 { + &self.tx_hash } } @@ -76,7 +87,7 @@ impl Future for PendingTransaction { mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - self.rx.poll_unpin(cx).map(|res| res.unwrap()) + self.rx.poll_unpin(cx).map(Result::unwrap) } } @@ -107,7 +118,7 @@ pub(crate) struct Heartbeat { waiting_confs: BTreeMap, /// Ordered map of transactions to reap at a certain time. - reap_at: BTreeMap, + reap_at: BTreeMap, } impl Heartbeat @@ -140,17 +151,16 @@ impl Heartbeat { /// Get the next time to reap a transaction. If no reaps, this is a very /// long time from now (i.e. will not be woken). - fn next_reap(&self) -> std::time::Instant { + fn next_reap(&self) -> Instant { self.reap_at - .keys() - .next() - .copied() - .unwrap_or_else(|| std::time::Instant::now() + Duration::from_secs(60_000)) + .first_key_value() + .map(|(k, _)| *k) + .unwrap_or_else(|| Instant::now() + Duration::from_secs(60_000)) } /// Reap any timeout fn reap_timeouts(&mut self) { - let now = std::time::Instant::now(); + let now = Instant::now(); let to_keep = self.reap_at.split_off(&now); let to_reap = std::mem::replace(&mut self.reap_at, to_keep); @@ -164,7 +174,7 @@ impl Heartbeat { fn handle_watch_ix(&mut self, to_watch: WatchConfig) { // start watching for the tx if let Some(timeout) = to_watch.timeout { - self.reap_at.insert(std::time::Instant::now() + timeout, to_watch.tx_hash); + self.reap_at.insert(Instant::now() + timeout, to_watch.tx_hash); } self.unconfirmed.insert(to_watch.tx_hash, to_watch); } @@ -180,11 +190,8 @@ impl Heartbeat { }; // check if we are watching for any of the txns in this block - let to_check = block - .transactions - .hashes() - .copied() - .filter_map(|tx_hash| self.unconfirmed.remove(&tx_hash)); + let to_check = + block.transactions.hashes().filter_map(|tx_hash| self.unconfirmed.remove(tx_hash)); // If confirmations is 1 or less, notify the watcher for watcher in to_check { diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 6f2d82cf7bd..b1035588a0e 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -16,6 +16,9 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[macro_use] +extern crate tracing; + mod builder; pub use builder::{ProviderBuilder, ProviderLayer, Stack}; diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 000faf9627c..2a0d89135be 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -29,8 +29,7 @@ pub struct RootProviderInner { impl RootProviderInner { pub(crate) fn new(client: RpcClient) -> Self { - let this = Self { client, heart: None, _network: PhantomData }; - this + Self { client, heart: None, _network: PhantomData } } /// Get a weak reference to the RPC client. diff --git a/crates/pubsub/src/service.rs b/crates/pubsub/src/service.rs index a57cbd67791..84ae35b966b 100644 --- a/crates/pubsub/src/service.rs +++ b/crates/pubsub/src/service.rs @@ -123,18 +123,10 @@ impl PubSubService { /// 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, - ) -> TransportResult<()> { - let local_id = local_id.into(); - - if let Some(rx) = self.subs.get_subscription(local_id) { + fn service_get_sub(&mut self, local_id: U256, tx: oneshot::Sender) { + if let Some(rx) = self.subs.get_subscription(local_id.into()) { let _ = tx.send(rx); } - - Ok(()) } /// Service an unsubscribe instruction. @@ -153,7 +145,10 @@ impl PubSubService { 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::GetSub(alias, tx) => { + self.service_get_sub(alias, tx); + Ok(()) + } PubSubInstruction::Unsubscribe(alias) => self.service_unsubscribe(alias), } } diff --git a/crates/pubsub/src/sub.rs b/crates/pubsub/src/sub.rs index a472ca466e8..b37cdea884f 100644 --- a/crates/pubsub/src/sub.rs +++ b/crates/pubsub/src/sub.rs @@ -1,9 +1,8 @@ -use std::{pin::Pin, task}; - use alloy_primitives::B256; use futures::{ready, Stream, StreamExt}; use serde::de::DeserializeOwned; use serde_json::value::RawValue; +use std::{pin::Pin, task}; use tokio::sync::broadcast; use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; @@ -22,8 +21,8 @@ pub struct RawSubscription { impl RawSubscription { /// Get the local ID of the subscription. - pub const fn local_id(&self) -> B256 { - self.local_id + pub const fn local_id(&self) -> &B256 { + &self.local_id } /// Wrapper for [`blocking_recv`]. Block the current thread until a message @@ -128,7 +127,7 @@ impl From for Subscription { impl Subscription { /// Get the local ID of the subscription. - pub const fn local_id(&self) -> B256 { + pub const fn local_id(&self) -> &B256 { self.inner.local_id() } @@ -218,7 +217,7 @@ impl Subscription { /// Convert the subscription into a stream that may yield unexpected types. pub fn into_any_stream(self) -> SubAnyStream { SubAnyStream { - id: self.local_id(), + id: self.inner.local_id, inner: self.inner.into_stream(), _pd: std::marker::PhantomData, } @@ -266,7 +265,7 @@ impl Subscription { /// Convert the subscription into a stream. pub fn into_stream(self) -> SubscriptionStream { SubscriptionStream { - id: self.local_id(), + id: self.inner.local_id, inner: self.inner.into_stream(), _pd: std::marker::PhantomData, } @@ -306,7 +305,7 @@ impl Subscription { /// results. pub fn into_result_stream(self) -> SubResultStream { SubResultStream { - id: self.local_id(), + id: self.inner.local_id, inner: self.inner.into_stream(), _pd: std::marker::PhantomData, } @@ -324,8 +323,8 @@ pub struct SubAnyStream { impl SubAnyStream { /// Get the local ID of the subscription. - pub const fn id(&self) -> B256 { - self.id + pub const fn id(&self) -> &B256 { + &self.id } } @@ -355,8 +354,8 @@ pub struct SubscriptionStream { impl SubscriptionStream { /// Get the local ID of the subscription. - pub const fn id(&self) -> B256 { - self.id + pub const fn id(&self) -> &B256 { + &self.id } } @@ -383,9 +382,10 @@ impl Stream for SubscriptionStream { } } -/// A stream of notifications from the server, identified by a local ID. This -/// stream will attempt to deserialize the notifications and yield the -/// `serde_json::Result` of the deserialization. +/// A stream of notifications from the server, identified by a local ID. +/// +/// This stream will attempt to deserialize the notifications and yield the [`serde_json::Result`] +/// of the deserialization. #[derive(Debug)] pub struct SubResultStream { id: B256, @@ -395,8 +395,8 @@ pub struct SubResultStream { impl SubResultStream { /// Get the local ID of the subscription. - pub const fn id(&self) -> B256 { - self.id + pub const fn id(&self) -> &B256 { + &self.id } } diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index d3a9c4698b7..04441a942e9 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -19,6 +19,8 @@ alloy-transport.workspace = true futures.workspace = true pin-project.workspace = true serde_json.workspace = true +tokio = { workspace = true, features = ["sync"] } +tokio-stream = { version = "0.1.14", features = ["sync"] } tower.workspace = true tracing.workspace = true @@ -30,9 +32,6 @@ reqwest = { workspace = true, optional = true } url = { workspace = true, optional = true } serde = { workspace = true, optional = true } -tokio = { workspace = true, features = ["sync"] } -tokio-stream = { version = "0.1.14", features = ["sync"] } - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] alloy-transport-ipc = { workspace = true, optional = true } diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index 09752205335..96593469bdd 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -1,16 +1,14 @@ +use crate::WeakClient; +use alloy_json_rpc::{RpcParam, RpcReturn}; +use alloy_transport::{utils::Spawnable, Transport}; use std::{ marker::PhantomData, ops::{Deref, DerefMut}, time::Duration, }; - -use alloy_json_rpc::{RpcParam, RpcReturn}; -use alloy_transport::{utils::Spawnable, Transport}; use tokio::sync::broadcast; use tokio_stream::wrappers::BroadcastStream; -use crate::WeakClient; - /// A Poller task. #[derive(Debug)] pub struct PollTask @@ -29,7 +27,7 @@ where // config options channel_size: usize, poll_interval: Duration, - limit: Option, + limit: usize, _pd: PhantomData Resp>, } @@ -48,22 +46,54 @@ where params, channel_size: 16, poll_interval: Duration::from_secs(10), - limit: None, + limit: usize::MAX, _pd: PhantomData, } } - /// Set a limit on the number of succesful polls. + /// Returns the channel size for the poller task. + pub fn channel_size(&self) -> usize { + self.channel_size + } + + /// Sets the channel size for the poller task. + pub fn set_channel_size(&mut self, channel_size: usize) { + self.channel_size = channel_size; + } + + /// Sets the channel size for the poller task. + pub fn with_channel_size(mut self, channel_size: usize) -> Self { + self.set_channel_size(channel_size); + self + } + + /// Retuns the limit on the number of succesful polls. + pub fn limit(&self) -> usize { + self.limit + } + + /// Sets a limit on the number of succesful polls. pub fn set_limit(&mut self, limit: Option) { - self.limit = limit; + self.limit = limit.unwrap_or(usize::MAX); + } + + /// Sets a limit on the number of succesful polls. + pub fn with_limit(mut self, limit: Option) -> Self { + self.set_limit(limit); + self } - /// Set the duration between polls. + /// Returns the duration between polls. + pub fn poll_interval(&self) -> Duration { + self.poll_interval + } + + /// Sets the duration between polls. pub fn set_poll_interval(&mut self, poll_interval: Duration) { self.poll_interval = poll_interval; } - /// Set the duration between polls. + /// Sets the duration between polls. pub fn with_poll_interval(mut self, poll_interval: Duration) -> Self { self.set_poll_interval(poll_interval); self @@ -72,15 +102,11 @@ where /// Spawn the poller task, producing a stream of responses. pub fn spawn(self) -> PollChannel { let (tx, rx) = broadcast::channel(self.channel_size); - let fut = async move { - let limit = self.limit.unwrap_or(usize::MAX); - for _ in 0..limit { - let client = match self.client.upgrade() { - Some(client) => client, - None => break, + for _ in 0..self.limit { + let Some(client) = self.client.upgrade() else { + break; }; - match client.prepare(self.method, &self.params).await { Ok(resp) => { if tx.send(resp).is_err() { @@ -88,10 +114,9 @@ where } } Err(e) => { - debug!(%e, "Error response in polling request."); + debug!(%e, "error response in polling request"); } } - tokio::time::sleep(self.poll_interval).await; } }; @@ -129,6 +154,7 @@ impl Deref for PollChannel { &self.rx } } + impl DerefMut for PollChannel { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.rx @@ -144,13 +170,15 @@ where Self { rx: self.rx.resubscribe() } } - /// Convert the poll_channel into a stream. + /// Convert the poll channel into a stream. pub fn into_stream(self) -> BroadcastStream { - BroadcastStream::from(self.rx) + self.rx.into() } } -fn __assert_unpin() { +#[cfg(test)] +#[allow(clippy::missing_const_for_fn)] +fn _assert_unpin() { fn _assert() {} _assert::>(); } diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index d1e4506f03d..9cfc7154600 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -96,7 +96,7 @@ pub enum AwsSignerError { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for AwsSigner { #[instrument(err)] - #[allow(clippy::blocks_in_conditions)] // tracing::instrument on async fn + #[allow(clippy::blocks_in_conditions)] // instrument on async fn async fn sign_hash(&self, hash: B256) -> Result { self.sign_digest_inner(hash).await.map_err(alloy_signer::Error::other) } diff --git a/crates/transport/src/error.rs b/crates/transport/src/error.rs index d6cecffc149..0ba76feb3d2 100644 --- a/crates/transport/src/error.rs +++ b/crates/transport/src/error.rs @@ -32,13 +32,10 @@ pub enum TransportErrorKind { } impl TransportErrorKind { - /// Whether the error is potentially recoverable. This is a naive heuristic - /// and should be used with caution. - pub fn recoverable(&self) -> bool { - match self { - Self::MissingBatchResponse(_) => true, - _ => false, - } + /// Returns `true` if the error is potentially recoverable. + /// This is a naive heuristic and should be used with caution. + pub const fn recoverable(&self) -> bool { + matches!(self, Self::MissingBatchResponse(_)) } /// Instantiate a new `TransportError` from a custom error. diff --git a/crates/transport/src/trait.rs b/crates/transport/src/trait.rs index 007a8c6b1b6..ca6b20c4ab7 100644 --- a/crates/transport/src/trait.rs +++ b/crates/transport/src/trait.rs @@ -47,7 +47,7 @@ pub trait Transport: /// Convert this transport into a boxed trait object. fn boxed(self) -> BoxTransport where - Self: Sized + Clone + Send + Sync + 'static, + Self: Sized + Clone, { BoxTransport::new(self) } @@ -55,9 +55,9 @@ pub trait Transport: /// Make a boxed trait object by cloning this transport. fn as_boxed(&self) -> BoxTransport where - Self: Sized + Clone + Send + Sync + 'static, + Self: Sized + Clone, { - BoxTransport::new(self.clone()) + self.clone().boxed() } } From 963797f6e80e3f245b18b704d5760267ee0521e3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:31:21 +0100 Subject: [PATCH 13/27] feat: move implementations to trait, implement send transaction --- Cargo.toml | 1 + crates/providers/Cargo.toml | 19 +-- crates/providers/src/builder.rs | 11 +- crates/providers/src/chain.rs | 89 ++++++++------ crates/providers/src/heart.rs | 98 +++++++-------- crates/providers/src/new.rs | 203 ++++++++++++++++---------------- crates/pubsub/Cargo.toml | 2 +- crates/rpc-client/Cargo.toml | 2 +- crates/rpc-client/src/batch.rs | 5 +- crates/rpc-client/src/client.rs | 19 ++- 10 files changed, 235 insertions(+), 214 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 514a6494887..389e9f600ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ futures-executor = "0.3.29" hyper = "0.14.27" tokio = "1.33" tokio-util = "0.7" +tokio-stream = "0.1.14" tower = { version = "0.4.13", features = ["util"] } tracing = "0.1.40" diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index bd3e691aa33..965537d4f27 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -13,26 +13,29 @@ exclude.workspace = true [dependencies] alloy-network.workspace = true -alloy-primitives.workspace = true alloy-rpc-client = { workspace = true, features = ["reqwest"] } -alloy-rpc-types.workspace = true alloy-rpc-trace-types.workspace = true +alloy-rpc-types.workspace = true alloy-transport-http = { workspace = true, features = ["reqwest"] } alloy-transport.workspace = true + +alloy-primitives.workspace = true + +async-stream = "0.3.5" async-trait.workspace = true -serde.workspace = true -thiserror.workspace = true -reqwest.workspace = true auto_impl = "1.1.0" -tokio = { version = "1.33.0", features = ["sync"] } futures.workspace = true -async-stream = "0.3.5" lru = "0.12.2" +reqwest.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["sync"] } +tokio-stream.workspace = true tracing.workspace = true [dev-dependencies] alloy-node-bindings.workspace = true -tokio = { version = "1.33.0", features = ["macros"] } +tokio = { workspace = true, features = ["macros"] } [features] anvil = [] diff --git a/crates/providers/src/builder.rs b/crates/providers/src/builder.rs index c352ba5da2c..6765cf2efd8 100644 --- a/crates/providers/src/builder.rs +++ b/crates/providers/src/builder.rs @@ -1,4 +1,4 @@ -use crate::new::{Provider, RootProvider, RootProviderInner}; +use crate::new::{Provider, RootProvider}; use alloy_network::Network; use alloy_rpc_client::RpcClient; use alloy_transport::Transport; @@ -7,7 +7,7 @@ use std::marker::PhantomData; /// A layering abstraction in the vein of [`tower::Layer`] /// /// [`tower::Layer`]: https://docs.rs/tower/latest/tower/trait.Layer.html -pub trait ProviderLayer, N: Network, T: Transport> { +pub trait ProviderLayer, N: Network, T: Transport + Clone> { type Provider: Provider; fn layer(&self, inner: P) -> Self::Provider; @@ -27,7 +27,7 @@ impl Stack { impl ProviderLayer for Stack where - T: Transport, + T: Transport + Clone, N: Network, P: Provider, Inner: ProviderLayer, @@ -91,7 +91,7 @@ impl ProviderBuilder { where L: ProviderLayer, P: Provider, - T: Transport, + T: Transport + Clone, N: Network, { self.layer.layer(provider) @@ -108,8 +108,7 @@ impl ProviderBuilder { T: Transport + Clone, N: Network, { - let root = RootProviderInner::new(client); - self.provider(root.into()) + self.provider(RootProvider::new(client)) } } diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index 13fd7afb950..8defe909032 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -1,48 +1,57 @@ use crate::{Provider, WeakProvider}; use alloy_network::Network; use alloy_primitives::BlockNumber; -use alloy_rpc_client::PollTask; +use alloy_rpc_client::{PollTask, WeakClient}; use alloy_rpc_types::Block; use alloy_transport::{RpcError, Transport}; use async_stream::stream; use futures::{Stream, StreamExt}; use lru::LruCache; -use std::{num::NonZeroUsize, time::Duration}; +use std::num::NonZeroUsize; +use tokio_stream::wrappers::BroadcastStream; /// The size of the block cache. const BLOCK_CACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(10) }; -fn chain_stream_poller( +/// Maximum number of retries for fetching a block. +const MAX_RETRIES: usize = 3; + +pub(crate) struct ChainStreamPoller

{ provider: WeakProvider

, - from_height: BlockNumber, - poll_interval: Duration, -) -> impl Stream -where - P: Provider, - N: Network, - T: Transport + Clone, -{ - let mut poll_stream = { - let upgraded_provider = provider.upgrade().expect("provider dropped before poller started"); - PollTask::new(upgraded_provider.weak_client(), "eth_blockNumber", ()) - .with_poll_interval(poll_interval) - .spawn() - .into_stream() - }; + poll_stream: BroadcastStream, + next_yield: BlockNumber, + known_blocks: LruCache, +} - let mut next_yield = from_height; - let mut known_blocks = LruCache::::new(BLOCK_CACHE_SIZE); +impl

ChainStreamPoller

{ + pub(crate) fn new(provider: WeakProvider

, client: WeakClient) -> Self + where + T: Transport + Clone, + { + Self { + provider, + poll_stream: PollTask::new(client, "eth_blockNumber", ()).spawn().into_stream(), + next_yield: BlockNumber::MAX, + known_blocks: LruCache::new(BLOCK_CACHE_SIZE), + } + } - stream! { + pub(crate) fn into_stream(mut self) -> impl Stream + where + P: Provider, + N: Network, + T: Transport + Clone, + { + stream! { 'task: loop { // Clear any buffered blocks. - while let Some(known_block) = known_blocks.pop(&next_yield) { - next_yield += 1; + while let Some(known_block) = self.known_blocks.pop(&self.next_yield) { + self.next_yield += 1; yield known_block; } // Get the tip. - let block_number = match poll_stream.next().await { + let block_number = match self.poll_stream.next().await { Some(Ok(block_number)) => block_number, Some(Err(err)) => { // This is fine. @@ -54,30 +63,38 @@ where break 'task; } }; + if block_number == self.next_yield { + continue 'task; + } + if self.next_yield > block_number { + self.next_yield = block_number; + } // Upgrade the provider. - let Some(provider) = provider.upgrade() else { + let Some(provider) = self.provider.upgrade() else { debug!("provider dropped"); break 'task; }; // Then try to fill as many blocks as possible. // TODO: Maybe use `join_all` - while !known_blocks.contains(&block_number) { - let block = provider.get_block_by_number(block_number, false).await; - match block { - Ok(block) => { - known_blocks.put(block_number, block); - }, - Err(RpcError::Transport(err)) if err.recoverable() => { - continue 'task; - }, + let mut retries = MAX_RETRIES; + for block_number in self.next_yield..=block_number { + let block = match provider.get_block_by_number(block_number, false).await { + Ok(block) => block, + Err(RpcError::Transport(err)) if retries > 0 && err.recoverable() => { + debug!(block_number, %err, "failed to fetch block, retrying"); + retries -= 1; + continue; + } Err(err) => { error!(block_number, %err, "failed to fetch block"); break 'task; - }, - } + } + }; + self.known_blocks.put(block_number, block); } } + } } } diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index 2b6cba9598e..da02eb5a59c 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -25,15 +25,12 @@ pub struct WatchConfig { /// Optional timeout for the transaction. timeout: Option, - - /// Notify the waiter. - tx: oneshot::Sender<()>, } impl WatchConfig { /// Create a new watch for a transaction. - pub fn new(tx_hash: B256, tx: oneshot::Sender<()>) -> Self { - Self { tx_hash, confirmations: 0, timeout: None, tx } + pub fn new(tx_hash: B256) -> Self { + Self { tx_hash, confirmations: 0, timeout: None } } /// Set the number of confirmations to wait for. @@ -57,7 +54,14 @@ impl WatchConfig { self.timeout = Some(timeout); self } +} + +struct TxWatcher { + config: WatchConfig, + tx: oneshot::Sender<()>, +} +impl TxWatcher { /// Notify the waiter. fn notify(self) { let _ = self.tx.send(()); @@ -67,10 +71,10 @@ impl WatchConfig { /// A pending transaction that can be awaited. pub struct PendingTransaction { /// The transaction hash. - tx_hash: B256, + pub(crate) tx_hash: B256, /// The receiver for the notification. // TODO: send a receipt? - rx: oneshot::Receiver<()>, + pub(crate) rx: oneshot::Receiver<()>, } impl PendingTransaction { @@ -94,39 +98,46 @@ impl Future for PendingTransaction { /// A handle to the heartbeat task. #[derive(Debug, Clone)] pub struct HeartbeatHandle { - tx: mpsc::Sender, + tx: mpsc::Sender, latest: watch::Receiver, } impl HeartbeatHandle { - /// Get a new watcher that always sees the latest block. - pub fn latest(&self) -> watch::Receiver { - self.latest.clone() + /// Watch for a transaction to be confirmed with the given config. + pub async fn watch_tx(&self, config: WatchConfig) -> Result { + let (tx, rx) = oneshot::channel(); + let tx_hash = config.tx_hash; + match self.tx.send(TxWatcher { config, tx }).await { + Ok(()) => Ok(PendingTransaction { tx_hash, rx }), + Err(e) => Err(e.0.config), + } + } + + /// Returns a watcher that always sees the latest block. + pub fn latest(&self) -> &watch::Receiver { + &self.latest } } // TODO: Parameterize with `Network` /// A heartbeat task that receives blocks and watches for transactions. -pub(crate) struct Heartbeat { +pub(crate) struct Heartbeat { /// The stream of incoming blocks to watch. - stream: futures::stream::Fuse, + stream: futures::stream::Fuse, /// Transactions to watch for. - unconfirmed: HashMap, + unconfirmed: HashMap, /// Ordered map of transactions waiting for confirmations. - waiting_confs: BTreeMap, + waiting_confs: BTreeMap, /// Ordered map of transactions to reap at a certain time. reap_at: BTreeMap, } -impl Heartbeat -where - St: Stream + Unpin + Send + 'static, -{ +impl> Heartbeat { /// Create a new heartbeat task. - pub fn new(stream: St) -> Self { + pub(crate) fn new(stream: S) -> Self { Self { stream: stream.fuse(), unconfirmed: Default::default(), @@ -136,7 +147,7 @@ where } } -impl Heartbeat { +impl Heartbeat { /// Check if any transactions have enough confirmations to notify. fn check_confirmations(&mut self, latest: &watch::Sender) { if let Some(current_height) = { latest.borrow().header.number } { @@ -171,12 +182,12 @@ impl Heartbeat { /// Handle a watch instruction by adding it to the watch list, and /// potentially adding it to our `reap_at` list. - fn handle_watch_ix(&mut self, to_watch: WatchConfig) { + fn handle_watch_ix(&mut self, to_watch: TxWatcher) { // start watching for the tx - if let Some(timeout) = to_watch.timeout { - self.reap_at.insert(Instant::now() + timeout, to_watch.tx_hash); + if let Some(timeout) = to_watch.config.timeout { + self.reap_at.insert(Instant::now() + timeout, to_watch.config.tx_hash); } - self.unconfirmed.insert(to_watch.tx_hash, to_watch); + self.unconfirmed.insert(to_watch.config.tx_hash, to_watch); } /// Handle a new block by checking if any of the transactions we're @@ -184,27 +195,26 @@ impl Heartbeat { /// the latest block. fn handle_new_block(&mut self, block: Block, latest: &watch::Sender) { // Blocks without numbers are ignored, as they're not part of the chain. - let block_height = match block.header.number { - Some(height) => height, - None => return, + let Some(block_height) = block.header.number else { + return; }; // check if we are watching for any of the txns in this block let to_check = block.transactions.hashes().filter_map(|tx_hash| self.unconfirmed.remove(tx_hash)); - - // If confirmations is 1 or less, notify the watcher for watcher in to_check { - if watcher.confirmations <= 1 { + // If `confirmations` is 1 or less, notify the watcher. + let confs = watcher.config.confirmations; + if confs <= 1 { watcher.notify(); continue; } - // Otherwise add it to the waiting list - self.waiting_confs.insert(block_height + U256::from(watcher.confirmations), watcher); + // Otherwise add it to the waiting list. + self.waiting_confs.insert(block_height + U256::from(confs), watcher); } - // update the latest block. We use `send_replace` here to ensure the - // latest block is always up to date, even if no receivers exist + // Update the latest block. We use `send_replace` here to ensure the + // latest block is always up to date, even if no receivers exist. // C.f. // https://docs.rs/tokio/latest/tokio/sync/watch/struct.Sender.html#method.send let _ = latest.send_replace(block); @@ -213,12 +223,10 @@ impl Heartbeat { } } -impl Heartbeat -where - St: Stream + Unpin + Send + 'static, -{ +impl + Unpin + Send + 'static> Heartbeat { /// Spawn the heartbeat task, returning a [`HeartbeatHandle`] - pub(crate) fn spawn(mut self, from: Block) -> HeartbeatHandle { + pub(crate) fn spawn(mut self) -> HeartbeatHandle { + let from = None.unwrap(); let (latest, latest_rx) = watch::channel(from); let (ix_tx, mut ixns) = mpsc::channel(16); @@ -232,11 +240,9 @@ where biased; // Watch for new transactions. - ix_opt = ixns.recv() => { - match ix_opt { - Some(to_watch) => self.handle_watch_ix(to_watch), - None => break 'shutdown, // ix channel is closed - } + ix_opt = ixns.recv() => match ix_opt { + Some(to_watch) => self.handle_watch_ix(to_watch), + None => break 'shutdown, // ix channel is closed }, // Wake up to handle new blocks @@ -254,8 +260,8 @@ where self.reap_timeouts(); } }; - fut.spawn_task(); + HeartbeatHandle { tx: ix_tx, latest: latest_rx } } } diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 2a0d89135be..7ee7947ceab 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -1,14 +1,15 @@ -use crate::heart::HeartbeatHandle; +use crate::{ + chain::ChainStreamPoller, + heart::{Heartbeat, HeartbeatHandle, PendingTransaction, WatchConfig}, +}; use alloy_network::{Network, Transaction}; -use alloy_primitives::{Address, BlockNumber, U64}; +use alloy_primitives::{hex, Address, BlockNumber, B256, U256, U64}; use alloy_rpc_client::{ClientRef, RpcClient, WeakClient}; use alloy_rpc_types::Block; -use alloy_transport::{BoxTransport, Transport, TransportResult}; +use alloy_transport::{BoxTransport, Transport, TransportErrorKind, TransportResult}; use std::{ - borrow::Cow, marker::PhantomData, - ops::Deref, - sync::{Arc, Weak}, + sync::{Arc, OnceLock, Weak}, }; /// A [`Provider`] in a [`Weak`] reference. @@ -19,96 +20,88 @@ pub type ProviderRef<'a, P> = &'a P; /// The root provider manages the RPC client and the heartbeat. It is at the /// base of every provider stack. -pub struct RootProviderInner { - client: RpcClient, - - heart: Option, - - _network: PhantomData N>, +pub struct RootProvider { + /// The inner state of the root provider. + pub(crate) inner: Arc>, } -impl RootProviderInner { +impl RootProvider { pub(crate) fn new(client: RpcClient) -> Self { - Self { client, heart: None, _network: PhantomData } - } - - /// Get a weak reference to the RPC client. - pub fn weak_client(&self) -> WeakClient { - self.client.get_weak() - } - - /// Get a reference to the RPC client. - pub fn client_ref(&self) -> ClientRef<'_, T> { - self.client.get_ref() + Self { inner: Arc::new(RootProviderInner::new(client)) } } +} - /// Get a clone of the RPC client. - pub fn client(&self) -> RpcClient { - self.client.clone() +impl RootProvider { + async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult { + self.get_heart() + .watch_tx(WatchConfig::new(tx_hash)) + .await + .map_err(|_| TransportErrorKind::backend_gone()) } - /// Init the heartbeat - async fn init_heartbeat(&mut self) { - if self.heart.is_some() { - return; - } - todo!() + #[inline] + fn get_heart(&self) -> &HeartbeatHandle { + self.inner.heart.get_or_init(|| { + let weak = Arc::downgrade(&self.inner); + let stream = ChainStreamPoller::new(weak, self.inner.weak_client()); + // TODO: Can we avoid `Pin>` here? + Heartbeat::new(Box::pin(stream.into_stream())).spawn() + }) } } /// The root provider manages the RPC client and the heartbeat. It is at the /// base of every provider stack. -pub struct RootProvider { - pub inner: Arc>, +pub(crate) struct RootProviderInner { + client: RpcClient, + heart: OnceLock, + _network: PhantomData, } -impl From> for RootProvider { - fn from(inner: RootProviderInner) -> Self { - Self { inner: Arc::new(inner) } +impl RootProviderInner { + pub(crate) fn new(client: RpcClient) -> Self { + Self { client, heart: OnceLock::new(), _network: PhantomData } } -} -impl Clone for RootProviderInner { - fn clone(&self) -> Self { - Self { client: self.client.clone(), heart: self.heart.clone(), _network: PhantomData } + fn weak_client(&self) -> WeakClient { + self.client.get_weak() } -} - -impl Deref for RootProvider { - type Target = RootProviderInner; - fn deref(&self) -> &Self::Target { - &self.inner + fn client_ref(&self) -> ClientRef<'_, T> { + self.client.get_ref() } } -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] /// Provider is parameterized with a network and a transport. The default /// transport is type-erased, but you can do `Provider`. -pub trait Provider: Send + Sync { - /// Get a reference to the RPC client. - fn client_ref(&self) -> ClientRef<'_, T>; +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +#[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)] +pub trait Provider: Send + Sync { + /// Returns the RPC client used to send requests. + fn client(&self) -> ClientRef<'_, T>; - /// Get a weak reference to the RPC client. + /// Returns a [`Weak`] RPC client used to send requests. fn weak_client(&self) -> WeakClient; - async fn estimate_gas( - &self, - tx: &N::TransactionRequest, - ) -> TransportResult; + async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult; + + async fn estimate_gas(&self, tx: &N::TransactionRequest) -> TransportResult { + self.client().prepare("eth_estimateGas", tx).await + } /// Get the last block number available. - async fn get_block_number(&self) -> TransportResult; + async fn get_block_number(&self) -> TransportResult { + self.client().prepare("eth_blockNumber", ()).await.map(|num: U64| num.to::()) + } /// Get the transaction count for an address. Used for finding the /// appropriate nonce. /// /// TODO: block number/hash/tag - async fn get_transaction_count( - &self, - address: Address, - ) -> TransportResult; + async fn get_transaction_count(&self, address: Address) -> TransportResult { + self.client().prepare("eth_getTransactionCount", (address, "latest")).await + } /// Get a block by its number. /// @@ -117,68 +110,76 @@ pub trait Provider: Send + Sync { &self, number: BlockNumber, hydrate: bool, - ) -> TransportResult; + ) -> TransportResult { + self.client().prepare("eth_getBlockByNumber", (number, hydrate)).await + } /// Populate the gas limit for a transaction. async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { - let gas = self.estimate_gas(&*tx).await; + let gas = self.estimate_gas(&*tx).await?; + if let Ok(gas) = gas.try_into() { + tx.set_gas_limit(gas); + } + Ok(()) + } + + /// Broadcasts a transaction, returning a [`PendingTransaction`] that resolves once the + /// transaction has been confirmed. + async fn send_transaction( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult { + let tx_hash = self.client().prepare("eth_sendTransaction", tx).await?; + self.new_pending_transaction(tx_hash).await + } - gas.map(|gas| tx.set_gas_limit(gas.try_into().unwrap())) + /// Broadcasts a transaction's raw RLP bytes, returning a [`PendingTransaction`] that resolves + /// once the transaction has been confirmed. + async fn send_raw_transaction(&self, rlp_bytes: &[u8]) -> TransportResult { + let rlp_hex = hex::encode(rlp_bytes); + let tx_hash = self.client().prepare("eth_sendRawTransaction", rlp_hex).await?; + self.new_pending_transaction(tx_hash).await } } #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for RootProvider { - fn client_ref(&self) -> ClientRef<'_, T> { + #[inline] + fn client(&self) -> ClientRef<'_, T> { self.inner.client_ref() } + #[inline] fn weak_client(&self) -> WeakClient { self.inner.weak_client() } - async fn estimate_gas( - &self, - tx: &::TransactionRequest, - ) -> TransportResult { - self.client.prepare("eth_estimateGas", Cow::Borrowed(tx)).await + #[inline] + async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult { + RootProvider::new_pending_transaction(self, tx_hash).await } +} - async fn get_block_number(&self) -> TransportResult { - self.client.prepare("eth_blockNumber", ()).await.map(|num: U64| num.to::()) +// Internal implementation for [`chain_stream_poller`]. +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Provider for RootProviderInner { + #[inline] + fn client(&self) -> ClientRef<'_, T> { + self.client_ref() } - async fn get_block_by_number( - &self, - number: BlockNumber, - hydrate: bool, - ) -> TransportResult { - self.client - .prepare("eth_getBlockByNumber", Cow::<(BlockNumber, bool)>::Owned((number, hydrate))) - .await + #[inline] + fn weak_client(&self) -> WeakClient { + self.weak_client() } - async fn get_transaction_count( - &self, - address: Address, - ) -> TransportResult { - self.client - .prepare( - "eth_getTransactionCount", - Cow::<(Address, String)>::Owned((address, "latest".to_string())), - ) - .await + #[inline] + async fn new_pending_transaction(&self, _tx_hash: B256) -> TransportResult { + unreachable!() } } #[cfg(test)] -mod test { - use super::Provider; - use alloy_network::Network; - - // checks that `Provider` is object-safe - fn __compile_check() -> Box> { - unimplemented!() - } -} +struct _ObjectSafe(dyn Provider); diff --git a/crates/pubsub/Cargo.toml b/crates/pubsub/Cargo.toml index 243ad75b4f3..ec1459dc15d 100644 --- a/crates/pubsub/Cargo.toml +++ b/crates/pubsub/Cargo.toml @@ -21,6 +21,6 @@ futures.workspace = true serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["macros", "sync"] } -tokio-stream = { version = "0.1.14", features = ["sync"] } +tokio-stream = { workspace = true, features = ["sync"] } tower.workspace = true tracing.workspace = true diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 04441a942e9..edcd026f82c 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -20,7 +20,7 @@ futures.workspace = true pin-project.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["sync"] } -tokio-stream = { version = "0.1.14", features = ["sync"] } +tokio-stream = { workspace = true, features = ["sync"] } tower.workspace = true tracing.workspace = true diff --git a/crates/rpc-client/src/batch.rs b/crates/rpc-client/src/batch.rs index b9b3e0391f8..1f208266a9d 100644 --- a/crates/rpc-client/src/batch.rs +++ b/crates/rpc-client/src/batch.rs @@ -66,10 +66,7 @@ where #[pin_project::pin_project(project = CallStateProj)] #[derive(Debug)] -pub enum BatchFuture -where - Conn: Transport, -{ +pub enum BatchFuture { Prepared { transport: Conn, requests: RequestPacket, diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index 60f34ee15d6..a99a4fe043f 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -27,6 +27,13 @@ impl Clone for RpcClient { } } +impl RpcClient { + /// Create a new [`ClientBuilder`]. + pub fn builder() -> ClientBuilder { + ClientBuilder { builder: ServiceBuilder::new() } + } +} + impl RpcClient { /// Create a new [`RpcClient`] with the given transport. pub fn new(t: T, is_local: bool) -> Self { @@ -109,13 +116,6 @@ pub struct RpcClientInner { pub(crate) id: AtomicU64, } -impl RpcClientInner { - /// Create a new [`ClientBuilder`]. - pub fn builder() -> ClientBuilder { - ClientBuilder { builder: ServiceBuilder::new() } - } -} - impl RpcClientInner { /// Create a new [`RpcClient`] with the given transport. pub const fn new(t: T, is_local: bool) -> Self { @@ -164,10 +164,7 @@ impl RpcClientInner { } } -impl RpcClientInner -where - T: Transport + Clone, -{ +impl RpcClientInner { /// Prepare an [`RpcCall`]. /// /// This function reserves an ID for the request, however the request From f092dd26b736b723cb76f93ed6acfb12a04ae1fd Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:32:54 +0100 Subject: [PATCH 14/27] chore: rename errors in logs to 'err' --- crates/json-rpc/src/error.rs | 2 +- crates/transport-ipc/src/lib.rs | 8 ++++---- crates/transport-ws/src/lib.rs | 4 ++-- crates/transport-ws/src/native.rs | 28 ++++++++++++++-------------- crates/transport-ws/src/wasm.rs | 16 ++++++++-------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/json-rpc/src/error.rs b/crates/json-rpc/src/error.rs index d398e0a46e8..d7b72bcffeb 100644 --- a/crates/json-rpc/src/error.rs +++ b/crates/json-rpc/src/error.rs @@ -2,7 +2,7 @@ use crate::{ErrorPayload, RpcReturn}; use serde_json::value::RawValue; /// An RPC error. -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum RpcError> { /// Server returned an error response. #[error("Server returned an error response: {0}")] diff --git a/crates/transport-ipc/src/lib.rs b/crates/transport-ipc/src/lib.rs index f6912d7d22d..03f2df86602 100644 --- a/crates/transport-ipc/src/lib.rs +++ b/crates/transport-ipc/src/lib.rs @@ -71,8 +71,8 @@ impl IpcBackend { match item { Some(msg) => { let bytes = msg.get(); - if let Err(e) = writer.write_all(bytes.as_bytes()).await { - error!(%e, "Failed to write to IPC socket"); + if let Err(err) = writer.write_all(bytes.as_bytes()).await { + error!(%err, "Failed to write to IPC socket"); break true; } }, @@ -205,8 +205,8 @@ where // can try decoding again *this.drained = false; } - Err(e) => { - error!(%e, "Failed to read from IPC socket, shutting down"); + Err(err) => { + error!(%err, "Failed to read from IPC socket, shutting down"); return Ready(None); } } diff --git a/crates/transport-ws/src/lib.rs b/crates/transport-ws/src/lib.rs index 218b19ecc76..8f6286a92a5 100644 --- a/crates/transport-ws/src/lib.rs +++ b/crates/transport-ws/src/lib.rs @@ -60,8 +60,8 @@ impl WsBackend { return Err(()); } } - Err(e) => { - error!(e = %e, "Failed to deserialize message"); + Err(err) => { + error!(%err, "Failed to deserialize message"); return Err(()); } } diff --git a/crates/transport-ws/src/native.rs b/crates/transport-ws/src/native.rs index 3196c869b87..8a71032245e 100644 --- a/crates/transport-ws/src/native.rs +++ b/crates/transport-ws/src/native.rs @@ -88,7 +88,7 @@ impl WsBackend { /// Spawn a new backend task. pub fn spawn(mut self) { let fut = async move { - let mut err = false; + let mut errored = false; let keepalive = sleep(Duration::from_secs(KEEPALIVE)); tokio::pin!(keepalive); loop { @@ -111,9 +111,9 @@ impl WsBackend { 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; + if let Err(err) = self.send(msg).await { + error!(%err, "WS connection error"); + errored = true; break } }, @@ -128,33 +128,33 @@ impl WsBackend { _ = &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; + if let Err(err) = self.socket.send(Message::Ping(vec![])).await { + error!(%err, "WS connection error"); + errored = true; break } } resp = self.socket.next() => { match resp { Some(Ok(item)) => { - err = self.handle(item).await.is_err(); - if err { break } + errored = self.handle(item).await.is_err(); + if errored { break } }, - Some(Err(e)) => { - error!(err = %e, "WS connection error"); - err = true; + Some(Err(err)) => { + error!(%err, "WS connection error"); + errored = true; break } None => { error!("WS server has gone away"); - err = true; + errored = true; break }, } } } } - if err { + if errored { self.interface.close_with_error(); } }; diff --git a/crates/transport-ws/src/wasm.rs b/crates/transport-ws/src/wasm.rs index 6fc965187f7..3789bbc7c97 100644 --- a/crates/transport-ws/src/wasm.rs +++ b/crates/transport-ws/src/wasm.rs @@ -53,7 +53,7 @@ impl WsBackend> { /// Spawn this backend on a loop. pub fn spawn(mut self) { let fut = async move { - let mut err = false; + let mut errored = false; loop { // We bias the loop as follows // 1. New dispatch to server. @@ -71,9 +71,9 @@ impl WsBackend> { inst = self.interface.recv_from_frontend() => { match inst { Some(msg) => { - if let Err(e) = self.send(msg).await { - error!(err = %e, "WS connection error"); - err = true; + if let Err(err) = self.send(msg).await { + error!(%err, "WS connection error"); + errored = true; break } }, @@ -86,19 +86,19 @@ impl WsBackend> { resp = self.socket.next() => { match resp { Some(item) => { - err = self.handle(item).await.is_err(); - if err { break } + errored = self.handle(item).await.is_err(); + if errored { break } }, None => { error!("WS server has gone away"); - err = true; + errored = true; break }, } } } } - if err { + if errored { self.interface.close_with_error(); } }; From a150bc704eeaf0e2a05e0e329b6f78345adf6a1b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:33:56 +0100 Subject: [PATCH 15/27] fix: correct ser/de, add more tracing --- crates/providers/src/chain.rs | 6 ++++-- crates/providers/src/heart.rs | 28 +++++++++++++++++----------- crates/providers/src/new.rs | 6 +++--- crates/rpc-client/src/poller.rs | 17 ++++++++++++----- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index 8defe909032..9599100c044 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -1,6 +1,6 @@ use crate::{Provider, WeakProvider}; use alloy_network::Network; -use alloy_primitives::BlockNumber; +use alloy_primitives::{BlockNumber, U64}; use alloy_rpc_client::{PollTask, WeakClient}; use alloy_rpc_types::Block; use alloy_transport::{RpcError, Transport}; @@ -18,7 +18,7 @@ const MAX_RETRIES: usize = 3; pub(crate) struct ChainStreamPoller

{ provider: WeakProvider

, - poll_stream: BroadcastStream, + poll_stream: BroadcastStream, next_yield: BlockNumber, known_blocks: LruCache, } @@ -46,6 +46,7 @@ impl

ChainStreamPoller

{ 'task: loop { // Clear any buffered blocks. while let Some(known_block) = self.known_blocks.pop(&self.next_yield) { + debug!("yielding block number {}", self.next_yield); self.next_yield += 1; yield known_block; } @@ -63,6 +64,7 @@ impl

ChainStreamPoller

{ break 'task; } }; + let block_number = block_number.to::(); if block_number == self.next_yield { continue 'task; } diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index da02eb5a59c..1cd46b35ef5 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -3,10 +3,11 @@ use alloy_primitives::{B256, U256}; use alloy_rpc_types::Block; -use alloy_transport::utils::Spawnable; +use alloy_transport::{utils::Spawnable, TransportErrorKind, TransportResult}; use futures::{stream::StreamExt, FutureExt, Stream}; use std::{ collections::{BTreeMap, HashMap}, + fmt, future::Future, time::{Duration, Instant}, }; @@ -77,6 +78,12 @@ pub struct PendingTransaction { pub(crate) rx: oneshot::Receiver<()>, } +impl fmt::Debug for PendingTransaction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PendingTransaction").field("tx_hash", &self.tx_hash).finish() + } +} + impl PendingTransaction { /// Returns this transaction's hash. pub const fn tx_hash(&self) -> &B256 { @@ -85,13 +92,13 @@ impl PendingTransaction { } impl Future for PendingTransaction { - type Output = (); + type Output = TransportResult<()>; fn poll( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - self.rx.poll_unpin(cx).map(Result::unwrap) + self.rx.poll_unpin(cx).map(|res| res.map_err(|_| TransportErrorKind::backend_gone())) } } @@ -99,7 +106,7 @@ impl Future for PendingTransaction { #[derive(Debug, Clone)] pub struct HeartbeatHandle { tx: mpsc::Sender, - latest: watch::Receiver, + latest: watch::Receiver>, } impl HeartbeatHandle { @@ -114,7 +121,7 @@ impl HeartbeatHandle { } /// Returns a watcher that always sees the latest block. - pub fn latest(&self) -> &watch::Receiver { + pub fn latest(&self) -> &watch::Receiver> { &self.latest } } @@ -149,8 +156,8 @@ impl> Heartbeat { impl Heartbeat { /// Check if any transactions have enough confirmations to notify. - fn check_confirmations(&mut self, latest: &watch::Sender) { - if let Some(current_height) = { latest.borrow().header.number } { + fn check_confirmations(&mut self, latest: &watch::Sender>) { + if let Some(current_height) = latest.borrow().as_ref().unwrap().header.number { let to_keep = self.waiting_confs.split_off(¤t_height); let to_notify = std::mem::replace(&mut self.waiting_confs, to_keep); @@ -193,7 +200,7 @@ impl Heartbeat { /// Handle a new block by checking if any of the transactions we're /// watching are in it, and if so, notifying the watcher. Also updates /// the latest block. - fn handle_new_block(&mut self, block: Block, latest: &watch::Sender) { + fn handle_new_block(&mut self, block: Block, latest: &watch::Sender>) { // Blocks without numbers are ignored, as they're not part of the chain. let Some(block_height) = block.header.number else { return; @@ -217,7 +224,7 @@ impl Heartbeat { // latest block is always up to date, even if no receivers exist. // C.f. // https://docs.rs/tokio/latest/tokio/sync/watch/struct.Sender.html#method.send - let _ = latest.send_replace(block); + let _ = latest.send_replace(Some(block)); self.check_confirmations(latest); } @@ -226,8 +233,7 @@ impl Heartbeat { impl + Unpin + Send + 'static> Heartbeat { /// Spawn the heartbeat task, returning a [`HeartbeatHandle`] pub(crate) fn spawn(mut self) -> HeartbeatHandle { - let from = None.unwrap(); - let (latest, latest_rx) = watch::channel(from); + let (latest, latest_rx) = watch::channel(None::); let (ix_tx, mut ixns) = mpsc::channel(16); let fut = async move { diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 7ee7947ceab..774ebc0e893 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -87,7 +87,7 @@ pub trait Provider: Send + Sync async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult; async fn estimate_gas(&self, tx: &N::TransactionRequest) -> TransportResult { - self.client().prepare("eth_estimateGas", tx).await + self.client().prepare("eth_estimateGas", (tx,)).await } /// Get the last block number available. @@ -129,7 +129,7 @@ pub trait Provider: Send + Sync &self, tx: &N::TransactionRequest, ) -> TransportResult { - let tx_hash = self.client().prepare("eth_sendTransaction", tx).await?; + let tx_hash = self.client().prepare("eth_sendTransaction", (tx,)).await?; self.new_pending_transaction(tx_hash).await } @@ -137,7 +137,7 @@ pub trait Provider: Send + Sync /// once the transaction has been confirmed. async fn send_raw_transaction(&self, rlp_bytes: &[u8]) -> TransportResult { let rlp_hex = hex::encode(rlp_bytes); - let tx_hash = self.client().prepare("eth_sendRawTransaction", rlp_hex).await?; + let tx_hash = self.client().prepare("eth_sendRawTransaction", (rlp_hex,)).await?; self.new_pending_transaction(tx_hash).await } } diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index 96593469bdd..4b5837a93bc 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -52,7 +52,7 @@ where } /// Returns the channel size for the poller task. - pub fn channel_size(&self) -> usize { + pub const fn channel_size(&self) -> usize { self.channel_size } @@ -68,7 +68,7 @@ where } /// Retuns the limit on the number of succesful polls. - pub fn limit(&self) -> usize { + pub const fn limit(&self) -> usize { self.limit } @@ -84,7 +84,7 @@ where } /// Returns the duration between polls. - pub fn poll_interval(&self) -> Duration { + pub const fn poll_interval(&self) -> Duration { self.poll_interval } @@ -105,18 +105,25 @@ where let fut = async move { for _ in 0..self.limit { let Some(client) = self.client.upgrade() else { + debug!("client dropped"); break; }; + + trace!("polling"); match client.prepare(self.method, &self.params).await { Ok(resp) => { if tx.send(resp).is_err() { + debug!("channel closed"); break; } } - Err(e) => { - debug!(%e, "error response in polling request"); + Err(err) => { + error!(%err, "error in polling request"); + break; } } + + trace!(duration=?self.poll_interval, "sleeping"); tokio::time::sleep(self.poll_interval).await; } }; From 78cba273a1a5ac4b058ac6de980eab3c94ff121a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:34:05 +0100 Subject: [PATCH 16/27] test: add a primitive test --- crates/providers/Cargo.toml | 3 + crates/providers/src/new.rs | 182 +++++++++++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 1 deletion(-) diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 965537d4f27..419661b7c2c 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -34,8 +34,11 @@ tokio-stream.workspace = true tracing.workspace = true [dev-dependencies] +alloy-consensus.workspace = true alloy-node-bindings.workspace = true +alloy-rlp.workspace = true tokio = { workspace = true, features = ["macros"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } [features] anvil = [] diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 774ebc0e893..99d97756a71 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -182,4 +182,184 @@ impl Provider for RootProviderInner(dyn Provider); +mod tests { + use super::*; + use alloy_primitives::address; + use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; + use alloy_transport_http::Http; + use reqwest::Client; + + struct _ObjectSafe(dyn Provider); + + #[derive(Clone)] + struct TxLegacy(alloy_consensus::TxLegacy); + impl serde::Serialize for TxLegacy { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let tx = &self.0; + TransactionRequest { + from: None, + to: tx.to().to(), + gas_price: tx.gas_price(), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + max_fee_per_blob_gas: None, + gas: Some(U256::from(tx.gas_limit())), + value: Some(tx.value()), + input: TransactionInput::new(tx.input().to_vec().into()), + nonce: Some(U64::from(tx.nonce())), + chain_id: tx.chain_id().map(U64::from), + access_list: None, + transaction_type: None, + blob_versioned_hashes: None, + sidecar: None, + other: Default::default(), + } + .serialize(serializer) + } + } + impl<'de> serde::Deserialize<'de> for TxLegacy { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + unimplemented!() + } + } + #[allow(unused)] + impl alloy_network::Transaction for TxLegacy { + type Signature = (); + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + todo!() + } + + fn payload_len_for_signature(&self) -> usize { + todo!() + } + + fn into_signed( + self, + signature: alloy_primitives::Signature, + ) -> alloy_network::Signed + where + Self: Sized, + { + todo!() + } + + fn encode_signed( + &self, + signature: &alloy_primitives::Signature, + out: &mut dyn alloy_primitives::bytes::BufMut, + ) { + todo!() + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> + where + Self: Sized, + { + todo!() + } + + fn input(&self) -> &[u8] { + todo!() + } + + fn input_mut(&mut self) -> &mut alloy_primitives::Bytes { + todo!() + } + + fn set_input(&mut self, data: alloy_primitives::Bytes) { + todo!() + } + + fn to(&self) -> alloy_network::TxKind { + todo!() + } + + fn set_to(&mut self, to: alloy_network::TxKind) { + todo!() + } + + fn value(&self) -> U256 { + todo!() + } + + fn set_value(&mut self, value: U256) { + todo!() + } + + fn chain_id(&self) -> Option { + todo!() + } + + fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { + todo!() + } + + fn nonce(&self) -> u64 { + todo!() + } + + fn set_nonce(&mut self, nonce: u64) { + todo!() + } + + fn gas_limit(&self) -> u64 { + todo!() + } + + fn set_gas_limit(&mut self, limit: u64) { + todo!() + } + + fn gas_price(&self) -> Option { + todo!() + } + + fn set_gas_price(&mut self, price: U256) { + todo!() + } + } + + struct TmpNetwork; + impl Network for TmpNetwork { + type TxEnvelope = alloy_consensus::TxEnvelope; + type ReceiptEnvelope = alloy_consensus::ReceiptEnvelope; + type Header = (); + type TransactionRequest = TxLegacy; + type TransactionResponse = (); + type ReceiptResponse = (); + type HeaderResponse = (); + } + + fn init_tracing() { + let _ = tracing_subscriber::fmt::try_init(); + } + + #[tokio::test] + async fn test_send_tx() { + init_tracing(); + + let anvil = alloy_node_bindings::Anvil::new().block_time(1u64).spawn(); + let url = anvil.endpoint().parse().unwrap(); + // let url = "http://127.0.0.1:8545".parse().unwrap(); + let http = Http::::new(url); + let provider: RootProvider = RootProvider::new(RpcClient::new(http, true)); + + let tx = alloy_consensus::TxLegacy { + value: U256::from(100), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + gas_price: 20e9 as u128, + gas_limit: 21000, + ..Default::default() + }; + let pending_tx = provider.send_transaction(&TxLegacy(tx)).await.expect("failed to send tx"); + eprintln!("{pending_tx:?}"); + let () = pending_tx.await.expect("failed to await pending tx"); + } +} From 1fc706e8a8ff937ca9b701c29b3a575d321744cc Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:50:58 +0100 Subject: [PATCH 17/27] chore: unused import --- crates/rpc-client/src/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index a99a4fe043f..be17d740a3b 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -1,6 +1,6 @@ use crate::{poller::PollTask, BatchRequest, ClientBuilder, RpcCall}; use alloy_json_rpc::{Id, Request, RpcParam, RpcReturn}; -use alloy_transport::{BoxTransport, Transport, TransportConnect, TransportError, TransportResult}; +use alloy_transport::{BoxTransport, Transport, TransportConnect, TransportError}; use alloy_transport_http::Http; use std::{ ops::Deref, @@ -201,6 +201,7 @@ impl RpcClientInner { mod pubsub_impl { use super::*; use alloy_pubsub::{PubSubConnect, PubSubFrontend, RawSubscription, Subscription}; + use alloy_transport::TransportResult; impl RpcClientInner { /// Get a [`RawSubscription`] for the given subscription ID. From 5b18bdafb4c8c2d38d21b577c1ab7637bb4d144b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:23:39 +0100 Subject: [PATCH 18/27] feat: add retrying and ser caching to poller --- crates/rpc-client/src/poller.rs | 79 +++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index 4b5837a93bc..b22f05b0ba7 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -1,6 +1,8 @@ use crate::WeakClient; -use alloy_json_rpc::{RpcParam, RpcReturn}; +use alloy_json_rpc::{RpcError, RpcParam, RpcReturn}; use alloy_transport::{utils::Spawnable, Transport}; +use serde::Serialize; +use serde_json::value::RawValue; use std::{ marker::PhantomData, ops::{Deref, DerefMut}, @@ -8,6 +10,10 @@ use std::{ }; use tokio::sync::broadcast; use tokio_stream::wrappers::BroadcastStream; +use tracing::Instrument; + +/// The number of retries for polling a request. +const MAX_RETRIES: usize = 3; /// A Poller task. #[derive(Debug)] @@ -102,37 +108,57 @@ where /// Spawn the poller task, producing a stream of responses. pub fn spawn(self) -> PollChannel { let (tx, rx) = broadcast::channel(self.channel_size); + let span = debug_span!("poller", method = self.method); let fut = async move { - for _ in 0..self.limit { + let mut params = ParamsOnce::Typed(self.params); + let mut retries = MAX_RETRIES; + 'outer: for _ in 0..self.limit { let Some(client) = self.client.upgrade() else { debug!("client dropped"); break; }; - trace!("polling"); - match client.prepare(self.method, &self.params).await { - Ok(resp) => { - if tx.send(resp).is_err() { - debug!("channel closed"); - break; - } - } + // Avoid serializing the params more than once. + let params = match params.get() { + Ok(p) => p, Err(err) => { - error!(%err, "error in polling request"); - break; + error!(%err, "failed to serialize params"); + break 'outer; + } + }; + + loop { + trace!("polling"); + match client.prepare(self.method, params).await { + Ok(resp) => { + if tx.send(resp).is_err() { + debug!("channel closed"); + break 'outer; + } + } + Err(RpcError::Transport(err)) if retries > 0 && err.recoverable() => { + debug!(%err, "failed to poll, retrying"); + retries -= 1; + continue; + } + Err(err) => { + error!(%err, "failed to poll"); + break 'outer; + } } + break; } trace!(duration=?self.poll_interval, "sleeping"); tokio::time::sleep(self.poll_interval).await; } }; - fut.spawn_task(); + fut.instrument(span).spawn_task(); rx.into() } } -/// A channel yeildiing responses from a poller task. +/// A channel yielding responses from a poller task. /// /// This stream is backed by a coroutine, and will continue to produce responses /// until the poller task is dropped. The poller task is dropped when all @@ -183,6 +209,31 @@ where } } +// Serializes the parameters only once. +enum ParamsOnce

{ + Typed(P), + Serialized(Box), +} + +impl ParamsOnce

{ + #[inline] + fn get(&mut self) -> serde_json::Result<&RawValue> { + match self { + ParamsOnce::Typed(_) => self.init(), + ParamsOnce::Serialized(p) => Ok(p), + } + } + + #[cold] + fn init(&mut self) -> serde_json::Result<&RawValue> { + let Self::Typed(p) = self else { unreachable!() }; + let v = serde_json::value::to_raw_value(p)?; + *self = ParamsOnce::Serialized(v); + let Self::Serialized(v) = self else { unreachable!() }; + Ok(v) + } +} + #[cfg(test)] #[allow(clippy::missing_const_for_fn)] fn _assert_unpin() { From 3f124c407ced39e48c39a15795053715a2d25485 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:24:48 +0100 Subject: [PATCH 19/27] feat: reduce polling interval if provider is local --- crates/providers/Cargo.toml | 1 - crates/providers/src/chain.rs | 35 ++++++++++++++++++++--------------- crates/providers/src/new.rs | 17 +++++++---------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 419661b7c2c..05ca24ec061 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -30,7 +30,6 @@ reqwest.workspace = true serde.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["sync"] } -tokio-stream.workspace = true tracing.workspace = true [dev-dependencies] diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index 9599100c044..d6ac091879f 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -1,4 +1,4 @@ -use crate::{Provider, WeakProvider}; +use crate::{new::RootProviderInner, Provider, RootProvider, WeakProvider}; use alloy_network::Network; use alloy_primitives::{BlockNumber, U64}; use alloy_rpc_client::{PollTask, WeakClient}; @@ -7,8 +7,7 @@ use alloy_transport::{RpcError, Transport}; use async_stream::stream; use futures::{Stream, StreamExt}; use lru::LruCache; -use std::num::NonZeroUsize; -use tokio_stream::wrappers::BroadcastStream; +use std::{num::NonZeroUsize, sync::Arc, time::Duration}; /// The size of the block cache. const BLOCK_CACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(10) }; @@ -16,33 +15,39 @@ const BLOCK_CACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(10) /// Maximum number of retries for fetching a block. const MAX_RETRIES: usize = 3; -pub(crate) struct ChainStreamPoller

{ +pub(crate) struct ChainStreamPoller { provider: WeakProvider

, - poll_stream: BroadcastStream, + poll_task: PollTask, next_yield: BlockNumber, known_blocks: LruCache, } -impl

ChainStreamPoller

{ - pub(crate) fn new(provider: WeakProvider

, client: WeakClient) -> Self - where - T: Transport + Clone, - { +impl ChainStreamPoller, T> { + pub(crate) fn from_root(p: &RootProvider) -> Self { + let mut this = Self::new(Arc::downgrade(&p.inner), p.inner.weak_client()); + if p.client().is_local() { + this.poll_task.set_poll_interval(Duration::from_secs(1)); + } + this + } +} + +impl ChainStreamPoller { + pub(crate) fn new(provider: WeakProvider

, client: WeakClient) -> Self { Self { provider, - poll_stream: PollTask::new(client, "eth_blockNumber", ()).spawn().into_stream(), + poll_task: PollTask::new(client, "eth_blockNumber", ()), next_yield: BlockNumber::MAX, known_blocks: LruCache::new(BLOCK_CACHE_SIZE), } } - pub(crate) fn into_stream(mut self) -> impl Stream + pub(crate) fn into_stream(mut self) -> impl Stream where P: Provider, - N: Network, - T: Transport + Clone, { stream! { + let mut poll_task = self.poll_task.spawn().into_stream(); 'task: loop { // Clear any buffered blocks. while let Some(known_block) = self.known_blocks.pop(&self.next_yield) { @@ -52,7 +57,7 @@ impl

ChainStreamPoller

{ } // Get the tip. - let block_number = match self.poll_stream.next().await { + let block_number = match poll_task.next().await { Some(Ok(block_number)) => block_number, Some(Err(err)) => { // This is fine. diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 99d97756a71..2ae7652380c 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -33,19 +33,17 @@ impl RootProvider { impl RootProvider { async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult { - self.get_heart() - .watch_tx(WatchConfig::new(tx_hash)) - .await - .map_err(|_| TransportErrorKind::backend_gone()) + // TODO: Make this configurable. + let cfg = WatchConfig::new(tx_hash); + self.get_heart().watch_tx(cfg).await.map_err(|_| TransportErrorKind::backend_gone()) } #[inline] fn get_heart(&self) -> &HeartbeatHandle { self.inner.heart.get_or_init(|| { - let weak = Arc::downgrade(&self.inner); - let stream = ChainStreamPoller::new(weak, self.inner.weak_client()); - // TODO: Can we avoid `Pin>` here? - Heartbeat::new(Box::pin(stream.into_stream())).spawn() + let poller = ChainStreamPoller::from_root(self); + // TODO: Can we avoid `Box::pin` here? + Heartbeat::new(Box::pin(poller.into_stream())).spawn() }) } } @@ -347,9 +345,8 @@ mod tests { let anvil = alloy_node_bindings::Anvil::new().block_time(1u64).spawn(); let url = anvil.endpoint().parse().unwrap(); - // let url = "http://127.0.0.1:8545".parse().unwrap(); let http = Http::::new(url); - let provider: RootProvider = RootProvider::new(RpcClient::new(http, true)); + let provider = RootProvider::::new(RpcClient::new(http, true)); let tx = alloy_consensus::TxLegacy { value: U256::from(100), From 5372780682c6522603215dddbf34f0358b295c22 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:26:02 +0100 Subject: [PATCH 20/27] fix: import serde --- 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 edcd026f82c..00488f3993a 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -19,6 +19,7 @@ alloy-transport.workspace = true futures.workspace = true pin-project.workspace = true serde_json.workspace = true +serde.workspace = true tokio = { workspace = true, features = ["sync"] } tokio-stream = { workspace = true, features = ["sync"] } tower.workspace = true @@ -31,7 +32,6 @@ hyper = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } url = { workspace = true, optional = true } -serde = { workspace = true, optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] alloy-transport-ipc = { workspace = true, optional = true } @@ -51,6 +51,6 @@ 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", "dep:serde"] +pubsub = ["dep:alloy-pubsub", "dep:alloy-primitives"] ws = ["pubsub", "dep:alloy-transport-ws"] ipc = ["pubsub", "dep:alloy-transport-ipc"] From 3e482e5d8393dd748e4a98c1f2fa162e516e5f16 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:56:11 +0100 Subject: [PATCH 21/27] fix: export PendingTransaction --- crates/providers/src/heart.rs | 9 ++++++--- crates/providers/src/lib.rs | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index 1cd46b35ef5..0958097cd19 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -104,14 +104,17 @@ impl Future for PendingTransaction { /// A handle to the heartbeat task. #[derive(Debug, Clone)] -pub struct HeartbeatHandle { +pub(crate) struct HeartbeatHandle { tx: mpsc::Sender, latest: watch::Receiver>, } impl HeartbeatHandle { /// Watch for a transaction to be confirmed with the given config. - pub async fn watch_tx(&self, config: WatchConfig) -> Result { + pub(crate) async fn watch_tx( + &self, + config: WatchConfig, + ) -> Result { let (tx, rx) = oneshot::channel(); let tx_hash = config.tx_hash; match self.tx.send(TxWatcher { config, tx }).await { @@ -121,7 +124,7 @@ impl HeartbeatHandle { } /// Returns a watcher that always sees the latest block. - pub fn latest(&self) -> &watch::Receiver> { + pub(crate) fn latest(&self) -> &watch::Receiver> { &self.latest } } diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index b1035588a0e..6ca633425a8 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -25,6 +25,7 @@ pub use builder::{ProviderBuilder, ProviderLayer, Stack}; mod chain; mod heart; +pub use heart::PendingTransaction; pub mod new; pub use new::{Provider, ProviderRef, RootProvider, WeakProvider}; From e07b5f2c39a2c2bdbaa15b0837946989fb70ac4b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:33:10 +0100 Subject: [PATCH 22/27] fix: PTX outputs hash, >1 waiting txs, fix initial block yield --- crates/providers/src/chain.rs | 27 +++++++++------- crates/providers/src/heart.rs | 59 ++++++++++++++++++++--------------- crates/providers/src/new.rs | 9 +++--- 3 files changed, 54 insertions(+), 41 deletions(-) diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index d6ac091879f..a233b2a9894 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -15,6 +15,9 @@ const BLOCK_CACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(10) /// Maximum number of retries for fetching a block. const MAX_RETRIES: usize = 3; +/// Default block number for when we don't have a block yet. +const NO_BLOCK_NUMBER: BlockNumber = BlockNumber::MAX; + pub(crate) struct ChainStreamPoller { provider: WeakProvider

, poll_task: PollTask, @@ -37,7 +40,7 @@ impl ChainStreamPoller { Self { provider, poll_task: PollTask::new(client, "eth_blockNumber", ()), - next_yield: BlockNumber::MAX, + next_yield: NO_BLOCK_NUMBER, known_blocks: LruCache::new(BLOCK_CACHE_SIZE), } } @@ -51,7 +54,7 @@ impl ChainStreamPoller { 'task: loop { // Clear any buffered blocks. while let Some(known_block) = self.known_blocks.pop(&self.next_yield) { - debug!("yielding block number {}", self.next_yield); + debug!(number=self.next_yield, "yielding block"); self.next_yield += 1; yield known_block; } @@ -70,11 +73,12 @@ impl ChainStreamPoller { } }; let block_number = block_number.to::(); - if block_number == self.next_yield { - continue 'task; - } - if self.next_yield > block_number { + if self.next_yield == NO_BLOCK_NUMBER { + assert!(block_number < NO_BLOCK_NUMBER, "too many blocks"); self.next_yield = block_number; + } else if block_number < self.next_yield { + debug!(block_number, self.next_yield, "not advanced yet"); + continue 'task; } // Upgrade the provider. @@ -86,20 +90,21 @@ impl ChainStreamPoller { // Then try to fill as many blocks as possible. // TODO: Maybe use `join_all` let mut retries = MAX_RETRIES; - for block_number in self.next_yield..=block_number { - let block = match provider.get_block_by_number(block_number, false).await { + for number in self.next_yield..=block_number { + debug!(number, "fetching block"); + let block = match provider.get_block_by_number(number, false).await { Ok(block) => block, Err(RpcError::Transport(err)) if retries > 0 && err.recoverable() => { - debug!(block_number, %err, "failed to fetch block, retrying"); + debug!(number, %err, "failed to fetch block, retrying"); retries -= 1; continue; } Err(err) => { - error!(block_number, %err, "failed to fetch block"); + error!(number, %err, "failed to fetch block"); break 'task; } }; - self.known_blocks.put(block_number, block); + self.known_blocks.put(number, block); } } } diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index 0958097cd19..c3e76ca1124 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -17,6 +17,7 @@ use tokio::{ }; /// A configuration object for watching for transaction confirmation. +#[derive(Debug)] pub struct WatchConfig { /// The transaction hash to watch for. tx_hash: B256, @@ -65,6 +66,7 @@ struct TxWatcher { impl TxWatcher { /// Notify the waiter. fn notify(self) { + debug!(tx=%self.config.tx_hash, "notifying"); let _ = self.tx.send(()); } } @@ -92,13 +94,15 @@ impl PendingTransaction { } impl Future for PendingTransaction { - type Output = TransportResult<()>; + type Output = TransportResult; fn poll( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - self.rx.poll_unpin(cx).map(|res| res.map_err(|_| TransportErrorKind::backend_gone())) + self.rx + .poll_unpin(cx) + .map(|res| res.map(|()| self.tx_hash).map_err(|_| TransportErrorKind::backend_gone())) } } @@ -139,7 +143,7 @@ pub(crate) struct Heartbeat { unconfirmed: HashMap, /// Ordered map of transactions waiting for confirmations. - waiting_confs: BTreeMap, + waiting_confs: BTreeMap>, /// Ordered map of transactions to reap at a certain time. reap_at: BTreeMap, @@ -159,14 +163,11 @@ impl> Heartbeat { impl Heartbeat { /// Check if any transactions have enough confirmations to notify. - fn check_confirmations(&mut self, latest: &watch::Sender>) { - if let Some(current_height) = latest.borrow().as_ref().unwrap().header.number { - let to_keep = self.waiting_confs.split_off(¤t_height); - let to_notify = std::mem::replace(&mut self.waiting_confs, to_keep); - - for (_, watcher) in to_notify.into_iter() { - watcher.notify(); - } + fn check_confirmations(&mut self, current_height: &U256) { + let to_keep = self.waiting_confs.split_off(current_height); + let to_notify = std::mem::replace(&mut self.waiting_confs, to_keep); + for watcher in to_notify.into_values().flatten() { + watcher.notify(); } } @@ -186,14 +187,18 @@ impl Heartbeat { let to_reap = std::mem::replace(&mut self.reap_at, to_keep); for tx_hash in to_reap.values() { - self.unconfirmed.remove(tx_hash); + if self.unconfirmed.remove(tx_hash).is_some() { + debug!(tx=%tx_hash, "reaped"); + } } } /// Handle a watch instruction by adding it to the watch list, and /// potentially adding it to our `reap_at` list. fn handle_watch_ix(&mut self, to_watch: TxWatcher) { - // start watching for the tx + // Start watching for the transaction. + debug!(tx=%to_watch.config.tx_hash, "watching"); + trace!(?to_watch.config); if let Some(timeout) = to_watch.config.timeout { self.reap_at.insert(Instant::now() + timeout, to_watch.config.tx_hash); } @@ -205,31 +210,33 @@ impl Heartbeat { /// the latest block. fn handle_new_block(&mut self, block: Block, latest: &watch::Sender>) { // Blocks without numbers are ignored, as they're not part of the chain. - let Some(block_height) = block.header.number else { - return; - }; + let Some(block_height) = &block.header.number else { return }; - // check if we are watching for any of the txns in this block + // Check if we are watching for any of the transactions in this block. let to_check = block.transactions.hashes().filter_map(|tx_hash| self.unconfirmed.remove(tx_hash)); for watcher in to_check { - // If `confirmations` is 1 or less, notify the watcher. - let confs = watcher.config.confirmations; - if confs <= 1 { + // If `confirmations` is 0 we can notify the watcher immediately. + let confirmations = watcher.config.confirmations; + if confirmations == 0 { watcher.notify(); continue; } // Otherwise add it to the waiting list. - self.waiting_confs.insert(block_height + U256::from(confs), watcher); + debug!(tx=%watcher.config.tx_hash, %block_height, confirmations, "adding to waiting list"); + self.waiting_confs + .entry(*block_height + U256::from(confirmations)) + .or_default() + .push(watcher); } + self.check_confirmations(block_height); + // Update the latest block. We use `send_replace` here to ensure the // latest block is always up to date, even if no receivers exist. - // C.f. - // https://docs.rs/tokio/latest/tokio/sync/watch/struct.Sender.html#method.send + // C.f. https://docs.rs/tokio/latest/tokio/sync/watch/struct.Sender.html#method.send + debug!(%block_height, "updating latest block"); let _ = latest.send_replace(Some(block)); - - self.check_confirmations(latest); } } @@ -254,7 +261,7 @@ impl + Unpin + Send + 'static> Heartbeat { None => break 'shutdown, // ix channel is closed }, - // Wake up to handle new blocks + // Wake up to handle new blocks. block = self.stream.select_next_some() => { self.handle_new_block(block, &latest); }, diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 2ae7652380c..71b37233e6d 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -159,7 +159,7 @@ impl Provider for RootProvider { } } -// Internal implementation for [`chain_stream_poller`]. +// Internal implementation for [`ChainStreamPoller`]. #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for RootProviderInner { @@ -343,7 +343,7 @@ mod tests { async fn test_send_tx() { init_tracing(); - let anvil = alloy_node_bindings::Anvil::new().block_time(1u64).spawn(); + let anvil = alloy_node_bindings::Anvil::new().spawn(); let url = anvil.endpoint().parse().unwrap(); let http = Http::::new(url); let provider = RootProvider::::new(RpcClient::new(http, true)); @@ -356,7 +356,8 @@ mod tests { ..Default::default() }; let pending_tx = provider.send_transaction(&TxLegacy(tx)).await.expect("failed to send tx"); - eprintln!("{pending_tx:?}"); - let () = pending_tx.await.expect("failed to await pending tx"); + let hash1 = pending_tx.tx_hash; + let hash2 = pending_tx.await.expect("failed to await pending tx"); + assert_eq!(hash1, hash2); } } From e9396b22a79e8df8cde2d6e040a177c5335873eb Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:50:47 +0100 Subject: [PATCH 23/27] nit --- crates/rpc-client/src/poller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-client/src/poller.rs b/crates/rpc-client/src/poller.rs index b22f05b0ba7..aa7934b68d3 100644 --- a/crates/rpc-client/src/poller.rs +++ b/crates/rpc-client/src/poller.rs @@ -123,7 +123,7 @@ where Ok(p) => p, Err(err) => { error!(%err, "failed to serialize params"); - break 'outer; + break; } }; From ad4b7d554bdad05b2d0bdffb922ffbcec6595387 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:53:45 +0100 Subject: [PATCH 24/27] fix: pin the sleep --- crates/providers/src/heart.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index c3e76ca1124..0c8428d5466 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -249,9 +249,11 @@ impl + Unpin + Send + 'static> Heartbeat { let fut = async move { 'shutdown: loop { { + let next_reap = self.next_reap(); + let sleep = std::pin::pin!(tokio::time::sleep_until(next_reap.into())); + // We bias the select so that we always handle new messages // before checking blocks, and reap timeouts are last. - let next_reap = self.next_reap(); select! { biased; @@ -268,7 +270,7 @@ impl + Unpin + Send + 'static> Heartbeat { // This arm ensures we always wake up to reap timeouts, // even if there are no other events. - _ = tokio::time::sleep_until(next_reap.into()) => {}, + _ = sleep => {}, } } From 781df8180d2e4d14dd8ebc1abaa9ac9ed281ee66 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:30:16 +0100 Subject: [PATCH 25/27] clippies --- crates/json-rpc/src/request.rs | 1 - crates/providers/src/heart.rs | 3 ++- crates/providers/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/json-rpc/src/request.rs b/crates/json-rpc/src/request.rs index 5ff5c175724..b5bc2021185 100644 --- a/crates/json-rpc/src/request.rs +++ b/crates/json-rpc/src/request.rs @@ -150,7 +150,6 @@ where // Params may be omitted if it is 0-sized if sized_params { - // TODO: remove unwrap map.serialize_entry("params", &self.params)?; } diff --git a/crates/providers/src/heart.rs b/crates/providers/src/heart.rs index 0c8428d5466..a34d9c2a968 100644 --- a/crates/providers/src/heart.rs +++ b/crates/providers/src/heart.rs @@ -1,4 +1,3 @@ -#![allow(dead_code, unreachable_pub)] // TODO: remove //! Block Hearbeat and Transaction Watcher use alloy_primitives::{B256, U256}; @@ -110,6 +109,7 @@ impl Future for PendingTransaction { #[derive(Debug, Clone)] pub(crate) struct HeartbeatHandle { tx: mpsc::Sender, + #[allow(dead_code)] latest: watch::Receiver>, } @@ -128,6 +128,7 @@ impl HeartbeatHandle { } /// Returns a watcher that always sees the latest block. + #[allow(dead_code)] pub(crate) fn latest(&self) -> &watch::Receiver> { &self.latest } diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 6ca633425a8..dab33965640 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -25,7 +25,7 @@ pub use builder::{ProviderBuilder, ProviderLayer, Stack}; mod chain; mod heart; -pub use heart::PendingTransaction; +pub use heart::{PendingTransaction, WatchConfig}; pub mod new; pub use new::{Provider, ProviderRef, RootProvider, WeakProvider}; From 1cee23c1965d778be54d50caad37228cd7cefc0e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:48:28 +0100 Subject: [PATCH 26/27] wasmm --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57673b507cc..f8e64e23dd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,12 +67,14 @@ jobs: - name: cargo hack run: | cargo hack check --workspace --target wasm32-unknown-unknown \ + --exclude alloy-contract \ + --exclude alloy-node-bindings \ + --exclude alloy-providers \ --exclude alloy-signer \ --exclude alloy-signer-aws \ --exclude alloy-signer-gcp \ --exclude alloy-signer-ledger \ --exclude alloy-signer-trezor \ - --exclude alloy-node-bindings \ --exclude alloy-transport-ipc feature-checks: From 4a34ef697b66f360def711b7457e28e908d1b229 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:58:28 +0100 Subject: [PATCH 27/27] fix: feature --- crates/providers/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 05ca24ec061..7cc9ff377c9 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -29,7 +29,7 @@ lru = "0.12.2" reqwest.workspace = true serde.workspace = true thiserror.workspace = true -tokio = { workspace = true, features = ["sync"] } +tokio = { workspace = true, features = ["sync", "macros"] } tracing.workspace = true [dev-dependencies]