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