Skip to main content

mega_evme/common/
tx_override.rs

1//! Transaction override support for mega-evme replay command.
2//!
3//! This module provides the ability to override transaction fields when replaying
4//! transactions from RPC.
5
6use std::cell::RefCell;
7
8use alloy_eips::{Encodable2718, Typed2718};
9use alloy_primitives::{Address, Bytes, TxHash, U256};
10use clap::Args;
11use mega_evm::{
12    alloy_evm::{IntoTxEnv, RecoveredTx},
13    MegaTransaction, MegaTransactionExt,
14};
15
16use super::{load_hex, parse_ether_value, Result};
17
18// Thread-local storage for input override (Bytes is not Copy, so we can't store it in TxOverrides)
19thread_local! {
20    static INPUT_OVERRIDE: RefCell<Option<Bytes>> = const { RefCell::new(None) };
21}
22
23/// Transaction override arguments for the replay command.
24#[derive(Args, Debug, Clone, Default)]
25#[command(next_help_heading = "Transaction Override Options")]
26pub struct TxOverrideArgs {
27    /// Override transaction gas limit
28    #[arg(long = "override.gas-limit", visible_aliases = ["override.gaslimit"], value_name = "GAS")]
29    pub gas_limit: Option<u64>,
30
31    /// Override transaction value.
32    /// VALUE can be: plain number (wei), or number with suffix (ether, gwei, wei).
33    /// Examples: `--override.value 1ether`, `--override.value 100gwei`
34    #[arg(long = "override.value", value_name = "VALUE")]
35    pub value: Option<String>,
36
37    /// Override transaction input data (hex string)
38    #[arg(long = "override.input", visible_aliases = ["override.data"], value_name = "HEX")]
39    pub input: Option<String>,
40
41    /// Override transaction input data from file (hex content)
42    #[arg(long = "override.input-file", visible_aliases = ["override.data-file"], value_name = "FILE")]
43    pub input_file: Option<String>,
44}
45
46impl TxOverrideArgs {
47    /// Returns true if any override is set.
48    pub fn has_overrides(&self) -> bool {
49        self.gas_limit.is_some() ||
50            self.value.is_some() ||
51            self.input.is_some() ||
52            self.input_file.is_some()
53    }
54
55    /// Wraps a transaction with overrides.
56    pub fn wrap<T: Copy>(&self, tx: T) -> Result<OverriddenTx<T>> {
57        // Parse and store input override in thread-local if present
58        let has_input_override =
59            if let Some(bytes) = load_hex(self.input.clone(), self.input_file.clone())? {
60                INPUT_OVERRIDE.with(|cell| cell.borrow_mut().replace(bytes));
61                true
62            } else {
63                INPUT_OVERRIDE.with(|cell| cell.borrow_mut().take());
64                false
65            };
66
67        Ok(OverriddenTx {
68            inner: tx,
69            overrides: TxOverrides {
70                gas_limit: self.gas_limit,
71                value: self.value.as_deref().map(parse_ether_value).transpose()?,
72                has_input_override,
73            },
74        })
75    }
76}
77
78/// Parsed transaction overrides.
79///
80/// All fields must be `Copy` because `OverriddenTx<T>` must implement `Copy`
81/// (required by block executor's `run_transaction`). The input override is stored
82/// in a thread-local (`INPUT_OVERRIDE`) since `Bytes` is not `Copy`.
83#[derive(Debug, Clone, Copy, Default)]
84pub struct TxOverrides {
85    /// Override for gas limit.
86    pub gas_limit: Option<u64>,
87    /// Override for value.
88    pub value: Option<U256>,
89    /// Whether input data should be overridden (actual data in thread-local).
90    pub has_input_override: bool,
91}
92
93impl TxOverrides {
94    /// Apply overrides to a [`MegaTransaction`].
95    pub fn apply(&self, tx: &mut MegaTransaction) {
96        if let Some(gas_limit) = self.gas_limit {
97            tx.base.gas_limit = gas_limit;
98        }
99        if let Some(value) = self.value {
100            tx.base.value = value;
101        }
102        if self.has_input_override {
103            if let Some(input) = INPUT_OVERRIDE.with(|cell| cell.borrow().clone()) {
104                tx.base.data = input;
105            }
106        }
107    }
108}
109
110/// A wrapper that applies overrides when converting to `TxEnv`.
111///
112/// This wrapper implements all the required traits by delegating to the inner
113/// transaction, but intercepts `IntoTxEnv` to apply overrides.
114#[derive(Debug, Clone, Copy)]
115pub struct OverriddenTx<T: Copy> {
116    inner: T,
117    overrides: TxOverrides,
118}
119
120impl<T: Copy> OverriddenTx<T> {
121    /// Get a reference to the inner transaction.
122    pub fn inner(&self) -> &T {
123        &self.inner
124    }
125}
126
127// Implement IntoTxEnv - this is where we apply the overrides
128impl<T: IntoTxEnv<MegaTransaction> + Copy> IntoTxEnv<MegaTransaction> for OverriddenTx<T> {
129    fn into_tx_env(self) -> MegaTransaction {
130        let mut tx = self.inner.into_tx_env();
131        self.overrides.apply(&mut tx);
132        tx
133    }
134}
135
136// Delegate RecoveredTx to inner
137impl<Tx, T: RecoveredTx<Tx> + Copy> RecoveredTx<Tx> for OverriddenTx<T> {
138    fn tx(&self) -> &Tx {
139        self.inner.tx()
140    }
141
142    fn signer(&self) -> &Address {
143        self.inner.signer()
144    }
145}
146
147// Delegate Typed2718 to inner (required as the `Encodable2718` supertrait below).
148impl<T: Typed2718 + Copy> Typed2718 for OverriddenTx<T> {
149    fn ty(&self) -> u8 {
150        self.inner.ty()
151    }
152}
153
154// Delegate Encodable2718 to inner. Overrides only affect the `TxEnv` produced by `IntoTxEnv`, not
155// the EIP-2718 encoding, so the encoded size reflects the original transaction — matching the
156// `tx_size`/`da_size` the executor charged before override support existed.
157impl<T: Encodable2718 + Copy> Encodable2718 for OverriddenTx<T> {
158    fn type_flag(&self) -> Option<u8> {
159        self.inner.type_flag()
160    }
161
162    fn encode_2718_len(&self) -> usize {
163        self.inner.encode_2718_len()
164    }
165
166    fn encode_2718(&self, out: &mut dyn alloy_primitives::bytes::BufMut) {
167        self.inner.encode_2718(out)
168    }
169}
170
171// Delegate MegaTransactionExt to inner so `OverriddenTx` is accepted by `run_transaction`.
172// `tx_size`/`estimated_da_size` fall back to the trait defaults (recomputed from the delegated
173// encoding above); only `tx_hash` needs explicit forwarding.
174impl<T: MegaTransactionExt + Copy> MegaTransactionExt for OverriddenTx<T> {
175    fn tx_hash(&self) -> TxHash {
176        self.inner.tx_hash()
177    }
178}