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
@@ -49,8 +49,17 @@ module BSV
49
49
 
50
50
  def self.call(txid:, network: nil, server_context: nil)
51
51
  net_sym = Helpers.resolve_network_sym(network, server_context)
52
- woc = BSV::Network::WhatsOnChain.new(network: net_sym)
53
- tx = woc.fetch_transaction(txid)
52
+ provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
53
+ fetch_result = provider.call(:get_tx, txid)
54
+
55
+ unless fetch_result.success?
56
+ code = fetch_result.metadata[:status_code]
57
+ msg = fetch_result.message
58
+ msg = "#{msg} (HTTP #{code})" if code
59
+ return Helpers.error_response(msg)
60
+ end
61
+
62
+ tx = BSV::Transaction::Transaction.from_hex(fetch_result.data)
54
63
 
55
64
  result = {
56
65
  hex: tx.to_hex,
@@ -61,8 +70,6 @@ module BSV
61
70
  [::MCP::Content::Text.new(result.to_json)],
62
71
  structured_content: result
63
72
  )
64
- rescue BSV::Network::ChainProviderError => e
65
- Helpers.error_response("#{e.message} (HTTP #{e.status_code})")
66
73
  rescue ArgumentError => e
67
74
  Helpers.error_response(e.message)
68
75
  end
@@ -50,8 +50,22 @@ module BSV
50
50
 
51
51
  def self.call(address:, network: nil, server_context: nil)
52
52
  net_sym = Helpers.resolve_network_sym(network, server_context)
53
- woc = BSV::Network::WhatsOnChain.new(network: net_sym)
54
- utxos = woc.fetch_utxos(address)
53
+ provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
54
+ utxo_result = provider.call(:get_utxos_all, address)
55
+
56
+ unless utxo_result.success?
57
+ code = utxo_result.metadata[:status_code]
58
+ msg = utxo_result.message
59
+ msg = "#{msg} (HTTP #{code})" if code
60
+ return Helpers.error_response(msg)
61
+ end
62
+
63
+ utxos = utxo_result.data.map do |entry|
64
+ BSV::Network::UTXO.new(
65
+ tx_hash: entry[:tx_hash], tx_pos: entry[:tx_pos],
66
+ satoshis: entry[:satoshis], height: entry[:height]
67
+ )
68
+ end
55
69
 
56
70
  result = {
57
71
  address: address,
@@ -63,8 +77,6 @@ module BSV
63
77
  [::MCP::Content::Text.new(result.to_json)],
64
78
  structured_content: result
65
79
  )
66
- rescue BSV::Network::ChainProviderError => e
67
- Helpers.error_response("#{e.message} (HTTP #{e.status_code})")
68
80
  end
69
81
  end
70
82
  end
@@ -25,7 +25,7 @@ module BSV
25
25
  # @return [Hash]
26
26
  def self.transaction_to_h(tx)
