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    /// The target address of the call.
31    fn target(&self) -> Address;
32    /// ABI-encoded input data for the call.
33    fn input(&self) -> Bytes;
34}
35
36/// Helper type to build a [`CallItem`]
37#[derive(Debug)]
38pub struct CallItemBuilder;
39
40impl CallItemBuilder {
41    /// Create a new [`CallItem`] instance.
42    #[expect(clippy::new_ret_no_self)]
43    pub fn new<Item: MulticallItem>(item: Item) -> CallItem<Item::Decoder> {
44        CallItem::new(item.target(), item.input())
45    }
46}
47
48/// A singular call type that is mapped into aggregate, aggregate3, aggregate3Value call structs via
49/// the [`CallInfoTrait`] trait.
50#[derive(Clone)]
51pub struct CallItem<D: SolCall> {
52    target: Address,
53    input: Bytes,
54    allow_failure: bool,
55    value: U256,
56    decoder: PhantomData<D>,
57}
58
59impl<D: SolCall> Debug for CallItem<D> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("CallItem")
62            .field("target", &self.target)
63            .field("allow_failure", &self.allow_failure)
64            .field("value", &self.value)
65            .field("input", &self.input)
66            .finish()
67    }
68}
69
70impl<D: SolCall> CallItem<D> {
71    /// Create a new [`CallItem`] instance.
72    pub const fn new(target: Address, input: Bytes) -> Self {
73        Self { target, input, allow_failure: false, value: U256::ZERO, decoder: PhantomData }
74    }
75
76    /// Set whether the call should be allowed to fail or not.
77    pub const fn allow_failure(mut self, allow_failure: bool) -> Self {
78        self.allow_failure = allow_failure;
79        self
80    }
81
82    /// Set the value to send with the call.
83    pub const fn value(mut self, value: U256) -> Self {
84        self.value = value;
85        self
86    }
87}
88impl<D: SolCall> CallInfoTrait for CallItem<D> {
89    fn to_call(&self) -> Call {
90        Call { target: self.target, callData: self.input.clone() }
91    }
92
93    fn to_call3(&self) -> Call3 {
94        Call3 {
95            target: self.target,
96            allowFailure: self.allow_failure,
97            callData: self.input.clone(),
98        }
99    }
100
101    fn to_call3_value(&self) -> Call3Value {
102        Call3Value {
103            target: self.target,
104            allowFailure: self.allow_failure,
105            callData: self.input.clone(),
106            value: self.value,
107        }
108    }
109}
110/// A trait for converting CallItem into relevant call types.
111pub trait CallInfoTrait: std::fmt::Debug {
112    /// Converts the [`CallItem`] into a [`Call`] struct for `aggregateCall`
113    fn to_call(&self) -> Call;
114    /// Converts the [`CallItem`] into a [`Call3`] struct for `aggregate3Call`
115    fn to_call3(&self) -> Call3;
116    /// Converts the [`CallItem`] into a [`Call3Value`] struct for `aggregate3Call`
117    fn to_call3_value(&self) -> Call3Value;
118}
119
120/// Marker for Dynamic Calls i.e where in SolCall type is locked to one specific type and multicall
121/// returns a Vec of the corresponding return type instead of a tuple.
122#[derive(Debug)]
123pub struct Dynamic<D: SolCall>(PhantomData<fn(D) -> D>);
124
125impl<D: SolCall> CallTuple for Dynamic<D> {
126    type Returns = Vec<Result<D::Return, Failure>>;
127    type SuccessReturns = Vec<D::Return>;
128
129    fn decode_returns(data: &[Bytes]) -> Result<Self::SuccessReturns> {
130        data.iter().map(|d| D::abi_decode_returns(d).map_err(MulticallError::DecodeError)).collect()
131    }
132
133    fn decode_return_results(
134        results: &[super::bindings::IMulticall3::Result],
135    ) -> Result<Self::Returns> {
136        let mut ret = vec![];
137        for (idx, res) in results.iter().enumerate() {
138            if res.success {
139                ret.push(Ok(
140                    D::abi_decode_returns(&res.returnData).map_err(MulticallError::DecodeError)?
141                ));
142            } else {
143                ret.push(Err(Failure { idx, return_data: res.returnData.clone() }));
144            }
145        }
146
147        Ok(ret)
148    }
149
150    fn try_into_success(results: Self::Returns) -> Result<Self::SuccessReturns> {
151        let mut ret = vec![];
152        for res in results {
153            ret.push(res.map_err(|e| MulticallError::CallFailed(e.return_data))?);
154        }
155        Ok(ret)
156    }
157}
158
159/// Multicall errors.
160#[derive(Debug, Error)]
161pub enum MulticallError {
162    /// Encountered when an `aggregate/aggregate3` batch contains a transaction with a value.
163    #[error("batch contains a tx with a value, try using .send() instead")]
164    ValueTx,
165    /// Error decoding return data.
166    #[error("could not decode")]
167    DecodeError(alloy_sol_types::Error),
168    /// No return data was found.
169    #[error("no return data")]
170    NoReturnData,
171    /// Call failed.
172    #[error("call failed when success was assured, this occurs when try_into_success is called on a failed call")]
173    CallFailed(Bytes),
174    /// Encountered when a transport error occurs while calling a multicall batch.
175    #[error("Transport error: {0}")]
176    TransportError(#[from] alloy_transport::TransportError),
177}