bsv-sdk 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/lib/bsv/auth/auth_middleware.rb +6 -6
  4. data/lib/bsv/auth/certificate.rb +22 -18
  5. data/lib/bsv/auth/master_certificate.rb +5 -5
  6. data/lib/bsv/auth/nonce.rb +13 -13
  7. data/lib/bsv/auth/peer.rb +53 -53
  8. data/lib/bsv/auth/verifiable_certificate.rb +1 -1
  9. data/lib/bsv/identity/client.rb +27 -32
  10. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +18 -12
  11. data/lib/bsv/mcp/tools/check_balance.rb +16 -4
  12. data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
  13. data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
  14. data/lib/bsv/mcp/tools/helpers.rb +2 -2
  15. data/lib/bsv/network/arc.rb +13 -153
  16. data/lib/bsv/network/broadcast_error.rb +1 -0
  17. data/lib/bsv/network/broadcast_response.rb +1 -0
  18. data/lib/bsv/network/protocols/arc.rb +4 -3
  19. data/lib/bsv/network/protocols/taal_binary.rb +1 -0
  20. data/lib/bsv/network/protocols/woc_rest.rb +2 -1
  21. data/lib/bsv/network/whats_on_chain.rb +13 -107
  22. data/lib/bsv/overlay/admin_token_template.rb +4 -4
  23. data/lib/bsv/overlay/lookup_resolver.rb +1 -0
  24. data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
  25. data/lib/bsv/overlay/types.rb +1 -0
  26. data/lib/bsv/primitives/hex.rb +64 -0
  27. data/lib/bsv/registry/client.rb +26 -28
  28. data/lib/bsv/registry/types.rb +1 -0
  29. data/lib/bsv/script/interpreter/interpreter.rb +7 -0
  30. data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
  31. data/lib/bsv/script/push_drop_template.rb +4 -4
  32. data/lib/bsv/transaction/beef.rb +122 -83
  33. data/lib/bsv/transaction/merkle_path.rb +54 -38
  34. data/lib/bsv/transaction/transaction.rb +81 -30
  35. data/lib/bsv/transaction/transaction_input.rb +23 -18
  36. data/lib/bsv/version.rb +1 -1
  37. data/lib/bsv/wallet/errors.rb +47 -0
  38. data/lib/bsv/wallet/interface/brc100.rb +270 -0
  39. data/lib/bsv/wallet/interface.rb +9 -0
  40. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +152 -0
  41. data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
  42. data/lib/bsv/wallet/proto_wallet.rb +327 -0
  43. data/lib/bsv/wallet.rb +16 -0
  44. data/lib/bsv-sdk.rb +18 -1
  45. metadata +22 -1
@@ -21,7 +21,8 @@ module BSV
21
21
  # @!attribute [r] hash
22
22
  # @return [String, nil] 32-byte hash (nil when duplicate)
23
23
  # @!attribute [r] txid
24
- # @return [Boolean] whether this leaf is a transaction ID
24
+ # @return [Boolean] BRC-74 flag — whether this leaf represents a transaction in
25
+ # the merkle tree (the 0x02 bit in the BRC-74 serialisation). Not a txid value.
25
26
  # @!attribute [r] duplicate
26
27
  # @return [Boolean] whether this leaf duplicates its sibling
27
28
  class PathElement
@@ -29,7 +30,9 @@ module BSV
29
30
 
30
31
  # @param offset [Integer] position index within the tree level
31
32
  # @param hash [String, nil] 32-byte hash (nil when duplicate)
32
- # @param txid [Boolean] whether this leaf is a transaction ID
33
+ # @param txid [Boolean] BRC-74 flag — true when this leaf represents a transaction
34
+ # in the tree (the 0x02 bit in the BRC-74 serialisation). This is NOT a txid
35
+ # value; it is a boolean presence flag mandated by the BRC-74 specification.
33
36
  # @param duplicate [Boolean] whether this leaf duplicates its sibling
