wp-evm-multicall 0.1.9

Thin async wrapper over canonical Multicall3 for dynamic heterogeneous batch reads
Documentation
#![warn(missing_docs)]

//! wp-evm-multicall — thin async wrapper over the canonical Multicall3
//! contract for dynamic, heterogeneous batch reads.
//!
//! `alloy::providers::MulticallBuilder` does not fit the CLI use case:
//! - The static (tuple-generic) builder requires call count + types
//!   known at compile time — can't loop with `.add(...)` over a
//!   runtime-sized `Vec<Address>`.
//! - The dynamic builder restricts all calls to a single decoder type
//!   (homogeneous), so it can't mix e.g. `slot0` + `liquidity` +
//!   `symbol` across pool and ERC20 contracts.
//!
//! This crate exposes a content-agnostic `aggregate3(provider,
//! multicall, calls)` that ships a `Vec<Call3>` and returns
//! `Vec<Result>` raw. Callers encode each calldata via the relevant
//! interfaces crate (e.g. `wp-evm-v3-interfaces`) and decode each
//! result with the matching `*Call::abi_decode_returns(returnData)`.
//!
//! Source: <https://github.com/mds1/multicall>

use alloy_primitives::{address, Address};
use alloy_provider::{network::Ethereum, Provider};
use alloy_sol_types::sol;
use anyhow::Result;

sol! {
    #[sol(rpc)]
    interface IMulticall3 {
        /// Single call within an `aggregate3` batch.
        #[derive(Debug, PartialEq, Eq)]
        struct Call3 {
            address target;
            bool allowFailure;
            bytes callData;
        }

        /// Per-call result. `success == false` indicates the inner
        /// call reverted (only possible when `Call3.allowFailure == true`;
        /// otherwise the whole multicall reverts).
        #[derive(Debug, PartialEq, Eq)]
        struct Result {
            bool success;
            bytes returnData;
        }

        function aggregate3(Call3[] calldata calls)
            external payable returns (Result[] memory returnData);

        function getBlockNumber() external view returns (uint256 blockNumber);
    }
}

/// Canonical Multicall3 deployment — same address on all major EVM
/// chains via deterministic CREATE2.
///
/// Source: <https://github.com/mds1/multicall>
pub const MULTICALL3_ADDRESS: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");

/// Send a heterogeneous batch of pre-encoded calls in a single
/// Multicall3 RPC. `output[i]` corresponds to `calls[i]`.
///
/// Per-call failures (when `Call3.allowFailure == true`) surface as
/// `IMulticall3::Result { success: false, returnData: <revert data> }`;
/// the outer `Result` only fails for transport/RPC errors.
///
/// Callers encode each calldata via the relevant `*Call::abi_encode()`
/// from the matching interfaces crate and decode each result via
/// `*Call::abi_decode_returns(&result.returnData)`.
///
/// `calls` is taken by value (not `&[Call3]`) so callers can transfer
/// freshly-built calldata bytes into the wrapper without an extra
/// clone. No internal chunking — `calls` ships as a single Multicall3
/// RPC, so very large batches may exceed the provider's `request_size`
/// budget; chunk caller-side when that bites.
pub async fn aggregate3<P: Provider<Ethereum>>(
    provider: &P,
    multicall: Address,
    calls: Vec<IMulticall3::Call3>,
) -> Result<Vec<IMulticall3::Result>> {
    let contract = IMulticall3::new(multicall, provider);
    let returns = contract.aggregate3(calls).call().await?;
    Ok(returns)
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_sol_types::SolCall;

    /// Lock the canonical Multicall3 address against the well-known
    /// CREATE2 deployment.
    #[test]
    fn canonical_multicall3_address() {
        assert_eq!(MULTICALL3_ADDRESS, address!("cA11bde05977b3631167028862bE2a173976CA11"),);
    }

    /// Lock the `aggregate3((address,bool,bytes)[])` selector against
    /// the known on-chain value. Any signature regression flips it.
    #[test]
    fn aggregate3_selector_matches_known_value() {
        // Computed from keccak256("aggregate3((address,bool,bytes)[])")[:4]
        assert_eq!(IMulticall3::aggregate3Call::SELECTOR, [0x82, 0xad, 0x56, 0xcb],);
    }

    /// Lock the `getBlockNumber()` selector against Multicall3's known on-chain value.
    #[test]
    fn get_block_number_selector_matches_known_value() {
        // Computed from keccak256("getBlockNumber()")[:4]
        assert_eq!(IMulticall3::getBlockNumberCall::SELECTOR, [0x42, 0xcb, 0xb1, 0x5c],);
    }
}