Skip to main content

txgate_chain/
ethereum.rs

1//! Ethereum transaction parser implementation.
2//!
3//! This module provides the [`EthereumParser`] struct that implements the [`Chain`]
4//! trait for parsing Ethereum transactions.
5//!
6//! # Supported Transaction Types
7//!
8//! - **Legacy (Type 0)**: Pre-EIP-2718 transactions, RLP-encoded directly
9//! - **EIP-2930 (Type 1)**: Access list transactions, prefixed with `0x01`
10//! - **EIP-1559 (Type 2)**: Dynamic fee transactions, prefixed with `0x02`
11//!
12//! # Example
13//!
14//! ```
15//! use txgate_chain::{Chain, EthereumParser};
16//!
17//! let parser = EthereumParser::new();
18//! assert_eq!(parser.id(), "ethereum");
19//!
20//! // Parse a raw transaction (hex-decoded bytes)
21//! // let parsed = parser.parse(&raw_tx_bytes)?;
22//! ```
23//!
24//! # Transaction Hash
25//!
26//! The transaction hash is computed as the Keccak-256 hash of the raw transaction bytes.
27//! This includes the type prefix for typed transactions (EIP-2718+).
28//!
29//! # Chain ID Extraction
30//!
31//! - For typed transactions (EIP-2930, EIP-1559): Chain ID is explicitly in the transaction
32//! - For legacy transactions with EIP-155: `chain_id = (v - 35) / 2`
33//! - For pre-EIP-155 legacy transactions (v = 27 or 28): Assumes mainnet (`chain_id` = 1)
34
35use alloy_primitives::{keccak256, Address, U256};
36use txgate_core::{error::ParseError, ParsedTx, TxType};
37use txgate_crypto::CurveType;
38
39use crate::erc20::{parse_erc20_call, Erc20Call};
40use crate::rlp::{
41    decode_bytes, decode_list, decode_optional_address, decode_u256, decode_u64, detect_tx_type,
42    typed_tx_payload,
43};
44use crate::Chain;
45
46/// Ethereum transaction parser.
47///
48/// This struct implements the [`Chain`] trait for parsing Ethereum transactions
49/// into the unified [`ParsedTx`] format.
50///
51/// # Supported Transaction Types
52///
53/// - Legacy transactions (type 0 or no type prefix)
54/// - EIP-2930 transactions (type 1)
55/// - EIP-1559 transactions (type 2)
56///
57/// # Example
58///
59/// ```
60/// use txgate_chain::{Chain, EthereumParser};
61///
62/// let parser = EthereumParser::new();
63///
64/// // Check chain ID and curve
65/// assert_eq!(parser.id(), "ethereum");
66/// assert_eq!(parser.curve(), txgate_crypto::CurveType::Secp256k1);
67///
68/// // Check supported versions
69/// assert!(parser.supports_version(0));
70/// assert!(parser.supports_version(1));
71/// assert!(parser.supports_version(2));
72/// assert!(!parser.supports_version(3)); // EIP-4844 not yet supported
73/// ```
74#[derive(Debug, Clone, Copy, Default)]
75pub struct EthereumParser;
76
77impl EthereumParser {
78    /// Create a new Ethereum parser instance.
79    ///
80    /// # Example
81    ///
82    /// ```
83    /// use txgate_chain::EthereumParser;
84    ///
85    /// let parser = EthereumParser::new();
86    /// ```
87    #[must_use]
88    pub const fn new() -> Self {
89        Self
90    }
91
92    /// Parse a legacy transaction (type 0 or no type prefix).
93    ///
94    /// Legacy transactions are RLP-encoded as:
95    /// `[nonce, gasPrice, gasLimit, to, value, data, v, r, s]`
96    ///
97    /// # Chain ID Extraction (EIP-155)
98    ///
99    /// - If `v >= 35`: `chain_id = (v - 35) / 2`
100    /// - If `v = 27` or `v = 28`: Pre-EIP-155, assumes mainnet (`chain_id` = 1)
101    fn parse_legacy(raw: &[u8]) -> Result<ParsedTx, ParseError> {
102        // For true legacy transactions, the hash source is the same as the RLP payload
103        Self::parse_legacy_with_hash_source(raw, raw)
104    }
105
106    /// Parse a legacy transaction with a separate hash source.
107    ///
108    /// This is used for type 0 transactions where the hash must be computed
109    /// over the full raw bytes (including type prefix), but the RLP payload
110    /// is without the type prefix.
111    ///
112    /// # Arguments
113    ///
114    /// * `hash_source` - The bytes to compute the transaction hash from (full raw for typed txs)
115    /// * `rlp_payload` - The RLP-encoded transaction data (without type prefix for typed txs)
116    fn parse_legacy_with_hash_source(
117        hash_source: &[u8],
118        rlp_payload: &[u8],
119    ) -> Result<ParsedTx, ParseError> {
120        // Decode the RLP list from the payload
121        let items = decode_list(rlp_payload)?;
122
123        // Legacy transaction:
124        // - Signed: 9 items [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
125        // - Unsigned pre-EIP-155: 6 items [nonce, gasPrice, gasLimit, to, value, data]
126        // - Unsigned EIP-155: 9 items [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
127        let is_unsigned = items.len() == 6;
128        if items.len() != 9 && items.len() != 6 {
129            return Err(ParseError::MalformedTransaction {
130                context: format!(
131                    "legacy transaction expected 6 or 9 items, got {}",
132                    items.len()
133                ),
134            });
135        }
136
137        // Extract fields using safe indexing
138        let nonce_bytes = items
139            .first()
140            .ok_or_else(|| ParseError::MalformedTransaction {
141                context: "missing nonce field".to_string(),
142            })?;
143        let to_bytes = items
144            .get(3)
145            .ok_or_else(|| ParseError::MalformedTransaction {
146                context: "missing to field".to_string(),
147            })?;
148        let value_bytes = items
149            .get(4)
150            .ok_or_else(|| ParseError::MalformedTransaction {
151                context: "missing value field".to_string(),
152            })?;
153        let data_bytes = items
154            .get(5)
155            .ok_or_else(|| ParseError::MalformedTransaction {
156                context: "missing data field".to_string(),
157            })?;
158
159        // Decode nonce
160        let nonce = decode_u64(nonce_bytes)?;
161
162        // Decode recipient (can be empty for contract deployments)
163        let recipient = decode_optional_address(to_bytes)?;
164
165        // Decode value
166        let amount = decode_u256(value_bytes)?;
167
168        // Decode data
169        let data = decode_bytes(data_bytes)?;
170
171        // Extract chain_id: from v field (signed) or default to 1 (unsigned pre-EIP-155)
172        let chain_id = if is_unsigned {
173            // Unsigned pre-EIP-155: no chain_id available, default to mainnet
174            1
175        } else {
176            let v_bytes = items
177                .get(6)
178                .ok_or_else(|| ParseError::MalformedTransaction {
179                    context: "missing v field".to_string(),
180                })?;
181            let v = decode_u64(v_bytes)?;
182
183            if v >= 35 {
184                // EIP-155: chain_id = (v - 35) / 2
185                (v - 35) / 2
186            } else if v == 27 || v == 28 {
187                // Pre-EIP-155: assume mainnet
188                1
189            } else {
190                // Could be EIP-155 unsigned where v == chain_id
191                // (items[6] = chain_id, items[7] = 0, items[8] = 0)
192                v
193            }
194        };
195
196        // Check for ERC-20 token call
197        let erc20_info = recipient
198            .as_ref()
199            .and_then(|addr| Self::analyze_erc20(addr, &data));
200
201        // Determine transaction type (ERC-20 detection takes precedence)
202        let (final_tx_type, final_recipient, final_amount, token_address) =
203            erc20_info.as_ref().map_or_else(
204                || {
205                    (
206                        Self::determine_tx_type(recipient.as_ref(), &data, &amount),
207                        recipient.map(|addr| format!("{addr}")),
208                        Some(amount),
209                        None,
210                    )
211                },
212                |info| {
213                    (
214                        info.tx_type,
215                        Some(format!("{}", info.recipient)),
216                        Some(info.amount),
217                        Some(format!("{}", info.token_address)),
218                    )
219                },
220            );
221
222        // Compute transaction hash from the hash source (includes type prefix for typed txs)
223        let hash = keccak256(hash_source);
224
225        let mut metadata = std::collections::HashMap::new();
226        if is_unsigned {
227            metadata.insert("unsigned".to_string(), serde_json::Value::Bool(true));
228        }
229
230        Ok(ParsedTx {
231            hash: hash.into(),
232            recipient: final_recipient,
233            amount: final_amount,
234            token: None, // Token symbol lookup not implemented yet
235            token_address,
236            tx_type: final_tx_type,
237            chain: "ethereum".to_string(),
238            nonce: Some(nonce),
239            chain_id: Some(chain_id),
240            metadata,
241        })
242    }
243
244    /// Parse an EIP-2930 transaction (type 1).
245    ///
246    /// EIP-2930 transactions are encoded as:
247    /// `0x01 || RLP([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS])`
248    fn parse_eip2930(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
249        // Decode the RLP list from the payload (without type byte)
250        let items = decode_list(payload)?;
251
252        // EIP-2930: signed has 11 items, unsigned has 8 items
253        let is_unsigned = items.len() == 8;
254        if items.len() != 11 && items.len() != 8 {
255            return Err(ParseError::MalformedTransaction {
256                context: format!(
257                    "EIP-2930 transaction expected 8 or 11 items, got {}",
258                    items.len()
259                ),
260            });
261        }
262
263        // Extract fields using safe indexing
264        let chain_id_bytes = items
265            .first()
266            .ok_or_else(|| ParseError::MalformedTransaction {
267                context: "missing chainId field".to_string(),
268            })?;
269        let nonce_bytes = items
270            .get(1)
271            .ok_or_else(|| ParseError::MalformedTransaction {
272                context: "missing nonce field".to_string(),
273            })?;
274        let to_bytes = items
275            .get(4)
276            .ok_or_else(|| ParseError::MalformedTransaction {
277                context: "missing to field".to_string(),
278            })?;
279        let value_bytes = items
280            .get(5)
281            .ok_or_else(|| ParseError::MalformedTransaction {
282                context: "missing value field".to_string(),
283            })?;
284        let data_bytes = items
285            .get(6)
286            .ok_or_else(|| ParseError::MalformedTransaction {
287                context: "missing data field".to_string(),
288            })?;
289
290        // Decode fields
291        let chain_id = decode_u64(chain_id_bytes)?;
292        let nonce = decode_u64(nonce_bytes)?;
293        let recipient = decode_optional_address(to_bytes)?;
294        let amount = decode_u256(value_bytes)?;
295        let data = decode_bytes(data_bytes)?;
296
297        // Check for ERC-20 token call
298        let erc20_info = recipient
299            .as_ref()
300            .and_then(|addr| Self::analyze_erc20(addr, &data));
301
302        // Determine transaction type (ERC-20 detection takes precedence)
303        let (final_tx_type, final_recipient, final_amount, token_address) =
304            erc20_info.as_ref().map_or_else(
305                || {
306                    (
307                        Self::determine_tx_type(recipient.as_ref(), &data, &amount),
308                        recipient.map(|addr| format!("{addr}")),
309                        Some(amount),
310                        None,
311                    )
312                },
313                |info| {
314                    (
315                        info.tx_type,
316                        Some(format!("{}", info.recipient)),
317                        Some(info.amount),
318                        Some(format!("{}", info.token_address)),
319                    )
320                },
321            );
322
323        // Compute transaction hash (includes type byte)
324        let hash = keccak256(raw);
325
326        Ok(ParsedTx {
327            hash: hash.into(),
328            recipient: final_recipient,
329            amount: final_amount,
330            token: None, // Token symbol lookup not implemented yet
331            token_address,
332            tx_type: final_tx_type,
333            chain: "ethereum".to_string(),
334            nonce: Some(nonce),
335            chain_id: Some(chain_id),
336            metadata: {
337                let mut m = std::collections::HashMap::new();
338                if is_unsigned {
339                    m.insert("unsigned".to_string(), serde_json::Value::Bool(true));
340                }
341                m
342            },
343        })
344    }
345
346    /// Parse an EIP-1559 transaction (type 2).
347    ///
348    /// EIP-1559 transactions are encoded as:
349    /// `0x02 || RLP([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])`
350    fn parse_eip1559(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
351        // Decode the RLP list from the payload (without type byte)
352        let items = decode_list(payload)?;
353
354        // EIP-1559: signed has 12 items, unsigned has 9 items
355        let is_unsigned = items.len() == 9;
356        if items.len() != 12 && items.len() != 9 {
357            return Err(ParseError::MalformedTransaction {
358                context: format!(
359                    "EIP-1559 transaction expected 9 or 12 items, got {}",
360                    items.len()
361                ),
362            });
363        }
364
365        // Extract fields using safe indexing
366        let chain_id_bytes = items
367            .first()
368            .ok_or_else(|| ParseError::MalformedTransaction {
369                context: "missing chainId field".to_string(),
370            })?;
371        let nonce_bytes = items
372            .get(1)
373            .ok_or_else(|| ParseError::MalformedTransaction {
374                context: "missing nonce field".to_string(),
375            })?;
376        let to_bytes = items
377            .get(5)
378            .ok_or_else(|| ParseError::MalformedTransaction {
379                context: "missing to field".to_string(),
380            })?;
381        let value_bytes = items
382            .get(6)
383            .ok_or_else(|| ParseError::MalformedTransaction {
384                context: "missing value field".to_string(),
385            })?;
386        let data_bytes = items
387            .get(7)
388            .ok_or_else(|| ParseError::MalformedTransaction {
389                context: "missing data field".to_string(),
390            })?;
391
392        // Decode fields
393        let chain_id = decode_u64(chain_id_bytes)?;
394        let nonce = decode_u64(nonce_bytes)?;
395        let recipient = decode_optional_address(to_bytes)?;
396        let amount = decode_u256(value_bytes)?;
397        let data = decode_bytes(data_bytes)?;
398
399        // Check for ERC-20 token call
400        let erc20_info = recipient
401            .as_ref()
402            .and_then(|addr| Self::analyze_erc20(addr, &data));
403
404        // Determine transaction type (ERC-20 detection takes precedence)
405        let (final_tx_type, final_recipient, final_amount, token_address) =
406            erc20_info.as_ref().map_or_else(
407                || {
408                    (
409                        Self::determine_tx_type(recipient.as_ref(), &data, &amount),
410                        recipient.map(|addr| format!("{addr}")),
411                        Some(amount),
412                        None,
413                    )
414                },
415                |info| {
416                    (
417                        info.tx_type,
418                        Some(format!("{}", info.recipient)),
419                        Some(info.amount),
420                        Some(format!("{}", info.token_address)),
421                    )
422                },
423            );
424
425        // Compute transaction hash (includes type byte)
426        let hash = keccak256(raw);
427
428        Ok(ParsedTx {
429            hash: hash.into(),
430            recipient: final_recipient,
431            amount: final_amount,
432            token: None, // Token symbol lookup not implemented yet
433            token_address,
434            tx_type: final_tx_type,
435            chain: "ethereum".to_string(),
436            nonce: Some(nonce),
437            chain_id: Some(chain_id),
438            metadata: {
439                let mut m = std::collections::HashMap::new();
440                if is_unsigned {
441                    m.insert("unsigned".to_string(), serde_json::Value::Bool(true));
442                }
443                m
444            },
445        })
446    }
447
448    /// Determine the transaction type based on recipient, data, and amount.
449    ///
450    /// - `Deployment`: No recipient (contract creation)
451    /// - `ContractCall`: Has data (non-empty calldata)
452    /// - `Transfer`: Simple ETH transfer (has recipient, no data)
453    const fn determine_tx_type(
454        recipient: Option<&alloy_primitives::Address>,
455        data: &[u8],
456        _amount: &U256,
457    ) -> TxType {
458        if recipient.is_none() {
459            // No recipient = contract deployment
460            TxType::Deployment
461        } else if !data.is_empty() {
462            // Has data = contract call
463            TxType::ContractCall
464        } else {
465            // Simple ETH transfer
466            TxType::Transfer
467        }
468    }
469
470    /// Analyze transaction data and return ERC-20 specific information if applicable.
471    ///
472    /// This function checks if the transaction is an ERC-20 token call and extracts
473    /// the relevant information for enriching the `ParsedTx`.
474    ///
475    /// # Arguments
476    ///
477    /// * `contract_address` - The address of the contract being called (the token contract)
478    /// * `data` - The transaction calldata
479    ///
480    /// # Returns
481    ///
482    /// Returns `Some(Erc20Info)` if this is an ERC-20 call, `None` otherwise.
483    fn analyze_erc20(contract_address: &Address, data: &[u8]) -> Option<Erc20Info> {
484        let erc20_call = parse_erc20_call(data)?;
485
486        let (tx_type, recipient_addr) = match &erc20_call {
487            Erc20Call::Transfer { to, .. } | Erc20Call::TransferFrom { to, .. } => {
488                (TxType::TokenTransfer, Address::from_slice(to))
489            }
490            Erc20Call::Approve { spender, .. } => {
491                (TxType::TokenApproval, Address::from_slice(spender))
492            }
493        };
494
495        Some(Erc20Info {
496            tx_type,
497            token_address: *contract_address,
498            recipient: recipient_addr,
499            amount: *erc20_call.amount(),
500        })
501    }
502
503    /// Assemble a fully signed transaction from raw bytes and a 65-byte signature.
504    ///
505    /// Takes the original raw transaction bytes (signed or unsigned) and the
506    /// signature (`r[32] || s[32] || v[1]`) and returns the RLP-encoded signed
507    /// transaction ready for broadcast.
508    ///
509    /// # Arguments
510    ///
511    /// * `raw` - The raw transaction bytes (as originally passed to `parse()`)
512    /// * `signature` - 65-byte signature: `r(32) || s(32) || recovery_id(1)`
513    ///
514    /// # Returns
515    ///
516    /// The fully assembled, RLP-encoded signed transaction bytes.
517    ///
518    /// # Errors
519    ///
520    /// Returns [`ParseError::AssemblyFailed`] if:
521    /// - The signature is not exactly 65 bytes
522    /// - The transaction cannot be decoded
523    /// - RLP encoding of the signed transaction fails
524    pub fn assemble_signed(raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
525        use alloy_primitives::Signature;
526
527        if signature.len() != 65 {
528            return Err(ParseError::assembly_failed(format!(
529                "expected 65-byte signature, got {}",
530                signature.len()
531            )));
532        }
533
534        // Extract signature components
535        let r = U256::from_be_slice(
536            signature
537                .get(0..32)
538                .ok_or_else(|| ParseError::assembly_failed("signature too short for r"))?,
539        );
540        let s = U256::from_be_slice(
541            signature
542                .get(32..64)
543                .ok_or_else(|| ParseError::assembly_failed("signature too short for s"))?,
544        );
545        let v_byte = *signature
546            .get(64)
547            .ok_or_else(|| ParseError::assembly_failed("signature too short for v"))?;
548        if v_byte > 1 {
549            return Err(ParseError::assembly_failed(format!(
550                "invalid recovery id: expected 0 or 1, got {v_byte}"
551            )));
552        }
553        let v_parity = v_byte != 0;
554
555        let sig = Signature::new(r, s, v_parity);
556
557        // Detect tx type and assemble
558        match detect_tx_type(raw) {
559            None => {
560                // Legacy transaction
561                if !crate::rlp::is_list(raw) {
562                    return Err(ParseError::assembly_failed("not a valid RLP list"));
563                }
564                Self::assemble_legacy(raw, raw, &sig, None)
565            }
566            Some(0x00) => {
567                // Typed legacy (type 0)
568                let payload = typed_tx_payload(raw)?;
569                Self::assemble_legacy(raw, payload, &sig, None)
570            }
571            Some(0x01) => {
572                // EIP-2930
573                let payload = typed_tx_payload(raw)?;
574                Self::assemble_eip2930(payload, &sig)
575            }
576            Some(0x02) => {
577                // EIP-1559
578                let payload = typed_tx_payload(raw)?;
579                Self::assemble_eip1559(payload, &sig)
580            }
581            Some(ty) => Err(ParseError::assembly_failed(format!(
582                "unsupported transaction type: 0x{ty:02x}"
583            ))),
584        }
585    }
586
587    /// Assemble a signed legacy transaction.
588    fn assemble_legacy(
589        _raw: &[u8],
590        rlp_payload: &[u8],
591        sig: &alloy_primitives::Signature,
592        _type_prefix: Option<u8>,
593    ) -> Result<Vec<u8>, ParseError> {
594        use alloy_consensus::transaction::RlpEcdsaEncodableTx;
595        use alloy_consensus::TxLegacy;
596        use alloy_primitives::{Bytes, TxKind};
597
598        let items = decode_list(rlp_payload)?;
599
600        // Accept 6 (unsigned pre-EIP-155) or 9 (signed / unsigned EIP-155)
601        if items.len() != 6 && items.len() != 9 {
602            return Err(ParseError::assembly_failed(format!(
603                "legacy tx expected 6 or 9 items, got {}",
604                items.len()
605            )));
606        }
607
608        let nonce = decode_u64(
609            items
610                .first()
611                .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
612        )?;
613
614        let gas_price_u256 = decode_u256(
615            items
616                .get(1)
617                .ok_or_else(|| ParseError::assembly_failed("missing gasPrice"))?,
618        )?;
619        let gas_price: u128 = gas_price_u256
620            .try_into()
621            .map_err(|_| ParseError::assembly_failed("gasPrice overflow"))?;
622
623        let gas_limit = decode_u64(
624            items
625                .get(2)
626                .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
627        )?;
628
629        let to_addr = decode_optional_address(
630            items
631                .get(3)
632                .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
633        )?;
634
635        let value = decode_u256(
636            items
637                .get(4)
638                .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
639        )?;
640
641        let data = decode_bytes(
642            items
643                .get(5)
644                .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
645        )?;
646
647        // Extract chain_id
648        let chain_id = if items.len() == 9 {
649            let v = decode_u64(
650                items
651                    .get(6)
652                    .ok_or_else(|| ParseError::assembly_failed("missing v"))?,
653            )?;
654            if v >= 35 {
655                Some((v - 35) / 2)
656            } else if v == 27 || v == 28 {
657                None // pre-EIP-155
658            } else {
659                // EIP-155 unsigned: v == chain_id
660                Some(v)
661            }
662        } else {
663            None // 6-item unsigned: no chain_id
664        };
665
666        let tx = TxLegacy {
667            chain_id,
668            nonce,
669            gas_price,
670            gas_limit,
671            to: to_addr.map_or(TxKind::Create, TxKind::Call),
672            value,
673            input: Bytes::from(data),
674        };
675
676        let mut buf = Vec::new();
677        tx.rlp_encode_signed(sig, &mut buf);
678        Ok(buf)
679    }
680
681    /// Assemble a signed EIP-2930 transaction.
682    fn assemble_eip2930(
683        payload: &[u8],
684        sig: &alloy_primitives::Signature,
685    ) -> Result<Vec<u8>, ParseError> {
686        use alloy_consensus::transaction::RlpEcdsaEncodableTx;
687        use alloy_consensus::TxEip2930;
688        use alloy_eips::eip2930::AccessList;
689        use alloy_primitives::{Bytes, TxKind};
690        use alloy_rlp::Decodable;
691
692        let items = decode_list(payload)?;
693
694        // Accept 8 (unsigned) or 11 (signed)
695        if items.len() != 8 && items.len() != 11 {
696            return Err(ParseError::assembly_failed(format!(
697                "EIP-2930 tx expected 8 or 11 items, got {}",
698                items.len()
699            )));
700        }
701
702        let chain_id = decode_u64(
703            items
704                .first()
705                .ok_or_else(|| ParseError::assembly_failed("missing chainId"))?,
706        )?;
707
708        let nonce = decode_u64(
709            items
710                .get(1)
711                .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
712        )?;
713
714        let gas_price_u256 = decode_u256(
715            items
716                .get(2)
717                .ok_or_else(|| ParseError::assembly_failed("missing gasPrice"))?,
718        )?;
719        let gas_price: u128 = gas_price_u256
720            .try_into()
721            .map_err(|_| ParseError::assembly_failed("gasPrice overflow"))?;
722
723        let gas_limit = decode_u64(
724            items
725                .get(3)
726                .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
727        )?;
728
729        let to_addr = decode_optional_address(
730            items
731                .get(4)
732                .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
733        )?;
734
735        let value = decode_u256(
736            items
737                .get(5)
738                .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
739        )?;
740
741        let data = decode_bytes(
742            items
743                .get(6)
744                .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
745        )?;
746
747        let access_list_bytes = items
748            .get(7)
749            .ok_or_else(|| ParseError::assembly_failed("missing accessList"))?;
750        let mut access_list_buf = *access_list_bytes;
751        let access_list = AccessList::decode(&mut access_list_buf).map_err(|e| {
752            ParseError::assembly_failed(format!("failed to decode access list: {e}"))
753        })?;
754
755        let tx = TxEip2930 {
756            chain_id,
757            nonce,
758            gas_price,
759            gas_limit,
760            to: to_addr.map_or(TxKind::Create, TxKind::Call),
761            value,
762            input: Bytes::from(data),
763            access_list,
764        };
765
766        let mut buf = vec![0x01]; // EIP-2930 type prefix
767        tx.rlp_encode_signed(sig, &mut buf);
768        Ok(buf)
769    }
770
771    /// Assemble a signed EIP-1559 transaction.
772    fn assemble_eip1559(
773        payload: &[u8],
774        sig: &alloy_primitives::Signature,
775    ) -> Result<Vec<u8>, ParseError> {
776        use alloy_consensus::transaction::RlpEcdsaEncodableTx;
777        use alloy_consensus::TxEip1559;
778        use alloy_eips::eip2930::AccessList;
779        use alloy_primitives::{Bytes, TxKind};
780        use alloy_rlp::Decodable;
781
782        let items = decode_list(payload)?;
783
784        // Accept 9 (unsigned) or 12 (signed)
785        if items.len() != 9 && items.len() != 12 {
786            return Err(ParseError::assembly_failed(format!(
787                "EIP-1559 tx expected 9 or 12 items, got {}",
788                items.len()
789            )));
790        }
791
792        let chain_id = decode_u64(
793            items
794                .first()
795                .ok_or_else(|| ParseError::assembly_failed("missing chainId"))?,
796        )?;
797
798        let nonce = decode_u64(
799            items
800                .get(1)
801                .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
802        )?;
803
804        let max_priority_fee_u256 = decode_u256(
805            items
806                .get(2)
807                .ok_or_else(|| ParseError::assembly_failed("missing maxPriorityFeePerGas"))?,
808        )?;
809        let max_priority_fee_per_gas: u128 = max_priority_fee_u256
810            .try_into()
811            .map_err(|_| ParseError::assembly_failed("maxPriorityFeePerGas overflow"))?;
812
813        let max_fee_u256 = decode_u256(
814            items
815                .get(3)
816                .ok_or_else(|| ParseError::assembly_failed("missing maxFeePerGas"))?,
817        )?;
818        let max_fee_per_gas: u128 = max_fee_u256
819            .try_into()
820            .map_err(|_| ParseError::assembly_failed("maxFeePerGas overflow"))?;
821
822        let gas_limit = decode_u64(
823            items
824                .get(4)
825                .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
826        )?;
827
828        let to_addr = decode_optional_address(
829            items
830                .get(5)
831                .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
832        )?;
833
834        let value = decode_u256(
835            items
836                .get(6)
837                .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
838        )?;
839
840        let data = decode_bytes(
841            items
842                .get(7)
843                .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
844        )?;
845
846        let access_list_bytes = items
847            .get(8)
848            .ok_or_else(|| ParseError::assembly_failed("missing accessList"))?;
849        let mut access_list_buf = *access_list_bytes;
850        let access_list = AccessList::decode(&mut access_list_buf).map_err(|e| {
851            ParseError::assembly_failed(format!("failed to decode access list: {e}"))
852        })?;
853
854        let tx = TxEip1559 {
855            chain_id,
856            nonce,
857            max_priority_fee_per_gas,
858            max_fee_per_gas,
859            gas_limit,
860            to: to_addr.map_or(TxKind::Create, TxKind::Call),
861            value,
862            input: Bytes::from(data),
863            access_list,
864        };
865
866        let mut buf = vec![0x02]; // EIP-1559 type prefix
867        tx.rlp_encode_signed(sig, &mut buf);
868        Ok(buf)
869    }
870}
871
872/// Information extracted from an ERC-20 function call.
873///
874/// Used internally to enrich `ParsedTx` with token-specific data.
875struct Erc20Info {
876    /// The transaction type (`TokenTransfer` or `TokenApproval`).
877    tx_type: TxType,
878    /// The token contract address.
879    token_address: Address,
880    /// The actual recipient/spender address from the ERC-20 call.
881    recipient: Address,
882    /// The token amount from the ERC-20 call.
883    amount: U256,
884}
885
886impl Chain for EthereumParser {
887    /// Returns the chain identifier.
888    ///
889    /// # Returns
890    ///
891    /// Always returns `"ethereum"`.
892    fn id(&self) -> &'static str {
893        "ethereum"
894    }
895
896    /// Parse raw transaction bytes into a [`ParsedTx`].
897    ///
898    /// This method detects the transaction type and delegates to the
899    /// appropriate parser:
900    ///
901    /// - Legacy transactions (no type prefix or type 0)
902    /// - EIP-2930 transactions (type 1)
903    /// - EIP-1559 transactions (type 2)
904    ///
905    /// # Arguments
906    ///
907    /// * `raw` - The raw transaction bytes
908    ///
909    /// # Returns
910    ///
911    /// * `Ok(ParsedTx)` - Successfully parsed transaction
912    /// * `Err(ParseError)` - Parsing failed
913    ///
914    /// # Errors
915    ///
916    /// Returns a [`ParseError`] if:
917    /// - The transaction type is not supported
918    /// - The RLP encoding is invalid
919    /// - Required fields are missing or malformed
920    fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
921        if raw.is_empty() {
922            return Err(ParseError::MalformedTransaction {
923                context: "empty transaction data".to_string(),
924            });
925        }
926
927        // Detect transaction type
928        match detect_tx_type(raw) {
929            None => {
930                // Legacy transaction (starts with RLP list prefix 0xc0-0xff)
931                // or potentially invalid data
932                if crate::rlp::is_list(raw) {
933                    Self::parse_legacy(raw)
934                } else {
935                    Err(ParseError::MalformedTransaction {
936                        context:
937                            "invalid transaction format: not a valid RLP list or typed transaction"
938                                .to_string(),
939                    })
940                }
941            }
942            Some(0) => {
943                // Type 0 - treat as legacy but skip the type byte for RLP parsing
944                // Hash must be computed over full raw bytes including type prefix
945                let payload = typed_tx_payload(raw)?;
946                Self::parse_legacy_with_hash_source(raw, payload)
947            }
948            Some(1) => {
949                // EIP-2930 (Access List)
950                let payload = typed_tx_payload(raw)?;
951                Self::parse_eip2930(raw, payload)
952            }
953            Some(2) => {
954                // EIP-1559 (Dynamic Fee)
955                let payload = typed_tx_payload(raw)?;
956                Self::parse_eip1559(raw, payload)
957            }
958            Some(_) => Err(ParseError::UnknownTxType),
959        }
960    }
961
962    /// Returns the elliptic curve used by Ethereum.
963    ///
964    /// # Returns
965    ///
966    /// Always returns [`CurveType::Secp256k1`].
967    fn curve(&self) -> CurveType {
968        CurveType::Secp256k1
969    }
970
971    /// Check if this parser supports a specific transaction version.
972    ///
973    /// # Arguments
974    ///
975    /// * `version` - The transaction type byte
976    ///
977    /// # Returns
978    ///
979    /// * `true` for versions 0, 1, 2 (Legacy, EIP-2930, EIP-1559)
980    /// * `false` for other versions (e.g., EIP-4844 blob transactions)
981    fn supports_version(&self, version: u8) -> bool {
982        matches!(version, 0..=2)
983    }
984
985    fn assemble_signed(&self, raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
986        Self::assemble_signed(raw, signature)
987    }
988}
989
990// ============================================================================
991// Tests
992// ============================================================================
993
994#[cfg(test)]
995mod tests {
996    #![allow(
997        clippy::expect_used,
998        clippy::unwrap_used,
999        clippy::panic,
1000        clippy::indexing_slicing,
1001        clippy::similar_names,
1002        clippy::redundant_clone,
1003        clippy::manual_string_new,
1004        clippy::needless_raw_string_hashes,
1005        clippy::needless_collect,
1006        clippy::unreadable_literal,
1007        clippy::default_trait_access,
1008        clippy::too_many_arguments,
1009        clippy::default_constructed_unit_structs
1010    )]
1011
1012    use super::*;
1013    use alloy_consensus::{transaction::RlpEcdsaEncodableTx, TxEip1559, TxEip2930, TxLegacy};
1014    use alloy_primitives::{hex, Address, Bytes, Signature, TxKind};
1015
1016    /// Helper to encode a legacy transaction with a fake signature
1017    fn encode_legacy_tx(
1018        nonce: u64,
1019        gas_price: u128,
1020        gas_limit: u64,
1021        to: Option<Address>,
1022        value: U256,
1023        data: Bytes,
1024        chain_id: Option<u64>,
1025    ) -> Vec<u8> {
1026        let tx = TxLegacy {
1027            chain_id,
1028            nonce,
1029            gas_price,
1030            gas_limit,
1031            to: to.map_or(TxKind::Create, TxKind::Call),
1032            value,
1033            input: data,
1034        };
1035
1036        // Create a fake signature
1037        let sig = Signature::new(
1038            U256::from(0xffff_ffff_ffff_ffffu64),
1039            U256::from(0xffff_ffff_ffff_ffffu64),
1040            false,
1041        );
1042
1043        let mut buf = Vec::new();
1044        tx.rlp_encode_signed(&sig, &mut buf);
1045        buf
1046    }
1047
1048    /// Helper to encode an EIP-2930 transaction with a fake signature
1049    fn encode_eip2930_tx(
1050        chain_id: u64,
1051        nonce: u64,
1052        gas_price: u128,
1053        gas_limit: u64,
1054        to: Option<Address>,
1055        value: U256,
1056        data: Bytes,
1057    ) -> Vec<u8> {
1058        let tx = TxEip2930 {
1059            chain_id,
1060            nonce,
1061            gas_price,
1062            gas_limit,
1063            to: to.map_or(TxKind::Create, TxKind::Call),
1064            value,
1065            input: data,
1066            access_list: Default::default(),
1067        };
1068
1069        // Create a fake signature
1070        let sig = Signature::new(
1071            U256::from(0xffff_ffff_ffff_ffffu64),
1072            U256::from(0xffff_ffff_ffff_ffffu64),
1073            false,
1074        );
1075
1076        // Build buffer with type prefix
1077        let mut buf = Vec::new();
1078        buf.push(0x01); // EIP-2930 type prefix
1079        tx.rlp_encode_signed(&sig, &mut buf);
1080        buf
1081    }
1082
1083    /// Helper to encode an EIP-1559 transaction with a fake signature
1084    fn encode_eip1559_tx(
1085        chain_id: u64,
1086        nonce: u64,
1087        max_priority_fee_per_gas: u128,
1088        max_fee_per_gas: u128,
1089        gas_limit: u64,
1090        to: Option<Address>,
1091        value: U256,
1092        data: Bytes,
1093    ) -> Vec<u8> {
1094        let tx = TxEip1559 {
1095            chain_id,
1096            nonce,
1097            max_priority_fee_per_gas,
1098            max_fee_per_gas,
1099            gas_limit,
1100            to: to.map_or(TxKind::Create, TxKind::Call),
1101            value,
1102            input: data,
1103            access_list: Default::default(),
1104        };
1105
1106        // Create a fake signature
1107        let sig = Signature::new(
1108            U256::from(0xffff_ffff_ffff_ffffu64),
1109            U256::from(0xffff_ffff_ffff_ffffu64),
1110            false,
1111        );
1112
1113        // Build buffer with type prefix
1114        let mut buf = Vec::new();
1115        buf.push(0x02); // EIP-1559 type prefix
1116        tx.rlp_encode_signed(&sig, &mut buf);
1117        buf
1118    }
1119
1120    // ------------------------------------------------------------------------
1121    // Constructor and Basic Tests
1122    // ------------------------------------------------------------------------
1123
1124    #[test]
1125    fn test_ethereum_parser_new() {
1126        let parser = EthereumParser::new();
1127        assert_eq!(parser.id(), "ethereum");
1128    }
1129
1130    #[test]
1131    fn test_ethereum_parser_default() {
1132        let parser = EthereumParser::default();
1133        assert_eq!(parser.id(), "ethereum");
1134    }
1135
1136    #[test]
1137    fn test_ethereum_parser_clone() {
1138        let parser = EthereumParser::new();
1139        let cloned = parser;
1140        assert_eq!(cloned.id(), "ethereum");
1141    }
1142
1143    #[test]
1144    fn test_ethereum_parser_debug() {
1145        let parser = EthereumParser::new();
1146        let debug_str = format!("{parser:?}");
1147        assert!(debug_str.contains("EthereumParser"));
1148    }
1149
1150    // ------------------------------------------------------------------------
1151    // Chain Trait Tests
1152    // ------------------------------------------------------------------------
1153
1154    #[test]
1155    fn test_chain_id() {
1156        let parser = EthereumParser::new();
1157        assert_eq!(parser.id(), "ethereum");
1158    }
1159
1160    #[test]
1161    fn test_chain_curve() {
1162        let parser = EthereumParser::new();
1163        assert_eq!(parser.curve(), CurveType::Secp256k1);
1164    }
1165
1166    #[test]
1167    fn test_supports_version() {
1168        let parser = EthereumParser::new();
1169
1170        // Supported versions
1171        assert!(parser.supports_version(0)); // Legacy
1172        assert!(parser.supports_version(1)); // EIP-2930
1173        assert!(parser.supports_version(2)); // EIP-1559
1174
1175        // Unsupported versions
1176        assert!(!parser.supports_version(3)); // EIP-4844 (blobs)
1177        assert!(!parser.supports_version(4));
1178        assert!(!parser.supports_version(255));
1179    }
1180
1181    // ------------------------------------------------------------------------
1182    // Empty Input Tests
1183    // ------------------------------------------------------------------------
1184
1185    #[test]
1186    fn test_parse_empty_input() {
1187        let parser = EthereumParser::new();
1188        let result = parser.parse(&[]);
1189
1190        assert!(result.is_err());
1191        assert!(matches!(
1192            result,
1193            Err(ParseError::MalformedTransaction { .. })
1194        ));
1195    }
1196
1197    // ------------------------------------------------------------------------
1198    // Legacy Transaction Tests
1199    // ------------------------------------------------------------------------
1200
1201    #[test]
1202    fn test_parse_legacy_transaction() {
1203        let parser = EthereumParser::new();
1204
1205        // Real legacy transaction from Ethereum mainnet
1206        // This is a simple ETH transfer
1207        // nonce=9, gasPrice=20gwei, gasLimit=21000, to=0x3535..., value=1ETH, data=empty
1208        let raw = hex::decode(
1209            "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1210        ).expect("valid hex");
1211
1212        let result = parser.parse(&raw);
1213        assert!(result.is_ok(), "parsing failed: {result:?}");
1214
1215        let parsed = result.expect("should parse successfully");
1216
1217        // Verify fields
1218        assert_eq!(parsed.chain, "ethereum");
1219        assert_eq!(parsed.nonce, Some(9));
1220        assert_eq!(parsed.tx_type, TxType::Transfer);
1221        assert!(parsed.recipient.is_some());
1222        assert_eq!(
1223            parsed.recipient.as_ref().map(|s| s.to_lowercase()),
1224            Some("0x3535353535353535353535353535353535353535".to_string())
1225        );
1226
1227        // Verify amount is 1 ETH (10^18 wei)
1228        let expected_amount = U256::from(1_000_000_000_000_000_000u64);
1229        assert_eq!(parsed.amount, Some(expected_amount));
1230
1231        // Verify chain_id extraction from v=37 -> chain_id = (37-35)/2 = 1
1232        assert_eq!(parsed.chain_id, Some(1));
1233
1234        // Verify hash is computed
1235        assert_ne!(parsed.hash, [0u8; 32]);
1236    }
1237
1238    #[test]
1239    fn test_parse_legacy_transaction_pre_eip155() {
1240        let parser = EthereumParser::new();
1241
1242        // Legacy transaction without chain_id (pre-EIP-155)
1243        let to_addr = Address::from([0x12; 20]);
1244        let raw = encode_legacy_tx(
1245            0,                // nonce
1246            1_000_000_000,    // gas_price (1 gwei)
1247            21000,            // gas_limit
1248            Some(to_addr),    // to
1249            U256::ZERO,       // value
1250            Bytes::default(), // data
1251            None,             // chain_id (None = pre-EIP-155)
1252        );
1253
1254        let result = parser.parse(&raw);
1255        assert!(result.is_ok(), "parsing failed: {result:?}");
1256
1257        let parsed = result.expect("should parse successfully");
1258        // Pre-EIP-155 defaults to mainnet (chain_id = 1)
1259        assert_eq!(parsed.chain_id, Some(1));
1260    }
1261
1262    #[test]
1263    fn test_parse_legacy_contract_deployment() {
1264        let parser = EthereumParser::new();
1265
1266        // Contract deployment: to field is None (Create)
1267        let raw = encode_legacy_tx(
1268            0,                                         // nonce
1269            1_000_000_000,                             // gas_price
1270            100000,                                    // gas_limit
1271            None,                                      // to = None for deployment
1272            U256::ZERO,                                // value
1273            Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), // data (some bytecode)
1274            Some(1),                                   // chain_id
1275        );
1276
1277        let result = parser.parse(&raw);
1278        assert!(result.is_ok(), "parsing failed: {result:?}");
1279
1280        let parsed = result.expect("should parse successfully");
1281        assert_eq!(parsed.tx_type, TxType::Deployment);
1282        assert!(parsed.recipient.is_none());
1283    }
1284
1285    #[test]
1286    fn test_parse_legacy_contract_call() {
1287        let parser = EthereumParser::new();
1288
1289        // Contract call: has recipient and non-empty data
1290        let to_addr = Address::from([0x12; 20]);
1291        let raw = encode_legacy_tx(
1292            1,                                         // nonce
1293            1_000_000_000,                             // gas_price
1294            100000,                                    // gas_limit
1295            Some(to_addr),                             // to
1296            U256::ZERO,                                // value
1297            Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), // data (transfer selector)
1298            Some(1),                                   // chain_id
1299        );
1300
1301        let result = parser.parse(&raw);
1302        assert!(result.is_ok(), "parsing failed: {result:?}");
1303
1304        let parsed = result.expect("should parse successfully");
1305        assert_eq!(parsed.tx_type, TxType::ContractCall);
1306        assert!(parsed.recipient.is_some());
1307    }
1308
1309    // ------------------------------------------------------------------------
1310    // EIP-2930 Transaction Tests
1311    // ------------------------------------------------------------------------
1312
1313    #[test]
1314    fn test_parse_eip2930_transaction() {
1315        let parser = EthereumParser::new();
1316
1317        // EIP-2930 transaction (type 1)
1318        let to_addr = Address::from([0x12; 20]);
1319        let raw = encode_eip2930_tx(
1320            1,                // chain_id
1321            0,                // nonce
1322            1_000_000_000,    // gas_price
1323            21000,            // gas_limit
1324            Some(to_addr),    // to
1325            U256::ZERO,       // value
1326            Bytes::default(), // data
1327        );
1328
1329        let result = parser.parse(&raw);
1330        assert!(result.is_ok(), "parsing failed: {result:?}");
1331
1332        let parsed = result.expect("should parse successfully");
1333
1334        assert_eq!(parsed.chain, "ethereum");
1335        assert_eq!(parsed.chain_id, Some(1));
1336        assert_eq!(parsed.nonce, Some(0));
1337        assert_eq!(parsed.tx_type, TxType::Transfer);
1338        assert!(parsed.recipient.is_some());
1339    }
1340
1341    #[test]
1342    fn test_parse_eip2930_contract_deployment() {
1343        let parser = EthereumParser::new();
1344
1345        // EIP-2930 contract deployment (to=None)
1346        let raw = encode_eip2930_tx(
1347            1,                                         // chain_id
1348            0,                                         // nonce
1349            1_000_000_000,                             // gas_price
1350            100000,                                    // gas_limit
1351            None,                                      // to = None for deployment
1352            U256::ZERO,                                // value
1353            Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), // data
1354        );
1355
1356        let result = parser.parse(&raw);
1357        assert!(result.is_ok(), "parsing failed: {result:?}");
1358
1359        let parsed = result.expect("should parse successfully");
1360        assert_eq!(parsed.tx_type, TxType::Deployment);
1361        assert!(parsed.recipient.is_none());
1362    }
1363
1364    // ------------------------------------------------------------------------
1365    // EIP-1559 Transaction Tests
1366    // ------------------------------------------------------------------------
1367
1368    #[test]
1369    fn test_parse_eip1559_transaction() {
1370        let parser = EthereumParser::new();
1371
1372        // EIP-1559 transaction (type 2)
1373        let to_addr = Address::from([0x12; 20]);
1374        let raw = encode_eip1559_tx(
1375            1,                // chain_id
1376            0,                // nonce
1377            1_000_000_000,    // max_priority_fee_per_gas
1378            2_000_000_000,    // max_fee_per_gas
1379            21000,            // gas_limit
1380            Some(to_addr),    // to
1381            U256::ZERO,       // value
1382            Bytes::default(), // data
1383        );
1384
1385        let result = parser.parse(&raw);
1386        assert!(result.is_ok(), "parsing failed: {result:?}");
1387
1388        let parsed = result.expect("should parse successfully");
1389
1390        assert_eq!(parsed.chain, "ethereum");
1391        assert_eq!(parsed.chain_id, Some(1));
1392        assert_eq!(parsed.nonce, Some(0));
1393        assert_eq!(parsed.tx_type, TxType::Transfer);
1394        assert!(parsed.recipient.is_some());
1395    }
1396
1397    #[test]
1398    fn test_parse_eip1559_with_value() {
1399        let parser = EthereumParser::new();
1400
1401        // EIP-1559 transaction with value
1402        let to_addr = Address::from([0x12; 20]);
1403        let value = U256::from(1_000_000_000_000_000_000u64); // 1 ETH
1404        let raw = encode_eip1559_tx(
1405            1,                // chain_id
1406            5,                // nonce
1407            1_000_000_000,    // max_priority_fee_per_gas
1408            100_000_000_000,  // max_fee_per_gas
1409            21000,            // gas_limit
1410            Some(to_addr),    // to
1411            value,            // value
1412            Bytes::default(), // data
1413        );
1414
1415        let result = parser.parse(&raw);
1416        assert!(result.is_ok(), "parsing failed: {result:?}");
1417
1418        let parsed = result.expect("should parse successfully");
1419
1420        assert_eq!(parsed.chain, "ethereum");
1421        assert_eq!(parsed.chain_id, Some(1));
1422        assert_eq!(parsed.nonce, Some(5));
1423        assert_eq!(parsed.tx_type, TxType::Transfer);
1424        assert_eq!(parsed.amount, Some(value));
1425    }
1426
1427    #[test]
1428    fn test_parse_eip1559_contract_deployment() {
1429        let parser = EthereumParser::new();
1430
1431        // EIP-1559 contract deployment (to=None)
1432        let raw = encode_eip1559_tx(
1433            1,                                         // chain_id
1434            0,                                         // nonce
1435            1_000_000_000,                             // max_priority_fee_per_gas
1436            2_000_000_000,                             // max_fee_per_gas
1437            100000,                                    // gas_limit
1438            None,                                      // to = None for deployment
1439            U256::ZERO,                                // value
1440            Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), // data
1441        );
1442
1443        let result = parser.parse(&raw);
1444        assert!(result.is_ok(), "parsing failed: {result:?}");
1445
1446        let parsed = result.expect("should parse successfully");
1447        assert_eq!(parsed.tx_type, TxType::Deployment);
1448        assert!(parsed.recipient.is_none());
1449    }
1450
1451    #[test]
1452    fn test_parse_eip1559_contract_call() {
1453        let parser = EthereumParser::new();
1454
1455        // EIP-1559 contract call (has data)
1456        let to_addr = Address::from([0x12; 20]);
1457        let raw = encode_eip1559_tx(
1458            1,                                         // chain_id
1459            0,                                         // nonce
1460            1_000_000_000,                             // max_priority_fee_per_gas
1461            2_000_000_000,                             // max_fee_per_gas
1462            100000,                                    // gas_limit
1463            Some(to_addr),                             // to
1464            U256::ZERO,                                // value
1465            Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), // data (transfer selector)
1466        );
1467
1468        let result = parser.parse(&raw);
1469        assert!(result.is_ok(), "parsing failed: {result:?}");
1470
1471        let parsed = result.expect("should parse successfully");
1472        assert_eq!(parsed.tx_type, TxType::ContractCall);
1473        assert!(parsed.recipient.is_some());
1474    }
1475
1476    // ------------------------------------------------------------------------
1477    // Hash Computation Tests
1478    // ------------------------------------------------------------------------
1479
1480    #[test]
1481    fn test_hash_is_correctly_computed() {
1482        let parser = EthereumParser::new();
1483
1484        let raw = hex::decode(
1485            "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1486        ).expect("valid hex");
1487
1488        let result = parser.parse(&raw);
1489        assert!(result.is_ok());
1490
1491        let parsed = result.expect("should parse");
1492
1493        // Compute expected hash
1494        let expected_hash = keccak256(&raw);
1495
1496        assert_eq!(parsed.hash, *expected_hash);
1497    }
1498
1499    #[test]
1500    fn test_eip1559_hash_includes_type_prefix() {
1501        let parser = EthereumParser::new();
1502
1503        let to_addr = Address::from([0x12; 20]);
1504        let raw = encode_eip1559_tx(
1505            1,                // chain_id
1506            0,                // nonce
1507            1_000_000_000,    // max_priority_fee_per_gas
1508            2_000_000_000,    // max_fee_per_gas
1509            21000,            // gas_limit
1510            Some(to_addr),    // to
1511            U256::ZERO,       // value
1512            Bytes::default(), // data
1513        );
1514
1515        let result = parser.parse(&raw);
1516        assert!(result.is_ok());
1517
1518        let parsed = result.expect("should parse");
1519
1520        // Hash should be of the entire raw bytes (including type prefix)
1521        let expected_hash = keccak256(&raw);
1522        assert_eq!(parsed.hash, *expected_hash);
1523    }
1524
1525    #[test]
1526    fn test_type0_hash_includes_type_prefix() {
1527        let parser = EthereumParser::new();
1528
1529        // Create a legacy transaction and then prefix it with 0x00 (type 0)
1530        let to_addr = Address::from([0x12; 20]);
1531        let legacy_raw = encode_legacy_tx(
1532            0,                // nonce
1533            1_000_000_000,    // gas_price (1 gwei)
1534            21000,            // gas_limit
1535            Some(to_addr),    // to
1536            U256::ZERO,       // value
1537            Bytes::default(), // data
1538            Some(1),          // chain_id
1539        );
1540
1541        // Create type 0 transaction by prefixing with 0x00
1542        let mut type0_raw = vec![0x00];
1543        type0_raw.extend_from_slice(&legacy_raw);
1544
1545        let result = parser.parse(&type0_raw);
1546        assert!(result.is_ok(), "parsing failed: {result:?}");
1547
1548        let parsed = result.expect("should parse");
1549
1550        // Hash MUST be computed over the full raw bytes INCLUDING the type prefix
1551        let expected_hash = keccak256(&type0_raw);
1552        assert_eq!(
1553            parsed.hash, *expected_hash,
1554            "type 0 hash should include type prefix"
1555        );
1556
1557        // Verify it's NOT the hash of just the payload (without type byte)
1558        let wrong_hash = keccak256(&legacy_raw);
1559        assert_ne!(
1560            parsed.hash, *wrong_hash,
1561            "hash should NOT be computed without type prefix"
1562        );
1563    }
1564
1565    #[test]
1566    fn test_type0_vs_legacy_same_content_different_hash() {
1567        let parser = EthereumParser::new();
1568
1569        // Create a legacy transaction
1570        let to_addr = Address::from([0x12; 20]);
1571        let legacy_raw = encode_legacy_tx(
1572            5,                                        // nonce
1573            2_000_000_000,                            // gas_price (2 gwei)
1574            21000,                                    // gas_limit
1575            Some(to_addr),                            // to
1576            U256::from(1_000_000_000_000_000_000u64), // 1 ETH
1577            Bytes::default(),                         // data
1578            Some(1),                                  // chain_id
1579        );
1580
1581        // Parse as pure legacy (no type prefix)
1582        let legacy_result = parser.parse(&legacy_raw);
1583        assert!(legacy_result.is_ok());
1584        let legacy_parsed = legacy_result.expect("should parse legacy");
1585
1586        // Create type 0 version (same content, with 0x00 prefix)
1587        let mut type0_raw = vec![0x00];
1588        type0_raw.extend_from_slice(&legacy_raw);
1589
1590        // Parse as type 0
1591        let type0_result = parser.parse(&type0_raw);
1592        assert!(type0_result.is_ok());
1593        let type0_parsed = type0_result.expect("should parse type 0");
1594
1595        // Both should have the same transaction data
1596        assert_eq!(legacy_parsed.nonce, type0_parsed.nonce);
1597        assert_eq!(legacy_parsed.recipient, type0_parsed.recipient);
1598        assert_eq!(legacy_parsed.amount, type0_parsed.amount);
1599        assert_eq!(legacy_parsed.chain_id, type0_parsed.chain_id);
1600
1601        // But the hashes MUST be different because type 0 includes the prefix
1602        assert_ne!(
1603            legacy_parsed.hash, type0_parsed.hash,
1604            "legacy and type 0 hashes should differ due to type prefix"
1605        );
1606    }
1607
1608    // ------------------------------------------------------------------------
1609    // Error Handling Tests
1610    // ------------------------------------------------------------------------
1611
1612    #[test]
1613    fn test_parse_unsupported_tx_type() {
1614        let parser = EthereumParser::new();
1615
1616        // Type 3 (EIP-4844 blob tx) - not supported
1617        let raw = hex::decode("03f8c0").expect("valid hex");
1618
1619        let result = parser.parse(&raw);
1620        assert!(result.is_err());
1621        assert!(matches!(result, Err(ParseError::UnknownTxType)));
1622    }
1623
1624    #[test]
1625    fn test_parse_malformed_legacy_too_few_items() {
1626        let parser = EthereumParser::new();
1627
1628        // List with only 3 items instead of 9
1629        let raw = hex::decode("c3010203").expect("valid hex");
1630
1631        let result = parser.parse(&raw);
1632        assert!(result.is_err());
1633        assert!(matches!(
1634            result,
1635            Err(ParseError::MalformedTransaction { .. })
1636        ));
1637    }
1638
1639    #[test]
1640    fn test_parse_invalid_rlp() {
1641        let parser = EthereumParser::new();
1642
1643        // Invalid RLP (claims to be a list but truncated)
1644        let raw = hex::decode("f8ff").expect("valid hex");
1645
1646        let result = parser.parse(&raw);
1647        assert!(result.is_err());
1648    }
1649
1650    #[test]
1651    fn test_parse_not_list_not_typed() {
1652        let parser = EthereumParser::new();
1653
1654        // Single byte that's not a valid type prefix (0x04-0xbf range)
1655        // and not an RLP list (0xc0+)
1656        let raw = hex::decode("80").expect("valid hex");
1657
1658        let result = parser.parse(&raw);
1659        assert!(result.is_err());
1660        assert!(matches!(
1661            result,
1662            Err(ParseError::MalformedTransaction { .. })
1663        ));
1664    }
1665
1666    // ------------------------------------------------------------------------
1667    // Different Chain IDs Tests
1668    // ------------------------------------------------------------------------
1669
1670    #[test]
1671    fn test_parse_eip1559_polygon() {
1672        let parser = EthereumParser::new();
1673
1674        // EIP-1559 on Polygon (chainId=137)
1675        let to_addr = Address::from([0x12; 20]);
1676        let raw = encode_eip1559_tx(
1677            137,              // chain_id (Polygon)
1678            0,                // nonce
1679            1_000_000_000,    // max_priority_fee_per_gas
1680            2_000_000_000,    // max_fee_per_gas
1681            21000,            // gas_limit
1682            Some(to_addr),    // to
1683            U256::ZERO,       // value
1684            Bytes::default(), // data
1685        );
1686
1687        let result = parser.parse(&raw);
1688        assert!(result.is_ok(), "parsing failed: {result:?}");
1689
1690        let parsed = result.expect("should parse");
1691        assert_eq!(parsed.chain_id, Some(137));
1692    }
1693
1694    #[test]
1695    fn test_parse_legacy_with_high_chain_id() {
1696        let parser = EthereumParser::new();
1697
1698        // Legacy transaction with BSC chain_id (56)
1699        let to_addr = Address::from([0x12; 20]);
1700        let raw = encode_legacy_tx(
1701            9,                                        // nonce
1702            20_000_000_000,                           // gas_price (20 gwei)
1703            21000,                                    // gas_limit
1704            Some(to_addr),                            // to
1705            U256::from(1_000_000_000_000_000_000u64), // value (1 ETH equivalent)
1706            Bytes::default(),                         // data
1707            Some(56),                                 // chain_id (BSC)
1708        );
1709
1710        let result = parser.parse(&raw);
1711        assert!(result.is_ok(), "parsing failed: {result:?}");
1712
1713        let parsed = result.expect("should parse");
1714        assert_eq!(parsed.chain_id, Some(56));
1715    }
1716
1717    // ------------------------------------------------------------------------
1718    // Thread Safety Tests
1719    // ------------------------------------------------------------------------
1720
1721    #[test]
1722    fn test_parser_is_send() {
1723        fn assert_send<T: Send>() {}
1724        assert_send::<EthereumParser>();
1725    }
1726
1727    #[test]
1728    fn test_parser_is_sync() {
1729        fn assert_sync<T: Sync>() {}
1730        assert_sync::<EthereumParser>();
1731    }
1732
1733    // ------------------------------------------------------------------------
1734    // Integration with Chain Registry
1735    // ------------------------------------------------------------------------
1736
1737    #[test]
1738    fn test_parser_as_trait_object() {
1739        let parser = EthereumParser::new();
1740        let chain: Box<dyn Chain> = Box::new(parser);
1741
1742        assert_eq!(chain.id(), "ethereum");
1743        assert_eq!(chain.curve(), CurveType::Secp256k1);
1744        assert!(chain.supports_version(0));
1745        assert!(chain.supports_version(1));
1746        assert!(chain.supports_version(2));
1747    }
1748
1749    // ------------------------------------------------------------------------
1750    // ERC-20 Detection Integration Tests
1751    // ------------------------------------------------------------------------
1752
1753    /// Helper to create ERC-20 transfer calldata
1754    fn erc20_transfer_calldata(to: Address, amount: U256) -> Bytes {
1755        let mut data = vec![0xa9, 0x05, 0x9c, 0xbb]; // transfer selector
1756                                                     // Address (32 bytes, left-padded)
1757        data.extend_from_slice(&[0u8; 12]);
1758        data.extend_from_slice(to.as_slice());
1759        // Amount (32 bytes, big-endian)
1760        data.extend_from_slice(&amount.to_be_bytes::<32>());
1761        Bytes::from(data)
1762    }
1763
1764    /// Helper to create ERC-20 approve calldata
1765    fn erc20_approve_calldata(spender: Address, amount: U256) -> Bytes {
1766        let mut data = vec![0x09, 0x5e, 0xa7, 0xb3]; // approve selector
1767                                                     // Spender address (32 bytes, left-padded)
1768        data.extend_from_slice(&[0u8; 12]);
1769        data.extend_from_slice(spender.as_slice());
1770        // Amount (32 bytes, big-endian)
1771        data.extend_from_slice(&amount.to_be_bytes::<32>());
1772        Bytes::from(data)
1773    }
1774
1775    /// Helper to create ERC-20 transferFrom calldata
1776    fn erc20_transfer_from_calldata(from: Address, to: Address, amount: U256) -> Bytes {
1777        let mut data = vec![0x23, 0xb8, 0x72, 0xdd]; // transferFrom selector
1778                                                     // From address (32 bytes, left-padded)
1779        data.extend_from_slice(&[0u8; 12]);
1780        data.extend_from_slice(from.as_slice());
1781        // To address (32 bytes, left-padded)
1782        data.extend_from_slice(&[0u8; 12]);
1783        data.extend_from_slice(to.as_slice());
1784        // Amount (32 bytes, big-endian)
1785        data.extend_from_slice(&amount.to_be_bytes::<32>());
1786        Bytes::from(data)
1787    }
1788
1789    #[test]
1790    fn test_erc20_transfer_detection_eip1559() {
1791        let parser = EthereumParser::new();
1792
1793        let token_contract = Address::from([0xaa; 20]); // Token contract address
1794        let recipient = Address::from([0xbb; 20]); // Actual recipient
1795        let token_amount = U256::from(1_000_000u64); // 1 USDC (6 decimals)
1796
1797        let calldata = erc20_transfer_calldata(recipient, token_amount);
1798
1799        let raw = encode_eip1559_tx(
1800            1,                    // chain_id
1801            0,                    // nonce
1802            1_000_000_000,        // max_priority_fee_per_gas
1803            2_000_000_000,        // max_fee_per_gas
1804            100_000,              // gas_limit
1805            Some(token_contract), // to (token contract)
1806            U256::ZERO,           // value (no ETH sent)
1807            calldata,             // data (ERC-20 transfer)
1808        );
1809
1810        let result = parser.parse(&raw);
1811        assert!(result.is_ok(), "parsing failed: {result:?}");
1812
1813        let parsed = result.expect("should parse");
1814
1815        // Should be detected as TokenTransfer
1816        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1817
1818        // Recipient should be the actual token recipient, not the contract
1819        assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1820
1821        // Amount should be the token amount
1822        assert_eq!(parsed.amount, Some(token_amount));
1823
1824        // Token address should be set
1825        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1826    }
1827
1828    #[test]
1829    fn test_erc20_approve_detection_eip1559() {
1830        let parser = EthereumParser::new();
1831
1832        let token_contract = Address::from([0xaa; 20]);
1833        let spender = Address::from([0xcc; 20]); // Spender (e.g., DEX router)
1834        let approval_amount = U256::MAX; // Unlimited approval
1835
1836        let calldata = erc20_approve_calldata(spender, approval_amount);
1837
1838        let raw = encode_eip1559_tx(
1839            1,                    // chain_id
1840            1,                    // nonce
1841            1_000_000_000,        // max_priority_fee_per_gas
1842            2_000_000_000,        // max_fee_per_gas
1843            60_000,               // gas_limit
1844            Some(token_contract), // to (token contract)
1845            U256::ZERO,           // value
1846            calldata,             // data (ERC-20 approve)
1847        );
1848
1849        let result = parser.parse(&raw);
1850        assert!(result.is_ok(), "parsing failed: {result:?}");
1851
1852        let parsed = result.expect("should parse");
1853
1854        // Should be detected as TokenApproval
1855        assert_eq!(parsed.tx_type, TxType::TokenApproval);
1856
1857        // Recipient should be the spender
1858        assert_eq!(parsed.recipient, Some(format!("{spender}")));
1859
1860        // Amount should be the approval amount
1861        assert_eq!(parsed.amount, Some(approval_amount));
1862
1863        // Token address should be set
1864        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1865    }
1866
1867    #[test]
1868    fn test_erc20_transfer_from_detection_eip1559() {
1869        let parser = EthereumParser::new();
1870
1871        let token_contract = Address::from([0xaa; 20]);
1872        let from_addr = Address::from([0xdd; 20]); // Token owner
1873        let to_addr = Address::from([0xee; 20]); // Token recipient
1874        let token_amount = U256::from(500_000_000_000_000_000u64); // 0.5 tokens (18 decimals)
1875
1876        let calldata = erc20_transfer_from_calldata(from_addr, to_addr, token_amount);
1877
1878        let raw = encode_eip1559_tx(
1879            1,                    // chain_id
1880            2,                    // nonce
1881            1_000_000_000,        // max_priority_fee_per_gas
1882            2_000_000_000,        // max_fee_per_gas
1883            100_000,              // gas_limit
1884            Some(token_contract), // to (token contract)
1885            U256::ZERO,           // value
1886            calldata,             // data (ERC-20 transferFrom)
1887        );
1888
1889        let result = parser.parse(&raw);
1890        assert!(result.is_ok(), "parsing failed: {result:?}");
1891
1892        let parsed = result.expect("should parse");
1893
1894        // Should be detected as TokenTransfer
1895        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1896
1897        // Recipient should be the actual token recipient (to_addr)
1898        assert_eq!(parsed.recipient, Some(format!("{to_addr}")));
1899
1900        // Amount should be the token amount
1901        assert_eq!(parsed.amount, Some(token_amount));
1902
1903        // Token address should be set
1904        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1905    }
1906
1907    #[test]
1908    fn test_erc20_transfer_detection_legacy() {
1909        let parser = EthereumParser::new();
1910
1911        let token_contract = Address::from([0xaa; 20]);
1912        let recipient = Address::from([0xbb; 20]);
1913        let token_amount = U256::from(2_000_000u64);
1914
1915        let calldata = erc20_transfer_calldata(recipient, token_amount);
1916
1917        let raw = encode_legacy_tx(
1918            5,                    // nonce
1919            20_000_000_000,       // gas_price (20 gwei)
1920            100_000,              // gas_limit
1921            Some(token_contract), // to (token contract)
1922            U256::ZERO,           // value
1923            calldata,             // data
1924            Some(1),              // chain_id
1925        );
1926
1927        let result = parser.parse(&raw);
1928        assert!(result.is_ok(), "parsing failed: {result:?}");
1929
1930        let parsed = result.expect("should parse");
1931
1932        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1933        assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1934        assert_eq!(parsed.amount, Some(token_amount));
1935        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1936    }
1937
1938    #[test]
1939    fn test_erc20_detection_eip2930() {
1940        let parser = EthereumParser::new();
1941
1942        let token_contract = Address::from([0xaa; 20]);
1943        let spender = Address::from([0xcc; 20]);
1944        let approval_amount = U256::from(1_000_000_000_000u64);
1945
1946        let calldata = erc20_approve_calldata(spender, approval_amount);
1947
1948        let raw = encode_eip2930_tx(
1949            1,                    // chain_id
1950            3,                    // nonce
1951            10_000_000_000,       // gas_price
1952            80_000,               // gas_limit
1953            Some(token_contract), // to (token contract)
1954            U256::ZERO,           // value
1955            calldata,             // data
1956        );
1957
1958        let result = parser.parse(&raw);
1959        assert!(result.is_ok(), "parsing failed: {result:?}");
1960
1961        let parsed = result.expect("should parse");
1962
1963        assert_eq!(parsed.tx_type, TxType::TokenApproval);
1964        assert_eq!(parsed.recipient, Some(format!("{spender}")));
1965        assert_eq!(parsed.amount, Some(approval_amount));
1966        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1967    }
1968
1969    #[test]
1970    fn test_non_erc20_contract_call_unchanged() {
1971        let parser = EthereumParser::new();
1972
1973        let contract = Address::from([0x12; 20]);
1974        // Unknown function selector (not ERC-20)
1975        let calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef, 0x00]);
1976
1977        let raw = encode_eip1559_tx(
1978            1,              // chain_id
1979            0,              // nonce
1980            1_000_000_000,  // max_priority_fee_per_gas
1981            2_000_000_000,  // max_fee_per_gas
1982            100_000,        // gas_limit
1983            Some(contract), // to
1984            U256::ZERO,     // value
1985            calldata,       // data (not ERC-20)
1986        );
1987
1988        let result = parser.parse(&raw);
1989        assert!(result.is_ok(), "parsing failed: {result:?}");
1990
1991        let parsed = result.expect("should parse");
1992
1993        // Should be a generic ContractCall
1994        assert_eq!(parsed.tx_type, TxType::ContractCall);
1995
1996        // Recipient should be the contract address
1997        assert_eq!(parsed.recipient, Some(format!("{contract}")));
1998
1999        // Token address should NOT be set
2000        assert!(parsed.token_address.is_none());
2001    }
2002
2003    #[test]
2004    fn test_simple_eth_transfer_unchanged() {
2005        let parser = EthereumParser::new();
2006
2007        let recipient = Address::from([0x12; 20]);
2008        let eth_amount = U256::from(1_000_000_000_000_000_000u64); // 1 ETH
2009
2010        let raw = encode_eip1559_tx(
2011            1,                // chain_id
2012            0,                // nonce
2013            1_000_000_000,    // max_priority_fee_per_gas
2014            2_000_000_000,    // max_fee_per_gas
2015            21_000,           // gas_limit
2016            Some(recipient),  // to
2017            eth_amount,       // value
2018            Bytes::default(), // data (empty)
2019        );
2020
2021        let result = parser.parse(&raw);
2022        assert!(result.is_ok(), "parsing failed: {result:?}");
2023
2024        let parsed = result.expect("should parse");
2025
2026        // Should be a simple Transfer
2027        assert_eq!(parsed.tx_type, TxType::Transfer);
2028
2029        // Recipient should be the ETH recipient
2030        assert_eq!(parsed.recipient, Some(format!("{recipient}")));
2031
2032        // Amount should be the ETH amount
2033        assert_eq!(parsed.amount, Some(eth_amount));
2034
2035        // Token address should NOT be set (native transfer)
2036        assert!(parsed.token_address.is_none());
2037    }
2038
2039    #[test]
2040    fn test_erc20_with_eth_value() {
2041        // Some exotic cases might send ETH value along with ERC-20 call
2042        // (e.g., WETH deposit with data, or payable token functions)
2043        let parser = EthereumParser::new();
2044
2045        let token_contract = Address::from([0xaa; 20]);
2046        let recipient = Address::from([0xbb; 20]);
2047        let token_amount = U256::from(1_000_000u64);
2048        let eth_value = U256::from(100_000_000_000_000_000u64); // 0.1 ETH
2049
2050        let calldata = erc20_transfer_calldata(recipient, token_amount);
2051
2052        let raw = encode_eip1559_tx(
2053            1,                    // chain_id
2054            0,                    // nonce
2055            1_000_000_000,        // max_priority_fee_per_gas
2056            2_000_000_000,        // max_fee_per_gas
2057            100_000,              // gas_limit
2058            Some(token_contract), // to (token contract)
2059            eth_value,            // value (some ETH too!)
2060            calldata,             // data (ERC-20 transfer)
2061        );
2062
2063        let result = parser.parse(&raw);
2064        assert!(result.is_ok(), "parsing failed: {result:?}");
2065
2066        let parsed = result.expect("should parse");
2067
2068        // Should still detect as TokenTransfer
2069        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2070
2071        // Amount should be the token amount (not ETH value)
2072        assert_eq!(parsed.amount, Some(token_amount));
2073
2074        // Token address should be set
2075        assert!(parsed.token_address.is_some());
2076    }
2077
2078    #[test]
2079    fn test_erc20_zero_amount() {
2080        let parser = EthereumParser::new();
2081
2082        let token_contract = Address::from([0xaa; 20]);
2083        let recipient = Address::from([0xbb; 20]);
2084        let token_amount = U256::ZERO;
2085
2086        let calldata = erc20_transfer_calldata(recipient, token_amount);
2087
2088        let raw = encode_eip1559_tx(
2089            1,                    // chain_id
2090            0,                    // nonce
2091            1_000_000_000,        // max_priority_fee_per_gas
2092            2_000_000_000,        // max_fee_per_gas
2093            60_000,               // gas_limit
2094            Some(token_contract), // to
2095            U256::ZERO,           // value
2096            calldata,             // data
2097        );
2098
2099        let result = parser.parse(&raw);
2100        assert!(result.is_ok(), "parsing failed: {result:?}");
2101
2102        let parsed = result.expect("should parse");
2103
2104        // Should still be detected as TokenTransfer
2105        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2106        assert_eq!(parsed.amount, Some(U256::ZERO));
2107    }
2108
2109    #[test]
2110    fn test_erc20_approve_zero_revoke() {
2111        // Approve with zero amount is a "revoke" pattern
2112        let parser = EthereumParser::new();
2113
2114        let token_contract = Address::from([0xaa; 20]);
2115        let spender = Address::from([0xcc; 20]);
2116        let approval_amount = U256::ZERO; // Revoke approval
2117
2118        let calldata = erc20_approve_calldata(spender, approval_amount);
2119
2120        let raw = encode_eip1559_tx(
2121            1,                    // chain_id
2122            0,                    // nonce
2123            1_000_000_000,        // max_priority_fee_per_gas
2124            2_000_000_000,        // max_fee_per_gas
2125            50_000,               // gas_limit
2126            Some(token_contract), // to
2127            U256::ZERO,           // value
2128            calldata,             // data
2129        );
2130
2131        let result = parser.parse(&raw);
2132        assert!(result.is_ok(), "parsing failed: {result:?}");
2133
2134        let parsed = result.expect("should parse");
2135
2136        // Should still be TokenApproval
2137        assert_eq!(parsed.tx_type, TxType::TokenApproval);
2138        assert_eq!(parsed.amount, Some(U256::ZERO));
2139    }
2140
2141    // ------------------------------------------------------------------------
2142    // Missing Field Error Tests
2143    // ------------------------------------------------------------------------
2144
2145    #[test]
2146    fn test_legacy_tx_truncated_data() {
2147        // Arrange: Create a truncated legacy transaction by encoding normally then truncating
2148        let parser = EthereumParser::new();
2149
2150        // First create a valid legacy transaction
2151        let valid_raw = encode_legacy_tx(
2152            9,
2153            20_000_000_000,
2154            21000,
2155            Some(Address::from([0x35; 20])),
2156            U256::from(1_000_000_000_000_000_000u64),
2157            Bytes::new(),
2158            Some(1),
2159        );
2160
2161        // Truncate it to simulate missing fields
2162        let truncated = &valid_raw[..valid_raw.len() / 2];
2163
2164        // Act
2165        let result = parser.parse(truncated);
2166
2167        // Assert: Should fail with InvalidRlp or MalformedTransaction
2168        assert!(result.is_err());
2169        assert!(matches!(
2170            result,
2171            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2172        ));
2173    }
2174
2175    #[test]
2176    fn test_eip1559_tx_truncated_data() {
2177        // Arrange: Create a truncated EIP-1559 transaction
2178        let parser = EthereumParser::new();
2179
2180        // First create a valid EIP-1559 transaction
2181        let valid_raw = encode_eip1559_tx(
2182            1,                               // chain_id
2183            0,                               // nonce
2184            1_000_000_000,                   // max_priority_fee_per_gas
2185            2_000_000_000,                   // max_fee_per_gas
2186            21000,                           // gas_limit
2187            Some(Address::from([0x35; 20])), // to
2188            U256::ZERO,                      // value
2189            Bytes::new(),                    // data
2190        );
2191
2192        // Truncate it to simulate missing fields
2193        let truncated = &valid_raw[..valid_raw.len() / 2];
2194
2195        // Act
2196        let result = parser.parse(truncated);
2197
2198        // Assert: Should fail with InvalidRlp or MalformedTransaction
2199        assert!(result.is_err());
2200        assert!(matches!(
2201            result,
2202            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2203        ));
2204    }
2205
2206    #[test]
2207    fn test_eip2930_tx_truncated_data() {
2208        // Arrange: Create a truncated EIP-2930 transaction
2209        let parser = EthereumParser::new();
2210
2211        // First create a valid EIP-2930 transaction
2212        let valid_raw = encode_eip2930_tx(
2213            1,                               // chain_id
2214            0,                               // nonce
2215            1_000_000_000,                   // gas_price
2216            21000,                           // gas_limit
2217            Some(Address::from([0x35; 20])), // to
2218            U256::ZERO,                      // value
2219            Bytes::new(),                    // data
2220        );
2221
2222        // Truncate it to simulate missing fields
2223        let truncated = &valid_raw[..valid_raw.len() / 2];
2224
2225        // Act
2226        let result = parser.parse(truncated);
2227
2228        // Assert: Should fail with InvalidRlp or MalformedTransaction
2229        assert!(result.is_err());
2230        assert!(matches!(
2231            result,
2232            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2233        ));
2234    }
2235
2236    #[test]
2237    fn test_legacy_tx_invalid_rlp_structure() {
2238        // Arrange: Create invalid RLP data that doesn't represent a valid transaction
2239        let parser = EthereumParser::new();
2240
2241        // Invalid RLP: claim to be a long list but provide insufficient data
2242        let invalid_rlp = vec![0xf8, 0xff, 0x01, 0x02, 0x03];
2243
2244        // Act
2245        let result = parser.parse(&invalid_rlp);
2246
2247        // Assert: Should fail with InvalidRlp or MalformedTransaction
2248        assert!(result.is_err());
2249        assert!(matches!(
2250            result,
2251            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2252        ));
2253    }
2254
2255    #[test]
2256    fn test_eip1559_tx_invalid_rlp_structure() {
2257        // Arrange: Create invalid EIP-1559 transaction with malformed RLP
2258        let parser = EthereumParser::new();
2259
2260        // Type 2 transaction with invalid RLP payload
2261        let invalid = vec![0x02, 0xf8, 0xff, 0x01, 0x02, 0x03];
2262
2263        // Act
2264        let result = parser.parse(&invalid);
2265
2266        // Assert: Should fail with InvalidRlp or MalformedTransaction
2267        assert!(result.is_err());
2268        assert!(matches!(
2269            result,
2270            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2271        ));
2272    }
2273
2274    #[test]
2275    fn test_analyze_erc20_returns_none_for_non_erc20() {
2276        // This test exercises the None return path in analyze_erc20
2277        // by providing invalid ERC-20 calldata
2278        let parser = EthereumParser::new();
2279
2280        let contract = Address::from([0xaa; 20]);
2281        // Calldata with unknown selector (not ERC-20)
2282        let invalid_calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78]);
2283
2284        let raw = encode_eip1559_tx(
2285            1,                // chain_id
2286            0,                // nonce
2287            1_000_000_000,    // max_priority_fee_per_gas
2288            2_000_000_000,    // max_fee_per_gas
2289            100_000,          // gas_limit
2290            Some(contract),   // to
2291            U256::ZERO,       // value
2292            invalid_calldata, // data (invalid ERC-20)
2293        );
2294
2295        let result = parser.parse(&raw);
2296        assert!(result.is_ok());
2297
2298        let parsed = result.unwrap();
2299
2300        // Should be ContractCall, not TokenTransfer/TokenApproval
2301        assert_eq!(parsed.tx_type, TxType::ContractCall);
2302        // Token address should NOT be set
2303        assert!(parsed.token_address.is_none());
2304    }
2305
2306    // ------------------------------------------------------------------------
2307    // Unsigned Transaction Parsing Tests
2308    // ------------------------------------------------------------------------
2309
2310    /// Helper to encode an unsigned EIP-1559 tx (9 items, no signature)
2311    fn encode_unsigned_eip1559_tx(
2312        chain_id: u64,
2313        nonce: u64,
2314        max_priority_fee_per_gas: u128,
2315        max_fee_per_gas: u128,
2316        gas_limit: u64,
2317        to: Option<Address>,
2318        value: U256,
2319        data: Bytes,
2320    ) -> Vec<u8> {
2321        use alloy_consensus::TxEip1559;
2322        use alloy_rlp::Encodable;
2323
2324        let tx = TxEip1559 {
2325            chain_id,
2326            nonce,
2327            max_priority_fee_per_gas,
2328            max_fee_per_gas,
2329            gas_limit,
2330            to: to.map_or(TxKind::Create, TxKind::Call),
2331            value,
2332            input: data,
2333            access_list: Default::default(),
2334        };
2335
2336        let mut payload = Vec::new();
2337        tx.encode(&mut payload);
2338
2339        let mut buf = vec![0x02]; // EIP-1559 type prefix
2340        buf.extend_from_slice(&payload);
2341        buf
2342    }
2343
2344    /// Helper to encode an unsigned EIP-2930 tx (8 items, no signature)
2345    fn encode_unsigned_eip2930_tx(
2346        chain_id: u64,
2347        nonce: u64,
2348        gas_price: u128,
2349        gas_limit: u64,
2350        to: Option<Address>,
2351        value: U256,
2352        data: Bytes,
2353    ) -> Vec<u8> {
2354        use alloy_consensus::TxEip2930;
2355        use alloy_rlp::Encodable;
2356
2357        let tx = TxEip2930 {
2358            chain_id,
2359            nonce,
2360            gas_price,
2361            gas_limit,
2362            to: to.map_or(TxKind::Create, TxKind::Call),
2363            value,
2364            input: data,
2365            access_list: Default::default(),
2366        };
2367
2368        let mut payload = Vec::new();
2369        tx.encode(&mut payload);
2370
2371        let mut buf = vec![0x01]; // EIP-2930 type prefix
2372        buf.extend_from_slice(&payload);
2373        buf
2374    }
2375
2376    /// Helper to encode an unsigned legacy tx (6 items, no signature)
2377    fn encode_unsigned_legacy_tx(
2378        nonce: u64,
2379        gas_price: u128,
2380        gas_limit: u64,
2381        to: Option<Address>,
2382        value: U256,
2383        data: Bytes,
2384    ) -> Vec<u8> {
2385        use alloy_consensus::TxLegacy;
2386        use alloy_rlp::Encodable;
2387
2388        let tx = TxLegacy {
2389            chain_id: None, // pre-EIP-155 unsigned: 6 items
2390            nonce,
2391            gas_price,
2392            gas_limit,
2393            to: to.map_or(TxKind::Create, TxKind::Call),
2394            value,
2395            input: data,
2396        };
2397
2398        let mut buf = Vec::new();
2399        tx.encode(&mut buf);
2400        buf
2401    }
2402
2403    #[test]
2404    fn test_parse_unsigned_eip1559() {
2405        let parser = EthereumParser::new();
2406        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2407        let raw = encode_unsigned_eip1559_tx(
2408            1,
2409            0,
2410            1_000_000_000,
2411            2_000_000_000,
2412            21000,
2413            Some(recipient),
2414            U256::from(1_000_000_000_000_000_000u64),
2415            Bytes::new(),
2416        );
2417
2418        let result = parser.parse(&raw);
2419        assert!(
2420            result.is_ok(),
2421            "Should parse unsigned EIP-1559: {:?}",
2422            result.err()
2423        );
2424        let parsed = result.unwrap();
2425        assert_eq!(parsed.chain_id, Some(1));
2426        assert_eq!(parsed.nonce, Some(0));
2427        assert_eq!(
2428            parsed.metadata.get("unsigned"),
2429            Some(&serde_json::Value::Bool(true))
2430        );
2431    }
2432
2433    #[test]
2434    fn test_parse_unsigned_eip2930() {
2435        let parser = EthereumParser::new();
2436        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2437        let raw = encode_unsigned_eip2930_tx(
2438            1,
2439            0,
2440            1_000_000_000,
2441            21000,
2442            Some(recipient),
2443            U256::from(1_000_000_000_000_000_000u64),
2444            Bytes::new(),
2445        );
2446
2447        let result = parser.parse(&raw);
2448        assert!(
2449            result.is_ok(),
2450            "Should parse unsigned EIP-2930: {:?}",
2451            result.err()
2452        );
2453        let parsed = result.unwrap();
2454        assert_eq!(parsed.chain_id, Some(1));
2455        assert_eq!(
2456            parsed.metadata.get("unsigned"),
2457            Some(&serde_json::Value::Bool(true))
2458        );
2459    }
2460
2461    #[test]
2462    fn test_parse_unsigned_legacy_pre_eip155() {
2463        let parser = EthereumParser::new();
2464        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2465        let raw = encode_unsigned_legacy_tx(
2466            0,
2467            20_000_000_000,
2468            21000,
2469            Some(recipient),
2470            U256::from(1_000_000_000_000_000_000u64),
2471            Bytes::new(),
2472        );
2473
2474        let result = parser.parse(&raw);
2475        assert!(
2476            result.is_ok(),
2477            "Should parse unsigned legacy: {:?}",
2478            result.err()
2479        );
2480        let parsed = result.unwrap();
2481        assert_eq!(parsed.chain_id, Some(1)); // defaults to mainnet
2482        assert_eq!(
2483            parsed.metadata.get("unsigned"),
2484            Some(&serde_json::Value::Bool(true))
2485        );
2486    }
2487
2488    // ------------------------------------------------------------------------
2489    // assemble_signed() Tests
2490    // ------------------------------------------------------------------------
2491
2492    #[test]
2493    fn test_assemble_signed_legacy_roundtrip() {
2494        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2495        let sig = Signature::new(
2496            U256::from(0xdeadbeef_u64),
2497            U256::from(0xcafebabe_u64),
2498            false,
2499        );
2500
2501        let tx = alloy_consensus::TxLegacy {
2502            chain_id: Some(1),
2503            nonce: 42,
2504            gas_price: 20_000_000_000,
2505            gas_limit: 21000,
2506            to: TxKind::Call(recipient),
2507            value: U256::from(1_000_000_000_000_000_000u64),
2508            input: Bytes::new(),
2509        };
2510
2511        // Encode the signed tx
2512        let mut expected = Vec::new();
2513        alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2514            &tx,
2515            &sig,
2516            &mut expected,
2517        );
2518
2519        // Now assemble from the same signed bytes (which is the input) + our signature
2520        // Build a 65-byte signature: r(32) || s(32) || v(1)
2521        let mut sig_bytes = [0u8; 65];
2522        sig.r()
2523            .to_be_bytes::<32>()
2524            .iter()
2525            .enumerate()
2526            .for_each(|(i, b)| sig_bytes[i] = *b);
2527        sig.s()
2528            .to_be_bytes::<32>()
2529            .iter()
2530            .enumerate()
2531            .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2532        sig_bytes[64] = u8::from(sig.v());
2533
2534        let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2535        assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2536        assert_eq!(assembled.unwrap(), expected);
2537    }
2538
2539    #[test]
2540    fn test_assemble_signed_eip1559_roundtrip() {
2541        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2542        let sig = Signature::new(U256::from(0xdeadbeef_u64), U256::from(0xcafebabe_u64), true);
2543
2544        let tx = alloy_consensus::TxEip1559 {
2545            chain_id: 1,
2546            nonce: 10,
2547            max_priority_fee_per_gas: 1_000_000_000,
2548            max_fee_per_gas: 2_000_000_000,
2549            gas_limit: 21000,
2550            to: TxKind::Call(recipient),
2551            value: U256::from(500_000_000_000_000_000u64),
2552            input: Bytes::new(),
2553            access_list: Default::default(),
2554        };
2555
2556        let mut expected = vec![0x02];
2557        alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2558            &tx,
2559            &sig,
2560            &mut expected,
2561        );
2562
2563        let mut sig_bytes = [0u8; 65];
2564        sig.r()
2565            .to_be_bytes::<32>()
2566            .iter()
2567            .enumerate()
2568            .for_each(|(i, b)| sig_bytes[i] = *b);
2569        sig.s()
2570            .to_be_bytes::<32>()
2571            .iter()
2572            .enumerate()
2573            .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2574        sig_bytes[64] = u8::from(sig.v());
2575
2576        let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2577        assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2578        assert_eq!(assembled.unwrap(), expected);
2579    }
2580
2581    #[test]
2582    fn test_assemble_signed_eip2930_roundtrip() {
2583        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2584        let sig = Signature::new(
2585            U256::from(0xdeadbeef_u64),
2586            U256::from(0xcafebabe_u64),
2587            false,
2588        );
2589
2590        let tx = alloy_consensus::TxEip2930 {
2591            chain_id: 1,
2592            nonce: 5,
2593            gas_price: 20_000_000_000,
2594            gas_limit: 21000,
2595            to: TxKind::Call(recipient),
2596            value: U256::from(1_000_000_000_000_000_000u64),
2597            input: Bytes::new(),
2598            access_list: Default::default(),
2599        };
2600
2601        let mut expected = vec![0x01];
2602        alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2603            &tx,
2604            &sig,
2605            &mut expected,
2606        );
2607
2608        let mut sig_bytes = [0u8; 65];
2609        sig.r()
2610            .to_be_bytes::<32>()
2611            .iter()
2612            .enumerate()
2613            .for_each(|(i, b)| sig_bytes[i] = *b);
2614        sig.s()
2615            .to_be_bytes::<32>()
2616            .iter()
2617            .enumerate()
2618            .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2619        sig_bytes[64] = u8::from(sig.v());
2620
2621        let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2622        assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2623        assert_eq!(assembled.unwrap(), expected);
2624    }
2625
2626    #[test]
2627    fn test_assemble_signed_from_unsigned_eip1559() {
2628        let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2629        let sig = Signature::new(
2630            U256::from(0xdeadbeef_u64),
2631            U256::from(0xcafebabe_u64),
2632            false,
2633        );
2634
2635        let tx = alloy_consensus::TxEip1559 {
2636            chain_id: 1,
2637            nonce: 0,
2638            max_priority_fee_per_gas: 1_000_000_000,
2639            max_fee_per_gas: 2_000_000_000,
2640            gas_limit: 21000,
2641            to: TxKind::Call(recipient),
2642            value: U256::from(1_000_000_000_000_000_000u64),
2643            input: Bytes::new(),
2644            access_list: Default::default(),
2645        };
2646
2647        // Create unsigned raw bytes
2648        let unsigned_raw = encode_unsigned_eip1559_tx(
2649            1,
2650            0,
2651            1_000_000_000,
2652            2_000_000_000,
2653            21000,
2654            Some(recipient),
2655            U256::from(1_000_000_000_000_000_000u64),
2656            Bytes::new(),
2657        );
2658
2659        // Build expected signed output
2660        let mut expected = vec![0x02];
2661        alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2662            &tx,
2663            &sig,
2664            &mut expected,
2665        );
2666
2667        let mut sig_bytes = [0u8; 65];
2668        sig.r()
2669            .to_be_bytes::<32>()
2670            .iter()
2671            .enumerate()
2672            .for_each(|(i, b)| sig_bytes[i] = *b);
2673        sig.s()
2674            .to_be_bytes::<32>()
2675            .iter()
2676            .enumerate()
2677            .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2678        sig_bytes[64] = u8::from(sig.v());
2679
2680        let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
2681        assert!(
2682            assembled.is_ok(),
2683            "Assembly from unsigned failed: {:?}",
2684            assembled.err()
2685        );
2686        assert_eq!(assembled.unwrap(), expected);
2687    }
2688
2689    #[test]
2690    fn test_assemble_signed_invalid_signature_length() {
2691        let raw = encode_eip1559_tx(
2692            1,
2693            0,
2694            1_000_000_000,
2695            2_000_000_000,
2696            21000,
2697            Some(Address::ZERO),
2698            U256::ZERO,
2699            Bytes::new(),
2700        );
2701
2702        let short_sig = [0u8; 32]; // too short
2703        let result = EthereumParser::assemble_signed(&raw, &short_sig);
2704        assert!(result.is_err());
2705        let err = result.unwrap_err();
2706        assert!(err.to_string().contains("expected 65-byte signature"));
2707    }
2708
2709    #[test]
2710    fn test_assemble_signed_invalid_recovery_id() {
2711        let raw = encode_eip1559_tx(
2712            1,
2713            0,
2714            1_000_000_000,
2715            2_000_000_000,
2716            21000,
2717            Some(Address::ZERO),
2718            U256::ZERO,
2719            Bytes::new(),
2720        );
2721
2722        let mut bad_sig = [0u8; 65];
2723        bad_sig[64] = 2; // invalid: must be 0 or 1
2724        let result = EthereumParser::assemble_signed(&raw, &bad_sig);
2725        assert!(result.is_err());
2726        let err = result.unwrap_err();
2727        assert!(err.to_string().contains("invalid recovery id"));
2728    }
2729}