1use crate::{poller::PollerBuilder, BatchRequest, ClientBuilder, RpcCall};
2use alloy_json_rpc::{Id, Request, RpcRecv, RpcSend};
3use alloy_transport::{mock::Asserter, BoxTransport, IntoBoxTransport};
4use std::{
5 borrow::Cow,
6 ops::Deref,
7 sync::{
8 atomic::{AtomicU64, Ordering},
9 Arc, Weak,
10 },
11 time::Duration,
12};
13use tower::{layer::util::Identity, ServiceBuilder};
14
15pub type WeakClient = Weak<RpcClientInner>;
17
18pub type ClientRef<'a> = &'a RpcClientInner;
20
21pub type NoParams = [(); 0];
23
24#[cfg(feature = "pubsub")]
25type MaybePubsub = Option<alloy_pubsub::PubSubFrontend>;
26
27#[derive(Debug)]
34pub struct RpcClient(Arc<RpcClientInner>);
35
36impl Clone for RpcClient {
37 fn clone(&self) -> Self {
38 Self(Arc::clone(&self.0))
39 }
40}
41
42impl RpcClient {
43 pub const fn builder() -> ClientBuilder<Identity> {
45 ClientBuilder { builder: ServiceBuilder::new() }
46 }
47}
48
49impl RpcClient {
50 pub fn new(t: impl IntoBoxTransport, is_local: bool) -> Self {
52 Self::new_maybe_pubsub(
53 t,
54 is_local,
55 #[cfg(feature = "pubsub")]
56 None,
57 )
58 }
59
60 pub fn mocked(asserter: Asserter) -> Self {
63 Self::new(alloy_transport::mock::MockTransport::new(asserter), true)
64 }
65
66 #[cfg(all(feature = "reqwest", not(all(target_os = "wasi", target_env = "p1"))))]
68 pub fn new_http(url: reqwest::Url) -> Self {
69 let http = alloy_transport_http::Http::new(url);
70 let is_local = http.guess_local();
71 Self::new(http, is_local)
72 }
73
74 #[cfg(all(feature = "reqwest", not(all(target_os = "wasi", target_env = "p1"))))]
76 pub fn new_http_with_client(client: reqwest::Client, url: reqwest::Url) -> Self {
77 let http = alloy_transport_http::Http::with_client(client, url);
78 let is_local = http.guess_local();
79 Self::new(http, is_local)
80 }
81
82 fn new_maybe_pubsub(
84 t: impl IntoBoxTransport,
85 is_local: bool,
86 #[cfg(feature = "pubsub")] pubsub: MaybePubsub,
87 ) -> Self {
88 Self(Arc::new(RpcClientInner::new_maybe_pubsub(
89 t,
90 is_local,
91 #[cfg(feature = "pubsub")]
92 pubsub,
93 )))
94 }
95
96 pub(crate) fn new_layered<F, T, R>(is_local: bool, main_transport: T, layer: F) -> Self
107 where
108 F: FnOnce(T) -> R,
109 T: IntoBoxTransport,
110 R: IntoBoxTransport,
111 {
112 #[cfg(feature = "pubsub")]
113 {
114 let t = main_transport.clone().into_box_transport();
115 let maybe_pubsub = t.as_any().downcast_ref::<alloy_pubsub::PubSubFrontend>().cloned();
116 Self::new_maybe_pubsub(layer(main_transport), is_local, maybe_pubsub)
117 }
118
119 #[cfg(not(feature = "pubsub"))]
120 Self::new(layer(main_transport), is_local)
121 }
122
123 pub fn from_inner(inner: RpcClientInner) -> Self {
125 Self(Arc::new(inner))
126 }
127
128 pub const fn inner(&self) -> &Arc<RpcClientInner> {
130 &self.0
131 }
132
133 pub fn into_inner(self) -> Arc<RpcClientInner> {
135 self.0
136 }
137
138 pub fn get_weak(&self) -> WeakClient {
140 Arc::downgrade(&self.0)
141 }
142
143 pub fn get_ref(&self) -> ClientRef<'_> {
145 &self.0
146 }
147
148 pub fn with_poll_interval(self, poll_interval: Duration) -> Self {
153 self.inner().set_poll_interval(poll_interval);
154 self
155 }
156
157 pub fn prepare_static_poller<Params, Resp>(
161 &self,
162 method: impl Into<Cow<'static, str>>,
163 params: Params,
164 ) -> PollerBuilder<Params, Resp>
165 where
166 Params: RpcSend + 'static,
167 Resp: RpcRecv + Clone,
168 {
169 PollerBuilder::new(self.get_weak(), method, params)
170 }
171
172 #[inline]
174 pub fn new_batch(&self) -> BatchRequest<'_> {
175 BatchRequest::new(&self.0)
176 }
177}
178
179impl Deref for RpcClient {
180 type Target = RpcClientInner;
181
182 #[inline]
183 fn deref(&self) -> &Self::Target {
184 &self.0
185 }
186}
187
188#[derive(Debug)]
201pub struct RpcClientInner {
202 pub(crate) transport: BoxTransport,
204 #[cfg(feature = "pubsub")]
211 pub(crate) pubsub: MaybePubsub,
212 pub(crate) is_local: bool,
214 pub(crate) id: AtomicU64,
216 pub(crate) poll_interval: AtomicU64,
218}
219
220impl RpcClientInner {
221 #[inline]
226 pub fn new(t: impl IntoBoxTransport, is_local: bool) -> Self {
227 Self {
228 transport: t.into_box_transport(),
229 #[cfg(feature = "pubsub")]
230 pubsub: None,
231 is_local,
232 id: AtomicU64::new(0),
233 poll_interval: if is_local { AtomicU64::new(250) } else { AtomicU64::new(7000) },
234 }
235 }
236
237 pub(crate) fn new_maybe_pubsub(
240 t: impl IntoBoxTransport,
241 is_local: bool,
242 #[cfg(feature = "pubsub")] pubsub: MaybePubsub,
243 ) -> Self {
244 Self {
245 #[cfg(feature = "pubsub")]
246 pubsub,
247 ..Self::new(t, is_local)
248 }
249 }
250
251 #[inline]
253 pub fn with_id(self, id: u64) -> Self {
254 Self { id: AtomicU64::new(id), ..self }
255 }
256
257 pub fn poll_interval(&self) -> Duration {
259 Duration::from_millis(self.poll_interval.load(Ordering::Relaxed))
260 }
261
262 pub fn set_poll_interval(&self, poll_interval: Duration) {
265 self.poll_interval.store(poll_interval.as_millis() as u64, Ordering::Relaxed);
266 }
267
268 #[inline]
270 pub const fn transport(&self) -> &BoxTransport {
271 &self.transport
272 }
273
274 #[inline]
276 pub const fn transport_mut(&mut self) -> &mut BoxTransport {
277 &mut self.transport
278 }
279
280 #[inline]
282 pub fn into_transport(self) -> BoxTransport {
283 self.transport
284 }
285
286 #[cfg(feature = "pubsub")]
288 #[inline]
289 #[track_caller]
290 pub fn pubsub_frontend(&self) -> Option<&alloy_pubsub::PubSubFrontend> {
291 if let Some(pubsub) = &self.pubsub {
292 return Some(pubsub);
293 }
294 self.transport.as_any().downcast_ref::<alloy_pubsub::PubSubFrontend>()
295 }
296
297 #[cfg(feature = "pubsub")]
303 #[inline]
304 #[track_caller]
305 pub fn expect_pubsub_frontend(&self) -> &alloy_pubsub::PubSubFrontend {
306 self.pubsub_frontend().expect("called pubsub_frontend on a non-pubsub transport")
307 }
308
309 #[inline]
315 pub fn make_request<Params: RpcSend>(
316 &self,
317 method: impl Into<Cow<'static, str>>,
318 params: Params,
319 ) -> Request<Params> {
320 Request::new(method, self.next_id(), params)
321 }
322
323 #[inline]
330 pub const fn is_local(&self) -> bool {
331 self.is_local
332 }
333
334 #[inline]
336 pub const fn set_local(&mut self, is_local: bool) {
337 self.is_local = is_local;
338 }
339
340 #[inline]
342 fn increment_id(&self) -> u64 {
343 self.id.fetch_add(1, Ordering::Relaxed)
344 }
345
346 #[inline]
348 pub fn next_id(&self) -> Id {
349 self.increment_id().into()
350 }
351
352 #[doc(alias = "prepare")]
363 pub fn request<Params: RpcSend, Resp: RpcRecv>(
364 &self,
365 method: impl Into<Cow<'static, str>>,
366 params: Params,
367 ) -> RpcCall<Params, Resp> {
368 let request = self.make_request(method, params);
369 RpcCall::new(request, self.transport.clone())
370 }
371
372 pub fn request_noparams<Resp: RpcRecv>(
376 &self,
377 method: impl Into<Cow<'static, str>>,
378 ) -> RpcCall<NoParams, Resp> {
379 self.request(method, [])
380 }
381}
382
383#[cfg(feature = "pubsub")]
384mod pubsub_impl {
385 use super::*;
386 use alloy_pubsub::{PubSubConnect, RawSubscription, Subscription};
387 use alloy_transport::TransportResult;
388
389 impl RpcClientInner {
390 pub async fn get_raw_subscription(&self, id: alloy_primitives::B256) -> RawSubscription {
396 self.expect_pubsub_frontend().get_subscription(id).await.unwrap()
397 }
398
399 pub async fn get_subscription<T: serde::de::DeserializeOwned>(
405 &self,
406 id: alloy_primitives::B256,
407 ) -> Subscription<T> {
408 Subscription::from(self.get_raw_subscription(id).await)
409 }
410 }
411
412 impl RpcClient {
413 pub async fn connect_pubsub<C: PubSubConnect>(connect: C) -> TransportResult<Self> {
415 ClientBuilder::default().pubsub(connect).await
416 }
417
418 #[track_caller]
429 pub fn channel_size(&self) -> usize {
430 self.expect_pubsub_frontend().channel_size()
431 }
432
433 #[track_caller]
439 pub fn set_channel_size(&self, size: usize) {
440 self.expect_pubsub_frontend().set_channel_size(size)
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use similar_asserts::assert_eq;
449
450 #[test]
451 fn test_client_with_poll_interval() {
452 let poll_interval = Duration::from_millis(5_000);
453 let client = RpcClient::new_http(reqwest::Url::parse("http://localhost").unwrap())
454 .with_poll_interval(poll_interval);
455 assert_eq!(client.poll_interval(), poll_interval);
456 }
457}