1use 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#[derive(Debug, Clone, Copy)]
61pub struct BitcoinParser {
62 network: Network,
63}
64
65impl BitcoinParser {
66 #[must_use]
68 pub const fn mainnet() -> Self {
69 Self {
70 network: Network::Bitcoin,
71 }
72 }
73
74 #[must_use]
76 pub const fn testnet() -> Self {
77 Self {
78 network: Network::Testnet,
79 }
80 }
81
82 #[must_use]
84 pub const fn signet() -> Self {
85 Self {
86 network: Network::Signet,
87 }
88 }
89
90 #[must_use]
92 pub const fn regtest() -> Self {
93 Self {
94 network: Network::Regtest,
95 }
96 }
97
98 #[must_use]
100 pub const fn new(network: Network) -> Self {
101 Self { network }
102 }
103
104 #[must_use]
106 pub const fn network(&self) -> Network {
107 self.network
108 }
109
110 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 fn determine_tx_type(tx: &Transaction) -> TxType {
119 let has_op_return = tx.output.iter().any(|out| out.script_pubkey.is_op_return());
121
122 if has_op_return {
123 return TxType::Other;
125 }
126
127 TxType::Transfer
129 }
130
131 fn is_segwit(tx: &Transaction) -> bool {
133 tx.input.iter().any(|input| !input.witness.is_empty())
134 }
135
136 fn is_taproot(tx: &Transaction) -> bool {
138 tx.input.iter().any(|input| {
140 if input.witness.is_empty() {
143 return false;
144 }
145
146 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 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 let txid = tx.compute_txid();
180 let mut hash = [0u8; 32];
181 hash.copy_from_slice(txid.as_ref());
182 hash.reverse();
184
185 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 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 let tx_type = Self::determine_tx_type(&tx);
205
206 let mut metadata = HashMap::new();
208
209 metadata.insert(
211 "version".to_string(),
212 serde_json::Value::Number(tx.version.0.into()),
213 );
214
215 metadata.insert(
217 "locktime".to_string(),
218 serde_json::Value::Number(tx.lock_time.to_consensus_u32().into()),
219 );
220
221 metadata.insert(
223 "input_count".to_string(),
224 serde_json::Value::Number(tx.input.len().into()),
225 );
226
227 metadata.insert(
229 "output_count".to_string(),
230 serde_json::Value::Number(tx.output.len().into()),
231 );
232
233 metadata.insert(
235 "segwit".to_string(),
236 serde_json::Value::Bool(Self::is_segwit(&tx)),
237 );
238
239 metadata.insert(
241 "taproot".to_string(),
242 serde_json::Value::Bool(Self::is_taproot(&tx)),
243 );
244
245 metadata.insert(
247 "vsize".to_string(),
248 serde_json::Value::Number(tx.vsize().into()),
249 );
250
251 metadata.insert(
253 "weight".to_string(),
254 serde_json::Value::Number(tx.weight().to_wu().into()),
255 );
256
257 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 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, tx_type,
287 chain: "bitcoin".to_string(),
288 nonce: None, 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 matches!(version, 1 | 2)
302 }
303}
304
305#[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 fn create_sample_legacy_tx() -> Vec<u8> {
323 hex::decode(
333 "0100000001000000000000000000000000000000000000000000000000\
334 0000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a\
335 0100000043410496b538e853519c726a2c91e61ec11600ae1390813a62\
336 7c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166\
337 bf621e73a82cbf2342c858eeac00000000",
338 )
339 .unwrap_or_else(|_| {
340 let mut tx_bytes = Vec::new();
342
343 tx_bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
345
346 tx_bytes.push(0x01);
348
349 tx_bytes.extend_from_slice(&[0x00; 32]);
351
352 tx_bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
354
355 tx_bytes.push(0x07);
357
358 tx_bytes.extend_from_slice(&[0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04]);
360
361 tx_bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
363
364 tx_bytes.push(0x01);
366
367 tx_bytes.extend_from_slice(&[0x00, 0xf2, 0x05, 0x2a, 0x01, 0x00, 0x00, 0x00]);
369
370 tx_bytes.push(0x43);
372
373 tx_bytes.push(0x41); tx_bytes.extend_from_slice(&[0x04; 65]); tx_bytes.push(0xac); 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 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 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 fn create_segwit_tx() -> Vec<u8> {
498 let mut tx = Vec::new();
505
506 tx.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]);
508
509 tx.push(0x00);
511
512 tx.push(0x01);
514
515 tx.push(0x01);
517
518 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 tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
528
529 tx.push(0x00);
531
532 tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
534
535 tx.push(0x01);
537
538 tx.extend_from_slice(&[0xa0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]);
541
542 tx.push(0x16);
544
545 tx.push(0x00); tx.push(0x14); tx.extend_from_slice(&[0xab; 20]); tx.push(0x02); tx.push(0x47); tx.extend_from_slice(&[0x30; 71]); tx.push(0x21); tx.extend_from_slice(&[0x02; 33]); 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 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 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 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 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 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 fn create_taproot_tx() -> Vec<u8> {
641 let mut tx = Vec::new();
642
643 tx.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]);
645
646 tx.push(0x00);
648 tx.push(0x01);
649
650 tx.push(0x01);
652
653 tx.extend_from_slice(&[0xab; 32]);
655
656 tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
658
659 tx.push(0x00);
661
662 tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
664
665 tx.push(0x01);
667
668 tx.extend_from_slice(&[0x50, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
671
672 tx.push(0x22);
674
675 tx.push(0x51); tx.push(0x20); tx.extend_from_slice(&[0xcd; 32]); tx.push(0x01);
683
684 tx.push(0x40); tx.extend_from_slice(&[0x88; 64]);
687
688 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 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 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 fn create_op_return_tx() -> Vec<u8> {
751 let mut tx = Vec::new();
752
753 tx.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
755
756 tx.push(0x01);
758
759 tx.extend_from_slice(&[0x00; 32]); tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]); tx.push(0x07); tx.extend_from_slice(&[0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04]); tx.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]); tx.push(0x02);
768
769 tx.extend_from_slice(&[0x00, 0xe1, 0xf5, 0x05, 0x00, 0x00, 0x00, 0x00]); tx.push(0x19); tx.push(0x76); tx.push(0xa9); tx.push(0x14); tx.extend_from_slice(&[0xbc; 20]); tx.push(0x88); tx.push(0xac); tx.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); tx.push(0x0d); tx.push(0x6a); tx.push(0x0b); tx.extend_from_slice(b"hello world"); 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 assert_eq!(
808 parsed.tx_type,
809 TxType::Other,
810 "OP_RETURN transaction should be TxType::Other"
811 );
812
813 assert!(parsed.amount.is_some());
816 assert_eq!(parsed.amount.unwrap(), U256::from(100_000_000u64));
817
818 assert!(parsed.recipient.is_some());
820 }
821
822 #[test]
823 fn test_determine_tx_type_helper() {
824 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 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 #[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 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 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 assert!(
919 addr.starts_with("tb1q"),
920 "Testnet P2WPKH should start with tb1q, got: {}",
921 addr
922 );
923 }
924 }
925 }
926 }
927}