34
37
  def initialize(offset:, hash: nil, txid: false, duplicate: false)
35
38
  @offset = offset
@@ -147,25 +150,26 @@ module BSV
147
150
  # @example Convert a WoC TSC proof
148
151
  # tsc = JSON.parse(woc_response).first
149
152
  # mp = BSV::Transaction::MerklePath.from_tsc(
150
- # txid: tsc['txOrId'],
153
+ # dtxid_hex: tsc['txOrId'],
151
154
  # index: tsc['index'],
152
155
  # nodes: tsc['nodes'],
153
156
  # block_height: 612_251
154
157
  # )
155
158
  # mp.compute_root_hex(tsc['txOrId']) #=> the block's merkle root
156
159
  #
157
- # @param txid [String] hex-encoded transaction ID in display byte order
160
+ # @param dtxid_hex [String] hex-encoded transaction ID in display byte order
158
161
  # @param index [Integer] the transaction's position in the block
159
162
  # @param nodes [Array<String>] sibling hashes leaf-to-root, each a 32-byte
160
163
  # hex string in display byte order, or +"*"+ for a duplicate node
161
164
  # @param block_height [Integer] the block's height (TSC carries the block
162
165
  # hash; the caller must look up the height separately)
163
166
  # @return [MerklePath] a BRC-74 merkle path equivalent to the TSC proof
164
- def self.from_tsc(txid:, index:, nodes:, block_height:)
165
- txid_bytes = [txid].pack('H*').reverse
167
+ def self.from_tsc(dtxid_hex:, index:, nodes:, block_height:)
168
+ BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
169
+ wtxid = [dtxid_hex].pack('H*').reverse
166
170
 
167
171
  # Level 0 always contains the txid leaf.
168
- level0 = [PathElement.new(offset: index, hash: txid_bytes, txid: true)]
172
+ level0 = [PathElement.new(offset: index, hash: wtxid, txid: true)]
169
173
 
170
174
  # A single-tx block has no siblings — the txid IS the merkle root.
171
175
  return new(block_height: block_height, path: [level0]) if nodes.empty?
@@ -193,6 +197,7 @@ module BSV
193
197
  if node == '*'
194
198
  PathElement.new(offset: offset, duplicate: true)
195
199
  else
200
+ BSV::Primitives::Hex.validate_dtxid_hex!(node, name: "TSC merkle node at offset #{offset}")
196
201
  PathElement.new(offset: offset, hash: [node].pack('H*').reverse)
197
202
  end
198
203
  end
@@ -240,17 +245,22 @@ module BSV
240
245
 
241
246
  # Recompute the merkle root from this path and a transaction ID.
242
247
  #
243
- # @param txid [String, nil] 32-byte txid in internal byte order (auto-detected if nil)
244
- # @return [String] 32-byte merkle root in internal byte order
245
- # @raise [ArgumentError] if the txid is not found in the path
246
- def compute_root(txid = nil)
247
- txid ||= @path[0].find(&:hash)&.hash
248
- return txid if @path.length == 1 && @path[0].length == 1
248
+ # @param wtxid [String, nil] 32-byte txid in wire byte order (auto-detected if nil)
249
+ # @return [String] 32-byte merkle root in wire byte order
250
+ # @raise [ArgumentError] if the wtxid is not found in the path
251
+ def compute_root(wtxid = nil)
252
+ wtxid ||= @path[0].find(&:hash)&.hash
253
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid') if wtxid
254
+ BSV.logger&.debug do
255
+ dtxid = wtxid&.reverse&.unpack1('H*')
256
+ "[MerklePath] compute_root: dtxid=#{dtxid} block_height=#{@block_height} levels=#{@path.length}"
257
+ end
258
+ return wtxid if @path.length == 1 && @path[0].length == 1
249
259
 
250
260
  indexed = build_indexed_path
251
261
 
