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 let is_unsigned = items.len() == 6;
128 if items.len() != 9 && items.len() != 6 {
129 return Err(ParseError::MalformedTransaction {
130 context: format!(
131 "legacy transaction expected 6 or 9 items, got {}",
132 items.len()
133 ),
134 });
135 }
136
137 let nonce_bytes = items
139 .first()
140 .ok_or_else(|| ParseError::MalformedTransaction {
141 context: "missing nonce field".to_string(),
142 })?;
143 let to_bytes = items
144 .get(3)
145 .ok_or_else(|| ParseError::MalformedTransaction {
146 context: "missing to field".to_string(),
147 })?;
148 let value_bytes = items
149 .get(4)
150 .ok_or_else(|| ParseError::MalformedTransaction {
151 context: "missing value field".to_string(),
152 })?;
153 let data_bytes = items
154 .get(5)
155 .ok_or_else(|| ParseError::MalformedTransaction {
156 context: "missing data field".to_string(),
157 })?;
158
159 let nonce = decode_u64(nonce_bytes)?;
161
162 let recipient = decode_optional_address(to_bytes)?;
164
165 let amount = decode_u256(value_bytes)?;
167
168 let data = decode_bytes(data_bytes)?;
170
171 let chain_id = if is_unsigned {
173 1
175 } else {
176 let v_bytes = items
177 .get(6)
178 .ok_or_else(|| ParseError::MalformedTransaction {
179 context: "missing v field".to_string(),
180 })?;
181 let v = decode_u64(v_bytes)?;
182
183 if v >= 35 {
184 (v - 35) / 2
186 } else if v == 27 || v == 28 {
187 1
189 } else {
190 v
193 }
194 };
195
196 let erc20_info = recipient
198 .as_ref()
199 .and_then(|addr| Self::analyze_erc20(addr, &data));
200
201 let (final_tx_type, final_recipient, final_amount, token_address) =
203 erc20_info.as_ref().map_or_else(
204 || {
205 (
206 Self::determine_tx_type(recipient.as_ref(), &data, &amount),
207 recipient.map(|addr| format!("{addr}")),
208 Some(amount),
209 None,
210 )
211 },
212 |info| {
213 (
214 info.tx_type,
215 Some(format!("{}", info.recipient)),
216 Some(info.amount),
217 Some(format!("{}", info.token_address)),
218 )
219 },
220 );
221
222 let hash = keccak256(hash_source);
224
225 let mut metadata = std::collections::HashMap::new();
226 if is_unsigned {
227 metadata.insert("unsigned".to_string(), serde_json::Value::Bool(true));
228 }
229
230 Ok(ParsedTx {
231 hash: hash.into(),
232 recipient: final_recipient,
233 amount: final_amount,
234 token: None, token_address,
236 tx_type: final_tx_type,
237 chain: "ethereum".to_string(),
238 nonce: Some(nonce),
239 chain_id: Some(chain_id),
240 metadata,
241 })
242 }
243
244 fn parse_eip2930(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
249 let items = decode_list(payload)?;
251
252 let is_unsigned = items.len() == 8;
254 if items.len() != 11 && items.len() != 8 {
255 return Err(ParseError::MalformedTransaction {
256 context: format!(
257 "EIP-2930 transaction expected 8 or 11 items, got {}",
258 items.len()
259 ),
260 });
261 }
262
263 let chain_id_bytes = items
265 .first()
266 .ok_or_else(|| ParseError::MalformedTransaction {
267 context: "missing chainId field".to_string(),
268 })?;
269 let nonce_bytes = items
270 .get(1)
271 .ok_or_else(|| ParseError::MalformedTransaction {
272 context: "missing nonce field".to_string(),
273 })?;
274 let to_bytes = items
275 .get(4)
276 .ok_or_else(|| ParseError::MalformedTransaction {
277 context: "missing to field".to_string(),
278 })?;
279 let value_bytes = items
280 .get(5)
281 .ok_or_else(|| ParseError::MalformedTransaction {
282 context: "missing value field".to_string(),
283 })?;
284 let data_bytes = items
285 .get(6)
286 .ok_or_else(|| ParseError::MalformedTransaction {
287 context: "missing data field".to_string(),
288 })?;
289
290 let chain_id = decode_u64(chain_id_bytes)?;
292 let nonce = decode_u64(nonce_bytes)?;
293 let recipient = decode_optional_address(to_bytes)?;
294 let amount = decode_u256(value_bytes)?;
295 let data = decode_bytes(data_bytes)?;
296
297 let erc20_info = recipient
299 .as_ref()
300 .and_then(|addr| Self::analyze_erc20(addr, &data));
301
302 let (final_tx_type, final_recipient, final_amount, token_address) =
304 erc20_info.as_ref().map_or_else(
305 || {
306 (
307 Self::determine_tx_type(recipient.as_ref(), &data, &amount),
308 recipient.map(|addr| format!("{addr}")),
309 Some(amount),
310 None,
311 )
312 },
313 |info| {
314 (
315 info.tx_type,
316 Some(format!("{}", info.recipient)),
317 Some(info.amount),
318 Some(format!("{}", info.token_address)),
319 )
320 },
321 );
322
323 let hash = keccak256(raw);
325
326 Ok(ParsedTx {
327 hash: hash.into(),
328 recipient: final_recipient,
329 amount: final_amount,
330 token: None, token_address,
332 tx_type: final_tx_type,
333 chain: "ethereum".to_string(),
334 nonce: Some(nonce),
335 chain_id: Some(chain_id),
336 metadata: {
337 let mut m = std::collections::HashMap::new();
338 if is_unsigned {
339 m.insert("unsigned".to_string(), serde_json::Value::Bool(true));
340 }
341 m
342 },
343 })
344 }
345
346 fn parse_eip1559(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
351 let items = decode_list(payload)?;
353
354 let is_unsigned = items.len() == 9;
356 if items.len() != 12 && items.len() != 9 {
357 return Err(ParseError::MalformedTransaction {
358 context: format!(
359 "EIP-1559 transaction expected 9 or 12 items, got {}",
360 items.len()
361 ),
362 });
363 }
364
365 let chain_id_bytes = items
367 .first()
368 .ok_or_else(|| ParseError::MalformedTransaction {
369 context: "missing chainId field".to_string(),
370 })?;
371 let nonce_bytes = items
372 .get(1)
373 .ok_or_else(|| ParseError::MalformedTransaction {
374 context: "missing nonce field".to_string(),
375 })?;
376 let to_bytes = items
377 .get(5)
378 .ok_or_else(|| ParseError::MalformedTransaction {
379 context: "missing to field".to_string(),
380 })?;
381 let value_bytes = items
382 .get(6)
383 .ok_or_else(|| ParseError::MalformedTransaction {
384 context: "missing value field".to_string(),
385 })?;
386 let data_bytes = items
387 .get(7)
388 .ok_or_else(|| ParseError::MalformedTransaction {
389 context: "missing data field".to_string(),
390 })?;
391
392 let chain_id = decode_u64(chain_id_bytes)?;
394 let nonce = decode_u64(nonce_bytes)?;
395 let recipient = decode_optional_address(to_bytes)?;
396 let amount = decode_u256(value_bytes)?;
397 let data = decode_bytes(data_bytes)?;
398
399 let erc20_info = recipient
401 .as_ref()
402 .and_then(|addr| Self::analyze_erc20(addr, &data));
403
404 let (final_tx_type, final_recipient, final_amount, token_address) =
406 erc20_info.as_ref().map_or_else(
407 || {
408 (
409 Self::determine_tx_type(recipient.as_ref(), &data, &amount),
410 recipient.map(|addr| format!("{addr}")),
411 Some(amount),
412 None,
413 )
414 },
415 |info| {
416 (
417 info.tx_type,
418 Some(format!("{}", info.recipient)),
419 Some(info.amount),
420 Some(format!("{}", info.token_address)),
421 )
422 },
423 );
424
425 let hash = keccak256(raw);
427
428 Ok(ParsedTx {
429 hash: hash.into(),
430 recipient: final_recipient,
431 amount: final_amount,
432 token: None, token_address,
434 tx_type: final_tx_type,
435 chain: "ethereum".to_string(),
436 nonce: Some(nonce),
437 chain_id: Some(chain_id),
438 metadata: {
439 let mut m = std::collections::HashMap::new();
440 if is_unsigned {
441 m.insert("unsigned".to_string(), serde_json::Value::Bool(true));
442 }
443 m
444 },
445 })
446 }
447
448 const fn determine_tx_type(
454 recipient: Option<&alloy_primitives::Address>,
455 data: &[u8],
456 _amount: &U256,
457 ) -> TxType {
458 if recipient.is_none() {
459 TxType::Deployment
461 } else if !data.is_empty() {
462 TxType::ContractCall
464 } else {
465 TxType::Transfer
467 }
468 }
469
470 fn analyze_erc20(contract_address: &Address, data: &[u8]) -> Option<Erc20Info> {
484 let erc20_call = parse_erc20_call(data)?;
485
486 let (tx_type, recipient_addr) = match &erc20_call {
487 Erc20Call::Transfer { to, .. } | Erc20Call::TransferFrom { to, .. } => {
488 (TxType::TokenTransfer, Address::from_slice(to))
489 }
490 Erc20Call::Approve { spender, .. } => {
491 (TxType::TokenApproval, Address::from_slice(spender))
492 }
493 };
494
495 Some(Erc20Info {
496 tx_type,
497 token_address: *contract_address,
498 recipient: recipient_addr,
499 amount: *erc20_call.amount(),
500 })
501 }
502
503 pub fn assemble_signed(raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
525 use alloy_primitives::Signature;
526
527 if signature.len() != 65 {
528 return Err(ParseError::assembly_failed(format!(
529 "expected 65-byte signature, got {}",
530 signature.len()
531 )));
532 }
533
534 let r = U256::from_be_slice(
536 signature
537 .get(0..32)
538 .ok_or_else(|| ParseError::assembly_failed("signature too short for r"))?,
539 );
540 let s = U256::from_be_slice(
541 signature
542 .get(32..64)
543 .ok_or_else(|| ParseError::assembly_failed("signature too short for s"))?,
544 );
545 let v_byte = *signature
546 .get(64)
547 .ok_or_else(|| ParseError::assembly_failed("signature too short for v"))?;
548 if v_byte > 1 {
549 return Err(ParseError::assembly_failed(format!(
550 "invalid recovery id: expected 0 or 1, got {v_byte}"
551 )));
552 }
553 let v_parity = v_byte != 0;
554
555 let sig = Signature::new(r, s, v_parity);
556
557 match detect_tx_type(raw) {
559 None => {
560 if !crate::rlp::is_list(raw) {
562 return Err(ParseError::assembly_failed("not a valid RLP list"));
563 }
564 Self::assemble_legacy(raw, raw, &sig, None)
565 }
566 Some(0x00) => {
567 let payload = typed_tx_payload(raw)?;
569 Self::assemble_legacy(raw, payload, &sig, None)
570 }
571 Some(0x01) => {
572 let payload = typed_tx_payload(raw)?;
574 Self::assemble_eip2930(payload, &sig)
575 }
576 Some(0x02) => {
577 let payload = typed_tx_payload(raw)?;
579 Self::assemble_eip1559(payload, &sig)
580 }
581 Some(ty) => Err(ParseError::assembly_failed(format!(
582 "unsupported transaction type: 0x{ty:02x}"
583 ))),
584 }
585 }
586
587 fn assemble_legacy(
589 _raw: &[u8],
590 rlp_payload: &[u8],
591 sig: &alloy_primitives::Signature,
592 _type_prefix: Option<u8>,
593 ) -> Result<Vec<u8>, ParseError> {
594 use alloy_consensus::transaction::RlpEcdsaEncodableTx;
595 use alloy_consensus::TxLegacy;
596 use alloy_primitives::{Bytes, TxKind};
597
598 let items = decode_list(rlp_payload)?;
599
600 if items.len() != 6 && items.len() != 9 {
602 return Err(ParseError::assembly_failed(format!(
603 "legacy tx expected 6 or 9 items, got {}",
604 items.len()
605 )));
606 }
607
608 let nonce = decode_u64(
609 items
610 .first()
611 .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
612 )?;
613
614 let gas_price_u256 = decode_u256(
615 items
616 .get(1)
617 .ok_or_else(|| ParseError::assembly_failed("missing gasPrice"))?,
618 )?;
619 let gas_price: u128 = gas_price_u256
620 .try_into()
621 .map_err(|_| ParseError::assembly_failed("gasPrice overflow"))?;
622
623 let gas_limit = decode_u64(
624 items
625 .get(2)
626 .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
627 )?;
628
629 let to_addr = decode_optional_address(
630 items
631 .get(3)
632 .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
633 )?;
634
635 let value = decode_u256(
636 items
637 .get(4)
638 .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
639 )?;
640
641 let data = decode_bytes(
642 items
643 .get(5)
644 .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
645 )?;
646
647 let chain_id = if items.len() == 9 {
649 let v = decode_u64(
650 items
651 .get(6)
652 .ok_or_else(|| ParseError::assembly_failed("missing v"))?,
653 )?;
654 if v >= 35 {
655 Some((v - 35) / 2)
656 } else if v == 27 || v == 28 {
657 None } else {
659 Some(v)
661 }
662 } else {
663 None };
665
666 let tx = TxLegacy {
667 chain_id,
668 nonce,
669 gas_price,
670 gas_limit,
671 to: to_addr.map_or(TxKind::Create, TxKind::Call),
672 value,
673 input: Bytes::from(data),
674 };
675
676 let mut buf = Vec::new();
677 tx.rlp_encode_signed(sig, &mut buf);
678 Ok(buf)
679 }
680
681 fn assemble_eip2930(
683 payload: &[u8],
684 sig: &alloy_primitives::Signature,
685 ) -> Result<Vec<u8>, ParseError> {
686 use alloy_consensus::transaction::RlpEcdsaEncodableTx;
687 use alloy_consensus::TxEip2930;
688 use alloy_eips::eip2930::AccessList;
689 use alloy_primitives::{Bytes, TxKind};
690 use alloy_rlp::Decodable;
691
692 let items = decode_list(payload)?;
693
694 if items.len() != 8 && items.len() != 11 {
696 return Err(ParseError::assembly_failed(format!(
697 "EIP-2930 tx expected 8 or 11 items, got {}",
698 items.len()
699 )));
700 }
701
702 let chain_id = decode_u64(
703 items
704 .first()
705 .ok_or_else(|| ParseError::assembly_failed("missing chainId"))?,
706 )?;
707
708 let nonce = decode_u64(
709 items
710 .get(1)
711 .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
712 )?;
713
714 let gas_price_u256 = decode_u256(
715 items
716 .get(2)
717 .ok_or_else(|| ParseError::assembly_failed("missing gasPrice"))?,
718 )?;
719 let gas_price: u128 = gas_price_u256
720 .try_into()
721 .map_err(|_| ParseError::assembly_failed("gasPrice overflow"))?;
722
723 let gas_limit = decode_u64(
724 items
725 .get(3)
726 .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
727 )?;
728
729 let to_addr = decode_optional_address(
730 items
731 .get(4)
732 .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
733 )?;
734
735 let value = decode_u256(
736 items
737 .get(5)
738 .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
739 )?;
740
741 let data = decode_bytes(
742 items
743 .get(6)
744 .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
745 )?;
746
747 let access_list_bytes = items
748 .get(7)
749 .ok_or_else(|| ParseError::assembly_failed("missing accessList"))?;
750 let mut access_list_buf = *access_list_bytes;
751 let access_list = AccessList::decode(&mut access_list_buf).map_err(|e| {
752 ParseError::assembly_failed(format!("failed to decode access list: {e}"))
753 })?;
754
755 let tx = TxEip2930 {
756 chain_id,
757 nonce,
758 gas_price,
759 gas_limit,
760 to: to_addr.map_or(TxKind::Create, TxKind::Call),
761 value,
762 input: Bytes::from(data),
763 access_list,
764 };
765
766 let mut buf = vec![0x01]; tx.rlp_encode_signed(sig, &mut buf);
768 Ok(buf)
769 }
770
771 fn assemble_eip1559(
773 payload: &[u8],
774 sig: &alloy_primitives::Signature,
775 ) -> Result<Vec<u8>, ParseError> {
776 use alloy_consensus::transaction::RlpEcdsaEncodableTx;
777 use alloy_consensus::TxEip1559;
778 use alloy_eips::eip2930::AccessList;
779 use alloy_primitives::{Bytes, TxKind};
780 use alloy_rlp::Decodable;
781
782 let items = decode_list(payload)?;
783
784 if items.len() != 9 && items.len() != 12 {
786 return Err(ParseError::assembly_failed(format!(
787 "EIP-1559 tx expected 9 or 12 items, got {}",
788 items.len()
789 )));
790 }
791
792 let chain_id = decode_u64(
793 items
794 .first()
795 .ok_or_else(|| ParseError::assembly_failed("missing chainId"))?,
796 )?;
797
798 let nonce = decode_u64(
799 items
800 .get(1)
801 .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
802 )?;
803
804 let max_priority_fee_u256 = decode_u256(
805 items
806 .get(2)
807 .ok_or_else(|| ParseError::assembly_failed("missing maxPriorityFeePerGas"))?,
808 )?;
809 let max_priority_fee_per_gas: u128 = max_priority_fee_u256
810 .try_into()
811 .map_err(|_| ParseError::assembly_failed("maxPriorityFeePerGas overflow"))?;
812
813 let max_fee_u256 = decode_u256(
814 items
815 .get(3)
816 .ok_or_else(|| ParseError::assembly_failed("missing maxFeePerGas"))?,
817 )?;
818 let max_fee_per_gas: u128 = max_fee_u256
819 .try_into()
820 .map_err(|_| ParseError::assembly_failed("maxFeePerGas overflow"))?;
821
822 let gas_limit = decode_u64(
823 items
824 .get(4)
825 .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
826 )?;
827
828 let to_addr = decode_optional_address(
829 items
830 .get(5)
831 .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
832 )?;
833
834 let value = decode_u256(
835 items
836 .get(6)
837 .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
838 )?;
839
840 let data = decode_bytes(
841 items
842 .get(7)
843 .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
844 )?;
845
846 let access_list_bytes = items
847 .get(8)
848 .ok_or_else(|| ParseError::assembly_failed("missing accessList"))?;
849 let mut access_list_buf = *access_list_bytes;
850 let access_list = AccessList::decode(&mut access_list_buf).map_err(|e| {
851 ParseError::assembly_failed(format!("failed to decode access list: {e}"))
852 })?;
853
854 let tx = TxEip1559 {
855 chain_id,
856 nonce,
857 max_priority_fee_per_gas,
858 max_fee_per_gas,
859 gas_limit,
860 to: to_addr.map_or(TxKind::Create, TxKind::Call),
861 value,
862 input: Bytes::from(data),
863 access_list,
864 };
865
866 let mut buf = vec![0x02]; tx.rlp_encode_signed(sig, &mut buf);
868 Ok(buf)
869 }
870}
871
872struct Erc20Info {
876 tx_type: TxType,
878 token_address: Address,
880 recipient: Address,
882 amount: U256,
884}
885
886impl Chain for EthereumParser {
887 fn id(&self) -> &'static str {
893 "ethereum"
894 }
895
896 fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
921 if raw.is_empty() {
922 return Err(ParseError::MalformedTransaction {
923 context: "empty transaction data".to_string(),
924 });
925 }
926
927 match detect_tx_type(raw) {
929 None => {
930 if crate::rlp::is_list(raw) {
933 Self::parse_legacy(raw)
934 } else {
935 Err(ParseError::MalformedTransaction {
936 context:
937 "invalid transaction format: not a valid RLP list or typed transaction"
938 .to_string(),
939 })
940 }
941 }
942 Some(0) => {
943 let payload = typed_tx_payload(raw)?;
946 Self::parse_legacy_with_hash_source(raw, payload)
947 }
948 Some(1) => {
949 let payload = typed_tx_payload(raw)?;
951 Self::parse_eip2930(raw, payload)
952 }
953 Some(2) => {
954 let payload = typed_tx_payload(raw)?;
956 Self::parse_eip1559(raw, payload)
957 }
958 Some(_) => Err(ParseError::UnknownTxType),
959 }
960 }
961
962 fn curve(&self) -> CurveType {
968 CurveType::Secp256k1
969 }
970
971 fn supports_version(&self, version: u8) -> bool {
982 matches!(version, 0..=2)
983 }
984
985 fn assemble_signed(&self, raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
986 Self::assemble_signed(raw, signature)
987 }
988}
989
990#[cfg(test)]
995mod tests {
996 #![allow(
997 clippy::expect_used,
998 clippy::unwrap_used,
999 clippy::panic,
1000 clippy::indexing_slicing,
1001 clippy::similar_names,
1002 clippy::redundant_clone,
1003 clippy::manual_string_new,
1004 clippy::needless_raw_string_hashes,
1005 clippy::needless_collect,
1006 clippy::unreadable_literal,
1007 clippy::default_trait_access,
1008 clippy::too_many_arguments,
1009 clippy::default_constructed_unit_structs
1010 )]
1011
1012 use super::*;
1013 use alloy_consensus::{transaction::RlpEcdsaEncodableTx, TxEip1559, TxEip2930, TxLegacy};
1014 use alloy_primitives::{hex, Address, Bytes, Signature, TxKind};
1015
1016 fn encode_legacy_tx(
1018 nonce: u64,
1019 gas_price: u128,
1020 gas_limit: u64,
1021 to: Option<Address>,
1022 value: U256,
1023 data: Bytes,
1024 chain_id: Option<u64>,
1025 ) -> Vec<u8> {
1026 let tx = TxLegacy {
1027 chain_id,
1028 nonce,
1029 gas_price,
1030 gas_limit,
1031 to: to.map_or(TxKind::Create, TxKind::Call),
1032 value,
1033 input: data,
1034 };
1035
1036 let sig = Signature::new(
1038 U256::from(0xffff_ffff_ffff_ffffu64),
1039 U256::from(0xffff_ffff_ffff_ffffu64),
1040 false,
1041 );
1042
1043 let mut buf = Vec::new();
1044 tx.rlp_encode_signed(&sig, &mut buf);
1045 buf
1046 }
1047
1048 fn encode_eip2930_tx(
1050 chain_id: u64,
1051 nonce: u64,
1052 gas_price: u128,
1053 gas_limit: u64,
1054 to: Option<Address>,
1055 value: U256,
1056 data: Bytes,
1057 ) -> Vec<u8> {
1058 let tx = TxEip2930 {
1059 chain_id,
1060 nonce,
1061 gas_price,
1062 gas_limit,
1063 to: to.map_or(TxKind::Create, TxKind::Call),
1064 value,
1065 input: data,
1066 access_list: Default::default(),
1067 };
1068
1069 let sig = Signature::new(
1071 U256::from(0xffff_ffff_ffff_ffffu64),
1072 U256::from(0xffff_ffff_ffff_ffffu64),
1073 false,
1074 );
1075
1076 let mut buf = Vec::new();
1078 buf.push(0x01); tx.rlp_encode_signed(&sig, &mut buf);
1080 buf
1081 }
1082
1083 fn encode_eip1559_tx(
1085 chain_id: u64,
1086 nonce: u64,
1087 max_priority_fee_per_gas: u128,
1088 max_fee_per_gas: u128,
1089 gas_limit: u64,
1090 to: Option<Address>,
1091 value: U256,
1092 data: Bytes,
1093 ) -> Vec<u8> {
1094 let tx = TxEip1559 {
1095 chain_id,
1096 nonce,
1097 max_priority_fee_per_gas,
1098 max_fee_per_gas,
1099 gas_limit,
1100 to: to.map_or(TxKind::Create, TxKind::Call),
1101 value,
1102 input: data,
1103 access_list: Default::default(),
1104 };
1105
1106 let sig = Signature::new(
1108 U256::from(0xffff_ffff_ffff_ffffu64),
1109 U256::from(0xffff_ffff_ffff_ffffu64),
1110 false,
1111 );
1112
1113 let mut buf = Vec::new();
1115 buf.push(0x02); tx.rlp_encode_signed(&sig, &mut buf);
1117 buf
1118 }
1119
1120 #[test]
1125 fn test_ethereum_parser_new() {
1126 let parser = EthereumParser::new();
1127 assert_eq!(parser.id(), "ethereum");
1128 }
1129
1130 #[test]
1131 fn test_ethereum_parser_default() {
1132 let parser = EthereumParser::default();
1133 assert_eq!(parser.id(), "ethereum");
1134 }
1135
1136 #[test]
1137 fn test_ethereum_parser_clone() {
1138 let parser = EthereumParser::new();
1139 let cloned = parser;
1140 assert_eq!(cloned.id(), "ethereum");
1141 }
1142
1143 #[test]
1144 fn test_ethereum_parser_debug() {
1145 let parser = EthereumParser::new();
1146 let debug_str = format!("{parser:?}");
1147 assert!(debug_str.contains("EthereumParser"));
1148 }
1149
1150 #[test]
1155 fn test_chain_id() {
1156 let parser = EthereumParser::new();
1157 assert_eq!(parser.id(), "ethereum");
1158 }
1159
1160 #[test]
1161 fn test_chain_curve() {
1162 let parser = EthereumParser::new();
1163 assert_eq!(parser.curve(), CurveType::Secp256k1);
1164 }
1165
1166 #[test]
1167 fn test_supports_version() {
1168 let parser = EthereumParser::new();
1169
1170 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));
1178 assert!(!parser.supports_version(255));
1179 }
1180
1181 #[test]
1186 fn test_parse_empty_input() {
1187 let parser = EthereumParser::new();
1188 let result = parser.parse(&[]);
1189
1190 assert!(result.is_err());
1191 assert!(matches!(
1192 result,
1193 Err(ParseError::MalformedTransaction { .. })
1194 ));
1195 }
1196
1197 #[test]
1202 fn test_parse_legacy_transaction() {
1203 let parser = EthereumParser::new();
1204
1205 let raw = hex::decode(
1209 "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1210 ).expect("valid hex");
1211
1212 let result = parser.parse(&raw);
1213 assert!(result.is_ok(), "parsing failed: {result:?}");
1214
1215 let parsed = result.expect("should parse successfully");
1216
1217 assert_eq!(parsed.chain, "ethereum");
1219 assert_eq!(parsed.nonce, Some(9));
1220 assert_eq!(parsed.tx_type, TxType::Transfer);
1221 assert!(parsed.recipient.is_some());
1222 assert_eq!(
1223 parsed.recipient.as_ref().map(|s| s.to_lowercase()),
1224 Some("0x3535353535353535353535353535353535353535".to_string())
1225 );
1226
1227 let expected_amount = U256::from(1_000_000_000_000_000_000u64);
1229 assert_eq!(parsed.amount, Some(expected_amount));
1230
1231 assert_eq!(parsed.chain_id, Some(1));
1233
1234 assert_ne!(parsed.hash, [0u8; 32]);
1236 }
1237
1238 #[test]
1239 fn test_parse_legacy_transaction_pre_eip155() {
1240 let parser = EthereumParser::new();
1241
1242 let to_addr = Address::from([0x12; 20]);
1244 let raw = encode_legacy_tx(
1245 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), None, );
1253
1254 let result = parser.parse(&raw);
1255 assert!(result.is_ok(), "parsing failed: {result:?}");
1256
1257 let parsed = result.expect("should parse successfully");
1258 assert_eq!(parsed.chain_id, Some(1));
1260 }
1261
1262 #[test]
1263 fn test_parse_legacy_contract_deployment() {
1264 let parser = EthereumParser::new();
1265
1266 let raw = encode_legacy_tx(
1268 0, 1_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), Some(1), );
1276
1277 let result = parser.parse(&raw);
1278 assert!(result.is_ok(), "parsing failed: {result:?}");
1279
1280 let parsed = result.expect("should parse successfully");
1281 assert_eq!(parsed.tx_type, TxType::Deployment);
1282 assert!(parsed.recipient.is_none());
1283 }
1284
1285 #[test]
1286 fn test_parse_legacy_contract_call() {
1287 let parser = EthereumParser::new();
1288
1289 let to_addr = Address::from([0x12; 20]);
1291 let raw = encode_legacy_tx(
1292 1, 1_000_000_000, 100000, Some(to_addr), U256::ZERO, Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), Some(1), );
1300
1301 let result = parser.parse(&raw);
1302 assert!(result.is_ok(), "parsing failed: {result:?}");
1303
1304 let parsed = result.expect("should parse successfully");
1305 assert_eq!(parsed.tx_type, TxType::ContractCall);
1306 assert!(parsed.recipient.is_some());
1307 }
1308
1309 #[test]
1314 fn test_parse_eip2930_transaction() {
1315 let parser = EthereumParser::new();
1316
1317 let to_addr = Address::from([0x12; 20]);
1319 let raw = encode_eip2930_tx(
1320 1, 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1328
1329 let result = parser.parse(&raw);
1330 assert!(result.is_ok(), "parsing failed: {result:?}");
1331
1332 let parsed = result.expect("should parse successfully");
1333
1334 assert_eq!(parsed.chain, "ethereum");
1335 assert_eq!(parsed.chain_id, Some(1));
1336 assert_eq!(parsed.nonce, Some(0));
1337 assert_eq!(parsed.tx_type, TxType::Transfer);
1338 assert!(parsed.recipient.is_some());
1339 }
1340
1341 #[test]
1342 fn test_parse_eip2930_contract_deployment() {
1343 let parser = EthereumParser::new();
1344
1345 let raw = encode_eip2930_tx(
1347 1, 0, 1_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), );
1355
1356 let result = parser.parse(&raw);
1357 assert!(result.is_ok(), "parsing failed: {result:?}");
1358
1359 let parsed = result.expect("should parse successfully");
1360 assert_eq!(parsed.tx_type, TxType::Deployment);
1361 assert!(parsed.recipient.is_none());
1362 }
1363
1364 #[test]
1369 fn test_parse_eip1559_transaction() {
1370 let parser = EthereumParser::new();
1371
1372 let to_addr = Address::from([0x12; 20]);
1374 let raw = encode_eip1559_tx(
1375 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1384
1385 let result = parser.parse(&raw);
1386 assert!(result.is_ok(), "parsing failed: {result:?}");
1387
1388 let parsed = result.expect("should parse successfully");
1389
1390 assert_eq!(parsed.chain, "ethereum");
1391 assert_eq!(parsed.chain_id, Some(1));
1392 assert_eq!(parsed.nonce, Some(0));
1393 assert_eq!(parsed.tx_type, TxType::Transfer);
1394 assert!(parsed.recipient.is_some());
1395 }
1396
1397 #[test]
1398 fn test_parse_eip1559_with_value() {
1399 let parser = EthereumParser::new();
1400
1401 let to_addr = Address::from([0x12; 20]);
1403 let value = U256::from(1_000_000_000_000_000_000u64); let raw = encode_eip1559_tx(
1405 1, 5, 1_000_000_000, 100_000_000_000, 21000, Some(to_addr), value, Bytes::default(), );
1414
1415 let result = parser.parse(&raw);
1416 assert!(result.is_ok(), "parsing failed: {result:?}");
1417
1418 let parsed = result.expect("should parse successfully");
1419
1420 assert_eq!(parsed.chain, "ethereum");
1421 assert_eq!(parsed.chain_id, Some(1));
1422 assert_eq!(parsed.nonce, Some(5));
1423 assert_eq!(parsed.tx_type, TxType::Transfer);
1424 assert_eq!(parsed.amount, Some(value));
1425 }
1426
1427 #[test]
1428 fn test_parse_eip1559_contract_deployment() {
1429 let parser = EthereumParser::new();
1430
1431 let raw = encode_eip1559_tx(
1433 1, 0, 1_000_000_000, 2_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), );
1442
1443 let result = parser.parse(&raw);
1444 assert!(result.is_ok(), "parsing failed: {result:?}");
1445
1446 let parsed = result.expect("should parse successfully");
1447 assert_eq!(parsed.tx_type, TxType::Deployment);
1448 assert!(parsed.recipient.is_none());
1449 }
1450
1451 #[test]
1452 fn test_parse_eip1559_contract_call() {
1453 let parser = EthereumParser::new();
1454
1455 let to_addr = Address::from([0x12; 20]);
1457 let raw = encode_eip1559_tx(
1458 1, 0, 1_000_000_000, 2_000_000_000, 100000, Some(to_addr), U256::ZERO, Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), );
1467
1468 let result = parser.parse(&raw);
1469 assert!(result.is_ok(), "parsing failed: {result:?}");
1470
1471 let parsed = result.expect("should parse successfully");
1472 assert_eq!(parsed.tx_type, TxType::ContractCall);
1473 assert!(parsed.recipient.is_some());
1474 }
1475
1476 #[test]
1481 fn test_hash_is_correctly_computed() {
1482 let parser = EthereumParser::new();
1483
1484 let raw = hex::decode(
1485 "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1486 ).expect("valid hex");
1487
1488 let result = parser.parse(&raw);
1489 assert!(result.is_ok());
1490
1491 let parsed = result.expect("should parse");
1492
1493 let expected_hash = keccak256(&raw);
1495
1496 assert_eq!(parsed.hash, *expected_hash);
1497 }
1498
1499 #[test]
1500 fn test_eip1559_hash_includes_type_prefix() {
1501 let parser = EthereumParser::new();
1502
1503 let to_addr = Address::from([0x12; 20]);
1504 let raw = encode_eip1559_tx(
1505 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1514
1515 let result = parser.parse(&raw);
1516 assert!(result.is_ok());
1517
1518 let parsed = result.expect("should parse");
1519
1520 let expected_hash = keccak256(&raw);
1522 assert_eq!(parsed.hash, *expected_hash);
1523 }
1524
1525 #[test]
1526 fn test_type0_hash_includes_type_prefix() {
1527 let parser = EthereumParser::new();
1528
1529 let to_addr = Address::from([0x12; 20]);
1531 let legacy_raw = encode_legacy_tx(
1532 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), Some(1), );
1540
1541 let mut type0_raw = vec![0x00];
1543 type0_raw.extend_from_slice(&legacy_raw);
1544
1545 let result = parser.parse(&type0_raw);
1546 assert!(result.is_ok(), "parsing failed: {result:?}");
1547
1548 let parsed = result.expect("should parse");
1549
1550 let expected_hash = keccak256(&type0_raw);
1552 assert_eq!(
1553 parsed.hash, *expected_hash,
1554 "type 0 hash should include type prefix"
1555 );
1556
1557 let wrong_hash = keccak256(&legacy_raw);
1559 assert_ne!(
1560 parsed.hash, *wrong_hash,
1561 "hash should NOT be computed without type prefix"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_type0_vs_legacy_same_content_different_hash() {
1567 let parser = EthereumParser::new();
1568
1569 let to_addr = Address::from([0x12; 20]);
1571 let legacy_raw = encode_legacy_tx(
1572 5, 2_000_000_000, 21000, Some(to_addr), U256::from(1_000_000_000_000_000_000u64), Bytes::default(), Some(1), );
1580
1581 let legacy_result = parser.parse(&legacy_raw);
1583 assert!(legacy_result.is_ok());
1584 let legacy_parsed = legacy_result.expect("should parse legacy");
1585
1586 let mut type0_raw = vec![0x00];
1588 type0_raw.extend_from_slice(&legacy_raw);
1589
1590 let type0_result = parser.parse(&type0_raw);
1592 assert!(type0_result.is_ok());
1593 let type0_parsed = type0_result.expect("should parse type 0");
1594
1595 assert_eq!(legacy_parsed.nonce, type0_parsed.nonce);
1597 assert_eq!(legacy_parsed.recipient, type0_parsed.recipient);
1598 assert_eq!(legacy_parsed.amount, type0_parsed.amount);
1599 assert_eq!(legacy_parsed.chain_id, type0_parsed.chain_id);
1600
1601 assert_ne!(
1603 legacy_parsed.hash, type0_parsed.hash,
1604 "legacy and type 0 hashes should differ due to type prefix"
1605 );
1606 }
1607
1608 #[test]
1613 fn test_parse_unsupported_tx_type() {
1614 let parser = EthereumParser::new();
1615
1616 let raw = hex::decode("03f8c0").expect("valid hex");
1618
1619 let result = parser.parse(&raw);
1620 assert!(result.is_err());
1621 assert!(matches!(result, Err(ParseError::UnknownTxType)));
1622 }
1623
1624 #[test]
1625 fn test_parse_malformed_legacy_too_few_items() {
1626 let parser = EthereumParser::new();
1627
1628 let raw = hex::decode("c3010203").expect("valid hex");
1630
1631 let result = parser.parse(&raw);
1632 assert!(result.is_err());
1633 assert!(matches!(
1634 result,
1635 Err(ParseError::MalformedTransaction { .. })
1636 ));
1637 }
1638
1639 #[test]
1640 fn test_parse_invalid_rlp() {
1641 let parser = EthereumParser::new();
1642
1643 let raw = hex::decode("f8ff").expect("valid hex");
1645
1646 let result = parser.parse(&raw);
1647 assert!(result.is_err());
1648 }
1649
1650 #[test]
1651 fn test_parse_not_list_not_typed() {
1652 let parser = EthereumParser::new();
1653
1654 let raw = hex::decode("80").expect("valid hex");
1657
1658 let result = parser.parse(&raw);
1659 assert!(result.is_err());
1660 assert!(matches!(
1661 result,
1662 Err(ParseError::MalformedTransaction { .. })
1663 ));
1664 }
1665
1666 #[test]
1671 fn test_parse_eip1559_polygon() {
1672 let parser = EthereumParser::new();
1673
1674 let to_addr = Address::from([0x12; 20]);
1676 let raw = encode_eip1559_tx(
1677 137, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1686
1687 let result = parser.parse(&raw);
1688 assert!(result.is_ok(), "parsing failed: {result:?}");
1689
1690 let parsed = result.expect("should parse");
1691 assert_eq!(parsed.chain_id, Some(137));
1692 }
1693
1694 #[test]
1695 fn test_parse_legacy_with_high_chain_id() {
1696 let parser = EthereumParser::new();
1697
1698 let to_addr = Address::from([0x12; 20]);
1700 let raw = encode_legacy_tx(
1701 9, 20_000_000_000, 21000, Some(to_addr), U256::from(1_000_000_000_000_000_000u64), Bytes::default(), Some(56), );
1709
1710 let result = parser.parse(&raw);
1711 assert!(result.is_ok(), "parsing failed: {result:?}");
1712
1713 let parsed = result.expect("should parse");
1714 assert_eq!(parsed.chain_id, Some(56));
1715 }
1716
1717 #[test]
1722 fn test_parser_is_send() {
1723 fn assert_send<T: Send>() {}
1724 assert_send::<EthereumParser>();
1725 }
1726
1727 #[test]
1728 fn test_parser_is_sync() {
1729 fn assert_sync<T: Sync>() {}
1730 assert_sync::<EthereumParser>();
1731 }
1732
1733 #[test]
1738 fn test_parser_as_trait_object() {
1739 let parser = EthereumParser::new();
1740 let chain: Box<dyn Chain> = Box::new(parser);
1741
1742 assert_eq!(chain.id(), "ethereum");
1743 assert_eq!(chain.curve(), CurveType::Secp256k1);
1744 assert!(chain.supports_version(0));
1745 assert!(chain.supports_version(1));
1746 assert!(chain.supports_version(2));
1747 }
1748
1749 fn erc20_transfer_calldata(to: Address, amount: U256) -> Bytes {
1755 let mut data = vec![0xa9, 0x05, 0x9c, 0xbb]; data.extend_from_slice(&[0u8; 12]);
1758 data.extend_from_slice(to.as_slice());
1759 data.extend_from_slice(&amount.to_be_bytes::<32>());
1761 Bytes::from(data)
1762 }
1763
1764 fn erc20_approve_calldata(spender: Address, amount: U256) -> Bytes {
1766 let mut data = vec![0x09, 0x5e, 0xa7, 0xb3]; data.extend_from_slice(&[0u8; 12]);
1769 data.extend_from_slice(spender.as_slice());
1770 data.extend_from_slice(&amount.to_be_bytes::<32>());
1772 Bytes::from(data)
1773 }
1774
1775 fn erc20_transfer_from_calldata(from: Address, to: Address, amount: U256) -> Bytes {
1777 let mut data = vec![0x23, 0xb8, 0x72, 0xdd]; data.extend_from_slice(&[0u8; 12]);
1780 data.extend_from_slice(from.as_slice());
1781 data.extend_from_slice(&[0u8; 12]);
1783 data.extend_from_slice(to.as_slice());
1784 data.extend_from_slice(&amount.to_be_bytes::<32>());
1786 Bytes::from(data)
1787 }
1788
1789 #[test]
1790 fn test_erc20_transfer_detection_eip1559() {
1791 let parser = EthereumParser::new();
1792
1793 let token_contract = Address::from([0xaa; 20]); let recipient = Address::from([0xbb; 20]); let token_amount = U256::from(1_000_000u64); let calldata = erc20_transfer_calldata(recipient, token_amount);
1798
1799 let raw = encode_eip1559_tx(
1800 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, );
1809
1810 let result = parser.parse(&raw);
1811 assert!(result.is_ok(), "parsing failed: {result:?}");
1812
1813 let parsed = result.expect("should parse");
1814
1815 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1817
1818 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1820
1821 assert_eq!(parsed.amount, Some(token_amount));
1823
1824 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1826 }
1827
1828 #[test]
1829 fn test_erc20_approve_detection_eip1559() {
1830 let parser = EthereumParser::new();
1831
1832 let token_contract = Address::from([0xaa; 20]);
1833 let spender = Address::from([0xcc; 20]); let approval_amount = U256::MAX; let calldata = erc20_approve_calldata(spender, approval_amount);
1837
1838 let raw = encode_eip1559_tx(
1839 1, 1, 1_000_000_000, 2_000_000_000, 60_000, Some(token_contract), U256::ZERO, calldata, );
1848
1849 let result = parser.parse(&raw);
1850 assert!(result.is_ok(), "parsing failed: {result:?}");
1851
1852 let parsed = result.expect("should parse");
1853
1854 assert_eq!(parsed.tx_type, TxType::TokenApproval);
1856
1857 assert_eq!(parsed.recipient, Some(format!("{spender}")));
1859
1860 assert_eq!(parsed.amount, Some(approval_amount));
1862
1863 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1865 }
1866
1867 #[test]
1868 fn test_erc20_transfer_from_detection_eip1559() {
1869 let parser = EthereumParser::new();
1870
1871 let token_contract = Address::from([0xaa; 20]);
1872 let from_addr = Address::from([0xdd; 20]); 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);
1877
1878 let raw = encode_eip1559_tx(
1879 1, 2, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, );
1888
1889 let result = parser.parse(&raw);
1890 assert!(result.is_ok(), "parsing failed: {result:?}");
1891
1892 let parsed = result.expect("should parse");
1893
1894 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1896
1897 assert_eq!(parsed.recipient, Some(format!("{to_addr}")));
1899
1900 assert_eq!(parsed.amount, Some(token_amount));
1902
1903 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1905 }
1906
1907 #[test]
1908 fn test_erc20_transfer_detection_legacy() {
1909 let parser = EthereumParser::new();
1910
1911 let token_contract = Address::from([0xaa; 20]);
1912 let recipient = Address::from([0xbb; 20]);
1913 let token_amount = U256::from(2_000_000u64);
1914
1915 let calldata = erc20_transfer_calldata(recipient, token_amount);
1916
1917 let raw = encode_legacy_tx(
1918 5, 20_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, Some(1), );
1926
1927 let result = parser.parse(&raw);
1928 assert!(result.is_ok(), "parsing failed: {result:?}");
1929
1930 let parsed = result.expect("should parse");
1931
1932 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1933 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1934 assert_eq!(parsed.amount, Some(token_amount));
1935 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1936 }
1937
1938 #[test]
1939 fn test_erc20_detection_eip2930() {
1940 let parser = EthereumParser::new();
1941
1942 let token_contract = Address::from([0xaa; 20]);
1943 let spender = Address::from([0xcc; 20]);
1944 let approval_amount = U256::from(1_000_000_000_000u64);
1945
1946 let calldata = erc20_approve_calldata(spender, approval_amount);
1947
1948 let raw = encode_eip2930_tx(
1949 1, 3, 10_000_000_000, 80_000, Some(token_contract), U256::ZERO, calldata, );
1957
1958 let result = parser.parse(&raw);
1959 assert!(result.is_ok(), "parsing failed: {result:?}");
1960
1961 let parsed = result.expect("should parse");
1962
1963 assert_eq!(parsed.tx_type, TxType::TokenApproval);
1964 assert_eq!(parsed.recipient, Some(format!("{spender}")));
1965 assert_eq!(parsed.amount, Some(approval_amount));
1966 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1967 }
1968
1969 #[test]
1970 fn test_non_erc20_contract_call_unchanged() {
1971 let parser = EthereumParser::new();
1972
1973 let contract = Address::from([0x12; 20]);
1974 let calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef, 0x00]);
1976
1977 let raw = encode_eip1559_tx(
1978 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(contract), U256::ZERO, calldata, );
1987
1988 let result = parser.parse(&raw);
1989 assert!(result.is_ok(), "parsing failed: {result:?}");
1990
1991 let parsed = result.expect("should parse");
1992
1993 assert_eq!(parsed.tx_type, TxType::ContractCall);
1995
1996 assert_eq!(parsed.recipient, Some(format!("{contract}")));
1998
1999 assert!(parsed.token_address.is_none());
2001 }
2002
2003 #[test]
2004 fn test_simple_eth_transfer_unchanged() {
2005 let parser = EthereumParser::new();
2006
2007 let recipient = Address::from([0x12; 20]);
2008 let eth_amount = U256::from(1_000_000_000_000_000_000u64); let raw = encode_eip1559_tx(
2011 1, 0, 1_000_000_000, 2_000_000_000, 21_000, Some(recipient), eth_amount, Bytes::default(), );
2020
2021 let result = parser.parse(&raw);
2022 assert!(result.is_ok(), "parsing failed: {result:?}");
2023
2024 let parsed = result.expect("should parse");
2025
2026 assert_eq!(parsed.tx_type, TxType::Transfer);
2028
2029 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
2031
2032 assert_eq!(parsed.amount, Some(eth_amount));
2034
2035 assert!(parsed.token_address.is_none());
2037 }
2038
2039 #[test]
2040 fn test_erc20_with_eth_value() {
2041 let parser = EthereumParser::new();
2044
2045 let token_contract = Address::from([0xaa; 20]);
2046 let recipient = Address::from([0xbb; 20]);
2047 let token_amount = U256::from(1_000_000u64);
2048 let eth_value = U256::from(100_000_000_000_000_000u64); let calldata = erc20_transfer_calldata(recipient, token_amount);
2051
2052 let raw = encode_eip1559_tx(
2053 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), eth_value, calldata, );
2062
2063 let result = parser.parse(&raw);
2064 assert!(result.is_ok(), "parsing failed: {result:?}");
2065
2066 let parsed = result.expect("should parse");
2067
2068 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2070
2071 assert_eq!(parsed.amount, Some(token_amount));
2073
2074 assert!(parsed.token_address.is_some());
2076 }
2077
2078 #[test]
2079 fn test_erc20_zero_amount() {
2080 let parser = EthereumParser::new();
2081
2082 let token_contract = Address::from([0xaa; 20]);
2083 let recipient = Address::from([0xbb; 20]);
2084 let token_amount = U256::ZERO;
2085
2086 let calldata = erc20_transfer_calldata(recipient, token_amount);
2087
2088 let raw = encode_eip1559_tx(
2089 1, 0, 1_000_000_000, 2_000_000_000, 60_000, Some(token_contract), U256::ZERO, calldata, );
2098
2099 let result = parser.parse(&raw);
2100 assert!(result.is_ok(), "parsing failed: {result:?}");
2101
2102 let parsed = result.expect("should parse");
2103
2104 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2106 assert_eq!(parsed.amount, Some(U256::ZERO));
2107 }
2108
2109 #[test]
2110 fn test_erc20_approve_zero_revoke() {
2111 let parser = EthereumParser::new();
2113
2114 let token_contract = Address::from([0xaa; 20]);
2115 let spender = Address::from([0xcc; 20]);
2116 let approval_amount = U256::ZERO; let calldata = erc20_approve_calldata(spender, approval_amount);
2119
2120 let raw = encode_eip1559_tx(
2121 1, 0, 1_000_000_000, 2_000_000_000, 50_000, Some(token_contract), U256::ZERO, calldata, );
2130
2131 let result = parser.parse(&raw);
2132 assert!(result.is_ok(), "parsing failed: {result:?}");
2133
2134 let parsed = result.expect("should parse");
2135
2136 assert_eq!(parsed.tx_type, TxType::TokenApproval);
2138 assert_eq!(parsed.amount, Some(U256::ZERO));
2139 }
2140
2141 #[test]
2146 fn test_legacy_tx_truncated_data() {
2147 let parser = EthereumParser::new();
2149
2150 let valid_raw = encode_legacy_tx(
2152 9,
2153 20_000_000_000,
2154 21000,
2155 Some(Address::from([0x35; 20])),
2156 U256::from(1_000_000_000_000_000_000u64),
2157 Bytes::new(),
2158 Some(1),
2159 );
2160
2161 let truncated = &valid_raw[..valid_raw.len() / 2];
2163
2164 let result = parser.parse(truncated);
2166
2167 assert!(result.is_err());
2169 assert!(matches!(
2170 result,
2171 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2172 ));
2173 }
2174
2175 #[test]
2176 fn test_eip1559_tx_truncated_data() {
2177 let parser = EthereumParser::new();
2179
2180 let valid_raw = encode_eip1559_tx(
2182 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(Address::from([0x35; 20])), U256::ZERO, Bytes::new(), );
2191
2192 let truncated = &valid_raw[..valid_raw.len() / 2];
2194
2195 let result = parser.parse(truncated);
2197
2198 assert!(result.is_err());
2200 assert!(matches!(
2201 result,
2202 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2203 ));
2204 }
2205
2206 #[test]
2207 fn test_eip2930_tx_truncated_data() {
2208 let parser = EthereumParser::new();
2210
2211 let valid_raw = encode_eip2930_tx(
2213 1, 0, 1_000_000_000, 21000, Some(Address::from([0x35; 20])), U256::ZERO, Bytes::new(), );
2221
2222 let truncated = &valid_raw[..valid_raw.len() / 2];
2224
2225 let result = parser.parse(truncated);
2227
2228 assert!(result.is_err());
2230 assert!(matches!(
2231 result,
2232 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2233 ));
2234 }
2235
2236 #[test]
2237 fn test_legacy_tx_invalid_rlp_structure() {
2238 let parser = EthereumParser::new();
2240
2241 let invalid_rlp = vec![0xf8, 0xff, 0x01, 0x02, 0x03];
2243
2244 let result = parser.parse(&invalid_rlp);
2246
2247 assert!(result.is_err());
2249 assert!(matches!(
2250 result,
2251 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2252 ));
2253 }
2254
2255 #[test]
2256 fn test_eip1559_tx_invalid_rlp_structure() {
2257 let parser = EthereumParser::new();
2259
2260 let invalid = vec![0x02, 0xf8, 0xff, 0x01, 0x02, 0x03];
2262
2263 let result = parser.parse(&invalid);
2265
2266 assert!(result.is_err());
2268 assert!(matches!(
2269 result,
2270 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2271 ));
2272 }
2273
2274 #[test]
2275 fn test_analyze_erc20_returns_none_for_non_erc20() {
2276 let parser = EthereumParser::new();
2279
2280 let contract = Address::from([0xaa; 20]);
2281 let invalid_calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78]);
2283
2284 let raw = encode_eip1559_tx(
2285 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(contract), U256::ZERO, invalid_calldata, );
2294
2295 let result = parser.parse(&raw);
2296 assert!(result.is_ok());
2297
2298 let parsed = result.unwrap();
2299
2300 assert_eq!(parsed.tx_type, TxType::ContractCall);
2302 assert!(parsed.token_address.is_none());
2304 }
2305
2306 fn encode_unsigned_eip1559_tx(
2312 chain_id: u64,
2313 nonce: u64,
2314 max_priority_fee_per_gas: u128,
2315 max_fee_per_gas: u128,
2316 gas_limit: u64,
2317 to: Option<Address>,
2318 value: U256,
2319 data: Bytes,
2320 ) -> Vec<u8> {
2321 use alloy_consensus::TxEip1559;
2322 use alloy_rlp::Encodable;
2323
2324 let tx = TxEip1559 {
2325 chain_id,
2326 nonce,
2327 max_priority_fee_per_gas,
2328 max_fee_per_gas,
2329 gas_limit,
2330 to: to.map_or(TxKind::Create, TxKind::Call),
2331 value,
2332 input: data,
2333 access_list: Default::default(),
2334 };
2335
2336 let mut payload = Vec::new();
2337 tx.encode(&mut payload);
2338
2339 let mut buf = vec![0x02]; buf.extend_from_slice(&payload);
2341 buf
2342 }
2343
2344 fn encode_unsigned_eip2930_tx(
2346 chain_id: u64,
2347 nonce: u64,
2348 gas_price: u128,
2349 gas_limit: u64,
2350 to: Option<Address>,
2351 value: U256,
2352 data: Bytes,
2353 ) -> Vec<u8> {
2354 use alloy_consensus::TxEip2930;
2355 use alloy_rlp::Encodable;
2356
2357 let tx = TxEip2930 {
2358 chain_id,
2359 nonce,
2360 gas_price,
2361 gas_limit,
2362 to: to.map_or(TxKind::Create, TxKind::Call),
2363 value,
2364 input: data,
2365 access_list: Default::default(),
2366 };
2367
2368 let mut payload = Vec::new();
2369 tx.encode(&mut payload);
2370
2371 let mut buf = vec![0x01]; buf.extend_from_slice(&payload);
2373 buf
2374 }
2375
2376 fn encode_unsigned_legacy_tx(
2378 nonce: u64,
2379 gas_price: u128,
2380 gas_limit: u64,
2381 to: Option<Address>,
2382 value: U256,
2383 data: Bytes,
2384 ) -> Vec<u8> {
2385 use alloy_consensus::TxLegacy;
2386 use alloy_rlp::Encodable;
2387
2388 let tx = TxLegacy {
2389 chain_id: None, nonce,
2391 gas_price,
2392 gas_limit,
2393 to: to.map_or(TxKind::Create, TxKind::Call),
2394 value,
2395 input: data,
2396 };
2397
2398 let mut buf = Vec::new();
2399 tx.encode(&mut buf);
2400 buf
2401 }
2402
2403 #[test]
2404 fn test_parse_unsigned_eip1559() {
2405 let parser = EthereumParser::new();
2406 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2407 let raw = encode_unsigned_eip1559_tx(
2408 1,
2409 0,
2410 1_000_000_000,
2411 2_000_000_000,
2412 21000,
2413 Some(recipient),
2414 U256::from(1_000_000_000_000_000_000u64),
2415 Bytes::new(),
2416 );
2417
2418 let result = parser.parse(&raw);
2419 assert!(
2420 result.is_ok(),
2421 "Should parse unsigned EIP-1559: {:?}",
2422 result.err()
2423 );
2424 let parsed = result.unwrap();
2425 assert_eq!(parsed.chain_id, Some(1));
2426 assert_eq!(parsed.nonce, Some(0));
2427 assert_eq!(
2428 parsed.metadata.get("unsigned"),
2429 Some(&serde_json::Value::Bool(true))
2430 );
2431 }
2432
2433 #[test]
2434 fn test_parse_unsigned_eip2930() {
2435 let parser = EthereumParser::new();
2436 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2437 let raw = encode_unsigned_eip2930_tx(
2438 1,
2439 0,
2440 1_000_000_000,
2441 21000,
2442 Some(recipient),
2443 U256::from(1_000_000_000_000_000_000u64),
2444 Bytes::new(),
2445 );
2446
2447 let result = parser.parse(&raw);
2448 assert!(
2449 result.is_ok(),
2450 "Should parse unsigned EIP-2930: {:?}",
2451 result.err()
2452 );
2453 let parsed = result.unwrap();
2454 assert_eq!(parsed.chain_id, Some(1));
2455 assert_eq!(
2456 parsed.metadata.get("unsigned"),
2457 Some(&serde_json::Value::Bool(true))
2458 );
2459 }
2460
2461 #[test]
2462 fn test_parse_unsigned_legacy_pre_eip155() {
2463 let parser = EthereumParser::new();
2464 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2465 let raw = encode_unsigned_legacy_tx(
2466 0,
2467 20_000_000_000,
2468 21000,
2469 Some(recipient),
2470 U256::from(1_000_000_000_000_000_000u64),
2471 Bytes::new(),
2472 );
2473
2474 let result = parser.parse(&raw);
2475 assert!(
2476 result.is_ok(),
2477 "Should parse unsigned legacy: {:?}",
2478 result.err()
2479 );
2480 let parsed = result.unwrap();
2481 assert_eq!(parsed.chain_id, Some(1)); assert_eq!(
2483 parsed.metadata.get("unsigned"),
2484 Some(&serde_json::Value::Bool(true))
2485 );
2486 }
2487
2488 #[test]
2493 fn test_assemble_signed_legacy_roundtrip() {
2494 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2495 let sig = Signature::new(
2496 U256::from(0xdeadbeef_u64),
2497 U256::from(0xcafebabe_u64),
2498 false,
2499 );
2500
2501 let tx = alloy_consensus::TxLegacy {
2502 chain_id: Some(1),
2503 nonce: 42,
2504 gas_price: 20_000_000_000,
2505 gas_limit: 21000,
2506 to: TxKind::Call(recipient),
2507 value: U256::from(1_000_000_000_000_000_000u64),
2508 input: Bytes::new(),
2509 };
2510
2511 let mut expected = Vec::new();
2513 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2514 &tx,
2515 &sig,
2516 &mut expected,
2517 );
2518
2519 let mut sig_bytes = [0u8; 65];
2522 sig.r()
2523 .to_be_bytes::<32>()
2524 .iter()
2525 .enumerate()
2526 .for_each(|(i, b)| sig_bytes[i] = *b);
2527 sig.s()
2528 .to_be_bytes::<32>()
2529 .iter()
2530 .enumerate()
2531 .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2532 sig_bytes[64] = u8::from(sig.v());
2533
2534 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2535 assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2536 assert_eq!(assembled.unwrap(), expected);
2537 }
2538
2539 #[test]
2540 fn test_assemble_signed_eip1559_roundtrip() {
2541 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2542 let sig = Signature::new(U256::from(0xdeadbeef_u64), U256::from(0xcafebabe_u64), true);
2543
2544 let tx = alloy_consensus::TxEip1559 {
2545 chain_id: 1,
2546 nonce: 10,
2547 max_priority_fee_per_gas: 1_000_000_000,
2548 max_fee_per_gas: 2_000_000_000,
2549 gas_limit: 21000,
2550 to: TxKind::Call(recipient),
2551 value: U256::from(500_000_000_000_000_000u64),
2552 input: Bytes::new(),
2553 access_list: Default::default(),
2554 };
2555
2556 let mut expected = vec![0x02];
2557 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2558 &tx,
2559 &sig,
2560 &mut expected,
2561 );
2562
2563 let mut sig_bytes = [0u8; 65];
2564 sig.r()
2565 .to_be_bytes::<32>()
2566 .iter()
2567 .enumerate()
2568 .for_each(|(i, b)| sig_bytes[i] = *b);
2569 sig.s()
2570 .to_be_bytes::<32>()
2571 .iter()
2572 .enumerate()
2573 .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2574 sig_bytes[64] = u8::from(sig.v());
2575
2576 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2577 assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2578 assert_eq!(assembled.unwrap(), expected);
2579 }
2580
2581 #[test]
2582 fn test_assemble_signed_eip2930_roundtrip() {
2583 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2584 let sig = Signature::new(
2585 U256::from(0xdeadbeef_u64),
2586 U256::from(0xcafebabe_u64),
2587 false,
2588 );
2589
2590 let tx = alloy_consensus::TxEip2930 {
2591 chain_id: 1,
2592 nonce: 5,
2593 gas_price: 20_000_000_000,
2594 gas_limit: 21000,
2595 to: TxKind::Call(recipient),
2596 value: U256::from(1_000_000_000_000_000_000u64),
2597 input: Bytes::new(),
2598 access_list: Default::default(),
2599 };
2600
2601 let mut expected = vec![0x01];
2602 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2603 &tx,
2604 &sig,
2605 &mut expected,
2606 );
2607
2608 let mut sig_bytes = [0u8; 65];
2609 sig.r()
2610 .to_be_bytes::<32>()
2611 .iter()
2612 .enumerate()
2613 .for_each(|(i, b)| sig_bytes[i] = *b);
2614 sig.s()
2615 .to_be_bytes::<32>()
2616 .iter()
2617 .enumerate()
2618 .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2619 sig_bytes[64] = u8::from(sig.v());
2620
2621 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2622 assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2623 assert_eq!(assembled.unwrap(), expected);
2624 }
2625
2626 #[test]
2627 fn test_assemble_signed_from_unsigned_eip1559() {
2628 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2629 let sig = Signature::new(
2630 U256::from(0xdeadbeef_u64),
2631 U256::from(0xcafebabe_u64),
2632 false,
2633 );
2634
2635 let tx = alloy_consensus::TxEip1559 {
2636 chain_id: 1,
2637 nonce: 0,
2638 max_priority_fee_per_gas: 1_000_000_000,
2639 max_fee_per_gas: 2_000_000_000,
2640 gas_limit: 21000,
2641 to: TxKind::Call(recipient),
2642 value: U256::from(1_000_000_000_000_000_000u64),
2643 input: Bytes::new(),
2644 access_list: Default::default(),
2645 };
2646
2647 let unsigned_raw = encode_unsigned_eip1559_tx(
2649 1,
2650 0,
2651 1_000_000_000,
2652 2_000_000_000,
2653 21000,
2654 Some(recipient),
2655 U256::from(1_000_000_000_000_000_000u64),
2656 Bytes::new(),
2657 );
2658
2659 let mut expected = vec![0x02];
2661 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2662 &tx,
2663 &sig,
2664 &mut expected,
2665 );
2666
2667 let mut sig_bytes = [0u8; 65];
2668 sig.r()
2669 .to_be_bytes::<32>()
2670 .iter()
2671 .enumerate()
2672 .for_each(|(i, b)| sig_bytes[i] = *b);
2673 sig.s()
2674 .to_be_bytes::<32>()
2675 .iter()
2676 .enumerate()
2677 .for_each(|(i, b)| sig_bytes[32 + i] = *b);
2678 sig_bytes[64] = u8::from(sig.v());
2679
2680 let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
2681 assert!(
2682 assembled.is_ok(),
2683 "Assembly from unsigned failed: {:?}",
2684 assembled.err()
2685 );
2686 assert_eq!(assembled.unwrap(), expected);
2687 }
2688
2689 #[test]
2690 fn test_assemble_signed_invalid_signature_length() {
2691 let raw = encode_eip1559_tx(
2692 1,
2693 0,
2694 1_000_000_000,
2695 2_000_000_000,
2696 21000,
2697 Some(Address::ZERO),
2698 U256::ZERO,
2699 Bytes::new(),
2700 );
2701
2702 let short_sig = [0u8; 32]; let result = EthereumParser::assemble_signed(&raw, &short_sig);
2704 assert!(result.is_err());
2705 let err = result.unwrap_err();
2706 assert!(err.to_string().contains("expected 65-byte signature"));
2707 }
2708
2709 #[test]
2710 fn test_assemble_signed_invalid_recovery_id() {
2711 let raw = encode_eip1559_tx(
2712 1,
2713 0,
2714 1_000_000_000,
2715 2_000_000_000,
2716 21000,
2717 Some(Address::ZERO),
2718 U256::ZERO,
2719 Bytes::new(),
2720 );
2721
2722 let mut bad_sig = [0u8; 65];
2723 bad_sig[64] = 2; let result = EthereumParser::assemble_signed(&raw, &bad_sig);
2725 assert!(result.is_err());
2726 let err = result.unwrap_err();
2727 assert!(err.to_string().contains("invalid recovery id"));
2728 }
2729}