Skip to main content

foundry_common/transactions/
builder.rs

1use std::num::NonZeroU64;
2
3use alloy_consensus::{
4    BlobTransactionSidecar, BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant,
5};
6use alloy_eips::{Encodable2718, eip7702::SignedAuthorization};
7use alloy_network::{AnyNetwork, Ethereum, Network, NetworkTransactionBuilder};
8use alloy_primitives::{Address, B256, Signature, TxKind, U256};
9use alloy_provider::Provider;
10use alloy_signer::Signer;
11use eyre::Result;
12#[cfg(feature = "optimism")]
13use op_alloy_network::Optimism;
14#[cfg(feature = "optimism")]
15use op_alloy_rpc_types::OpTransactionRequest;
16use tempo_alloy::{TempoNetwork, provider::TempoProviderExt};
17use tempo_primitives::{
18    TempoSignature, TempoTxType,
19    transaction::{Call, KeychainSignature, PrimitiveSignature, SignedKeyAuthorization},
20};
21
22/// Composite transaction builder trait for Foundry transactions.
23///
24/// This extends the base `TransactionBuilder` trait with the same methods as
25/// [`alloy_network::TransactionBuilder4844`] for handling blob transaction sidecars, and
26/// [`alloy_network::TransactionBuilder7702`] for handling EIP-7702 authorization lists.
27///
28/// By default, all methods have no-op implementations, so this can be implemented for any Network.
29///
30/// If the Network supports Eip4844 blob transactions implement these methods:
31/// - [`FoundryTransactionBuilder::max_fee_per_blob_gas`]
32/// - [`FoundryTransactionBuilder::set_max_fee_per_blob_gas`]
33/// - [`FoundryTransactionBuilder::blob_versioned_hashes`]
34/// - [`FoundryTransactionBuilder::set_blob_versioned_hashes`]
35/// - [`FoundryTransactionBuilder::blob_sidecar`]
36/// - [`FoundryTransactionBuilder::set_blob_sidecar`]
37///
38/// If the Network supports EIP-7702 authorization lists, implement these methods:
39/// - [`FoundryTransactionBuilder::authorization_list`]
40/// - [`FoundryTransactionBuilder::set_authorization_list`]
41///
42/// If the Network supports Tempo transactions, implement these methods:
43/// - [`FoundryTransactionBuilder::set_fee_token`]
44/// - [`FoundryTransactionBuilder::set_nonce_key`]
45/// - [`FoundryTransactionBuilder::set_key_id`]
46/// - [`FoundryTransactionBuilder::set_valid_before`]
47/// - [`FoundryTransactionBuilder::set_valid_after`]
48/// - [`FoundryTransactionBuilder::set_fee_payer_signature`]
49pub trait FoundryTransactionBuilder<N: Network>: NetworkTransactionBuilder<N> {
50    /// Reset gas limit
51    fn reset_gas_limit(&mut self);
52
53    /// Get the max fee per blob gas for the transaction.
54    fn max_fee_per_blob_gas(&self) -> Option<u128> {
55        None
56    }
57
58    /// Set the max fee per blob gas for the transaction.
59    fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {}
60
61    /// Builder-pattern method for setting max fee per blob gas.
62    fn with_max_fee_per_blob_gas(mut self, max_fee_per_blob_gas: u128) -> Self {
63        self.set_max_fee_per_blob_gas(max_fee_per_blob_gas);
64        self
65    }
66
67    /// Gets the EIP-4844 blob versioned hashes of the transaction.
68    ///
69    /// These may be set independently of the sidecar, e.g. when the sidecar
70    /// has been pruned but the hashes are still needed for `eth_call`.
71    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
72        None
73    }
74
75    /// Sets the EIP-4844 blob versioned hashes of the transaction.
76    fn set_blob_versioned_hashes(&mut self, _hashes: Vec<B256>) {}
77
78    /// Builder-pattern method for setting the EIP-4844 blob versioned hashes.
79    fn with_blob_versioned_hashes(mut self, hashes: Vec<B256>) -> Self {
80        self.set_blob_versioned_hashes(hashes);
81        self
82    }
83
84    /// Gets the blob sidecar (either EIP-4844 or EIP-7594 variant) of the transaction.
85    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
86        None
87    }
88
89    /// Sets the blob sidecar (either EIP-4844 or EIP-7594 variant) of the transaction.
90    ///
91    /// Note: This will also set the versioned blob hashes accordingly:
92    /// [BlobTransactionSidecarVariant::versioned_hashes]
93    fn set_blob_sidecar(&mut self, _sidecar: BlobTransactionSidecarVariant) {}
94
95    /// Builder-pattern method for setting the blob sidecar of the transaction.
96    fn with_blob_sidecar(mut self, sidecar: BlobTransactionSidecarVariant) -> Self {
97        self.set_blob_sidecar(sidecar);
98        self
99    }
100
101    /// Gets the EIP-4844 blob sidecar if the current sidecar is of that variant.
102    fn blob_sidecar_4844(&self) -> Option<&BlobTransactionSidecar> {
103        self.blob_sidecar().and_then(|s| s.as_eip4844())
104    }
105
106    /// Sets the EIP-4844 blob sidecar of the transaction.
107    fn set_blob_sidecar_4844(&mut self, sidecar: BlobTransactionSidecar) {
108        self.set_blob_sidecar(BlobTransactionSidecarVariant::Eip4844(sidecar));
109    }
110
111    /// Builder-pattern method for setting the EIP-4844 blob sidecar of the transaction.
112    fn with_blob_sidecar_4844(mut self, sidecar: BlobTransactionSidecar) -> Self {
113        self.set_blob_sidecar_4844(sidecar);
114        self
115    }
116
117    /// Gets the EIP-7594 blob sidecar if the current sidecar is of that variant.
118    fn blob_sidecar_7594(&self) -> Option<&BlobTransactionSidecarEip7594> {
119        self.blob_sidecar().and_then(|s| s.as_eip7594())
120    }
121
122    /// Sets the EIP-7594 blob sidecar of the transaction.
123    fn set_blob_sidecar_7594(&mut self, sidecar: BlobTransactionSidecarEip7594) {
124        self.set_blob_sidecar(BlobTransactionSidecarVariant::Eip7594(sidecar));
125    }
126
127    /// Builder-pattern method for setting the EIP-7594 blob sidecar of the transaction.
128    fn with_blob_sidecar_7594(mut self, sidecar: BlobTransactionSidecarEip7594) -> Self {
129        self.set_blob_sidecar_7594(sidecar);
130        self
131    }
132
133    /// Get the EIP-7702 authorization list for the transaction.
134    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
135        None
136    }
137
138    /// Sets the EIP-7702 authorization list.
139    fn set_authorization_list(&mut self, _authorization_list: Vec<SignedAuthorization>) {}
140
141    /// Builder-pattern method for setting the authorization list.
142    fn with_authorization_list(mut self, authorization_list: Vec<SignedAuthorization>) -> Self {
143        self.set_authorization_list(authorization_list);
144        self
145    }
146
147    /// Get the fee token for a Tempo transaction.
148    fn fee_token(&self) -> Option<Address> {
149        None
150    }
151
152    /// Gets top-level Tempo calls for local fee-token inference.
153    fn tempo_calls(&self) -> Vec<(TxKind, &[u8])> {
154        Vec::new()
155    }
156
157    /// Returns true when [`Self::tempo_calls`] came from a Tempo AA `calls` list.
158    fn has_tempo_call_list(&self) -> bool {
159        false
160    }
161
162    /// Returns true when this request will be built as a Tempo AA transaction.
163    fn is_tempo_aa(&self) -> bool {
164        false
165    }
166
167    /// Set the fee token for a Tempo transaction.
168    fn set_fee_token(&mut self, _fee_token: Address) {}
169
170    /// Builder-pattern method for setting the Tempo fee token.
171    fn with_fee_token(mut self, fee_token: Address) -> Self {
172        self.set_fee_token(fee_token);
173        self
174    }
175
176    /// Get the 2D nonce key for a Tempo transaction.
177    fn nonce_key(&self) -> Option<U256> {
178        None
179    }
180
181    /// Set the 2D nonce key for the Tempo transaction.
182    fn set_nonce_key(&mut self, _nonce_key: U256) {}
183
184    /// Builder-pattern method for setting a 2D nonce key for a Tempo transaction.
185    fn with_nonce_key(mut self, nonce_key: U256) -> Self {
186        self.set_nonce_key(nonce_key);
187        self
188    }
189
190    /// Get the access key ID for a Tempo transaction.
191    fn key_id(&self) -> Option<Address> {
192        None
193    }
194
195    /// Set the access key ID for a Tempo transaction.
196    ///
197    /// Used during gas estimation to override the key_id that would normally be
198    /// recovered from the signature.
199    fn set_key_id(&mut self, _key_id: Address) {}
200
201    /// Builder-pattern method for setting the Tempo access key ID.
202    fn with_key_id(mut self, key_id: Address) -> Self {
203        self.set_key_id(key_id);
204        self
205    }
206
207    /// Get the valid_before timestamp for a Tempo expiring nonce transaction.
208    fn valid_before(&self) -> Option<NonZeroU64> {
209        None
210    }
211
212    /// Set the valid_before timestamp for a Tempo expiring nonce transaction.
213    fn set_valid_before(&mut self, _valid_before: NonZeroU64) {}
214
215    /// Builder-pattern method for setting the valid_before timestamp.
216    fn with_valid_before(mut self, valid_before: NonZeroU64) -> Self {
217        self.set_valid_before(valid_before);
218        self
219    }
220
221    /// Get the valid_after timestamp for a Tempo expiring nonce transaction.
222    fn valid_after(&self) -> Option<NonZeroU64> {
223        None
224    }
225
226    /// Set the valid_after timestamp for a Tempo expiring nonce transaction.
227    fn set_valid_after(&mut self, _valid_after: NonZeroU64) {}
228
229    /// Builder-pattern method for setting the valid_after timestamp.
230    fn with_valid_after(mut self, valid_after: NonZeroU64) -> Self {
231        self.set_valid_after(valid_after);
232        self
233    }
234
235    /// Get the fee payer (sponsor) signature for a Tempo sponsored transaction.
236    fn fee_payer_signature(&self) -> Option<Signature> {
237        None
238    }
239
240    /// Set the fee payer (sponsor) signature for a Tempo sponsored transaction.
241    fn set_fee_payer_signature(&mut self, _signature: Signature) {}
242
243    /// Builder-pattern method for setting the fee payer signature.
244    fn with_fee_payer_signature(mut self, signature: Signature) -> Self {
245        self.set_fee_payer_signature(signature);
246        self
247    }
248
249    /// Computes the sponsor (fee payer) signature hash for this transaction.
250    ///
251    /// This builds an unsigned consensus-level transaction from the request and computes
252    /// the hash that a sponsor needs to sign. Returns `None` for networks that don't
253    /// support sponsored transactions.
254    fn compute_sponsor_hash(&self, _from: Address) -> Option<B256> {
255        None
256    }
257
258    /// Set the key authorization for a Tempo transaction.
259    ///
260    /// Embeds a [`SignedKeyAuthorization`] in the transaction body, provisioning the access key
261    /// on-chain as part of this transaction.
262    fn set_key_authorization(&mut self, _key_authorization: SignedKeyAuthorization) {}
263
264    /// Embeds key authorization before gas estimation/signing if the access key is not yet
265    /// provisioned on-chain.
266    ///
267    /// This mirrors the mutation performed by [`Self::sign_with_access_key`], but makes the final
268    /// transaction body available before fee-payer sponsor digests are computed.
269    fn prepare_access_key_authorization<'a>(
270        &'a mut self,
271        _provider: &'a impl Provider<N>,
272        _wallet_address: Address,
273        _key_address: Address,
274        _key_authorization: Option<&'a SignedKeyAuthorization>,
275    ) -> impl Future<Output = Result<()>> + Send + 'a
276    where
277        Self: Send,
278    {
279        async { Ok(()) }
280    }
281
282    /// Converts a CREATE transaction into an AA-compatible call entry.
283    ///
284    /// Tempo AA transactions use a `calls` list instead of `to`+`input`. Must be
285    /// called before gas estimation so the RPC sees the correct tx structure.
286    /// No-op for non-Tempo networks.
287    fn convert_create_to_call(&mut self) {}
288
289    /// Clears the `to` and `value` fields for batch transactions that use `calls`.
290    ///
291    /// In Tempo AA batch transactions, targets are specified in the `calls` field, not in `to`.
292    /// If `to` is set, `build_aa()` would add a spurious extra call. Must be called after
293    /// `prepare()` sets `kind`/`to` but before gas estimation.
294    /// No-op for non-Tempo networks.
295    fn clear_batch_to(&mut self) {}
296
297    /// Signs the transaction using an access key (keychain mode).
298    ///
299    /// If `key_authorization` is provided and the key is not yet provisioned on-chain,
300    /// embeds the authorization in the transaction before signing.
301    ///
302    /// The default implementation returns an error. Only `TempoNetwork` supports this.
303    fn sign_with_access_key(
304        self,
305        _provider: &impl Provider<N>,
306        _signer: &(impl Signer + Sync),
307        _wallet_address: Address,
308        _key_address: Address,
309        _key_authorization: Option<&SignedKeyAuthorization>,
310    ) -> impl Future<Output = Result<Vec<u8>>> + Send {
311        async {
312            eyre::bail!("access key signing is not supported for this network");
313        }
314    }
315}
316
317impl FoundryTransactionBuilder<Ethereum> for <Ethereum as Network>::TransactionRequest {
318    fn reset_gas_limit(&mut self) {
319        self.gas = None;
320    }
321
322    fn max_fee_per_blob_gas(&self) -> Option<u128> {
323        self.max_fee_per_blob_gas
324    }
325
326    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
327        self.max_fee_per_blob_gas = Some(max_fee_per_blob_gas);
328    }
329
330    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
331        self.blob_versioned_hashes.as_deref()
332    }
333
334    fn set_blob_versioned_hashes(&mut self, hashes: Vec<B256>) {
335        self.blob_versioned_hashes = Some(hashes);
336    }
337
338    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
339        self.sidecar.as_ref()
340    }
341
342    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
343        self.sidecar = Some(sidecar);
344        self.populate_blob_hashes();
345    }
346
347    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
348        self.authorization_list.as_ref()
349    }
350
351    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
352        self.authorization_list = Some(authorization_list);
353    }
354}
355
356impl FoundryTransactionBuilder<AnyNetwork> for <AnyNetwork as Network>::TransactionRequest {
357    fn reset_gas_limit(&mut self) {
358        self.gas = None;
359    }
360
361    fn max_fee_per_blob_gas(&self) -> Option<u128> {
362        self.max_fee_per_blob_gas
363    }
364
365    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
366        self.max_fee_per_blob_gas = Some(max_fee_per_blob_gas);
367    }
368
369    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
370        self.blob_versioned_hashes.as_deref()
371    }
372
373    fn set_blob_versioned_hashes(&mut self, hashes: Vec<B256>) {
374        self.blob_versioned_hashes = Some(hashes);
375    }
376
377    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
378        self.sidecar.as_ref()
379    }
380
381    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
382        self.sidecar = Some(sidecar);
383        self.populate_blob_hashes();
384    }
385
386    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
387        self.authorization_list.as_ref()
388    }
389
390    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
391        self.authorization_list = Some(authorization_list);
392    }
393}
394
395#[cfg(feature = "optimism")]
396impl FoundryTransactionBuilder<Optimism> for OpTransactionRequest {
397    fn reset_gas_limit(&mut self) {
398        self.as_mut().gas = None;
399    }
400
401    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
402        self.as_ref().authorization_list.as_ref()
403    }
404
405    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
406        self.as_mut().authorization_list = Some(authorization_list);
407    }
408}
409
410impl FoundryTransactionBuilder<TempoNetwork> for <TempoNetwork as Network>::TransactionRequest {
411    fn reset_gas_limit(&mut self) {
412        self.gas = None;
413    }
414
415    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
416        self.authorization_list.as_ref()
417    }
418
419    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
420        self.authorization_list = Some(authorization_list);
421    }
422
423    fn fee_token(&self) -> Option<Address> {
424        self.fee_token
425    }
426
427    fn tempo_calls(&self) -> Vec<(TxKind, &[u8])> {
428        self.calls
429            .iter()
430            .map(|call| (call.to, call.input.as_ref()))
431            .chain(self.inner.to.map(|to| {
432                (to, self.inner.input.input().map_or(&[] as &[u8], |input| input.as_ref()))
433            }))
434            .collect()
435    }
436
437    fn has_tempo_call_list(&self) -> bool {
438        !self.calls.is_empty()
439    }
440
441    fn is_tempo_aa(&self) -> bool {
442        NetworkTransactionBuilder::<TempoNetwork>::output_tx_type(self) == TempoTxType::AA
443    }
444
445    fn set_fee_token(&mut self, fee_token: Address) {
446        self.fee_token = Some(fee_token);
447    }
448
449    fn nonce_key(&self) -> Option<U256> {
450        self.nonce_key
451    }
452
453    fn set_nonce_key(&mut self, nonce_key: U256) {
454        self.nonce_key = Some(nonce_key);
455    }
456
457    fn key_id(&self) -> Option<Address> {
458        self.key_id
459    }
460
461    fn set_key_id(&mut self, key_id: Address) {
462        self.key_id = Some(key_id);
463    }
464
465    fn valid_before(&self) -> Option<NonZeroU64> {
466        self.valid_before
467    }
468
469    fn set_valid_before(&mut self, valid_before: NonZeroU64) {
470        self.valid_before = Some(valid_before);
471    }
472
473    fn valid_after(&self) -> Option<NonZeroU64> {
474        self.valid_after
475    }
476
477    fn set_valid_after(&mut self, valid_after: NonZeroU64) {
478        self.valid_after = Some(valid_after);
479    }
480
481    fn fee_payer_signature(&self) -> Option<Signature> {
482        self.fee_payer_signature
483    }
484
485    fn set_fee_payer_signature(&mut self, signature: Signature) {
486        self.fee_payer_signature = Some(signature);
487    }
488
489    fn compute_sponsor_hash(&self, from: Address) -> Option<B256> {
490        let tx = self.clone().build_aa().ok()?;
491        Some(tx.fee_payer_signature_hash(from))
492    }
493
494    fn set_key_authorization(&mut self, key_authorization: SignedKeyAuthorization) {
495        self.key_authorization = Some(key_authorization);
496    }
497
498    fn prepare_access_key_authorization<'a>(
499        &'a mut self,
500        provider: &'a impl Provider<TempoNetwork>,
501        wallet_address: Address,
502        key_address: Address,
503        key_authorization: Option<&'a SignedKeyAuthorization>,
504    ) -> impl Future<Output = Result<()>> + Send + 'a
505    where
506        Self: Send,
507    {
508        let auth = key_authorization.cloned();
509
510        async move {
511            if let Some(auth) = auth {
512                let is_provisioned = provider
513                    .get_keychain_key(wallet_address, key_address)
514                    .await
515                    .map(|info| info.keyId != Address::ZERO)
516                    .unwrap_or(false);
517
518                if !is_provisioned {
519                    self.set_key_authorization(auth);
520                }
521            }
522
523            Ok(())
524        }
525    }
526
527    fn convert_create_to_call(&mut self) {
528        if self.calls.is_empty() && self.inner.to.is_some_and(|to| to.is_create()) {
529            let input = self.inner.input.input().cloned().unwrap_or_default();
530            let value = self.inner.value.unwrap_or(U256::ZERO);
531            self.calls.push(Call { to: TxKind::Create, value, input });
532            self.inner.input = Default::default();
533            self.inner.value = None;
534            self.inner.to = None;
535        }
536    }
537
538    fn clear_batch_to(&mut self) {
539        if !self.calls.is_empty() {
540            self.inner.to = None;
541            self.inner.value = None;
542        }
543    }
544
545    fn sign_with_access_key(
546        mut self,
547        provider: &impl Provider<TempoNetwork>,
548        signer: &(impl Signer + Sync),
549        wallet_address: Address,
550        key_address: Address,
551        key_authorization: Option<&SignedKeyAuthorization>,
552    ) -> impl Future<Output = Result<Vec<u8>>> + Send {
553        let auth = key_authorization.cloned();
554        let provisioning_fut = provider.get_keychain_key(wallet_address, key_address);
555
556        async move {
557            if let Some(auth) = auth {
558                let is_provisioned =
559                    provisioning_fut.await.map(|info| info.keyId != Address::ZERO).unwrap_or(false);
560
561                if !is_provisioned && self.key_authorization.is_none() {
562                    if self.fee_payer_signature.is_some() {
563                        eyre::bail!(
564                            "cannot add Tempo key authorization after fee payer signature was attached"
565                        );
566                    }
567                    self.set_key_authorization(auth);
568                }
569            }
570
571            let tempo_tx = self
572                .build_aa()
573                .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?;
574
575            let sig_hash = tempo_tx.signature_hash();
576            let signing_hash = KeychainSignature::signing_hash(sig_hash, wallet_address);
577            let raw_sig = signer.sign_hash(&signing_hash).await?;
578
579            let keychain_sig =
580                KeychainSignature::new(wallet_address, PrimitiveSignature::Secp256k1(raw_sig));
581            let aa_signed = tempo_tx.into_signed(TempoSignature::Keychain(keychain_sig));
582
583            let mut buf = Vec::new();
584            aa_signed.encode_2718(&mut buf);
585            Ok(buf)
586        }
587    }
588}