foundry_primitives/transaction/
request.rs

1use alloy_consensus::EthereumTypedTransaction;
2use alloy_network::{
3    BuildResult, NetworkWallet, TransactionBuilder, TransactionBuilder4844, TransactionBuilderError,
4};
5use alloy_primitives::{Address, B256, ChainId, TxKind, U256};
6use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
7use alloy_serde::{OtherFields, WithOtherFields};
8use derive_more::{AsMut, AsRef, From, Into};
9use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
10use op_revm::transaction::deposit::DepositTransactionParts;
11use serde::{Deserialize, Serialize};
12use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTransaction, transaction::Call};
13
14use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
15use crate::FoundryNetwork;
16
17/// Foundry transaction request builder.
18///
19/// This is implemented as a wrapper around [`WithOtherFields<TransactionRequest>`],
20/// which provides handling of deposit transactions.
21#[derive(Clone, Debug, Default, PartialEq, Eq, From, Into, AsRef, AsMut)]
22pub struct FoundryTransactionRequest(WithOtherFields<TransactionRequest>);
23
24impl Serialize for FoundryTransactionRequest {
25    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26    where
27        S: serde::Serializer,
28    {
29        self.as_ref().serialize(serializer)
30    }
31}
32
33impl<'de> Deserialize<'de> for FoundryTransactionRequest {
34    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35    where
36        D: serde::Deserializer<'de>,
37    {
38        WithOtherFields::<TransactionRequest>::deserialize(deserializer).map(Self)
39    }
40}
41
42impl FoundryTransactionRequest {
43    /// Create a new [`FoundryTransactionRequest`] from given
44    /// [`WithOtherFields<TransactionRequest>`].
45    #[inline]
46    pub fn new(inner: WithOtherFields<TransactionRequest>) -> Self {
47        Self(inner)
48    }
49
50    /// Consume self and return the inner [`WithOtherFields<TransactionRequest>`].
51    #[inline]
52    pub fn into_inner(self) -> WithOtherFields<TransactionRequest> {
53        self.0
54    }
55
56    /// Check if this is a deposit transaction.
57    #[inline]
58    pub fn is_deposit(&self) -> bool {
59        self.as_ref().transaction_type == Some(DEPOSIT_TX_TYPE_ID)
60    }
61
62    /// Check if this is a Tempo transaction.
63    ///
64    /// Returns true if the transaction type is explicitly set to Tempo (0x76) or if
65    /// a `feeToken` is set in OtherFields.
66    #[inline]
67    pub fn is_tempo(&self) -> bool {
68        self.as_ref().transaction_type == Some(TEMPO_TX_TYPE_ID)
69            || self.as_ref().other.contains_key("feeToken")
70            || self.as_ref().other.contains_key("nonceKey")
71    }
72
73    /// Get the Tempo fee token from OtherFields if present.
74    fn get_tempo_fee_token(&self) -> Option<Address> {
75        self.as_ref().other.get_deserialized::<Address>("feeToken").transpose().ok().flatten()
76    }
77
78    /// Get the Tempo nonce sequence key from OtherFields if present.
79    fn get_tempo_nonce_key(&self) -> U256 {
80        self.as_ref()
81            .other
82            .get_deserialized::<U256>("nonceKey")
83            .transpose()
84            .ok()
85            .flatten()
86            .unwrap_or_default()
87    }
88
89    /// Check if all necessary keys are present to build a Tempo transaction, returning a list of
90    /// keys that are missing.
91    pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
92        let mut missing = Vec::new();
93        if self.chain_id().is_none() {
94            missing.push("chain_id");
95        }
96        if self.gas_limit().is_none() {
97            missing.push("gas_limit");
98        }
99        if self.max_fee_per_gas().is_none() {
100            missing.push("max_fee_per_gas");
101        }
102        if self.max_priority_fee_per_gas().is_none() {
103            missing.push("max_priority_fee_per_gas");
104        }
105        if self.nonce().is_none() {
106            missing.push("nonce");
107        }
108        if missing.is_empty() { Ok(()) } else { Err(missing) }
109    }
110
111    /// Returns the minimal transaction type this request can be converted into based on the fields
112    /// that are set. See [`TransactionRequest::preferred_type`].
113    pub fn preferred_type(&self) -> FoundryTxType {
114        if self.is_deposit() {
115            FoundryTxType::Deposit
116        } else if self.is_tempo() {
117            FoundryTxType::Tempo
118        } else {
119            self.as_ref().preferred_type().into()
120        }
121    }
122
123    /// Check if all necessary keys are present to build a 4844 transaction,
124    /// returning a list of keys that are missing.
125    ///
126    /// **NOTE:** Inner [`TransactionRequest::complete_4844`] method but "sidecar" key is filtered
127    /// from error.
128    pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
129        match self.as_ref().complete_4844() {
130            Ok(()) => Ok(()),
131            Err(missing) => {
132                let filtered: Vec<_> =
133                    missing.into_iter().filter(|&key| key != "sidecar").collect();
134                if filtered.is_empty() { Ok(()) } else { Err(filtered) }
135            }
136        }
137    }
138
139    /// Check if all necessary keys are present to build a Deposit transaction, returning a list of
140    /// keys that are missing.
141    pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
142        get_deposit_tx_parts(&self.as_ref().other).map(|_| ())
143    }
144
145    /// Return the tx type this request can be built as. Computed by checking
146    /// the preferred type, and then checking for completeness.
147    pub fn buildable_type(&self) -> Option<FoundryTxType> {
148        let pref = self.preferred_type();
149        match pref {
150            FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
151            FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
152            FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
153            FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
154            FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
155            FoundryTxType::Deposit => self.complete_deposit().ok(),
156            FoundryTxType::Tempo => self.complete_tempo().ok(),
157        }?;
158        Some(pref)
159    }
160
161    /// Check if all necessary keys are present to build a transaction.
162    ///
163    /// # Returns
164    ///
165    /// - Ok(type) if all necessary keys are present to build the preferred type.
166    /// - Err((type, missing)) if some keys are missing to build the preferred type.
167    pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
168        let pref = self.preferred_type();
169        if let Err(missing) = match pref {
170            FoundryTxType::Legacy => self.as_ref().complete_legacy(),
171            FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
172            FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
173            FoundryTxType::Eip4844 => self.complete_4844(),
174            FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
175            FoundryTxType::Deposit => self.complete_deposit(),
176            FoundryTxType::Tempo => self.complete_tempo(),
177        } {
178            Err((pref, missing))
179        } else {
180            Ok(pref)
181        }
182    }
183
184    /// Build a typed transaction from this request.
185    ///
186    /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction
187    /// types.
188    pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
189        // Handle deposit transactions
190        if let Ok(deposit_tx_parts) = get_deposit_tx_parts(&self.as_ref().other) {
191            Ok(FoundryTypedTx::Deposit(TxDeposit {
192                from: self.from().unwrap_or_default(),
193                source_hash: deposit_tx_parts.source_hash,
194                to: self.kind().unwrap_or_default(),
195                mint: deposit_tx_parts.mint.unwrap_or_default(),
196                value: self.value().unwrap_or_default(),
197                gas_limit: self.gas_limit().unwrap_or_default(),
198                is_system_transaction: deposit_tx_parts.is_system_transaction,
199                input: self.input().cloned().unwrap_or_default(),
200            }))
201        } else if self.is_tempo() {
202            // Build Tempo transaction from request fields
203            Ok(FoundryTypedTx::Tempo(TempoTransaction {
204                chain_id: self.chain_id().unwrap_or_default(),
205                fee_token: self.get_tempo_fee_token(),
206                max_fee_per_gas: self.max_fee_per_gas().unwrap_or_default(),
207                max_priority_fee_per_gas: self.max_priority_fee_per_gas().unwrap_or_default(),
208                gas_limit: self.gas_limit().unwrap_or_default(),
209                nonce_key: self.get_tempo_nonce_key(),
210                nonce: self.nonce().unwrap_or_default(),
211                calls: vec![Call {
212                    to: self.kind().unwrap_or_default(),
213                    value: self.value().unwrap_or_default(),
214                    input: self.input().cloned().unwrap_or_default(),
215                }],
216                access_list: self.access_list().cloned().unwrap_or_default(),
217                ..Default::default()
218            }))
219        } else if self.as_ref().has_eip4844_fields() && self.as_ref().blob_sidecar().is_none() {
220            // if request has eip4844 fields but no blob sidecar, try to build to eip4844 without
221            // sidecar
222            self.0
223                .into_inner()
224                .build_4844_without_sidecar()
225                .map_err(|e| Self(e.into_value().into()))
226                .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
227        } else {
228            // Use the inner transaction request to build EthereumTypedTransaction
229            let typed_tx = self.0.into_inner().build_typed_tx().map_err(|tx| Self(tx.into()))?;
230            // Convert EthereumTypedTransaction to FoundryTypedTx
231            Ok(match typed_tx {
232                EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
233                EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
234                EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
235                EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
236                EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
237            })
238        }
239    }
240}
241
242impl From<FoundryTypedTx> for FoundryTransactionRequest {
243    fn from(tx: FoundryTypedTx) -> Self {
244        match tx {
245            FoundryTypedTx::Legacy(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
246            FoundryTypedTx::Eip2930(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
247            FoundryTypedTx::Eip1559(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
248            FoundryTypedTx::Eip4844(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
249            FoundryTypedTx::Eip7702(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
250            FoundryTypedTx::Deposit(tx) => {
251                let other = OtherFields::from_iter([
252                    ("sourceHash", tx.source_hash.to_string().into()),
253                    ("mint", tx.mint.to_string().into()),
254                    ("isSystemTx", tx.is_system_transaction.to_string().into()),
255                ]);
256                WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }.into()
257            }
258            FoundryTypedTx::Tempo(tx) => {
259                let mut other = OtherFields::default();
260                if let Some(fee_token) = tx.fee_token {
261                    other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
262                }
263                other.insert("nonceKey".to_string(), serde_json::to_value(tx.nonce_key).unwrap());
264                let first_call = tx.calls.first();
265                let mut inner = TransactionRequest::default()
266                    .with_chain_id(tx.chain_id)
267                    .with_nonce(tx.nonce)
268                    .with_gas_limit(tx.gas_limit)
269                    .with_max_fee_per_gas(tx.max_fee_per_gas)
270                    .with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas)
271                    .with_kind(first_call.map(|c| c.to).unwrap_or_default())
272                    .with_value(first_call.map(|c| c.value).unwrap_or_default())
273                    .with_input(first_call.map(|c| c.input.clone()).unwrap_or_default())
274                    .with_access_list(tx.access_list);
275                inner.transaction_type = Some(TEMPO_TX_TYPE_ID);
276                WithOtherFields { inner, other }.into()
277            }
278        }
279    }
280}
281
282impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
283    fn from(tx: FoundryTxEnvelope) -> Self {
284        FoundryTypedTx::from(tx).into()
285    }
286}
287
288// TransactionBuilder trait implementation for FoundryNetwork
289impl TransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
290    fn chain_id(&self) -> Option<ChainId> {
291        self.as_ref().chain_id
292    }
293
294    fn set_chain_id(&mut self, chain_id: ChainId) {
295        self.as_mut().chain_id = Some(chain_id);
296    }
297
298    fn nonce(&self) -> Option<u64> {
299        self.as_ref().nonce
300    }
301
302    fn set_nonce(&mut self, nonce: u64) {
303        self.as_mut().nonce = Some(nonce);
304    }
305
306    fn take_nonce(&mut self) -> Option<u64> {
307        self.as_mut().nonce.take()
308    }
309
310    fn input(&self) -> Option<&alloy_primitives::Bytes> {
311        self.as_ref().input.input()
312    }
313
314    fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
315        self.as_mut().input.input = Some(input.into());
316    }
317
318    fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
319        &mut self,
320        input: T,
321        kind: TransactionInputKind,
322    ) {
323        let inner = self.as_mut();
324        match kind {
325            TransactionInputKind::Input => inner.input.input = Some(input.into()),
326            TransactionInputKind::Data => inner.input.data = Some(input.into()),
327            TransactionInputKind::Both => {
328                let bytes = input.into();
329                inner.input.input = Some(bytes.clone());
330                inner.input.data = Some(bytes);
331            }
332        }
333    }
334
335    fn from(&self) -> Option<Address> {
336        self.as_ref().from
337    }
338
339    fn set_from(&mut self, from: Address) {
340        self.as_mut().from = Some(from);
341    }
342
343    fn kind(&self) -> Option<TxKind> {
344        self.as_ref().to
345    }
346
347    fn clear_kind(&mut self) {
348        self.as_mut().to = None;
349    }
350
351    fn set_kind(&mut self, kind: TxKind) {
352        self.as_mut().to = Some(kind);
353    }
354
355    fn value(&self) -> Option<U256> {
356        self.as_ref().value
357    }
358
359    fn set_value(&mut self, value: U256) {
360        self.as_mut().value = Some(value);
361    }
362
363    fn gas_price(&self) -> Option<u128> {
364        self.as_ref().gas_price
365    }
366
367    fn set_gas_price(&mut self, gas_price: u128) {
368        self.as_mut().gas_price = Some(gas_price);
369    }
370
371    fn max_fee_per_gas(&self) -> Option<u128> {
372        self.as_ref().max_fee_per_gas
373    }
374
375    fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
376        self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
377    }
378
379    fn max_priority_fee_per_gas(&self) -> Option<u128> {
380        self.as_ref().max_priority_fee_per_gas
381    }
382
383    fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
384        self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
385    }
386
387    fn gas_limit(&self) -> Option<u64> {
388        self.as_ref().gas
389    }
390
391    fn set_gas_limit(&mut self, gas_limit: u64) {
392        self.as_mut().gas = Some(gas_limit);
393    }
394
395    fn access_list(&self) -> Option<&AccessList> {
396        self.as_ref().access_list.as_ref()
397    }
398
399    fn set_access_list(&mut self, access_list: AccessList) {
400        self.as_mut().access_list = Some(access_list);
401    }
402
403    fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
404        match ty {
405            FoundryTxType::Legacy => self.as_ref().complete_legacy(),
406            FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
407            FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
408            FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
409            FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
410            FoundryTxType::Deposit => self.complete_deposit(),
411            FoundryTxType::Tempo => self.complete_tempo(),
412        }
413    }
414
415    fn can_submit(&self) -> bool {
416        self.from().is_some()
417    }
418
419    fn can_build(&self) -> bool {
420        self.as_ref().can_build()
421            || get_deposit_tx_parts(&self.as_ref().other).is_ok()
422            || self.is_tempo()
423    }
424
425    fn output_tx_type(&self) -> FoundryTxType {
426        self.preferred_type()
427    }
428
429    fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
430        self.buildable_type()
431    }
432
433    /// Prepares [`FoundryTransactionRequest`] by trimming conflicting fields, and filling with
434    /// default values the mandatory fields.
435    fn prep_for_submission(&mut self) {
436        let preferred_type = self.preferred_type();
437        let inner = self.as_mut();
438        inner.transaction_type = Some(preferred_type as u8);
439        inner.gas_limit().is_none().then(|| inner.set_gas_limit(Default::default()));
440        if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) {
441            inner.trim_conflicting_keys();
442            inner.populate_blob_hashes();
443        }
444        if preferred_type != FoundryTxType::Deposit {
445            inner.nonce().is_none().then(|| inner.set_nonce(Default::default()));
446        }
447        if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
448            inner.gas_price().is_none().then(|| inner.set_gas_price(Default::default()));
449        }
450        if preferred_type == FoundryTxType::Eip2930 {
451            inner.access_list().is_none().then(|| inner.set_access_list(Default::default()));
452        }
453        if matches!(
454            preferred_type,
455            FoundryTxType::Eip1559
456                | FoundryTxType::Eip4844
457                | FoundryTxType::Eip7702
458                | FoundryTxType::Tempo
459        ) {
460            inner
461                .max_priority_fee_per_gas()
462                .is_none()
463                .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
464            inner
465                .max_fee_per_gas()
466                .is_none()
467                .then(|| inner.set_max_fee_per_gas(Default::default()));
468        }
469        if preferred_type == FoundryTxType::Eip4844 {
470            inner
471                .as_ref()
472                .max_fee_per_blob_gas()
473                .is_none()
474                .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
475        }
476    }
477
478    fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
479        if let Err((tx_type, missing)) = self.missing_keys() {
480            return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
481                .into_unbuilt(self));
482        }
483        Ok(self.build_typed_tx().expect("checked by missing_keys"))
484    }
485
486    async fn build<W: NetworkWallet<FoundryNetwork>>(
487        self,
488        wallet: &W,
489    ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
490        Ok(wallet.sign_request(self).await?)
491    }
492}
493
494/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields
495pub fn get_deposit_tx_parts(
496    other: &OtherFields,
497) -> Result<DepositTransactionParts, Vec<&'static str>> {
498    let mut missing = Vec::new();
499    let source_hash =
500        other.get_deserialized::<B256>("sourceHash").transpose().ok().flatten().unwrap_or_else(
501            || {
502                missing.push("sourceHash");
503                Default::default()
504            },
505        );
506    let mint = other
507        .get_deserialized::<U256>("mint")
508        .transpose()
509        .unwrap_or_else(|_| {
510            missing.push("mint");
511            Default::default()
512        })
513        .map(|value| value.to::<u128>());
514    let is_system_transaction =
515        other.get_deserialized::<bool>("isSystemTx").transpose().ok().flatten().unwrap_or_else(
516            || {
517                missing.push("isSystemTx");
518                Default::default()
519            },
520        );
521    if missing.is_empty() {
522        Ok(DepositTransactionParts { source_hash, mint, is_system_transaction })
523    } else {
524        Err(missing)
525    }
526}