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,
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    /// Set the fee token for a Tempo transaction.
153    fn set_fee_token(&mut self, _fee_token: Address) {}
154
155    /// Builder-pattern method for setting the Tempo fee token.
156    fn with_fee_token(mut self, fee_token: Address) -> Self {
157        self.set_fee_token(fee_token);
158        self
159    }
160
161    /// Get the 2D nonce key for a Tempo transaction.
162    fn nonce_key(&self) -> Option<U256> {
163        None
164    }
165
166    /// Set the 2D nonce key for the Tempo transaction.
167    fn set_nonce_key(&mut self, _nonce_key: U256) {}
168
169    /// Builder-pattern method for setting a 2D nonce key for a Tempo transaction.
170    fn with_nonce_key(mut self, nonce_key: U256) -> Self {
171        self.set_nonce_key(nonce_key);
172        self
173    }
174
175    /// Get the access key ID for a Tempo transaction.
176    fn key_id(&self) -> Option<Address> {
177        None
178    }
179
180    /// Set the access key ID for a Tempo transaction.
181    ///
182    /// Used during gas estimation to override the key_id that would normally be
183    /// recovered from the signature.
184    fn set_key_id(&mut self, _key_id: Address) {}
185
186    /// Builder-pattern method for setting the Tempo access key ID.
187    fn with_key_id(mut self, key_id: Address) -> Self {
188        self.set_key_id(key_id);
189        self
190    }
191
192    /// Get the valid_before timestamp for a Tempo expiring nonce transaction.
193    fn valid_before(&self) -> Option<NonZeroU64> {
194        None
195    }
196
197    /// Set the valid_before timestamp for a Tempo expiring nonce transaction.
198    fn set_valid_before(&mut self, _valid_before: NonZeroU64) {}
199
200    /// Builder-pattern method for setting the valid_before timestamp.
201    fn with_valid_before(mut self, valid_before: NonZeroU64) -> Self {
202        self.set_valid_before(valid_before);
203        self
204    }
205
206    /// Get the valid_after timestamp for a Tempo expiring nonce transaction.
207    fn valid_after(&self) -> Option<NonZeroU64> {
208        None
209    }
210
211    /// Set the valid_after timestamp for a Tempo expiring nonce transaction.
212    fn set_valid_after(&mut self, _valid_after: NonZeroU64) {}
213
214    /// Builder-pattern method for setting the valid_after timestamp.
215    fn with_valid_after(mut self, valid_after: NonZeroU64) -> Self {
216        self.set_valid_after(valid_after);
217        self
218    }
219
220    /// Get the fee payer (sponsor) signature for a Tempo sponsored transaction.
221    fn fee_payer_signature(&self) -> Option<Signature> {
222        None
223    }
224
225    /// Set the fee payer (sponsor) signature for a Tempo sponsored transaction.
226    fn set_fee_payer_signature(&mut self, _signature: Signature) {}
227
228    /// Builder-pattern method for setting the fee payer signature.
229    fn with_fee_payer_signature(mut self, signature: Signature) -> Self {
230        self.set_fee_payer_signature(signature);
231        self
232    }
233
234    /// Computes the sponsor (fee payer) signature hash for this transaction.
235    ///
236    /// This builds an unsigned consensus-level transaction from the request and computes
237    /// the hash that a sponsor needs to sign. Returns `None` for networks that don't
238    /// support sponsored transactions.
239    fn compute_sponsor_hash(&self, _from: Address) -> Option<B256> {
240        None
241    }
242
243    /// Set the key authorization for a Tempo transaction.
244    ///
245    /// Embeds a [`SignedKeyAuthorization`] in the transaction body, provisioning the access key
246    /// on-chain as part of this transaction.
247    fn set_key_authorization(&mut self, _key_authorization: SignedKeyAuthorization) {}
248
249    /// Embeds key authorization before gas estimation/signing if the access key is not yet
250    /// provisioned on-chain.
251    ///
252    /// This mirrors the mutation performed by [`Self::sign_with_access_key`], but makes the final
253    /// transaction body available before fee-payer sponsor digests are computed.
254    fn prepare_access_key_authorization<'a>(
255        &'a mut self,
256        _provider: &'a impl Provider<N>,
257        _wallet_address: Address,
258        _key_address: Address,
259        _key_authorization: Option<&'a SignedKeyAuthorization>,
260    ) -> impl Future<Output = Result<()>> + Send + 'a
261    where
262        Self: Send,
263    {
264        async { Ok(()) }
265    }
266
267    /// Converts a CREATE transaction into an AA-compatible call entry.
268    ///
269    /// Tempo AA transactions use a `calls` list instead of `to`+`input`. Must be
270    /// called before gas estimation so the RPC sees the correct tx structure.
271    /// No-op for non-Tempo networks.
272    fn convert_create_to_call(&mut self) {}
273
274    /// Clears the `to` and `value` fields for batch transactions that use `calls`.
275    ///
276    /// In Tempo AA batch transactions, targets are specified in the `calls` field, not in `to`.
277    /// If `to` is set, `build_aa()` would add a spurious extra call. Must be called after
278    /// `prepare()` sets `kind`/`to` but before gas estimation.
279    /// No-op for non-Tempo networks.
280    fn clear_batch_to(&mut self) {}
281
282    /// Signs the transaction using an access key (keychain mode).
283    ///
284    /// If `key_authorization` is provided and the key is not yet provisioned on-chain,
285    /// embeds the authorization in the transaction before signing.
286    ///
287    /// The default implementation returns an error. Only `TempoNetwork` supports this.
288    fn sign_with_access_key(
289        self,
290        _provider: &impl Provider<N>,
291        _signer: &(impl Signer + Sync),
292        _wallet_address: Address,
293        _key_address: Address,
294        _key_authorization: Option<&SignedKeyAuthorization>,
295    ) -> impl Future<Output = Result<Vec<u8>>> + Send {
296        async { eyre::bail!("access key signing is not supported for this network") }
297    }
298}
299
300impl FoundryTransactionBuilder<Ethereum> for <Ethereum as Network>::TransactionRequest {
301    fn reset_gas_limit(&mut self) {
302        self.gas = None;
303    }
304
305    fn max_fee_per_blob_gas(&self) -> Option<u128> {
306        self.max_fee_per_blob_gas
307    }
308
309    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
310        self.max_fee_per_blob_gas = Some(max_fee_per_blob_gas);
311    }
312
313    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
314        self.blob_versioned_hashes.as_deref()
315    }
316
317    fn set_blob_versioned_hashes(&mut self, hashes: Vec<B256>) {
318        self.blob_versioned_hashes = Some(hashes);
319    }
320
321    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
322        self.sidecar.as_ref()
323    }
324
325    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
326        self.sidecar = Some(sidecar);
327        self.populate_blob_hashes();
328    }
329
330    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
331        self.authorization_list.as_ref()
332    }
333
334    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
335        self.authorization_list = Some(authorization_list);
336    }
337}
338
339impl FoundryTransactionBuilder<AnyNetwork> for <AnyNetwork as Network>::TransactionRequest {
340    fn reset_gas_limit(&mut self) {
341        self.gas = None;
342    }
343
344    fn max_fee_per_blob_gas(&self) -> Option<u128> {
345        self.max_fee_per_blob_gas
346    }
347
348    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
349        self.max_fee_per_blob_gas = Some(max_fee_per_blob_gas);
350    }
351
352    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
353        self.blob_versioned_hashes.as_deref()
354    }
355
356    fn set_blob_versioned_hashes(&mut self, hashes: Vec<B256>) {
357        self.blob_versioned_hashes = Some(hashes);
358    }
359
360    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
361        self.sidecar.as_ref()
362    }
363
364    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
365        self.sidecar = Some(sidecar);
366        self.populate_blob_hashes();
367    }
368
369    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
370        self.authorization_list.as_ref()
371    }
372
373    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
374        self.authorization_list = Some(authorization_list);
375    }
376}
377
378#[cfg(feature = "optimism")]
379impl FoundryTransactionBuilder<Optimism> for OpTransactionRequest {
380    fn reset_gas_limit(&mut self) {
381        self.as_mut().gas = None;
382    }
383
384    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
385        self.as_ref().authorization_list.as_ref()
386    }
387
388    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
389        self.as_mut().authorization_list = Some(authorization_list);
390    }
391}
392
393impl FoundryTransactionBuilder<TempoNetwork> for <TempoNetwork as Network>::TransactionRequest {
394    fn reset_gas_limit(&mut self) {
395        self.gas = None;
396    }
397
398    fn authorization_list(&self) -> Option<&Vec<SignedAuthorization>> {
399        self.authorization_list.as_ref()
400    }
401
402    fn set_authorization_list(&mut self, authorization_list: Vec<SignedAuthorization>) {
403        self.authorization_list = Some(authorization_list);
404    }
405
406    fn fee_token(&self) -> Option<Address> {
407        self.fee_token
408    }
409
410    fn set_fee_token(&mut self, fee_token: Address) {
411        self.fee_token = Some(fee_token);
412    }
413
414    fn nonce_key(&self) -> Option<U256> {
415        self.nonce_key
416    }
417
418    fn set_nonce_key(&mut self, nonce_key: U256) {
419        self.nonce_key = Some(nonce_key);
420    }
421
422    fn key_id(&self) -> Option<Address> {
423        self.key_id
424    }
425
426    fn set_key_id(&mut self, key_id: Address) {
427        self.key_id = Some(key_id);
428    }
429
430    fn valid_before(&self) -> Option<NonZeroU64> {
431        self.valid_before
432    }
433
434    fn set_valid_before(&mut self, valid_before: NonZeroU64) {
435        self.valid_before = Some(valid_before);
436    }
437
438    fn valid_after(&self) -> Option<NonZeroU64> {
439        self.valid_after
440    }
441
442    fn set_valid_after(&mut self, valid_after: NonZeroU64) {
443        self.valid_after = Some(valid_after);
444    }
445
446    fn fee_payer_signature(&self) -> Option<Signature> {
447        self.fee_payer_signature
448    }
449
450    fn set_fee_payer_signature(&mut self, signature: Signature) {
451        self.fee_payer_signature = Some(signature);
452    }
453
454    fn compute_sponsor_hash(&self, from: Address) -> Option<B256> {
455        let tx = self.clone().build_aa().ok()?;
456        Some(tx.fee_payer_signature_hash(from))
457    }
458
459    fn set_key_authorization(&mut self, key_authorization: SignedKeyAuthorization) {
460        self.key_authorization = Some(key_authorization);
461    }
462
463    fn prepare_access_key_authorization<'a>(
464        &'a mut self,
465        provider: &'a impl Provider<TempoNetwork>,
466        wallet_address: Address,
467        key_address: Address,
468        key_authorization: Option<&'a SignedKeyAuthorization>,
469    ) -> impl Future<Output = Result<()>> + Send + 'a
470    where
471        Self: Send,
472    {
473        let auth = key_authorization.cloned();
474
475        async move {
476            if let Some(auth) = auth {
477                let is_provisioned = provider
478                    .get_keychain_key(wallet_address, key_address)
479                    .await
480                    .map(|info| info.keyId != Address::ZERO)
481                    .unwrap_or(false);
482
483                if !is_provisioned {
484                    self.set_key_authorization(auth);
485                }
486            }
487
488            Ok(())
489        }
490    }
491
492    fn convert_create_to_call(&mut self) {
493        if self.calls.is_empty() && self.inner.to.is_some_and(|to| to.is_create()) {
494            let input = self.inner.input.input().cloned().unwrap_or_default();
495            let value = self.inner.value.unwrap_or(U256::ZERO);
496            self.calls.push(Call { to: TxKind::Create, value, input });
497            self.inner.input = Default::default();
498            self.inner.value = None;
499            self.inner.to = None;
500        }
501    }
502
503    fn clear_batch_to(&mut self) {
504        if !self.calls.is_empty() {
505            self.inner.to = None;
506            self.inner.value = None;
507        }
508    }
509
510    fn sign_with_access_key(
511        mut self,
512        provider: &impl Provider<TempoNetwork>,
513        signer: &(impl Signer + Sync),
514        wallet_address: Address,
515        key_address: Address,
516        key_authorization: Option<&SignedKeyAuthorization>,
517    ) -> impl Future<Output = Result<Vec<u8>>> + Send {
518        let auth = key_authorization.cloned();
519        let provisioning_fut = provider.get_keychain_key(wallet_address, key_address);
520
521        async move {
522            if let Some(auth) = auth {
523                let is_provisioned =
524                    provisioning_fut.await.map(|info| info.keyId != Address::ZERO).unwrap_or(false);
525
526                if !is_provisioned && self.key_authorization.is_none() {
527                    if self.fee_payer_signature.is_some() {
528                        eyre::bail!(
529                            "cannot add Tempo key authorization after fee payer signature was attached"
530                        );
531                    }
532                    self.set_key_authorization(auth);
533                }
534            }
535
536            let tempo_tx = self
537                .build_aa()
538                .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?;
539
540            let sig_hash = tempo_tx.signature_hash();
541            let signing_hash = KeychainSignature::signing_hash(sig_hash, wallet_address);
542            let raw_sig = signer.sign_hash(&signing_hash).await?;
543
544            let keychain_sig =
545                KeychainSignature::new(wallet_address, PrimitiveSignature::Secp256k1(raw_sig));
546            let aa_signed = tempo_tx.into_signed(TempoSignature::Keychain(keychain_sig));
547
548            let mut buf = Vec::new();
549            aa_signed.encode_2718(&mut buf);
550            Ok(buf)
551        }
552    }
553}