trident-sdk 0.2.0

End-user SDK for Trident deposits and cross-chain spends
Documentation
use alloy_primitives::{Address, U256};
use alloy_provider::network::AnyNetwork;
use alloy_provider::{DynProvider, PendingTransactionBuilder};
use alloy_sol_types::SolCall;
use std::marker::PhantomData;
use trident_evm_aa::{MulticallBuilder, MulticallError};

use crate::evm::abi::TridentDelivery;
use crate::evm::abi::TridentDelivery::tridentSpendCall;
use crate::proto::v1;

/// Raw pieces needed to execute the `tridentSpend` call against a Trident
/// delivery contract.
///
/// Returned by [`SpendAttestation::into_call`] for callers that want to route
/// the spend through their own transaction strategy — for example, composing
/// it into a [`MulticallBuilder`] with custom ordering.
#[derive(Debug)]
pub struct EvmCallContext {
    /// Trident delivery contract the `tridentSpend` call targets.
    pub delivery_contract: Address,
    /// Prepared `SolCall` carrying the ABI-encoded attestation and signature.
    pub trident_spend_call: tridentSpendCall,
    /// Live provider attached to the attestation when it was materialized.
    pub provider: DynProvider<AnyNetwork>,
    /// Address of the 7702-delegated EOA signer for this chain. Use as the
    /// `tx_signer` argument to [`MulticallBuilder::send`].
    pub signer_address: Address,
}

/// Chain-specific attestation data and execution context.
#[derive(Debug)]
pub(crate) enum SpendAttestationChainContext {
    Evm {
        provider: DynProvider<AnyNetwork>,
        delivery_contract: Address,
        /// Address of the signer registered for this chain. Carried so the
        /// 7702 self-call in [`SpendMulticall::send`] knows the EOA to target
        /// without needing a `WalletProvider`-bound provider.
        signer_address: Address,
    },
}

/// A spend attestation ready to be executed on-chain.
///
/// Obtained from [`TridentClient::spend`](crate::TridentClient::spend). Call
/// [`send`](Self::send) to submit the transaction; await `.get_receipt()` on the
/// returned builder to block until inclusion.
#[derive(Debug)]
pub struct SpendAttestation<T> {
    inner: SpendAttestationChainContext,
    attestation: v1::SpendAttestation,
    spend_spec_hash: [u8; 32],
    _chain_type: PhantomData<T>,
}

impl<T> SpendAttestation<T> {
    pub(crate) fn new(
        ctx: SpendAttestationChainContext,
        attestation: v1::SpendAttestation,
        spend_spec_hash: [u8; 32],
    ) -> Self {
        Self {
            inner: ctx,
            attestation,
            spend_spec_hash,
            _chain_type: PhantomData,
        }
    }
    /// Returns the canonical spend spec hash that identifies this attestation.
    pub fn spend_spec_hash(&self) -> [u8; 32] {
        self.spend_spec_hash
    }
}

impl SpendAttestation<Address> {
    /// Submits the attestation on-chain as a standalone `tridentSpend`
    /// transaction.
    ///
    /// Use [`chain`](Self::chain) or [`into_call`](Self::into_call)
    /// to batch the spend with other calls in a single 7702-delegated
    /// transaction instead.
    pub async fn send(
        self,
    ) -> Result<PendingTransactionBuilder<AnyNetwork>, alloy_contract::Error> {
        let SpendAttestationChainContext::Evm {
            provider,
            delivery_contract,
            ..
        } = self.inner;
        let delivery = TridentDelivery::new(delivery_contract, &provider);
        delivery
            .tridentSpend(
                self.attestation.bytes.into(),
                self.attestation.signature.into(),
            )
            .send()
            .await
    }

