txgate_chain/chain.rs
1//! Chain trait for blockchain transaction parsers.
2//!
3//! This module defines the [`Chain`] trait that all blockchain parsers must implement.
4//! The trait provides a unified interface for parsing raw transaction bytes into
5//! a common [`ParsedTx`] structure that can be evaluated by the policy engine.
6//!
7//! # Design Philosophy
8//!
9//! The `Chain` trait is designed to:
10//! - Provide a minimal, focused interface for transaction parsing
11//! - Be thread-safe (`Send + Sync`) for concurrent parsing in async runtimes
12//! - Support multiple transaction versions through the `supports_version` method
13//! - Enable runtime chain selection through trait objects (`Box<dyn Chain>`)
14//!
15//! # Version Handling Strategy
16//!
17//! Different blockchains have different versioning schemes:
18//!
19//! ## Ethereum
20//! - Legacy transactions (type 0): No version byte, RLP-encoded
21//! - EIP-2930 (type 1): Access list transactions
22//! - EIP-1559 (type 2): Dynamic fee transactions
23//! - EIP-4844 (type 3): Blob transactions
24//!
25//! The version byte is the first byte of the raw transaction data for typed
26//! transactions. Legacy transactions start with an RLP list marker (0xc0-0xff).
27//!
28//! ## Bitcoin
29//! - Version field in transaction header (typically 1 or 2)
30//! - `SegWit` transactions have a marker/flag after version
31//!
32//! ## Solana
33//! - Message version in the first byte (0 for legacy, 128+ for versioned)
34//!
35//! Parsers should use `supports_version` to indicate which versions they handle
36//! and return `ParseError::UnknownTxType` for unsupported versions.
37//!
38//! # Example Implementation
39//!
40//! ```ignore
41//! use txgate_chain::Chain;
42//! use txgate_core::{ParsedTx, error::ParseError};
43//! use txgate_crypto::CurveType;
44//!
45//! struct EthereumParser;
46//!
47//! impl Chain for EthereumParser {
48//! fn id(&self) -> &'static str {
49//! "ethereum"
50//! }
51//!
52//! fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
53//! // Parse Ethereum transaction bytes...
54//! todo!()
55//! }
56//!
57//! fn curve(&self) -> CurveType {
58//! CurveType::Secp256k1
59//! }
60//!
61//! fn supports_version(&self, version: u8) -> bool {
62//! // Support legacy (detected by RLP marker), EIP-2930, EIP-1559, EIP-4844
63//! matches!(version, 0 | 1 | 2 | 3)
64//! }
65//! }
66//! ```
67//!
68//! # Security Considerations
69//!
70//! - Parsers must validate all input data thoroughly
71//! - The `hash` field in `ParsedTx` must be the exact bytes that will be signed
72//! - Parsers should not trust any data from the raw transaction without validation
73//! - Integer overflow and underflow must be handled carefully
74
75use txgate_core::{error::ParseError, ParsedTx};
76use txgate_crypto::CurveType;
77
78/// Trait for blockchain transaction parsers.
79///
80/// Each supported blockchain implements this trait to parse raw transaction
81/// bytes into a unified [`ParsedTx`] structure that can be evaluated by the
82/// policy engine.
83///
84/// # Implementor Requirements
85///
86/// Implementations must:
87/// - Parse raw transaction bytes according to the chain's encoding format
88/// - Extract recipient address, amount, and token information
89/// - Compute the transaction hash that will be signed
90/// - Detect token operations (ERC-20, SPL, TRC-20, etc.)
91/// - Set appropriate [`TxType`](txgate_core::TxType) for policy evaluation
92/// - Return appropriate errors for malformed or unsupported transactions
93///
94/// # Thread Safety
95///
96/// Implementations must be `Send + Sync` to allow concurrent parsing
97/// in the server's async runtime. This is typically achieved by:
98/// - Not storing mutable state in the parser
99/// - Using interior mutability with proper synchronization if state is needed
100///
101/// # Example: Using with Trait Objects
102///
103/// ```
104/// use txgate_chain::Chain;
105/// use txgate_core::{ParsedTx, error::ParseError};
106/// use txgate_crypto::CurveType;
107///
108/// // Create a registry of chain parsers
109/// struct ChainRegistry {
110/// chains: Vec<Box<dyn Chain>>,
111/// }
112///
113/// impl ChainRegistry {
114/// fn find(&self, id: &str) -> Option<&dyn Chain> {
115/// self.chains.iter().find(|c| c.id() == id).map(|c| c.as_ref())
116/// }
117/// }
118/// ```
119pub trait Chain: Send + Sync {
120 /// Returns the chain identifier (e.g., "ethereum", "bitcoin", "solana").
121 ///
122 /// This identifier is used for:
123 /// - Chain lookup in the registry
124 /// - Logging and metrics
125 /// - Configuration matching
126 /// - Correlation with key storage
127 ///
128 /// # Naming Convention
129 ///
130 /// Chain identifiers should be:
131 /// - Lowercase
132 /// - Alphanumeric with hyphens for multi-word names
133 /// - Consistent with industry conventions
134 ///
135 /// Examples: `"ethereum"`, `"bitcoin"`, `"solana"`, `"polygon"`, `"arbitrum-one"`
136 ///
137 /// # Example
138 ///
139 /// ```
140 /// use txgate_chain::Chain;
141 /// # use txgate_core::{ParsedTx, error::ParseError};
142 /// # use txgate_crypto::CurveType;
143 ///
144 /// struct EthereumParser;
145 ///
146 /// impl Chain for EthereumParser {
147 /// fn id(&self) -> &'static str {
148 /// "ethereum"
149 /// }
150 /// # fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
151 /// # Err(ParseError::UnknownTxType)
152 /// # }
153 /// # fn curve(&self) -> CurveType {
154 /// # CurveType::Secp256k1
155 /// # }
156 /// }
157 ///
158 /// let parser = EthereumParser;
159 /// assert_eq!(parser.id(), "ethereum");
160 /// ```
161 fn id(&self) -> &'static str;
162
163 /// Parse raw transaction bytes into a [`ParsedTx`].
164 ///
165 /// This is the core method of the trait. It transforms chain-specific
166 /// transaction bytes into a normalized format for policy evaluation.
167 ///
168 /// # Arguments
169 ///
170 /// * `raw` - The raw transaction bytes in the chain's native format
171 ///
172 /// # Returns
173 ///
174 /// * `Ok(ParsedTx)` - Successfully parsed transaction with all fields populated
175 /// * `Err(ParseError)` - Parsing failed
176 ///
177 /// # Errors
178 ///
179 /// This method returns a [`ParseError`] in the following cases:
180 /// - [`ParseError::UnknownTxType`] - Transaction type byte is not recognized
181 /// - [`ParseError::InvalidRlp`] - RLP decoding failed (for Ethereum)
182 /// - [`ParseError::MalformedTransaction`] - Transaction structure is invalid
183 /// - [`ParseError::MalformedCalldata`] - Contract call data is invalid
184 /// - [`ParseError::InvalidAddress`] - Address format is invalid
185 ///
186 /// # Transaction Hash
187 ///
188 /// The returned `ParsedTx.hash` must be the exact hash that will be signed.
189 /// This is critical for security:
190 /// - For Ethereum legacy: Keccak256 of RLP-encoded transaction without signature
191 /// - For Ethereum EIP-155: Includes `chain_id` in the signing hash
192 /// - For Ethereum typed: Domain-separated hash with type prefix
193 /// - For Bitcoin: Double SHA256 of serialized transaction
194 /// - For Solana: SHA256 of serialized message
195 ///
196 /// # Example
197 ///
198 /// ```
199 /// use txgate_chain::Chain;
200 /// use txgate_core::{ParsedTx, TxType, error::ParseError};
201 /// # use txgate_crypto::CurveType;
202 ///
203 /// struct SimpleParser;
204 ///
205 /// impl Chain for SimpleParser {
206 /// fn id(&self) -> &'static str { "test" }
207 ///
208 /// fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
209 /// if raw.is_empty() {
210 /// return Err(ParseError::MalformedTransaction {
211 /// context: "empty transaction data".to_string(),
212 /// });
213 /// }
214 /// // Parse the transaction...
215 /// Ok(ParsedTx::default())
216 /// }
217 /// # fn curve(&self) -> CurveType { CurveType::Secp256k1 }
218 /// }
219 /// ```
220 fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError>;
221
222 /// Returns the elliptic curve used by this chain.
223 ///
224 /// This is used to select the appropriate signer for the chain.
225 /// Most EVM-compatible chains use secp256k1, while Solana uses Ed25519.
226 ///
227 /// # Curve Selection
228 ///
229 /// - [`CurveType::Secp256k1`] - Ethereum, Bitcoin, Tron, Ripple, most EVM chains
230 /// - [`CurveType::Ed25519`] - Solana, NEAR, Cosmos (some chains)
231 ///
232 /// # Example
233 ///
234 /// ```
235 /// use txgate_chain::Chain;
236 /// use txgate_crypto::CurveType;
237 /// # use txgate_core::{ParsedTx, error::ParseError};
238 ///
239 /// struct SolanaParser;
240 ///
241 /// impl Chain for SolanaParser {
242 /// fn id(&self) -> &'static str { "solana" }
243 /// # fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
244 /// # Err(ParseError::UnknownTxType)
245 /// # }
246 /// fn curve(&self) -> CurveType {
247 /// CurveType::Ed25519
248 /// }
249 /// }
250 ///
251 /// let parser = SolanaParser;
252 /// assert_eq!(parser.curve(), CurveType::Ed25519);
253 /// ```
254 fn curve(&self) -> CurveType;
255
256 /// Check if this parser supports a specific transaction version/type.
257 ///
258 /// This method allows parsers to declare which transaction versions they
259 /// can handle. The registry can use this to select the appropriate parser
260 /// for a given transaction.
261 ///
262 /// # Arguments
263 ///
264 /// * `version` - Chain-specific version identifier
265 ///
266 /// # Returns
267 ///
268 /// * `true` if this parser can handle the version
269 /// * `false` otherwise
270 ///
271 /// # Default Implementation
272 ///
273 /// The default implementation returns `true` for all versions, which is
274 /// suitable for parsers that handle all known transaction types.
275 ///
276 /// # Version Semantics
277 ///
278 /// The meaning of `version` is chain-specific:
279 /// - Ethereum: Transaction type (0=legacy, 1=EIP-2930, 2=EIP-1559, 3=EIP-4844)
280 /// - Bitcoin: Transaction version field
281 /// - Solana: Message version (0=legacy, 128+=versioned)
282 ///
283 /// # Example
284 ///
285 /// ```
286 /// use txgate_chain::Chain;
287 /// # use txgate_core::{ParsedTx, error::ParseError};
288 /// # use txgate_crypto::CurveType;
289 ///
290 /// struct LegacyEthereumParser;
291 ///
292 /// impl Chain for LegacyEthereumParser {
293 /// fn id(&self) -> &'static str { "ethereum-legacy" }
294 /// # fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
295 /// # Err(ParseError::UnknownTxType)
296 /// # }
297 /// # fn curve(&self) -> CurveType { CurveType::Secp256k1 }
298 ///
299 /// fn supports_version(&self, version: u8) -> bool {
300 /// // Only support legacy transactions (type 0)
301 /// version == 0
302 /// }
303 /// }
304 ///
305 /// let parser = LegacyEthereumParser;
306 /// assert!(parser.supports_version(0)); // Legacy
307 /// assert!(!parser.supports_version(2)); // EIP-1559 not supported
308 /// ```
309 fn supports_version(&self, version: u8) -> bool {
310 // Default: support all versions
311 let _ = version;
312 true
313 }
314
315 /// Assemble a signed transaction from raw bytes and a signature.
316 ///
317 /// Takes the original raw transaction bytes and the 65-byte signature
318 /// (`r[32] || s[32] || recovery_id[1]`) and returns the fully encoded
319 /// signed transaction ready for network broadcast.
320 ///
321 /// # Default Implementation
322 ///
323 /// Returns [`ParseError::AssemblyFailed`] — not all chains support assembly.
324 /// Chains that can assemble signed transactions should override this method.
325 ///
326 /// # Arguments
327 ///
328 /// * `raw` - The raw transaction bytes (as passed to `parse()`)
329 /// * `signature` - 65-byte signature: `r(32) || s(32) || recovery_id(1)`
330 ///
331 /// # Returns
332 ///
333 /// * `Ok(Vec<u8>)` - The assembled signed transaction bytes
334 /// * `Err(ParseError::AssemblyFailed)` - Assembly is not supported or failed
335 ///
336 /// # Errors
337 ///
338 /// Returns [`ParseError::AssemblyFailed`] if the chain does not support
339 /// transaction assembly, or if the assembly process fails.
340 fn assemble_signed(&self, raw: &[u8], signature: &[u8]) -> Result<Vec<u8>, ParseError> {
341 let _ = (raw, signature);
342 Err(ParseError::assembly_failed(format!(
343 "transaction assembly not supported for chain: {}",
344 self.id()
345 )))
346 }
347}
348
349// ============================================================================
350// Mock Implementation for Testing
351// ============================================================================
352
353/// Error type for `MockChain` configuration.
354///
355/// This enum mirrors the common `ParseError` variants but is `Clone`-able
356/// for use in mock configurations.
357#[cfg(any(test, feature = "mock"))]
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum MockParseError {
360 /// Unknown transaction type.
361 UnknownTxType,
362 /// Malformed transaction.
363 MalformedTransaction,
364 /// Malformed calldata.
365 MalformedCalldata,
366 /// Invalid address.
367 InvalidAddress,
368}
369
370#[cfg(any(test, feature = "mock"))]
371impl MockParseError {
372 /// Convert to a real `ParseError`.
373 #[must_use]
374 pub fn to_parse_error(self, context: &str) -> ParseError {
375 match self {
376 Self::UnknownTxType => ParseError::UnknownTxType,
377 Self::MalformedTransaction => ParseError::MalformedTransaction {
378 context: context.to_string(),
379 },
380 Self::MalformedCalldata => ParseError::MalformedCalldata,
381 Self::InvalidAddress => ParseError::InvalidAddress {
382 address: context.to_string(),
383 },
384 }
385 }
386}
387
388/// A mock chain implementation for testing.
389///
390/// This struct allows tests to configure the behavior of a chain parser
391/// without implementing actual parsing logic.
392///
393/// # Example
394///
395/// ```
396/// use txgate_chain::{Chain, MockChain, MockParseError};
397/// use txgate_core::{ParsedTx, TxType, error::ParseError};
398/// use txgate_crypto::CurveType;
399///
400/// // Create a mock that returns a successful parse result
401/// let mock = MockChain {
402/// id: "test-chain",
403/// curve: CurveType::Secp256k1,
404/// parse_result: Some(ParsedTx {
405/// chain: "test-chain".to_string(),
406/// tx_type: TxType::Transfer,
407/// ..Default::default()
408/// }),
409/// parse_error: None,
410/// };
411///
412/// let result = mock.parse(&[0x01, 0x02, 0x03]);
413/// assert!(result.is_ok());
414///
415/// // Create a mock that returns an error
416/// let error_mock = MockChain {
417/// id: "failing-chain",
418/// curve: CurveType::Ed25519,
419/// parse_result: None,
420/// parse_error: Some(MockParseError::UnknownTxType),
421/// };
422///
423/// let result = error_mock.parse(&[0x01]);
424/// assert!(matches!(result, Err(ParseError::UnknownTxType)));
425/// ```
426#[cfg(any(test, feature = "mock"))]
427#[derive(Debug, Clone)]
428pub struct MockChain {
429 /// The chain identifier to return from `id()`.
430 pub id: &'static str,
431
432 /// The curve type to return from `curve()`.
433 pub curve: CurveType,
434
435 /// The parse result to return (if `parse_error` is `None`).
436 pub parse_result: Option<ParsedTx>,
437
438 /// The parse error to return (takes precedence over `parse_result`).
439 pub parse_error: Option<MockParseError>,
440}
441
442#[cfg(any(test, feature = "mock"))]
443impl Chain for MockChain {
444 fn id(&self) -> &'static str {
445 self.id
446 }
447
448 fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
449 if let Some(error) = self.parse_error {
450 return Err(error.to_parse_error("mock error"));
451 }
452 self.parse_result
453 .clone()
454 .ok_or_else(|| ParseError::MalformedTransaction {
455 context: "mock not configured with parse_result".to_string(),
456 })
457 }
458
459 fn curve(&self) -> CurveType {
460 self.curve
461 }
462}
463
464#[cfg(any(test, feature = "mock"))]
465impl Default for MockChain {
466 fn default() -> Self {
467 Self {
468 id: "mock",
469 curve: CurveType::Secp256k1,
470 parse_result: Some(ParsedTx::default()),
471 parse_error: None,
472 }
473 }
474}
475
476// ============================================================================
477// Tests
478// ============================================================================
479
480#[cfg(test)]
481mod tests {
482 #![allow(
483 clippy::expect_used,
484 clippy::unwrap_used,
485 clippy::panic,
486 clippy::indexing_slicing,
487 clippy::similar_names,
488 clippy::redundant_clone,
489 clippy::manual_string_new,
490 clippy::needless_raw_string_hashes,
491 clippy::needless_collect,
492 clippy::unreadable_literal
493 )]
494
495 use super::*;
496 use txgate_core::TxType;
497
498 // ------------------------------------------------------------------------
499 // MockChain Tests
500 // ------------------------------------------------------------------------
501
502 #[test]
503 fn test_mock_chain_id() {
504 let mock = MockChain {
505 id: "test-chain",
506 ..Default::default()
507 };
508
509 assert_eq!(mock.id(), "test-chain");
510 }
511
512 #[test]
513 fn test_mock_chain_curve() {
514 let mock_secp = MockChain {
515 curve: CurveType::Secp256k1,
516 ..Default::default()
517 };
518 assert_eq!(mock_secp.curve(), CurveType::Secp256k1);
519
520 let mock_ed = MockChain {
521 curve: CurveType::Ed25519,
522 ..Default::default()
523 };
524 assert_eq!(mock_ed.curve(), CurveType::Ed25519);
525 }
526
527 #[test]
528 fn test_mock_chain_parse_success() {
529 let expected_tx = ParsedTx {
530 hash: [0xab; 32],
531 recipient: Some("0x1234".to_string()),
532 chain: "ethereum".to_string(),
533 tx_type: TxType::Transfer,
534 ..Default::default()
535 };
536
537 let mock = MockChain {
538 id: "ethereum",
539 curve: CurveType::Secp256k1,
540 parse_result: Some(expected_tx.clone()),
541 parse_error: None,
542 };
543
544 let result = mock.parse(&[0x01, 0x02, 0x03]);
545 assert!(result.is_ok());
546
547 let parsed = result.unwrap();
548 assert_eq!(parsed.hash, expected_tx.hash);
549 assert_eq!(parsed.recipient, expected_tx.recipient);
550 assert_eq!(parsed.chain, expected_tx.chain);
551 }
552
553 #[test]
554 fn test_mock_chain_parse_error() {
555 let mock = MockChain {
556 id: "failing",
557 curve: CurveType::Secp256k1,
558 parse_result: None,
559 parse_error: Some(super::MockParseError::UnknownTxType),
560 };
561
562 let result = mock.parse(&[0x01]);
563 assert!(matches!(result, Err(ParseError::UnknownTxType)));
564 }
565
566 #[test]
567 fn test_mock_chain_error_takes_precedence() {
568 // If both parse_error and parse_result are set, error takes precedence
569 let mock = MockChain {
570 id: "test",
571 curve: CurveType::Secp256k1,
572 parse_result: Some(ParsedTx::default()),
573 parse_error: Some(super::MockParseError::MalformedCalldata),
574 };
575
576 let result = mock.parse(&[0x01]);
577 assert!(matches!(result, Err(ParseError::MalformedCalldata)));
578 }
579
580 #[test]
581 fn test_mock_chain_no_result_configured() {
582 let mock = MockChain {
583 id: "unconfigured",
584 curve: CurveType::Secp256k1,
585 parse_result: None,
586 parse_error: None,
587 };
588
589 let result = mock.parse(&[0x01]);
590 assert!(matches!(
591 result,
592 Err(ParseError::MalformedTransaction { .. })
593 ));
594 }
595
596 #[test]
597 fn test_mock_chain_default() {
598 let mock = MockChain::default();
599
600 assert_eq!(mock.id(), "mock");
601 assert_eq!(mock.curve(), CurveType::Secp256k1);
602 assert!(mock.parse(&[]).is_ok());
603 }
604
605 // ------------------------------------------------------------------------
606 // supports_version Tests
607 // ------------------------------------------------------------------------
608
609 #[test]
610 fn test_default_supports_all_versions() {
611 let mock = MockChain::default();
612
613 // Default implementation should support all versions
614 assert!(mock.supports_version(0));
615 assert!(mock.supports_version(1));
616 assert!(mock.supports_version(2));
617 assert!(mock.supports_version(3));
618 assert!(mock.supports_version(255));
619 }
620
621 // ------------------------------------------------------------------------
622 // Trait Object Tests
623 // ------------------------------------------------------------------------
624
625 #[test]
626 fn test_chain_as_trait_object() {
627 let mock = MockChain::default();
628 let chain: Box<dyn Chain> = Box::new(mock);
629
630 assert_eq!(chain.id(), "mock");
631 assert_eq!(chain.curve(), CurveType::Secp256k1);
632 assert!(chain.parse(&[]).is_ok());
633 }
634
635 #[test]
636 fn test_chain_trait_object_vec() {
637 let mock1 = MockChain {
638 id: "chain1",
639 ..Default::default()
640 };
641 let mock2 = MockChain {
642 id: "chain2",
643 curve: CurveType::Ed25519,
644 ..Default::default()
645 };
646
647 let chains: Vec<Box<dyn Chain>> = vec![Box::new(mock1), Box::new(mock2)];
648
649 assert_eq!(chains.len(), 2);
650 assert_eq!(chains[0].id(), "chain1");
651 assert_eq!(chains[1].id(), "chain2");
652 assert_eq!(chains[0].curve(), CurveType::Secp256k1);
653 assert_eq!(chains[1].curve(), CurveType::Ed25519);
654 }
655
656 #[test]
657 fn test_find_chain_by_id() {
658 let chains: Vec<Box<dyn Chain>> = vec![
659 Box::new(MockChain {
660 id: "ethereum",
661 curve: CurveType::Secp256k1,
662 ..Default::default()
663 }),
664 Box::new(MockChain {
665 id: "solana",
666 curve: CurveType::Ed25519,
667 ..Default::default()
668 }),
669 ];
670
671 let found = chains.iter().find(|c| c.id() == "ethereum");
672 assert!(found.is_some());
673 assert_eq!(found.unwrap().curve(), CurveType::Secp256k1);
674
675 let not_found = chains.iter().find(|c| c.id() == "bitcoin");
676 assert!(not_found.is_none());
677 }
678
679 // ------------------------------------------------------------------------
680 // Send + Sync Tests
681 // ------------------------------------------------------------------------
682
683 #[test]
684 fn test_chain_is_send_sync() {
685 fn assert_send_sync<T: Send + Sync>() {}
686 assert_send_sync::<MockChain>();
687 }
688
689 #[test]
690 fn test_chain_trait_object_is_send_sync() {
691 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
692 assert_send_sync::<dyn Chain>();
693 }
694
695 #[test]
696 fn test_boxed_chain_is_send_sync() {
697 fn assert_send_sync<T: Send + Sync>() {}
698 assert_send_sync::<Box<dyn Chain>>();
699 }
700
701 // ------------------------------------------------------------------------
702 // Custom Implementation Test
703 // ------------------------------------------------------------------------
704
705 /// A custom chain implementation for testing custom behavior.
706 struct CustomVersionedChain {
707 supported_versions: Vec<u8>,
708 }
709
710 impl Chain for CustomVersionedChain {
711 fn id(&self) -> &'static str {
712 "custom-versioned"
713 }
714
715 fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
716 if raw.is_empty() {
717 return Err(ParseError::malformed_transaction("empty data"));
718 }
719
720 let version = raw[0];
721 if !self.supports_version(version) {
722 return Err(ParseError::UnknownTxType);
723 }
724
725 Ok(ParsedTx {
726 chain: self.id().to_string(),
727 ..Default::default()
728 })
729 }
730
731 fn curve(&self) -> CurveType {
732 CurveType::Secp256k1
733 }
734
735 fn supports_version(&self, version: u8) -> bool {
736 self.supported_versions.contains(&version)
737 }
738 }
739
740 #[test]
741 fn test_custom_versioned_chain() {
742 let chain = CustomVersionedChain {
743 supported_versions: vec![0, 2],
744 };
745
746 assert!(chain.supports_version(0));
747 assert!(!chain.supports_version(1));
748 assert!(chain.supports_version(2));
749 assert!(!chain.supports_version(3));
750
751 // Version 0 should parse successfully
752 let result = chain.parse(&[0x00, 0x01, 0x02]);
753 assert!(result.is_ok());
754
755 // Version 1 should fail
756 let result = chain.parse(&[0x01, 0x01, 0x02]);
757 assert!(matches!(result, Err(ParseError::UnknownTxType)));
758
759 // Empty data should fail
760 let result = chain.parse(&[]);
761 assert!(matches!(
762 result,
763 Err(ParseError::MalformedTransaction { .. })
764 ));
765 }
766
767 #[test]
768 fn test_default_assemble_signed_returns_error() {
769 let mock = MockChain {
770 id: "test-chain",
771 curve: CurveType::Secp256k1,
772 parse_result: None,
773 parse_error: None,
774 };
775
776 let result = mock.assemble_signed(&[0x01, 0x02], &[0u8; 65]);
777 assert!(result.is_err());
778 let err = result.unwrap_err();
779 assert!(
780 err.to_string().contains("not supported"),
781 "Expected 'not supported' error, got: {err}"
782 );
783 }
784
785 #[test]
786 fn test_ethereum_parser_assemble_signed_via_trait() {
787 use crate::ethereum::EthereumParser;
788
789 let parser = EthereumParser::new();
790 // Invalid raw data should produce an error, not panic
791 let result = parser.assemble_signed(&[0xc0], &[0u8; 65]);
792 // The result should be an error since 0xc0 is an empty RLP list
793 assert!(result.is_err());
794 }
795}