bsv-sdk 0.23.1 → 0.25.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +1 -1
  4. data/lib/bsv/auth/auth_payload.rb +5 -0
  5. data/lib/bsv/identity/client.rb +9 -5
  6. data/lib/bsv/kv_store/entry.rb +15 -0
  7. data/lib/bsv/kv_store/global.rb +210 -0
  8. data/lib/bsv/kv_store/interpreter.rb +109 -0
  9. data/lib/bsv/kv_store/token.rb +10 -0
  10. data/lib/bsv/kv_store.rb +10 -0
  11. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -2
  12. data/lib/bsv/mcp/tools/decode_tx.rb +1 -1
  13. data/lib/bsv/mcp/tools/fetch_tx.rb +1 -1
  14. data/lib/bsv/mcp/tools/helpers.rb +3 -3
  15. data/lib/bsv/network/protocol.rb +12 -1
  16. data/lib/bsv/network/util.rb +13 -5
  17. data/lib/bsv/overlay/admin_token_template.rb +2 -2
  18. data/lib/bsv/overlay/historian.rb +118 -0
  19. data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
  20. data/lib/bsv/overlay/types.rb +37 -0
  21. data/lib/bsv/overlay.rb +1 -0
  22. data/lib/bsv/primitives/ecies.rb +12 -3
  23. data/lib/bsv/registry/client.rb +54 -7
  24. data/lib/bsv/script/bip276.rb +143 -0
  25. data/lib/bsv/script/interpreter/interpreter.rb +1 -1
  26. data/lib/bsv/script/push_drop_template.rb +2 -2
  27. data/lib/bsv/script.rb +1 -0
  28. data/lib/bsv/storage/downloader.rb +174 -0
  29. data/lib/bsv/storage/errors.rb +8 -0
  30. data/lib/bsv/storage/utils.rb +90 -0
  31. data/lib/bsv/storage.rb +16 -0
  32. data/lib/bsv/transaction/beef.rb +173 -19
  33. data/lib/bsv/transaction/beef_party.rb +119 -0
  34. data/lib/bsv/transaction/chain_tracker.rb +2 -2
  35. data/lib/bsv/transaction/fee_model.rb +1 -1
  36. data/lib/bsv/transaction/fee_models/live_policy.rb +1 -1
  37. data/lib/bsv/transaction/fee_models/satoshis_per_kilobyte.rb +1 -1
  38. data/lib/bsv/transaction/merkle_path.rb +2 -2
  39. data/lib/bsv/transaction/p2pkh.rb +1 -1
  40. data/lib/bsv/transaction/transaction_input.rb +1 -1
  41. data/lib/bsv/transaction/{transaction.rb → tx.rb} +18 -18
  42. data/lib/bsv/transaction/unlocking_script_template.rb +2 -2
  43. data/lib/bsv/transaction.rb +3 -2
  44. data/lib/bsv/version.rb +1 -1
  45. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +13 -0
  46. data/lib/bsv/wallet/proto_wallet.rb +12 -2
  47. data/lib/bsv/wallet/serializer/create_signature.rb +7 -0
  48. data/lib/bsv/wallet/serializer/get_public_key.rb +5 -1
  49. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +6 -3
  50. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +6 -3
  51. data/lib/bsv/wallet/serializer/verify_signature.rb +7 -0
  52. data/lib/bsv-sdk.rb +2 -0
  53. metadata +14 -2
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module BSV
6
+ module Storage
7
+ # Helpers for encoding and decoding UHRP (Unified Hash Resource Protocol) URLs.
8
+ #
9
+ # A UHRP URL is a Base58Check string with a two-byte `\xCE\x00` prefix prepended
10
+ # to the SHA-256 hash of the content. This makes each URL self-verifying and
11
+ # stable across storage providers.
12
+ #
13
+ # == URL normalisation
14
+ #
15
+ # `normalize_url` strips the `uhrp:` scheme (case-insensitive) and the optional
16
+ # `//` authority prefix, matching the TS SDK contract exactly. The `web+uhrp://`
17
+ # variant used by some browser extension manifests is *not* normalised here — that
18
+ # diverges from the TS reference (Python normalises it; we follow TS).
19
+ module Utils
20
+ # Two-byte UHRP version prefix: 0xCE 0x00.
21
+ UHRP_PREFIX = "\xCE\x00".b.freeze
22
+
23
+ module_function
24
+
25
+ # Encode a 32-byte binary SHA-256 hash as a UHRP URL.
26
+ #
27
+ # @param hash [String] 32-byte binary string (SHA-256 hash)
28
+ # @return [String] Base58Check-encoded UHRP URL
29
+ # @raise [ArgumentError] if hash is not exactly 32 bytes
30
+ def get_url_for_hash(hash)
31
+ raise ArgumentError, 'Hash length must be 32 bytes (sha256)' unless hash.bytesize == 32
32
+
33
+ BSV::Primitives::Base58.check_encode(hash.b, prefix: UHRP_PREFIX)
34
+ end
35
+
36
+ # Compute the SHA-256 hash of +data+ and encode it as a UHRP URL.
37
+ #
38
+ # @param data [String] binary file content
39
+ # @return [String] Base58Check-encoded UHRP URL
40
+ def get_url_for_file(data)
41
+ hash = OpenSSL::Digest::SHA256.digest(data.b)
42
+ get_url_for_hash(hash)
43
+ end
44
+
45
+ # Decode a UHRP URL and return the 32-byte binary SHA-256 hash.
46
+ #
47
+ # Accepts URLs with or without the `uhrp:` scheme and optional `//` authority.
48
+ #
49
+ # @param url [String] UHRP URL (with or without `uhrp:` prefix)
50
+ # @return [String] 32-byte binary SHA-256 hash
51
+ # @raise [ArgumentError] if the URL is not a String, empty, has a bad prefix, or has wrong length
52
+ # @raise [BSV::Primitives::Base58::ChecksumError] if the Base58Check checksum fails
53
+ def get_hash_from_url(url)
54
+ raise ArgumentError, 'URL must be a String' unless url.is_a?(String)
55
+ raise ArgumentError, 'URL must not be empty' if url.empty?
56
+
57
+ normalised = normalize_url(url)
58
+ result = BSV::Primitives::Base58.check_decode(normalised, prefix_length: 2)
59
+ raise ArgumentError, 'Bad prefix' unless result[:prefix] == UHRP_PREFIX
60
+ raise ArgumentError, 'Invalid length!' unless result[:data].bytesize == 32
61
+
62
+ result[:data]
63
+ end
64
+
65
+ # Strip the `uhrp:` scheme (case-insensitive) and optional `//` from a URL.
66
+ #
67
+ # Follows the TS SDK contract: only `uhrp:` and `uhrp://` are normalised.
68
+ # `web+uhrp://` is deliberately not stripped (TS does not strip it; Python does).
69
+ #
70
+ # @param url [String] URL to normalise
71
+ # @return [String] normalised URL
72
+ def normalize_url(url)
73
+ url = url.slice(5..) if url.downcase.start_with?('uhrp:')
74
+ url = url.slice(2..) if url.start_with?('//')
75
+ url
76
+ end
77
+
78
+ # Return +true+ if +url+ is a syntactically valid UHRP URL.
79
+ #
80
+ # @param url [String] URL to validate
81
+ # @return [Boolean]
82
+ def valid_url?(url)
83
+ get_hash_from_url(url)
84
+ true
85
+ rescue ArgumentError, BSV::Primitives::Base58::ChecksumError
86
+ false
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ # Storage module for UHRP (Unified Hash Resource Protocol) URL encoding,
5
+ # decoding, and validation helpers.
6
+ #
7
+ # UHRP enables content-addressed storage on BSV: a SHA-256 hash of a file
8
+ # is Base58Check-encoded with the `\xCE\x00` prefix to produce a stable,
9
+ # self-verifying URL.
10
+ module Storage
11
+ autoload :Utils, 'bsv/storage/utils'
12
+ autoload :DownloadResult, 'bsv/storage/downloader'
13
+ autoload :Downloader, 'bsv/storage/downloader'
14
+ autoload :DownloadError, 'bsv/storage/errors'
15
+ end
16
+ end
@@ -72,10 +72,10 @@ module BSV
72
72
 
