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