Skip to main content

foundry_common/transactions/
builder.rs

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