73
73
  # A BEEF entry containing a raw transaction without a merkle proof.
74
74
  class RawTxEntry < BeefTx
75
- # @return [Transaction] the transaction
75
+ # @return [Transaction::Tx] the transaction
76
76
  attr_reader :transaction
77
77
 
78
- # @param transaction [Transaction] the transaction
78
+ # @param transaction [Transaction::Tx] the transaction
79
79
  # @raise [ArgumentError] if transaction is nil
80
80
  def initialize(transaction:)
81
81
  raise ArgumentError, 'RawTxEntry requires a transaction' if transaction.nil?
@@ -97,13 +97,13 @@ module BSV
97
97
 
98
98
  # A BEEF entry containing a raw transaction with an associated BUMP index.
99
99
  class ProvenTxEntry < BeefTx
100
- # @return [Transaction] the transaction
100
+ # @return [Transaction::Tx] the transaction
101
101
  attr_reader :transaction
102
102
 
103
103
  # @return [Integer] index into the BEEF bumps array
104
104
  attr_reader :bump_index
105
105
 
106
- # @param transaction [Transaction] the transaction
106
+ # @param transaction [Transaction::Tx] the transaction
107
107
  # @param bump_index [Integer] index into the bumps array
108
108
  # @raise [ArgumentError] if transaction or bump_index is nil
