Skip to main content

bsv_transaction/
transaction.rs

1//! Core transaction type for the BSV blockchain.
2//!
3//! Represents a complete transaction with version, inputs, outputs, and locktime.
4//! Supports binary and hex serialization, transaction ID computation, coinbase
5//! detection, and various builder-pattern methods for adding inputs and outputs.
6//! Ported from the Go BSV SDK (`transaction` package).
7
8use bsv_primitives::chainhash::Hash;
9use bsv_primitives::hash::sha256d;
10use bsv_primitives::util::{BsvReader, BsvWriter, VarInt};
11
12use crate::input::{TransactionInput, DEFAULT_SEQUENCE_NUMBER};
13use crate::output::TransactionOutput;
14use crate::sighash;
15use crate::TransactionError;
16
17/// A BSV transaction consisting of a version, a set of inputs, a set of
18/// outputs, and a lock time.
19///
20/// # Wire format
21///
22/// | Field        | Size                      |
23/// |--------------|---------------------------|
24/// | version      | 4 bytes (LE)              |
25/// | input count  | VarInt                    |
26/// | inputs       | variable (per input)      |
27/// | output count | VarInt                    |
28/// | outputs      | variable (per output)     |
29/// | lock_time    | 4 bytes (LE)              |
30#[derive(Clone, Debug)]
31pub struct Transaction {
32    /// Transaction format version. Currently 1 or 2.
33    pub version: u32,
34
35    /// Ordered list of transaction inputs.
36    pub inputs: Vec<TransactionInput>,
37
38    /// Ordered list of transaction outputs.
39    pub outputs: Vec<TransactionOutput>,
40
41    /// Lock time. If non-zero, the transaction is not valid until the
42    /// specified block height or Unix timestamp.
43    pub lock_time: u32,
44}
45
46impl Transaction {
47    /// Create a new empty transaction with version 1 and lock time 0.
48    ///
49    /// # Returns
50    /// A `Transaction` with no inputs or outputs.
51    pub fn new() -> Self {
52        Transaction {
53            version: 1,
54            inputs: Vec::new(),
55            outputs: Vec::new(),
56            lock_time: 0,
57        }
58    }
59
60    // -----------------------------------------------------------------
61    // Deserialization
62    // -----------------------------------------------------------------
63
64    /// Parse a transaction from a hex-encoded string.
65    ///
66    /// # Arguments
67    /// * `hex_str` - A hex string of the raw transaction bytes.
68    ///
69    /// # Returns
70    /// `Ok(Transaction)` on success, or a `TransactionError` if the hex is
71    /// invalid or the bytes do not form a valid transaction.
72    pub fn from_hex(hex_str: &str) -> Result<Self, TransactionError> {
73        let bytes = hex::decode(hex_str)
74            .map_err(|e| TransactionError::SerializationError(format!("invalid hex: {}", e)))?;
75        Self::from_bytes(&bytes)
76    }
77
78    /// Parse a transaction from raw bytes.
79    ///
80    /// This method requires the byte slice to contain exactly one complete
81    /// transaction with no trailing data.
82    ///
83    /// # Arguments
84    /// * `bytes` - The raw transaction bytes.
85    ///
86    /// # Returns
87    /// `Ok(Transaction)` on success, or a `TransactionError` if the data
88    /// is truncated, malformed, or has trailing bytes.
89    pub fn from_bytes(bytes: &[u8]) -> Result<Self, TransactionError> {
90        let mut reader = BsvReader::new(bytes);
91        let tx = Self::read_from(&mut reader)?;
92        if reader.remaining() != 0 {
93            return Err(TransactionError::SerializationError(format!(
94                "trailing {} bytes after transaction",
95                reader.remaining()
96            )));
97        }
98        Ok(tx)
99    }
100
101    /// Deserialize a transaction from a `BsvReader`.
102    ///
103    /// Reads the version, input count, inputs, output count, outputs, and
104    /// lock time in standard Bitcoin wire format.
105    ///
106    /// # Arguments
107    /// * `reader` - The reader positioned at the start of a serialized transaction.
108    ///
109    /// # Returns
110    /// `Ok(Transaction)` on success, or a `TransactionError` on I/O or
111    /// format errors.
112    pub fn read_from(reader: &mut BsvReader) -> Result<Self, TransactionError> {
113        let version = reader
114            .read_u32_le()
115            .map_err(|e| TransactionError::SerializationError(format!("reading version: {}", e)))?;
116
117        let input_count = reader.read_varint().map_err(|e| {
118            TransactionError::SerializationError(format!("reading input count: {}", e))
119        })?;
120
121        let mut inputs = Vec::with_capacity(input_count.value() as usize);
122        for _ in 0..input_count.value() {
123            inputs.push(TransactionInput::read_from(reader)?);
124        }
125
126        let output_count = reader.read_varint().map_err(|e| {
127            TransactionError::SerializationError(format!("reading output count: {}", e))
128        })?;
129
130        let mut outputs = Vec::with_capacity(output_count.value() as usize);
131        for _ in 0..output_count.value() {
132            outputs.push(TransactionOutput::read_from(reader)?);
133        }
134
135        let lock_time = reader.read_u32_le().map_err(|e| {
136            TransactionError::SerializationError(format!("reading lock time: {}", e))
137        })?;
138
139        Ok(Transaction {
140            version,
141            inputs,
142            outputs,
143            lock_time,
144        })
145    }
146
147    // -----------------------------------------------------------------
148    // Serialization
149    // -----------------------------------------------------------------
150
151    /// Serialize this transaction to raw bytes.
152    ///
153    /// # Returns
154    /// A `Vec<u8>` containing the standard wire-format bytes:
155    /// version(4) + varint(n_in) + inputs + varint(n_out) + outputs + locktime(4).
156    pub fn to_bytes(&self) -> Vec<u8> {
157        let mut writer = BsvWriter::with_capacity(256);
158        writer.write_u32_le(self.version);
159
160        writer.write_varint(VarInt::from(self.inputs.len()));
161        for input in &self.inputs {
162            input.write_to(&mut writer);
163        }
164
165        writer.write_varint(VarInt::from(self.outputs.len()));
166        for output in &self.outputs {
167            output.write_to(&mut writer);
168        }
169
170        writer.write_u32_le(self.lock_time);
171        writer.into_bytes()
172    }
173
174    /// Serialize this transaction to a hex string.
175    ///
176    /// # Returns
177    /// A lowercase hex-encoded string of the raw bytes.
178    pub fn to_hex(&self) -> String {
179        hex::encode(self.to_bytes())
180    }
181
182    // -----------------------------------------------------------------
183    // Transaction ID
184    // -----------------------------------------------------------------
185
186    /// Compute the transaction ID (double SHA-256 of serialized bytes).
187    ///
188    /// The txid bytes are in internal (little-endian) order. To get the
189    /// conventional display string, use `tx_id_hex()`.
190    ///
191    /// # Returns
192    /// A 32-byte array containing the txid in internal byte order.
193    pub fn tx_id(&self) -> [u8; 32] {
194        sha256d(&self.to_bytes())
195    }
196
197    /// Compute the transaction ID as a human-readable hex string.
198    ///
199    /// The hex string is byte-reversed from the internal hash, following
200    /// Bitcoin's convention where txids are displayed in big-endian order.
201    ///
202    /// # Returns
203    /// A 64-character hex string of the txid.
204    pub fn tx_id_hex(&self) -> String {
205        let mut id = self.tx_id();
206        id.reverse();
207        hex::encode(id)
208    }
209
210    // -----------------------------------------------------------------
211    // Inputs
212    // -----------------------------------------------------------------
213
214    /// Append a `TransactionInput` to this transaction.
215    ///
216    /// # Arguments
217    /// * `input` - The input to add.
218    pub fn add_input(&mut self, input: TransactionInput) {
219        self.inputs.push(input);
220    }
221
222    /// Return the number of inputs in the transaction.
223    ///
224    /// # Returns
225    /// The input count.
226    pub fn input_count(&self) -> usize {
227        self.inputs.len()
228    }
229
230    // -----------------------------------------------------------------
231    // Outputs
232    // -----------------------------------------------------------------
233
234    /// Append a `TransactionOutput` to this transaction.
235    ///
236    /// # Arguments
237    /// * `output` - The output to add.
238    pub fn add_output(&mut self, output: TransactionOutput) {
239        self.outputs.push(output);
240    }
241
242    /// Return the number of outputs in the transaction.
243    ///
244    /// # Returns
245    /// The output count.
246    pub fn output_count(&self) -> usize {
247        self.outputs.len()
248    }
249
250    /// Compute the sum of all output satoshi values.
251    ///
252    /// # Returns
253    /// The total satoshis across all outputs.
254    pub fn total_output_satoshis(&self) -> u64 {
255        self.outputs.iter().map(|o| o.satoshis).sum()
256    }
257
258    /// Compute the sum of all input satoshi values from their source outputs.
259    ///
260    /// Returns an error if any input does not have its source transaction set.
261    ///
262    /// # Returns
263    /// `Ok(total)` with the sum of input satoshis, or an error if a source
264    /// transaction is missing.
265    pub fn total_input_satoshis(&self) -> Result<u64, TransactionError> {
266        let mut total = 0u64;
267        for input in &self.inputs {
268            let sats = input.source_tx_satoshis().ok_or_else(|| {
269                TransactionError::InvalidTransaction(
270                    "missing source transaction on input".to_string(),
271                )
272            })?;
273            total += sats;
274        }
275        Ok(total)
276    }
277
278    // -----------------------------------------------------------------
279    // Coinbase detection
280    // -----------------------------------------------------------------
281
282    /// Determine whether this transaction is a coinbase transaction.
283    ///
284    /// A coinbase transaction has exactly one input with an all-zero txid
285    /// and either `source_tx_out_index == 0xFFFFFFFF` or
286    /// `sequence_number == 0xFFFFFFFF`.
287    ///
288    /// # Returns
289    /// `true` if this is a coinbase transaction.
290    pub fn is_coinbase(&self) -> bool {
291        if self.inputs.len() != 1 {
292            return false;
293        }
294
295        let input = &self.inputs[0];
296
297        // Check that the source txid is all zeros.
298        if input.source_txid != [0u8; 32] {
299            return false;
300        }
301
302        // Either the output index or the sequence must be 0xFFFFFFFF.
303        input.source_tx_out_index == 0xFFFF_FFFF || input.sequence_number == 0xFFFF_FFFF
304    }
305
306    /// Return the size of this transaction in bytes.
307    ///
308    /// # Returns
309    /// The byte length of the serialized transaction.
310    pub fn size(&self) -> usize {
311        self.to_bytes().len()
312    }
313
314    // -----------------------------------------------------------------
315    // Input helpers
316    // -----------------------------------------------------------------
317
318    /// Add an input from UTXO information.
319    ///
320    /// Creates a new input referencing the given previous transaction
321    /// output and stores the locking script and satoshi value for
322    /// sighash computation during signing.
323    ///
324    /// Matches the Go SDK's `Transaction.AddInputFrom(prevTxID, vout,
325    /// prevTxLockingScript, satoshis, ...)`.
326    ///
327    /// # Arguments
328    /// * `prev_tx_id` - The hex txid of the previous transaction (display order).
329    /// * `vout` - The output index being spent.
330    /// * `prev_locking_script_hex` - Hex-encoded locking script of the previous output.
331    /// * `satoshis` - The satoshi value of the previous output.
332    ///
333    /// # Returns
334    /// `Ok(())` on success, or a `TransactionError` if any hex is invalid.
335    pub fn add_input_from(
336        &mut self,
337        prev_tx_id: &str,
338        vout: u32,
339        prev_locking_script_hex: &str,
340        satoshis: u64,
341    ) -> Result<(), TransactionError> {
342        let hash = Hash::from_hex(prev_tx_id)?;
343
344        let locking_script = if prev_locking_script_hex.is_empty() {
345            bsv_script::Script::new()
346        } else {
347            bsv_script::Script::from_hex(prev_locking_script_hex)?
348        };
349
350        let mut input = TransactionInput::new();
351        input.source_txid = *hash.as_bytes();
352        input.source_tx_out_index = vout;
353        input.sequence_number = DEFAULT_SEQUENCE_NUMBER;
354        input.set_source_output(Some(TransactionOutput {
355            satoshis,
356            locking_script,
357            change: false,
358        }));
359
360        self.inputs.push(input);
361        Ok(())
362    }
363
364    // -----------------------------------------------------------------
365    // Signature hash
366    // -----------------------------------------------------------------
367
368    /// Compute the BIP-143-style signature hash for a given input.
369    ///
370    /// Looks up the source output's locking script and satoshi value
371    /// from the input's stored source info, then delegates to
372    /// `sighash::signature_hash`.
373    ///
374    /// Matches the Go SDK's `Transaction.CalcInputSignatureHash(inputNumber, sigHashFlag)`.
375    ///
376    /// # Arguments
377    /// * `input_index` - Index of the input being signed.
378    /// * `sighash_flag` - The combined sighash flags (e.g. `SIGHASH_ALL_FORKID`).
379    ///
380    /// # Returns
381    /// A 32-byte double-SHA256 hash to be signed by ECDSA.
382    pub fn calc_input_signature_hash(
383        &self,
384        input_index: usize,
385        sighash_flag: u32,
386    ) -> Result<[u8; 32], TransactionError> {
387        if input_index >= self.inputs.len() {
388            return Err(TransactionError::InvalidTransaction(format!(
389                "input index {} out of range (tx has {} inputs)",
390                input_index,
391                self.inputs.len()
392            )));
393        }
394
395        let input = &self.inputs[input_index];
396        let source_output = input.source_tx_output().ok_or_else(|| {
397            TransactionError::SigningError(
398                "missing source output on input (no previous tx info)".to_string(),
399            )
400        })?;
401
402        let script_bytes = source_output.locking_script.to_bytes();
403        let satoshis = source_output.satoshis;
404
405        sighash::signature_hash(self, input_index, script_bytes, sighash_flag, satoshis)
406    }
407}
408
409impl Default for Transaction {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415impl std::fmt::Display for Transaction {
416    /// Display the transaction as its hex-encoded serialization.
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        write!(f, "{}", self.to_hex())
419    }
420}