Skip to main content

chains_sdk/bitcoin/
transaction.rs

1//! Bitcoin transaction serialization and ID computation.
2//!
3//! Provides lightweight, consensus-correct serialization for Bitcoin
4//! transactions (both legacy and SegWit/witness formats).
5
6use crate::crypto;
7use crate::encoding;
8
9// ─── Transaction Components ─────────────────────────────────────────
10
11/// A transaction outpoint (reference to a previous output).
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct OutPoint {
14    /// Previous transaction ID (32 bytes, internal byte order).
15    pub txid: [u8; 32],
16    /// Output index within that transaction.
17    pub vout: u32,
18}
19
20/// A transaction input.
21#[derive(Clone, Debug)]
22pub struct TxIn {
23    /// The outpoint being spent.
24    pub previous_output: OutPoint,
25    /// The scriptSig (empty for SegWit inputs).
26    pub script_sig: Vec<u8>,
27    /// Sequence number (0xFFFFFFFF = final).
28    pub sequence: u32,
29}
30
31/// A transaction output.
32#[derive(Clone, Debug)]
33pub struct TxOut {
34    /// Value in satoshis.
35    pub value: u64,
36    /// The scriptPubKey.
37    pub script_pubkey: Vec<u8>,
38}
39
40/// A Bitcoin transaction with optional witness data.
41#[derive(Clone, Debug)]
42pub struct Transaction {
43    /// Transaction version (typically 1 or 2).
44    pub version: i32,
45    /// Transaction inputs.
46    pub inputs: Vec<TxIn>,
47    /// Transaction outputs.
48    pub outputs: Vec<TxOut>,
49    /// Per-input witness stacks (empty for legacy transactions).
50    pub witnesses: Vec<Vec<Vec<u8>>>,
51    /// Lock time.
52    pub locktime: u32,
53}
54
55impl Transaction {
56    /// Create a new empty transaction.
57    #[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    /// Returns true if any input has witness data.
69    #[must_use]
70    pub fn has_witness(&self) -> bool {
71        self.witnesses.iter().any(|w| !w.is_empty())
72    }
73
74    /// Serialize without witness data (used for txid computation).
75    #[must_use]
76    pub fn serialize_legacy(&self) -> Vec<u8> {
77        let mut buf = Vec::with_capacity(256);
78
79        // Version (4 bytes LE)
80        buf.extend_from_slice(&self.version.to_le_bytes());
81
82        // Input count
83        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        // Output count
93        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        // Locktime (4 bytes LE)
101        buf.extend_from_slice(&self.locktime.to_le_bytes());
102
103        buf
104    }
105
106    /// Serialize with witness data (BIP-144 format).
107    ///
108    /// If no witnesses exist, falls back to legacy serialization.
109    #[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        // Version
118        buf.extend_from_slice(&self.version.to_le_bytes());
119
120        // Witness marker + flag
121        buf.push(0x00); // marker
122        buf.push(0x01); // flag
123
124        // Inputs
125        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        // Outputs
135        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        // Witness data for each input
143        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); // empty witness
155                }
156            }
157        }
158
159        // Locktime
160        buf.extend_from_slice(&self.locktime.to_le_bytes());
161
162        buf
163    }
164
165    /// Compute the transaction ID (double-SHA256 of legacy serialization, reversed).
166    ///
167    /// The txid is displayed in reversed byte order by convention.
168    #[must_use]
169    pub fn txid(&self) -> [u8; 32] {
170        let mut hash = crypto::double_sha256(&self.serialize_legacy());
171        hash.reverse(); // Bitcoin displays txid in reversed byte order
172        hash
173    }
174
175    /// Compute the witness transaction ID (wtxid).
176    ///
177    /// For legacy transactions, wtxid == txid.
178    #[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    /// Compute the virtual size (vsize) for fee calculation.
186    ///
187    /// `vsize = ceil((weight + 3) / 4)` where
188    /// `weight = base_size * 3 + total_size`
189    #[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
198/// Parse a raw unsigned transaction (no witness) into a `Transaction` struct.
199///
200/// This is the inverse of `Transaction::serialize_legacy()`. Used by the PSBT
201/// signer to reconstruct the transaction for sighash computation.
202pub fn parse_unsigned_tx(data: &[u8]) -> Result<Transaction, crate::error::SignerError> {
203    use crate::error::SignerError;
204
205    // Minimal serialized sizes used for sanity checks before allocation:
206    // input  = outpoint(36) + script_len varint(>=1) + sequence(4)
207    // output = value(8) + script_len varint(>=1)
208    const MIN_INPUT_BYTES: usize = 41;
209    const MIN_OUTPUT_BYTES: usize = 9;
210
211    /// Convert u64 to usize, rejecting overflow on 32-bit platforms.
212    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    // version (4 bytes LE)
221    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    // input count
228    let input_count = safe_usize(encoding::read_compact_size(data, &mut off)?)?;
229
230    // Reject impossible counts early to avoid unbounded allocation attempts.
231    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    // output count
292    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    // locktime (4 bytes LE)
344    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    // Strict parsing: reject trailing bytes
358    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
374// ═══════════════════════════════════════════════════════════════════
375// Fee Estimation Helpers
376// ═══════════════════════════════════════════════════════════════════
377
378/// Minimum relay fee (1 sat/vB).
379pub const MIN_RELAY_FEE_SAT_PER_VB: u64 = 1;
380
381/// The dust threshold for P2WPKH outputs (546 satoshis).
382pub const DUST_LIMIT_P2WPKH: u64 = 546;
383
384/// The dust threshold for P2PKH outputs (546 satoshis).
385pub const DUST_LIMIT_P2PKH: u64 = 546;
386
387/// The dust threshold for P2TR outputs (330 satoshis).
388pub const DUST_LIMIT_P2TR: u64 = 330;
389
390/// Estimate the fee for a transaction given a fee rate in sat/vB.
391///
392/// Uses a pre-built transaction to measure its virtual size.
393///
394/// # Arguments
395/// - `tx` — The transaction (can have placeholder witness for size estimation)
396/// - `fee_rate_sat_per_vb` — Fee rate in satoshis per virtual byte
397pub 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
404/// Estimate the weight/vsize of a transaction before construction.
405///
406/// # Arguments
407/// - `num_p2wpkh_inputs` — Number of P2WPKH (native SegWit) inputs
408/// - `num_p2tr_inputs` — Number of P2TR (Taproot) inputs
409/// - `num_p2pkh_inputs` — Number of P2PKH (legacy) inputs
410/// - `num_outputs` — Number of outputs
411pub 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    // Base overhead: version(4) + marker/flag(2) + input_count(1) + output_count(1) + locktime(4)
418    let overhead = 10 + 2; // 12 bytes (with witness flag)
419
420    // Per-input sizes (base + witness)
421    // P2WPKH: base=41, witness=107 → weight = 41*4+107 = 271 → vsize≈68
422    let p2wpkh_weight = num_p2wpkh_inputs * 271;
423    // P2TR: base=41, witness=66 → weight = 41*4+66 = 230 → vsize≈58
424    let p2tr_weight = num_p2tr_inputs * 230;
425    // P2PKH: base=148, no witness → weight = 148*4 = 592 → vsize=148
426    let p2pkh_weight = num_p2pkh_inputs * 592;
427
428    // Per-output: ~34 bytes (value=8 + scriptPubKey length=1 + scriptPubKey≈25)
429    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// ═══════════════════════════════════════════════════════════════════
436// Multi-Output Batch Builder
437// ═══════════════════════════════════════════════════════════════════
438
439/// A recipient for the batch builder.
440#[derive(Clone, Debug)]
441pub struct Recipient {
442    /// The scriptPubKey for the recipient.
443    pub script_pubkey: Vec<u8>,
444    /// Amount in satoshis.
445    pub amount: u64,
446}
447
448/// Build a multi-output transaction with automatic change calculation.
449///
450/// # Arguments
451/// - `utxos` — List of UTXOs to spend (outpoint + value pairs)
452/// - `recipients` — List of output recipients
453/// - `change_script_pubkey` — ScriptPubKey for the change output
454/// - `fee_rate_sat_per_vb` — Fee rate in satoshis per virtual byte
455///
456/// # Returns
457/// A `Transaction` with inputs, recipient outputs, and a change output (if above dust).
458///
459/// # Errors
460/// Returns an error if the total input value is insufficient to cover outputs + fees.
461pub 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    // Build transaction with change to estimate size
493    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    // Add inputs
509    for (outpoint, _) in utxos {
510        tx.inputs.push(TxIn {
511            previous_output: outpoint.clone(),
512            script_sig: vec![],
513            sequence: 0xFFFFFFFD, // RBF-enabled
514        });
515    }
516
517    // Add recipient outputs
518    for recipient in recipients {
519        tx.outputs.push(TxOut {
520            value: recipient.amount,
521            script_pubkey: recipient.script_pubkey.clone(),
522        });
523    }
524
525    // Add change output if above dust
526    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    // Final fee verification
534    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// ─── Tests ──────────────────────────────────────────────────────────
549
550#[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            ], // P2WPKH scriptPubKey
571        });
572        tx
573    }
574
575    #[test]
576    fn test_legacy_serialization_structure() {
577        let tx = sample_tx();
578        let raw = tx.serialize_legacy();
579        // version(4) + input_count(1) + prevout(32+4) + scriptsig_len(1) + seq(4)
580        // + output_count(1) + value(8) + spk_len(1) + spk(22) + locktime(4)
581        // = 4 + 1 + 36 + 1 + 4 + 1 + 8 + 1 + 22 + 4 = 82
582        assert_eq!(raw.len(), 82);
583        // Version should be 2
584        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        // No witnesses → witness serialization == legacy
591        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], // mock DER signature
600            vec![0x02; 33], // mock compressed pubkey
601        ]);
602        assert!(tx.has_witness());
603        let witness_raw = tx.serialize_witness();
604        let legacy_raw = tx.serialize_legacy();
605        // Witness serialization should be longer (marker+flag+witness data)
606        assert!(witness_raw.len() > legacy_raw.len());
607        // Witness marker/flag at bytes 4-5
608        assert_eq!(witness_raw[4], 0x00); // marker
609        assert_eq!(witness_raw[5], 0x01); // flag
610    }
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        // txid excludes witness, wtxid includes it
623        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        // No witness → vsize == base_size (weight = 4*base, vsize = base)
637        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        // With witness, vsize should be less than total_size but >= base_size
648        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        // version(4) + input_count(1=0) + output_count(1=0) + locktime(4) = 10
675        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        // Ensure it round-trips the input/output counts correctly
700        assert_eq!(raw[4], 3); // 3 inputs
701    }
702
703    // ─── Fee Estimation Tests ───────────────────────────────────
704
705    #[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        // 1 P2WPKH input, 2 outputs
723        let vsize = estimate_vsize(1, 0, 0, 2);
724        assert!(vsize > 0);
725        // Should be roughly 141 vbytes for 1-in-2-out P2WPKH
726        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        // P2TR is more compact
734        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    // ─── Batch Builder Tests ────────────────────────────────────
745
746    #[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()); // at least recipient
763    }
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        // Should have change output
781        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); // 3 recipients + possible change
822    }
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    // ─── Official Test Vectors ──────────────────────────────────
879
880    /// Real-world P2PKH transaction from the Bitcoin blockchain.
881    /// Source: bitcoin.org documentation example.
882    ///
883    /// Raw hex:
884    /// 01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48
885    /// 010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537
886    /// a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307
887    /// 012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff
888    /// 02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac
889    /// 845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac00000000
890    #[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        // Version
897        assert_eq!(tx.version, 1, "version must be 1");
898
899        // One input
900        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        // Input prevout txid (internal byte order from deserialization)
904        assert_eq!(
905            hex::encode(tx.inputs[0].previous_output.txid),
906            "9c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48"
907        );
908
909        // ScriptSig length: 0x6a = 106 bytes
910        assert_eq!(tx.inputs[0].script_sig.len(), 106);
911
912        // Two outputs
913        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        // P2PKH scriptPubKey format: OP_DUP OP_HASH160 <20bytes> OP_EQUALVERIFY OP_CHECKSIG
921        assert_eq!(tx.outputs[0].script_pubkey.len(), 25);
922        assert_eq!(tx.outputs[0].script_pubkey[0], 0x76); // OP_DUP
923        assert_eq!(tx.outputs[0].script_pubkey[1], 0xa9); // OP_HASH160
924        assert_eq!(tx.outputs[0].script_pubkey[24], 0xac); // OP_CHECKSIG
925
926        // Pubkey hash in output 0
927        assert_eq!(
928            hex::encode(&tx.outputs[0].script_pubkey[3..23]),
929            "bdf63990d6dc33d705b756e13dd135466c06b3b5"
930        );
931
932        // Locktime
933        assert_eq!(tx.locktime, 0);
934    }
935
936    /// Test that serialization of the parsed tx round-trips back to the same bytes.
937    #[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    /// Verify transaction ID matches the known txid for this transaction.
951    #[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        // txid is 32 bytes, displayed in hex (reversed by convention)
958        let txid_hex = hex::encode(txid);
959        assert_eq!(txid_hex.len(), 64);
960        // The txid should be deterministic
961        let txid2 = tx.txid();
962        assert_eq!(txid, txid2);
963    }
964
965    /// Test that fee estimation with a real transaction gives sensible results.
966    #[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        // For a legacy tx, vsize == raw byte count
973        let vsize = tx.vsize();
974        assert_eq!(vsize, raw.len(), "legacy tx vsize == serialized length");
975
976        // At 10 sat/vB
977        let fee = estimate_fee(&tx, 10);
978        assert_eq!(fee, vsize as u64 * 10);
979
980        // At 50 sat/vB
981        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()); // version
989        raw.push(0xFD); // CompactSize u16
990        raw.extend_from_slice(&0xFFFFu16.to_le_bytes()); // 65_535 inputs
991
992        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()); // version
999        raw.push(0x01); // 1 input
1000        raw.extend_from_slice(&[0u8; 32]); // prev txid
1001        raw.extend_from_slice(&0u32.to_le_bytes()); // prev vout
1002        raw.push(0x00); // scriptSig len
1003        raw.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes()); // sequence
1004        raw.push(0xFD); // CompactSize u16
1005        raw.extend_from_slice(&0xFFFFu16.to_le_bytes()); // 65_535 outputs
1006        raw.extend_from_slice(&0u32.to_le_bytes()); // locktime
1007
1008        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}