109
109
  def initialize(transaction:, bump_index:)
@@ -188,7 +188,7 @@ module BSV
188
188
  # After parsing, input source transactions are wired automatically.
189
189
  #
190
190
  # @param data [String] raw BEEF binary
191
- # @return [Beef] the parsed BEEF bundle
191
+ # @return [Transaction::Beef] the parsed BEEF bundle
192
192
  def self.from_binary(data)
193
193
  raise ArgumentError, "truncated BEEF: need at least 4 bytes for version, got #{data.bytesize}" if data.bytesize < 4
194
194
 
@@ -247,7 +247,7 @@ module BSV
247
247
  # Deserialise a BEEF bundle from a hex string.
248
248
  #
249
249
  # @param hex [String] hex-encoded BEEF data
250
- # @return [Beef] the parsed BEEF bundle
250
+ # @return [Transaction::Beef] the parsed BEEF bundle
251
251
  def self.from_hex(hex)
252
252
  from_binary(BSV::Primitives::Hex.decode(hex, name: 'BEEF hex'))
253
253
  end
@@ -320,7 +320,7 @@ module BSV
320
320
  # Find a transaction in the bundle by its wire-order transaction ID.
321
321
  #
322
322
  # @param wtxid [String] 32-byte wire-order wtxid
323
- # @return [Transaction, nil] the matching transaction, or nil
323
+ # @return [Transaction::Tx, nil] the matching transaction, or nil
324
324
  def find_transaction(wtxid)
325
325
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
326
326
  BSV.logger&.debug { "[Beef] find_transaction: #{wtxid.reverse.unpack1('H*')} in #{@transactions.length} entries" }
@@ -353,7 +353,7 @@ module BSV
353
353
  # Find a transaction with all source_transactions wired for signing.
354
354
  #
355
355
  # @param wtxid [String] 32-byte wire-order wtxid
356
- # @return [Transaction, nil] the transaction with wired inputs, or nil
356
+ # @return [Transaction::Tx, nil] the transaction with wired inputs, or nil
357
357
  def find_transaction_for_signing(wtxid)
358
358
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
359
359
  tx = find_transaction(wtxid)
@@ -367,7 +367,7 @@ module BSV
367
367
  # and merkle paths) for atomic proof validation.
368
368
  #
369
369
  # @param wtxid [String] 32-byte wire-order wtxid
370
- # @return [Transaction, nil] the transaction with full proof tree, or nil
370
+ # @return [Transaction::Tx, nil] the transaction with full proof tree, or nil
371
371
  def find_atomic_transaction(wtxid)
372
372
  BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
373
373
  tx = find_transaction(wtxid)
@@ -385,6 +385,161 @@ module BSV
385
385
  to_atomic_binary(subject_wtxid).unpack1('H*')
386
386
  end
387
387
 
