Skip to main content

alloy_provider/provider/
prov_call.rs

1use alloy_json_rpc::{RequestMeta, RpcRecv, RpcSend};
2use alloy_rpc_client::{RpcCall, Waiter};
3use alloy_transport::TransportResult;
4use futures::FutureExt;
5use http::{HeaderMap, HeaderName, HeaderValue};
6use pin_project::pin_project;
7use serde_json::value::RawValue;
8use std::{
9    future::Future,
10    pin::Pin,
11    task::{self, Poll},
12};
13use tokio::sync::oneshot;
14
15#[cfg(not(target_family = "wasm"))]
16/// Boxed future type used in [`ProviderCall`] for non-wasm targets.
17pub type BoxedFut<Output> = Pin<Box<dyn Future<Output = TransportResult<Output>> + Send>>;
18
19#[cfg(target_family = "wasm")]
20/// Boxed future type used in [`ProviderCall`] for wasm targets.
21pub type BoxedFut<Output> = Pin<Box<dyn Future<Output = TransportResult<Output>>>>;
22/// The primary future type for the [`Provider`].
23///
24/// This future abstracts over several potential data sources. It allows
25/// providers to:
26/// - produce data via an [`RpcCall`]
27/// - produce data by waiting on a batched RPC [`Waiter`]
28/// - proudce data via an arbitrary boxed future
29/// - produce data in any synchronous way
30///
31/// [`Provider`]: crate::Provider
32#[pin_project(project = ProviderCallProj)]
33pub enum ProviderCall<Params, Resp, Output = Resp, Map = fn(Resp) -> Output>
34where
35    Params: RpcSend,
36    Resp: RpcRecv,
37    Map: Fn(Resp) -> Output,
38{
39    /// An underlying call to an RPC server.
40    RpcCall(RpcCall<Params, Resp, Output, Map>),
41    /// A waiter for a batched call to a remote RPC server.
42    Waiter(Waiter<Resp, Output, Map>),
43    /// A boxed future.
44    BoxedFuture(BoxedFut<Output>),
45    /// The output, produces synchronously.
46    Ready(Option<TransportResult<Output>>),
47}
48
49impl<Params, Resp, Output, Map> ProviderCall<Params, Resp, Output, Map>
50where
51    Params: RpcSend,
52    Resp: RpcRecv,
53    Map: Fn(Resp) -> Output,
54{
55    /// Instantiate a new [`ProviderCall`] from the output.
56    pub const fn ready(output: TransportResult<Output>) -> Self {
57        Self::Ready(Some(output))
58    }
59
60    /// True if this is an RPC call.
61    pub const fn is_rpc_call(&self) -> bool {
62        matches!(self, Self::RpcCall(_))
63    }
64
65    /// Fallible cast to [`RpcCall`]
66    pub const fn as_rpc_call(&self) -> Option<&RpcCall<Params, Resp, Output, Map>> {
67        match self {
68            Self::RpcCall(call) => Some(call),
69            _ => None,
70        }
71    }
72
73    /// Fallible cast to mutable [`RpcCall`]
74    pub const fn as_mut_rpc_call(&mut self) -> Option<&mut RpcCall<Params, Resp, Output, Map>> {
75        match self {
76            Self::RpcCall(call) => Some(call),
77            _ => None,
78        }
79    }
80
81    /// True if this is a waiter.
82    pub const fn is_waiter(&self) -> bool {
83        matches!(self, Self::Waiter(_))
84    }
85
86    /// Fallible cast to [`Waiter`]
87    pub const fn as_waiter(&self) -> Option<&Waiter<Resp, Output, Map>> {
88        match self {
89            Self::Waiter(waiter) => Some(waiter),
90            _ => None,
91        }
92    }
93
94    /// Fallible cast to mutable [`Waiter`]
95    pub const fn as_mut_waiter(&mut self) -> Option<&mut Waiter<Resp, Output, Map>> {
96        match self {
97            Self::Waiter(waiter) => Some(waiter),
98            _ => None,
99        }
100    }
101
102    /// True if this is a boxed future.
103    pub const fn is_boxed_future(&self) -> bool {
104        matches!(self, Self::BoxedFuture(_))
105    }
106
107    /// Fallible cast to a boxed future.
108    pub const fn as_boxed_future(&self) -> Option<&BoxedFut<Output>> {
109        match self {
110            Self::BoxedFuture(fut) => Some(fut),
111            _ => None,
112        }
113    }
114
115    /// True if this is a ready value.
116    pub const fn is_ready(&self) -> bool {
117        matches!(self, Self::Ready(_))
118    }
119
120    /// Fallible cast to a ready value.
121    ///
122    /// # Panics
123    ///
124    /// Panics if the future is already complete
125    pub const fn as_ready(&self) -> Option<&TransportResult<Output>> {
126        match self {
127            Self::Ready(Some(output)) => Some(output),
128            Self::Ready(None) => panic!("tried to access ready value after taking"),
129            _ => None,
130        }
131    }
132
133    /// Set a function to map the response into a different type. This is
134    /// useful for transforming the response into a more usable type, e.g.
135    /// changing `U64` to `u64`.
136    ///
137    /// This function fails if the inner future is not an [`RpcCall`] or
138    /// [`Waiter`].
139    ///
140    /// ## Note
141    ///
142    /// Carefully review the rust documentation on [fn pointers] before passing
143    /// them to this function. Unless the pointer is specifically coerced to a
144    /// `fn(_) -> _`, the `NewMap` will be inferred as that function's unique
145    /// type. This can lead to confusing error messages.
146    ///
147    /// [fn pointers]: https://doc.rust-lang.org/std/primitive.fn.html#creating-function-pointers
148    pub fn map_resp<NewOutput, NewMap>(
149        self,
150        map: NewMap,
151    ) -> Result<ProviderCall<Params, Resp, NewOutput, NewMap>, Self>
152    where
153        NewMap: Fn(Resp) -> NewOutput + Clone,
154    {
155        match self {
156            Self::RpcCall(call) => Ok(ProviderCall::RpcCall(call.map_resp(map))),
157            Self::Waiter(waiter) => Ok(ProviderCall::Waiter(waiter.map_resp(map))),
158            _ => Err(self),
159        }
160    }
161
162    /// Maps the metadata of the underlying RPC request.
163    ///
164    /// This can be used with typed [`Provider`](crate::Provider) methods to
165    /// attach request-scoped metadata such as HTTP headers without falling
166    /// back to a raw RPC call:
167    ///
168    /// ```no_run
169    /// # use alloy_provider::{Provider, ProviderBuilder};
170    /// # use http::{HeaderMap, HeaderValue};
171    /// # async fn example() -> alloy_transport::TransportResult<()> {
172    /// # let provider = ProviderBuilder::new().connect("http://localhost:8545").await?;
173    /// let mut headers = HeaderMap::new();
174    /// headers.insert("x-api-key", HeaderValue::from_static("secret"));
175    ///
176    /// let call = provider.get_block_number().map_meta(|mut meta| {
177    ///     meta.headers_mut().extend(headers);
178    ///     meta
179    /// });
180    /// let Ok(call) = call else { unreachable!("typed provider method should produce an RPC call") };
181    /// let block_number = call.await?;
182    /// # Ok(())
183    /// # }
184    /// ```
185    ///
186    /// This function fails if the inner future is not an [`RpcCall`], since
187    /// boxed, batched, or ready calls no longer expose an individual request
188    /// whose metadata can be changed.
189    pub fn map_meta(self, f: impl FnOnce(RequestMeta) -> RequestMeta) -> Result<Self, Self> {
190        match self {
191            Self::RpcCall(call) => Ok(Self::RpcCall(call.map_meta(f))),
192            _ => Err(self),
193        }
194    }
195
196    /// Adds HTTP headers to the underlying RPC request.
197    ///
198    /// Existing values with the same header names are replaced. This function
199    /// fails if the inner future is not an [`RpcCall`].
200    pub fn with_headers(self, headers: HeaderMap) -> Result<Self, Self> {
201        self.map_meta(|mut meta| {
202            meta.headers_mut().extend(headers);
203            meta
204        })
205    }
206
207    /// Adds an HTTP header to the underlying RPC request.
208    ///
209    /// An existing value with the same header name is replaced. This function
210    /// fails if the inner future is not an [`RpcCall`].
211    pub fn with_header(self, name: HeaderName, value: HeaderValue) -> Result<Self, Self> {
212        self.map_meta(|mut meta| {
213            meta.headers_mut().insert(name, value);
214            meta
215        })
216    }
217}
218
219impl<Params, Resp, Output, Map> ProviderCall<&Params, Resp, Output, Map>
220where
221    Params: RpcSend + ToOwned,
222    Params::Owned: RpcSend,
223    Resp: RpcRecv,
224    Map: Fn(Resp) -> Output,
225{
226    /// Convert this call into one with owned params, by cloning the params.
227    ///
228    /// # Panics
229    ///
230    /// Panics if called after the request has been polled.
231    pub fn into_owned_params(self) -> ProviderCall<Params::Owned, Resp, Output, Map> {
232        match self {
233            Self::RpcCall(call) => ProviderCall::RpcCall(call.into_owned_params()),
234            _ => panic!(),
235        }
236    }
237}
238
239impl<Params, Resp> std::fmt::Debug for ProviderCall<Params, Resp>
240where
241    Params: RpcSend,
242    Resp: RpcRecv,
243{
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            Self::RpcCall(call) => f.debug_tuple("RpcCall").field(call).finish(),
247            Self::Waiter { .. } => f.debug_struct("Waiter").finish_non_exhaustive(),
248            Self::BoxedFuture(_) => f.debug_struct("BoxedFuture").finish_non_exhaustive(),
249            Self::Ready(_) => f.debug_struct("Ready").finish_non_exhaustive(),
250        }
251    }
252}
253
254impl<Params, Resp, Output, Map> From<RpcCall<Params, Resp, Output, Map>>
255    for ProviderCall<Params, Resp, Output, Map>
256where
257    Params: RpcSend,
258    Resp: RpcRecv,
259    Map: Fn(Resp) -> Output,
260{
261    fn from(call: RpcCall<Params, Resp, Output, Map>) -> Self {
262        Self::RpcCall(call)
263    }
264}
265
266impl<Params, Resp> From<Waiter<Resp>> for ProviderCall<Params, Resp, Resp, fn(Resp) -> Resp>
267where
268    Params: RpcSend,
269    Resp: RpcRecv,
270{
271    fn from(waiter: Waiter<Resp>) -> Self {
272        Self::Waiter(waiter)
273    }
274}
275
276impl<Params, Resp, Output, Map> From<BoxedFut<Output>> for ProviderCall<Params, Resp, Output, Map>
277where
278    Params: RpcSend,
279    Resp: RpcRecv,
280    Map: Fn(Resp) -> Output,
281{
282    fn from(fut: BoxedFut<Output>) -> Self {
283        Self::BoxedFuture(fut)
284    }
285}
286
287impl<Params, Resp> From<oneshot::Receiver<TransportResult<Box<RawValue>>>>
288    for ProviderCall<Params, Resp>
289where
290    Params: RpcSend,
291    Resp: RpcRecv,
292{
293    fn from(rx: oneshot::Receiver<TransportResult<Box<RawValue>>>) -> Self {
294        Waiter::from(rx).into()
295    }
296}
297
298impl<Params, Resp, Output, Map> Future for ProviderCall<Params, Resp, Output, Map>
299where
300    Params: RpcSend,
301    Resp: RpcRecv,
302    Output: 'static,
303    Map: Fn(Resp) -> Output,
304{
305    type Output = TransportResult<Output>;
306
307    fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
308        match self.as_mut().project() {
309            ProviderCallProj::RpcCall(call) => call.poll_unpin(cx),
310            ProviderCallProj::Waiter(waiter) => waiter.poll_unpin(cx),
311            ProviderCallProj::BoxedFuture(fut) => fut.poll_unpin(cx),
312            ProviderCallProj::Ready(output) => {
313                Poll::Ready(output.take().expect("output taken twice"))
314            }
315        }
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use alloy_rpc_client::{ClientBuilder, NoParams};
323    use alloy_transport::mock::{Asserter, MockTransport};
324    use http::HeaderValue;
325
326    #[test]
327    fn map_meta_updates_rpc_call_metadata() {
328        let client = ClientBuilder::default().transport(MockTransport::new(Asserter::new()), true);
329        let call: ProviderCall<NoParams, u64> = client.request_noparams("test_method").into();
330
331        let call = call
332            .map_meta(|mut meta| {
333                meta.headers_mut().insert("x-api-key", HeaderValue::from_static("secret"));
334                meta
335            })
336            .expect("call is an RPC call");
337
338        assert_eq!(
339            call.as_rpc_call().unwrap().request().meta.headers().unwrap().get("x-api-key"),
340            Some(&HeaderValue::from_static("secret"))
341        );
342    }
343
344    #[test]
345    fn map_meta_returns_non_rpc_call() {
346        let call = ProviderCall::<NoParams, u64>::ready(Ok(1));
347        assert!(call.map_meta(std::convert::identity).is_err());
348    }
349
350    #[test]
351    fn with_headers_updates_rpc_call_headers() {
352        let client = ClientBuilder::default().transport(MockTransport::new(Asserter::new()), true);
353        let call: ProviderCall<NoParams, u64> = client.request_noparams("test_method").into();
354        let mut headers = HeaderMap::new();
355        headers.insert("x-api-key", HeaderValue::from_static("secret"));
356
357        let call = call.with_headers(headers).expect("call is an RPC call");
358
359        assert_eq!(
360            call.as_rpc_call().unwrap().request().meta.headers().unwrap().get("x-api-key"),
361            Some(&HeaderValue::from_static("secret"))
362        );
363    }
364
365    #[test]
366    fn with_header_updates_rpc_call_header() {
367        let client = ClientBuilder::default().transport(MockTransport::new(Asserter::new()), true);
368        let call: ProviderCall<NoParams, u64> = client.request_noparams("test_method").into();
369
370        let call = call
371            .with_header(HeaderName::from_static("x-api-key"), HeaderValue::from_static("secret"))
372            .expect("call is an RPC call");
373
374        assert_eq!(
375            call.as_rpc_call().unwrap().request().meta.headers().unwrap().get("x-api-key"),
376            Some(&HeaderValue::from_static("secret"))
377        );
378    }
379}