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}