388
+ # --- Supporting methods for multi-party BEEF exchange ---
389
+
390
+ # Add a TXID-only entry for +wtxid+ if no entry exists yet.
391
+ #
392
+ # If an entry already exists (in any format), the call is a no-op —
393
+ # TXID-only is the weakest format, so an existing stronger entry is kept.
394
+ #
395
+ # @param wtxid [String] 32-byte wire-order binary txid
396
+ # @return [BeefTx] the existing or newly added entry
397
+ def merge_txid_only(wtxid)
398
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
399
+ existing = @transactions.find { |bt| bt.wtxid == wtxid }
400
+ return existing if existing
401
+
402
+ entry = TxidOnlyEntry.new(known_wtxid: wtxid)
403
+ @transactions << entry
404
+ entry
405
+ end
406
+
407
+ # Return a shallow copy of this Transaction::Beef.
408
+ #
409
+ # Both +@bumps+ and +@transactions+ arrays are duplicated (new arrays),
410
+ # but the +BeefTx+ and +MerklePath+ objects they contain are shared
411
+ # references. This mirrors the TS SDK's +clone+ contract: entries are
412
+ # effectively immutable once added to a bundle, so shallow semantics are
413
+ # correct. If a deeper copy is ever required, add a separate +deep_dup+
414
+ # rather than changing this method's contract.
415
+ #
416
+ # +@subject_wtxid+ (Atomic BEEF) and +@txs_not_valid+ (cyclic-graph
417
+ # metadata) are preserved on the copy.
418
+ #
419
+ # @return [Transaction::Beef] a new shallow copy
420
+ def clone
421
+ # Use super (Object#clone) rather than self.class.new so subclasses
422
+ # (e.g. Transaction::BeefParty) with different initialize signatures
423
+ # work correctly. Object#clone does a shallow ivar copy without
424
+ # invoking initialize; we then dup the two arrays so the copy's
425
+ # contents can mutate independently.
426
+ c = super
427
+ c.instance_variable_set(:@bumps, @bumps.dup)
428
+ c.instance_variable_set(:@transactions, @transactions.dup)
429
+ c.instance_variable_set(:@txs_not_valid, @txs_not_valid&.dup)
430
+ c
431
+ end
432
+
433
+ # Return a new Transaction::Beef with TXID-only entries removed for any
434
+ # wtxid in +known_wtxids+.
435
+ #
436
+ # RAW_TX and RAW_TX_AND_BUMP entries are always retained, even when their
437
+ # wtxid appears in +known_wtxids+. Only +TxidOnlyEntry+ records are
438
+ # candidates for removal (they carry no proof data the recipient needs).
439
+ #
440
+ # After dropping TXID-only entries, any BUMP that is no longer referenced
441
+ # by any remaining +ProvenTxEntry+ is removed, and all +bump_index+
442
+ # fields are renumbered to match the new bumps array (mirrors TS
443
+ # +trimKnownTxids+ at +Beef.ts:861-914+).
444
+ #
445
+ # Does not mutate +self+. Starts from a {#clone} so the caller's state
446
+ # is preserved.
447
+ #
448
+ # @param known_wtxids [Array<String>] binary wtxids the recipient already has
449
+ # @return [Transaction::Beef] a new bundle with the specified entries trimmed
450
+ def trim_known_wtxids(known_wtxids)
451
+ known_set = known_wtxids.to_set
452
+
453
+ trimmed = clone
454
+ trimmed.instance_variable_set(
455
+ :@transactions,
456
+ trimmed.transactions.reject { |bt| bt.is_a?(TxidOnlyEntry) && known_set.include?(bt.wtxid) }
457
+ )
458
+
459
+ # Find which bump indices are still referenced
460
+ referenced = Set.new
461
+ trimmed.transactions.each do |bt|
462
+ referenced.add(bt.bump_index) if bt.is_a?(ProvenTxEntry)
463
+ end
464
+
465
+ # If all bumps are still referenced, nothing more to do
466
+ return trimmed if referenced.size == trimmed.bumps.length
467
+
468
+ # Build old → new index map for surviving bumps
469
+ index_map = {}
470
+ new_idx = 0
471
+ trimmed.bumps.each_with_index do |_, old_idx|
472
+ if referenced.include?(old_idx)
473
+ index_map[old_idx] = new_idx
474
+ new_idx += 1
475
+ end
476
+ end
477
+
478
+ # Drop unreferenced bumps
479
+ trimmed.instance_variable_set(
480
+ :@bumps,
481
+ trimmed.bumps.each_with_index.filter_map { |bump, i| bump if referenced.include?(i) }
482
+ )
483
+
484
+ # Renumber bump_index on all ProvenTxEntry records
485
+ trimmed.instance_variable_set(
486
+ :@transactions,
487
+ trimmed.transactions.map do |bt|
488
+ next bt unless bt.is_a?(ProvenTxEntry)
489
+
490
+ new_bump_idx = index_map.fetch(bt.bump_index)
491
+ ProvenTxEntry.new(transaction: bt.transaction, bump_index: new_bump_idx).tap do |e|
492
+ e.transaction.merkle_path = trimmed.bumps[new_bump_idx]
493
+ end
494
+ end
495
+ )
496
+
497
+ trimmed
498
+ end
499
+
500
+ # Return the wtxids of transactions that are "valid" in this bundle.
501
+ #
502
+ # A transaction is valid when it either:
503
+ # - has a merkle proof (is a +ProvenTxEntry+), or
504
+ # - all of its inputs chain back to proven transactions within the bundle.
505
+ #
506
+ # TXID-only entries are excluded. Cyclic transactions (from
507
+ # +@txs_not_valid+) are also excluded.
508
+ #
509
+ # Used by {Transaction::BeefParty} to record which wtxids a party gains
510
+ # knowledge of after a {#merge_beef_from_party} call.
511
+ #
512
+ # @return [Array<String>] binary wire-order wtxids
513
+ def valid_wtxids
514
+ invalid = @txs_not_valid || Set.new
515
+ known = Set.new
516
+
517
+ # Seed with proven entries (excluding any marked cyclic/unsortable)
518
+ @transactions.each do |bt|
519
+ known.add(bt.wtxid) if bt.is_a?(ProvenTxEntry) && !invalid.include?(bt.wtxid)
520
+ end
521
+
522
+ # Iteratively resolve unproven entries whose inputs are all known.
523
+ # Skip @txs_not_valid entries — by definition they can't be ordered
524
+ # for validation, so a counterparty receiving them can't validate either.
525
+ changed = true
526
+ while changed
527
+ changed = false
528
+ @transactions.each do |bt|
529
+ next if bt.is_a?(TxidOnlyEntry) || known.include?(bt.wtxid)
530
+ next if invalid.include?(bt.wtxid)
531
+ next unless bt.respond_to?(:transaction)
532
+
533
+ if bt.transaction.inputs.all? { |inp| known.include?(inp.prev_wtxid) }
534
+ known.add(bt.wtxid)
535
+ changed = true
536
+ end
537
+ end
538
+ end
539
+
540
+ known.to_a
541
+ end
542
+
388
543
  # --- Merge operations ---
