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            FoundryTxEnvelope::Celo(_) => unreachable!("CIP-64 transaction in base context"),
88        }
89    }
90}
91
92/// Projects the complete AA body into a simulation request without inventing a single call.
93pub(super) fn simulation_request(
94    tx: TxEip8130,
95    from: Option<Address>,
96    sender_auth: Option<Bytes>,
97    payer_auth: Option<Bytes>,
98) -> BaseTransactionRequest {
99    let inner = TransactionRequest {
100        transaction_type: Some(EIP8130_TRANSACTION_TYPE),
101        chain_id: Some(tx.chain_id),
102        from,
103        nonce: Some(tx.nonce_sequence),
104        gas: Some(tx.gas_limit),
105        max_fee_per_gas: Some(tx.max_fee_per_gas),
106        max_priority_fee_per_gas: Some(tx.max_priority_fee_per_gas),
107        ..Default::default()
108    };
109    let fields = Eip8130RequestFields {
110        nonce_key: Some(tx.nonce_key),
111        account_changes: Some(tx.account_changes),
112        calls: Some(tx.calls),
113        valid_after: Some(tx.valid_after),
114        valid_before: Some(tx.valid_before),
115        metadata: Some(tx.metadata),
116        sender: tx.sender,
117        sender_auth,
118        payer: tx.payer,
119        payer_auth,
120        ..Default::default()
121    };
122    // The upstream request exposes no constructor for its private AA fields.
123    #[derive(Serialize)]
124    struct SimulationRequest {
125        #[serde(flatten)]
126        inner: TransactionRequest,
127        #[serde(flatten)]
128        fields: Eip8130RequestFields,
129    }
130    let value = serde_json::to_value(SimulationRequest { inner, fields })
131        .expect("serializable Base simulation request");
132    serde_json::from_value(value).expect("compatible Base simulation fields")
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::{FoundryTransactionRequest, FoundryTypedTx};
139    use alloy_network::NetworkTransactionBuilder;
140    use alloy_primitives::U256;
141    use base_common_consensus::{AccountChange, Call, Delegation, Eip8130Signed};
142
143    #[test]
144    fn eip8130_simulation_projection() {
145        let body = TxEip8130 {
146            chain_id: 8453,
147            sender: Some(Address::repeat_byte(1)),
148            payer: Some(Address::repeat_byte(2)),
149            nonce_key: U256::from(7),
150            nonce_sequence: 9,
151            gas_limit: 100_000,
152            max_fee_per_gas: 20,
153            max_priority_fee_per_gas: 3,
154            valid_after: 100,
155            valid_before: 200,
156            calls: vec![vec![Call {
157                to: Address::repeat_byte(3),
158                data: Bytes::from_static(b"call"),
159            }]],
160            account_changes: vec![AccountChange::Delegation(Delegation {
161                target: Address::repeat_byte(4),
162            })],
163            metadata: Bytes::from_static(b"metadata"),
164        };
165        let signed = Eip8130Signed::new(
166            body.clone(),
167            Bytes::from_static(b"sender"),
168            Bytes::from_static(b"payer"),
169        );
170        let envelope = FoundryTxEnvelope::Eip8130(signed.clone());
171        for (request, from, sender_auth, payer_auth) in [
172            (
173                FoundryTransactionRequest::from(FoundryTypedTx::Eip8130(body.clone())),
174                None,
175                None,
176                None,
177            ),
178            (
179                FoundryTransactionRequest::from(envelope),
180                body.sender,
181                Some(signed.sender_auth().clone()),
182                Some(signed.payer_auth().clone()),
183            ),
184        ] {
185            assert!(!request.can_build());
186            let base = request.as_base().unwrap();
187            assert_eq!(
188                base.as_eip8130(),
189                Some(&Eip8130RequestFields {
190                    nonce_key: Some(body.nonce_key),
191                    account_changes: Some(body.account_changes.clone()),
192                    calls: Some(body.calls.clone()),
193                    valid_after: Some(body.valid_after),
194                    valid_before: Some(body.valid_before),
195                    metadata: Some(body.metadata.clone()),
196                    sender: body.sender,
197                    payer: body.payer,
198                    sender_auth,
199                    payer_auth,
200                    ..Default::default()
201                })
202            );
203            assert_eq!(
204                base.as_ref(),
205                &TransactionRequest {
206                    transaction_type: Some(EIP8130_TRANSACTION_TYPE),
207                    chain_id: Some(body.chain_id),
208                    from,
209                    nonce: Some(body.nonce_sequence),
210                    gas: Some(body.gas_limit),
211                    max_fee_per_gas: Some(body.max_fee_per_gas),
212                    max_priority_fee_per_gas: Some(body.max_priority_fee_per_gas),
213                    ..Default::default()
214                }
215            );
216            let roundtrip: FoundryTransactionRequest =
217                serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
218            assert_eq!(request, roundtrip);
219        }
220    }
221    #[test]
222    fn eip8130_conversion_preserves_supplied_encoding() {
223        let envelope = FoundryTxEnvelope::Eip8130(Eip8130Signed::new(
224            TxEip8130::default(),
225            Bytes::new(),
226            Bytes::new(),
227        ));
228        let encoded = Bytes::from_static(b"already encoded");
229        let env =
230            BaseTransaction::<TxEnv>::from_encoded_tx(&envelope, Address::ZERO, encoded.clone());
231        assert_eq!(env.enveloped_tx, Some(encoded));
232    }
233}