252
- tx_leaf = @path[0].find { |l| l.hash == txid }
253
- raise ArgumentError, 'the BUMP does not contain the txid' unless tx_leaf
262
+ tx_leaf = @path[0].find { |l| l.hash == wtxid }
263
+ raise ArgumentError, 'the BUMP does not contain the given wtxid' unless tx_leaf
254
264
 
255
265
  working = tx_leaf.hash
256
266
  index = tx_leaf.offset
@@ -289,11 +299,12 @@ module BSV
289
299
 
290
300
  # Recompute the merkle root and return it as a hex string.
291
301
  #
292
- # @param txid_hex [String, nil] hex-encoded txid (display order)
302
+ # @param dtxid_hex [String, nil] hex-encoded txid (display order)
293
303
  # @return [String] hex-encoded merkle root (display order)
294
- def compute_root_hex(txid_hex = nil)
295
- txid = txid_hex ? [txid_hex].pack('H*').reverse : nil
296
- compute_root(txid).reverse.unpack1('H*')
304
+ def compute_root_hex(dtxid_hex = nil)
305
+ BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'compute_root_hex dtxid_hex') if dtxid_hex
306
+ wtxid = dtxid_hex ? [dtxid_hex].pack('H*').reverse : nil
307
+ compute_root(wtxid).reverse.unpack1('H*')
297
308
  end
298
309
 
299
310
  # --- Verification ---
@@ -312,22 +323,27 @@ module BSV
312
323
  # and accepts immature ones — the opposite of the intended behaviour. The correct
313
324
  # logic is: reject when `current_height - block_height < 100` (immature).
314
325
  #
315
- # @param txid_hex [String] hex-encoded transaction ID (display order)
326
+ # @param dtxid_hex [String] hex-encoded transaction ID (display order)
316
327
  # @param chain_tracker [ChainTracker] chain tracker to verify the root against
317
328
  # @return [Boolean] true if the computed root matches the block at this height
318
- def verify(txid_hex, chain_tracker)
319
- txid_bytes = [txid_hex].pack('H*').reverse
320
- txid_leaf = @path[0].find { |l| l.hash == txid_bytes }
329
+ def verify(dtxid_hex, chain_tracker)
330
+ BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
331
+ wtxid = [dtxid_hex].pack('H*').reverse
332
+ tx_leaf = @path[0].find { |l| l.hash == wtxid }
321
333
 
322
334
  # Offset 0 in a block's merkle tree is always the coinbase transaction —
323
335
  # a Bitcoin protocol invariant. Apply the 100-block maturity check.
324
- if txid_leaf&.offset&.zero?
336
+ if tx_leaf&.offset&.zero?
325
337
  current = chain_tracker.current_height
326
338
  return false if current - @block_height < 100
327
339
  end
328
340
 
329
- root_hex = compute_root_hex(txid_hex)
330
- chain_tracker.valid_root_for_height?(root_hex, @block_height)
341
+ root_hex = compute_root_hex(dtxid_hex)
342
+ valid = chain_tracker.valid_root_for_height?(root_hex, @block_height)
343
+ BSV.logger&.debug do
344
+ "[MerklePath] verify: dtxid=#{dtxid_hex} height=#{@block_height} root=#{root_hex} valid=#{valid}"
345
+ end
346
+ valid
331
347
  end
332
348
 
333
349
  # --- Combine ---
@@ -445,24 +461,24 @@ module BSV
445
461
  #
446
462
  # Matches the TS SDK's +MerklePath.extract+ behaviour.
447
463
  #
448
- # @param txid_hashes [Array<String>] 32-byte txids in internal byte
464
+ # @param wtxid_hashes [Array<String>] 32-byte txids in wire byte
449
465
  # order (reverse of display order). To pass hex strings, use
450
- # +txid_hexes.map { |h| [h].pack('H*').reverse }+.
466
+ # +dtxid_hexes.map { |h| [h].pack('H*').reverse }+.
451
467
  # @return [MerklePath] a new trimmed compound path proving only the