389
544
 
390
545
  # Add or deduplicate a merkle path (BUMP) in this BEEF bundle.
@@ -440,7 +595,7 @@ module BSV
440
595
  # (same txid) are upgraded if a stronger format is now available (F5.7):
441
596
  # TXID_ONLY → RAW_TX or RAW_TX_AND_BUMP; RAW_TX → RAW_TX_AND_BUMP.
442
597
  #
443
- # @param tx [Transaction] the transaction to merge
598
+ # @param tx [Transaction::Tx] the transaction to merge
444
599
  # @return [BeefTx] the (possibly existing or upgraded) BeefTx entry
445
600
  def merge_transaction(tx)
446
601
  wtxid = tx.wtxid
@@ -479,7 +634,7 @@ module BSV
479
634
  # @param bump_index [Integer, nil] optional BUMP index
480
635
  # @return [BeefTx] the new or upgraded BeefTx entry
481
636
  def merge_raw_tx(raw_bytes, bump_index: nil)
482
- tx = Transaction.from_binary(raw_bytes)
637
+ tx = Tx.from_binary(raw_bytes)
483
638
 
484
639
  if bump_index
485
640
  unless bump_index.is_a?(Integer) && bump_index >= 0 && bump_index < @bumps.length
@@ -512,7 +667,7 @@ module BSV
512
667
  # BUMP indices are remapped during merge. New BeefTx instances are
513
668
  # constructed rather than sharing references with the source bundle (F5.9).
514
669
  #
515
- # @param other [Beef] the BEEF bundle to merge from
670
+ # @param other [Transaction::Beef] the BEEF bundle to merge from
516
671
  # @return [self]
517
672
  # @raise [ArgumentError] if a transaction in +other+ has a +bump_index+
518
673
  # that does not point to any BUMP in +other.bumps+ (i.e. the source
@@ -740,7 +895,7 @@ module BSV
740
895
  case format
741
896
  when FORMAT_TXID_ONLY
742
897
  # Wire stores txid in internal (little-endian / wire) byte order;
