1use crate::crypto;
7use crate::encoding;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct OutPoint {
14 pub txid: [u8; 32],
16 pub vout: u32,
18}
19
20#[derive(Clone, Debug)]
22pub struct TxIn {
23 pub previous_output: OutPoint,
25 pub script_sig: Vec<u8>,
27 pub sequence: u32,
29}
30
31#[derive(Clone, Debug)]
33pub struct TxOut {
34 pub value: u64,
36 pub script_pubkey: Vec<u8>,
38}
39
40#[derive(Clone, Debug)]
42pub struct Transaction {
43 pub version: i32,
45 pub inputs: Vec<TxIn>,
47 pub outputs: Vec<TxOut>,
49 pub witnesses: Vec<Vec<Vec<u8>>>,
51 pub locktime: u32,
53}
54
55impl Transaction {
56 #[must_use]
58 pub fn new(version: i32) -> Self {
59 Self {
60 version,
61 inputs: Vec::new(),
62 outputs: Vec::new(),
63 witnesses: Vec::new(),
64 locktime: 0,
65 }
66 }
67
68 #[must_use]
70 pub fn has_witness(&self) -> bool {
71 self.witnesses.iter().any(|w| !w.is_empty())
72 }
73
74 #[must_use]
76 pub fn serialize_legacy(&self) -> Vec<u8> {
77 let mut buf = Vec::with_capacity(256);
78
79 buf.extend_from_slice(&self.version.to_le_bytes());
81
82 encoding::encode_compact_size(&mut buf, self.inputs.len() as u64);
84 for input in &self.inputs {
85 buf.extend_from_slice(&input.previous_output.txid);
86 buf.extend_from_slice(&input.previous_output.vout.to_le_bytes());
87 encoding::encode_compact_size(&mut buf, input.script_sig.len() as u64);
88 buf.extend_from_slice(&input.script_sig);
89 buf.extend_from_slice(&input.sequence.to_le_bytes());
90 }
91
92 encoding::encode_compact_size(&mut buf, self.outputs.len() as u64);
94 for output in &self.outputs {
95 buf.extend_from_slice(&output.value.to_le_bytes());
96 encoding::encode_compact_size(&mut buf, output.script_pubkey.len() as u64);
97 buf.extend_from_slice(&output.script_pubkey);
98 }
99
100 buf.extend_from_slice(&self.locktime.to_le_bytes());
102
103 buf
104 }
105
106 #[must_use]
110 pub fn serialize_witness(&self) -> Vec<u8> {
111 if !self.has_witness() {
112 return self.serialize_legacy();
113 }
114
115 let mut buf = Vec::with_capacity(512);
116
117 buf.extend_from_slice(&self.version.to_le_bytes());
119
120 buf.push(0x00); buf.push(0x01); encoding::encode_compact_size(&mut buf, self.inputs.len() as u64);
126 for input in &self.inputs {
127 buf.extend_from_slice(&input.previous_output.txid);
128 buf.extend_from_slice(&input.previous_output.vout.to_le_bytes());
129 encoding::encode_compact_size(&mut buf, input.script_sig.len() as u64);
130 buf.extend_from_slice(&input.script_sig);
131 buf.extend_from_slice(&input.sequence.to_le_bytes());
132 }
133
134 encoding::encode_compact_size(&mut buf, self.outputs.len() as u64);
136 for output in &self.outputs {
137 buf.extend_from_slice(&output.value.to_le_bytes());
138 encoding::encode_compact_size(&mut buf, output.script_pubkey.len() as u64);
139 buf.extend_from_slice(&output.script_pubkey);
140 }
141
142 for (i, _input) in self.inputs.iter().enumerate() {
144 let witness_stack = self.witnesses.get(i);
145 match witness_stack {
146 Some(stack) if !stack.is_empty() => {
147 encoding::encode_compact_size(&mut buf, stack.len() as u64);
148 for item in stack {
149 encoding::encode_compact_size(&mut buf, item.len() as u64);
150 buf.extend_from_slice(item);
151 }
152 }
153 _ => {
154 buf.push(0x00); }
156 }
157 }
158
159 buf.extend_from_slice(&self.locktime.to_le_bytes());
161
162 buf
163 }
164
165 #[must_use]
169 pub fn txid(&self) -> [u8; 32] {
170 let mut hash = crypto::double_sha256(&self.serialize_legacy());
171 hash.reverse(); hash
173 }
174
175 #[must_use]
179 pub fn wtxid(&self) -> [u8; 32] {
180 let mut hash = crypto::double_sha256(&self.serialize_witness());
181 hash.reverse();
182 hash
183 }
184
185 #[must_use]
190 pub fn vsize(&self) -> usize {
191 let base_size = self.serialize_legacy().len();
192 let total_size = self.serialize_witness().len();
193 let weight = base_size * 3 + total_size;
194 weight.div_ceil(4)
195 }
196}
197
198pub fn parse_unsigned_tx(data: &[u8]) -> Result<Transaction, crate::error::SignerError> {
203 use crate::error::SignerError;
204
205 const MIN_INPUT_BYTES: usize = 41;
209 const MIN_OUTPUT_BYTES: usize = 9;
210
211 fn safe_usize(val: u64) -> Result<usize, SignerError> {
213 usize::try_from(val).map_err(|_| {
214 SignerError::ParseError(format!("compact size {val} exceeds platform usize"))
215 })
216 }
217
218 let mut off;
219
220 if data.len() < 4 {
222 return Err(SignerError::ParseError("tx too short for version".into()));
223 }
224 let version = i32::from_le_bytes([data[0], data[1], data[2], data[3]]);
225 off = 4;
226
227 let input_count = safe_usize(encoding::read_compact_size(data, &mut off)?)?;
229
230 let remaining_after_input_count = data.len().saturating_sub(off);
232 let max_possible_inputs = remaining_after_input_count / MIN_INPUT_BYTES;
233 if input_count > max_possible_inputs {
234 return Err(SignerError::ParseError(format!(
235 "tx: input count {input_count} exceeds possible maximum {max_possible_inputs}"
236 )));
237 }
238
239 let mut inputs = Vec::with_capacity(input_count);
240 for _ in 0..input_count {
241 let outpoint_end = off
242 .checked_add(36)
243 .ok_or_else(|| SignerError::ParseError("tx: input outpoint offset overflow".into()))?;
244 if outpoint_end > data.len() {
245 return Err(SignerError::ParseError(
246 "tx truncated in input outpoint".into(),
247 ));
248 }
249 let txid_end = off
250 .checked_add(32)
251 .ok_or_else(|| SignerError::ParseError("tx: txid offset overflow".into()))?;
252 let mut txid = [0u8; 32];
253 txid.copy_from_slice(&data[off..txid_end]);
254 let vout = u32::from_le_bytes(
255 data[txid_end..outpoint_end]
256 .try_into()
257 .map_err(|_| SignerError::ParseError("tx truncated in input vout".into()))?,
258 );
259 off = outpoint_end;
260
261 let script_len = safe_usize(encoding::read_compact_size(data, &mut off)?)?;
262 let script_end = off
263 .checked_add(script_len)
264 .ok_or_else(|| SignerError::ParseError("tx: scriptSig length overflow".into()))?;
265 if script_end > data.len() {
266 return Err(SignerError::ParseError("tx truncated in scriptSig".into()));
267 }
268 let script_sig = data[off..script_end].to_vec();
269 off = script_end;
270
271 let sequence_end = off
272 .checked_add(4)
273 .ok_or_else(|| SignerError::ParseError("tx: sequence offset overflow".into()))?;
274 if sequence_end > data.len() {
275 return Err(SignerError::ParseError("tx truncated in sequence".into()));
276 }
277 let sequence = u32::from_le_bytes(
278 data[off..sequence_end]
279 .try_into()
280 .map_err(|_| SignerError::ParseError("tx truncated in sequence".into()))?,
281 );
282 off = sequence_end;
283
284 inputs.push(TxIn {
285 previous_output: OutPoint { txid, vout },
286 script_sig,
287 sequence,
288 });
289 }
290
291 let output_count = safe_usize(encoding::read_compact_size(data, &mut off)?)?;
293
294 let remaining_after_output_count = data
295 .len()
296 .checked_sub(off)
297 .ok_or_else(|| SignerError::ParseError("tx: output count offset overflow".into()))?;
298 if remaining_after_output_count < 4 {
299 return Err(SignerError::ParseError(
300 "tx truncated before locktime".into(),
301 ));
302 }
303 let max_possible_outputs = (remaining_after_output_count - 4) / MIN_OUTPUT_BYTES;
304 if output_count > max_possible_outputs {
305 return Err(SignerError::ParseError(format!(
306 "tx: output count {output_count} exceeds possible maximum {max_possible_outputs}"
307 )));
308 }
309
310 let mut outputs = Vec::with_capacity(output_count);
311 for _ in 0..output_count {
312 let value_end = off
313 .checked_add(8)
314 .ok_or_else(|| SignerError::ParseError("tx: output value offset overflow".into()))?;
315 if value_end > data.len() {
316 return Err(SignerError::ParseError(
317 "tx truncated in output value".into(),
318 ));
319 }
320 let mut val_bytes = [0u8; 8];
321 val_bytes.copy_from_slice(&data[off..value_end]);
322 let value = u64::from_le_bytes(val_bytes);
323 off = value_end;
324
325 let spk_len = safe_usize(encoding::read_compact_size(data, &mut off)?)?;
326 let spk_end = off
327 .checked_add(spk_len)
328 .ok_or_else(|| SignerError::ParseError("tx: scriptPubKey length overflow".into()))?;
329 if spk_end > data.len() {
330 return Err(SignerError::ParseError(
331 "tx truncated in scriptPubKey".into(),
332 ));
333 }
334 let script_pubkey = data[off..spk_end].to_vec();
335 off = spk_end;
336
337 outputs.push(TxOut {
338 value,
339 script_pubkey,
340 });
341 }
342
343 let locktime_end = off
345 .checked_add(4)
346 .ok_or_else(|| SignerError::ParseError("tx: locktime offset overflow".into()))?;
347 if locktime_end > data.len() {
348 return Err(SignerError::ParseError("tx truncated in locktime".into()));
349 }
350 let locktime = u32::from_le_bytes(
351 data[off..locktime_end]
352 .try_into()
353 .map_err(|_| SignerError::ParseError("tx truncated in locktime".into()))?,
354 );
355 off = locktime_end;
356
357 if off != data.len() {
359 return Err(SignerError::ParseError(format!(
360 "tx has {} trailing bytes after locktime",
361 data.len() - off
362 )));
363 }
364
365 Ok(Transaction {
366 version,
367 inputs,
368 outputs,
369 witnesses: Vec::new(),
370 locktime,
371 })
372}
373
374pub const MIN_RELAY_FEE_SAT_PER_VB: u64 = 1;
380
381pub const DUST_LIMIT_P2WPKH: u64 = 546;
383
384pub const DUST_LIMIT_P2PKH: u64 = 546;
386
387pub const DUST_LIMIT_P2TR: u64 = 330;
389
390pub fn estimate_fee(tx: &Transaction, fee_rate_sat_per_vb: u64) -> u64 {
398 let vsize = tx.vsize() as u64;
399 vsize
400 .saturating_mul(fee_rate_sat_per_vb)
401 .max(MIN_RELAY_FEE_SAT_PER_VB)
402}
403
404pub fn estimate_vsize(
412 num_p2wpkh_inputs: usize,
413 num_p2tr_inputs: usize,
414 num_p2pkh_inputs: usize,
415 num_outputs: usize,
416) -> usize {
417 let overhead = 10 + 2; let p2wpkh_weight = num_p2wpkh_inputs * 271;
423 let p2tr_weight = num_p2tr_inputs * 230;
425 let p2pkh_weight = num_p2pkh_inputs * 592;
427
428 let output_weight = num_outputs * 34 * 4;
430
431 let total_weight = overhead * 4 + p2wpkh_weight + p2tr_weight + p2pkh_weight + output_weight;
432 total_weight.div_ceil(4)
433}
434
435#[derive(Clone, Debug)]
441pub struct Recipient {
442 pub script_pubkey: Vec<u8>,
444 pub amount: u64,
446}
447
448pub fn build_batch_transaction(
462 utxos: &[(OutPoint, u64)],
463 recipients: &[Recipient],
464 change_script_pubkey: &[u8],
465 fee_rate_sat_per_vb: u64,
466) -> Result<Transaction, crate::error::SignerError> {
467 use crate::error::SignerError;
468
469 if utxos.is_empty() {
470 return Err(SignerError::ParseError("no UTXOs provided".into()));
471 }
472 if recipients.is_empty() {
473 return Err(SignerError::ParseError("no recipients provided".into()));
474 }
475
476 let total_input = utxos.iter().try_fold(0u64, |acc, (_, v)| {
477 acc.checked_add(*v)
478 .ok_or_else(|| SignerError::ParseError("total input amount overflowed u64".into()))
479 })?;
480 let total_output = recipients.iter().try_fold(0u64, |acc, r| {
481 acc.checked_add(r.amount)
482 .ok_or_else(|| SignerError::ParseError("total output amount overflowed u64".into()))
483 })?;
484
485 if total_input < total_output {
486 return Err(SignerError::ParseError(format!(
487 "insufficient funds: {} < {}",
488 total_input, total_output
489 )));
490 }
491
492 let num_outputs_with_change = recipients
494 .len()
495 .checked_add(1)
496 .ok_or_else(|| SignerError::ParseError("recipient count overflow".into()))?;
497 let estimated_vsize = estimate_vsize(utxos.len(), 0, 0, num_outputs_with_change);
498 let estimated_fee = (estimated_vsize as u64).saturating_mul(fee_rate_sat_per_vb);
499
500 let change_amount = total_input
501 .checked_sub(total_output)
502 .and_then(|r| r.checked_sub(estimated_fee))
503 .unwrap_or(0);
504
505 let mut tx = Transaction::new(2);
506 tx.locktime = 0;
507
508 for (outpoint, _) in utxos {
510 tx.inputs.push(TxIn {
511 previous_output: outpoint.clone(),
512 script_sig: vec![],
513 sequence: 0xFFFFFFFD, });
515 }
516
517 for recipient in recipients {
519 tx.outputs.push(TxOut {
520 value: recipient.amount,
521 script_pubkey: recipient.script_pubkey.clone(),
522 });
523 }
524
525 if change_amount >= DUST_LIMIT_P2WPKH {
527 tx.outputs.push(TxOut {
528 value: change_amount,
529 script_pubkey: change_script_pubkey.to_vec(),
530 });
531 }
532
533 let actual_output_total = tx.outputs.iter().try_fold(0u64, |acc, o| {
535 acc.checked_add(o.value)
536 .ok_or_else(|| SignerError::ParseError("actual output total overflowed u64".into()))
537 })?;
538 if total_input < actual_output_total {
539 return Err(SignerError::ParseError(format!(
540 "insufficient after fee: {} < {}",
541 total_input, actual_output_total
542 )));
543 }
544
545 Ok(tx)
546}
547
548#[cfg(test)]
551#[allow(clippy::unwrap_used, clippy::expect_used)]
552mod tests {
553 use super::*;
554
555 fn sample_tx() -> Transaction {
556 let mut tx = Transaction::new(2);
557 tx.inputs.push(TxIn {
558 previous_output: OutPoint {
559 txid: [0xAA; 32],
560 vout: 0,
561 },
562 script_sig: vec![],
563 sequence: 0xFFFFFFFF,
564 });
565 tx.outputs.push(TxOut {
566 value: 50_000,
567 script_pubkey: vec![
568 0x00, 0x14, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
569 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
570 ], });
572 tx
573 }
574
575 #[test]
576 fn test_legacy_serialization_structure() {
577 let tx = sample_tx();
578 let raw = tx.serialize_legacy();
579 assert_eq!(raw.len(), 82);
583 assert_eq!(&raw[..4], &2i32.to_le_bytes());
585 }
586
587 #[test]
588 fn test_witness_serialization_no_witness() {
589 let tx = sample_tx();
590 assert_eq!(tx.serialize_legacy(), tx.serialize_witness());
592 assert!(!tx.has_witness());
593 }
594
595 #[test]
596 fn test_witness_serialization_with_witness() {
597 let mut tx = sample_tx();
598 tx.witnesses.push(vec![
599 vec![0x30; 72], vec![0x02; 33], ]);
602 assert!(tx.has_witness());
603 let witness_raw = tx.serialize_witness();
604 let legacy_raw = tx.serialize_legacy();
605 assert!(witness_raw.len() > legacy_raw.len());
607 assert_eq!(witness_raw[4], 0x00); assert_eq!(witness_raw[5], 0x01); }
611
612 #[test]
613 fn test_txid_is_deterministic() {
614 let tx = sample_tx();
615 assert_eq!(tx.txid(), tx.txid());
616 }
617
618 #[test]
619 fn test_txid_ne_wtxid_with_witness() {
620 let mut tx = sample_tx();
621 tx.witnesses.push(vec![vec![0x01; 64]]);
622 assert_ne!(tx.txid(), tx.wtxid());
624 }
625
626 #[test]
627 fn test_txid_eq_wtxid_without_witness() {
628 let tx = sample_tx();
629 assert_eq!(tx.txid(), tx.wtxid());
630 }
631
632 #[test]
633 fn test_vsize_legacy() {
634 let tx = sample_tx();
635 let base = tx.serialize_legacy().len();
636 assert_eq!(tx.vsize(), base);
638 }
639
640 #[test]
641 fn test_vsize_segwit_is_discounted() {
642 let mut tx = sample_tx();
643 tx.witnesses.push(vec![vec![0x30; 72], vec![0x02; 33]]);
644 let base = tx.serialize_legacy().len();
645 let total = tx.serialize_witness().len();
646 let vsize = tx.vsize();
647 assert!(vsize < total);
649 assert!(vsize >= base);
650 }
651
652 #[test]
653 fn test_outpoint_equality() {
654 let o1 = OutPoint {
655 txid: [0x01; 32],
656 vout: 0,
657 };
658 let o2 = OutPoint {
659 txid: [0x01; 32],
660 vout: 0,
661 };
662 let o3 = OutPoint {
663 txid: [0x02; 32],
664 vout: 0,
665 };
666 assert_eq!(o1, o2);
667 assert_ne!(o1, o3);
668 }
669
670 #[test]
671 fn test_empty_transaction() {
672 let tx = Transaction::new(1);
673 let raw = tx.serialize_legacy();
674 assert_eq!(raw.len(), 10);
676 }
677
678 #[test]
679 fn test_multiple_inputs_outputs() {
680 let mut tx = Transaction::new(2);
681 for i in 0..3 {
682 tx.inputs.push(TxIn {
683 previous_output: OutPoint {
684 txid: [i as u8; 32],
685 vout: 0,
686 },
687 script_sig: vec![],
688 sequence: 0xFFFFFFFF,
689 });
690 }
691 for _ in 0..2 {
692 tx.outputs.push(TxOut {
693 value: 10_000,
694 script_pubkey: vec![0x76, 0xa9, 0x14],
695 });
696 }
697 let raw = tx.serialize_legacy();
698 assert!(raw.len() > 10);
699 assert_eq!(raw[4], 3); }
702
703 #[test]
706 fn test_estimate_fee_basic() {
707 let tx = sample_tx();
708 let fee = estimate_fee(&tx, 10);
709 assert!(fee > 0);
710 assert_eq!(fee, tx.vsize() as u64 * 10);
711 }
712
713 #[test]
714 fn test_estimate_fee_minimum() {
715 let tx = Transaction::new(1);
716 let fee = estimate_fee(&tx, 0);
717 assert!(fee >= MIN_RELAY_FEE_SAT_PER_VB);
718 }
719
720 #[test]
721 fn test_estimate_vsize_basic() {
722 let vsize = estimate_vsize(1, 0, 0, 2);
724 assert!(vsize > 0);
725 assert!(vsize > 100 && vsize < 250);
727 }
728
729 #[test]
730 fn test_estimate_vsize_taproot() {
731 let vsize = estimate_vsize(0, 1, 0, 1);
732 assert!(vsize > 0);
733 assert!(vsize > 50 && vsize < 200);
735 }
736
737 #[test]
738 fn test_dust_limits() {
739 assert_eq!(DUST_LIMIT_P2WPKH, 546);
740 assert_eq!(DUST_LIMIT_P2PKH, 546);
741 assert_eq!(DUST_LIMIT_P2TR, 330);
742 }
743
744 #[test]
747 fn test_batch_build_basic() {
748 let utxos = vec![(
749 OutPoint {
750 txid: [0x01; 32],
751 vout: 0,
752 },
753 100_000,
754 )];
755 let recipients = vec![Recipient {
756 script_pubkey: vec![0x00; 22],
757 amount: 50_000,
758 }];
759 let change_spk = vec![0x00; 22];
760 let tx = build_batch_transaction(&utxos, &recipients, &change_spk, 5).unwrap();
761 assert_eq!(tx.inputs.len(), 1);
762 assert!(!tx.outputs.is_empty()); }
764
765 #[test]
766 fn test_batch_build_with_change() {
767 let utxos = vec![(
768 OutPoint {
769 txid: [0x01; 32],
770 vout: 0,
771 },
772 1_000_000,
773 )];
774 let recipients = vec![Recipient {
775 script_pubkey: vec![0x00; 22],
776 amount: 100_000,
777 }];
778 let change_spk = vec![0x00; 22];
779 let tx = build_batch_transaction(&utxos, &recipients, &change_spk, 5).unwrap();
780 assert_eq!(tx.outputs.len(), 2);
782 let change = &tx.outputs[1];
783 assert!(change.value >= DUST_LIMIT_P2WPKH);
784 }
785
786 #[test]
787 fn test_batch_build_multi_recipient() {
788 let utxos = vec![
789 (
790 OutPoint {
791 txid: [0x01; 32],
792 vout: 0,
793 },
794 500_000,
795 ),
796 (
797 OutPoint {
798 txid: [0x02; 32],
799 vout: 1,
800 },
801 500_000,
802 ),
803 ];
804 let recipients = vec![
805 Recipient {
806 script_pubkey: vec![0x00; 22],
807 amount: 100_000,
808 },
809 Recipient {
810 script_pubkey: vec![0x01; 22],
811 amount: 200_000,
812 },
813 Recipient {
814 script_pubkey: vec![0x02; 22],
815 amount: 150_000,
816 },
817 ];
818 let change_spk = vec![0x00; 22];
819 let tx = build_batch_transaction(&utxos, &recipients, &change_spk, 10).unwrap();
820 assert_eq!(tx.inputs.len(), 2);
821 assert!(tx.outputs.len() >= 3); }
823
824 #[test]
825 fn test_batch_build_insufficient_funds() {
826 let utxos = vec![(
827 OutPoint {
828 txid: [0x01; 32],
829 vout: 0,
830 },
831 1_000,
832 )];
833 let recipients = vec![Recipient {
834 script_pubkey: vec![0x00; 22],
835 amount: 100_000,
836 }];
837 assert!(build_batch_transaction(&utxos, &recipients, &[], 5).is_err());
838 }
839
840 #[test]
841 fn test_batch_build_empty_utxos() {
842 let recipients = vec![Recipient {
843 script_pubkey: vec![],
844 amount: 100,
845 }];
846 assert!(build_batch_transaction(&[], &recipients, &[], 5).is_err());
847 }
848
849 #[test]
850 fn test_batch_build_empty_recipients() {
851 let utxos = vec![(
852 OutPoint {
853 txid: [0x01; 32],
854 vout: 0,
855 },
856 100_000,
857 )];
858 assert!(build_batch_transaction(&utxos, &[], &[], 5).is_err());
859 }
860
861 #[test]
862 fn test_batch_build_rbf_enabled() {
863 let utxos = vec![(
864 OutPoint {
865 txid: [0x01; 32],
866 vout: 0,
867 },
868 100_000,
869 )];
870 let recipients = vec![Recipient {
871 script_pubkey: vec![0x00; 22],
872 amount: 50_000,
873 }];
874 let tx = build_batch_transaction(&utxos, &recipients, &[0x00; 22], 5).unwrap();
875 assert_eq!(tx.inputs[0].sequence, 0xFFFFFFFD);
876 }
877
878 #[test]
891 fn test_btc_deserialize_real_p2pkh_tx() {
892 let raw_hex = "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac00000000";
893 let raw = hex::decode(raw_hex).unwrap();
894 let tx = parse_unsigned_tx(&raw).unwrap();
895
896 assert_eq!(tx.version, 1, "version must be 1");
898
899 assert_eq!(tx.inputs.len(), 1, "must have 1 input");
901 assert_eq!(tx.inputs[0].previous_output.vout, 1, "vout must be 1");
902 assert_eq!(tx.inputs[0].sequence, 0xFFFFFFFF, "sequence must be final");
903 assert_eq!(
905 hex::encode(tx.inputs[0].previous_output.txid),
906 "9c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48"
907 );
908
909 assert_eq!(tx.inputs[0].script_sig.len(), 106);
911
912 assert_eq!(tx.outputs.len(), 2, "must have 2 outputs");
914 assert_eq!(tx.outputs[0].value, 390_582, "output 0 value: 390582 sats");
915 assert_eq!(
916 tx.outputs[1].value, 16_932_484,
917 "output 1 value: 16932484 sats"
918 );
919
920 assert_eq!(tx.outputs[0].script_pubkey.len(), 25);
922 assert_eq!(tx.outputs[0].script_pubkey[0], 0x76); assert_eq!(tx.outputs[0].script_pubkey[1], 0xa9); assert_eq!(tx.outputs[0].script_pubkey[24], 0xac); assert_eq!(
928 hex::encode(&tx.outputs[0].script_pubkey[3..23]),
929 "bdf63990d6dc33d705b756e13dd135466c06b3b5"
930 );
931
932 assert_eq!(tx.locktime, 0);
934 }
935
936 #[test]
938 fn test_btc_serialize_roundtrip_p2pkh() {
939 let raw_hex = "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac00000000";
940 let raw = hex::decode(raw_hex).unwrap();
941 let tx = parse_unsigned_tx(&raw).unwrap();
942 let re_serialized = tx.serialize_legacy();
943 assert_eq!(
944 hex::encode(&re_serialized),
945 raw_hex,
946 "serialize(deserialize(raw)) must equal raw"
947 );
948 }
949
950 #[test]
952 fn test_btc_txid_from_real_tx() {
953 let raw_hex = "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac00000000";
954 let raw = hex::decode(raw_hex).unwrap();
955 let tx = parse_unsigned_tx(&raw).unwrap();
956 let txid = tx.txid();
957 let txid_hex = hex::encode(txid);
959 assert_eq!(txid_hex.len(), 64);
960 let txid2 = tx.txid();
962 assert_eq!(txid, txid2);
963 }
964
965 #[test]
967 fn test_btc_fee_estimation_known_size() {
968 let raw_hex = "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac00000000";
969 let raw = hex::decode(raw_hex).unwrap();
970 let tx = parse_unsigned_tx(&raw).unwrap();
971
972 let vsize = tx.vsize();
974 assert_eq!(vsize, raw.len(), "legacy tx vsize == serialized length");
975
976 let fee = estimate_fee(&tx, 10);
978 assert_eq!(fee, vsize as u64 * 10);
979
980 let fee_high = estimate_fee(&tx, 50);
982 assert_eq!(fee_high, vsize as u64 * 50);
983 }
984
985 #[test]
986 fn test_parse_unsigned_tx_rejects_unreasonable_input_count() {
987 let mut raw = Vec::new();
988 raw.extend_from_slice(&1u32.to_le_bytes()); raw.push(0xFD); raw.extend_from_slice(&0xFFFFu16.to_le_bytes()); assert!(parse_unsigned_tx(&raw).is_err());
993 }
994
995 #[test]
996 fn test_parse_unsigned_tx_rejects_unreasonable_output_count() {
997 let mut raw = Vec::new();
998 raw.extend_from_slice(&1u32.to_le_bytes()); raw.push(0x01); raw.extend_from_slice(&[0u8; 32]); raw.extend_from_slice(&0u32.to_le_bytes()); raw.push(0x00); raw.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes()); raw.push(0xFD); raw.extend_from_slice(&0xFFFFu16.to_le_bytes()); raw.extend_from_slice(&0u32.to_le_bytes()); assert!(parse_unsigned_tx(&raw).is_err());
1009 }
1010
1011 #[test]
1012 fn test_build_batch_transaction_rejects_input_sum_overflow() {
1013 let utxos = vec![
1014 (
1015 OutPoint {
1016 txid: [0x11; 32],
1017 vout: 0,
1018 },
1019 u64::MAX,
1020 ),
1021 (
1022 OutPoint {
1023 txid: [0x22; 32],
1024 vout: 1,
1025 },
1026 1,
1027 ),
1028 ];
1029 let recipients = vec![Recipient {
1030 script_pubkey: vec![0x00; 22],
1031 amount: 1,
1032 }];
1033 assert!(build_batch_transaction(&utxos, &recipients, &[0x00; 22], 1).is_err());
1034 }
1035
1036 #[test]
1037 fn test_build_batch_transaction_rejects_output_sum_overflow() {
1038 let utxos = vec![(
1039 OutPoint {
1040 txid: [0x33; 32],
1041 vout: 0,
1042 },
1043 u64::MAX,
1044 )];
1045 let recipients = vec![
1046 Recipient {
1047 script_pubkey: vec![0x00; 22],
1048 amount: u64::MAX,
1049 },
1050 Recipient {
1051 script_pubkey: vec![0x00; 22],
1052 amount: 1,
1053 },
1054 ];
1055 assert!(build_batch_transaction(&utxos, &recipients, &[0x00; 22], 1).is_err());
1056 }
1057}