Skip to content

Commit

Permalink
feat: eth_call builder (alloy-rs#645)
Browse files Browse the repository at this point in the history
* feature: eth_call via builder pattern

* fix: ethcall and ethcallfut in contract

* lint: clippy

* fix: must_use

* fix: must_use again

* refactor: borrow hashmap instead of cloning

* refactor: use cow and make non-optional

* lint: clippy false positive

* refactor: rename to eth_call

* refactor: remove a lifetime

* fix: useless match

* fix: poll_running instead of yielding forever

* refactor: poll_unpin

* doc: example and aliases

* doc: add a header to the example
  • Loading branch information
prestwich authored and ben186 committed Jul 27, 2024
1 parent 838975a commit 39db466
Show file tree
Hide file tree
Showing 7 changed files with 449 additions and 32 deletions.
29 changes: 15 additions & 14 deletions crates/contract/src/call.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Error, Result};
use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt};
use crate::{CallDecoder, Error, EthCall, Result};
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
use alloy_json_abi::Function;
use alloy_network::{Ethereum, Network, ReceiptResponse, TransactionBuilder};
use alloy_primitives::{Address, Bytes, TxKind, U256};
Expand Down Expand Up @@ -484,9 +484,10 @@ impl<T: Transport + Clone, P: Provider<T, N>, D: CallDecoder, N: Network> CallBu
///
/// Returns the decoded the output by using the provided decoder.
/// If this is not desired, use [`call_raw`](Self::call_raw) to get the raw output data.
pub async fn call(&self) -> Result<D::CallOutput> {
let data = self.call_raw().await?;
self.decode_output(data, false)
#[doc(alias = "eth_call")]
#[doc(alias = "call_with_overrides")]
pub fn call(&self) -> EthCall<'_, '_, '_, D, T, N> {
self.call_raw().with_decoder(&self.decoder)
}

/// Queries the blockchain via an `eth_call` without submitting a transaction to the network.
Expand All @@ -495,13 +496,13 @@ impl<T: Transport + Clone, P: Provider<T, N>, D: CallDecoder, N: Network> CallBu
/// Does not decode the output of the call, returning the raw output data instead.
///
/// See [`call`](Self::call) for more information.
pub async fn call_raw(&self) -> Result<Bytes> {
if let Some(state) = &self.state {
self.provider.call_with_overrides(&self.request, self.block, state.clone()).await
} else {
self.provider.call(&self.request, self.block).await
}
.map_err(Into::into)
pub fn call_raw(&self) -> EthCall<'_, '_, '_, (), T, N> {
let call = self.provider.call(&self.request).block(self.block);
let call = match &self.state {
Some(state) => call.overrides(state),
None => call,
};
call.into()
}

/// Returns a cloned instance of this CallBuilder's decoder
Expand Down Expand Up @@ -579,7 +580,7 @@ impl<T, P, D, N> IntoFuture for CallBuilder<T, P, D, N>
where
T: Transport + Clone,
P: Provider<T, N>,
D: CallDecoder + Send + Sync,
D: CallDecoder + Send + Sync + Unpin,
N: Network,
Self: 'static,
{
Expand Down Expand Up @@ -734,7 +735,7 @@ mod tests {
);
// Box the future to assert its concrete output type.
let _future: Box<dyn Future<Output = Result<MyContract::doStuffReturn>> + Send> =
Box::new(call_builder.call());
Box::new(async move { call_builder.call().await });
}

#[test]
Expand Down
221 changes: 221 additions & 0 deletions crates/contract/src/eth_call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::{future::IntoFuture, marker::PhantomData};

use alloy_dyn_abi::{DynSolValue, FunctionExt};
use alloy_json_abi::Function;
use alloy_network::Network;
use alloy_primitives::Bytes;
use alloy_rpc_types::{state::StateOverride, BlockId};
use alloy_sol_types::SolCall;
use alloy_transport::Transport;

use crate::{Error, Result};

/// Raw coder.
const RAW_CODER: () = ();

mod private {
pub trait Sealed {}
impl Sealed for super::Function {}
impl<C: super::SolCall> Sealed for super::PhantomData<C> {}
impl Sealed for () {}
}

/// An [`alloy_provider::EthCall`] with an abi decoder.
#[must_use = "EthCall must be awaited to execute the call"]
#[derive(Debug, Clone)]
pub struct EthCall<'req, 'state, 'coder, D, T, N>
where
T: Transport + Clone,
N: Network,
D: CallDecoder,
{
inner: alloy_provider::EthCall<'req, 'state, T, N>,

decoder: &'coder D,
}

impl<'req, 'state, 'coder, D, T, N> EthCall<'req, 'state, 'coder, D, T, N>
where
T: Transport + Clone,
N: Network,
D: CallDecoder,
{
/// Create a new [`EthCall`].
pub const fn new(
inner: alloy_provider::EthCall<'req, 'state, T, N>,
decoder: &'coder D,
) -> Self {
Self { inner, decoder }
}
}

impl<'req, 'state, T, N> EthCall<'req, 'state, 'static, (), T, N>
where
T: Transport + Clone,
N: Network,
{
/// Create a new [`EthCall`].
pub const fn new_raw(inner: alloy_provider::EthCall<'req, 'state, T, N>) -> Self {
EthCall::new(inner, &RAW_CODER)
}
}