743
- # store as-is in known_wtxid so it matches Transaction#wtxid.
898
+ # store as-is in known_wtxid so it matches Tx#wtxid.
744
899
  raise ArgumentError, 'truncated BEEF: not enough bytes for TXID_ONLY entry' if offset + 32 > data.bytesize
745
900
 
746
901
  known_wtxid = data.byteslice(offset, 32)
@@ -749,12 +904,12 @@ module BSV
749
904
  when FORMAT_RAW_TX_AND_BUMP
750
905
  bump_index, vi_size = VarInt.decode(data, offset)
751
906
  offset += vi_size
752
- tx, consumed = Transaction.from_binary_with_offset(data, offset)
907
+ tx, consumed = Tx.from_binary_with_offset(data, offset)
753
908
  offset += consumed
754
909
  tx.merkle_path = beef.bumps[bump_index] if bump_index < beef.bumps.length
755
910
  beef.transactions << ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
756
911
  when FORMAT_RAW_TX
757
- tx, consumed = Transaction.from_binary_with_offset(data, offset)
912
+ tx, consumed = Tx.from_binary_with_offset(data, offset)
758
913
  offset += consumed
759
914
  beef.transactions << RawTxEntry.new(transaction: tx)
760
915
  end
@@ -768,7 +923,7 @@ module BSV
768
923
  offset += vi_size
769
924
 
770
925
  num_txs.times do
771
- tx, consumed = Transaction.from_binary_with_offset(data, offset)
926
+ tx, consumed = Tx.from_binary_with_offset(data, offset)
772
927
  offset += consumed
773
928
 
774
929
  has_bump = data.getbyte(offset)
@@ -800,8 +955,7 @@ module BSV
800
955
 
801
956
  input.source_transaction = source
802
957
  BSV.logger&.debug do
803
- "[Beef] wired input #{input.prev_wtxid.reverse.unpack1('H*')}:#{input.prev_tx_out_index} " \
804
- "-> source #{source.wtxid.reverse.unpack1('H*')}"
958
+ "[Beef] wired input #{input.dtxid_hex}:#{input.prev_tx_out_index} -> source #{source.dtxid}"
805
959
  end
806
960
  end
807
961
 
@@ -821,7 +975,7 @@ module BSV
821
975
  # RAW_TX → RAW_TX_AND_BUMP (if bump now available)
822
976
  #
823
977
  # @param existing [BeefTx] the current entry in @transactions
824
- # @param tx [Transaction, nil] the raw transaction (may be nil for TXID_ONLY → TXID_ONLY)
978
+ # @param tx [Transaction::Tx, nil] the raw transaction (may be nil for TXID_ONLY → TXID_ONLY)
825
979
  # @param bump_index [Integer, nil] a BUMP index already validated against @bumps
826
980
  # @return [BeefTx, nil] the upgraded entry, or nil when no upgrade is needed
