1use 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#[derive(Debug, Clone, Copy, Default)]
75pub struct EthereumParser;
76
77impl EthereumParser {
78 #[must_use]
88 pub const fn new() -> Self {
89 Self
90 }
91
92 fn parse_legacy(raw: &[u8]) -> Result<ParsedTx, ParseError> {
102 Self::parse_legacy_with_hash_source(raw, raw)
104 }
105
106 fn parse_legacy_with_hash_source(
117 hash_source: &[u8],
118 rlp_payload: &[u8],
119 ) -> Result<ParsedTx, ParseError> {
120 let items = decode_list(rlp_payload)?;
122
123 if items.len() != 9 {
125 return Err(ParseError::MalformedTransaction {
126 context: format!("legacy transaction expected 9 items, got {}", items.len()),
127 });
128 }
129
130 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 let nonce = decode_u64(nonce_bytes)?;
159
160 let recipient = decode_optional_address(to_bytes)?;
162
163 let amount = decode_u256(value_bytes)?;
165
166 let data = decode_bytes(data_bytes)?;
168
169 let v = decode_u64(v_bytes)?;
171
172 let chain_id = if v >= 35 {
174 (v - 35) / 2
176 } else if v == 27 || v == 28 {
177 1
179 } else {
180 1
182 };
183
184 let erc20_info = recipient
186 .as_ref()
187 .and_then(|addr| Self::analyze_erc20(addr, &data));
188
189 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 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_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 fn parse_eip2930(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
232 let items = decode_list(payload)?;
234
235 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 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 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 let erc20_info = recipient
281 .as_ref()
282 .and_then(|addr| Self::analyze_erc20(addr, &data));
283
284 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 let hash = keccak256(raw);
307
308 Ok(ParsedTx {
309 hash: hash.into(),
310 recipient: final_recipient,
311 amount: final_amount,
312 token: None, 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 fn parse_eip1559(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
327 let items = decode_list(payload)?;
329
330 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 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 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 let erc20_info = recipient
376 .as_ref()
377 .and_then(|addr| Self::analyze_erc20(addr, &data));
378
379 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 let hash = keccak256(raw);
402
403 Ok(ParsedTx {
404 hash: hash.into(),
405 recipient: final_recipient,
406 amount: final_amount,
407 token: None, 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 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 TxType::Deployment
430 } else if !data.is_empty() {
431 TxType::ContractCall
433 } else {
434 TxType::Transfer
436 }
437 }
438
439 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
473struct Erc20Info {
477 tx_type: TxType,
479 token_address: Address,
481 recipient: Address,
483 amount: U256,
485}
486
487impl Chain for EthereumParser {
488 fn id(&self) -> &'static str {
494 "ethereum"
495 }
496
497 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 match detect_tx_type(raw) {
530 None => {
531 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 let payload = typed_tx_payload(raw)?;
547 Self::parse_legacy_with_hash_source(raw, payload)
548 }
549 Some(1) => {
550 let payload = typed_tx_payload(raw)?;
552 Self::parse_eip2930(raw, payload)
553 }
554 Some(2) => {
555 let payload = typed_tx_payload(raw)?;
557 Self::parse_eip1559(raw, payload)
558 }
559 Some(_) => Err(ParseError::UnknownTxType),
560 }
561 }
562
563 fn curve(&self) -> CurveType {
569 CurveType::Secp256k1
570 }
571
572 fn supports_version(&self, version: u8) -> bool {
583 matches!(version, 0..=2)
584 }
585}
586
587#[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 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 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 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 let sig = Signature::new(
668 U256::from(0xffff_ffff_ffff_ffffu64),
669 U256::from(0xffff_ffff_ffff_ffffu64),
670 false,
671 );
672
673 let mut buf = Vec::new();
675 buf.push(0x01); tx.rlp_encode_signed(&sig, &mut buf);
677 buf
678 }
679
680 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 let sig = Signature::new(
705 U256::from(0xffff_ffff_ffff_ffffu64),
706 U256::from(0xffff_ffff_ffff_ffffu64),
707 false,
708 );
709
710 let mut buf = Vec::new();
712 buf.push(0x02); tx.rlp_encode_signed(&sig, &mut buf);
714 buf
715 }
716
717 #[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 #[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 assert!(parser.supports_version(0)); assert!(parser.supports_version(1)); assert!(parser.supports_version(2)); assert!(!parser.supports_version(3)); assert!(!parser.supports_version(4));
775 assert!(!parser.supports_version(255));
776 }
777
778 #[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 #[test]
799 fn test_parse_legacy_transaction() {
800 let parser = EthereumParser::new();
801
802 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 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 let expected_amount = U256::from(1_000_000_000_000_000_000u64);
826 assert_eq!(parsed.amount, Some(expected_amount));
827
828 assert_eq!(parsed.chain_id, Some(1));
830
831 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 let to_addr = Address::from([0x12; 20]);
841 let raw = encode_legacy_tx(
842 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), None, );
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 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 let raw = encode_legacy_tx(
865 0, 1_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), Some(1), );
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 let to_addr = Address::from([0x12; 20]);
888 let raw = encode_legacy_tx(
889 1, 1_000_000_000, 100000, Some(to_addr), U256::ZERO, Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), Some(1), );
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 #[test]
911 fn test_parse_eip2930_transaction() {
912 let parser = EthereumParser::new();
913
914 let to_addr = Address::from([0x12; 20]);
916 let raw = encode_eip2930_tx(
917 1, 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
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 let raw = encode_eip2930_tx(
944 1, 0, 1_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), );
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 #[test]
966 fn test_parse_eip1559_transaction() {
967 let parser = EthereumParser::new();
968
969 let to_addr = Address::from([0x12; 20]);
971 let raw = encode_eip1559_tx(
972 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
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 let to_addr = Address::from([0x12; 20]);
1000 let value = U256::from(1_000_000_000_000_000_000u64); let raw = encode_eip1559_tx(
1002 1, 5, 1_000_000_000, 100_000_000_000, 21000, Some(to_addr), value, Bytes::default(), );
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 let raw = encode_eip1559_tx(
1030 1, 0, 1_000_000_000, 2_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), );
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 let to_addr = Address::from([0x12; 20]);
1054 let raw = encode_eip1559_tx(
1055 1, 0, 1_000_000_000, 2_000_000_000, 100000, Some(to_addr), U256::ZERO, Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), );
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 #[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 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, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1111
1112 let result = parser.parse(&raw);
1113 assert!(result.is_ok());
1114
1115 let parsed = result.expect("should parse");
1116
1117 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 let to_addr = Address::from([0x12; 20]);
1128 let legacy_raw = encode_legacy_tx(
1129 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), Some(1), );
1137
1138 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 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 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 let to_addr = Address::from([0x12; 20]);
1168 let legacy_raw = encode_legacy_tx(
1169 5, 2_000_000_000, 21000, Some(to_addr), U256::from(1_000_000_000_000_000_000u64), Bytes::default(), Some(1), );
1177
1178 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 let mut type0_raw = vec![0x00];
1185 type0_raw.extend_from_slice(&legacy_raw);
1186
1187 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 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 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 #[test]
1210 fn test_parse_unsupported_tx_type() {
1211 let parser = EthereumParser::new();
1212
1213 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 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 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 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 #[test]
1268 fn test_parse_eip1559_polygon() {
1269 let parser = EthereumParser::new();
1270
1271 let to_addr = Address::from([0x12; 20]);
1273 let raw = encode_eip1559_tx(
1274 137, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
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 let to_addr = Address::from([0x12; 20]);
1297 let raw = encode_legacy_tx(
1298 9, 20_000_000_000, 21000, Some(to_addr), U256::from(1_000_000_000_000_000_000u64), Bytes::default(), Some(56), );
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 #[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 #[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 fn erc20_transfer_calldata(to: Address, amount: U256) -> Bytes {
1352 let mut data = vec![0xa9, 0x05, 0x9c, 0xbb]; data.extend_from_slice(&[0u8; 12]);
1355 data.extend_from_slice(to.as_slice());
1356 data.extend_from_slice(&amount.to_be_bytes::<32>());
1358 Bytes::from(data)
1359 }
1360
1361 fn erc20_approve_calldata(spender: Address, amount: U256) -> Bytes {
1363 let mut data = vec![0x09, 0x5e, 0xa7, 0xb3]; data.extend_from_slice(&[0u8; 12]);
1366 data.extend_from_slice(spender.as_slice());
1367 data.extend_from_slice(&amount.to_be_bytes::<32>());
1369 Bytes::from(data)
1370 }
1371
1372 fn erc20_transfer_from_calldata(from: Address, to: Address, amount: U256) -> Bytes {
1374 let mut data = vec![0x23, 0xb8, 0x72, 0xdd]; data.extend_from_slice(&[0u8; 12]);
1377 data.extend_from_slice(from.as_slice());
1378 data.extend_from_slice(&[0u8; 12]);
1380 data.extend_from_slice(to.as_slice());
1381 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]); let recipient = Address::from([0xbb; 20]); let token_amount = U256::from(1_000_000u64); let calldata = erc20_transfer_calldata(recipient, token_amount);
1395
1396 let raw = encode_eip1559_tx(
1397 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, );
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 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1414
1415 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1417
1418 assert_eq!(parsed.amount, Some(token_amount));
1420
1421 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]); let approval_amount = U256::MAX; let calldata = erc20_approve_calldata(spender, approval_amount);
1434
1435 let raw = encode_eip1559_tx(
1436 1, 1, 1_000_000_000, 2_000_000_000, 60_000, Some(token_contract), U256::ZERO, calldata, );
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 assert_eq!(parsed.tx_type, TxType::TokenApproval);
1453
1454 assert_eq!(parsed.recipient, Some(format!("{spender}")));
1456
1457 assert_eq!(parsed.amount, Some(approval_amount));
1459
1460 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]); let to_addr = Address::from([0xee; 20]); let token_amount = U256::from(500_000_000_000_000_000u64); let calldata = erc20_transfer_from_calldata(from_addr, to_addr, token_amount);
1474
1475 let raw = encode_eip1559_tx(
1476 1, 2, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, );
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 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1493
1494 assert_eq!(parsed.recipient, Some(format!("{to_addr}")));
1496
1497 assert_eq!(parsed.amount, Some(token_amount));
1499
1500 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, 20_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, Some(1), );
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, 3, 10_000_000_000, 80_000, Some(token_contract), U256::ZERO, calldata, );
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 let calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef, 0x00]);
1573
1574 let raw = encode_eip1559_tx(
1575 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(contract), U256::ZERO, calldata, );
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 assert_eq!(parsed.tx_type, TxType::ContractCall);
1592
1593 assert_eq!(parsed.recipient, Some(format!("{contract}")));
1595
1596 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); let raw = encode_eip1559_tx(
1608 1, 0, 1_000_000_000, 2_000_000_000, 21_000, Some(recipient), eth_amount, Bytes::default(), );
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 assert_eq!(parsed.tx_type, TxType::Transfer);
1625
1626 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1628
1629 assert_eq!(parsed.amount, Some(eth_amount));
1631
1632 assert!(parsed.token_address.is_none());
1634 }
1635
1636 #[test]
1637 fn test_erc20_with_eth_value() {
1638 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); let calldata = erc20_transfer_calldata(recipient, token_amount);
1648
1649 let raw = encode_eip1559_tx(
1650 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), eth_value, calldata, );
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 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1667
1668 assert_eq!(parsed.amount, Some(token_amount));
1670
1671 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, 0, 1_000_000_000, 2_000_000_000, 60_000, Some(token_contract), U256::ZERO, calldata, );
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 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 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; let calldata = erc20_approve_calldata(spender, approval_amount);
1716
1717 let raw = encode_eip1559_tx(
1718 1, 0, 1_000_000_000, 2_000_000_000, 50_000, Some(token_contract), U256::ZERO, calldata, );
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 assert_eq!(parsed.tx_type, TxType::TokenApproval);
1735 assert_eq!(parsed.amount, Some(U256::ZERO));
1736 }
1737
1738 #[test]
1743 fn test_legacy_tx_truncated_data() {
1744 let parser = EthereumParser::new();
1746
1747 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 let truncated = &valid_raw[..valid_raw.len() / 2];
1760
1761 let result = parser.parse(truncated);
1763
1764 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 let parser = EthereumParser::new();
1776
1777 let valid_raw = encode_eip1559_tx(
1779 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(Address::from([0x35; 20])), U256::ZERO, Bytes::new(), );
1788
1789 let truncated = &valid_raw[..valid_raw.len() / 2];
1791
1792 let result = parser.parse(truncated);
1794
1795 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 let parser = EthereumParser::new();
1807
1808 let valid_raw = encode_eip2930_tx(
1810 1, 0, 1_000_000_000, 21000, Some(Address::from([0x35; 20])), U256::ZERO, Bytes::new(), );
1818
1819 let truncated = &valid_raw[..valid_raw.len() / 2];
1821
1822 let result = parser.parse(truncated);
1824
1825 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 let parser = EthereumParser::new();
1837
1838 let invalid_rlp = vec![0xf8, 0xff, 0x01, 0x02, 0x03];
1840
1841 let result = parser.parse(&invalid_rlp);
1843
1844 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 let parser = EthereumParser::new();
1856
1857 let invalid = vec![0x02, 0xf8, 0xff, 0x01, 0x02, 0x03];
1859
1860 let result = parser.parse(&invalid);
1862
1863 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 let parser = EthereumParser::new();
1876
1877 let contract = Address::from([0xaa; 20]);
1878 let invalid_calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78]);
1880
1881 let raw = encode_eip1559_tx(
1882 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(contract), U256::ZERO, invalid_calldata, );
1891
1892 let result = parser.parse(&raw);
1893 assert!(result.is_ok());
1894
1895 let parsed = result.unwrap();
1896
1897 assert_eq!(parsed.tx_type, TxType::ContractCall);
1899 assert!(parsed.token_address.is_none());
1901 }
1902}