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 extract_legacy_chain_info(items: &[&[u8]]) -> Result<(bool, Option<u64>), ParseError> {
113 if items.len() == 6 {
114 return Ok((true, None));
116 }
117
118 let v_raw = decode_u64(
121 items
122 .get(6)
123 .ok_or_else(|| ParseError::MalformedTransaction {
124 context: "missing v/chainId field".to_string(),
125 })?,
126 )?;
127 let r_val = decode_u256(
128 items
129 .get(7)
130 .ok_or_else(|| ParseError::MalformedTransaction {
131 context: "missing r field".to_string(),
132 })?,
133 )?;
134 let s_val = decode_u256(
135 items
136 .get(8)
137 .ok_or_else(|| ParseError::MalformedTransaction {
138 context: "missing s field".to_string(),
139 })?,
140 )?;
141
142 let is_unsigned = r_val.is_zero() && s_val.is_zero();
143
144 let chain_id = if is_unsigned {
145 if v_raw == 0 {
148 return Err(ParseError::MalformedTransaction {
149 context: "unsigned EIP-155 transaction has chain_id=0".to_string(),
150 });
151 }
152 Some(v_raw)
153 } else if v_raw >= 35 {
154 Some((v_raw - 35) / 2)
156 } else if v_raw == 27 || v_raw == 28 {
157 None
159 } else {
160 return Err(ParseError::MalformedTransaction {
162 context: format!("invalid v value for signed legacy transaction: {v_raw}"),
163 });
164 };
165
166 Ok((is_unsigned, chain_id))
167 }
168
169 fn parse_legacy_with_hash_source(
180 hash_source: &[u8],
181 rlp_payload: &[u8],
182 ) -> Result<ParsedTx, ParseError> {
183 let items = decode_list(rlp_payload)?;
185
186 if items.len() != 9 && items.len() != 6 {
191 return Err(ParseError::MalformedTransaction {
192 context: format!(
193 "legacy transaction expected 6 or 9 items, got {}",
194 items.len()
195 ),
196 });
197 }
198
199 let (is_unsigned, chain_id_opt) = Self::extract_legacy_chain_info(&items)?;
200 let chain_id = chain_id_opt.unwrap_or(1);
202
203 let nonce_bytes = items
205 .first()
206 .ok_or_else(|| ParseError::MalformedTransaction {
207 context: "missing nonce field".to_string(),
208 })?;
209 let to_bytes = items
210 .get(3)
211 .ok_or_else(|| ParseError::MalformedTransaction {
212 context: "missing to field".to_string(),
213 })?;
214 let value_bytes = items
215 .get(4)
216 .ok_or_else(|| ParseError::MalformedTransaction {
217 context: "missing value field".to_string(),
218 })?;
219 let data_bytes = items
220 .get(5)
221 .ok_or_else(|| ParseError::MalformedTransaction {
222 context: "missing data field".to_string(),
223 })?;
224
225 let nonce = decode_u64(nonce_bytes)?;
227
228 let recipient = decode_optional_address(to_bytes)?;
230
231 let amount = decode_u256(value_bytes)?;
233
234 let data = decode_bytes(data_bytes)?;
236
237 let erc20_info = recipient
239 .as_ref()
240 .and_then(|addr| Self::analyze_erc20(addr, &data));
241
242 let (final_tx_type, final_recipient, final_amount, token_address) =
244 erc20_info.as_ref().map_or_else(
245 || {
246 (
247 Self::determine_tx_type(recipient.as_ref(), &data, &amount),
248 recipient.map(|addr| format!("{addr}")),
249 Some(amount),
250 None,
251 )
252 },
253 |info| {
254 (
255 info.tx_type,
256 Some(format!("{}", info.recipient)),
257 Some(info.amount),
258 Some(format!("{}", info.token_address)),
259 )
260 },
261 );
262
263 let hash = keccak256(hash_source);
265
266 let mut metadata = std::collections::HashMap::new();
267 if is_unsigned {
268 metadata.insert("unsigned".to_string(), serde_json::Value::Bool(true));
269 }
270
271 Ok(ParsedTx {
272 hash: hash.into(),
273 recipient: final_recipient,
274 amount: final_amount,
275 token: None, token_address,
277 tx_type: final_tx_type,
278 chain: "ethereum".to_string(),
279 nonce: Some(nonce),
280 chain_id: Some(chain_id),
281 metadata,
282 })
283 }
284
285 fn parse_eip2930(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
290 let items = decode_list(payload)?;
292
293 let is_unsigned = items.len() == 8;
295 if items.len() != 11 && items.len() != 8 {
296 return Err(ParseError::MalformedTransaction {
297 context: format!(
298 "EIP-2930 transaction expected 8 or 11 items, got {}",
299 items.len()
300 ),
301 });
302 }
303
304 let chain_id_bytes = items
306 .first()
307 .ok_or_else(|| ParseError::MalformedTransaction {
308 context: "missing chainId field".to_string(),
309 })?;
310 let nonce_bytes = items
311 .get(1)
312 .ok_or_else(|| ParseError::MalformedTransaction {
313 context: "missing nonce field".to_string(),
314 })?;
315 let to_bytes = items
316 .get(4)
317 .ok_or_else(|| ParseError::MalformedTransaction {
318 context: "missing to field".to_string(),
319 })?;
320 let value_bytes = items
321 .get(5)
322 .ok_or_else(|| ParseError::MalformedTransaction {
323 context: "missing value field".to_string(),
324 })?;
325 let data_bytes = items
326 .get(6)
327 .ok_or_else(|| ParseError::MalformedTransaction {
328 context: "missing data field".to_string(),
329 })?;
330
331 let chain_id = decode_u64(chain_id_bytes)?;
333 let nonce = decode_u64(nonce_bytes)?;
334 let recipient = decode_optional_address(to_bytes)?;
335 let amount = decode_u256(value_bytes)?;
336 let data = decode_bytes(data_bytes)?;
337
338 let erc20_info = recipient
340 .as_ref()
341 .and_then(|addr| Self::analyze_erc20(addr, &data));
342
343 let (final_tx_type, final_recipient, final_amount, token_address) =
345 erc20_info.as_ref().map_or_else(
346 || {
347 (
348 Self::determine_tx_type(recipient.as_ref(), &data, &amount),
349 recipient.map(|addr| format!("{addr}")),
350 Some(amount),
351 None,
352 )
353 },
354 |info| {
355 (
356 info.tx_type,
357 Some(format!("{}", info.recipient)),
358 Some(info.amount),
359 Some(format!("{}", info.token_address)),
360 )
361 },
362 );
363
364 let hash = keccak256(raw);
366
367 Ok(ParsedTx {
368 hash: hash.into(),
369 recipient: final_recipient,
370 amount: final_amount,
371 token: None, token_address,
373 tx_type: final_tx_type,
374 chain: "ethereum".to_string(),
375 nonce: Some(nonce),
376 chain_id: Some(chain_id),
377 metadata: {
378 let mut m = std::collections::HashMap::new();
379 if is_unsigned {
380 m.insert("unsigned".to_string(), serde_json::Value::Bool(true));
381 }
382 m
383 },
384 })
385 }
386
387 fn parse_eip1559(raw: &[u8], payload: &[u8]) -> Result<ParsedTx, ParseError> {
392 let items = decode_list(payload)?;
394
395 let is_unsigned = items.len() == 9;
397 if items.len() != 12 && items.len() != 9 {
398 return Err(ParseError::MalformedTransaction {
399 context: format!(
400 "EIP-1559 transaction expected 9 or 12 items, got {}",
401 items.len()
402 ),
403 });
404 }
405
406 let chain_id_bytes = items
408 .first()
409 .ok_or_else(|| ParseError::MalformedTransaction {
410 context: "missing chainId field".to_string(),
411 })?;
412 let nonce_bytes = items
413 .get(1)
414 .ok_or_else(|| ParseError::MalformedTransaction {
415 context: "missing nonce field".to_string(),
416 })?;
417 let to_bytes = items
418 .get(5)
419 .ok_or_else(|| ParseError::MalformedTransaction {
420 context: "missing to field".to_string(),
421 })?;
422 let value_bytes = items
423 .get(6)
424 .ok_or_else(|| ParseError::MalformedTransaction {
425 context: "missing value field".to_string(),
426 })?;
427 let data_bytes = items
428 .get(7)
429 .ok_or_else(|| ParseError::MalformedTransaction {
430 context: "missing data field".to_string(),
431 })?;
432
433 let chain_id = decode_u64(chain_id_bytes)?;
435 let nonce = decode_u64(nonce_bytes)?;
436 let recipient = decode_optional_address(to_bytes)?;
437 let amount = decode_u256(value_bytes)?;
438 let data = decode_bytes(data_bytes)?;
439
440 let erc20_info = recipient
442 .as_ref()
443 .and_then(|addr| Self::analyze_erc20(addr, &data));
444
445 let (final_tx_type, final_recipient, final_amount, token_address) =
447 erc20_info.as_ref().map_or_else(
448 || {
449 (
450 Self::determine_tx_type(recipient.as_ref(), &data, &amount),
451 recipient.map(|addr| format!("{addr}")),
452 Some(amount),
453 None,
454 )
455 },
456 |info| {
457 (
458 info.tx_type,
459 Some(format!("{}", info.recipient)),
460 Some(info.amount),
461 Some(format!("{}", info.token_address)),
462 )
463 },
464 );
465
466 let hash = keccak256(raw);
468
469 Ok(ParsedTx {
470 hash: hash.into(),
471 recipient: final_recipient,
472 amount: final_amount,
473 token: None, token_address,
475 tx_type: final_tx_type,
476 chain: "ethereum".to_string(),
477 nonce: Some(nonce),
478 chain_id: Some(chain_id),
479 metadata: {
480 let mut m = std::collections::HashMap::new();
481 if is_unsigned {
482 m.insert("unsigned".to_string(), serde_json::Value::Bool(true));
483 }
484 m
485 },
486 })
487 }
488
489 const fn determine_tx_type(
495 recipient: Option<&alloy_primitives::Address>,
496 data: &[u8],
497 _amount: &U256,
498 ) -> TxType {
499 if recipient.is_none() {
500 TxType::Deployment
502 } else if !data.is_empty() {
503 TxType::ContractCall
505 } else {
506 TxType::Transfer
508 }
509 }
510
511 fn analyze_erc20(contract_address: &Address, data: &[u8]) -> Option<Erc20Info> {
525 let erc20_call = parse_erc20_call(data)?;
526
527 let (tx_type, recipient_addr) = match &erc20_call {
528 Erc20Call::Transfer { to, .. } | Erc20Call::TransferFrom { to, .. } => {
529 (TxType::TokenTransfer, Address::from_slice(to))
530 }
531 Erc20Call::Approve { spender, .. } => {
532 (TxType::TokenApproval, Address::from_slice(spender))
533 }
534 };
535
536 Some(Erc20Info {
537 tx_type,
538 token_address: *contract_address,
539 recipient: recipient_addr,
540 amount: *erc20_call.amount(),
541 })
542 }
543
544 pub fn assemble_signed(raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
566 use alloy_primitives::Signature;
567
568 if signature.len() != 65 {
569 return Err(ParseError::assembly_failed(format!(
570 "expected 65-byte signature, got {}",
571 signature.len()
572 )));
573 }
574
575 let r = U256::from_be_slice(
577 signature
578 .get(0..32)
579 .ok_or_else(|| ParseError::assembly_failed("signature too short for r"))?,
580 );
581 let s = U256::from_be_slice(
582 signature
583 .get(32..64)
584 .ok_or_else(|| ParseError::assembly_failed("signature too short for s"))?,
585 );
586 let v_byte = *signature
587 .get(64)
588 .ok_or_else(|| ParseError::assembly_failed("signature too short for v"))?;
589 if v_byte > 1 {
590 return Err(ParseError::assembly_failed(format!(
591 "invalid recovery id: expected 0 or 1, got {v_byte}"
592 )));
593 }
594 let v_parity = v_byte != 0;
595
596 let sig = Signature::new(r, s, v_parity);
597
598 match detect_tx_type(raw) {
600 None => {
601 if !crate::rlp::is_list(raw) {
603 return Err(ParseError::assembly_failed("not a valid RLP list"));
604 }
605 Self::assemble_legacy(raw, raw, &sig, None)
606 }
607 Some(0x00) => {
608 let payload = typed_tx_payload(raw)?;
610 Self::assemble_legacy(raw, payload, &sig, None)
611 }
612 Some(0x01) => {
613 let payload = typed_tx_payload(raw)?;
615 Self::assemble_eip2930(payload, &sig)
616 }
617 Some(0x02) => {
618 let payload = typed_tx_payload(raw)?;
620 Self::assemble_eip1559(payload, &sig)
621 }
622 Some(ty) => Err(ParseError::assembly_failed(format!(
623 "unsupported transaction type: 0x{ty:02x}"
624 ))),
625 }
626 }
627
628 fn assemble_legacy(
630 _raw: &[u8],
631 rlp_payload: &[u8],
632 sig: &alloy_primitives::Signature,
633 _type_prefix: Option<u8>,
634 ) -> Result<Vec<u8>, ParseError> {
635 use alloy_consensus::transaction::RlpEcdsaEncodableTx;
636 use alloy_consensus::TxLegacy;
637 use alloy_primitives::{Bytes, TxKind};
638
639 let items = decode_list(rlp_payload)?;
640
641 if items.len() != 6 && items.len() != 9 {
643 return Err(ParseError::assembly_failed(format!(
644 "legacy tx expected 6 or 9 items, got {}",
645 items.len()
646 )));
647 }
648
649 let nonce = decode_u64(
650 items
651 .first()
652 .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
653 )?;
654
655 let gas_price_u256 = decode_u256(
656 items
657 .get(1)
658 .ok_or_else(|| ParseError::assembly_failed("missing gasPrice"))?,
659 )?;
660 let gas_price: u128 = gas_price_u256
661 .try_into()
662 .map_err(|_| ParseError::assembly_failed("gasPrice overflow"))?;
663
664 let gas_limit = decode_u64(
665 items
666 .get(2)
667 .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
668 )?;
669
670 let to_addr = decode_optional_address(
671 items
672 .get(3)
673 .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
674 )?;
675
676 let value = decode_u256(
677 items
678 .get(4)
679 .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
680 )?;
681
682 let data = decode_bytes(
683 items
684 .get(5)
685 .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
686 )?;
687
688 let (_is_unsigned, chain_id) = Self::extract_legacy_chain_info(&items)?;
690
691 let tx = TxLegacy {
692 chain_id,
693 nonce,
694 gas_price,
695 gas_limit,
696 to: to_addr.map_or(TxKind::Create, TxKind::Call),
697 value,
698 input: Bytes::from(data),
699 };
700
701 let mut buf = Vec::new();
702 tx.rlp_encode_signed(sig, &mut buf);
703 Ok(buf)
704 }
705
706 fn assemble_eip2930(
708 payload: &[u8],
709 sig: &alloy_primitives::Signature,
710 ) -> Result<Vec<u8>, ParseError> {
711 use alloy_consensus::transaction::RlpEcdsaEncodableTx;
712 use alloy_consensus::TxEip2930;
713 use alloy_eips::eip2930::AccessList;
714 use alloy_primitives::{Bytes, TxKind};
715 use alloy_rlp::Decodable;
716
717 let items = decode_list(payload)?;
718
719 if items.len() != 8 && items.len() != 11 {
721 return Err(ParseError::assembly_failed(format!(
722 "EIP-2930 tx expected 8 or 11 items, got {}",
723 items.len()
724 )));
725 }
726
727 let chain_id = decode_u64(
728 items
729 .first()
730 .ok_or_else(|| ParseError::assembly_failed("missing chainId"))?,
731 )?;
732
733 let nonce = decode_u64(
734 items
735 .get(1)
736 .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
737 )?;
738
739 let gas_price_u256 = decode_u256(
740 items
741 .get(2)
742 .ok_or_else(|| ParseError::assembly_failed("missing gasPrice"))?,
743 )?;
744 let gas_price: u128 = gas_price_u256
745 .try_into()
746 .map_err(|_| ParseError::assembly_failed("gasPrice overflow"))?;
747
748 let gas_limit = decode_u64(
749 items
750 .get(3)
751 .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
752 )?;
753
754 let to_addr = decode_optional_address(
755 items
756 .get(4)
757 .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
758 )?;
759
760 let value = decode_u256(
761 items
762 .get(5)
763 .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
764 )?;
765
766 let data = decode_bytes(
767 items
768 .get(6)
769 .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
770 )?;
771
772 let access_list_bytes = items
773 .get(7)
774 .ok_or_else(|| ParseError::assembly_failed("missing accessList"))?;
775 let mut access_list_buf = *access_list_bytes;
776 let access_list = AccessList::decode(&mut access_list_buf).map_err(|e| {
777 ParseError::assembly_failed(format!("failed to decode access list: {e}"))
778 })?;
779
780 let tx = TxEip2930 {
781 chain_id,
782 nonce,
783 gas_price,
784 gas_limit,
785 to: to_addr.map_or(TxKind::Create, TxKind::Call),
786 value,
787 input: Bytes::from(data),
788 access_list,
789 };
790
791 let mut buf = vec![0x01]; tx.rlp_encode_signed(sig, &mut buf);
793 Ok(buf)
794 }
795
796 fn assemble_eip1559(
798 payload: &[u8],
799 sig: &alloy_primitives::Signature,
800 ) -> Result<Vec<u8>, ParseError> {
801 use alloy_consensus::transaction::RlpEcdsaEncodableTx;
802 use alloy_consensus::TxEip1559;
803 use alloy_eips::eip2930::AccessList;
804 use alloy_primitives::{Bytes, TxKind};
805 use alloy_rlp::Decodable;
806
807 let items = decode_list(payload)?;
808
809 if items.len() != 9 && items.len() != 12 {
811 return Err(ParseError::assembly_failed(format!(
812 "EIP-1559 tx expected 9 or 12 items, got {}",
813 items.len()
814 )));
815 }
816
817 let chain_id = decode_u64(
818 items
819 .first()
820 .ok_or_else(|| ParseError::assembly_failed("missing chainId"))?,
821 )?;
822
823 let nonce = decode_u64(
824 items
825 .get(1)
826 .ok_or_else(|| ParseError::assembly_failed("missing nonce"))?,
827 )?;
828
829 let max_priority_fee_u256 = decode_u256(
830 items
831 .get(2)
832 .ok_or_else(|| ParseError::assembly_failed("missing maxPriorityFeePerGas"))?,
833 )?;
834 let max_priority_fee_per_gas: u128 = max_priority_fee_u256
835 .try_into()
836 .map_err(|_| ParseError::assembly_failed("maxPriorityFeePerGas overflow"))?;
837
838 let max_fee_u256 = decode_u256(
839 items
840 .get(3)
841 .ok_or_else(|| ParseError::assembly_failed("missing maxFeePerGas"))?,
842 )?;
843 let max_fee_per_gas: u128 = max_fee_u256
844 .try_into()
845 .map_err(|_| ParseError::assembly_failed("maxFeePerGas overflow"))?;
846
847 let gas_limit = decode_u64(
848 items
849 .get(4)
850 .ok_or_else(|| ParseError::assembly_failed("missing gasLimit"))?,
851 )?;
852
853 let to_addr = decode_optional_address(
854 items
855 .get(5)
856 .ok_or_else(|| ParseError::assembly_failed("missing to"))?,
857 )?;
858
859 let value = decode_u256(
860 items
861 .get(6)
862 .ok_or_else(|| ParseError::assembly_failed("missing value"))?,
863 )?;
864
865 let data = decode_bytes(
866 items
867 .get(7)
868 .ok_or_else(|| ParseError::assembly_failed("missing data"))?,
869 )?;
870
871 let access_list_bytes = items
872 .get(8)
873 .ok_or_else(|| ParseError::assembly_failed("missing accessList"))?;
874 let mut access_list_buf = *access_list_bytes;
875 let access_list = AccessList::decode(&mut access_list_buf).map_err(|e| {
876 ParseError::assembly_failed(format!("failed to decode access list: {e}"))
877 })?;
878
879 let tx = TxEip1559 {
880 chain_id,
881 nonce,
882 max_priority_fee_per_gas,
883 max_fee_per_gas,
884 gas_limit,
885 to: to_addr.map_or(TxKind::Create, TxKind::Call),
886 value,
887 input: Bytes::from(data),
888 access_list,
889 };
890
891 let mut buf = vec![0x02]; tx.rlp_encode_signed(sig, &mut buf);
893 Ok(buf)
894 }
895}
896
897struct Erc20Info {
901 tx_type: TxType,
903 token_address: Address,
905 recipient: Address,
907 amount: U256,
909}
910
911impl Chain for EthereumParser {
912 fn id(&self) -> &'static str {
918 "ethereum"
919 }
920
921 fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
946 if raw.is_empty() {
947 return Err(ParseError::MalformedTransaction {
948 context: "empty transaction data".to_string(),
949 });
950 }
951
952 match detect_tx_type(raw) {
954 None => {
955 if crate::rlp::is_list(raw) {
958 Self::parse_legacy(raw)
959 } else {
960 Err(ParseError::MalformedTransaction {
961 context:
962 "invalid transaction format: not a valid RLP list or typed transaction"
963 .to_string(),
964 })
965 }
966 }
967 Some(0) => {
968 let payload = typed_tx_payload(raw)?;
971 Self::parse_legacy_with_hash_source(raw, payload)
972 }
973 Some(1) => {
974 let payload = typed_tx_payload(raw)?;
976 Self::parse_eip2930(raw, payload)
977 }
978 Some(2) => {
979 let payload = typed_tx_payload(raw)?;
981 Self::parse_eip1559(raw, payload)
982 }
983 Some(_) => Err(ParseError::UnknownTxType),
984 }
985 }
986
987 fn curve(&self) -> CurveType {
993 CurveType::Secp256k1
994 }
995
996 fn supports_version(&self, version: u8) -> bool {
1007 matches!(version, 0..=2)
1008 }
1009
1010 fn assemble_signed(&self, raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
1011 Self::assemble_signed(raw, signature)
1012 }
1013}
1014
1015#[cfg(test)]
1020mod tests {
1021 #![allow(
1022 clippy::expect_used,
1023 clippy::unwrap_used,
1024 clippy::panic,
1025 clippy::indexing_slicing,
1026 clippy::similar_names,
1027 clippy::redundant_clone,
1028 clippy::manual_string_new,
1029 clippy::needless_raw_string_hashes,
1030 clippy::needless_collect,
1031 clippy::unreadable_literal,
1032 clippy::default_trait_access,
1033 clippy::too_many_arguments,
1034 clippy::default_constructed_unit_structs
1035 )]
1036
1037 use super::*;
1038 use alloy_consensus::{transaction::RlpEcdsaEncodableTx, TxEip1559, TxEip2930, TxLegacy};
1039 use alloy_primitives::{hex, Address, Bytes, Signature, TxKind};
1040
1041 fn sig_to_bytes(sig: &Signature) -> [u8; 65] {
1043 let mut buf = [0u8; 65];
1044 buf[..32].copy_from_slice(&sig.r().to_be_bytes::<32>());
1045 buf[32..64].copy_from_slice(&sig.s().to_be_bytes::<32>());
1046 buf[64] = u8::from(sig.v());
1047 buf
1048 }
1049
1050 fn encode_legacy_tx(
1052 nonce: u64,
1053 gas_price: u128,
1054 gas_limit: u64,
1055 to: Option<Address>,
1056 value: U256,
1057 data: Bytes,
1058 chain_id: Option<u64>,
1059 ) -> Vec<u8> {
1060 let tx = TxLegacy {
1061 chain_id,
1062 nonce,
1063 gas_price,
1064 gas_limit,
1065 to: to.map_or(TxKind::Create, TxKind::Call),
1066 value,
1067 input: data,
1068 };
1069
1070 let sig = Signature::new(
1072 U256::from(0xffff_ffff_ffff_ffffu64),
1073 U256::from(0xffff_ffff_ffff_ffffu64),
1074 false,
1075 );
1076
1077 let mut buf = Vec::new();
1078 tx.rlp_encode_signed(&sig, &mut buf);
1079 buf
1080 }
1081
1082 fn encode_eip2930_tx(
1084 chain_id: u64,
1085 nonce: u64,
1086 gas_price: u128,
1087 gas_limit: u64,
1088 to: Option<Address>,
1089 value: U256,
1090 data: Bytes,
1091 ) -> Vec<u8> {
1092 let tx = TxEip2930 {
1093 chain_id,
1094 nonce,
1095 gas_price,
1096 gas_limit,
1097 to: to.map_or(TxKind::Create, TxKind::Call),
1098 value,
1099 input: data,
1100 access_list: Default::default(),
1101 };
1102
1103 let sig = Signature::new(
1105 U256::from(0xffff_ffff_ffff_ffffu64),
1106 U256::from(0xffff_ffff_ffff_ffffu64),
1107 false,
1108 );
1109
1110 let mut buf = Vec::new();
1112 buf.push(0x01); tx.rlp_encode_signed(&sig, &mut buf);
1114 buf
1115 }
1116
1117 fn encode_eip1559_tx(
1119 chain_id: u64,
1120 nonce: u64,
1121 max_priority_fee_per_gas: u128,
1122 max_fee_per_gas: u128,
1123 gas_limit: u64,
1124 to: Option<Address>,
1125 value: U256,
1126 data: Bytes,
1127 ) -> Vec<u8> {
1128 let tx = TxEip1559 {
1129 chain_id,
1130 nonce,
1131 max_priority_fee_per_gas,
1132 max_fee_per_gas,
1133 gas_limit,
1134 to: to.map_or(TxKind::Create, TxKind::Call),
1135 value,
1136 input: data,
1137 access_list: Default::default(),
1138 };
1139
1140 let sig = Signature::new(
1142 U256::from(0xffff_ffff_ffff_ffffu64),
1143 U256::from(0xffff_ffff_ffff_ffffu64),
1144 false,
1145 );
1146
1147 let mut buf = Vec::new();
1149 buf.push(0x02); tx.rlp_encode_signed(&sig, &mut buf);
1151 buf
1152 }
1153
1154 #[test]
1159 fn test_ethereum_parser_new() {
1160 let parser = EthereumParser::new();
1161 assert_eq!(parser.id(), "ethereum");
1162 }
1163
1164 #[test]
1165 fn test_ethereum_parser_default() {
1166 let parser = EthereumParser::default();
1167 assert_eq!(parser.id(), "ethereum");
1168 }
1169
1170 #[test]
1171 fn test_ethereum_parser_clone() {
1172 let parser = EthereumParser::new();
1173 let cloned = parser;
1174 assert_eq!(cloned.id(), "ethereum");
1175 }
1176
1177 #[test]
1178 fn test_ethereum_parser_debug() {
1179 let parser = EthereumParser::new();
1180 let debug_str = format!("{parser:?}");
1181 assert!(debug_str.contains("EthereumParser"));
1182 }
1183
1184 #[test]
1189 fn test_chain_id() {
1190 let parser = EthereumParser::new();
1191 assert_eq!(parser.id(), "ethereum");
1192 }
1193
1194 #[test]
1195 fn test_chain_curve() {
1196 let parser = EthereumParser::new();
1197 assert_eq!(parser.curve(), CurveType::Secp256k1);
1198 }
1199
1200 #[test]
1201 fn test_supports_version() {
1202 let parser = EthereumParser::new();
1203
1204 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));
1212 assert!(!parser.supports_version(255));
1213 }
1214
1215 #[test]
1220 fn test_parse_empty_input() {
1221 let parser = EthereumParser::new();
1222 let result = parser.parse(&[]);
1223
1224 assert!(result.is_err());
1225 assert!(matches!(
1226 result,
1227 Err(ParseError::MalformedTransaction { .. })
1228 ));
1229 }
1230
1231 #[test]
1236 fn test_parse_legacy_transaction() {
1237 let parser = EthereumParser::new();
1238
1239 let raw = hex::decode(
1243 "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1244 ).expect("valid hex");
1245
1246 let result = parser.parse(&raw);
1247 assert!(result.is_ok(), "parsing failed: {result:?}");
1248
1249 let parsed = result.expect("should parse successfully");
1250
1251 assert_eq!(parsed.chain, "ethereum");
1253 assert_eq!(parsed.nonce, Some(9));
1254 assert_eq!(parsed.tx_type, TxType::Transfer);
1255 assert!(parsed.recipient.is_some());
1256 assert_eq!(
1257 parsed.recipient.as_ref().map(|s| s.to_lowercase()),
1258 Some("0x3535353535353535353535353535353535353535".to_string())
1259 );
1260
1261 let expected_amount = U256::from(1_000_000_000_000_000_000u64);
1263 assert_eq!(parsed.amount, Some(expected_amount));
1264
1265 assert_eq!(parsed.chain_id, Some(1));
1267
1268 assert_ne!(parsed.hash, [0u8; 32]);
1270 }
1271
1272 #[test]
1273 fn test_parse_legacy_transaction_pre_eip155() {
1274 let parser = EthereumParser::new();
1275
1276 let to_addr = Address::from([0x12; 20]);
1278 let raw = encode_legacy_tx(
1279 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), None, );
1287
1288 let result = parser.parse(&raw);
1289 assert!(result.is_ok(), "parsing failed: {result:?}");
1290
1291 let parsed = result.expect("should parse successfully");
1292 assert_eq!(parsed.chain_id, Some(1));
1294 }
1295
1296 #[test]
1297 fn test_parse_legacy_contract_deployment() {
1298 let parser = EthereumParser::new();
1299
1300 let raw = encode_legacy_tx(
1302 0, 1_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), Some(1), );
1310
1311 let result = parser.parse(&raw);
1312 assert!(result.is_ok(), "parsing failed: {result:?}");
1313
1314 let parsed = result.expect("should parse successfully");
1315 assert_eq!(parsed.tx_type, TxType::Deployment);
1316 assert!(parsed.recipient.is_none());
1317 }
1318
1319 #[test]
1320 fn test_parse_legacy_contract_call() {
1321 let parser = EthereumParser::new();
1322
1323 let to_addr = Address::from([0x12; 20]);
1325 let raw = encode_legacy_tx(
1326 1, 1_000_000_000, 100000, Some(to_addr), U256::ZERO, Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), Some(1), );
1334
1335 let result = parser.parse(&raw);
1336 assert!(result.is_ok(), "parsing failed: {result:?}");
1337
1338 let parsed = result.expect("should parse successfully");
1339 assert_eq!(parsed.tx_type, TxType::ContractCall);
1340 assert!(parsed.recipient.is_some());
1341 }
1342
1343 #[test]
1348 fn test_parse_eip2930_transaction() {
1349 let parser = EthereumParser::new();
1350
1351 let to_addr = Address::from([0x12; 20]);
1353 let raw = encode_eip2930_tx(
1354 1, 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1362
1363 let result = parser.parse(&raw);
1364 assert!(result.is_ok(), "parsing failed: {result:?}");
1365
1366 let parsed = result.expect("should parse successfully");
1367
1368 assert_eq!(parsed.chain, "ethereum");
1369 assert_eq!(parsed.chain_id, Some(1));
1370 assert_eq!(parsed.nonce, Some(0));
1371 assert_eq!(parsed.tx_type, TxType::Transfer);
1372 assert!(parsed.recipient.is_some());
1373 }
1374
1375 #[test]
1376 fn test_parse_eip2930_contract_deployment() {
1377 let parser = EthereumParser::new();
1378
1379 let raw = encode_eip2930_tx(
1381 1, 0, 1_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), );
1389
1390 let result = parser.parse(&raw);
1391 assert!(result.is_ok(), "parsing failed: {result:?}");
1392
1393 let parsed = result.expect("should parse successfully");
1394 assert_eq!(parsed.tx_type, TxType::Deployment);
1395 assert!(parsed.recipient.is_none());
1396 }
1397
1398 #[test]
1403 fn test_parse_eip1559_transaction() {
1404 let parser = EthereumParser::new();
1405
1406 let to_addr = Address::from([0x12; 20]);
1408 let raw = encode_eip1559_tx(
1409 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1418
1419 let result = parser.parse(&raw);
1420 assert!(result.is_ok(), "parsing failed: {result:?}");
1421
1422 let parsed = result.expect("should parse successfully");
1423
1424 assert_eq!(parsed.chain, "ethereum");
1425 assert_eq!(parsed.chain_id, Some(1));
1426 assert_eq!(parsed.nonce, Some(0));
1427 assert_eq!(parsed.tx_type, TxType::Transfer);
1428 assert!(parsed.recipient.is_some());
1429 }
1430
1431 #[test]
1432 fn test_parse_eip1559_with_value() {
1433 let parser = EthereumParser::new();
1434
1435 let to_addr = Address::from([0x12; 20]);
1437 let value = U256::from(1_000_000_000_000_000_000u64); let raw = encode_eip1559_tx(
1439 1, 5, 1_000_000_000, 100_000_000_000, 21000, Some(to_addr), value, Bytes::default(), );
1448
1449 let result = parser.parse(&raw);
1450 assert!(result.is_ok(), "parsing failed: {result:?}");
1451
1452 let parsed = result.expect("should parse successfully");
1453
1454 assert_eq!(parsed.chain, "ethereum");
1455 assert_eq!(parsed.chain_id, Some(1));
1456 assert_eq!(parsed.nonce, Some(5));
1457 assert_eq!(parsed.tx_type, TxType::Transfer);
1458 assert_eq!(parsed.amount, Some(value));
1459 }
1460
1461 #[test]
1462 fn test_parse_eip1559_contract_deployment() {
1463 let parser = EthereumParser::new();
1464
1465 let raw = encode_eip1559_tx(
1467 1, 0, 1_000_000_000, 2_000_000_000, 100000, None, U256::ZERO, Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), );
1476
1477 let result = parser.parse(&raw);
1478 assert!(result.is_ok(), "parsing failed: {result:?}");
1479
1480 let parsed = result.expect("should parse successfully");
1481 assert_eq!(parsed.tx_type, TxType::Deployment);
1482 assert!(parsed.recipient.is_none());
1483 }
1484
1485 #[test]
1486 fn test_parse_eip1559_contract_call() {
1487 let parser = EthereumParser::new();
1488
1489 let to_addr = Address::from([0x12; 20]);
1491 let raw = encode_eip1559_tx(
1492 1, 0, 1_000_000_000, 2_000_000_000, 100000, Some(to_addr), U256::ZERO, Bytes::from(vec![0xa9, 0x05, 0x9c, 0xbb]), );
1501
1502 let result = parser.parse(&raw);
1503 assert!(result.is_ok(), "parsing failed: {result:?}");
1504
1505 let parsed = result.expect("should parse successfully");
1506 assert_eq!(parsed.tx_type, TxType::ContractCall);
1507 assert!(parsed.recipient.is_some());
1508 }
1509
1510 #[test]
1515 fn test_hash_is_correctly_computed() {
1516 let parser = EthereumParser::new();
1517
1518 let raw = hex::decode(
1519 "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1520 ).expect("valid hex");
1521
1522 let result = parser.parse(&raw);
1523 assert!(result.is_ok());
1524
1525 let parsed = result.expect("should parse");
1526
1527 let expected_hash = keccak256(&raw);
1529
1530 assert_eq!(parsed.hash, *expected_hash);
1531 }
1532
1533 #[test]
1534 fn test_eip1559_hash_includes_type_prefix() {
1535 let parser = EthereumParser::new();
1536
1537 let to_addr = Address::from([0x12; 20]);
1538 let raw = encode_eip1559_tx(
1539 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1548
1549 let result = parser.parse(&raw);
1550 assert!(result.is_ok());
1551
1552 let parsed = result.expect("should parse");
1553
1554 let expected_hash = keccak256(&raw);
1556 assert_eq!(parsed.hash, *expected_hash);
1557 }
1558
1559 #[test]
1560 fn test_type0_hash_includes_type_prefix() {
1561 let parser = EthereumParser::new();
1562
1563 let to_addr = Address::from([0x12; 20]);
1565 let legacy_raw = encode_legacy_tx(
1566 0, 1_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), Some(1), );
1574
1575 let mut type0_raw = vec![0x00];
1577 type0_raw.extend_from_slice(&legacy_raw);
1578
1579 let result = parser.parse(&type0_raw);
1580 assert!(result.is_ok(), "parsing failed: {result:?}");
1581
1582 let parsed = result.expect("should parse");
1583
1584 let expected_hash = keccak256(&type0_raw);
1586 assert_eq!(
1587 parsed.hash, *expected_hash,
1588 "type 0 hash should include type prefix"
1589 );
1590
1591 let wrong_hash = keccak256(&legacy_raw);
1593 assert_ne!(
1594 parsed.hash, *wrong_hash,
1595 "hash should NOT be computed without type prefix"
1596 );
1597 }
1598
1599 #[test]
1600 fn test_type0_vs_legacy_same_content_different_hash() {
1601 let parser = EthereumParser::new();
1602
1603 let to_addr = Address::from([0x12; 20]);
1605 let legacy_raw = encode_legacy_tx(
1606 5, 2_000_000_000, 21000, Some(to_addr), U256::from(1_000_000_000_000_000_000u64), Bytes::default(), Some(1), );
1614
1615 let legacy_result = parser.parse(&legacy_raw);
1617 assert!(legacy_result.is_ok());
1618 let legacy_parsed = legacy_result.expect("should parse legacy");
1619
1620 let mut type0_raw = vec![0x00];
1622 type0_raw.extend_from_slice(&legacy_raw);
1623
1624 let type0_result = parser.parse(&type0_raw);
1626 assert!(type0_result.is_ok());
1627 let type0_parsed = type0_result.expect("should parse type 0");
1628
1629 assert_eq!(legacy_parsed.nonce, type0_parsed.nonce);
1631 assert_eq!(legacy_parsed.recipient, type0_parsed.recipient);
1632 assert_eq!(legacy_parsed.amount, type0_parsed.amount);
1633 assert_eq!(legacy_parsed.chain_id, type0_parsed.chain_id);
1634
1635 assert_ne!(
1637 legacy_parsed.hash, type0_parsed.hash,
1638 "legacy and type 0 hashes should differ due to type prefix"
1639 );
1640 }
1641
1642 #[test]
1647 fn test_parse_unsupported_tx_type() {
1648 let parser = EthereumParser::new();
1649
1650 let raw = hex::decode("03f8c0").expect("valid hex");
1652
1653 let result = parser.parse(&raw);
1654 assert!(result.is_err());
1655 assert!(matches!(result, Err(ParseError::UnknownTxType)));
1656 }
1657
1658 #[test]
1659 fn test_parse_malformed_legacy_too_few_items() {
1660 let parser = EthereumParser::new();
1661
1662 let raw = hex::decode("c3010203").expect("valid hex");
1664
1665 let result = parser.parse(&raw);
1666 assert!(result.is_err());
1667 assert!(matches!(
1668 result,
1669 Err(ParseError::MalformedTransaction { .. })
1670 ));
1671 }
1672
1673 #[test]
1674 fn test_parse_invalid_rlp() {
1675 let parser = EthereumParser::new();
1676
1677 let raw = hex::decode("f8ff").expect("valid hex");
1679
1680 let result = parser.parse(&raw);
1681 assert!(result.is_err());
1682 }
1683
1684 #[test]
1685 fn test_parse_not_list_not_typed() {
1686 let parser = EthereumParser::new();
1687
1688 let raw = hex::decode("80").expect("valid hex");
1691
1692 let result = parser.parse(&raw);
1693 assert!(result.is_err());
1694 assert!(matches!(
1695 result,
1696 Err(ParseError::MalformedTransaction { .. })
1697 ));
1698 }
1699
1700 #[test]
1705 fn test_parse_eip1559_polygon() {
1706 let parser = EthereumParser::new();
1707
1708 let to_addr = Address::from([0x12; 20]);
1710 let raw = encode_eip1559_tx(
1711 137, 0, 1_000_000_000, 2_000_000_000, 21000, Some(to_addr), U256::ZERO, Bytes::default(), );
1720
1721 let result = parser.parse(&raw);
1722 assert!(result.is_ok(), "parsing failed: {result:?}");
1723
1724 let parsed = result.expect("should parse");
1725 assert_eq!(parsed.chain_id, Some(137));
1726 }
1727
1728 #[test]
1729 fn test_parse_legacy_with_high_chain_id() {
1730 let parser = EthereumParser::new();
1731
1732 let to_addr = Address::from([0x12; 20]);
1734 let raw = encode_legacy_tx(
1735 9, 20_000_000_000, 21000, Some(to_addr), U256::from(1_000_000_000_000_000_000u64), Bytes::default(), Some(56), );
1743
1744 let result = parser.parse(&raw);
1745 assert!(result.is_ok(), "parsing failed: {result:?}");
1746
1747 let parsed = result.expect("should parse");
1748 assert_eq!(parsed.chain_id, Some(56));
1749 }
1750
1751 #[test]
1752 fn test_parse_unsigned_eip155_with_high_chain_id() {
1753 let parser = EthereumParser::new();
1757 let recipient = Address::from([0x42; 20]);
1758 let chain_id: u64 = 84532;
1759
1760 let tx = TxLegacy {
1761 chain_id: Some(chain_id),
1762 nonce: 5,
1763 gas_price: 1_000_000_000,
1764 gas_limit: 21000,
1765 to: TxKind::Call(recipient),
1766 value: U256::from(1_000_000u64),
1767 input: Bytes::new(),
1768 };
1769
1770 let mut unsigned_raw = Vec::new();
1772 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
1773
1774 let result = parser.parse(&unsigned_raw);
1775 assert!(
1776 result.is_ok(),
1777 "parsing unsigned EIP-155 failed: {result:?}"
1778 );
1779
1780 let parsed = result.expect("should parse");
1781 assert_eq!(
1782 parsed.chain_id,
1783 Some(chain_id),
1784 "chain_id should be {chain_id}, not halved"
1785 );
1786 assert_eq!(
1788 parsed.metadata.get("unsigned"),
1789 Some(&serde_json::Value::Bool(true)),
1790 "unsigned EIP-155 tx should be flagged as unsigned"
1791 );
1792 }
1793
1794 #[test]
1795 fn test_parse_unsigned_eip155_with_chain_id_1() {
1796 let parser = EthereumParser::new();
1800 let recipient = Address::from([0xAB; 20]);
1801
1802 let tx = TxLegacy {
1803 chain_id: Some(1),
1804 nonce: 0,
1805 gas_price: 20_000_000_000,
1806 gas_limit: 21000,
1807 to: TxKind::Call(recipient),
1808 value: U256::from(1_000_000_000_000_000_000u64),
1809 input: Bytes::new(),
1810 };
1811
1812 let mut unsigned_raw = Vec::new();
1813 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
1814
1815 let result = parser.parse(&unsigned_raw);
1816 assert!(
1817 result.is_ok(),
1818 "parsing unsigned EIP-155 chain_id=1 failed: {result:?}"
1819 );
1820
1821 let parsed = result.expect("should parse");
1822 assert_eq!(parsed.chain_id, Some(1));
1823 assert_eq!(
1824 parsed.metadata.get("unsigned"),
1825 Some(&serde_json::Value::Bool(true)),
1826 );
1827 }
1828
1829 #[test]
1834 fn test_parser_is_send() {
1835 fn assert_send<T: Send>() {}
1836 assert_send::<EthereumParser>();
1837 }
1838
1839 #[test]
1840 fn test_parser_is_sync() {
1841 fn assert_sync<T: Sync>() {}
1842 assert_sync::<EthereumParser>();
1843 }
1844
1845 #[test]
1850 fn test_parser_as_trait_object() {
1851 let parser = EthereumParser::new();
1852 let chain: Box<dyn Chain> = Box::new(parser);
1853
1854 assert_eq!(chain.id(), "ethereum");
1855 assert_eq!(chain.curve(), CurveType::Secp256k1);
1856 assert!(chain.supports_version(0));
1857 assert!(chain.supports_version(1));
1858 assert!(chain.supports_version(2));
1859 }
1860
1861 fn erc20_transfer_calldata(to: Address, amount: U256) -> Bytes {
1867 let mut data = vec![0xa9, 0x05, 0x9c, 0xbb]; data.extend_from_slice(&[0u8; 12]);
1870 data.extend_from_slice(to.as_slice());
1871 data.extend_from_slice(&amount.to_be_bytes::<32>());
1873 Bytes::from(data)
1874 }
1875
1876 fn erc20_approve_calldata(spender: Address, amount: U256) -> Bytes {
1878 let mut data = vec![0x09, 0x5e, 0xa7, 0xb3]; data.extend_from_slice(&[0u8; 12]);
1881 data.extend_from_slice(spender.as_slice());
1882 data.extend_from_slice(&amount.to_be_bytes::<32>());
1884 Bytes::from(data)
1885 }
1886
1887 fn erc20_transfer_from_calldata(from: Address, to: Address, amount: U256) -> Bytes {
1889 let mut data = vec![0x23, 0xb8, 0x72, 0xdd]; data.extend_from_slice(&[0u8; 12]);
1892 data.extend_from_slice(from.as_slice());
1893 data.extend_from_slice(&[0u8; 12]);
1895 data.extend_from_slice(to.as_slice());
1896 data.extend_from_slice(&amount.to_be_bytes::<32>());
1898 Bytes::from(data)
1899 }
1900
1901 #[test]
1902 fn test_erc20_transfer_detection_eip1559() {
1903 let parser = EthereumParser::new();
1904
1905 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);
1910
1911 let raw = encode_eip1559_tx(
1912 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, );
1921
1922 let result = parser.parse(&raw);
1923 assert!(result.is_ok(), "parsing failed: {result:?}");
1924
1925 let parsed = result.expect("should parse");
1926
1927 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
1929
1930 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
1932
1933 assert_eq!(parsed.amount, Some(token_amount));
1935
1936 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1938 }
1939
1940 #[test]
1941 fn test_erc20_approve_detection_eip1559() {
1942 let parser = EthereumParser::new();
1943
1944 let token_contract = Address::from([0xaa; 20]);
1945 let spender = Address::from([0xcc; 20]); let approval_amount = U256::MAX; let calldata = erc20_approve_calldata(spender, approval_amount);
1949
1950 let raw = encode_eip1559_tx(
1951 1, 1, 1_000_000_000, 2_000_000_000, 60_000, Some(token_contract), U256::ZERO, calldata, );
1960
1961 let result = parser.parse(&raw);
1962 assert!(result.is_ok(), "parsing failed: {result:?}");
1963
1964 let parsed = result.expect("should parse");
1965
1966 assert_eq!(parsed.tx_type, TxType::TokenApproval);
1968
1969 assert_eq!(parsed.recipient, Some(format!("{spender}")));
1971
1972 assert_eq!(parsed.amount, Some(approval_amount));
1974
1975 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
1977 }
1978
1979 #[test]
1980 fn test_erc20_transfer_from_detection_eip1559() {
1981 let parser = EthereumParser::new();
1982
1983 let token_contract = Address::from([0xaa; 20]);
1984 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);
1989
1990 let raw = encode_eip1559_tx(
1991 1, 2, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, );
2000
2001 let result = parser.parse(&raw);
2002 assert!(result.is_ok(), "parsing failed: {result:?}");
2003
2004 let parsed = result.expect("should parse");
2005
2006 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2008
2009 assert_eq!(parsed.recipient, Some(format!("{to_addr}")));
2011
2012 assert_eq!(parsed.amount, Some(token_amount));
2014
2015 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
2017 }
2018
2019 #[test]
2020 fn test_erc20_transfer_detection_legacy() {
2021 let parser = EthereumParser::new();
2022
2023 let token_contract = Address::from([0xaa; 20]);
2024 let recipient = Address::from([0xbb; 20]);
2025 let token_amount = U256::from(2_000_000u64);
2026
2027 let calldata = erc20_transfer_calldata(recipient, token_amount);
2028
2029 let raw = encode_legacy_tx(
2030 5, 20_000_000_000, 100_000, Some(token_contract), U256::ZERO, calldata, Some(1), );
2038
2039 let result = parser.parse(&raw);
2040 assert!(result.is_ok(), "parsing failed: {result:?}");
2041
2042 let parsed = result.expect("should parse");
2043
2044 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2045 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
2046 assert_eq!(parsed.amount, Some(token_amount));
2047 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
2048 }
2049
2050 #[test]
2051 fn test_erc20_detection_eip2930() {
2052 let parser = EthereumParser::new();
2053
2054 let token_contract = Address::from([0xaa; 20]);
2055 let spender = Address::from([0xcc; 20]);
2056 let approval_amount = U256::from(1_000_000_000_000u64);
2057
2058 let calldata = erc20_approve_calldata(spender, approval_amount);
2059
2060 let raw = encode_eip2930_tx(
2061 1, 3, 10_000_000_000, 80_000, Some(token_contract), U256::ZERO, calldata, );
2069
2070 let result = parser.parse(&raw);
2071 assert!(result.is_ok(), "parsing failed: {result:?}");
2072
2073 let parsed = result.expect("should parse");
2074
2075 assert_eq!(parsed.tx_type, TxType::TokenApproval);
2076 assert_eq!(parsed.recipient, Some(format!("{spender}")));
2077 assert_eq!(parsed.amount, Some(approval_amount));
2078 assert_eq!(parsed.token_address, Some(format!("{token_contract}")));
2079 }
2080
2081 #[test]
2082 fn test_non_erc20_contract_call_unchanged() {
2083 let parser = EthereumParser::new();
2084
2085 let contract = Address::from([0x12; 20]);
2086 let calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef, 0x00]);
2088
2089 let raw = encode_eip1559_tx(
2090 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(contract), U256::ZERO, calldata, );
2099
2100 let result = parser.parse(&raw);
2101 assert!(result.is_ok(), "parsing failed: {result:?}");
2102
2103 let parsed = result.expect("should parse");
2104
2105 assert_eq!(parsed.tx_type, TxType::ContractCall);
2107
2108 assert_eq!(parsed.recipient, Some(format!("{contract}")));
2110
2111 assert!(parsed.token_address.is_none());
2113 }
2114
2115 #[test]
2116 fn test_simple_eth_transfer_unchanged() {
2117 let parser = EthereumParser::new();
2118
2119 let recipient = Address::from([0x12; 20]);
2120 let eth_amount = U256::from(1_000_000_000_000_000_000u64); let raw = encode_eip1559_tx(
2123 1, 0, 1_000_000_000, 2_000_000_000, 21_000, Some(recipient), eth_amount, Bytes::default(), );
2132
2133 let result = parser.parse(&raw);
2134 assert!(result.is_ok(), "parsing failed: {result:?}");
2135
2136 let parsed = result.expect("should parse");
2137
2138 assert_eq!(parsed.tx_type, TxType::Transfer);
2140
2141 assert_eq!(parsed.recipient, Some(format!("{recipient}")));
2143
2144 assert_eq!(parsed.amount, Some(eth_amount));
2146
2147 assert!(parsed.token_address.is_none());
2149 }
2150
2151 #[test]
2152 fn test_erc20_with_eth_value() {
2153 let parser = EthereumParser::new();
2156
2157 let token_contract = Address::from([0xaa; 20]);
2158 let recipient = Address::from([0xbb; 20]);
2159 let token_amount = U256::from(1_000_000u64);
2160 let eth_value = U256::from(100_000_000_000_000_000u64); let calldata = erc20_transfer_calldata(recipient, token_amount);
2163
2164 let raw = encode_eip1559_tx(
2165 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(token_contract), eth_value, calldata, );
2174
2175 let result = parser.parse(&raw);
2176 assert!(result.is_ok(), "parsing failed: {result:?}");
2177
2178 let parsed = result.expect("should parse");
2179
2180 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2182
2183 assert_eq!(parsed.amount, Some(token_amount));
2185
2186 assert!(parsed.token_address.is_some());
2188 }
2189
2190 #[test]
2191 fn test_erc20_zero_amount() {
2192 let parser = EthereumParser::new();
2193
2194 let token_contract = Address::from([0xaa; 20]);
2195 let recipient = Address::from([0xbb; 20]);
2196 let token_amount = U256::ZERO;
2197
2198 let calldata = erc20_transfer_calldata(recipient, token_amount);
2199
2200 let raw = encode_eip1559_tx(
2201 1, 0, 1_000_000_000, 2_000_000_000, 60_000, Some(token_contract), U256::ZERO, calldata, );
2210
2211 let result = parser.parse(&raw);
2212 assert!(result.is_ok(), "parsing failed: {result:?}");
2213
2214 let parsed = result.expect("should parse");
2215
2216 assert_eq!(parsed.tx_type, TxType::TokenTransfer);
2218 assert_eq!(parsed.amount, Some(U256::ZERO));
2219 }
2220
2221 #[test]
2222 fn test_erc20_approve_zero_revoke() {
2223 let parser = EthereumParser::new();
2225
2226 let token_contract = Address::from([0xaa; 20]);
2227 let spender = Address::from([0xcc; 20]);
2228 let approval_amount = U256::ZERO; let calldata = erc20_approve_calldata(spender, approval_amount);
2231
2232 let raw = encode_eip1559_tx(
2233 1, 0, 1_000_000_000, 2_000_000_000, 50_000, Some(token_contract), U256::ZERO, calldata, );
2242
2243 let result = parser.parse(&raw);
2244 assert!(result.is_ok(), "parsing failed: {result:?}");
2245
2246 let parsed = result.expect("should parse");
2247
2248 assert_eq!(parsed.tx_type, TxType::TokenApproval);
2250 assert_eq!(parsed.amount, Some(U256::ZERO));
2251 }
2252
2253 #[test]
2258 fn test_legacy_tx_truncated_data() {
2259 let parser = EthereumParser::new();
2261
2262 let valid_raw = encode_legacy_tx(
2264 9,
2265 20_000_000_000,
2266 21000,
2267 Some(Address::from([0x35; 20])),
2268 U256::from(1_000_000_000_000_000_000u64),
2269 Bytes::new(),
2270 Some(1),
2271 );
2272
2273 let truncated = &valid_raw[..valid_raw.len() / 2];
2275
2276 let result = parser.parse(truncated);
2278
2279 assert!(result.is_err());
2281 assert!(matches!(
2282 result,
2283 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2284 ));
2285 }
2286
2287 #[test]
2288 fn test_eip1559_tx_truncated_data() {
2289 let parser = EthereumParser::new();
2291
2292 let valid_raw = encode_eip1559_tx(
2294 1, 0, 1_000_000_000, 2_000_000_000, 21000, Some(Address::from([0x35; 20])), U256::ZERO, Bytes::new(), );
2303
2304 let truncated = &valid_raw[..valid_raw.len() / 2];
2306
2307 let result = parser.parse(truncated);
2309
2310 assert!(result.is_err());
2312 assert!(matches!(
2313 result,
2314 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2315 ));
2316 }
2317
2318 #[test]
2319 fn test_eip2930_tx_truncated_data() {
2320 let parser = EthereumParser::new();
2322
2323 let valid_raw = encode_eip2930_tx(
2325 1, 0, 1_000_000_000, 21000, Some(Address::from([0x35; 20])), U256::ZERO, Bytes::new(), );
2333
2334 let truncated = &valid_raw[..valid_raw.len() / 2];
2336
2337 let result = parser.parse(truncated);
2339
2340 assert!(result.is_err());
2342 assert!(matches!(
2343 result,
2344 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2345 ));
2346 }
2347
2348 #[test]
2349 fn test_legacy_tx_invalid_rlp_structure() {
2350 let parser = EthereumParser::new();
2352
2353 let invalid_rlp = vec![0xf8, 0xff, 0x01, 0x02, 0x03];
2355
2356 let result = parser.parse(&invalid_rlp);
2358
2359 assert!(result.is_err());
2361 assert!(matches!(
2362 result,
2363 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2364 ));
2365 }
2366
2367 #[test]
2368 fn test_eip1559_tx_invalid_rlp_structure() {
2369 let parser = EthereumParser::new();
2371
2372 let invalid = vec![0x02, 0xf8, 0xff, 0x01, 0x02, 0x03];
2374
2375 let result = parser.parse(&invalid);
2377
2378 assert!(result.is_err());
2380 assert!(matches!(
2381 result,
2382 Err(ParseError::InvalidRlp { .. } | ParseError::MalformedTransaction { .. })
2383 ));
2384 }
2385
2386 #[test]
2387 fn test_analyze_erc20_returns_none_for_non_erc20() {
2388 let parser = EthereumParser::new();
2391
2392 let contract = Address::from([0xaa; 20]);
2393 let invalid_calldata = Bytes::from(vec![0x12, 0x34, 0x56, 0x78]);
2395
2396 let raw = encode_eip1559_tx(
2397 1, 0, 1_000_000_000, 2_000_000_000, 100_000, Some(contract), U256::ZERO, invalid_calldata, );
2406
2407 let result = parser.parse(&raw);
2408 assert!(result.is_ok());
2409
2410 let parsed = result.unwrap();
2411
2412 assert_eq!(parsed.tx_type, TxType::ContractCall);
2414 assert!(parsed.token_address.is_none());
2416 }
2417
2418 fn encode_unsigned_eip1559_tx(
2424 chain_id: u64,
2425 nonce: u64,
2426 max_priority_fee_per_gas: u128,
2427 max_fee_per_gas: u128,
2428 gas_limit: u64,
2429 to: Option<Address>,
2430 value: U256,
2431 data: Bytes,
2432 ) -> Vec<u8> {
2433 use alloy_consensus::TxEip1559;
2434 use alloy_rlp::Encodable;
2435
2436 let tx = TxEip1559 {
2437 chain_id,
2438 nonce,
2439 max_priority_fee_per_gas,
2440 max_fee_per_gas,
2441 gas_limit,
2442 to: to.map_or(TxKind::Create, TxKind::Call),
2443 value,
2444 input: data,
2445 access_list: Default::default(),
2446 };
2447
2448 let mut payload = Vec::new();
2449 tx.encode(&mut payload);
2450
2451 let mut buf = vec![0x02]; buf.extend_from_slice(&payload);
2453 buf
2454 }
2455
2456 fn encode_unsigned_eip2930_tx(
2458 chain_id: u64,
2459 nonce: u64,
2460 gas_price: u128,
2461 gas_limit: u64,
2462 to: Option<Address>,
2463 value: U256,
2464 data: Bytes,
2465 ) -> Vec<u8> {
2466 use alloy_consensus::TxEip2930;
2467 use alloy_rlp::Encodable;
2468
2469 let tx = TxEip2930 {
2470 chain_id,
2471 nonce,
2472 gas_price,
2473 gas_limit,
2474 to: to.map_or(TxKind::Create, TxKind::Call),
2475 value,
2476 input: data,
2477 access_list: Default::default(),
2478 };
2479
2480 let mut payload = Vec::new();
2481 tx.encode(&mut payload);
2482
2483 let mut buf = vec![0x01]; buf.extend_from_slice(&payload);
2485 buf
2486 }
2487
2488 fn encode_unsigned_legacy_tx(
2490 nonce: u64,
2491 gas_price: u128,
2492 gas_limit: u64,
2493 to: Option<Address>,
2494 value: U256,
2495 data: Bytes,
2496 ) -> Vec<u8> {
2497 use alloy_consensus::TxLegacy;
2498 use alloy_rlp::Encodable;
2499
2500 let tx = TxLegacy {
2501 chain_id: None, nonce,
2503 gas_price,
2504 gas_limit,
2505 to: to.map_or(TxKind::Create, TxKind::Call),
2506 value,
2507 input: data,
2508 };
2509
2510 let mut buf = Vec::new();
2511 tx.encode(&mut buf);
2512 buf
2513 }
2514
2515 #[test]
2516 fn test_parse_unsigned_eip1559() {
2517 let parser = EthereumParser::new();
2518 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2519 let raw = encode_unsigned_eip1559_tx(
2520 1,
2521 0,
2522 1_000_000_000,
2523 2_000_000_000,
2524 21000,
2525 Some(recipient),
2526 U256::from(1_000_000_000_000_000_000u64),
2527 Bytes::new(),
2528 );
2529
2530 let result = parser.parse(&raw);
2531 assert!(
2532 result.is_ok(),
2533 "Should parse unsigned EIP-1559: {:?}",
2534 result.err()
2535 );
2536 let parsed = result.unwrap();
2537 assert_eq!(parsed.chain_id, Some(1));
2538 assert_eq!(parsed.nonce, Some(0));
2539 assert_eq!(
2540 parsed.metadata.get("unsigned"),
2541 Some(&serde_json::Value::Bool(true))
2542 );
2543 }
2544
2545 #[test]
2546 fn test_parse_unsigned_eip2930() {
2547 let parser = EthereumParser::new();
2548 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2549 let raw = encode_unsigned_eip2930_tx(
2550 1,
2551 0,
2552 1_000_000_000,
2553 21000,
2554 Some(recipient),
2555 U256::from(1_000_000_000_000_000_000u64),
2556 Bytes::new(),
2557 );
2558
2559 let result = parser.parse(&raw);
2560 assert!(
2561 result.is_ok(),
2562 "Should parse unsigned EIP-2930: {:?}",
2563 result.err()
2564 );
2565 let parsed = result.unwrap();
2566 assert_eq!(parsed.chain_id, Some(1));
2567 assert_eq!(
2568 parsed.metadata.get("unsigned"),
2569 Some(&serde_json::Value::Bool(true))
2570 );
2571 }
2572
2573 #[test]
2574 fn test_parse_unsigned_legacy_pre_eip155() {
2575 let parser = EthereumParser::new();
2576 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2577 let raw = encode_unsigned_legacy_tx(
2578 0,
2579 20_000_000_000,
2580 21000,
2581 Some(recipient),
2582 U256::from(1_000_000_000_000_000_000u64),
2583 Bytes::new(),
2584 );
2585
2586 let result = parser.parse(&raw);
2587 assert!(
2588 result.is_ok(),
2589 "Should parse unsigned legacy: {:?}",
2590 result.err()
2591 );
2592 let parsed = result.unwrap();
2593 assert_eq!(parsed.chain_id, Some(1)); assert_eq!(
2595 parsed.metadata.get("unsigned"),
2596 Some(&serde_json::Value::Bool(true))
2597 );
2598 }
2599
2600 #[test]
2605 fn test_assemble_signed_legacy_roundtrip() {
2606 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2607 let sig = Signature::new(
2608 U256::from(0xdeadbeef_u64),
2609 U256::from(0xcafebabe_u64),
2610 false,
2611 );
2612
2613 let tx = alloy_consensus::TxLegacy {
2614 chain_id: Some(1),
2615 nonce: 42,
2616 gas_price: 20_000_000_000,
2617 gas_limit: 21000,
2618 to: TxKind::Call(recipient),
2619 value: U256::from(1_000_000_000_000_000_000u64),
2620 input: Bytes::new(),
2621 };
2622
2623 let mut expected = Vec::new();
2625 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2626 &tx,
2627 &sig,
2628 &mut expected,
2629 );
2630
2631 let sig_bytes = sig_to_bytes(&sig);
2634
2635 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2636 assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2637 assert_eq!(assembled.unwrap(), expected);
2638 }
2639
2640 #[test]
2641 fn test_assemble_signed_eip1559_roundtrip() {
2642 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2643 let sig = Signature::new(U256::from(0xdeadbeef_u64), U256::from(0xcafebabe_u64), true);
2644
2645 let tx = alloy_consensus::TxEip1559 {
2646 chain_id: 1,
2647 nonce: 10,
2648 max_priority_fee_per_gas: 1_000_000_000,
2649 max_fee_per_gas: 2_000_000_000,
2650 gas_limit: 21000,
2651 to: TxKind::Call(recipient),
2652 value: U256::from(500_000_000_000_000_000u64),
2653 input: Bytes::new(),
2654 access_list: Default::default(),
2655 };
2656
2657 let mut expected = vec![0x02];
2658 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2659 &tx,
2660 &sig,
2661 &mut expected,
2662 );
2663
2664 let sig_bytes = sig_to_bytes(&sig);
2665
2666 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2667 assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2668 assert_eq!(assembled.unwrap(), expected);
2669 }
2670
2671 #[test]
2672 fn test_assemble_signed_eip2930_roundtrip() {
2673 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2674 let sig = Signature::new(
2675 U256::from(0xdeadbeef_u64),
2676 U256::from(0xcafebabe_u64),
2677 false,
2678 );
2679
2680 let tx = alloy_consensus::TxEip2930 {
2681 chain_id: 1,
2682 nonce: 5,
2683 gas_price: 20_000_000_000,
2684 gas_limit: 21000,
2685 to: TxKind::Call(recipient),
2686 value: U256::from(1_000_000_000_000_000_000u64),
2687 input: Bytes::new(),
2688 access_list: Default::default(),
2689 };
2690
2691 let mut expected = vec![0x01];
2692 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2693 &tx,
2694 &sig,
2695 &mut expected,
2696 );
2697
2698 let sig_bytes = sig_to_bytes(&sig);
2699
2700 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
2701 assert!(assembled.is_ok(), "Assembly failed: {:?}", assembled.err());
2702 assert_eq!(assembled.unwrap(), expected);
2703 }
2704
2705 #[test]
2706 fn test_assemble_signed_from_unsigned_eip1559() {
2707 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2708 let sig = Signature::new(
2709 U256::from(0xdeadbeef_u64),
2710 U256::from(0xcafebabe_u64),
2711 false,
2712 );
2713
2714 let tx = alloy_consensus::TxEip1559 {
2715 chain_id: 1,
2716 nonce: 0,
2717 max_priority_fee_per_gas: 1_000_000_000,
2718 max_fee_per_gas: 2_000_000_000,
2719 gas_limit: 21000,
2720 to: TxKind::Call(recipient),
2721 value: U256::from(1_000_000_000_000_000_000u64),
2722 input: Bytes::new(),
2723 access_list: Default::default(),
2724 };
2725
2726 let unsigned_raw = encode_unsigned_eip1559_tx(
2728 1,
2729 0,
2730 1_000_000_000,
2731 2_000_000_000,
2732 21000,
2733 Some(recipient),
2734 U256::from(1_000_000_000_000_000_000u64),
2735 Bytes::new(),
2736 );
2737
2738 let mut expected = vec![0x02];
2740 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2741 &tx,
2742 &sig,
2743 &mut expected,
2744 );
2745
2746 let sig_bytes = sig_to_bytes(&sig);
2747
2748 let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
2749 assert!(
2750 assembled.is_ok(),
2751 "Assembly from unsigned failed: {:?}",
2752 assembled.err()
2753 );
2754 assert_eq!(assembled.unwrap(), expected);
2755 }
2756
2757 #[test]
2758 fn test_assemble_signed_invalid_signature_length() {
2759 let raw = encode_eip1559_tx(
2760 1,
2761 0,
2762 1_000_000_000,
2763 2_000_000_000,
2764 21000,
2765 Some(Address::ZERO),
2766 U256::ZERO,
2767 Bytes::new(),
2768 );
2769
2770 let short_sig = [0u8; 32]; let result = EthereumParser::assemble_signed(&raw, &short_sig);
2772 assert!(result.is_err());
2773 let err = result.unwrap_err();
2774 assert!(err.to_string().contains("expected 65-byte signature"));
2775 }
2776
2777 #[test]
2778 fn test_assemble_signed_invalid_recovery_id() {
2779 let raw = encode_eip1559_tx(
2780 1,
2781 0,
2782 1_000_000_000,
2783 2_000_000_000,
2784 21000,
2785 Some(Address::ZERO),
2786 U256::ZERO,
2787 Bytes::new(),
2788 );
2789
2790 let mut bad_sig = [0u8; 65];
2791 bad_sig[64] = 2; let result = EthereumParser::assemble_signed(&raw, &bad_sig);
2793 assert!(result.is_err());
2794 let err = result.unwrap_err();
2795 assert!(err.to_string().contains("invalid recovery id"));
2796 }
2797
2798 #[test]
2799 fn test_assemble_signed_from_unsigned_legacy_eip155_high_chain_id() {
2800 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2804 let chain_id: u64 = 84532; let sig = Signature::new(
2807 U256::from_be_slice(&hex!(
2808 "f02452acc422c61ef96aa74d71c41ef272535b7bedb9a3cc923b0e5528558c6f"
2809 )),
2810 U256::from_be_slice(&hex!(
2811 "6b39d1e300572cbb0fdd4d5e4f866663bc2ad6caea0e41996033c90d8a97fedb"
2812 )),
2813 true, );
2815
2816 let tx = alloy_consensus::TxLegacy {
2818 chain_id: Some(chain_id),
2819 nonce: 0,
2820 gas_price: 1_000_000_000,
2821 gas_limit: 21000,
2822 to: TxKind::Call(recipient),
2823 value: U256::ZERO,
2824 input: Bytes::new(),
2825 };
2826
2827 let mut unsigned_raw = Vec::new();
2828 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
2829
2830 let mut expected = Vec::new();
2832 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2833 &tx,
2834 &sig,
2835 &mut expected,
2836 );
2837
2838 let sig_bytes = sig_to_bytes(&sig);
2840
2841 let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
2842 assert!(
2843 assembled.is_ok(),
2844 "Assembly from unsigned EIP-155 (chain_id={chain_id}) failed: {:?}",
2845 assembled.err()
2846 );
2847 let assembled_bytes = assembled.unwrap();
2848 assert_eq!(
2849 hex::encode(&assembled_bytes),
2850 hex::encode(&expected),
2851 "Assembled signed tx does not match expected for chain_id={chain_id}"
2852 );
2853 }
2854
2855 #[test]
2856 fn test_assemble_signed_from_unsigned_legacy_eip155_chain_id_1() {
2857 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2861 let chain_id: u64 = 1;
2862
2863 let sig = Signature::new(
2864 U256::from_be_slice(&hex!(
2865 "f02452acc422c61ef96aa74d71c41ef272535b7bedb9a3cc923b0e5528558c6f"
2866 )),
2867 U256::from_be_slice(&hex!(
2868 "6b39d1e300572cbb0fdd4d5e4f866663bc2ad6caea0e41996033c90d8a97fedb"
2869 )),
2870 true,
2871 );
2872
2873 let tx = TxLegacy {
2874 chain_id: Some(chain_id),
2875 nonce: 0,
2876 gas_price: 1_000_000_000,
2877 gas_limit: 21000,
2878 to: TxKind::Call(recipient),
2879 value: U256::ZERO,
2880 input: Bytes::new(),
2881 };
2882
2883 let mut unsigned_raw = Vec::new();
2884 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
2885
2886 let mut expected = Vec::new();
2887 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2888 &tx,
2889 &sig,
2890 &mut expected,
2891 );
2892
2893 let sig_bytes = sig_to_bytes(&sig);
2894
2895 let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
2896 assert!(
2897 assembled.is_ok(),
2898 "Assembly from unsigned EIP-155 (chain_id={chain_id}) failed: {:?}",
2899 assembled.err()
2900 );
2901 assert_eq!(
2902 hex::encode(assembled.unwrap()),
2903 hex::encode(&expected),
2904 "Assembled signed tx does not match expected for chain_id={chain_id}"
2905 );
2906 }
2907
2908 #[test]
2909 fn test_assemble_signed_from_signed_legacy_high_chain_id() {
2910 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2914 let chain_id: u64 = 84532;
2915
2916 let sig = Signature::new(
2917 U256::from_be_slice(&hex!(
2918 "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
2919 )),
2920 U256::from_be_slice(&hex!(
2921 "1122334455667788112233445566778811223344556677881122334455667788"
2922 )),
2923 false, );
2925
2926 let tx = TxLegacy {
2927 chain_id: Some(chain_id),
2928 nonce: 42,
2929 gas_price: 2_000_000_000,
2930 gas_limit: 100_000,
2931 to: TxKind::Call(recipient),
2932 value: U256::from(500_000_000_000_000_000u64),
2933 input: Bytes::new(),
2934 };
2935
2936 let mut signed_raw = Vec::new();
2938 alloy_consensus::transaction::RlpEcdsaEncodableTx::rlp_encode_signed(
2939 &tx,
2940 &sig,
2941 &mut signed_raw,
2942 );
2943
2944 let mut unsigned_raw = Vec::new();
2946 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
2947
2948 let sig_bytes = sig_to_bytes(&sig);
2949
2950 let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
2951 assert!(
2952 assembled.is_ok(),
2953 "Assembly from unsigned with high chain_id failed: {:?}",
2954 assembled.err()
2955 );
2956 assert_eq!(
2957 hex::encode(assembled.unwrap()),
2958 hex::encode(&signed_raw),
2959 "Assembled signed tx should match alloy's output for chain_id={chain_id}"
2960 );
2961 }
2962
2963 #[test]
2964 fn test_assemble_signed_from_unsigned_legacy_eip155_chain_id_35() {
2965 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2969 let chain_id: u64 = 35;
2970
2971 let sig = Signature::new(
2972 U256::from_be_slice(&hex!(
2973 "f02452acc422c61ef96aa74d71c41ef272535b7bedb9a3cc923b0e5528558c6f"
2974 )),
2975 U256::from_be_slice(&hex!(
2976 "6b39d1e300572cbb0fdd4d5e4f866663bc2ad6caea0e41996033c90d8a97fedb"
2977 )),
2978 false,
2979 );
2980
2981 let tx = TxLegacy {
2982 chain_id: Some(chain_id),
2983 nonce: 0,
2984 gas_price: 1_000_000_000,
2985 gas_limit: 21000,
2986 to: TxKind::Call(recipient),
2987 value: U256::ZERO,
2988 input: Bytes::new(),
2989 };
2990
2991 let mut unsigned_raw = Vec::new();
2992 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
2993
2994 let mut expected = Vec::new();
2995 RlpEcdsaEncodableTx::rlp_encode_signed(&tx, &sig, &mut expected);
2996
2997 let sig_bytes = sig_to_bytes(&sig);
2998
2999 let assembled = EthereumParser::assemble_signed(&unsigned_raw, &sig_bytes);
3000 assert!(
3001 assembled.is_ok(),
3002 "Assembly from unsigned EIP-155 (chain_id={chain_id}) failed: {:?}",
3003 assembled.err()
3004 );
3005 assert_eq!(
3006 hex::encode(assembled.unwrap()),
3007 hex::encode(&expected),
3008 "Assembled signed tx should match expected for chain_id={chain_id}"
3009 );
3010 }
3011
3012 #[test]
3013 fn test_parse_unsigned_eip155_chain_id_35() {
3014 let parser = EthereumParser::new();
3016 let recipient = Address::from([0x42; 20]);
3017
3018 let tx = TxLegacy {
3019 chain_id: Some(35),
3020 nonce: 0,
3021 gas_price: 1_000_000_000,
3022 gas_limit: 21000,
3023 to: TxKind::Call(recipient),
3024 value: U256::ZERO,
3025 input: Bytes::new(),
3026 };
3027
3028 let mut unsigned_raw = Vec::new();
3029 alloy_rlp::Encodable::encode(&tx, &mut unsigned_raw);
3030
3031 let parsed = parser.parse(&unsigned_raw).expect("should parse");
3032 assert_eq!(parsed.chain_id, Some(35));
3033 assert_eq!(
3034 parsed.metadata.get("unsigned"),
3035 Some(&serde_json::Value::Bool(true)),
3036 );
3037 }
3038
3039 #[test]
3040 fn test_parse_unsigned_eip155_chain_id_0_is_error() {
3041 let parser = EthereumParser::new();
3043
3044 use alloy_rlp::Encodable;
3048 let mut inner = Vec::new();
3049 0u64.encode(&mut inner); 1_000_000_000u64.encode(&mut inner); 21000u64.encode(&mut inner); Address::from([0x42; 20]).encode(&mut inner); U256::ZERO.encode(&mut inner); alloy_primitives::Bytes::new().encode(&mut inner); 0u64.encode(&mut inner); 0u64.encode(&mut inner); 0u64.encode(&mut inner); let mut raw = Vec::new();
3060 alloy_rlp::Header {
3061 list: true,
3062 payload_length: inner.len(),
3063 }
3064 .encode(&mut raw);
3065 raw.extend_from_slice(&inner);
3066
3067 let result = parser.parse(&raw);
3068 assert!(
3069 result.is_err(),
3070 "chain_id=0 unsigned EIP-155 tx should be rejected"
3071 );
3072 assert!(result.unwrap_err().to_string().contains("chain_id=0"),);
3073 }
3074
3075 #[test]
3076 fn test_parse_signed_legacy_with_invalid_v_is_error() {
3077 use alloy_rlp::Encodable;
3080 let parser = EthereumParser::new();
3081
3082 let mut inner = Vec::new();
3083 0u64.encode(&mut inner); 1_000_000_000u64.encode(&mut inner); 21000u64.encode(&mut inner); Address::from([0x42; 20]).encode(&mut inner); U256::ZERO.encode(&mut inner); alloy_primitives::Bytes::new().encode(&mut inner); 5u64.encode(&mut inner); 0xdeadbeefu64.encode(&mut inner); 0xcafebabeu64.encode(&mut inner); let mut raw = Vec::new();
3094 alloy_rlp::Header {
3095 list: true,
3096 payload_length: inner.len(),
3097 }
3098 .encode(&mut raw);
3099 raw.extend_from_slice(&inner);
3100
3101 let result = parser.parse(&raw);
3102 assert!(
3103 result.is_err(),
3104 "signed legacy tx with v=5 should be rejected"
3105 );
3106 assert!(result.unwrap_err().to_string().contains("invalid v value"),);
3107 }
3108
3109 #[test]
3110 fn test_nine_item_legacy_with_only_r_zero_is_treated_as_signed() {
3111 use alloy_rlp::Encodable;
3114 let parser = EthereumParser::new();
3115
3116 let mut inner = Vec::new();
3117 0u64.encode(&mut inner); 1_000_000_000u64.encode(&mut inner); 21000u64.encode(&mut inner); Address::from([0x42; 20]).encode(&mut inner); U256::ZERO.encode(&mut inner); alloy_primitives::Bytes::new().encode(&mut inner); 37u64.encode(&mut inner); 0u64.encode(&mut inner); 0xcafebabeu64.encode(&mut inner); let mut raw = Vec::new();
3128 alloy_rlp::Header {
3129 list: true,
3130 payload_length: inner.len(),
3131 }
3132 .encode(&mut raw);
3133 raw.extend_from_slice(&inner);
3134
3135 let parsed = parser.parse(&raw).expect("should parse as signed");
3136 assert_eq!(parsed.metadata.get("unsigned"), None);
3138 assert_eq!(parsed.chain_id, Some(1));
3140 }
3141
3142 #[test]
3143 fn test_assemble_pre_eip155_signed_legacy() {
3144 let recipient = Address::from(hex!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
3147 let sig = Signature::new(
3148 U256::from(0xdeadbeef_u64),
3149 U256::from(0xcafebabe_u64),
3150 false, );
3152
3153 let tx = TxLegacy {
3154 chain_id: None, nonce: 0,
3156 gas_price: 1_000_000_000,
3157 gas_limit: 21000,
3158 to: TxKind::Call(recipient),
3159 value: U256::from(1_000_000u64),
3160 input: Bytes::new(),
3161 };
3162
3163 let mut expected = Vec::new();
3164 RlpEcdsaEncodableTx::rlp_encode_signed(&tx, &sig, &mut expected);
3165
3166 let sig_bytes = sig_to_bytes(&sig);
3167
3168 let assembled = EthereumParser::assemble_signed(&expected, &sig_bytes);
3169 assert!(
3170 assembled.is_ok(),
3171 "Assembly of pre-EIP-155 signed legacy failed: {:?}",
3172 assembled.err()
3173 );
3174 assert_eq!(assembled.unwrap(), expected);
3175 }
3176}