827
981
  def upgrade_beef_tx(existing, tx, bump_index: nil)
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Transaction
5
+ # Transaction::Beef subclass that tracks per-party knowledge of wtxids.
6
+ #
7
+ # Used in multi-party BEEF exchange to avoid re-transmitting transaction
8
+ # data and proofs that a counterparty already possesses. Each party is
9
+ # identified by a caller-supplied string and associated with the set of
10
+ # wtxids it is known to hold.
11
+ #
12
+ # @example Two-party exchange
13
+ # party = BSV::Transaction::BeefParty.new(['alice', 'bob'])
14
+ # party.merge_beef_from_party('alice', alice_beef)
15
+ # trimmed = party.trimmed_beef_for_party('bob') # plain Transaction::Beef
16
+ class BeefParty < Beef
17
+ # @param initial_parties [Array<String>] optional initial party identifiers
18
+ # @raise [ArgumentError] if +initial_parties+ contains duplicates
19
+ #
20
+ # Defaults to BEEF_V2 (BRC-96) because TXID-only entries — which are
21
+ # central to BeefParty's purpose — are only valid in V2. Matches the TS
22
+ # SDK's +new Beef()+ default.
23
+ def initialize(initial_parties = [])
24
+ super(version: BEEF_V2)
25
+ @party_knowledge = {}
26
+ initial_parties.each { |p| add_party(p) }
27
+ end
28
+
29
+ # @param party_id [String]
30
+ # @return [Boolean] true if +party_id+ has been added
31
+ def party?(party_id)
32
+ @party_knowledge.key?(party_id)
33
+ end
34
+
35
+ # Add a new unique party identifier.
36
+ #
37
+ # @param party_id [String]
38
+ # @raise [ArgumentError] if +party_id+ is already known
39
+ def add_party(party_id)
40
+ raise ArgumentError, "duplicate party #{party_id}" if party?(party_id)
41
+
42
+ @party_knowledge[party_id] = Set.new
43
+ end
44
+
45
+ # Record additional wtxids as known to +party_id+.
46
+ #
47
+ # Auto-creates the party if it is not yet known (TS parity). Also
48
+ # merges a TXID-only entry into the underlying bundle for each wtxid.
49
+ #
50
+ # @param party_id [String] party identifier (auto-created if unknown)
51
+ # @param wtxids [Array<String>] 32-byte wire-order binary wtxids
52
+ # @raise [ArgumentError] if any element of +wtxids+ is not a valid wtxid
53
+ def add_known_wtxids_for_party(party_id, wtxids)
54
+ add_party(party_id) unless party?(party_id)
55
+ wtxids.each do |wtxid|
56
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
57
+ @party_knowledge[party_id].add(wtxid)
58
+ merge_txid_only(wtxid)
59
+ end
60
+ end
61
+
62
+ # @param party_id [String]
63
+ # @return [Array<String>] 32-byte wire-order binary wtxids known to +party_id+
64
+ # @raise [ArgumentError] if +party_id+ is unknown
65
+ def known_wtxids_for_party(party_id)
66
+ raise ArgumentError, "unknown party #{party_id}" unless party?(party_id)
67
+
68
+ @party_knowledge[party_id].to_a
69
+ end
70
+
71
+ # Merge a Transaction::Beef received from +party_id+.
72
+ #
73
+ # Merges all transactions and BUMPs from +beef_or_binary+ into +self+,
74
+ # then records the valid wtxids of the merged bundle as known to
75
+ # +party_id+. Auto-creates the party if not yet known.
76
+ #
77
+ # @param party_id [String] party identifier
78
+ # @param beef_or_binary [Transaction::Beef, String] a Transaction::Beef
79
+ # instance or raw binary BEEF bytes
80
+ def merge_beef_from_party(party_id, beef_or_binary)
81
+ beef = beef_or_binary.is_a?(Beef) ? beef_or_binary : Beef.from_binary(beef_or_binary)
82
+ # Capture the set of wtxids the party actually sent before merging.
83
+ beef_wtxids = beef.transactions.map(&:wtxid)
84
+ merge(beef)
85
+ # Record knowledge of any tx the party sent that is now provably valid
86
+ # against the merged state. This captures the cross-bundle case where
87
+ # an unproven tx in +beef+ becomes valid via inputs already proven in
88
+ # +self+ (and conversely doesn't record knowledge the party never sent).
89
+ known = valid_wtxids & beef_wtxids
90
+ add_known_wtxids_for_party(party_id, known)
91
+ end
92
+
93
+ # Return a new Transaction::Beef (not Transaction::BeefParty) with
94
+ # TXID-only entries that are known to +party_id+ removed.
95
+ #
96
+ # RAW_TX and RAW_TX_AND_BUMP entries are always retained even when the
97
+ # party knows the txid — those formats carry proof data. BUMP indices
98
+ # are renumbered if any unreferenced bumps are dropped.
99
+ #
100
+ # Does not mutate +self+.
101
+ #
102
+ # @param party_id [String]
103
+ # @return [Transaction::Beef] a new trimmed bundle (plain Beef, not BeefParty)
104
+ # @raise [ArgumentError] if +party_id+ is unknown
105
+ def trimmed_beef_for_party(party_id)
106
+ raise ArgumentError, "unknown party #{party_id}" unless party?(party_id)
107
+
108
+ known = @party_knowledge[party_id].to_a
109
+
110
+ # Build a plain Beef from our current state so trim_known_wtxids
111
+ # returns a Beef, not a BeefParty.
112
+ plain = Beef.new(version: @version, bumps: @bumps.dup, transactions: @transactions.dup)
113
+ plain.instance_variable_set(:@subject_wtxid, @subject_wtxid)
114
+ plain.instance_variable_set(:@txs_not_valid, @txs_not_valid&.dup)
115
+ plain.trim_known_wtxids(known)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -4,7 +4,7 @@ module BSV
4
4
  module Transaction
