Skip to main content

foundry_primitives/transaction/
base.rs

1//! Base-specific transaction conversions.
2
3use super::FoundryTxEnvelope;
4use alloy_consensus::Typed2718;
5use alloy_evm::{FromRecoveredTx, FromTxWithEncoded};
6use alloy_network::eip2718::Encodable2718;
7use alloy_primitives::{Address, Bytes};
8use alloy_rpc_types::TransactionRequest;
9use base_common_consensus::{BaseTxEnvelope, TxEip8130};
10use base_common_evm::{BaseTransaction, DepositTransactionParts, EIP8130_TRANSACTION_TYPE};
11use base_common_rpc_types::{BaseTransactionRequest, Eip8130RequestFields};
12use revm::context::TxEnv;
13use serde::Serialize;
14
15impl FromRecoveredTx<FoundryTxEnvelope> for BaseTransaction<TxEnv> {
16    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
17        let encoded = tx.encoded_2718().into();
18        Self::from_encoded_tx(tx, caller, encoded)
19    }
20}
21
22impl FromTxWithEncoded<FoundryTxEnvelope> for BaseTransaction<TxEnv> {
23    fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self {
24        match tx {
25            FoundryTxEnvelope::Legacy(signed) => Self {
26                base: TxEnv::from_recovered_tx(signed, caller),
27                enveloped_tx: Some(encoded),
28                deposit: Default::default(),
29                eip8130: None,
30            },
31            FoundryTxEnvelope::Eip2930(signed) => Self {
32                base: TxEnv::from_recovered_tx(signed, caller),
33                enveloped_tx: Some(encoded),
34                deposit: Default::default(),
35                eip8130: None,
36            },
37            FoundryTxEnvelope::Eip1559(signed) => Self {
38                base: TxEnv::from_recovered_tx(signed, caller),
39                enveloped_tx: Some(encoded),
40                deposit: Default::default(),
41                eip8130: None,
42            },
43            FoundryTxEnvelope::Eip4844(signed) => Self {
44                base: TxEnv::from_recovered_tx(signed, caller),
45                enveloped_tx: Some(encoded),
46                deposit: Default::default(),
47                eip8130: None,
48            },
49            FoundryTxEnvelope::Eip7702(signed) => Self {
50                base: TxEnv::from_recovered_tx(signed, caller),
51                enveloped_tx: Some(encoded),
52                deposit: Default::default(),
53                eip8130: None,
54            },
55            #[cfg(any(feature = "base", feature = "optimism"))]
56            FoundryTxEnvelope::Deposit(sealed) => {
57                let deposit = sealed.inner();
58                let base = TxEnv {
59                    tx_type: deposit.ty(),
60                    caller,
61                    gas_limit: deposit.gas_limit,
62                    kind: deposit.to,
63                    value: deposit.value,
64                    data: deposit.input.clone(),
65                    ..Default::default()
66                };
67                Self {
68                    base,
69                    enveloped_tx: None,
70                    deposit: DepositTransactionParts {
71                        source_hash: deposit.source_hash,
72                        mint: Some(deposit.mint),
73                        is_system_transaction: deposit.is_system_transaction,
74                    },
75                    eip8130: None,
76                }
77            }
78            #[cfg(feature = "optimism")]
79            FoundryTxEnvelope::PostExec(_) => {
80                unreachable!("post-execution transaction in Base context")
81            }
82            FoundryTxEnvelope::Eip8130(signed) => {
83                let envelope = BaseTxEnvelope::Eip8130(signed.clone());
84                Self::from_encoded_tx(&envelope, caller, encoded)
85            }
86            FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo transaction in Base context"),
87        }
88    }
89}
90
91/// Projects the complete AA body into a simulation request without inventing a single call.
92pub(super) fn simulation_request(
93    tx: TxEip8130,
94    from: Option<Address>,
95    sender_auth: Option<Bytes>,
96    payer_auth: Option<Bytes>,
97) -> BaseTransactionRequest {
98    let inner = TransactionRequest {
99        transaction_type: Some(EIP8130_TRANSACTION_TYPE),
100        chain_id: Some(tx.chain_id),
101        from,
102        nonce: Some(tx.nonce_sequence),
103        gas: Some(tx.gas_limit),
104        max_fee_per_gas: Some(tx.max_fee_per_gas),
105        max_priority_fee_per_gas: Some(tx.max_priority_fee_per_gas),
106        ..Default::default()
107    };
108    let fields = Eip8130RequestFields {
109        nonce_key: Some(tx.nonce_key),
110        account_changes: Some(tx.account_changes),
111        calls: Some(tx.calls),
112        valid_after: Some(tx.valid_after),
113        valid_before: Some(tx.valid_before),
114        metadata: Some(tx.metadata),
115        sender: tx.sender,
116        sender_auth,
117        payer: tx.payer,
118        payer_auth,
119        ..Default::default()
120    };
121    // The upstream request exposes no constructor for its private AA fields.
122    #[derive(Serialize)]
123    struct SimulationRequest {
124        #[serde(flatten)]
125        inner: TransactionRequest,
126        #[serde(flatten)]
127        fields: Eip8130RequestFields,
128    }
129    let value = serde_json::to_value(SimulationRequest { inner, fields })
130        .expect("serializable Base simulation request");
131    serde_json::from_value(value).expect("compatible Base simulation fields")
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::{FoundryTransactionRequest, FoundryTypedTx};
138    use alloy_network::NetworkTransactionBuilder;
139    use alloy_primitives::U256;
140    use base_common_consensus::{AccountChange, Call, Delegation, Eip8130Signed};
141
142    #[test]
143    fn eip8130_simulation_projection() {
144        let body = TxEip8130 {
145            chain_id: 8453,
146            sender: Some(Address::repeat_byte(1)),
147            payer: Some(Address::repeat_byte(2)),
148            nonce_key: U256::from(7),
149            nonce_sequence: 9,
150            gas_limit: 100_000,
151            max_fee_per_gas: 20,
152            max_priority_fee_per_gas: 3,
153            valid_after: 100,
154            valid_before: 200,
155            calls: vec![vec![Call {
156                to: Address::repeat_byte(3),
157                data: Bytes::from_static(b"call"),
158            }]],
159            account_changes: vec![AccountChange::Delegation(Delegation {
160                target: Address::repeat_byte(4),
161            })],
162            metadata: Bytes::from_static(b"metadata"),
163        };
164        let signed = Eip8130Signed::new(
165            body.clone(),
166            Bytes::from_static(b"sender"),
167            Bytes::from_static(b"payer"),
168        );
169        let envelope = FoundryTxEnvelope::Eip8130(signed.clone());
170        for (request, from, sender_auth, payer_auth) in [
171            (
172                FoundryTransactionRequest::from(FoundryTypedTx::Eip8130(body.clone())),
173                None,
174                None,
175                None,
176            ),
177            (
178                FoundryTransactionRequest::from(envelope),
179                body.sender,
180                Some(signed.sender_auth().clone()),
181                Some(signed.payer_auth().clone()),
182            ),
183        ] {
184            assert!(!request.can_build());
185            let base = request.as_base().unwrap();
186            assert_eq!(
187                base.as_eip8130(),
188                Some(&Eip8130RequestFields {
189                    nonce_key: Some(body.nonce_key),
190                    account_changes: Some(body.account_changes.clone()),
191                    calls: Some(body.calls.clone()),
192                    valid_after: Some(body.valid_after),
193                    valid_before: Some(body.valid_before),
194                    metadata: Some(body.metadata.clone()),
195                    sender: body.sender,
196                    payer: body.payer,
197                    sender_auth,
198                    payer_auth,
199                    ..Default::default()
200                })
201            );
202            assert_eq!(
203                base.as_ref(),
204                &TransactionRequest {
205                    transaction_type: Some(EIP8130_TRANSACTION_TYPE),
206                    chain_id: Some(body.chain_id),
207                    from,
208                    nonce: Some(body.nonce_sequence),
209                    gas: Some(body.gas_limit),
210                    max_fee_per_gas: Some(body.max_fee_per_gas),
211                    max_priority_fee_per_gas: Some(body.max_priority_fee_per_gas),
212                    ..Default::default()
213                }
214            );
215            let roundtrip: FoundryTransactionRequest =
216                serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
217            assert_eq!(request, roundtrip);
218        }
219    }
220    #[test]
221    fn eip8130_conversion_preserves_supplied_encoding() {
222        let envelope = FoundryTxEnvelope::Eip8130(Eip8130Signed::new(
223            TxEip8130::default(),
224            Bytes::new(),
225            Bytes::new(),
226        ));
227        let encoded = Bytes::from_static(b"already encoded");
228        let env =
229            BaseTransaction::<TxEnv>::from_encoded_tx(&envelope, Address::ZERO, encoded.clone());
230        assert_eq!(env.enveloped_tx, Some(encoded));
231    }
232}