    /// Consumes the attestation and returns the raw pieces needed to execute
    /// `tridentSpend`: the delivery-contract address, the prepared `SolCall`,
    /// and the live provider.
    ///
    /// Use this when you want custom ordering against [`MulticallBuilder`] —
    /// e.g. pre-spend calls, interleaving, or calling `aggregate3_calldata()`
    /// yourself.
    pub fn into_call(self) -> EvmCallContext {
        let SpendAttestationChainContext::Evm {
            provider,
            delivery_contract,
            signer_address,
        } = self.inner;
        EvmCallContext {
            delivery_contract,
            trident_spend_call: tridentSpendCall {
                attestation: self.attestation.bytes.into(),
                signature: self.attestation.signature.into(),
            },
            provider,
            signer_address,
        }
    }

    /// Returns a [`SpendMulticall`] whose first entry is this attestation's
    /// `tridentSpend` call and whose second entry is `call` against
    /// `contract` (zero value, run after the spend).
    ///
    /// The returned wrapper carries this attestation's provider so the caller
    /// can just `.send().await` without rethreading one.
    ///
    /// Convenience for "spend + one follow-up" flows. For pre-spend calls or
    /// any other ordering, use [`into_call`](Self::into_call) and compose a
    /// [`MulticallBuilder`] yourself.
    pub fn chain<C: SolCall>(self, contract: Address, call: &C) -> SpendMulticall {
        let ctx = self.into_call();
        let builder = MulticallBuilder::new()
            .call(ctx.delivery_contract, &ctx.trident_spend_call)
            .call(contract, call);
        SpendMulticall {
            builder,
            provider: ctx.provider,
            signer_address: ctx.signer_address,
        }
    }

    /// Like [`chain`](Self::chain) but forwards `value` wei to `contract` on
    /// the follow-up call.
    pub fn chain_value<C: SolCall>(
        self,
        contract: Address,
        call: &C,
        value: U256,
    ) -> SpendMulticall {
        let ctx = self.into_call();
        let builder = MulticallBuilder::new()
            .call(ctx.delivery_contract, &ctx.trident_spend_call)
            .call_with_value(contract, call, value);
        SpendMulticall {
            builder,
            provider: ctx.provider,
            signer_address: ctx.signer_address,
        }
    }
}

/// A [`MulticallBuilder`] seeded with a Trident spend, carrying the
/// attestation's provider so `.send()` doesn't need one passed in.
///
/// Built by [`SpendAttestation::chain`] and
/// [`SpendAttestation::chain_value`]. Forwards the common
/// `MulticallBuilder` push methods so additional calls can be appended with
/// normal builder ergonomics.
pub struct SpendMulticall {
    builder: MulticallBuilder,
    provider: DynProvider<AnyNetwork>,
    signer_address: Address,
}

impl SpendMulticall {
    /// Appends a zero-value contract call to the batch.
    #[must_use]
    pub fn call<C: SolCall>(self, contract: Address, call: &C) -> Self {
        Self {
            builder: self.builder.call(contract, call),
            ..self
        }
    }

    /// Appends a value-bearing contract call to the batch.
    #[must_use]
    pub fn call_with_value<C: SolCall>(self, contract: Address, call: &C, value: U256) -> Self {
        Self {
            builder: self.builder.call_with_value(contract, call, value),
            ..self
        }
    }

    /// Appends a pure ETH transfer to the batch.
    #[must_use]
    pub fn send_value(self, to: Address, value: U256) -> Self {
        Self {
            builder: self.builder.send_value(to, value),
            ..self
        }
    }

    /// Submits the batch using the provider and 7702-delegated signer address
    /// carried since construction.
    pub async fn send(self) -> Result<PendingTransactionBuilder<AnyNetwork>, MulticallError> {
        self.builder.send(self.provider, self.signer_address).await
    }

    /// Drops the wrapper and returns the underlying [`MulticallBuilder`], the
    /// provider, and the 7702-delegated signer address — for callers that
    /// need the raw pieces.
    pub fn into_parts(self) -> (MulticallBuilder, DynProvider<AnyNetwork>, Address) {
        (self.builder, self.provider, self.signer_address)
    }
}