5
5
  # Duck type for block header lookups used by the SDK's verify methods.
6
6
  #
7
- # {Beef#verify}, {MerklePath#verify}, and {Transaction#verify} define
7
+ # {Beef#verify}, {MerklePath#verify}, and {Tx#verify} define
8
8
  # what a valid structure *is* — they walk trees, check proofs, and
9
9
  # compare roots. But they have no data source of their own. The
10
10
  # chain tracker is the data source: an object the consumer provides
@@ -56,7 +56,7 @@ module BSV
56
56
  # Return a default ChainTracker backed by the GorillaPool provider.
57
57
  #
58
58
  # @param testnet [Boolean] when true, uses the testnet provider
59
- # @return [ChainTracker]
59
+ # @return [Transaction::ChainTracker]
60
60
  def self.default(testnet: false)
61
61
  new(BSV::Network::Providers::GorillaPool.default(testnet: testnet))
62
62
  end
@@ -17,7 +17,7 @@ module BSV
17
17
  class FeeModel
18
18
  # Compute the fee for a transaction.
19
19
  #
20
- # @param transaction [Transaction] the transaction to compute the fee for
20
+ # @param transaction [Transaction::Tx] the transaction to compute the fee for
21
21
  # @return [Integer] the fee in satoshis
22
22
  # @raise [NotImplementedError] if not overridden by a subclass
23
23
  def compute_fee(_transaction)
@@ -66,7 +66,7 @@ module BSV
66
66
 
67
67
  # Compute the fee for a transaction using the latest ARC rate.
68
68
  #
69
- # @param transaction [Transaction] the transaction to compute the fee for
69
+ # @param transaction [Transaction::Tx] the transaction to compute the fee for
70
70
  # @return [Integer] the fee in satoshis
71
71
  def compute_fee(transaction)
72
72
  rate = current_rate
@@ -23,7 +23,7 @@ module BSV
23
23
 
24
24
  # Compute the fee for a transaction based on its estimated size.
25
25
  #
26
- # @param transaction [Transaction] the transaction to compute the fee for
26
+ # @param transaction [Transaction::Tx] the transaction to compute the fee for
27
27
  # @return [Integer] the fee in satoshis
28
28
  def compute_fee(transaction)
29
29
  size = transaction.estimated_size
@@ -322,7 +322,7 @@ module BSV
322
322
  # logic is: reject when `current_height - block_height < 100` (immature).
323
323
  #
324
324
  # @param dtxid_hex [String] hex-encoded transaction ID (display order)
325
- # @param chain_tracker [ChainTracker] chain tracker to verify the root against
325
+ # @param chain_tracker [Transaction::ChainTracker] chain tracker to verify the root against
326
326
  # @return [Boolean] true if the computed root matches the block at this height
327
327
  def verify(dtxid_hex, chain_tracker)
328
328
  BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
@@ -450,7 +450,7 @@ module BSV
450
450
  # that the extracted path computes the same merkle root as the
451
451
  # source.
452
452
  #
453
- # The primary use case is +Transaction#to_beef+: when a BUMP loaded
453
+ # The primary use case is +Tx#to_beef+: when a BUMP loaded
454
454
  # from a proof store carries +txid: true+ flags for transactions
455
455
  # that are not part of the current BEEF bundle, extracting only the
456
456
  # bundled txids strips the phantom flags (and the now-unneeded
@@ -24,7 +24,7 @@ module BSV
24
24
 
25
25
  # Generate the P2PKH unlocking script for the given input.
26
26
  #
27
- # @param tx [Transaction] the transaction being signed
27
+ # @param tx [Transaction::Tx] the transaction being signed
28
28
  # @param input_index [Integer] the input index to sign
29
29
  # @return [Script::Script] the unlocking script (signature + pubkey)
30
30
  def sign(tx, input_index)
@@ -26,7 +26,7 @@ module BSV
26
26
  # @return [Script::Script, nil] locking script of the source output (needed for sighash)
27
27
  attr_accessor :source_locking_script
28
28
 
29
- # @return [Transaction, nil] the full source transaction (for BEEF wiring)
29
+ # @return [Transaction::Tx, nil] the full source transaction (for BEEF wiring)
30
30
  attr_accessor :source_transaction
31
31
 
32
32
  # @return [UnlockingScriptTemplate, nil] template for deferred signing