452
468
  # requested txids
453
- # @raise [ArgumentError] if +txid_hashes+ is empty, any requested
469
+ # @raise [ArgumentError] if +wtxid_hashes+ is empty, any requested
454
470
  # txid is not present in the source path's level 0, or the
455
471
  # extracted path's root does not match the source root
456
- def extract(txid_hashes)
457
- raise ArgumentError, 'at least one txid must be provided to extract' if txid_hashes.empty?
472
+ def extract(wtxid_hashes)
473
+ raise ArgumentError, 'at least one wtxid must be provided to extract' if wtxid_hashes.empty?
458
474
 
459
475
  original_root = compute_root
460
476
  indexed = build_indexed_path
461
477
 
462
478
  # Build a level-0 hash → offset lookup
463
- txid_to_offset = {}
479
+ wtxid_to_offset = {}
464
480
  @path[0].each do |leaf|
465
- txid_to_offset[leaf.hash] = leaf.offset if leaf.hash
481
+ wtxid_to_offset[leaf.hash] = leaf.offset if leaf.hash
466
482
  end
467
483
 
468
484
  max_offset = @path[0].map(&:offset).max || 0
@@ -470,15 +486,15 @@ module BSV
470
486
 
471
487
  needed = Array.new(tree_height) { {} }
472
488
 
473
- txid_hashes.each do |txid|
474
- tx_offset = txid_to_offset[txid]
489
+ wtxid_hashes.each do |wtxid_hash|
490
+ tx_offset = wtxid_to_offset[wtxid_hash]
475
491
  if tx_offset.nil?
476
492
  raise ArgumentError,
477
- "transaction ID #{txid.reverse.unpack1('H*')} not found in the Merkle Path"
493
+ "wtxid #{wtxid_hash.reverse.unpack1('H*')} not found in the Merkle Path"
478
494
  end
479
495
 
480
- # Level 0: the txid leaf itself + its tree sibling
481
- needed[0][tx_offset] = PathElement.new(offset: tx_offset, hash: txid, txid: true)
496
+ # Level 0: the transaction leaf itself + its tree sibling
497
+ needed[0][tx_offset] = PathElement.new(offset: tx_offset, hash: wtxid_hash, txid: true)
482
498
  sib0_offset = tx_offset ^ 1
483
499
  unless needed[0].key?(sib0_offset)
484
500
  sib = offset_leaf(indexed, 0, sib0_offset)
@@ -329,6 +329,11 @@ module BSV
329
329
  ancestors = collect_ancestors
330
330
 
331
331
  bump_index_by_height = build_beef_bumps(beef, ancestors)
332
+ BSV.logger&.debug do
333
+ proven = ancestors.count(&:merkle_path)
334
+ "[Transaction] BEEF: #{ancestors.length} ancestors, #{proven} proven " \
335
+ "across #{bump_index_by_height.length} block heights"
336
+ end
332
337
 
333
338
  ancestors.each do |tx|
334
339
  entry = if tx.merkle_path
@@ -370,11 +375,11 @@ module BSV
370
375
  # or nil if the BEEF is empty or contains no raw transaction entries
371
376
  def self.from_beef(data)
372
377
  beef = Beef.from_binary(data)
373
- subject_txid = beef.subject_txid ||
374
- beef.transactions.reverse.find(&:transaction)&.transaction&.txid
375
- return nil unless subject_txid
378
+ subject_wtxid = beef.subject_wtxid ||
379
+ beef.transactions.reverse.find(&:transaction)&.transaction&.wtxid
380
+ return nil unless subject_wtxid
376
381
 
377
- beef.find_atomic_transaction(subject_txid)
382
+ beef.find_atomic_transaction(subject_wtxid)
378
383
  end
379
384
 
380
385
  # Parse a BEEF hex string and return the subject transaction.
@@ -388,15 +393,26 @@ module BSV
388
393
 
389
394
  # --- Transaction ID ---
390
395
 
