use alloy_network::{Network, TransactionBuilder};
use alloy_primitives::{Address, Bytes, U256, address};
use alloy_provider::{PendingTransactionBuilder, Provider, WalletProvider};
use alloy_sol_types::SolCall;
use crate::abi::{Multicall3, TridentAccount};
pub const MULTICALL3: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");
#[derive(Debug, thiserror::Error)]
pub enum MulticallError {
#[error("rpc: {0}")]
Rpc(Box<dyn std::error::Error + Send + Sync>),
}
#[derive(Default, Clone)]
pub struct MulticallBuilder {
calls: Vec<Multicall3::Call3Value>,
multicall3: Address,
}
impl MulticallBuilder {
pub fn new() -> Self {
Self {
calls: Vec::new(),
multicall3: MULTICALL3,
}
}
pub fn with_multicall3(mut self, addr: Address) -> Self {
self.multicall3 = addr;
self
}
pub fn call<C: SolCall>(mut self, contract: Address, call: &C) -> Self {
self.calls.push(Multicall3::Call3Value {
target: contract,
allowFailure: false,
value: U256::ZERO,
callData: call.abi_encode().into(),
});
self
}
pub fn call_with_value<C: SolCall>(mut self, contract: Address, call: &C, value: U256) -> Self {
self.calls.push(Multicall3::Call3Value {
target: contract,
allowFailure: false,
value,
callData: call.abi_encode().into(),
});
self
}
pub fn send_value(mut self, to: Address, value: U256) -> Self {
self.calls.push(Multicall3::Call3Value {
target: to,
allowFailure: false,
value,
callData: Bytes::new(),
});
self
}
pub fn total_value(&self) -> U256 {
self.calls.iter().map(|c| c.value).sum()
}
pub fn aggregate3_calldata(&self) -> Bytes {
Multicall3::aggregate3ValueCall {
calls: self.calls.clone(),
}
.abi_encode()
.into()
}
pub fn execute_call_calldata(&self) -> Bytes {
let inner = self.aggregate3_calldata();
TridentAccount::executeCall {
target: self.multicall3,
data: inner,
}
.abi_encode()
.into()
}
pub async fn send_with_wallet_provider<P, N>(
self,
provider: P,
) -> Result<PendingTransactionBuilder<N>, MulticallError>
where
P: Provider<N> + WalletProvider<N>,
N: Network,
{
let tx_signer = provider.default_signer_address();
self.send(provider, tx_signer).await
}
pub async fn send<P, N>(
self,
provider: P,
tx_signer: Address,
) -> Result<PendingTransactionBuilder<N>, MulticallError>
where
P: Provider<N>,
N: Network,
{
let tx = self.build_transaction_request::<N>(tx_signer);
provider
.send_transaction(tx)
.await
.map_err(|e| MulticallError::Rpc(Box::new(e)))
}
pub fn build_transaction_request<N: Network>(
self,
tx_signer: Address,
) -> <N as Network>::TransactionRequest {
let total = self.total_value();
let input = self.execute_call_calldata();
<N::TransactionRequest as Default>::default()
.with_to(tx_signer)
.with_value(total)
.with_input(input)
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::address;
use super::*;
use crate::{
deployer,
eip7702_installer::SelfInstaller,
test_util::{IWETH9, TestSetup, WETH9_ADDRESS},
};
#[test]
fn aggregate3_calldata_starts_with_correct_selector() {
let calldata = MulticallBuilder::new()
.send_value(Address::ZERO, U256::from(1))
.aggregate3_calldata();
assert_eq!(&calldata[..4], &Multicall3::aggregate3ValueCall::SELECTOR);
}
#[test]
fn total_value_sums_per_call_values() {
let builder = MulticallBuilder::new()
.call(Address::ZERO, &IWETH9::depositCall {})
.call_with_value(Address::ZERO, &IWETH9::depositCall {}, U256::from(5))
.send_value(Address::ZERO, U256::from(7));
assert_eq!(builder.total_value(), U256::from(12));
}
#[tokio::test]
async fn send_executes_multi_step_batch_on_delegated_eoa() {
let setup = TestSetup::new().await;
let provider = setup.provider();
let eoa = setup.signer.address();
let trident_addr = deployer::deploy(provider.clone())
.await
.expect("deploy trident");
SelfInstaller::new()
.with_target(trident_addr)
.install(provider.clone(), &setup.signer)
.await
.expect("install delegation")
.get_receipt()
.await
.expect("install confirmed");
let bob = address!("00000000000000000000000000000000000000bb");
let carol = address!("00000000000000000000000000000000000000cc");
let deposit_amount = U256::from(500_000_000_000_000_000u128); let transfer_amount = U256::from(100_000_000_000_000_000u128); let send_amount = U256::from(300_000_000_000_000_000u128);
let eoa_balance_before = provider.get_balance(eoa).await.expect("balance");
let carol_balance_before = provider.get_balance(carol).await.expect("balance");
MulticallBuilder::new()
.call_with_value(WETH9_ADDRESS, &IWETH9::depositCall {}, deposit_amount)
.call(
WETH9_ADDRESS,
&IWETH9::transferCall {
to: bob,
amount: transfer_amount,
},
)
.send_value(carol, send_amount)
.send_with_wallet_provider(provider.clone())
.await
.expect("multicall send")
.get_receipt()
.await
.expect("multicall confirmed");
let weth = IWETH9::new(WETH9_ADDRESS, provider.clone());
let eoa_weth = weth
.balanceOf(eoa)
.call()
.await
.expect("weth balanceOf eoa");
let bob_weth = weth
.balanceOf(bob)
.call()
.await
.expect("weth balanceOf bob");
assert_eq!(eoa_weth, deposit_amount - transfer_amount);
assert_eq!(bob_weth, transfer_amount);
let carol_balance_after = provider.get_balance(carol).await.expect("balance");
assert_eq!(
carol_balance_after - carol_balance_before,
send_amount,
"carol should have received exactly send_amount",
);
let eoa_balance_after = provider.get_balance(eoa).await.expect("balance");
let eoa_spent = eoa_balance_before - eoa_balance_after;
let expected = deposit_amount + send_amount;
let gas_ceiling = U256::from(100_000_000_000_000_000u128); assert!(
eoa_spent >= expected,
"eoa spent ({eoa_spent}) should be at least deposit+send ({expected})"
);
assert!(
eoa_spent <= expected + gas_ceiling,
"eoa spent ({eoa_spent}) exceeds deposit+send+gas ceiling — \
looks like a double-charge, self-send should be net zero"
);
}
}