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