impl<'req, 'state, 'coder, D, T, N> EthCall<'req, 'state, 'coder, D, T, N>
where
T: Transport + Clone,
N: Network,
D: CallDecoder,
{
/// Swap the decoder for this call.
#[allow(clippy::missing_const_for_fn)] // false positive
pub fn with_decoder<'new_coder, E>(
self,
decoder: &'new_coder E,
) -> EthCall<'req, 'state, 'new_coder, E, T, N>
where
E: CallDecoder,
{
EthCall { inner: self.inner, decoder }
}

/// Set the state overrides for this call.
pub fn overrides(mut self, overrides: &'state StateOverride) -> Self {
self.inner = self.inner.overrides(overrides);
self
}

/// Set the block to use for this call.
pub fn block(mut self, block: BlockId) -> Self {
self.inner = self.inner.block(block);
self
}
}

impl<'req, 'state, T, N> From<alloy_provider::EthCall<'req, 'state, T, N>>
for EthCall<'req, 'state, 'static, (), T, N>
where
T: Transport + Clone,
N: Network,
{
fn from(inner: alloy_provider::EthCall<'req, 'state, T, N>) -> Self {
EthCall { inner, decoder: &RAW_CODER }
}
}

impl<'req, 'state, 'coder, D, T, N> std::future::IntoFuture
for EthCall<'req, 'state, 'coder, D, T, N>
where
D: CallDecoder + Unpin,
T: Transport + Clone,
N: Network,
{
type Output = Result<D::CallOutput>;

type IntoFuture = EthCallFut<'req, 'state, 'coder, D, T, N>;

fn into_future(self) -> Self::IntoFuture {
EthCallFut { inner: self.inner.into_future(), decoder: self.decoder }
}
}

/// Future for the [`EthCall`] type. This future wraps an RPC call with an abi
/// decoder.
#[must_use = "futures do nothing unless you `.await` or poll them"]
#[derive(Debug, Clone)]
pub struct EthCallFut<'req, 'state, 'coder, D, T, N>
where
T: Transport + Clone,
N: Network,
D: CallDecoder,
{
inner: <alloy_provider::EthCall<'req, 'state, T, N> as IntoFuture>::IntoFuture,
decoder: &'coder D,
}

impl<'req, 'state, 'coder, D, T, N> std::future::Future
for EthCallFut<'req, 'state, 'coder, D, T, N>
where
D: CallDecoder + Unpin,
T: Transport + Clone,
N: Network,
{
type Output = Result<D::CallOutput>;

fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.get_mut();
let pin = std::pin::pin!(&mut this.inner);
match pin.poll(cx) {
std::task::Poll::Ready(Ok(data)) => {
std::task::Poll::Ready(this.decoder.abi_decode_output(data, true))
}
std::task::Poll::Ready(Err(e)) => std::task::Poll::Ready(Err(e.into())),
std::task::Poll::Pending => std::task::Poll::Pending,
}
}
}

/// A trait for decoding the output of a contract function.
///
/// This trait is sealed and cannot be implemented manually.
/// It is an implementation detail of [`CallBuilder`].
///
/// [`CallBuilder`]: crate::CallBuilder
pub trait CallDecoder: private::Sealed {
// Not public API.

/// The output type of the contract function.
#[doc(hidden)]
type CallOutput;

/// Decodes the output of a contract function.
#[doc(hidden)]
fn abi_decode_output(&self, data: Bytes, validate: bool) -> Result<Self::CallOutput>;

#[doc(hidden)]
fn as_debug_field(&self) -> impl std::fmt::Debug;
}

impl CallDecoder for Function {
type CallOutput = Vec<DynSolValue>;

#[inline]
fn abi_decode_output(&self, data: Bytes, validate: bool) -> Result<Self::CallOutput> {
FunctionExt::abi_decode_output(self, &data, validate).map_err(Error::AbiError)
}

#[inline]
fn as_debug_field(&self) -> impl std::fmt::Debug {
self
}
}

impl<C: SolCall> CallDecoder for PhantomData<C> {
type CallOutput = C::Return;

#[inline]
fn abi_decode_output(&self, data: Bytes, validate: bool) -> Result<Self::CallOutput> {
C::abi_decode_returns(&data, validate).map_err(|e| Error::AbiError(e.into()))
}

#[inline]
fn as_debug_field(&self) -> impl std::fmt::Debug {
std::any::type_name::<C>()
}
}

impl CallDecoder for () {
type CallOutput = Bytes;

#[inline]
fn abi_decode_output(&self, data: Bytes, _validate: bool) -> Result<Self::CallOutput> {
Ok(data)
}

#[inline]
fn as_debug_field(&self) -> impl std::fmt::Debug {
format_args!("()")
}
}
3 changes: 3 additions & 0 deletions crates/contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#[cfg(any(test, feature = "multicall"))]
extern crate self as alloy_contract;

mod eth_call;
pub use eth_call::{CallDecoder, EthCall};

mod error;
pub use error::*;

Expand Down
4 changes: 3 additions & 1 deletion crates/provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ mod heart;
pub use heart::{PendingTransaction, PendingTransactionBuilder, PendingTransactionConfig};

mod provider;
pub use provider::{FilterPollerBuilder, Provider, RootProvider, SendableTx, WalletProvider};
pub use provider::{
EthCall, FilterPollerBuilder, Provider, RootProvider, SendableTx, WalletProvider,
};

pub mod utils;

Expand Down
Loading

0 comments on commit 39db466

Please sign in to comment.