391
- # Compute the transaction ID (double-SHA-256 of the serialised tx, byte-reversed).
396
+ # Wire-order transaction ID (raw SHA-256d of the serialised tx).
397
+ #
398
+ # Used by BEEF, BUMPs, and merkle paths, which all work in wire byte order
399
+ # to match {TransactionInput#prev_wtxid}.
400
+ #
401
+ # @return [String] 32-byte transaction ID in wire byte order
402
+ def wtxid
403
+ id = BSV::Primitives::Digest.sha256d(to_binary)
404
+ BSV.logger&.debug { "[Transaction] wtxid computed (dtxid=#{id.reverse.unpack1('H*')})" }
405
+ id
406
+ end
407
+
408
+ # Display-order transaction ID (reversed from the natural SHA-256d hash).
392
409
  #
393
- # Returns display byte order (reversed from the natural hash).
394
- # Compare with {TransactionInput#prev_tx_id} which stores wire byte
395
- # order (natural hash). Use +.reverse+ to convert between the two.
410
+ # This is the conventional human-readable representation used in block
411
+ # explorers, wallets, and all user-facing contexts.
396
412
  #
397
413
  # @return [String] 32-byte transaction ID in display byte order
398
414
  def txid
399
- BSV::Primitives::Digest.sha256d(to_binary).reverse
415
+ wtxid.reverse
400
416
  end
401
417
 
402
418
  # The transaction ID as a hex string (display byte order).
@@ -406,6 +422,19 @@ module BSV
406
422
  txid.unpack1('H*')
407
423
  end
408
424
 
425
+ # Display-order transaction ID as a hex string.
426
+ #
427
+ # Mirrors the wallet gem's +DisplayTxid+ pattern. +dtxid+ always returns
428
+ # a 64-char hex string suitable for JSON and UI boundaries.
429
+ #
430
+ # @return [String] hex-encoded transaction ID (display order)
431
+ alias dtxid txid_hex
432
+
433
+ # Display-order transaction ID as a hex string (alias for {#dtxid}).
434
+ #
435
+ # @return [String] hex-encoded transaction ID (display order)
436
+ alias dtxid_hex txid_hex
437
+
409
438
  # --- Sighash (BIP-143 with FORKID) ---
410
439
 
411
440
  # Build the BIP-143 sighash preimage for an input.
@@ -456,6 +485,19 @@ module BSV
456
485
  # 10. sighash type (4 LE) — includes FORKID flag
457
486
  buf << [sighash_type].pack('V')
458
487
 
488
+ BSV.logger&.debug do
489
+ hp = buf.byteslice(4, 32).unpack1('H*')
490
+ hs = buf.byteslice(36, 32).unpack1('H*')
491
+ op = input.outpoint_binary.unpack1('H*')
492
+ sc = script_bytes.unpack1('H*')
493
+ ho = buf.byteslice(-40, 32).unpack1('H*')
494
+ "[Sighash] input=#{input_index} type=0x#{format('%02x', sighash_type)} " \
495
+ "version=#{@version} hashPrevouts=#{hp} hashSequence=#{hs} " \
496
+ "outpoint=#{op} scriptCode=#{sc[0, 40]}#{'...' if sc.length > 40} " \
497
+ "value=#{input.source_satoshis} seq=#{input.sequence} " \
498
+ "hashOutputs=#{ho} locktime=#{@lock_time}"
499
+ end
500
+
459
501
  buf
460
502
  end
461
503
 
@@ -466,7 +508,9 @@ module BSV
466
508
  # @param subscript [Script::Script, nil] override locking script for the input
467
509
  # @return [String] 32-byte sighash digest
468
510
  def sighash(input_index, sighash_type = Sighash::ALL_FORK_ID, subscript: nil)
469
- BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
511
+ digest = BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
512
+ BSV.logger&.debug { "[Sighash] digest=#{digest.unpack1('H*')}" }
513
+ digest
470
514
  end
471
515
 
