alloy_provider/provider/multicall/
inner_types.rs

1use std::{fmt::Debug, marker::PhantomData};
2
3use super::{
4    bindings::IMulticall3::{Call, Call3, Call3Value},
5    CallTuple,
6};
7use alloy_primitives::{Address, Bytes, U256};
8use alloy_sol_types::SolCall;
9use thiserror::Error;
10
11/// Result type for multicall operations.
12pub type Result<T, E = MulticallError> = core::result::Result<T, E>;
13
14/// A struct representing a failure in a multicall
15#[derive(Debug, Clone, PartialEq, Eq, Error)]
16#[error("Call failed at index {idx} with return data: {return_data:?}")]
17pub struct Failure {
18    /// The index-position of the call that failed
19    pub idx: usize,
20    /// The return data of the call that failed
21    pub return_data: Bytes,
22}
23
24/// A trait that is to be implemented by a type that can be distilled to a singular contract call
25/// item.
26pub trait MulticallItem {
27    /// Decoder for the return data of the call.
28    type Decoder: SolCall;
29
30    /// Returns the value to send with the call.
31    fn value(&self) -> U256;
32
33    /// The target address of the call.
34    fn target(&self) -> Address;
35    /// ABI-encoded input data for the call.
36    fn input(&self) -> Bytes;
37
38    /// Converts `self` to a [`CallItem`] while specifying whether it can fail.
39    fn into_call(self, allow_failure: bool) -> CallItem<Self::Decoder>
40    where
41        Self: Sized,
42    {
43        CallItem::<Self::Decoder>::from(self).allow_failure(allow_failure)
44    }
45}
46
47/// Helper type to build a [`CallItem`]
48#[derive(Debug)]
49pub struct CallItemBuilder;
50
51impl CallItemBuilder {
52    /// Create a new [`CallItem`] instance.
53    #[expect(clippy::new_ret_no_self)]
54    pub fn new<Item: MulticallItem>(item: Item) -> CallItem<Item::Decoder> {
55        CallItem::new(item.target(), item.input())
56    }
57}
58
59/// A singular call type that is mapped into aggregate, aggregate3, aggregate3Value call structs via
60/// the [`CallInfoTrait`] trait.
61#[derive(Clone)]
62pub struct CallItem<D: SolCall> {
63    target: Address,
64    input: Bytes,
65    allow_failure: bool,
66    value: U256,
67    decoder: PhantomData<D>,
68}
69
70impl<D: SolCall> Debug for CallItem<D> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("CallItem")
73            .field("target", &self.target)
74            .field("allow_failure", &self.allow_failure)
75            .field("value", &self.value)
76            .field("input", &self.input)
77            .finish()
78    }
79}
80
81impl<D: SolCall> CallItem<D> {
82    /// Create a new [`CallItem`] instance.
83    pub const fn new(target: Address, input: Bytes) -> Self {
84        Self { target, input, allow_failure: false, value: U256::ZERO, decoder: PhantomData }
85    }
86
87    /// Set whether the call should be allowed to fail or not.
88    pub const fn allow_failure(mut self, allow_failure: bool) -> Self {
89        self.allow_failure = allow_failure;
90        self
91    }
92
93    /// Convenience function for `allow_failure(true)`
94    pub const fn with_failure_allowed(self) -> Self {
95        self.allow_failure(true)
96    }
97
98    /// Set the value to send with the call.
99    pub const fn value(mut self, value: U256) -> Self {
100        self.value = value;
101        self
102    }
103}
104impl<D: SolCall> CallInfoTrait for CallItem<D> {
105    fn to_call(&self) -> Call {
106        Call { target: self.target, callData: self.input.clone() }
107    }
108
109    fn to_call3(&self) -> Call3 {
110        Call3 {
111            target: self.target,
112            allowFailure: self.allow_failure,
113            callData: self.input.clone(),
114        }
115    }
116
117    fn to_call3_value(&self) -> Call3Value {
118        Call3Value {
119            target: self.target,
120            allowFailure: self.allow_failure,
121            callData: self.input.clone(),
122            value: self.value,
123        }
124    }
125}
126/// A trait for converting CallItem into relevant call types.
127pub trait CallInfoTrait: std::fmt::Debug {
128    /// Converts the [`CallItem`] into a [`Call`] struct for `aggregateCall`
129    fn to_call(&self) -> Call;
130    /// Converts the [`CallItem`] into a [`Call3`] struct for `aggregate3Call`
131    fn to_call3(&self) -> Call3;
132    /// Converts the [`CallItem`] into a [`Call3Value`] struct for `aggregate3Call`
133    fn to_call3_value(&self) -> Call3Value;
134}
135
136impl<T, D> From<T> for CallItem<D>
137where
138    T: MulticallItem,
139    D: SolCall,
140{
141    /// Converts a [`MulticallItem`] into a [`CallItem`]
142    ///
143    /// By default, it doesn't allow for failure when used in
144    /// [`aggregate3`][crate::MulticallBuilder::aggregate3].
145    /// Call [`allow_failure`][CallItem::allow_failure] on the result to specify the failure
146    /// behavior, or use [`into_call`][MulticallItem::into_call] instead.
147    fn from(value: T) -> Self {
148        Self::new(value.target(), value.input()).value(value.value())
149    }
150}
151
152/// Marker for Dynamic Calls i.e where in SolCall type is locked to one specific type and multicall
153/// returns a Vec of the corresponding return type instead of a tuple.
154#[derive(Debug)]
155pub struct Dynamic<D: SolCall>(PhantomData<fn(D) -> D>);
156
157impl<D: SolCall> CallTuple for Dynamic<D> {
158    type Returns = Vec<Result<D::Return, Failure>>;
159    type SuccessReturns = Vec<D::Return>;
160
161    fn decode_returns(data: &[Bytes]) -> Result<Self::SuccessReturns> {
162        data.iter().map(|d| D::abi_decode_returns(d).map_err(MulticallError::DecodeError)).collect()
163    }
164
165    fn decode_return_results(
166        results: &[super::bindings::IMulticall3::Result],
167    ) -> Result<Self::Returns> {
168        let mut ret = vec![];
169        for (idx, res) in results.iter().enumerate() {
170            if res.success {
171                ret.push(
172                    D::abi_decode_returns(&res.returnData)
173                        .map_err(|_| Failure { idx, return_data: res.returnData.clone() }),
174                )
175            } else {
176                ret.push(Err(Failure { idx, return_data: res.returnData.clone() }));
177            }
178        }
179
180        Ok(ret)
181    }
182
183    fn try_into_success(results: Self::Returns) -> Result<Self::SuccessReturns> {
184        let mut ret = vec![];
185        for res in results {
186            ret.push(res.map_err(|e| MulticallError::CallFailed(e.return_data))?);
187        }
188        Ok(ret)
189    }
190}
191
192/// Multicall errors.
193#[derive(Debug, Error)]
194pub enum MulticallError {
195    /// Encountered when an `aggregate/aggregate3` batch contains a transaction with a value.
196    #[error("batch contains a tx with a value, try using .send() instead")]
197    ValueTx,
198    /// Error decoding return data.
199    #[error("could not decode")]
200    DecodeError(alloy_sol_types::Error),
201    /// No return data was found.
202    #[error("no return data")]
203    NoReturnData,
204    /// Call failed.
205    #[error("call failed when success was assured, this occurs when try_into_success is called on a failed call")]
206    CallFailed(Bytes),
207    /// Encountered when a transport error occurs while calling a multicall batch.
208    #[error("Transport error: {0}")]
209    TransportError(#[from] alloy_transport::TransportError),
210}