Skip to main content

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}