472
516
  # --- Signing ---
@@ -580,17 +624,17 @@ module BSV
580
624
 
581
625
  until queue.empty?
582
626
  tx = queue.shift
583
- tx_id = tx.txid_hex
584
- next if verified[tx_id]
627
+ wtxid = tx.wtxid
628
+ next if verified[wtxid]
585
629
 
586
630
  # Merkle path short-circuit: proven transaction needs no input verification
587
631
  if tx.merkle_path
588
- unless tx.merkle_path.verify(tx_id, chain_tracker)
632
+ unless tx.merkle_path.verify(tx.txid_hex, chain_tracker)
589
633
  raise VerificationError.new(:invalid_merkle_proof,
590
- "invalid merkle proof for transaction #{tx_id}")
634
+ "invalid merkle proof for transaction #{tx.txid_hex}")
591
635
  end
592
636
 
593
- verified[tx_id] = true
637
+ verified[wtxid] = true
594
638
  next
595
639
  end
596
640
 
@@ -622,13 +666,13 @@ module BSV
622
666
 
623
667
  # Enqueue source transaction for verification if not yet verified
624
668
  source_tx = input.source_transaction
625
- queue << source_tx if source_tx && !verified[source_tx.txid_hex]
669
+ queue << source_tx if source_tx && !verified[source_tx.wtxid]
626
670
  end
627
671
 
628
672
  # Output ≤ input check
629
673
  verify_output_constraint(tx)
630
674
 
631
- verified[tx_id] = true
675
+ verified[wtxid] = true
632
676
  end
633
677
 
634
678
  true
@@ -777,19 +821,19 @@ module BSV
777
821
  end
778
822
 
779
823
  def verify_input_requirements(tx, input, index)
780
- tx_id = tx.txid_hex
824
+ dtxid_hex = tx.txid_hex
781
825
  if input.unlocking_script.nil?
782
826
  raise VerificationError.new(:missing_source,
783
- "input #{index} of transaction #{tx_id} has no unlocking script")
827
+ "input #{index} of transaction #{dtxid_hex} has no unlocking script")
784
828
  end
785
829
  if input.source_locking_script.nil?
786
830
  raise VerificationError.new(:missing_source,
787
- "input #{index} of transaction #{tx_id} has no source locking script")
831
+ "input #{index} of transaction #{dtxid_hex} has no source locking script")
788
832
  end
789
833
  return unless input.source_satoshis.nil?
790
834
 
791
835
  raise VerificationError.new(:missing_source,
792
- "input #{index} of transaction #{tx_id} has no source satoshis")
836
+ "input #{index} of transaction #{dtxid_hex} has no source satoshis")
793
837
  end
794
838
 
795
839
  def verify_fee(fee_model)
@@ -843,7 +887,7 @@ module BSV
843
887
 
844
888
  # Collect this transaction and all its ancestors in dependency order
845
889
  # (ancestors first, self last). Stops recursion at transactions with
846
- # a merkle_path (proven leaves). Deduplicates by txid.
890
+ # a merkle_path (proven leaves). Deduplicates by wtxid (wire-order bytes).
847
891
  def collect_ancestors
848
892
  seen = {}
849
893
  result = []
@@ -852,18 +896,25 @@ module BSV
852
896
  end
853
897
 
854
898
  def collect_ancestors_recursive(tx, seen, result)
855
- txid = tx.txid
856
- return if seen.key?(txid)
899
+ wtxid = tx.wtxid
900
+ return if seen.key?(wtxid)
857
901
 
858
- unless tx.merkle_path
859
- tx.inputs.each do |input|
860
- next unless input.source_transaction
902
+ if tx.merkle_path
903
+ BSV.logger&.debug do
904
+ "[Transaction] ancestor: #{tx.dtxid_hex} proven at height #{tx.merkle_path.block_height} (leaf stop)"
905
+ end
906
+ else
907
+ tx.inputs.each_with_index do |input, idx|
908
+ unless input.source_transaction
909
+ BSV.logger&.debug { "[Transaction] ancestor: #{tx.dtxid_hex} input #{idx} has no source_transaction (skipped)" }
910
+ next
911
+ end
861
912
 
