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