27
27
  {
28
- txid: tx.txid_hex,
28
+ txid: tx.txid_hex, # MCP tool boundary: display-order hex for human consumption
29
29
  version: tx.version,
30
30
  lock_time: tx.lock_time,
31
31
  inputs: tx.inputs.each_with_index.map { |inp, i| input_to_h(inp, i) },
@@ -38,7 +38,7 @@ module BSV
38
38
  def self.input_to_h(input, _index)
39
39
  unlock_script = input.unlocking_script
40
40
  {
41
- prev_txid: input.prev_tx_id.reverse.unpack1('H*'),
41
+ prev_txid: input.dtxid_hex,
42
42
  vout: input.prev_tx_out_index,
43
43
  script_hex: unlock_script ? unlock_script.to_hex : '',
44
44
  script_asm: unlock_script ? unlock_script.to_asm : '',
@@ -2,164 +2,24 @@
2
2
 
3
3
  module BSV
4
4
  module Network
5
- # ARC broadcaster for submitting transactions to the BSV network.
6
- #
7
- # This facade preserves the legacy public API contract while delegating all
8
- # HTTP logic to +Protocols::ARC+. It translates +Result+ objects returned by
9
- # the protocol into +BroadcastResponse+ instances or raised +BroadcastError+
10
- # exceptions as required by consumer code.
11
- #
12
- # Any object responding to #broadcast(tx) can serve as a broadcaster;
13
- # this class implements that contract using the ARC API.
5
+ # @deprecated Use {BSV::Network::Protocols::ARC} directly instead.
6
+ # The facade converted clean Result objects into exceptions — every
7
+ # consumer immediately caught them and converted back to data.
8
+ # Use the protocol layer, which returns Result objects natively.
14
9
  class ARC
15
- # Returns an ARC instance pointed at the GorillaPool public ARC endpoint.
16
- #
17
- # @param testnet [Boolean] when true, uses the GorillaPool testnet endpoint
18
- # @param api_key [String, nil] optional bearer token for Authorization
19
- # @param http_client [#request, nil] injectable HTTP client for testing
20
- # @param opts [Hash] ARC-specific options forwarded to the protocol
21
- # (e.g. +deployment_id:+, +callback_url:+, +callback_token:+)
22
- # @return [ARC]
23
- def self.default(testnet: false, api_key: nil, http_client: nil, **opts)
24
- provider = Providers::GorillaPool.default(testnet: testnet)
25
- arc_protocol = provider.protocol_for(:broadcast)
26
- base_url = arc_protocol.base_url
27
- new(
28
- base_url,
29
- api_key: api_key,
30
- http_client: http_client,
31
- **opts
32
- )
33
- end
34
-
35
- # ARC response statuses that indicate the transaction was NOT accepted.
36
- REJECTED_STATUSES = %w[
37
- REJECTED
38
- DOUBLE_SPEND_ATTEMPTED
39
- INVALID
40
- MALFORMED
41
- MINED_IN_STALE_BLOCK
42
- ].freeze
43
-
44
- # @param url_or_protocol [String, Protocols::ARC] ARC base URL (without
45
- # trailing slash) or a pre-configured +Protocols::ARC+ instance. When a
46
- # URL string is supplied, the remaining keyword arguments are forwarded to
47
- # the underlying protocol constructor. When a protocol instance is supplied,
48
- # all keyword arguments are ignored.
49
- # @param api_key [String, nil] optional bearer token for Authorization
50
- # @param deployment_id [String, nil] optional deployment identifier for
51
- # the +XDeployment-ID+ header; defaults to a per-instance random value
52
- # @param callback_url [String, nil] optional +X-CallbackUrl+ for ARC
53
- # status callbacks
54
- # @param callback_token [String, nil] optional +X-CallbackToken+ for
55
- # ARC status callback authentication
56
- # @param http_client [#request, nil] injectable HTTP client for testing
57
- def initialize(url_or_protocol, api_key: nil, deployment_id: nil, callback_url: nil,
58
- callback_token: nil, http_client: nil)
59
- @protocol =
60
- if url_or_protocol.is_a?(String)
61
- Protocols::ARC.new(
62
- base_url: url_or_protocol,
63
- api_key: api_key,
64
- deployment_id: deployment_id,
65
- callback_url: callback_url,
66
- callback_token: callback_token,
67
- http_client: http_client
68
- )
69
- else
70
- url_or_protocol
71
- end
72
- end
73
-
74
- # Submit a transaction to ARC.
75
- #
76
- # @param tx [Transaction] the transaction to broadcast
77
- # @param wait_for [String, nil] ARC wait condition
78
- # @param skip_fee_validation [Boolean, nil] when truthy, sends X-SkipFeeValidation header
79
- # @param skip_script_validation [Boolean, nil] when truthy, sends X-SkipScriptValidation header
80
- # @return [BroadcastResponse]
81
- # @raise [BroadcastError] when ARC returns a non-2xx HTTP status or a
82
- # rejected/orphan +txStatus+
83
- def broadcast(tx, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
84
- result = @protocol.call(
85
- :broadcast, tx,
86
- wait_for: wait_for,
87
- skip_fee_validation: skip_fee_validation,
88
- skip_script_validation: skip_script_validation
89
- )
90
- result_to_response!(result)
91
- end
92
-
93
- # Submit multiple transactions to ARC in a single batch request.
94
- #
95
- # Returns a mixed array of {BroadcastResponse} and {BroadcastError} objects.
96
- # Per-transaction rejections are returned as {BroadcastError} values rather
97
- # than raised. Only HTTP-level errors raise a {BroadcastError} for the whole batch.
98
- #
99
- # @param txs [Array<Transaction>] transactions to broadcast
100
- # @param wait_for [String, nil] ARC wait condition
101
- # @param skip_fee_validation [Boolean, nil]
102
- # @param skip_script_validation [Boolean, nil]
103
- # @return [Array<BroadcastResponse, BroadcastError>]
104
- # @raise [BroadcastError] when ARC returns a non-2xx HTTP status
105
- def broadcast_many(txs, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
106
- result = @protocol.call(
107
- :broadcast_many, txs,
108
- wait_for: wait_for,
109
- skip_fee_validation: skip_fee_validation,
110
- skip_script_validation: skip_script_validation
111
- )
10
+ # Raised when deprecated facade classes are instantiated.
11
+ class DeprecationError < StandardError; end
112
12
 
113
- case result
114
- when Result::Success
115
- result.data.map { |item| item_to_response_or_error(item) }
116
- else
117
- raise broadcast_error_from_result(result)
118
- end
119
- end
120
-
121
- # Query the status of a previously submitted transaction.
122
- #
123
- # @param txid [String] transaction ID to query
124
- # @return [BroadcastResponse]
125
- # @raise [BroadcastError] on failure
126
- def status(txid)
127
- result = @protocol.call(:get_tx_status, txid)
128
- result_to_response!(result)
129
- end
130
-
131
- private
132
-
133
- # Translate a single-tx Result into a BroadcastResponse or raise BroadcastError.
134
- def result_to_response!(result)
135
- case result
136
- when Result::Success
137
- BroadcastResponse.new(result.data)
138
- else
139
- raise broadcast_error_from_result(result)
140
- end
141
- end
13
+ MESSAGE = 'BSV::Network::ARC is deprecated. ' \
14
+ 'Use BSV::Network::Protocols::ARC directly — it returns Result objects ' \
15
+ 'instead of raising exceptions. See BSV::Network::Protocols::ARC for usage.'
142
16
 
143
- # Translate a per-item batch Result into a BroadcastResponse or BroadcastError
144
- # (returned, not raised).
145
- def item_to_response_or_error(item)
146
- case item
147
- when Result::Success
148
- BroadcastResponse.new(item.data)
149
- else
150
- broadcast_error_from_result(item)
151
- end
17
+ def self.default(**)
18
+ raise DeprecationError, MESSAGE
152
19
  end
153
20
 
154
- # Build a BroadcastError from a Result::Error or Result::NotFound.
155
- def broadcast_error_from_result(result)
156
- meta = result.metadata || {}
157
- BroadcastError.new(
158
- result.message,
159
- status_code: meta[:status_code],
160
- txid: meta[:txid],
161
- arc_status: meta[:arc_status]
162
- )
21
+ def initialize(*)
22
+ raise DeprecationError, MESSAGE
163
23
  end
164
24
  end
165
25
  end
@@ -3,6 +3,7 @@
3
3
  module BSV
4
4
  module Network
5
5
  class BroadcastError < StandardError
6
+ # ARC API boundary: display-order hex txid as returned by the ARC error response.
6
7
  attr_reader :status_code, :txid, :arc_status
7
8
 
8
9
  def initialize(message, status_code: nil, txid: nil, arc_status: nil)
@@ -3,6 +3,7 @@
3
3
  module BSV
4
4
  module Network
5
5
  class BroadcastResponse
6
+ # ARC API boundary: display-order hex txid as returned by the ARC broadcast endpoint.
6
7
  attr_reader :txid, :tx_status, :message, :extra_info, :block_hash, :block_height, :timestamp, :competing_txs
7
8
 
8
9
  def initialize(attrs = {})
@@ -138,7 +138,8 @@ module BSV
138
138
  # lacks source_satoshis / source_locking_script.
139
139
  def ef_hex_with_fallback(tx)
140
140
  tx.to_ef_hex
141
- rescue ArgumentError
141
+ rescue ArgumentError => e
142
+ BSV.logger&.debug { "[ARC] EF serialisation failed: #{e.message} — falling back to raw hex" }
142
143
  tx.to_hex
143
144
  end
144
145
 
@@ -269,7 +270,7 @@ module BSV
269
270
  # same field set as broadcast responses rather than the raw parsed JSON.
270
271
  # Also checks for rejection status and missing txid (malformed 2xx).
271
272
  #
272
- # @param txid [String] the transaction ID to query
273
+ # @param txid [String] ARC API boundary: display-order hex transaction ID to query
273
274
  # @return [Result::Success, Result::Error, Result::NotFound]
274
275
  def call_get_tx_status(txid, **)
275
276
  response = default_call(:get_tx_status, txid)
@@ -306,7 +307,7 @@ module BSV
306
307
  # @return [Hash]
307
308
  def arc_data_from(body)
308
309
  {
309
- txid: body['txid'],
310
+ txid: body['txid'], # ARC API boundary: display-order hex from the ARC JSON response
310
311
  tx_status: body['txStatus'],
311
312
  message: body['title'],
312
313
  extra_info: body['extraInfo'],
@@ -53,6 +53,7 @@ module BSV
53
53
  code = response.code.to_i
54
54
  body = parse_json_body(response.body)
55
55
 
56
+ # TAAL API boundary: display-order hex txid from the TAAL broadcast response
56
57
  return Result::Success.new(data: { txid: body['txid'] }) if already_known?(body) && body['txid']
57
58
 
58
59
  retryable = code == 429 || (500..599).cover?(code)
@@ -175,7 +175,7 @@ module BSV
175
175
  # The +script_hash:+ keyword is accepted for future fallback support
176
176
  # but not used in this implementation.
177
177
  #
178
- # @param txid [String] transaction ID
178
+ # @param txid [String] WoC API boundary: display-order hex transaction ID
179
179
  # @param vout [Integer] output index
180
180
  # @param script_hash [String, nil] ignored
181
181
  # @return [Result::Success<Boolean>, Result::Error, Result::NotFound]
@@ -242,6 +242,7 @@ module BSV
242
242
  return result unless result.success?
243
243
 
244
244
  # WoC returns plain-text txid — result.data is the raw body string
245
+ # WoC API boundary: display-order hex txid returned as plain text
245
246
  Result::Success.new(data: { txid: result.data.to_s.strip })
246
247
  end
247
248
 
@@ -2,118 +2,24 @@
2
2
 
3
3
  module BSV
4
4
  module Network
5
- # WhatsOnChain chain data provider for reading transactions and UTXOs
6
- # from the BSV network.
7
- #
8
- # Any object responding to #fetch_utxos(address),
9
- # #fetch_transaction(txid), #current_height,
10
- # #get_block_header(height), and optionally
11
- # #valid_root_for_height?(root_hex, height) can serve as a chain
12
- # data source;
13
- # this class implements that contract by delegating to
14
- # Protocols::WoCREST.
15
- #
16
- # The HTTP client is injectable for testability. It must respond to
17
- # #request(uri, request) and return an object with #code and #body.
5
+ # @deprecated Use {BSV::Network::Protocols::WoCREST} directly instead.
6
+ # The facade converted clean Result objects into exceptions — every
7
+ # consumer immediately caught them and converted back to data.
8
+ # Use the protocol layer, which returns Result objects natively.
18
9
  class WhatsOnChain
19
- # Returns a WhatsOnChain instance using the provider default.
20
- #
21
- # @param testnet [Boolean] when true, uses the testnet endpoint
22
- # @param opts [Hash] forwarded to the underlying protocol (e.g. +api_key:+, +http_client:+)
23
- # @return [WhatsOnChain]
24
- def self.default(testnet: false, **opts)
25
- provider = Providers::WhatsOnChain.default(testnet: testnet, **opts)
26
- new(protocol: provider.protocol_for(:get_tx))
27
- end
28
-
29
- # @param network [Symbol] :main, :mainnet, :test, :testnet, :stn (legacy compat)
30
- # @param http_client [#request, nil] injectable HTTP client
31
- # @param protocol [BSV::Network::Protocols::WoCREST, nil] pre-configured protocol
32
- def initialize(network: :mainnet, http_client: nil, protocol: nil)
33
- if protocol
34
- @protocol = protocol
35
- else
36
- provider = Providers::WhatsOnChain.default(network: network, http_client: http_client)
37
- @protocol = provider.protocol_for(:get_tx)
38
- end
39
- end
40
-
41
- # Fetch unspent transaction outputs for an address.
42
- # @param address [String] BSV address
43
- # @return [Array<UTXO>]
44
- def fetch_utxos(address)
45
- result = @protocol.call(:get_utxos_all, address)
46
- raise_on_error(result)
47
-
48
- result.data.map do |entry|
49
- UTXO.new(
50
- tx_hash: entry[:tx_hash],
51
- tx_pos: entry[:tx_pos],
52
- satoshis: entry[:satoshis],
53
- height: entry[:height]
54
- )
55
- end
56
- end
57
-
58
- # Fetch a raw transaction by its txid and parse it.
59
- # @param txid [String] transaction ID (hex)
60
- # @return [BSV::Transaction::Transaction]
61
- def fetch_transaction(txid)
62
- result = @protocol.call(:get_tx, txid)
63
- raise_on_error(result)
64
-
65
- BSV::Transaction::Transaction.from_hex(result.data)
66
- end
10
+ # Raised when deprecated facade classes are instantiated.
11
+ class DeprecationError < StandardError; end
67
12
 
68
- # Return the current blockchain height.
69
- # @return [Integer]
70
- # @raise [BSV::Network::ChainProviderError] on network or API error
71
- def current_height
72
- result = @protocol.call(:current_height)
73
- raise_on_error(result)
13
+ MESSAGE = 'BSV::Network::WhatsOnChain is deprecated. ' \
14
+ 'Use BSV::Network::Protocols::WoCREST directly — it returns Result objects ' \
15
+ 'instead of raising exceptions. See BSV::Network::Protocols::WoCREST for usage.'
74
16
 
75
- result.data
17
+ def self.default(**)
18
+ raise DeprecationError, MESSAGE
76
19
  end
77
20
 
78
- # Fetch the block header for a given height.
79
- # @param height [Integer] block height
80
- # @return [Hash] parsed block header JSON
81
- # @raise [BSV::Network::ChainProviderError] on network or API error
82
- def get_block_header(height)
83
- result = @protocol.call(:get_block_header, height)
84
- raise_on_error(result)
85
-
86
- result.data
87
- end
88
-
89
- # Verify that a merkle root is valid for the given block height.
90
- # Returns +false+ when the block is not found (404); raises on other errors.
91
- # @param root [String] expected merkle root as hex
92
- # @param height [Integer] block height
93
- # @return [Boolean]
94
- # @raise [BSV::Network::ChainProviderError] on network or non-404 API error
95
- def valid_root_for_height?(root, height)
96
- result = @protocol.call(:valid_root, root, height)
97
- return false if result.not_found?
98
-
99
- raise_on_error(result)
100
-
101
- result.data == true
102
- end
103
-
104
- private
105
-
106
- # Translates a non-success Protocol result into a raised ChainProviderError.
107
- #
108
- # @param result [Result::Error, Result::NotFound]
109
- # @raise [ChainProviderError]
110
- def raise_on_error(result)
111
- return if result.success?
112
-
113
- raise ChainProviderError.new(
114
- result.message || 'Request failed',
115
- status_code: result.metadata[:status_code]
116
- )
21
+ def initialize(*)
22
+ raise DeprecationError, MESSAGE
117
23
  end
118
24
  end
119
25
  end
@@ -90,7 +90,7 @@ module BSV
90
90
 
91
91
  sig_args = { hash_to_directly_sign: hash_bytes, protocol_id: @protocol_id, key_id: '1', counterparty: 'self' }
92
92
  sig_args[:originator] = @originator if @originator
93
- result = @wallet.create_signature(sig_args)
93
+ result = @wallet.create_signature(**sig_args)
94
94
 
95
95
  sig_bytes = result[:signature].pack('C*')
96
96
  sig_with_hashtype = sig_bytes + [sighash_type].pack('C')
@@ -171,14 +171,14 @@ module BSV
171
171
  # Fetch the wallet's identity key (compressed public key hex)
172
172
  id_args = { identity_key: true }
173
173
  id_args[:originator] = @originator if @originator
174
- identity_result = @wallet.get_public_key(id_args)
174
+ identity_result = @wallet.get_public_key(**id_args)
175
175
  identity_key_hex = identity_result[:public_key]
176
176
  identity_key_bytes = [identity_key_hex].pack('H*')
177
177
 
178
178
  # Derive the locking public key for this protocol
179
179
  lock_args = { protocol_id: protocol_id, key_id: '1', counterparty: 'self' }
180
180
  lock_args[:originator] = @originator if @originator
181
- locking_result = @wallet.get_public_key(lock_args)
181
+ locking_result = @wallet.get_public_key(**lock_args)
182
182
  locking_pubkey_hex = locking_result[:public_key]
183
183
  locking_pubkey_bytes = [locking_pubkey_hex].pack('H*')
184
184
 
@@ -192,7 +192,7 @@ module BSV
192
192
  data_to_sign = (field_protocol + field_identity + field_domain + field_topic).unpack('C*')
193
193
  sig_args = { data: data_to_sign, protocol_id: protocol_id, key_id: '1', counterparty: 'self' }
194
194
  sig_args[:originator] = @originator if @originator
195
- sig_result = @wallet.create_signature(sig_args)
195
+ sig_result = @wallet.create_signature(**sig_args)
196
196
  field_sig = sig_result[:signature].pack('C*')
197
197
 
198
198
  fields = [field_protocol, field_identity, field_domain, field_topic, field_sig]
@@ -334,6 +334,7 @@ module BSV
334
334
  beef_data = output['beef'] || output[:beef]
335
335
  output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
336
336
 
337
+ # Overlay API boundary: outpoint key uses display-order txid bytes (via Transaction#txid)
337
338
  txid =
338
339
  begin
339
340
  beef = parse_beef(beef_data)
@@ -110,7 +110,7 @@ module BSV
110
110
 
111
111
  OverlayBroadcastResult.new(
112
112
  status: 'success',
113
- txid: tx.txid_hex,
113
+ txid: tx.txid_hex, # Overlay API boundary: display-order hex txid for the broadcast result
114
114
  message: "Sent to #{successful.size} Overlay Service host(s)."
115
115
  )
116
116
  end
@@ -82,6 +82,7 @@ module BSV
82
82
  # @return [String] result status ('success' or 'error')
83
83
  attr_reader :status
84
84
 
85
+ # Overlay API boundary: display-order hex txid echoed from the broadcast response.
85
86
  # @return [String, nil] transaction identifier (present on success)
86
87
  attr_reader :txid
87
88
 
@@ -72,6 +72,70 @@ module BSV
72
72
  def self.encode(bytes)
73
73
  bytes.unpack1('H*')
74
74
  end
75
+
76
+ # Validate that +value+ is a 32-byte wire-order transaction ID.
77
+ #
78
+ # @param value [String] expected 32-byte binary string
79
+ # @param name [String] label for the error message (e.g. +'prev_wtxid'+)
80
+ # @return [String] the input value (pass-through for chaining)
81
+ # @raise [ArgumentError] if +value+ is not a 32-byte binary string
82
+ def self.validate_wtxid!(value, name: 'wtxid')
83
+ unless value.is_a?(String) && value.bytesize == 32
84
+ hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
85
+ ' (looks like a hex txid — use wtxid_from_hex to convert)'
86
+ else
87
+ ''
88
+ end
89
+ size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
90
+ raise ArgumentError,
91
+ "expected 32-byte wire-order wtxid for #{name}, got #{size}#{hint}"
92
+ end
93
+ value
94
+ end
95
+
96
+ # Validate that +value+ is a 32-byte binary hash.
97
+ #
98
+ # General-purpose validator for any 32-byte hash (merkle nodes, roots,
99
+ # etc.) — not specific to transaction IDs. For txid-specific validation
100
+ # use {.validate_wtxid!} or {.validate_dtxid_hex!} instead.
101
+ #
102
+ # @param value [String] expected 32-byte binary string
103
+ # @param name [String] label for the error message
104
+ # @return [String] the input value (pass-through for chaining)
105
+ # @raise [ArgumentError] if +value+ is not a 32-byte binary string
106
+ def self.validate_hash32!(value, name: 'hash')
107
+ unless value.is_a?(String) && value.bytesize == 32
108
+ hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
109
+ ' (looks like hex — decode it first)'
110
+ else
111
+ ''
112
+ end
113
+ size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
114
+ raise ArgumentError,
115
+ "expected 32-byte hash for #{name}, got #{size}#{hint}"
116
+ end
117
+ value
118
+ end
119
+
120
+ # Validate that +value+ is a 64-character display-order hex transaction ID.
121
+ #
122
+ # @param value [String] expected 64-char hex string
123
+ # @param name [String] label for the error message (e.g. +'dtxid_hex'+)
124
+ # @return [String] the input value (pass-through for chaining)
125
+ # @raise [ArgumentError] if +value+ is not a 64-char hex string
126
+ def self.validate_dtxid_hex!(value, name: 'dtxid_hex')
127
+ unless value.is_a?(String) && value.length == 64 && value.match?(HEX_RE)
128
+ hint = if value.is_a?(String) && value.bytesize == 32 && !value.match?(HEX_RE)
129
+ ' (looks like binary bytes — use dtxid_hex or unpack to convert)'
130
+ else
131
+ ''
132
+ end
133
+ size = value.is_a?(String) ? "#{value.length}-char string" : value.class.to_s
134
+ raise ArgumentError,
135
+ "expected 64-char display-order hex for #{name}, got #{size}#{hint}"
136
+ end
137
+ value
138
+ end
75
139
  end
76
140
  end
77
141
  end