862
913
  collect_ancestors_recursive(input.source_transaction, seen, result)
863
914
  end
864
915
  end
865
916
 
866
- seen[txid] = true
917
+ seen[wtxid] = true
867
918
  result << tx
868
919
  end
869
920
 
@@ -886,8 +937,8 @@ module BSV
886
937
  merged = txs.first.merkle_path.dup
887
938
  txs.drop(1).each { |t| merged.combine(t.merkle_path) }
888
939
 
889
- txid_hashes = txs.map { |t| t.txid.reverse }
890
- clean = merged.extract(txid_hashes)
940
+ wtxid_hashes = txs.map(&:wtxid)
941
+ clean = merged.extract(wtxid_hashes)
891
942
 
892
943
  bump_index_by_height[height] = beef.bumps.length
893
944
  beef.bumps << clean
@@ -8,8 +8,8 @@ module BSV
8
8
  # output index (the "outpoint"), and provide an unlocking script to
9
9
  # satisfy the locking script conditions.
10
10
  class TransactionInput
11
- # @return [String] 32-byte transaction ID of the output being spent (internal byte order)
12
- attr_reader :prev_tx_id
11
+ # @return [String] 32-byte wire-order transaction ID of the output being spent
12
+ attr_reader :prev_wtxid
13
13
 
14
14
  # @return [Integer] index of the output within the previous transaction
15
15
  attr_reader :prev_tx_out_index
@@ -32,15 +32,17 @@ module BSV
32
32
  # @return [UnlockingScriptTemplate, nil] template for deferred signing
33
33
  attr_accessor :unlocking_script_template
34
34
 
35
- # @param prev_tx_id [String] 32-byte transaction ID (internal byte order)
35
+ # @param prev_wtxid [String] 32-byte wire-order transaction ID
36
36
  # @param prev_tx_out_index [Integer] output index in the previous transaction
37
37
  # @param unlocking_script [Script::Script, nil] unlocking script (nil if unsigned)
38
38
  # @param sequence [Integer] sequence number
39
- def initialize(prev_tx_id:, prev_tx_out_index:, unlocking_script: nil, sequence: 0xFFFFFFFF)
40
- @prev_tx_id = prev_tx_id.b
39
+ def initialize(prev_wtxid:, prev_tx_out_index:, unlocking_script: nil, sequence: 0xFFFFFFFF)
40
+ BSV::Primitives::Hex.validate_wtxid!(prev_wtxid, name: 'prev_wtxid')
41
+ @prev_wtxid = prev_wtxid.b
41
42
  @prev_tx_out_index = prev_tx_out_index
42
43
  @unlocking_script = unlocking_script
43
44
  @sequence = sequence
45
+ BSV.logger&.debug { "[TransactionInput] prev_wtxid set: #{dtxid_hex}:#{@prev_tx_out_index}" }
44
46
  end
45
47
 
46
48
  # Serialise the input to its binary wire format.
@@ -48,7 +50,7 @@ module BSV
48
50
  # @return [String] binary input (outpoint + varint + script + sequence)
49
51
  def to_binary
50
52
  script_bytes = @unlocking_script ? @unlocking_script.to_binary : ''.b
51
- @prev_tx_id +
53
+ @prev_wtxid +
52
54
  [@prev_tx_out_index].pack('V') +
53
55
  VarInt.encode(script_bytes.bytesize) +
54
56
  script_bytes +
@@ -66,7 +68,7 @@ module BSV
66
68
  "truncated input: need 36 bytes for outpoint at offset #{offset}, got #{data.bytesize - offset}"
67
69
  end
68
70
 
