1use alloy_primitives::{address, Address, Bytes, Signature, B256, U256};
4use clap::Args;
5use mega_evm::{
6 alloy_consensus::{
7 transaction::SignerRecoverable, Sealed, Signed, Transaction as _, TxEip1559, TxEip2930,
8 TxEip7702, TxLegacy,
9 },
10 alloy_eips::{
11 eip2930::{AccessList, AccessListItem},
12 eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization, SignedAuthorization},
13 Decodable2718, Encodable2718, Typed2718 as _,
14 },
15 op_alloy_consensus::{OpTxEnvelope, TxDeposit},
16 op_revm::transaction::deposit::DepositTransactionParts,
17 revm::{context::tx::TxEnv, primitives::TxKind},
18 Either, MegaTransaction, MegaTxEnvelope, MegaTxType,
19};
20use tracing::{debug, trace};
21
22use super::{load_hex, parse_ether_value, EvmeError, Result};
23
24pub const DEFAULT_SENDER: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
26
27#[derive(Args, Debug, Clone)]
29#[command(next_help_heading = "Transaction Options")]
30pub struct TxArgs {
31 #[arg(long = "tx-type", visible_aliases = ["type", "ty"])]
33 pub tx_type: Option<u8>,
34
35 #[arg(long = "gas", visible_aliases = ["gas-limit"])]
37 pub gas: Option<u64>,
38
39 #[arg(long = "basefee", visible_aliases = ["gas-price", "price", "base-fee"])]
41 pub basefee: Option<u64>,
42
43 #[arg(long = "priority-fee", visible_aliases = ["priorityfee", "tip"])]
45 pub priority_fee: Option<u64>,
46
47 #[arg(long = "sender", visible_aliases = ["from"])]
49 pub sender: Option<Address>,
50
51 #[arg(long = "receiver", visible_aliases = ["to"])]
53 pub receiver: Option<Address>,
54
55 #[arg(long = "nonce")]
57 pub nonce: Option<u64>,
58
59 #[arg(long = "create")]
61 pub create: Option<bool>,
62
63 #[arg(long = "value")]
67 pub value: Option<String>,
68
69 #[arg(long = "input", visible_aliases = ["data"])]
71 pub input: Option<String>,
72
73 #[arg(long = "inputfile", visible_aliases = ["datafile", "input-file", "data-file"])]
75 pub inputfile: Option<String>,
76
77 #[arg(long = "source-hash", visible_aliases = ["sourcehash"], value_name = "HASH")]
79 pub source_hash: Option<B256>,
80
81 #[arg(long = "mint")]
83 pub mint: Option<u128>,
84
85 #[arg(long = "auth", visible_aliases = ["authorization"], value_name = "AUTH")]
87 pub auth: Vec<String>,
88
89 #[arg(long = "access", visible_aliases = ["accesslist", "access-list"], value_name = "ACCESS")]
91 pub access: Vec<String>,
92}
93
94impl TxArgs {
95 pub fn validate(&self) -> Result<()> {
104 let tx_type = self.mega_tx_type()?;
105
106 if tx_type != MegaTxType::Deposit && (self.source_hash.is_some() || self.mint.is_some()) {
108 return Err(EvmeError::InvalidInput(
109 "--source-hash and --mint are only valid for deposit transactions (--tx-type 126)"
110 .to_string(),
111 ));
112 }
113 if tx_type == MegaTxType::Deposit && self.source_hash.is_none() {
114 return Err(EvmeError::InvalidInput(
115 "--source-hash is required for deposit transactions (--tx-type 126)".to_string(),
116 ));
117 }
118
119 if matches!(tx_type, MegaTxType::Legacy | MegaTxType::Eip2930) &&
121 self.priority_fee.is_some()
122 {
123 return Err(EvmeError::InvalidInput(
124 "--priority-fee is not valid for legacy (0) or EIP-2930 (1) transactions"
125 .to_string(),
126 ));
127 }
128
129 if self.create() && self.receiver.is_some() {
131 return Err(EvmeError::InvalidInput(
132 "--receiver must not be set when --create is specified".to_string(),
133 ));
134 }
135
136 if tx_type != MegaTxType::Eip7702 && !self.auth.is_empty() {
138 return Err(EvmeError::InvalidInput(
139 "--auth is only valid for EIP-7702 transactions (--tx-type 4)".to_string(),
140 ));
141 }
142
143 if !self.access.is_empty() &&
146 !matches!(tx_type, MegaTxType::Eip2930 | MegaTxType::Eip1559 | MegaTxType::Eip7702)
147 {
148 return Err(EvmeError::InvalidInput(
149 "--access is only valid for EIP-2930 (1), EIP-1559 (2), or EIP-7702 (4) transactions"
150 .to_string(),
151 ));
152 }
153
154 Ok(())
155 }
156
157 pub(crate) fn parse_authorization_list(
164 &self,
165 chain_id: u64,
166 ) -> Result<Vec<RecoveredAuthorization>> {
167 self.auth.iter().map(|s| Self::parse_authorization(s, chain_id)).collect()
168 }
169
170 fn parse_auth_fields(s: &str, chain_id: u64) -> Result<(Authorization, Address)> {
174 let parts: Vec<&str> = s.split("->").collect();
176 if parts.len() != 2 {
177 return Err(EvmeError::InvalidInput(format!(
178 "Invalid authorization format '{}'. Expected: AUTHORITY:NONCE->DELEGATION",
179 s
180 )));
181 }
182
183 let delegation: Address = parts[1].trim().parse().map_err(|_| {
184 EvmeError::InvalidInput(format!("Invalid delegation address: {}", parts[1].trim()))
185 })?;
186
187 let auth_parts: Vec<&str> = parts[0].split(':').collect();
189 if auth_parts.len() != 2 {
190 return Err(EvmeError::InvalidInput(format!(
191 "Invalid authorization format '{}'. Expected: AUTHORITY:NONCE->DELEGATION",
192 s
193 )));
194 }
195
196 let authority: Address = auth_parts[0].trim().parse().map_err(|_| {
197 EvmeError::InvalidInput(format!("Invalid authority address: {}", auth_parts[0].trim()))
198 })?;
199
200 let nonce: u64 = if auth_parts[1].trim().starts_with("0x") {
201 u64::from_str_radix(auth_parts[1].trim().trim_start_matches("0x"), 16).map_err(
202 |_| EvmeError::InvalidInput(format!("Invalid nonce: {}", auth_parts[1].trim())),
203 )?
204 } else {
205 auth_parts[1].trim().parse().map_err(|_| {
206 EvmeError::InvalidInput(format!("Invalid nonce: {}", auth_parts[1].trim()))
207 })?
208 };
209
210 let auth = Authorization { chain_id: U256::from(chain_id), address: delegation, nonce };
211 Ok((auth, authority))
212 }
213
214 fn parse_authorization(s: &str, chain_id: u64) -> Result<RecoveredAuthorization> {
216 let (auth, authority) = Self::parse_auth_fields(s, chain_id)?;
217
218 trace!(string = %s, chain_id = %chain_id, authority = %authority, delegation = %auth.address, nonce = %auth.nonce, "Parsed authorization");
219 Ok(RecoveredAuthorization::new_unchecked(auth, RecoveredAuthority::Valid(authority)))
220 }
221
222 pub(crate) fn parse_access_list(&self) -> Result<AccessList> {
228 let items: Result<Vec<AccessListItem>> =
229 self.access.iter().map(|s| Self::parse_access_list_item(s)).collect();
230 Ok(AccessList(items?))
231 }
232
233 fn parse_access_list_item(s: &str) -> Result<AccessListItem> {
235 if let Some((addr_str, keys_str)) = s.split_once(':') {
237 let address: Address = addr_str.trim().parse().map_err(|_| {
238 EvmeError::InvalidInput(format!("Invalid access list address: {}", addr_str.trim()))
239 })?;
240
241 let storage_keys: Result<Vec<B256>> = keys_str
242 .split(',')
243 .map(|k| {
244 k.trim().parse().map_err(|_| {
245 EvmeError::InvalidInput(format!("Invalid storage key: {}", k.trim()))
246 })
247 })
248 .collect();
249
250 trace!(string = %s, address = %address, storage_keys = ?storage_keys, "Parsed access list item");
251 Ok(AccessListItem { address, storage_keys: storage_keys? })
252 } else {
253 let address: Address = s.trim().parse().map_err(|_| {
255 EvmeError::InvalidInput(format!("Invalid access list address: {}", s.trim()))
256 })?;
257
258 trace!(string = %s, address = %address, "Parsed access list item");
259 Ok(AccessListItem { address, storage_keys: Vec::new() })
260 }
261 }
262
263 pub fn gas(&self) -> u64 {
265 self.gas.unwrap_or(10_000_000)
266 }
267
268 pub fn basefee(&self) -> u64 {
270 self.basefee.unwrap_or(0)
271 }
272
273 pub fn sender(&self) -> Address {
275 self.sender.unwrap_or(DEFAULT_SENDER)
276 }
277
278 pub fn create(&self) -> bool {
280 self.create.unwrap_or(false)
281 }
282
283 pub fn receiver(&self) -> Address {
285 self.receiver.unwrap_or_default()
286 }
287
288 pub fn value(&self) -> Result<U256> {
290 self.value.as_deref().map(parse_ether_value).transpose().map(|v| v.unwrap_or_default())
291 }
292
293 pub fn tx_type(&self) -> u8 {
295 self.tx_type.unwrap_or(0)
296 }
297
298 pub fn mega_tx_type(&self) -> Result<MegaTxType> {
300 let ty = self.tx_type();
301 match ty {
302 0 => Ok(MegaTxType::Legacy),
303 1 => Ok(MegaTxType::Eip2930),
304 2 => Ok(MegaTxType::Eip1559),
305 4 => Ok(MegaTxType::Eip7702),
306 126 => Ok(MegaTxType::Deposit),
307 _ => Err(EvmeError::UnsupportedTxType(ty)),
308 }
309 }
310
311 pub fn create_tx_env(&self, chain_id: u64) -> Result<TxEnv> {
317 self.validate()?;
318
319 let data = load_hex(self.input.clone(), self.inputfile.clone())?.unwrap_or_default();
320 let kind = if self.create() { TxKind::Create } else { TxKind::Call(self.receiver()) };
321 let authorization_list =
322 self.parse_authorization_list(chain_id)?.into_iter().map(Either::Right).collect();
323 let access_list = self.parse_access_list()?;
324
325 let tx = TxEnv {
326 caller: self.sender(),
327 gas_price: self.basefee() as u128,
328 gas_priority_fee: self.priority_fee.map(|pf| pf as u128),
329 blob_hashes: Vec::new(),
330 max_fee_per_blob_gas: 0,
331 tx_type: self.tx_type(),
332 gas_limit: self.gas(),
333 data,
334 nonce: self.nonce.unwrap_or(0),
335 value: self.value()?,
336 access_list,
337 authorization_list,
338 kind,
339 chain_id: Some(chain_id),
340 };
341 debug!(tx = ?tx, "Creating TxEnv");
342 Ok(tx)
343 }
344
345 pub fn create_tx(&self, chain_id: u64) -> Result<MegaTransaction> {
349 let tx_env = self.create_tx_env(chain_id)?;
350 let envelope = create_fake_envelope(&tx_env)?;
351 let mut tx = MegaTransaction::new(tx_env);
352 tx.enveloped_tx = Some(Bytes::from(envelope.encoded_2718()));
353
354 if self.mega_tx_type()? == MegaTxType::Deposit {
356 tx.deposit = DepositTransactionParts {
357 source_hash: self.source_hash.unwrap_or(B256::ZERO),
358 mint: self.mint,
359 is_system_transaction: false,
360 };
361 }
362
363 Ok(tx)
364 }
365}
366
367#[derive(Debug)]
369pub struct DecodedRawTx {
370 pub tx_env: TxEnv,
372 pub raw_bytes: Bytes,
374 pub deposit: Option<(B256, Option<u128>, bool)>,
377}
378
379impl DecodedRawTx {
380 pub fn from_raw(raw_bytes: impl Into<Bytes>) -> Result<Self> {
385 let raw_bytes = raw_bytes.into();
386 let envelope = OpTxEnvelope::decode_2718(&mut &raw_bytes[..]).map_err(|e| {
387 EvmeError::InvalidInput(format!("Failed to decode raw transaction: {e}"))
388 })?;
389
390 let caller = envelope
391 .recover_signer()
392 .map_err(|e| EvmeError::InvalidInput(format!("Failed to recover signer: {e}")))?;
393
394 let deposit = envelope.as_deposit().map(|d| {
395 let mint = if d.mint == 0 { None } else { Some(d.mint) };
396 (d.source_hash, mint, d.is_system_transaction)
397 });
398
399 let decoded_chain_id = envelope.chain_id();
400 let (gas_price, gas_priority_fee) = match envelope {
401 OpTxEnvelope::Legacy(_) | OpTxEnvelope::Eip2930(_) => {
402 (envelope.gas_price().unwrap_or(0), None)
403 }
404 OpTxEnvelope::Eip1559(_) | OpTxEnvelope::Eip7702(_) => {
405 (envelope.max_fee_per_gas(), envelope.max_priority_fee_per_gas())
406 }
407 OpTxEnvelope::Deposit(_) => (0, None),
408 };
409
410 let authorization_list = envelope
411 .authorization_list()
412 .map(|list| list.iter().map(|sa| Either::Right(sa.clone().into_recovered())).collect())
413 .unwrap_or_default();
414
415 let tx_env = TxEnv {
416 caller,
417 gas_price,
418 gas_priority_fee,
419 blob_hashes: Vec::new(),
420 max_fee_per_blob_gas: 0,
421 tx_type: envelope.ty(),
422 gas_limit: envelope.gas_limit(),
423 data: envelope.input().clone(),
424 nonce: envelope.nonce(),
425 value: envelope.value(),
426 access_list: envelope.access_list().cloned().unwrap_or_default(),
427 authorization_list,
428 kind: envelope.kind(),
429 chain_id: decoded_chain_id,
430 };
431
432 Ok(Self { tx_env, raw_bytes, deposit })
433 }
434
435 pub fn override_tx_env(mut self, tx_args: &TxArgs) -> Result<Self> {
440 if let Some(tx_type) = tx_args.tx_type {
441 self.tx_env.tx_type = tx_type;
442 }
443 if let Some(gas) = tx_args.gas {
444 self.tx_env.gas_limit = gas;
445 }
446 if let Some(basefee) = tx_args.basefee {
447 self.tx_env.gas_price = basefee as u128;
448 }
449 if let Some(priority_fee) = tx_args.priority_fee {
450 self.tx_env.gas_priority_fee = Some(priority_fee as u128);
451 }
452 if let Some(sender) = tx_args.sender {
453 self.tx_env.caller = sender;
454 }
455 if let Some(ref value) = tx_args.value {
456 self.tx_env.value = parse_ether_value(value)?;
457 }
458 if let Some(nonce) = tx_args.nonce {
459 self.tx_env.nonce = nonce;
460 }
461 if tx_args.input.is_some() || tx_args.inputfile.is_some() {
462 self.tx_env.data =
463 load_hex(tx_args.input.clone(), tx_args.inputfile.clone())?.unwrap_or_default();
464 }
465 if tx_args.create.unwrap_or(false) {
466 self.tx_env.kind = TxKind::Create;
467 } else if let Some(receiver) = tx_args.receiver {
468 self.tx_env.kind = TxKind::Call(receiver);
469 }
470 if !tx_args.access.is_empty() {
471 self.tx_env.access_list = tx_args.parse_access_list()?;
472 }
473 if !tx_args.auth.is_empty() {
474 let chain_id = self.tx_env.chain_id.unwrap_or(0);
475 self.tx_env.authorization_list = tx_args
476 .parse_authorization_list(chain_id)?
477 .into_iter()
478 .map(Either::Right)
479 .collect();
480 }
481 if let Some((ref mut source_hash, ref mut mint, _)) = self.deposit {
482 if let Some(sh) = tx_args.source_hash {
483 *source_hash = sh;
484 }
485 if tx_args.mint.is_some() {
486 *mint = tx_args.mint;
487 }
488 }
489 Ok(self)
490 }
491
492 pub fn into_tx(self) -> MegaTransaction {
496 let mut tx = MegaTransaction::new(self.tx_env);
497 tx.enveloped_tx = Some(self.raw_bytes);
498 if let Some((source_hash, mint, is_system_transaction)) = self.deposit {
499 tx.deposit = DepositTransactionParts { source_hash, mint, is_system_transaction };
500 }
501 tx
502 }
503}
504
505fn create_fake_envelope(tx_env: &TxEnv) -> Result<MegaTxEnvelope> {
512 let dummy_sig = Signature::new(U256::from(1u64), U256::from(1u64), false);
513 let chain_id = tx_env.chain_id.unwrap_or(0);
514 let tx_type = MegaTxType::try_from(tx_env.tx_type)
515 .map_err(|_| EvmeError::UnsupportedTxType(tx_env.tx_type))?;
516
517 match tx_type {
518 MegaTxType::Legacy => {
519 let tx = TxLegacy {
520 chain_id: tx_env.chain_id,
521 nonce: tx_env.nonce,
522 gas_price: tx_env.gas_price,
523 gas_limit: tx_env.gas_limit,
524 to: tx_env.kind,
525 value: tx_env.value,
526 input: tx_env.data.clone(),
527 };
528 Ok(MegaTxEnvelope::Legacy(Signed::new_unchecked(tx, dummy_sig, Default::default())))
529 }
530 MegaTxType::Eip2930 => {
531 let tx = TxEip2930 {
532 chain_id,
533 nonce: tx_env.nonce,
534 gas_price: tx_env.gas_price,
535 gas_limit: tx_env.gas_limit,
536 to: tx_env.kind,
537 value: tx_env.value,
538 access_list: tx_env.access_list.clone(),
539 input: tx_env.data.clone(),
540 };
541 Ok(MegaTxEnvelope::Eip2930(Signed::new_unchecked(tx, dummy_sig, Default::default())))
542 }
543 MegaTxType::Eip1559 => {
544 let tx = TxEip1559 {
545 chain_id,
546 nonce: tx_env.nonce,
547 gas_limit: tx_env.gas_limit,
548 max_fee_per_gas: tx_env.gas_price,
549 max_priority_fee_per_gas: tx_env.gas_priority_fee.unwrap_or(0),
550 to: tx_env.kind,
551 value: tx_env.value,
552 access_list: tx_env.access_list.clone(),
553 input: tx_env.data.clone(),
554 };
555 Ok(MegaTxEnvelope::Eip1559(Signed::new_unchecked(tx, dummy_sig, Default::default())))
556 }
557 MegaTxType::Eip7702 => {
558 let to = match tx_env.kind {
559 TxKind::Call(addr) => addr,
560 TxKind::Create => {
561 return Err(EvmeError::InvalidInput(
562 "EIP-7702 transactions cannot be contract creation".to_string(),
563 ));
564 }
565 };
566
567 let authorization_list: Vec<SignedAuthorization> = tx_env
568 .authorization_list
569 .iter()
570 .map(|either| match either {
571 Either::Left(signed) => signed.clone(),
572 Either::Right(recovered) => SignedAuthorization::new_unchecked(
573 Authorization::clone(recovered),
574 0,
575 U256::from(1u64),
576 U256::from(1u64),
577 ),
578 })
579 .collect();
580
581 let tx = TxEip7702 {
582 chain_id,
583 nonce: tx_env.nonce,
584 gas_limit: tx_env.gas_limit,
585 max_fee_per_gas: tx_env.gas_price,
586 max_priority_fee_per_gas: tx_env.gas_priority_fee.unwrap_or(0),
587 to,
588 value: tx_env.value,
589 access_list: tx_env.access_list.clone(),
590 authorization_list,
591 input: tx_env.data.clone(),
592 };
593 Ok(MegaTxEnvelope::Eip7702(Signed::new_unchecked(tx, dummy_sig, Default::default())))
594 }
595 MegaTxType::Deposit => {
596 let tx = TxDeposit {
597 source_hash: B256::ZERO,
598 from: tx_env.caller,
599 to: tx_env.kind,
600 mint: 0,
601 value: tx_env.value,
602 gas_limit: tx_env.gas_limit,
603 is_system_transaction: false,
604 input: tx_env.data.clone(),
605 };
606 Ok(MegaTxEnvelope::Deposit(Sealed::new_unchecked(tx, B256::ZERO)))
607 }
608 }
609}