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 has 9 items: [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
124        if items.len() != 9 {
125            return Err(ParseError::MalformedTransaction {
126                context: format!("legacy transaction expected 9 items, got {}", items.len()),
127            });
128        }
129
130        // Extract fields using safe indexing
131        let nonce_bytes = items
132            .first()
133            .ok_or_else(|| ParseError::MalformedTransaction {
134                context: "missing nonce field".to_string(),
135            })?;
136        let to_bytes = items
137            .get(3)
138            .ok_or_else(|| ParseError::MalformedTransaction {
139                context: "missing to field".to_string(),
140            })?;
141        let value_bytes = items
142            .get(4)
143            .ok_or_else(|| ParseError::MalformedTransaction {
144                context: "missing value field".to_string(),
145            })?;
146        let data_bytes = items
147            .get(5)
148            .ok_or_else(|| ParseError::MalformedTransaction {
149                context: "missing data field".to_string(),
150            })?;
151        let v_bytes = items
152            .get(6)
153            .ok_or_else(|| ParseError::MalformedTransaction {
154                context: "missing v field".to_string(),
155            })?;
156
157        // Decode nonce
158        let nonce = decode_u64(nonce_bytes)?;
159
160        // Decode recipient (can be empty for contract deployments)
161        let recipient = decode_optional_address(to_bytes)?;
162
163        // Decode value
164        let amount = decode_u256(value_bytes)?;
165
166        // Decode data
167        let data = decode_bytes(data_bytes)?;
168
169        // Decode v for chain_id extraction
170        let v = decode_u64(v_bytes)?;
171
172        // Extract chain_id from v (EIP-155)
173        let chain_id = if v >= 35 {
174            // EIP-155: chain_id = (v - 35) / 2
175            (v - 35) / 2
176        } else if v == 27 || v == 28 {
177            // Pre-EIP-155: assume mainnet
178            1
179        } else {
180            // Invalid v value, but we'll default to mainnet
181            1
182        };
183
184        // Check for ERC-20 token call
185        let erc20_info = recipient
186            .as_ref()
187            .and_then(|addr| Self::analyze_erc20(addr, &data));
188
189        // Determine transaction type (ERC-20 detection takes precedence)
190        let (final_tx_type, final_recipient, final_amount, token_address) =
191            erc20_info.as_ref().map_or_else(
192                || {
193                    (
194                        Self::determine_tx_type(recipient.as_ref(), &data, &amount),
195                        recipient.map(|addr| format!("{addr}")),
196                        Some(amount),
197                        None,
198                    )
199                },
200                |info| {
201                    (
202                        info.tx_type,
203                        Some(format!("{}", info.recipient)),
204                        Some(info.amount),
205                        Some(format!("{}", info.token_address)),
206                    )
207                },
208            );
209
210        // Compute transaction hash from the hash source (includes type prefix for typed txs)
211        let hash = keccak256(hash_source);
212
213        Ok(ParsedTx {
214            hash: hash.into(),
215            recipient: final_recipient,
216            amount: final_amount,
217            token: None, // Token symbol lookup not implemented yet
218            token_address,
219            tx_type: final_tx_type,
220            chain: "ethereum".to_string(),
221            nonce: Some(nonce),
222            chain_id: Some(chain_id),
223            metadata: std::collections::HashMap::new(),
224        })
225    }
226
227    /// Parse an EIP-2930 transaction (type 1).
228    ///
229    /// EIP-2930 transactions are encoded as:
230    /// `0x01 || RLP([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS])`
231    fn parse_eip2930(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
232        // Decode the RLP list from the payload (without type byte)
233        let items = decode_list(payload)?;
234
235        // EIP-2930 has 11 items
236        if items.len() != 11 {
237            return Err(ParseError::MalformedTransaction {
238                context: format!(
239                    "EIP-2930 transaction expected 11 items, got {}",
240                    items.len()
241                ),
242            });
243        }
244
245        // Extract fields using safe indexing
246        let chain_id_bytes = items
247            .first()
248            .ok_or_else(|| ParseError::MalformedTransaction {
249                context: "missing chainId field".to_string(),
250            })?;
251        let nonce_bytes = items
252            .get(1)
253            .ok_or_else(|| ParseError::MalformedTransaction {
254                context: "missing nonce field".to_string(),
255            })?;
256        let to_bytes = items
257            .get(4)
258            .ok_or_else(|| ParseError::MalformedTransaction {
259                context: "missing to field".to_string(),
260            })?;
261        let value_bytes = items
262            .get(5)
263            .ok_or_else(|| ParseError::MalformedTransaction {
264                context: "missing value field".to_string(),
265            })?;
266        let data_bytes = items
267            .get(6)
268            .ok_or_else(|| ParseError::MalformedTransaction {
269                context: "missing data field".to_string(),
270            })?;
271
272        // Decode fields
273        let chain_id = decode_u64(chain_id_bytes)?;
274        let nonce = decode_u64(nonce_bytes)?;
275        let recipient = decode_optional_address(to_bytes)?;
276        let amount = decode_u256(value_bytes)?;
277        let data = decode_bytes(data_bytes)?;
278
279        // Check for ERC-20 token call
280        let erc20_info = recipient
281            .as_ref()
282            .and_then(|addr| Self::analyze_erc20(addr, &data));
283
284        // Determine transaction type (ERC-20 detection takes precedence)
285        let (final_tx_type, final_recipient, final_amount, token_address) =
286            erc20_info.as_ref().map_or_else(
287                || {
288                    (
289                        Self::determine_tx_type(recipient.as_ref(), &data, &amount),
290                        recipient.map(|addr| format!("{addr}")),
291                        Some(amount),
292                        None,
293                    )
294                },
295                |info| {
296                    (
297                        info.tx_type,
298                        Some(format!("{}", info.recipient)),
299                        Some(info.amount),
300                        Some(format!("{}", info.token_address)),
301                    )
302                },
303            );
304
305        // Compute transaction hash (includes type byte)
306        let hash = keccak256(raw);
307
308        Ok(ParsedTx {
309            hash: hash.into(),
310            recipient: final_recipient,
311            amount: final_amount,
312            token: None, // Token symbol lookup not implemented yet
313            token_address,
314            tx_type: final_tx_type,
315            chain: "ethereum".to_string(),
316            nonce: Some(nonce),
317            chain_id: Some(chain_id),
318            metadata: std::collections::HashMap::new(),
319        })
320    }
321
322    /// Parse an EIP-1559 transaction (type 2).
323    ///
324    /// EIP-1559 transactions are encoded as:
325    /// `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])`
326    fn parse_eip1559(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
327        // Decode the RLP list from the payload (without type byte)
328        let items = decode_list(payload)?;
329
330        // EIP-1559 has 12 items
331        if items.len() != 12 {
332            return Err(ParseError::MalformedTransaction {
333                context: format!(
334                    "EIP-1559 transaction expected 12 items, got {}",
335                    items.len()
336                ),
337            });
338        }
339
340        // Extract fields using safe indexing
341        let chain_id_bytes = items
342            .first()
343            .ok_or_else(|| ParseError::MalformedTransaction {
344                context: "missing chainId field".to_string(),
345            })?;
346        let nonce_bytes = items
347            .get(1)
348            .ok_or_else(|| ParseError::MalformedTransaction {
349                context: "missing nonce field".to_string(),
350            })?;
351        let to_bytes = items
352            .get(5)
353            .ok_or_else(|| ParseError::MalformedTransaction {
354                context: "missing to field".to_string(),
355            })?;
356        let value_bytes = items
357            .get(6)
358            .ok_or_else(|| ParseError::MalformedTransaction {
359                context: "missing value field".to_string(),
360            })?;
361        let data_bytes = items
362            .get(7)
363            .ok_or_else(|| ParseError::MalformedTransaction {
364                context: "missing data field".to_string(),
365            })?;
366
367        // Decode fields
368        let chain_id = decode_u64(chain_id_bytes)?;
369        let nonce = decode_u64(nonce_bytes)?;
370        let recipient = decode_optional_address(to_bytes)?;
371        let amount = decode_u256(value_bytes)?;
372        let data = decode_bytes(data_bytes)?;
373
374        // Check for ERC-20 token call
375        let erc20_info = recipient
376            .as_ref()
377            .and_then(|addr| Self::analyze_erc20(addr, &data));
378
379        // Determine transaction type (ERC-20 detection takes precedence)
380        let (final_tx_type, final_recipient, final_amount, token_address) =
381            erc20_info.as_ref().map_or_else(
382                || {
383                    (
384                        Self::determine_tx_type(recipient.as_ref(), &data, &amount),
385                        recipient.map(|addr| format!("{addr}")),
386                        Some(amount),
387                        None,
388                    )
389                },
390                |info| {
391                    (
392                        info.tx_type,
393                        Some(format!("{}", info.recipient)),
394                        Some(info.amount),
395                        Some(format!("{}", info.token_address)),
396                    )
397                },
398            );
399
400        // Compute transaction hash (includes type byte)
401        let hash = keccak256(raw);
402
403        Ok(ParsedTx {
404            hash: hash.into(),
405            recipient: final_recipient,
406            amount: final_amount,
407            token: None, // Token symbol lookup not implemented yet
408            token_address,
409            tx_type: final_tx_type,
410            chain: "ethereum".to_string(),
411            nonce: Some(nonce),
412            chain_id: Some(chain_id),
413            metadata: std::collections::HashMap::new(),
414        })
415    }
416
417    /// Determine the transaction type based on recipient, data, and amount.
418    ///
419    /// - `Deployment`: No recipient (contract creation)
420    /// - `ContractCall`: Has data (non-empty calldata)
421    /// - `Transfer`: Simple ETH transfer (has recipient, no data)
422    const fn determine_tx_type(
423        recipient: Option<&alloy_primitives::Address>,
424        data: &[u8],
425        _amount: &U256,
426    ) -> TxType {
427        if recipient.is_none() {
428            // No recipient = contract deployment
429            TxType::Deployment
430        } else if !data.is_empty() {
431            // Has data = contract call
432            TxType::ContractCall
433        } else {
434            // Simple ETH transfer
435            TxType::Transfer
436        }
437    }
438
439    /// Analyze transaction data and return ERC-20 specific information if applicable.
440    ///
441    /// This function checks if the transaction is an ERC-20 token call and extracts
442    /// the relevant information for enriching the `ParsedTx`.
443    ///
444    /// # Arguments
445    ///
446    /// * `contract_address` - The address of the contract being called (the token contract)
447    /// * `data` - The transaction calldata
448    ///
449    /// # Returns
450    ///
451    /// Returns `Some(Erc20Info)` if this is an ERC-20 call, `None` otherwise.
452    fn analyze_erc20(contract_address: &Address, data: &[u8]) -> Option<Erc20Info> {
453        let erc20_call = parse_erc20_call(data)?;
454
455        let (tx_type, recipient_addr) = match &erc20_call {
456            Erc20Call::Transfer { to, .. } | Erc20Call::TransferFrom { to, .. } => {
457                (TxType::TokenTransfer, Address::from_slice(to))
458            }
459            Erc20Call::Approve { spender, .. } => {
460                (TxType::TokenApproval, Address::from_slice(spender))
461            }
462        };
463
464        Some(Erc20Info {
465            tx_type,
466            token_address: *contract_address,
467            recipient: recipient_addr,
468            amount: *erc20_call.amount(),
469        })
470    }
471}
472
473/// Information extracted from an ERC-20 function call.
474///
475/// Used internally to enrich `ParsedTx` with token-specific data.
476struct Erc20Info {
477    /// The transaction type (`TokenTransfer` or `TokenApproval`).
478    tx_type: TxType,
479    /// The token contract address.
480    token_address: Address,
481    /// The actual recipient/spender address from the ERC-20 call.
482    recipient: Address,
483    /// The token amount from the ERC-20 call.
484    amount: U256,
485}
486
487impl Chain for EthereumParser {
488    /// Returns the chain identifier.
489    ///
490    /// # Returns
491    ///
492    /// Always returns `"ethereum"`.
493    fn id(&self) -> &'static str {
494        "ethereum"
495    }
496
497    /// Parse raw transaction bytes into a [`ParsedTx`].
498    ///
499    /// This method detects the transaction type and delegates to the
500    /// appropriate parser:
501    ///
502    /// - Legacy transactions (no type prefix or type 0)
503    /// - EIP-2930 transactions (type 1)
504    /// - EIP-1559 transactions (type 2)
505    ///
506    /// # Arguments
507    ///
508    /// * `raw` - The raw transaction bytes
509    ///
510    /// # Returns
511    ///
512    /// * `Ok(ParsedTx)` - Successfully parsed transaction
513    /// * `Err(ParseError)` - Parsing failed
514    ///
515    /// # Errors
516    ///
517    /// Returns a [`ParseError`] if:
518    /// - The transaction type is not supported
519    /// - The RLP encoding is invalid
520    /// - Required fields are missing or malformed
521    fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
522        if raw.is_empty() {
523            return Err(ParseError::MalformedTransaction {
524                context: "empty transaction data".to_string(),
525            });
526        }
527
528        // Detect transaction type
529        match detect_tx_type(raw) {
530            None => {
531                // Legacy transaction (starts with RLP list prefix 0xc0-0xff)
532                // or potentially invalid data
533                if crate::rlp::is_list(raw) {
534                    Self::parse_legacy(raw)
535                } else {
536                    Err(ParseError::MalformedTransaction {
537                        context:
538                            "invalid transaction format: not a valid RLP list or typed transaction"
539                                .to_string(),
540                    })
541                }
542            }
543            Some(0) => {
544                // Type 0 - treat as legacy but skip the type byte for RLP parsing
545                // Hash must be computed over full raw bytes including type prefix
546                let payload = typed_tx_payload(raw)?;
547                Self::parse_legacy_with_hash_source(raw, payload)
548            }
549            Some(1) => {
550                // EIP-2930 (Access List)
551                let payload = typed_tx_payload(raw)?;
552                Self::parse_eip2930(raw, payload)
553            }
554            Some(2) => {
555                // EIP-1559 (Dynamic Fee)
556                let payload = typed_tx_payload(raw)?;
557                Self::parse_eip1559(raw, payload)
558            }
559            Some(_) => Err(ParseError::UnknownTxType),
560        }
561    }
562
563    /// Returns the elliptic curve used by Ethereum.
564    ///
565    /// # Returns
566    ///
567    /// Always returns [`CurveType::Secp256k1`].
568    fn curve(&self) -> CurveType {
569        CurveType::Secp256k1
570    }
571
572    /// Check if this parser supports a specific transaction version.
573    ///
574    /// # Arguments
575    ///
576    /// * `version` - The transaction type byte
577    ///
578    /// # Returns
579    ///
580    /// * `true` for versions 0, 1, 2 (Legacy, EIP-2930, EIP-1559)
581    /// * `false` for other versions (e.g., EIP-4844 blob transactions)
582    fn supports_version(&self, version: u8) -> bool {
583        matches!(version, 0..=2)
584    }
585}
586
587// ============================================================================
588// Tests
589// ============================================================================
590
591#[cfg(test)]
592mod tests {
593    #![allow(
594        clippy::expect_used,
595        clippy::unwrap_used,
596        clippy::panic,
597        clippy::indexing_slicing,
598        clippy::similar_names,
599        clippy::redundant_clone,
600        clippy::manual_string_new,
601        clippy::needless_raw_string_hashes,
602        clippy::needless_collect,
603        clippy::unreadable_literal,
604        clippy::default_trait_access,
605        clippy::too_many_arguments,
606        clippy::default_constructed_unit_structs
607    )]
608
609    use super::*;
610    use alloy_consensus::{transaction::RlpEcdsaEncodableTx, TxEip1559, TxEip2930, TxLegacy};
611    use alloy_primitives::{hex, Address, Bytes, Signature, TxKind};
612
613    /// Helper to encode a legacy transaction with a fake signature
614    fn encode_legacy_tx(
615        nonce: u64,
616        gas_price: u128,
617        gas_limit: u64,
618        to: Option<Address>,
619        value: U256,
620        data: Bytes,
621        chain_id: Option<u64>,
622    ) -> Vec<u8> {
623        let tx = TxLegacy {
624            chain_id,
625            nonce,
626            gas_price,
627            gas_limit,
628            to: to.map_or(TxKind::Create, TxKind::Call),
629            value,
630            input: data,
631        };
632
633        // Create a fake signature
634        let sig = Signature::new(
635            U256::from(0xffff_ffff_ffff_ffffu64),
636            U256::from(0xffff_ffff_ffff_ffffu64),
637            false,
638        );
639
640        let mut buf = Vec::new();
641        tx.rlp_encode_signed(&sig, &mut buf);
642        buf
643    }
644
645    /// Helper to encode an EIP-2930 transaction with a fake signature
646    fn encode_eip2930_tx(
647        chain_id: u64,
648        nonce: u64,
649        gas_price: u128,
650        gas_limit: u64,
651        to: Option<Address>,
652        value: U256,
653        data: Bytes,
654    ) -> Vec<u8> {
655        let tx = TxEip2930 {
656            chain_id,
657            nonce,
658            gas_price,
659            gas_limit,
660            to: to.map_or(TxKind::Create, TxKind::Call),
661            value,
662            input: data,
663            access_list: Default::default(),
664        };
665
666        // Create a fake signature
667        let sig = Signature::new(
668            U256::from(0xffff_ffff_ffff_ffffu64),
669            U256::from(0xffff_ffff_ffff_ffffu64),
670            false,
671        );
672
673        // Build buffer with type prefix
674        let mut buf = Vec::new();
675        buf.push(0x01); // EIP-2930 type prefix
676        tx.rlp_encode_signed(&sig, &mut buf);
677        buf
678    }
679
680    /// Helper to encode an EIP-1559 transaction with a fake signature
681    fn encode_eip1559_tx(
682        chain_id: u64,
683        nonce: u64,
684        max_priority_fee_per_gas: u128,
685        max_fee_per_gas: u128,
686        gas_limit: u64,
687        to: Option<Address>,
688        value: U256,
689        data: Bytes,
690    ) -> Vec<u8> {
691        let tx = TxEip1559 {
692            chain_id,
693            nonce,
694            max_priority_fee_per_gas,
695            max_fee_per_gas,
696            gas_limit,
697            to: to.map_or(TxKind::Create, TxKind::Call),
698            value,
699            input: data,
700            access_list: Default::default(),
701        };
702
703        // Create a fake signature
704        let sig = Signature::new(
705            U256::from(0xffff_ffff_ffff_ffffu64),
706            U256::from(0xffff_ffff_ffff_ffffu64),
707            false,
708        );
709
710        // Build buffer with type prefix
711        let mut buf = Vec::new();
712        buf.push(0x02); // EIP-1559 type prefix
713        tx.rlp_encode_signed(&sig, &mut buf);
714        buf
715    }
716
717    // ------------------------------------------------------------------------
718    // Constructor and Basic Tests
719    // ------------------------------------------------------------------------
720
721    #[test]
722    fn test_ethereum_parser_new() {
723        let parser = EthereumParser::new();
724        assert_eq!(parser.id(), "ethereum");
725    }
726
727    #[test]
728    fn test_ethereum_parser_default() {
729        let parser = EthereumParser::default();
730        assert_eq!(parser.id(), "ethereum");
731    }
732
733    #[test]
734    fn test_ethereum_parser_clone() {
735        let parser = EthereumParser::new();
736        let cloned = parser;
737        assert_eq!(cloned.id(), "ethereum");
738    }
739
740    #[test]
741    fn test_ethereum_parser_debug() {
742        let parser = EthereumParser::new();
743        let debug_str = format!("{parser:?}");
744        assert!(debug_str.contains("EthereumParser"));
745    }
746
747    // ------------------------------------------------------------------------
748    // Chain Trait Tests
749    // ------------------------------------------------------------------------
750
751    #[test]
752    fn test_chain_id() {
753        let parser = EthereumParser::new();
754        assert_eq!(parser.id(), "ethereum");
755    }
756
757    #[test]
758    fn test_chain_curve() {
759        let parser = EthereumParser::new();
760        assert_eq!(parser.curve(), CurveType::Secp256k1);
761    }
762
763    #[test]
764    fn test_supports_version() {
765        let parser = EthereumParser::new();
766
767        // Supported versions
768        assert!(parser.supports_version(0)); // Legacy
769        assert!(parser.supports_version(1)); // EIP-2930
770        assert!(parser.supports_version(2)); // EIP-1559
771
772        // Unsupported versions
773        assert!(!parser.supports_version(3)); // EIP-4844 (blobs)
774        assert!(!parser.supports_version(4));
775        assert!(!parser.supports_version(255));
776    }
777
778    // ------------------------------------------------------------------------
779    // Empty Input Tests
780    // ------------------------------------------------------------------------
781
782    #[test]
783    fn test_parse_empty_input() {
784        let parser = EthereumParser::new();
785        let result = parser.parse(&[]);
786
787        assert!(result.is_err());
788        assert!(matches!(
789            result,
790            Err(ParseError::MalformedTransaction { .. })
791        ));
792    }
793
794    // ------------------------------------------------------------------------
795    // Legacy Transaction Tests
796    // ------------------------------------------------------------------------
797
798    #[test]
799    fn test_parse_legacy_transaction() {
800        let parser = EthereumParser::new();
801
802        // Real legacy transaction from Ethereum mainnet
803        // This is a simple ETH transfer
804        // nonce=9, gasPrice=20gwei, gasLimit=21000, to=0x3535..., value=1ETH, data=empty
805        let raw = hex::decode(
806            "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
807        ).expect("valid hex");
808
809        let result = parser.parse(&raw);
810        assert!(result.is_ok(), "parsing failed: {result:?}");
811
812        let parsed = result.expect("should parse successfully");
813
814        // Verify fields
815        assert_eq!(parsed.chain, "ethereum");
816        assert_eq!(parsed.nonce, Some(9));
817        assert_eq!(parsed.tx_type, TxType::Transfer);
818        assert!(parsed.recipient.is_some());
819        assert_eq!(
820            parsed.recipient.as_ref().map(|s| s.to_lowercase()),
821            Some("0x3535353535353535353535353535353535353535".to_string())
822        );
823
824        // Verify amount is 1 ETH (10^18 wei)
825        let expected_amount = U256::from(1_000_000_000_000_000_000u64);
826        assert_eq!(parsed.amount, Some(expected_amount));
827
828        // Verify chain_id extraction from v=37 -> chain_id = (37-35)/2 = 1
829        assert_eq!(parsed.chain_id, Some(1));
830
831        // Verify hash is computed
832        assert_ne!(parsed.hash, [0u8; 32]);
833    }
834
835    #[test]
836    fn test_parse_legacy_transaction_pre_eip155() {
837        let parser = EthereumParser::new();
838
839        // Legacy transaction without chain_id (pre-EIP-155)
840        let to_addr = Address::from([0x12; 20]);
841        let raw = encode_legacy_tx(
842            0,                // nonce
843            1_000_000_000,    // gas_price (1 gwei)
844            21000,            // gas_limit
845            Some(to_addr),    // to
846            U256::ZERO,       // value
847            Bytes::default(), // data
848            None,             // chain_id (None = pre-EIP-155)
849        );
850
851        let result = parser.parse(&raw);
852        assert!(result.is_ok(), "parsing failed: {result:?}");
853
854        let parsed = result.expect("should parse successfully");
855        // Pre-EIP-155 defaults to mainnet (chain_id = 1)
856        assert_eq!(parsed.chain_id, Some(1));
857    }
858
859    #[test]
860    fn test_parse_legacy_contract_deployment() {
861        let parser = EthereumParser::new();
862
863        // Contract deployment: to field is None (Create)
864        let raw = encode_legacy_tx(
865            0,                                         // nonce
866            1_000_000_000,                             // gas_price
867            100000,                                    // gas_limit
868            None,                                      // to = None for deployment
869            U256::ZERO,                                // value
870            Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), // data (some bytecode)
871            Some(1),                                   // chain_id
872        );
873
874        let result = parser.parse(&raw);
875        assert!(result.is_ok(), "parsing failed: {result:?}");
876
877        let parsed = result.expect("should parse successfully");
878        assert_eq!(parsed.tx_type, TxType::Deployment);
879        assert!(parsed.recipient.is_none());
880    }
881
882    #[test]
883    fn test_parse_legacy_contract_call() {
884        let parser = EthereumParser::new();
885
886        // Contract call: has recipient and non-empty data
887        let to_addr = Address::from([0x12; 20]);
888        let raw = encode_legacy_tx(
889            1,                                         // nonce
890            1_000_000_000,                             // gas_price
891            100000,                                    // gas_limit
892            Some(to_addr),                             // to
893            U256::ZERO,                                // value
894            Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), // data (transfer selector)
895            Some(1),                                   // chain_id
896        );
897
898        let result = parser.parse(&raw);
899        assert!(result.is_ok(), "parsing failed: {result:?}");
900
901        let parsed = result.expect("should parse successfully");
902        assert_eq!(parsed.tx_type, TxType::ContractCall);
903        assert!(parsed.recipient.is_some());
904    }
905
906    // ------------------------------------------------------------------------
907    // EIP-2930 Transaction Tests
908    // ------------------------------------------------------------------------
909
910    #[test]
911    fn test_parse_eip2930_transaction() {
912        let parser = EthereumParser::new();
913
914        // EIP-2930 transaction (type 1)
915        let to_addr = Address::from([0x12; 20]);
916        let raw = encode_eip2930_tx(
917            1,                // chain_id
918            0,                // nonce
919            1_000_000_000,    // gas_price
920            21000,            // gas_limit
921            Some(to_addr),    // to
922            U256::ZERO,       // value
923            Bytes::default(), // data
924        );
925
926        let result = parser.parse(&raw);
927        assert!(result.is_ok(), "parsing failed: {result:?}");
928
929        let parsed = result.expect("should parse successfully");
930
931        assert_eq!(parsed.chain, "ethereum");
932        assert_eq!(parsed.chain_id, Some(1));
933        assert_eq!(parsed.nonce, Some(0));
934        assert_eq!(parsed.tx_type, TxType::Transfer);
935        assert!(parsed.recipient.is_some());
936    }
937
938    #[test]
939    fn test_parse_eip2930_contract_deployment() {
940        let parser = EthereumParser::new();
941
942        // EIP-2930 contract deployment (to=None)
943        let raw = encode_eip2930_tx(
944            1,                                         // chain_id
945            0,                                         // nonce
946            1_000_000_000,                             // gas_price
947            100000,                                    // gas_limit
948            None,                                      // to = None for deployment
949            U256::ZERO,                                // value
950            Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), // data
951        );
952
953        let result = parser.parse(&raw);
954        assert!(result.is_ok(), "parsing failed: {result:?}");
955
956        let parsed = result.expect("should parse successfully");
957        assert_eq!(parsed.tx_type, TxType::Deployment);
958        assert!(parsed.recipient.is_none());
959    }
960
961    // ------------------------------------------------------------------------
962    // EIP-1559 Transaction Tests
963    // ------------------------------------------------------------------------
964
965    #[test]
966    fn test_parse_eip1559_transaction() {
967        let parser = EthereumParser::new();
968
969        // EIP-1559 transaction (type 2)
970        let to_addr = Address::from([0x12; 20]);
971        let raw = encode_eip1559_tx(
972            1,                // chain_id
973            0,                // nonce
974            1_000_000_000,    // max_priority_fee_per_gas
975            2_000_000_000,    // max_fee_per_gas
976            21000,            // gas_limit
977            Some(to_addr),    // to
978            U256::ZERO,       // value
979            Bytes::default(), // data
980        );
981
982        let result = parser.parse(&raw);
983        assert!(result.is_ok(), "parsing failed: {result:?}");
984
985        let parsed = result.expect("should parse successfully");
986
987        assert_eq!(parsed.chain, "ethereum");
988        assert_eq!(parsed.chain_id, Some(1));
989        assert_eq!(parsed.nonce, Some(0));
990        assert_eq!(parsed.tx_type, TxType::Transfer);
991        assert!(parsed.recipient.is_some());
992    }
993
994    #[test]
995    fn test_parse_eip1559_with_value() {
996        let parser = EthereumParser::new();
997
998        // EIP-1559 transaction with value
999        let to_addr = Address::from([0x12; 20]);
1000        let value = U256::from(1_000_000_000_000_000_000u64); // 1 ETH
1001        let raw = encode_eip1559_tx(
1002            1,                // chain_id
1003            5,                // nonce
1004            1_000_000_000,    // max_priority_fee_per_gas
1005            100_000_000_000,  // max_fee_per_gas
1006            21000,            // gas_limit
1007            Some(to_addr),    // to
1008            value,            // value
1009            Bytes::default(), // data
1010        );
1011
1012        let result = parser.parse(&raw);
1013        assert!(result.is_ok(), "parsing failed: {result:?}");
1014
1015        let parsed = result.expect("should parse successfully");
1016
1017        assert_eq!(parsed.chain, "ethereum");
1018        assert_eq!(parsed.chain_id, Some(1));
1019        assert_eq!(parsed.nonce, Some(5));
1020        assert_eq!(parsed.tx_type, TxType::Transfer);
1021        assert_eq!(parsed.amount, Some(value));
1022    }
1023
1024    #[test]
1025    fn test_parse_eip1559_contract_deployment() {
1026        let parser = EthereumParser::new();
1027
1028        // EIP-1559 contract deployment (to=None)
1029        let raw = encode_eip1559_tx(
1030            1,                                         // chain_id
1031            0,                                         // nonce
1032            1_000_000_000,                             // max_priority_fee_per_gas
1033            2_000_000_000,                             // max_fee_per_gas
1034            100000,                                    // gas_limit
1035            None,                                      // to = None for deployment
1036            U256::ZERO,                                // value
1037            Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), // data
1038        );
1039
1040        let result = parser.parse(&raw);
1041        assert!(result.is_ok(), "parsing failed: {result:?}");
1042
1043        let parsed = result.expect("should parse successfully");
1044        assert_eq!(parsed.tx_type, TxType::Deployment);
1045        assert!(parsed.recipient.is_none());
1046    }
1047
1048    #[test]
1049    fn test_parse_eip1559_contract_call() {
1050        let parser = EthereumParser::new();
1051
1052        // EIP-1559 contract call (has data)
1053        let to_addr = Address::from([0x12; 20]);
1054        let raw = encode_eip1559_tx(
1055            1,                                         // chain_id
1056            0,                                         // nonce
1057            1_000_000_000,                             // max_priority_fee_per_gas
1058            2_000_000_000,                             // max_fee_per_gas
1059            100000,                                    // gas_limit
1060            Some(to_addr),                             // to
1061            U256::ZERO,                                // value
1062            Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), // data (transfer selector)
1063        );
1064
1065        let result = parser.parse(&raw);
1066        assert!(result.is_ok(), "parsing failed: {result:?}");
1067
1068        let parsed = result.expect("should parse successfully");
1069        assert_eq!(parsed.tx_type, TxType::ContractCall);
1070        assert!(parsed.recipient.is_some());
1071    }
1072
1073    // ------------------------------------------------------------------------
1074    // Hash Computation Tests
1075    // ------------------------------------------------------------------------
1076
1077    #[test]
1078    fn test_hash_is_correctly_computed() {
1079        let parser = EthereumParser::new();
1080
1081        let raw = hex::decode(
1082            "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1083        ).expect("valid hex");
1084
1085        let result = parser.parse(&raw);
1086        assert!(result.is_ok());
1087
1088        let parsed = result.expect("should parse");
1089
1090        // Compute expected hash
1091        let expected_hash = keccak256(&raw);
1092
1093        assert_eq!(parsed.hash, *expected_hash);
1094    }
1095
1096    #[test]
1097    fn test_eip1559_hash_includes_type_prefix() {
1098        let parser = EthereumParser::new();
1099
1100        let to_addr = Address::from([0x12; 20]);
1101        let raw = encode_eip1559_tx(
1102            1,                // chain_id
1103            0,                // nonce
1104            1_000_000_000,    // max_priority_fee_per_gas
1105            2_000_000_000,    // max_fee_per_gas
1106            21000,            // gas_limit
1107            Some(to_addr),    // to
1108            U256::ZERO,       // value
1109            Bytes::default(), // data
1110        );
1111
1112        let result = parser.parse(&raw);
1113        assert!(result.is_ok());
1114
1115        let parsed = result.expect("should parse");
1116
1117        // Hash should be of the entire raw bytes (including type prefix)
1118        let expected_hash = keccak256(&raw);
1119        assert_eq!(parsed.hash, *expected_hash);
1120    }
1121
1122    #[test]
1123    fn test_type0_hash_includes_type_prefix() {
1124        let parser = EthereumParser::new();
1125
1126        // Create a legacy transaction and then prefix it with 0x00 (type 0)
1127        let to_addr = Address::from([0x12; 20]);
1128        let legacy_raw = encode_legacy_tx(
1129            0,                // nonce
1130            1_000_000_000,    // gas_price (1 gwei)
1131            21000,            // gas_limit
1132            Some(to_addr),    // to
1133            U256::ZERO,       // value
1134            Bytes::default(), // data
1135            Some(1),          // chain_id
1136        );
1137
1138        // Create type 0 transaction by prefixing with 0x00
1139        let mut type0_raw = vec![0x00];
1140        type0_raw.extend_from_slice(&legacy_raw);
1141
1142        let result = parser.parse(&type0_raw);
1143        assert!(result.is_ok(), "parsing failed: {result:?}");
1144
1145        let parsed = result.expect("should parse");
1146
1147        // Hash MUST be computed over the full raw bytes INCLUDING the type prefix
1148        let expected_hash = keccak256(&type0_raw);
1149        assert_eq!(
1150            parsed.hash, *expected_hash,
1151            "type 0 hash should include type prefix"
1152        );
1153
1154        // Verify it's NOT the hash of just the payload (without type byte)
1155        let wrong_hash = keccak256(&legacy_raw);
1156        assert_ne!(
1157            parsed.hash, *wrong_hash,
1158            "hash should NOT be computed without type prefix"
1159        );
1160    }
1161
1162    #[test]
1163    fn test_type0_vs_legacy_same_content_different_hash() {
1164        let parser = EthereumParser::new();
1165
1166        // Create a legacy transaction
1167        let to_addr = Address::from([0x12; 20]);
1168        let legacy_raw = encode_legacy_tx(
1169            5,                                        // nonce
1170            2_000_000_000,                            // gas_price (2 gwei)
1171            21000,                                    // gas_limit
1172            Some(to_addr),                            // to
1173            U256::from(1_000_000_000_000_000_000u64), // 1 ETH
1174            Bytes::default(),                         // data
1175            Some(1),                                  // chain_id
1176        );
1177
1178        // Parse as pure legacy (no type prefix)
1179        let legacy_result = parser.parse(&legacy_raw);
1180        assert!(legacy_result.is_ok());
1181        let legacy_parsed = legacy_result.expect("should parse legacy");
1182
1183        // Create type 0 version (same content, with 0x00 prefix)
1184        let mut type0_raw = vec![0x00];
1185        type0_raw.extend_from_slice(&legacy_raw);
1186
1187        // Parse as type 0
1188        let type0_result = parser.parse(&type0_raw);
1189        assert!(type0_result.is_ok());
1190        let type0_parsed = type0_result.expect("should parse type 0");
1191
1192        // Both should have the same transaction data
1193        assert_eq!(legacy_parsed.nonce, type0_parsed.nonce);
1194        assert_eq!(legacy_parsed.recipient, type0_parsed.recipient);
1195        assert_eq!(legacy_parsed.amount, type0_parsed.amount);
1196        assert_eq!(legacy_parsed.chain_id, type0_parsed.chain_id);
1197
1198        // But the hashes MUST be different because type 0 includes the prefix
1199        assert_ne!(
1200            legacy_parsed.hash, type0_parsed.hash,
1201            "legacy and type 0 hashes should differ due to type prefix"
1202        );
1203    }
1204
1205    // ------------------------------------------------------------------------
1206    // Error Handling Tests
1207    // ------------------------------------------------------------------------
1208
1209    #[test]
1210    fn test_parse_unsupported_tx_type() {
1211        let parser = EthereumParser::new();
1212
1213        // Type 3 (EIP-4844 blob tx) - not supported
1214        let raw = hex::decode("03f8c0").expect("valid hex");
1215
1216        let result = parser.parse(&raw);
1217        assert!(result.is_err());
1218        assert!(matches!(result, Err(ParseError::UnknownTxType)));
1219    }
1220
1221    #[test]
1222    fn test_parse_malformed_legacy_too_few_items() {
1223        let parser = EthereumParser::new();
1224
1225        // List with only 3 items instead of 9
1226        let raw = hex::decode("c3010203").expect("valid hex");
1227
1228        let result = parser.parse(&raw);
1229        assert!(result.is_err());
1230        assert!(matches!(
1231            result,
1232            Err(ParseError::MalformedTransaction { .. })
1233        ));
1234    }
1235
1236    #[test]
1237    fn test_parse_invalid_rlp() {
1238        let parser = EthereumParser::new();
1239
1240        // Invalid RLP (claims to be a list but truncated)
1241        let raw = hex::decode("f8ff").expect("valid hex");
1242
1243        let result = parser.parse(&raw);
1244        assert!(result.is_err());
1245    }
1246
1247    #[test]
1248    fn test_parse_not_list_not_typed() {
1249        let parser = EthereumParser::new();
1250
1251        // Single byte that's not a valid type prefix (0x04-0xbf range)
1252        // and not an RLP list (0xc0+)
1253        let raw = hex::decode("80").expect("valid hex");
1254
1255        let result = parser.parse(&raw);
1256        assert!(result.is_err());
1257        assert!(matches!(
1258            result,
1259            Err(ParseError::MalformedTransaction { .. })
1260        ));
1261    }
1262
1263    // ------------------------------------------------------------------------
1264    // Different Chain IDs Tests
1265    // ------------------------------------------------------------------------
1266
1267    #[test]
1268    fn test_parse_eip1559_polygon() {
1269        let parser = EthereumParser::new();
1270
1271        // EIP-1559 on Polygon (chainId=137)
1272        let to_addr = Address::from([0x12; 20]);
1273        let raw = encode_eip1559_tx(
1274            137,              // chain_id (Polygon)
1275            0,                // nonce
1276            1_000_000_000,    // max_priority_fee_per_gas
1277            2_000_000_000,    // max_fee_per_gas
1278            21000,            // gas_limit
1279            Some(to_addr),    // to
1280            U256::ZERO,       // value
1281            Bytes::default(), // data
1282        );
1283
1284        let result = parser.parse(&raw);
1285        assert!(result.is_ok(), "parsing failed: {result:?}");
1286
1287        let parsed = result.expect("should parse");
1288        assert_eq!(parsed.chain_id, Some(137));
1289    }
1290
1291    #[test]
1292    fn test_parse_legacy_with_high_chain_id() {
1293        let parser = EthereumParser::new();
1294
1295        // Legacy transaction with BSC chain_id (56)
1296        let to_addr = Address::from([0x12; 20]);
1297        let raw = encode_legacy_tx(
1298            9,                                        // nonce
1299            20_000_000_000,                           // gas_price (20 gwei)
1300            21000,                                    // gas_limit
1301            Some(to_addr),                            // to
1302            U256::from(1_000_000_000_000_000_000u64), // value (1 ETH equivalent)
1303            Bytes::default(),                         // data
1304            Some(56),                                 // chain_id (BSC)
1305        );
1306
1307        let result = parser.parse(&raw);
1308        assert!(result.is_ok(), "parsing failed: {result:?}");
1309
1310        let parsed = result.expect("should parse");
1311        assert_eq!(parsed.chain_id, Some(56));
1312    }
1313
1314    // ------------------------------------------------------------------------
1315    // Thread Safety Tests
1316    // ------------------------------------------------------------------------
1317
1318    #[test]
1319    fn test_parser_is_send() {
1320        fn assert_send<T: Send>() {}
1321        assert_send::<EthereumParser>();
1322    }
1323
1324    #[test]
1325    fn test_parser_is_sync() {
1326        fn assert_sync<T: Sync>() {}
1327        assert_sync::<EthereumParser>();
1328    }
1329
1330    // ------------------------------------------------------------------------
1331    // Integration with Chain Registry
1332    // ------------------------------------------------------------------------
1333
1334    #[test]
1335    fn test_parser_as_trait_object() {
1336        let parser = EthereumParser::new();
1337        let chain: Box<dyn Chain> = Box::new(parser);
1338
1339        assert_eq!(chain.id(), "ethereum");
1340        assert_eq!(chain.curve(), CurveType::Secp256k1);
1341        assert!(chain.supports_version(0));
1342        assert!(chain.supports_version(1));
1343        assert!(chain.supports_version(2));
1344    }
1345
1346    // ------------------------------------------------------------------------
1347    // ERC-20 Detection Integration Tests
1348    // ------------------------------------------------------------------------
1349
1350    /// Helper to create ERC-20 transfer calldata
1351    fn erc20_transfer_calldata(to: Address, amount: U256) -> Bytes {
1352        let mut data = vec![0xa9, 0x05, 0x9c, 0xbb]; // transfer selector
1353                                                     // Address (32 bytes, left-padded)
1354        data.extend_from_slice(&[0u8; 12]);
1355        data.extend_from_slice(to.as_slice());
1356        // Amount (32 bytes, big-endian)
1357        data.extend_from_slice(&amount.to_be_bytes::<32>());
1358        Bytes::from(data)
1359    }
1360
1361    /// Helper to create ERC-20 approve calldata
1362    fn erc20_approve_calldata(spender: Address, amount: U256) -> Bytes {
1363        let mut data = vec![0x09, 0x5e, 0xa7, 0xb3]; // approve selector
1364                                                     // Spender address (32 bytes, left-padded)
1365        data.extend_from_slice(&[0u8; 12]);
1366        data.extend_from_slice(spender.as_slice());
1367        // Amount (32 bytes, big-endian)
1368        data.extend_from_slice(&amount.to_be_bytes::<32>());
1369        Bytes::from(data)
1370    }
1371
1372    /// Helper to create ERC-20 transferFrom calldata
1373    fn erc20_transfer_from_calldata(from: Address, to: Address, amount: U256) -> Bytes {
1374        let mut data = vec![0x23, 0xb8, 0x72, 0xdd]; // transferFrom selector
1375                                                     // From address (32 bytes, left-padded)
1376        data.extend_from_slice(&[0u8; 12]);
1377        data.extend_from_slice(from.as_slice());
1378        // To address (32 bytes, left-padded)
1379        data.extend_from_slice(&[0u8; 12]);
1380        data.extend_from_slice(to.as_slice());
1381        // Amount (32 bytes, big-endian)
1382        data.extend_from_slice(&amount.to_be_bytes::<32>());
1383        Bytes::from(data)
1384    }
1385
1386    #[test]
1387    fn test_erc20_transfer_detection_eip1559() {
1388        let parser = EthereumParser::new();
1389
1390        let token_contract = Address::from([0xaa; 20]); // Token contract address
1391        let recipient = Address::from([0xbb; 20]); // Actual recipient
1392        let token_amount = U256::from(1_000_000u64); // 1 USDC (6 decimals)
1393
1394        let calldata = erc20_transfer_calldata(recipient, token_amount);
1395
1396        let raw = encode_eip1559_tx(
1397            1,                    // chain_id
1398            0,                    // nonce
1399            1_000_000_000,        // max_priority_fee_per_gas
1400            2_000_000_000,        // max_fee_per_gas
1401            100_000,              // gas_limit
1402            Some(token_contract), // to (token contract)
1403            U256::ZERO,           // value (no ETH sent)
1404            calldata,             // data (ERC-20 transfer)
1405        );
1406
1407        let result = parser.parse(&raw);
1408        assert!(result.is_ok(), "parsing failed: {result:?}");
1409
1410        let parsed = result.expect("should parse");
1411
1412        // Should be detected as TokenTransfer
1413        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1414
1415        // Recipient should be the actual token recipient, not the contract
1416        assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1417
1418        // Amount should be the token amount
1419        assert_eq!(parsed.amount, Some(token_amount));
1420
1421        // Token address should be set
1422        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1423    }
1424
1425    #[test]
1426    fn test_erc20_approve_detection_eip1559() {
1427        let parser = EthereumParser::new();
1428
1429        let token_contract = Address::from([0xaa; 20]);
1430        let spender = Address::from([0xcc; 20]); // Spender (e.g., DEX router)
1431        let approval_amount = U256::MAX; // Unlimited approval
1432
1433        let calldata = erc20_approve_calldata(spender, approval_amount);
1434
1435        let raw = encode_eip1559_tx(
1436            1,                    // chain_id
1437            1,                    // nonce
1438            1_000_000_000,        // max_priority_fee_per_gas
1439            2_000_000_000,        // max_fee_per_gas
1440            60_000,               // gas_limit
1441            Some(token_contract), // to (token contract)
1442            U256::ZERO,           // value
1443            calldata,             // data (ERC-20 approve)
1444        );
1445
1446        let result = parser.parse(&raw);
1447        assert!(result.is_ok(), "parsing failed: {result:?}");
1448
1449        let parsed = result.expect("should parse");
1450
1451        // Should be detected as TokenApproval
1452        assert_eq!(parsed.tx_type, TxType::TokenApproval);
1453
1454        // Recipient should be the spender
1455        assert_eq!(parsed.recipient, Some(format!("{spender}")));
1456
1457        // Amount should be the approval amount
1458        assert_eq!(parsed.amount, Some(approval_amount));
1459
1460        // Token address should be set
1461        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1462    }
1463
1464    #[test]
1465    fn test_erc20_transfer_from_detection_eip1559() {
1466        let parser = EthereumParser::new();
1467
1468        let token_contract = Address::from([0xaa; 20]);
1469        let from_addr = Address::from([0xdd; 20]); // Token owner
1470        let to_addr = Address::from([0xee; 20]); // Token recipient
1471        let token_amount = U256::from(500_000_000_000_000_000u64); // 0.5 tokens (18 decimals)
1472
1473        let calldata = erc20_transfer_from_calldata(from_addr, to_addr, token_amount);
1474
1475        let raw = encode_eip1559_tx(
1476            1,                    // chain_id
1477            2,                    // nonce
1478            1_000_000_000,        // max_priority_fee_per_gas
1479            2_000_000_000,        // max_fee_per_gas
1480            100_000,              // gas_limit
1481            Some(token_contract), // to (token contract)
1482            U256::ZERO,           // value
1483            calldata,             // data (ERC-20 transferFrom)
1484        );
1485
1486        let result = parser.parse(&raw);
1487        assert!(result.is_ok(), "parsing failed: {result:?}");
1488
1489        let parsed = result.expect("should parse");
1490
1491        // Should be detected as TokenTransfer
1492        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1493
1494        // Recipient should be the actual token recipient (to_addr)
1495        assert_eq!(parsed.recipient, Some(format!("{to_addr}")));
1496
1497        // Amount should be the token amount
1498        assert_eq!(parsed.amount, Some(token_amount));
1499
1500        // Token address should be set
1501        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1502    }
1503
1504    #[test]
1505    fn test_erc20_transfer_detection_legacy() {
1506        let parser = EthereumParser::new();
1507
1508        let token_contract = Address::from([0xaa; 20]);
1509        let recipient = Address::from([0xbb; 20]);
1510        let token_amount = U256::from(2_000_000u64);
1511
1512        let calldata = erc20_transfer_calldata(recipient, token_amount);
1513
1514        let raw = encode_legacy_tx(
1515            5,                    // nonce
1516            20_000_000_000,       // gas_price (20 gwei)
1517            100_000,              // gas_limit
1518            Some(token_contract), // to (token contract)
1519            U256::ZERO,           // value
1520            calldata,             // data
1521            Some(1),              // chain_id
1522        );
1523
1524        let result = parser.parse(&raw);
1525        assert!(result.is_ok(), "parsing failed: {result:?}");
1526
1527        let parsed = result.expect("should parse");
1528
1529        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1530        assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1531        assert_eq!(parsed.amount, Some(token_amount));
1532        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1533    }
1534
1535    #[test]
1536    fn test_erc20_detection_eip2930() {
1537        let parser = EthereumParser::new();
1538
1539        let token_contract = Address::from([0xaa; 20]);
1540        let spender = Address::from([0xcc; 20]);
1541        let approval_amount = U256::from(1_000_000_000_000u64);
1542
1543        let calldata = erc20_approve_calldata(spender, approval_amount);
1544
1545        let raw = encode_eip2930_tx(
1546            1,                    // chain_id
1547            3,                    // nonce
1548            10_000_000_000,       // gas_price
1549            80_000,               // gas_limit
1550            Some(token_contract), // to (token contract)
1551            U256::ZERO,           // value
1552            calldata,             // data
1553        );
1554
1555        let result = parser.parse(&raw);
1556        assert!(result.is_ok(), "parsing failed: {result:?}");
1557
1558        let parsed = result.expect("should parse");
1559
1560        assert_eq!(parsed.tx_type, TxType::TokenApproval);
1561        assert_eq!(parsed.recipient, Some(format!("{spender}")));
1562        assert_eq!(parsed.amount, Some(approval_amount));
1563        assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1564    }
1565
1566    #[test]
1567    fn test_non_erc20_contract_call_unchanged() {
1568        let parser = EthereumParser::new();
1569
1570        let contract = Address::from([0x12; 20]);
1571        // Unknown function selector (not ERC-20)
1572        let calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef, 0x00]);
1573
1574        let raw = encode_eip1559_tx(
1575            1,              // chain_id
1576            0,              // nonce
1577            1_000_000_000,  // max_priority_fee_per_gas
1578            2_000_000_000,  // max_fee_per_gas
1579            100_000,        // gas_limit
1580            Some(contract), // to
1581            U256::ZERO,     // value
1582            calldata,       // data (not ERC-20)
1583        );
1584
1585        let result = parser.parse(&raw);
1586        assert!(result.is_ok(), "parsing failed: {result:?}");
1587
1588        let parsed = result.expect("should parse");
1589
1590        // Should be a generic ContractCall
1591        assert_eq!(parsed.tx_type, TxType::ContractCall);
1592
1593        // Recipient should be the contract address
1594        assert_eq!(parsed.recipient, Some(format!("{contract}")));
1595
1596        // Token address should NOT be set
1597        assert!(parsed.token_address.is_none());
1598    }
1599
1600    #[test]
1601    fn test_simple_eth_transfer_unchanged() {
1602        let parser = EthereumParser::new();
1603
1604        let recipient = Address::from([0x12; 20]);
1605        let eth_amount = U256::from(1_000_000_000_000_000_000u64); // 1 ETH
1606
1607        let raw = encode_eip1559_tx(
1608            1,                // chain_id
1609            0,                // nonce
1610            1_000_000_000,    // max_priority_fee_per_gas
1611            2_000_000_000,    // max_fee_per_gas
1612            21_000,           // gas_limit
1613            Some(recipient),  // to
1614            eth_amount,       // value
1615            Bytes::default(), // data (empty)
1616        );
1617
1618        let result = parser.parse(&raw);
1619        assert!(result.is_ok(), "parsing failed: {result:?}");
1620
1621        let parsed = result.expect("should parse");
1622
1623        // Should be a simple Transfer
1624        assert_eq!(parsed.tx_type, TxType::Transfer);
1625
1626        // Recipient should be the ETH recipient
1627        assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1628
1629        // Amount should be the ETH amount
1630        assert_eq!(parsed.amount, Some(eth_amount));
1631
1632        // Token address should NOT be set (native transfer)
1633        assert!(parsed.token_address.is_none());
1634    }
1635
1636    #[test]
1637    fn test_erc20_with_eth_value() {
1638        // Some exotic cases might send ETH value along with ERC-20 call
1639        // (e.g., WETH deposit with data, or payable token functions)
1640        let parser = EthereumParser::new();
1641
1642        let token_contract = Address::from([0xaa; 20]);
1643        let recipient = Address::from([0xbb; 20]);
1644        let token_amount = U256::from(1_000_000u64);
1645        let eth_value = U256::from(100_000_000_000_000_000u64); // 0.1 ETH
1646
1647        let calldata = erc20_transfer_calldata(recipient, token_amount);
1648
1649        let raw = encode_eip1559_tx(
1650            1,                    // chain_id
1651            0,                    // nonce
1652            1_000_000_000,        // max_priority_fee_per_gas
1653            2_000_000_000,        // max_fee_per_gas
1654            100_000,              // gas_limit
1655            Some(token_contract), // to (token contract)
1656            eth_value,            // value (some ETH too!)
1657            calldata,             // data (ERC-20 transfer)
1658        );
1659
1660        let result = parser.parse(&raw);
1661        assert!(result.is_ok(), "parsing failed: {result:?}");
1662
1663        let parsed = result.expect("should parse");
1664
1665        // Should still detect as TokenTransfer
1666        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1667
1668        // Amount should be the token amount (not ETH value)
1669        assert_eq!(parsed.amount, Some(token_amount));
1670
1671        // Token address should be set
1672        assert!(parsed.token_address.is_some());
1673    }
1674
1675    #[test]
1676    fn test_erc20_zero_amount() {
1677        let parser = EthereumParser::new();
1678
1679        let token_contract = Address::from([0xaa; 20]);
1680        let recipient = Address::from([0xbb; 20]);
1681        let token_amount = U256::ZERO;
1682
1683        let calldata = erc20_transfer_calldata(recipient, token_amount);
1684
1685        let raw = encode_eip1559_tx(
1686            1,                    // chain_id
1687            0,                    // nonce
1688            1_000_000_000,        // max_priority_fee_per_gas
1689            2_000_000_000,        // max_fee_per_gas
1690            60_000,               // gas_limit
1691            Some(token_contract), // to
1692            U256::ZERO,           // value
1693            calldata,             // data
1694        );
1695
1696        let result = parser.parse(&raw);
1697        assert!(result.is_ok(), "parsing failed: {result:?}");
1698
1699        let parsed = result.expect("should parse");
1700
1701        // Should still be detected as TokenTransfer
1702        assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1703        assert_eq!(parsed.amount, Some(U256::ZERO));
1704    }
1705
1706    #[test]
1707    fn test_erc20_approve_zero_revoke() {
1708        // Approve with zero amount is a "revoke" pattern
1709        let parser = EthereumParser::new();
1710
1711        let token_contract = Address::from([0xaa; 20]);
1712        let spender = Address::from([0xcc; 20]);
1713        let approval_amount = U256::ZERO; // Revoke approval
1714
1715        let calldata = erc20_approve_calldata(spender, approval_amount);
1716
1717        let raw = encode_eip1559_tx(
1718            1,                    // chain_id
1719            0,                    // nonce
1720            1_000_000_000,        // max_priority_fee_per_gas
1721            2_000_000_000,        // max_fee_per_gas
1722            50_000,               // gas_limit
1723            Some(token_contract), // to
1724            U256::ZERO,           // value
1725            calldata,             // data
1726        );
1727
1728        let result = parser.parse(&raw);
1729        assert!(result.is_ok(), "parsing failed: {result:?}");
1730
1731        let parsed = result.expect("should parse");
1732
1733        // Should still be TokenApproval
1734        assert_eq!(parsed.tx_type, TxType::TokenApproval);
1735        assert_eq!(parsed.amount, Some(U256::ZERO));
1736    }
1737
1738    // ------------------------------------------------------------------------
1739    // Missing Field Error Tests
1740    // ------------------------------------------------------------------------
1741
1742    #[test]
1743    fn test_legacy_tx_truncated_data() {
1744        // Arrange: Create a truncated legacy transaction by encoding normally then truncating
1745        let parser = EthereumParser::new();
1746
1747        // First create a valid legacy transaction
1748        let valid_raw = encode_legacy_tx(
1749            9,
1750            20_000_000_000,
1751            21000,
1752            Some(Address::from([0x35; 20])),
1753            U256::from(1_000_000_000_000_000_000u64),
1754            Bytes::new(),
1755            Some(1),
1756        );
1757
1758        // Truncate it to simulate missing fields
1759        let truncated = &valid_raw[..valid_raw.len() / 2];
1760
1761        // Act
1762        let result = parser.parse(truncated);
1763
1764        // Assert: Should fail with InvalidRlp or MalformedTransaction
1765        assert!(result.is_err());
1766        assert!(matches!(
1767            result,
1768            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
1769        ));
1770    }
1771
1772    #[test]
1773    fn test_eip1559_tx_truncated_data() {
1774        // Arrange: Create a truncated EIP-1559 transaction
1775        let parser = EthereumParser::new();
1776
1777        // First create a valid EIP-1559 transaction
1778        let valid_raw = encode_eip1559_tx(
1779            1,                               // chain_id
1780            0,                               // nonce
1781            1_000_000_000,                   // max_priority_fee_per_gas
1782            2_000_000_000,                   // max_fee_per_gas
1783            21000,                           // gas_limit
1784            Some(Address::from([0x35; 20])), // to
1785            U256::ZERO,                      // value
1786            Bytes::new(),                    // data
1787        );
1788
1789        // Truncate it to simulate missing fields
1790        let truncated = &valid_raw[..valid_raw.len() / 2];
1791
1792        // Act
1793        let result = parser.parse(truncated);
1794
1795        // Assert: Should fail with InvalidRlp or MalformedTransaction
1796        assert!(result.is_err());
1797        assert!(matches!(
1798            result,
1799            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
1800        ));
1801    }
1802
1803    #[test]
1804    fn test_eip2930_tx_truncated_data() {
1805        // Arrange: Create a truncated EIP-2930 transaction
1806        let parser = EthereumParser::new();
1807
1808        // First create a valid EIP-2930 transaction
1809        let valid_raw = encode_eip2930_tx(
1810            1,                               // chain_id
1811            0,                               // nonce
1812            1_000_000_000,                   // gas_price
1813            21000,                           // gas_limit
1814            Some(Address::from([0x35; 20])), // to
1815            U256::ZERO,                      // value
1816            Bytes::new(),                    // data
1817        );
1818
1819        // Truncate it to simulate missing fields
1820        let truncated = &valid_raw[..valid_raw.len() / 2];
1821
1822        // Act
1823        let result = parser.parse(truncated);
1824
1825        // Assert: Should fail with InvalidRlp or MalformedTransaction
1826        assert!(result.is_err());
1827        assert!(matches!(
1828            result,
1829            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
1830        ));
1831    }
1832
1833    #[test]
1834    fn test_legacy_tx_invalid_rlp_structure() {
1835        // Arrange: Create invalid RLP data that doesn't represent a valid transaction
1836        let parser = EthereumParser::new();
1837
1838        // Invalid RLP: claim to be a long list but provide insufficient data
1839        let invalid_rlp = vec![0xf8, 0xff, 0x01, 0x02, 0x03];
1840
1841        // Act
1842        let result = parser.parse(&invalid_rlp);
1843
1844        // Assert: Should fail with InvalidRlp or MalformedTransaction
1845        assert!(result.is_err());
1846        assert!(matches!(
1847            result,
1848            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
1849        ));
1850    }
1851
1852    #[test]
1853    fn test_eip1559_tx_invalid_rlp_structure() {
1854        // Arrange: Create invalid EIP-1559 transaction with malformed RLP
1855        let parser = EthereumParser::new();
1856
1857        // Type 2 transaction with invalid RLP payload
1858        let invalid = vec![0x02, 0xf8, 0xff, 0x01, 0x02, 0x03];
1859
1860        // Act
1861        let result = parser.parse(&invalid);
1862
1863        // Assert: Should fail with InvalidRlp or MalformedTransaction
1864        assert!(result.is_err());
1865        assert!(matches!(
1866            result,
1867            Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
1868        ));
1869    }
1870
1871    #[test]
1872    fn test_analyze_erc20_returns_none_for_non_erc20() {
1873        // This test exercises the None return path in analyze_erc20
1874        // by providing invalid ERC-20 calldata
1875        let parser = EthereumParser::new();
1876
1877        let contract = Address::from([0xaa; 20]);
1878        // Calldata with unknown selector (not ERC-20)
1879        let invalid_calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78]);
1880
1881        let raw = encode_eip1559_tx(
1882            1,                // chain_id
1883            0,                // nonce
1884            1_000_000_000,    // max_priority_fee_per_gas
1885            2_000_000_000,    // max_fee_per_gas
1886            100_000,          // gas_limit
1887            Some(contract),   // to
1888            U256::ZERO,       // value
1889            invalid_calldata, // data (invalid ERC-20)
1890        );
1891
1892        let result = parser.parse(&raw);
1893        assert!(result.is_ok());
1894
1895        let parsed = result.unwrap();
1896
1897        // Should be ContractCall, not TokenTransfer/TokenApproval
1898        assert_eq!(parsed.tx_type, TxType::ContractCall);
1899        // Token address should NOT be set
1900        assert!(parsed.token_address.is_none());
1901    }
1902}