69
- prev_tx_id = data.byteslice(offset, 32)
71
+ prev_wtxid = data.byteslice(offset, 32)
70
72
  prev_tx_out_index = data.byteslice(offset + 32, 4).unpack1('V')
71
73
  offset += 36
72
74
 
@@ -90,7 +92,7 @@ module BSV
90
92
 
91
93
  total = 36 + vi_size + script_len + 4
92
94
  input = new(
93
- prev_tx_id: prev_tx_id,
95
+ prev_wtxid: prev_wtxid,
94
96
  prev_tx_out_index: prev_tx_out_index,
95
97
  unlocking_script: unlocking_script,
96
98
  sequence: sequence
@@ -98,26 +100,29 @@ module BSV
98
100
  [input, total]
99
101
  end
100
102
 
101
- # Convert a hex transaction ID to internal byte order (reversed).
103
+ # Convert a display-order hex transaction ID to wire-order bytes.
102
104
  #
103
105
  # @param hex [String] hex-encoded transaction ID (display order)
104
- # @return [String] 32-byte transaction ID in internal byte order
105
- def self.txid_from_hex(hex)
106
- [hex].pack('H*').reverse
106
+ # @return [String] 32-byte transaction ID in wire byte order
107
+ def self.wtxid_from_hex(hex)
108
+ BSV::Primitives::Hex.validate_dtxid_hex!(hex, name: 'wtxid_from_hex input')
109
+ wtxid = [hex].pack('H*').reverse
110
+ BSV.logger&.debug { "[TransactionInput] wtxid_from_hex: #{hex} -> #{wtxid.bytesize}B wire-order" }
111
+ wtxid
107
112
  end
108
113
 
109
- # Serialise the outpoint (prev_tx_id + output index) as binary.
114
+ # Serialise the outpoint (prev_wtxid + output index) as binary.
110
115
  #
111
116
  # @return [String] 36-byte outpoint
112
117
  def outpoint_binary
113
- @prev_tx_id + [@prev_tx_out_index].pack('V')
118
+ @prev_wtxid + [@prev_tx_out_index].pack('V')
114
119
  end
115
120
 
116
- # The previous transaction ID in hex (display order).
121
+ # The previous transaction ID in display-order hex.
117
122
  #
118
- # @return [String] hex-encoded transaction ID
119
- def txid_hex
120
- @prev_tx_id.reverse.unpack1('H*')
123
+ # @return [String] hex-encoded transaction ID (display order)
124
+ def dtxid_hex
125
+ @prev_wtxid.reverse.unpack1('H*')
121
126
  end
122
127
  end
123
128
  end
data/lib/bsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.15.0'
4
+ VERSION = '0.17.0'
5
5
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Base error for all wallet operations. Carries a machine-readable code
6
+ # per the BRC-100 error structure.
7
+ class Error < StandardError
8
+ attr_reader :code
9
+
10
+ def initialize(message, code = 1)
11
+ @code = code
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ # Raised when a required parameter is missing or invalid.
17
+ class InvalidParameterError < Error
18
+ attr_reader :parameter
19
+
20
+ def initialize(parameter, must_be = 'valid')
21
+ @parameter = parameter
22
+ super("the #{parameter} parameter must be #{must_be}", 6)
23
+ end
24
+ end
25
+
26
+ # Raised when an HMAC fails to verify.
27
+ class InvalidHmacError < Error
28
+ def initialize(message = 'the provided HMAC is invalid')
29
+ super(message, 3)
30
+ end
31
+ end
32
+
33
+ # Raised when a signature fails to verify.
34
+ class InvalidSignatureError < Error
35
+ def initialize(message = 'the provided signature is invalid')
36
+ super(message, 4)
37
+ end
38
+ end
39
+
40
+ # Raised when an operation is not supported by this wallet implementation.
41
+ class UnsupportedActionError < Error
42
+ def initialize(method_name = 'this method')
43
+ super("#{method_name} is not supported by this wallet implementation", 2)
44
+ end
45
+ end
46
+ end
47
+ end