Skip to main content

txgate_chain/
bitcoin.rs

1//! Bitcoin transaction parser.
2//!
3//! This module provides the [`BitcoinParser`] implementation for parsing
4//! Bitcoin transactions into the common [`ParsedTx`] format.
5//!
6//! # Supported Transaction Types
7//!
8//! - **Legacy**: Pre-`SegWit` transactions (P2PKH, P2SH)
9//! - **`SegWit` v0**: Native `SegWit` transactions (P2WPKH, P2WSH)
10//! - **Taproot**: `SegWit` v1 transactions (P2TR)
11//!
12//! # Example
13//!
14//! ```ignore
15//! use txgate_chain::{Chain, BitcoinParser};
16//!
17//! let parser = BitcoinParser::mainnet();
18//!
19//! // Parse a raw Bitcoin transaction
20//! let raw_tx = hex::decode("0100000001...").unwrap();
21//! let parsed = parser.parse(&raw_tx)?;
22//!
23//! println!("Recipients: {:?}", parsed.recipient);
24//! println!("Amount: {:?}", parsed.amount);
25//! ```
26//!
27//! # Limitations
28//!
29//! - Fee calculation requires UTXO lookup (input amounts are not in the raw transaction)
30//! - Token detection (Ordinals, BRC-20, Runes) is not yet implemented
31
32use bitcoin::consensus::Decodable;
33use bitcoin::{Address, Network, Transaction, TxOut};
34use std::collections::HashMap;
35use std::io::Cursor;
36use txgate_core::error::ParseError;
37use txgate_core::{ParsedTx, TxType, U256};
38use txgate_crypto::CurveType;
39
40use crate::Chain;
41
42/// Bitcoin transaction parser.
43///
44/// Parses raw Bitcoin transactions into the common [`ParsedTx`] format
45/// for policy evaluation.
46///
47/// # Network Configuration
48///
49/// The parser must be configured with the appropriate Bitcoin network
50/// to correctly decode addresses:
51///
52/// - [`BitcoinParser::mainnet()`] - Bitcoin Mainnet
53/// - [`BitcoinParser::testnet()`] - Bitcoin Testnet
54/// - [`BitcoinParser::signet()`] - Bitcoin Signet
55/// - [`BitcoinParser::regtest()`] - Bitcoin Regtest
56///
57/// # Thread Safety
58///
59/// `BitcoinParser` is `Send + Sync` and can be safely shared across threads.
60#[derive(Debug, Clone, Copy)]
61pub struct BitcoinParser {
62    network: Network,
63}
64
65impl BitcoinParser {
66    /// Create a parser for Bitcoin Mainnet.
67    #[must_use]
68    pub const fn mainnet() -> Self {
69        Self {
70            network: Network::Bitcoin,
71        }
72    }
73
74    /// Create a parser for Bitcoin Testnet.
75    #[must_use]
76    pub const fn testnet() -> Self {
77        Self {
78            network: Network::Testnet,
79        }
80    }
81
82    /// Create a parser for Bitcoin Signet.
83    #[must_use]
84    pub const fn signet() -> Self {
85        Self {
86            network: Network::Signet,
87        }
88    }
89
90    /// Create a parser for Bitcoin Regtest.
91    #[must_use]
92    pub const fn regtest() -> Self {
93        Self {
94            network: Network::Regtest,
95        }
96    }
97
98    /// Create a parser with a custom network.
99    #[must_use]
100    pub const fn new(network: Network) -> Self {
101        Self { network }
102    }
103
104    /// Get the configured network.
105    #[must_use]
106    pub const fn network(&self) -> Network {
107        self.network
108    }
109
110    /// Extract address from a transaction output.
111    fn extract_address(self, output: &TxOut) -> Option<String> {
112        Address::from_script(&output.script_pubkey, self.network)
113            .ok()
114            .map(|addr| addr.to_string())
115    }
116
117    /// Determine transaction type based on outputs.
118    fn determine_tx_type(tx: &Transaction) -> TxType {
119        // Check for OP_RETURN outputs (data carrier)
120        let has_op_return = tx.output.iter().any(|out| out.script_pubkey.is_op_return());
121
122        if has_op_return {
123            // Could be Ordinals, BRC-20, Runes, or other protocols
124            return TxType::Other;
125        }
126
127        // Simple transfer if we have standard outputs
128        TxType::Transfer
129    }
130
131    /// Check if transaction uses `SegWit`.
132    fn is_segwit(tx: &Transaction) -> bool {
133        tx.input.iter().any(|input| !input.witness.is_empty())
134    }
135
136    /// Check if transaction uses Taproot.
137    fn is_taproot(tx: &Transaction) -> bool {
138        // Check if any input has a Taproot witness (key path or script path)
139        tx.input.iter().any(|input| {
140            // Taproot key-path spend has exactly one witness element (64-byte Schnorr sig)
141            // Taproot script-path spend has multiple elements
142            if input.witness.is_empty() {
143                return false;
144            }
145
146            // Check if any output is P2TR (witness version 1)
147            tx.output.iter().any(|out| out.script_pubkey.is_p2tr())
148        })
149    }
150}
151
152impl Default for BitcoinParser {
153    fn default() -> Self {
154        Self::mainnet()
155    }
156}
157
158impl Chain for BitcoinParser {
159    fn id(&self) -> &'static str {
160        "bitcoin"
161    }
162
163    fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
164        if raw.is_empty() {
165            return Err(ParseError::MalformedTransaction {
166                context: "empty transaction data".to_string(),
167            });
168        }
169
170        // Decode the transaction
171        let mut cursor = Cursor::new(raw);
172        let tx = Transaction::consensus_decode(&mut cursor).map_err(|e| {
173            ParseError::MalformedTransaction {
174                context: format!("failed to decode Bitcoin transaction: {e}"),
175            }
176        })?;
177
178        // Compute transaction ID (hash)
179        let txid = tx.compute_txid();
180        let mut hash = [0u8; 32];
181        hash.copy_from_slice(txid.as_ref());
182        // Bitcoin uses little-endian display, but we store as-is for signing
183        hash.reverse();
184
185        // Find the primary recipient (first non-OP_RETURN, non-change output)
186        // In practice, determining "change" requires UTXO context
187        let recipient = tx
188            .output
189            .iter()
190            .filter(|out| !out.script_pubkey.is_op_return())
191            .find_map(|out| self.extract_address(out));
192
193        // Calculate total output amount (sum of all outputs except OP_RETURN)
194        let total_output: u64 = tx
195            .output
196            .iter()
197            .filter(|out| !out.script_pubkey.is_op_return())
198            .map(|out| out.value.to_sat())
199            .sum();
200
201        let amount = Some(U256::from(total_output));
202
203        // Determine transaction type
204        let tx_type = Self::determine_tx_type(&tx);
205
206        // Build metadata
207        let mut metadata = HashMap::new();
208
209        // Add version
210        metadata.insert(
211            "version".to_string(),
212            serde_json::Value::Number(tx.version.0.into()),
213        );
214
215        // Add locktime
216        metadata.insert(
217            "locktime".to_string(),
218            serde_json::Value::Number(tx.lock_time.to_consensus_u32().into()),
219        );
220
221        // Add input count
222        metadata.insert(
223            "input_count".to_string(),
224            serde_json::Value::Number(tx.input.len().into()),
225        );
226
227        // Add output count
228        metadata.insert(
229            "output_count".to_string(),
230            serde_json::Value::Number(tx.output.len().into()),
231        );
232
233        // Add SegWit flag
234        metadata.insert(
235            "segwit".to_string(),
236            serde_json::Value::Bool(Self::is_segwit(&tx)),
237        );
238
239        // Add Taproot flag
240        metadata.insert(
241            "taproot".to_string(),
242            serde_json::Value::Bool(Self::is_taproot(&tx)),
243        );
244
245        // Add virtual size (vsize) for fee estimation
246        metadata.insert(
247            "vsize".to_string(),
248            serde_json::Value::Number(tx.vsize().into()),
249        );
250
251        // Add weight
252        metadata.insert(
253            "weight".to_string(),
254            serde_json::Value::Number(tx.weight().to_wu().into()),
255        );
256
257        // Add all output addresses for policy evaluation
258        let output_addresses: Vec<serde_json::Value> = tx
259            .output
260            .iter()
261            .filter_map(|out| self.extract_address(out))
262            .map(serde_json::Value::String)
263            .collect();
264        metadata.insert(
265            "output_addresses".to_string(),
266            serde_json::Value::Array(output_addresses),
267        );
268
269        // Add all output amounts
270        let output_amounts: Vec<serde_json::Value> = tx
271            .output
272            .iter()
273            .map(|out| serde_json::Value::Number(out.value.to_sat().into()))
274            .collect();
275        metadata.insert(
276            "output_amounts".to_string(),
277            serde_json::Value::Array(output_amounts),
278        );
279
280        Ok(ParsedTx {
281            hash,
282            recipient,
283            amount,
284            token: Some("BTC".to_string()),
285            token_address: None, // Native currency
286            tx_type,
287            chain: "bitcoin".to_string(),
288            nonce: None, // Bitcoin doesn't have nonces
289            chain_id: None,
290            metadata,
291        })
292    }
293
294    fn curve(&self) -> CurveType {
295        CurveType::Secp256k1
296    }
297
298    fn supports_version(&self, version: u8) -> bool {
299        // Bitcoin transaction versions 1 and 2 are common
300        // Version 2 enables BIP68 (relative lock-time)
301        matches!(version, 1 | 2)
302    }
303}
304
305// ============================================================================
306// Tests
307// ============================================================================
308
309#[cfg(test)]
310mod tests {
311    #![allow(
312        clippy::expect_used,
313        clippy::unwrap_used,
314        clippy::panic,
315        clippy::indexing_slicing
316    )]
317
318    use super::*;
319
320    // Sample legacy P2PKH transaction (simplified for testing)
321    // This is a minimal valid transaction structure
322    fn create_sample_legacy_tx() -> Vec<u8> {
323        // A simple 1-input, 1-output transaction
324        // Version: 01000000
325        // Input count: 01
326        // Input: 32-byte txid + 4-byte vout + script + sequence
327        // Output count: 01
328        // Output: 8-byte value + script
329        // Locktime: 00000000
330
331        // Using a real testnet transaction hex for testing
332        hex::decode(
333            "0100000001000000000000000000000000000000000000000000000000\
334             0000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a\
335             0100000043410496b538e853519c726a2c91e61ec11600ae1390813a62\
336             7c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166\
337             bf621e73a82cbf2342c858eeac00000000",
338        )
339        .unwrap_or_else(|_| {
340            // Fallback: create a minimal transaction manually
341            let mut tx_bytes = Vec::new();
342
343            // Version (4 bytes, little-endian)
344            tx_bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
345
346            // Input count (varint)
347            tx_bytes.push(0x01);
348
349            // Input: previous txid (32 bytes, all zeros for coinbase)
350            tx_bytes.extend_from_slice(&[0x00; 32]);
351
352            // Input: previous output index (4 bytes)
353            tx_bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
354
355            // Input: script length (varint)
356            tx_bytes.push(0x07);
357
358            // Input: script (7 bytes)
359            tx_bytes.extend_from_slice(&[0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04]);
360
361            // Input: sequence (4 bytes)
362            tx_bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
363
364            // Output count (varint)
365            tx_bytes.push(0x01);
366
367            // Output: value (8 bytes, little-endian) - 50 BTC in satoshis
368            tx_bytes.extend_from_slice(&[0x00, 0xf2, 0x05, 0x2a, 0x01, 0x00, 0x00, 0x00]);
369
370            // Output: script length (varint)
371            tx_bytes.push(0x43);
372
373            // Output: P2PK script (67 bytes)
374            tx_bytes.push(0x41); // Push 65 bytes
375            tx_bytes.extend_from_slice(&[0x04; 65]); // Dummy public key
376            tx_bytes.push(0xac); // OP_CHECKSIG
377
378            // Locktime (4 bytes)
379            tx_bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
380
381            tx_bytes
382        })
383    }
384
385    #[test]
386    fn test_bitcoin_parser_id() {
387        let parser = BitcoinParser::mainnet();
388        assert_eq!(parser.id(), "bitcoin");
389    }
390
391    #[test]
392    fn test_bitcoin_parser_curve() {
393        let parser = BitcoinParser::mainnet();
394        assert_eq!(parser.curve(), CurveType::Secp256k1);
395    }
396
397    #[test]
398    fn test_bitcoin_parser_networks() {
399        assert_eq!(BitcoinParser::mainnet().network(), Network::Bitcoin);
400        assert_eq!(BitcoinParser::testnet().network(), Network::Testnet);
401        assert_eq!(BitcoinParser::signet().network(), Network::Signet);
402        assert_eq!(BitcoinParser::regtest().network(), Network::Regtest);
403    }
404
405    #[test]
406    fn test_bitcoin_parser_default() {
407        let parser = BitcoinParser::default();
408        assert_eq!(parser.network(), Network::Bitcoin);
409    }
410
411    #[test]
412    fn test_bitcoin_parser_empty_input() {
413        let parser = BitcoinParser::mainnet();
414        let result = parser.parse(&[]);
415
416        assert!(result.is_err());
417        assert!(matches!(
418            result,
419            Err(ParseError::MalformedTransaction { .. })
420        ));
421    }
422
423    #[test]
424    fn test_bitcoin_parser_invalid_input() {
425        let parser = BitcoinParser::mainnet();
426        let result = parser.parse(&[0x00, 0x01, 0x02]);
427
428        assert!(result.is_err());
429        assert!(matches!(
430            result,
431            Err(ParseError::MalformedTransaction { .. })
432        ));
433    }
434
435    #[test]
436    fn test_bitcoin_parser_parse_transaction() {
437        let parser = BitcoinParser::mainnet();
438        let tx_bytes = create_sample_legacy_tx();
439
440        let result = parser.parse(&tx_bytes);
441
442        // The transaction should parse (even if it's a coinbase)
443        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
444
445        let parsed = result.unwrap();
446        assert_eq!(parsed.chain, "bitcoin");
447        assert_eq!(parsed.token, Some("BTC".to_string()));
448        assert!(parsed.token_address.is_none());
449        assert_eq!(parsed.tx_type, TxType::Transfer);
450
451        // Check metadata
452        assert!(parsed.metadata.contains_key("version"));
453        assert!(parsed.metadata.contains_key("locktime"));
454        assert!(parsed.metadata.contains_key("input_count"));
455        assert!(parsed.metadata.contains_key("output_count"));
456        assert!(parsed.metadata.contains_key("segwit"));
457        assert!(parsed.metadata.contains_key("vsize"));
458    }
459
460    #[test]
461    fn test_bitcoin_parser_supports_version() {
462        let parser = BitcoinParser::mainnet();
463
464        assert!(parser.supports_version(1));
465        assert!(parser.supports_version(2));
466        assert!(!parser.supports_version(0));
467        assert!(!parser.supports_version(3));
468    }
469
470    #[test]
471    fn test_bitcoin_parser_is_send_sync() {
472        fn assert_send_sync<T: Send + Sync>() {}
473        assert_send_sync::<BitcoinParser>();
474    }
475
476    #[test]
477    fn test_bitcoin_parser_clone() {
478        let parser = BitcoinParser::mainnet();
479        let cloned = parser;
480        assert_eq!(parser.network(), cloned.network());
481    }
482
483    #[test]
484    fn test_bitcoin_parser_debug() {
485        let parser = BitcoinParser::mainnet();
486        let debug_str = format!("{parser:?}");
487        assert!(debug_str.contains("BitcoinParser"));
488        assert!(debug_str.contains("Bitcoin"));
489    }
490
491    // ========================================================================
492    // SegWit Transaction Tests
493    // ========================================================================
494
495    /// Create a SegWit P2WPKH transaction for testing.
496    /// This is a real-format SegWit transaction with witness data.
497    fn create_segwit_tx() -> Vec<u8> {
498        // SegWit transaction format:
499        // [version][marker=0x00][flag=0x01][inputs][outputs][witness][locktime]
500        //
501        // Real SegWit P2WPKH spending transaction (simplified)
502        // From testnet transaction
503
504        let mut tx = Vec::new();
505
506        // Version (4 bytes, little-endian) - version 2 for BIP68
507        tx.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]);
508
509        // Marker (1 byte) - indicates SegWit
510        tx.push(0x00);
511
512        // Flag (1 byte) - must be 0x01 for witness
513        tx.push(0x01);
514
515        // Input count (varint)
516        tx.push(0x01);
517
518        // Input 1:
519        // Previous txid (32 bytes, little-endian)
520        tx.extend_from_slice(&[
521            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
522            0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb,
523            0xcc, 0xdd, 0xee, 0xff,
524        ]);
525
526        // Previous output index (4 bytes)
527        tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
528
529        // ScriptSig length (0 for native SegWit)
530        tx.push(0x00);
531
532        // Sequence (4 bytes)
533        tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
534
535        // Output count (varint)
536        tx.push(0x01);
537
538        // Output 1: P2WPKH output
539        // Value: 100000 satoshis (8 bytes, little-endian)
540        tx.extend_from_slice(&[0xa0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]);
541
542        // Script length: 22 bytes (P2WPKH: OP_0 <20-byte-hash>)
543        tx.push(0x16);
544
545        // P2WPKH script: OP_0 OP_PUSHBYTES_20 <pubkey-hash>
546        tx.push(0x00); // OP_0 (witness version 0)
547        tx.push(0x14); // OP_PUSHBYTES_20
548        tx.extend_from_slice(&[0xab; 20]); // 20-byte pubkey hash (dummy)
549
550        // Witness data for input 1:
551        // Stack items count
552        tx.push(0x02); // 2 items: signature and pubkey
553
554        // Item 1: Signature (71-73 bytes typically, using 71)
555        tx.push(0x47); // 71 bytes
556        tx.extend_from_slice(&[0x30; 71]); // Dummy DER signature
557
558        // Item 2: Public key (33 bytes compressed)
559        tx.push(0x21); // 33 bytes
560        tx.extend_from_slice(&[0x02; 33]); // Dummy compressed pubkey
561
562        // Locktime (4 bytes)
563        tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
564
565        tx
566    }
567
568    #[test]
569    fn test_bitcoin_parser_segwit_transaction() {
570        let parser = BitcoinParser::mainnet();
571        let tx_bytes = create_segwit_tx();
572
573        let result = parser.parse(&tx_bytes);
574        assert!(
575            result.is_ok(),
576            "Failed to parse SegWit tx: {:?}",
577            result.err()
578        );
579
580        let parsed = result.unwrap();
581        assert_eq!(parsed.chain, "bitcoin");
582        assert_eq!(parsed.token, Some("BTC".to_string()));
583
584        // Check SegWit metadata
585        let segwit = parsed.metadata.get("segwit");
586        assert!(segwit.is_some(), "Missing segwit metadata");
587        assert_eq!(segwit.unwrap(), &serde_json::Value::Bool(true));
588
589        // Should not be Taproot
590        let taproot = parsed.metadata.get("taproot");
591        assert!(taproot.is_some(), "Missing taproot metadata");
592        assert_eq!(taproot.unwrap(), &serde_json::Value::Bool(false));
593
594        // Check version is 2 (BIP68)
595        let version = parsed.metadata.get("version");
596        assert!(version.is_some());
597        assert_eq!(version.unwrap(), &serde_json::Value::Number(2.into()));
598    }
599
600    #[test]
601    fn test_is_segwit_helper() {
602        // Parse a SegWit transaction and verify the helper function
603        let tx_bytes = create_segwit_tx();
604        let mut cursor = Cursor::new(&tx_bytes);
605        let tx = Transaction::consensus_decode(&mut cursor).unwrap();
606
607        assert!(
608            BitcoinParser::is_segwit(&tx),
609            "is_segwit should return true for SegWit transaction"
610        );
611        assert!(
612            !BitcoinParser::is_taproot(&tx),
613            "is_taproot should return false for non-Taproot SegWit transaction"
614        );
615    }
616
617    #[test]
618    fn test_is_segwit_false_for_legacy() {
619        // Parse a legacy transaction and verify is_segwit returns false
620        let tx_bytes = create_sample_legacy_tx();
621        let mut cursor = Cursor::new(&tx_bytes);
622        let tx = Transaction::consensus_decode(&mut cursor).unwrap();
623
624        assert!(
625            !BitcoinParser::is_segwit(&tx),
626            "is_segwit should return false for legacy transaction"
627        );
628        assert!(
629            !BitcoinParser::is_taproot(&tx),
630            "is_taproot should return false for legacy transaction"
631        );
632    }
633
634    // ========================================================================
635    // Taproot Transaction Tests
636    // ========================================================================
637
638    /// Create a Taproot (P2TR) transaction for testing.
639    /// Uses witness version 1 with 32-byte program.
640    fn create_taproot_tx() -> Vec<u8> {
641        let mut tx = Vec::new();
642
643        // Version (4 bytes)
644        tx.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]);
645
646        // Marker and flag for SegWit
647        tx.push(0x00);
648        tx.push(0x01);
649
650        // Input count
651        tx.push(0x01);
652
653        // Input: Previous txid
654        tx.extend_from_slice(&[0xab; 32]);
655
656        // Previous output index
657        tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
658
659        // Empty scriptSig for Taproot
660        tx.push(0x00);
661
662        // Sequence
663        tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
664
665        // Output count
666        tx.push(0x01);
667
668        // Output: P2TR (Taproot) output
669        // Value: 50000 satoshis
670        tx.extend_from_slice(&[0x50, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
671
672        // Script length: 34 bytes (P2TR: OP_1 <32-byte-x-only-pubkey>)
673        tx.push(0x22);
674
675        // P2TR script: OP_1 OP_PUSHBYTES_32 <x-only-pubkey>
676        tx.push(0x51); // OP_1 (witness version 1 = Taproot)
677        tx.push(0x20); // OP_PUSHBYTES_32
678        tx.extend_from_slice(&[0xcd; 32]); // 32-byte x-only pubkey (dummy)
679
680        // Witness data for Taproot key-path spend
681        // Stack items count: 1 (just the Schnorr signature)
682        tx.push(0x01);
683
684        // Schnorr signature (64 bytes)
685        tx.push(0x40); // 64 bytes
686        tx.extend_from_slice(&[0x88; 64]);
687
688        // Locktime
689        tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
690
691        tx
692    }
693
694    #[test]
695    fn test_bitcoin_parser_taproot_transaction() {
696        let parser = BitcoinParser::mainnet();
697        let tx_bytes = create_taproot_tx();
698
699        let result = parser.parse(&tx_bytes);
700        assert!(
701            result.is_ok(),
702            "Failed to parse Taproot tx: {:?}",
703            result.err()
704        );
705
706        let parsed = result.unwrap();
707        assert_eq!(parsed.chain, "bitcoin");
708        assert_eq!(parsed.token, Some("BTC".to_string()));
709
710        // Should be both SegWit and Taproot
711        let segwit = parsed.metadata.get("segwit");
712        assert!(segwit.is_some(), "Missing segwit metadata");
713        assert_eq!(
714            segwit.unwrap(),
715            &serde_json::Value::Bool(true),
716            "Taproot transaction should also be SegWit"
717        );
718
719        let taproot = parsed.metadata.get("taproot");
720        assert!(taproot.is_some(), "Missing taproot metadata");
721        assert_eq!(
722            taproot.unwrap(),
723            &serde_json::Value::Bool(true),
724            "Should be identified as Taproot"
725        );
726    }
727
728    #[test]
729    fn test_is_taproot_helper() {
730        // Parse a Taproot transaction and verify the helper
731        let tx_bytes = create_taproot_tx();
732        let mut cursor = Cursor::new(&tx_bytes);
733        let tx = Transaction::consensus_decode(&mut cursor).unwrap();
734
735        assert!(
736            BitcoinParser::is_segwit(&tx),
737            "Taproot transactions are also SegWit"
738        );
739        assert!(
740            BitcoinParser::is_taproot(&tx),
741            "is_taproot should return true for P2TR transaction"
742        );
743    }
744
745    // ========================================================================
746    // Transaction Type Detection Tests
747    // ========================================================================
748
749    /// Create a transaction with OP_RETURN output (data carrier).
750    fn create_op_return_tx() -> Vec<u8> {
751        let mut tx = Vec::new();
752
753        // Version
754        tx.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
755
756        // Input count
757        tx.push(0x01);
758
759        // Input: coinbase-style for simplicity
760        tx.extend_from_slice(&[0x00; 32]); // txid
761        tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]); // vout
762        tx.push(0x07); // script length
763        tx.extend_from_slice(&[0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04]); // coinbase script
764        tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]); // sequence
765
766        // Output count: 2 outputs (one P2PKH, one OP_RETURN)
767        tx.push(0x02);
768
769        // Output 1: Regular P2PKH
770        tx.extend_from_slice(&[0x00, 0xe1, 0xf5, 0x05, 0x00, 0x00, 0x00, 0x00]); // 1 BTC
771        tx.push(0x19); // 25 bytes
772        tx.push(0x76); // OP_DUP
773        tx.push(0xa9); // OP_HASH160
774        tx.push(0x14); // Push 20 bytes
775        tx.extend_from_slice(&[0xbc; 20]); // pubkey hash
776        tx.push(0x88); // OP_EQUALVERIFY
777        tx.push(0xac); // OP_CHECKSIG
778
779        // Output 2: OP_RETURN with data
780        tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // 0 value
781        tx.push(0x0d); // 13 bytes
782        tx.push(0x6a); // OP_RETURN
783        tx.push(0x0b); // Push 11 bytes
784        tx.extend_from_slice(b"hello world"); // data payload
785
786        // Locktime
787        tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
788
789        tx
790    }
791
792    #[test]
793    fn test_bitcoin_parser_op_return_transaction() {
794        let parser = BitcoinParser::mainnet();
795        let tx_bytes = create_op_return_tx();
796
797        let result = parser.parse(&tx_bytes);
798        assert!(
799            result.is_ok(),
800            "Failed to parse OP_RETURN tx: {:?}",
801            result.err()
802        );
803
804        let parsed = result.unwrap();
805
806        // OP_RETURN transactions should be TxType::Other
807        assert_eq!(
808            parsed.tx_type,
809            TxType::Other,
810            "OP_RETURN transaction should be TxType::Other"
811        );
812
813        // The amount should only include non-OP_RETURN outputs
814        // Output 1 has 1 BTC = 100_000_000 satoshis
815        assert!(parsed.amount.is_some());
816        assert_eq!(parsed.amount.unwrap(), U256::from(100_000_000u64));
817
818        // Recipient should be from the non-OP_RETURN output
819        assert!(parsed.recipient.is_some());
820    }
821
822    #[test]
823    fn test_determine_tx_type_helper() {
824        // Test with OP_RETURN
825        let tx_bytes = create_op_return_tx();
826        let mut cursor = Cursor::new(&tx_bytes);
827        let tx = Transaction::consensus_decode(&mut cursor).unwrap();
828        assert_eq!(
829            BitcoinParser::determine_tx_type(&tx),
830            TxType::Other,
831            "OP_RETURN should be TxType::Other"
832        );
833
834        // Test with regular transfer
835        let tx_bytes = create_sample_legacy_tx();
836        let mut cursor = Cursor::new(&tx_bytes);
837        let tx = Transaction::consensus_decode(&mut cursor).unwrap();
838        assert_eq!(
839            BitcoinParser::determine_tx_type(&tx),
840            TxType::Transfer,
841            "Regular transaction should be TxType::Transfer"
842        );
843    }
844
845    // ========================================================================
846    // Address Extraction Tests
847    // ========================================================================
848
849    #[test]
850    fn test_bitcoin_parser_p2wpkh_address_extraction() {
851        let parser = BitcoinParser::mainnet();
852        let tx_bytes = create_segwit_tx();
853
854        let result = parser.parse(&tx_bytes);
855        assert!(result.is_ok());
856
857        let parsed = result.unwrap();
858
859        // Check that output_addresses contains a bech32 address
860        let addresses = parsed.metadata.get("output_addresses");
861        assert!(addresses.is_some());
862
863        if let serde_json::Value::Array(addrs) = addresses.unwrap() {
864            assert!(!addrs.is_empty(), "Should have at least one address");
865            if let serde_json::Value::String(addr) = &addrs[0] {
866                assert!(
867                    addr.starts_with("bc1q"),
868                    "P2WPKH address should start with bc1q, got: {}",
869                    addr
870                );
871            }
872        }
873    }
874
875    #[test]
876    fn test_bitcoin_parser_p2tr_address_extraction() {
877        let parser = BitcoinParser::mainnet();
878        let tx_bytes = create_taproot_tx();
879
880        let result = parser.parse(&tx_bytes);
881        assert!(result.is_ok());
882
883        let parsed = result.unwrap();
884
885        // Check that output_addresses contains a Taproot bech32m address
886        let addresses = parsed.metadata.get("output_addresses");
887        assert!(addresses.is_some());
888
889        if let serde_json::Value::Array(addrs) = addresses.unwrap() {
890            assert!(!addrs.is_empty(), "Should have at least one address");
891            if let serde_json::Value::String(addr) = &addrs[0] {
892                assert!(
893                    addr.starts_with("bc1p"),
894                    "P2TR address should start with bc1p, got: {}",
895                    addr
896                );
897            }
898        }
899    }
900
901    #[test]
902    fn test_bitcoin_parser_testnet_addresses() {
903        let parser = BitcoinParser::testnet();
904        let tx_bytes = create_segwit_tx();
905
906        let result = parser.parse(&tx_bytes);
907        assert!(result.is_ok());
908
909        let parsed = result.unwrap();
910
911        let addresses = parsed.metadata.get("output_addresses");
912        assert!(addresses.is_some());
913
914        if let serde_json::Value::Array(addrs) = addresses.unwrap() {
915            if !addrs.is_empty() {
916                if let serde_json::Value::String(addr) = &addrs[0] {
917                    // Testnet P2WPKH should start with tb1q
918                    assert!(
919                        addr.starts_with("tb1q"),
920                        "Testnet P2WPKH should start with tb1q, got: {}",
921                        addr
922                    );
923                }
924            }
925        }
926    }
927}