trident_sdk/spend_attestation.rs
1use alloy_primitives::{Address, U256};
2use alloy_provider::network::AnyNetwork;
3use alloy_provider::{DynProvider, PendingTransactionBuilder};
4use alloy_sol_types::SolCall;
5use std::marker::PhantomData;
6use trident_evm_aa::{MulticallBuilder, MulticallError};
7
8use crate::evm::abi::TridentDelivery;
9use crate::evm::abi::TridentDelivery::tridentSpendCall;
10use crate::proto::v1;
11
12/// Raw pieces needed to execute the `tridentSpend` call against a Trident
13/// delivery contract.
14///
15/// Returned by [`SpendAttestation::into_call`] for callers that want to route
16/// the spend through their own transaction strategy — for example, composing
17/// it into a [`MulticallBuilder`] with custom ordering.
18#[derive(Debug)]
19pub struct EvmCallContext {
20 /// Trident delivery contract the `tridentSpend` call targets.
21 pub delivery_contract: Address,
22 /// Prepared `SolCall` carrying the ABI-encoded attestation and signature.
23 pub trident_spend_call: tridentSpendCall,
24 /// Live provider attached to the attestation when it was materialized.
25 pub provider: DynProvider<AnyNetwork>,
26 /// Address of the 7702-delegated EOA signer for this chain. Use as the
27 /// `tx_signer` argument to [`MulticallBuilder::send`].
28 pub signer_address: Address,
29}
30
31/// Chain-specific attestation data and execution context.
32#[derive(Debug)]
33pub(crate) enum SpendAttestationChainContext {
34 Evm {
35 provider: DynProvider<AnyNetwork>,
36 delivery_contract: Address,
37 /// Address of the signer registered for this chain. Carried so the
38 /// 7702 self-call in [`SpendMulticall::send`] knows the EOA to target
39 /// without needing a `WalletProvider`-bound provider.
40 signer_address: Address,
41 },
42}
43
44/// A spend attestation ready to be executed on-chain.
45///
46/// Obtained from [`TridentClient::spend`](crate::TridentClient::spend). Call
47/// [`send`](Self::send) to submit the transaction; await `.get_receipt()` on the
48/// returned builder to block until inclusion.
49#[derive(Debug)]
50pub struct SpendAttestation<T> {
51 inner: SpendAttestationChainContext,
52 attestation: v1::SpendAttestation,
53 spend_spec_hash: [u8; 32],
54 _chain_type: PhantomData<T>,
55}
56
57impl<T> SpendAttestation<T> {
58 pub(crate) fn new(
59 ctx: SpendAttestationChainContext,
60 attestation: v1::SpendAttestation,
61 spend_spec_hash: [u8; 32],
62 ) -> Self {
63 Self {
64 inner: ctx,
65 attestation,
66 spend_spec_hash,
67 _chain_type: PhantomData,
68 }
69 }
70 /// Returns the canonical spend spec hash that identifies this attestation.
71 pub fn spend_spec_hash(&self) -> [u8; 32] {
72 self.spend_spec_hash
73 }
74}
75
76impl SpendAttestation<Address> {
77 /// Submits the attestation on-chain as a standalone `tridentSpend`
78 /// transaction.
79 ///
80 /// Use [`chain`](Self::chain) or [`into_call`](Self::into_call)
81 /// to batch the spend with other calls in a single 7702-delegated
82 /// transaction instead.
83 pub async fn send(
84 self,
85 ) -> Result<PendingTransactionBuilder<AnyNetwork>, alloy_contract::Error> {
86 let SpendAttestationChainContext::Evm {
87 provider,
88 delivery_contract,
89 ..
90 } = self.inner;
91 let delivery = TridentDelivery::new(delivery_contract, &provider);
92 delivery
93 .tridentSpend(
94 self.attestation.bytes.into(),
95 self.attestation.signature.into(),
96 )
97 .send()
98 .await
99 }
100
101 /// Consumes the attestation and returns the raw pieces needed to execute
102 /// `tridentSpend`: the delivery-contract address, the prepared `SolCall`,
103 /// and the live provider.
104 ///
105 /// Use this when you want custom ordering against [`MulticallBuilder`] —
106 /// e.g. pre-spend calls, interleaving, or calling `aggregate3_calldata()`
107 /// yourself.
108 pub fn into_call(self) -> EvmCallContext {
109 let SpendAttestationChainContext::Evm {
110 provider,
111 delivery_contract,
112 signer_address,
113 } = self.inner;
114 EvmCallContext {
115 delivery_contract,
116 trident_spend_call: tridentSpendCall {
117 attestation: self.attestation.bytes.into(),
118 signature: self.attestation.signature.into(),
119 },
120 provider,
121 signer_address,
122 }
123 }
124
125 /// Returns a [`SpendMulticall`] whose first entry is this attestation's
126 /// `tridentSpend` call and whose second entry is `call` against
127 /// `contract` (zero value, run after the spend).
128 ///
129 /// The returned wrapper carries this attestation's provider so the caller
130 /// can just `.send().await` without rethreading one.
131 ///
132 /// Convenience for "spend + one follow-up" flows. For pre-spend calls or
133 /// any other ordering, use [`into_call`](Self::into_call) and compose a
134 /// [`MulticallBuilder`] yourself.
135 pub fn chain<C: SolCall>(self, contract: Address, call: &C) -> SpendMulticall {
136 let ctx = self.into_call();
137 let builder = MulticallBuilder::new()
138 .call(ctx.delivery_contract, &ctx.trident_spend_call)
139 .call(contract, call);
140 SpendMulticall {
141 builder,
142 provider: ctx.provider,
143 signer_address: ctx.signer_address,
144 }
145 }
146
147 /// Like [`chain`](Self::chain) but forwards `value` wei to `contract` on
148 /// the follow-up call.
149 pub fn chain_value<C: SolCall>(
150 self,
151 contract: Address,
152 call: &C,
153 value: U256,
154 ) -> SpendMulticall {
155 let ctx = self.into_call();
156 let builder = MulticallBuilder::new()
157 .call(ctx.delivery_contract, &ctx.trident_spend_call)
158 .call_with_value(contract, call, value);
159 SpendMulticall {
160 builder,
161 provider: ctx.provider,
162 signer_address: ctx.signer_address,
163 }
164 }
165}
166
167/// A [`MulticallBuilder`] seeded with a Trident spend, carrying the
168/// attestation's provider so `.send()` doesn't need one passed in.
169///
170/// Built by [`SpendAttestation::chain`] and
171/// [`SpendAttestation::chain_value`]. Forwards the common
172/// `MulticallBuilder` push methods so additional calls can be appended with
173/// normal builder ergonomics.
174pub struct SpendMulticall {
175 builder: MulticallBuilder,
176 provider: DynProvider<AnyNetwork>,
177 signer_address: Address,
178}
179
180impl SpendMulticall {
181 /// Appends a zero-value contract call to the batch.
182 #[must_use]
183 pub fn call<C: SolCall>(self, contract: Address, call: &C) -> Self {
184 Self {
185 builder: self.builder.call(contract, call),
186 ..self
187 }
188 }
189
190 /// Appends a value-bearing contract call to the batch.
191 #[must_use]
192 pub fn call_with_value<C: SolCall>(self, contract: Address, call: &C, value: U256) -> Self {
193 Self {
194 builder: self.builder.call_with_value(contract, call, value),
195 ..self
196 }
197 }
198
199 /// Appends a pure ETH transfer to the batch.
200 #[must_use]
201 pub fn send_value(self, to: Address, value: U256) -> Self {
202 Self {
203 builder: self.builder.send_value(to, value),
204 ..self
205 }
206 }
207
208 /// Submits the batch using the provider and 7702-delegated signer address
209 /// carried since construction.
210 pub async fn send(self) -> Result<PendingTransactionBuilder<AnyNetwork>, MulticallError> {
211 self.builder.send(self.provider, self.signer_address).await
212 }
213
214 /// Drops the wrapper and returns the underlying [`MulticallBuilder`], the
215 /// provider, and the 7702-delegated signer address — for callers that
216 /// need the raw pieces.
217 pub fn into_parts(self) -> (MulticallBuilder, DynProvider<AnyNetwork>, Address) {
218 (self.builder, self.provider, self.signer_address)
219 }
220}