bsv-sdk 0.18.1 → 0.19.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e425cc46fca3c3db1d2dc79702491600ec302625132ba10c95ee8dee7242ed9
4
- data.tar.gz: 1839e24be2d7ebea84478b54526f7793839681cd385fb9de962f118826220338
3
+ metadata.gz: ade93923526aab66c548ffc2326e09e439e73a8403748afe3bcd2c1e6e04d58d
4
+ data.tar.gz: aff5ede00eb02991dca9daa171f0f21d04d975f6ad3f012a817cfc5b46710ceb
5
5
  SHA512:
6
- metadata.gz: 285dd4fc51b19fef84f31071cec373b43c70347de9cd5bf904c17fff65ff1c964b62d49969de79b98caa241198b6e689c7e50551a66aac2c1c593b1b9f491dc9
7
- data.tar.gz: 3e5536f21302a75992e221967fa5de233cb4200a9efc34cd71b6d892f714955fd2dda73625a998a9943110274d63925e044690a99fe8870ef5a09323d6c1d901
6
+ metadata.gz: 32271e4c61f304555853790682f9008f4789e34059d6d868207f0ef07cf9744cf87ae67e5589ad451d22bebc7a435ae4989deaa48f9254b900691b303b961a7d
7
+ data.tar.gz: f51f7b37da7a8388606763df33bf71900592f37f7cb2ca6690337a7dfd04821eca14d78153f8dc470ad01c377b1b0e8fa300a661686e54255dac54d9a90ae190
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to the `bsv-sdk` gem are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 0.19.0 — 2026-05-11
9
+
10
+ ### Breaking Changes
11
+ - `Result::Success/Error/NotFound` replaced by single `ProtocolResponse` class composing `Net::HTTPResponse`
12
+ - Predicates renamed: `success?` → `http_success?`, `not_found?` → `http_not_found?`, `error?` removed (use `!http_success?`)
13
+ - Constructor keyword `ok:` → `http_success:`
14
+ - ARC `arc_data_from` key remapping removed — `.data` returns raw API JSON with string keys (`'txid'` not `:txid`)
15
+ - Batch broadcast `.data` returns raw JSON array (no per-item `Result` wrapping)
16
+ - `BroadcastError`, `BroadcastResponse`, `ChainProviderError`, deprecated `BSV::Network::ARC` and `BSV::Network::WhatsOnChain` facades removed
17
+
18
+ ### Added
19
+ - `ProtocolResponse` class with progressive enhancement: `.body`/`.code` (raw HTTP) → `.data` (parsed) → `.canonical` (placeholder)
20
+ - `ProtocolResponse#with(**overrides)` for escape hatch post-processing
21
+ - `fake_http_response` shared test helper for building real `Net::HTTPResponse` subclass instances
22
+ - Debug logging in Protocol (`call`, `default_call`, `build_response`) and `ProtocolResponse#with`
23
+
24
+ ### Changed
25
+ - `Protocol#map_response` renamed to `build_response`
26
+ - Chain trackers raise `RuntimeError` instead of deleted `ChainProviderError`
27
+ - `ChainTracker` doc rewritten to explain the declarative/imperative split
28
+ - MCP tools updated: `.metadata[:status_code]` → `.code`, symbol keys → string keys
29
+
30
+ ### Fixed
31
+ - `http_success?` returns `false` (not `nil`) when `http_response` is nil
32
+ - `Ordinals#call_get_spend` rescues `TypeError` alongside `JSON::ParserError`
33
+ - ARC malformed-2xx error messages include diagnostic detail
34
+ - WoC `call_is_utxo` clears stale `error_message` when overriding 404 to success
35
+
8
36
  ## 0.18.1 — 2026-05-09
9
37
 
10
38
  ### Added
@@ -93,7 +93,7 @@ module BSV
93
93
 
94
94
  woc = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
95
95
  utxo_result = woc.call(:get_utxos_all, sender_address)
96
- return Helpers.error_response("UTXO fetch failed: #{utxo_result.message}") unless utxo_result.success?
96
+ return Helpers.error_response("UTXO fetch failed: #{utxo_result.message}") unless utxo_result.http_success?
97
97
 
98
98
  all_utxos = utxo_result.data.map do |entry|
99
99
  BSV::Network::UTXO.new(
@@ -113,11 +113,11 @@ module BSV
113
113
 
114
114
  arc = build_arc(net_sym, server_context)
115
115
  arc_result = arc.call(:broadcast, tx)
116
- return Helpers.error_response("Broadcast failed: #{arc_result.message}") unless arc_result.success?
116
+ return Helpers.error_response("Broadcast failed: #{arc_result.message}") unless arc_result.http_success?
117
117
 
118
118
  result = {
119
- txid: arc_result.data[:txid], # MCP tool boundary: display-order hex from ARC response
120
- tx_status: arc_result.data[:tx_status],
119
+ txid: arc_result.data['txid'], # MCP tool boundary: display-order hex from ARC response
120
+ tx_status: arc_result.data['txStatus'],
121
121
  hex: tx.to_hex
122
122
  }
123
123
 
@@ -62,8 +62,8 @@ module BSV
62
62
  provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
63
63
  utxo_result = provider.call(:get_utxos_all, address)
64
64
 
65
- unless utxo_result.success?
66
- code = utxo_result.metadata[:status_code]
65
+ unless utxo_result.http_success?
66
+ code = utxo_result.code
67
67
  msg = utxo_result.message
68
68
  msg = "#{msg} (HTTP #{code})" if code
69
69
  return Helpers.error_response(msg)
@@ -52,8 +52,8 @@ module BSV
52
52
  provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
53
53
  fetch_result = provider.call(:get_tx, txid)
54
54
 
55
- unless fetch_result.success?
56
- code = fetch_result.metadata[:status_code]
55
+ unless fetch_result.http_success?
56
+ code = fetch_result.code
57
57
  msg = fetch_result.message
58
58
  msg = "#{msg} (HTTP #{code})" if code
59
59
  return Helpers.error_response(msg)
@@ -53,8 +53,8 @@ module BSV
53
53
  provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
54
54
  utxo_result = provider.call(:get_utxos_all, address)
55
55
 
56
- unless utxo_result.success?
57
- code = utxo_result.metadata[:status_code]
56
+ unless utxo_result.http_success?
57
+ code = utxo_result.code
58
58
  msg = utxo_result.message
59
59
  msg = "#{msg} (HTTP #{code})" if code
60
60
  return Helpers.error_response(msg)
@@ -20,8 +20,8 @@ module BSV
20
20
  #
21
21
  # HTTP dispatch routes through +call+: if a +call_<name>+ escape hatch
22
22
  # method exists on the instance, it is called; otherwise +default_call+
23
- # interpolates the URL template, makes the HTTP request, and maps the
24
- # response to a +Result+.
23
+ # interpolates the URL template, makes the HTTP request, and wraps the
24
+ # response in a +ProtocolResponse+.
25
25
  #
26
26
  # == Example
27
27
  #
@@ -127,14 +127,14 @@ module BSV
127
127
  #
128
128
  # If a method named +call_<command_name>+ exists on the instance it is
129
129
  # used as an escape hatch — that method receives +args+ and +kwargs+
130
- # and MUST return a +Result+. Otherwise +default_call+ is invoked.
130
+ # and MUST return a +ProtocolResponse+. Otherwise +default_call+ is invoked.
131
131
  #
132
132
  # Subscriptions are not callable; calling one raises +NotImplementedError+.
133
133
  #
134
134
  # @param command_name [Symbol, String] command to invoke
135
135
  # @param args [Array] positional arguments forwarded to path interpolation
136
136
  # @param kwargs [Hash] keyword arguments forwarded to path interpolation
137
- # @return [Result::Success, Result::Error, Result::NotFound]
137
+ # @return [ProtocolResponse]
138
138
  # @raise [ArgumentError] when command_name is not registered
139
139
  def call(command_name, *args, **kwargs)
140
140
  name = command_name.to_sym
@@ -146,6 +146,7 @@ module BSV
146
146
 
147
147
  escape = :"call_#{name}"
148
148
  if respond_to?(escape, true)
149
+ BSV.logger&.debug { "[Protocol] #{self.class.name} :#{name} → escape hatch" }
149
150
  return kwargs.empty? ? send(escape, *args) : send(escape, *args, **kwargs)
150
151
  end
151
152
 
@@ -164,7 +165,7 @@ module BSV
164
165
  # @param command_name [Symbol] registered command name
165
166
  # @param args [Array] positional path parameters
166
167
  # @param kwargs [Hash] named path parameters (and optional +:body+)
167
- # @return [Result::Success, Result::Error, Result::NotFound]
168
+ # @return [ProtocolResponse]
168
169
  # @raise [ArgumentError] when command_name is not registered or a
169
170
  # required path parameter is missing
170
171
  def default_call(command_name, *args, **kwargs)
@@ -176,9 +177,12 @@ module BSV
176
177
  path = interpolate_path(defn[:path], args, kwargs)
177
178
  uri = URI("#{@base_url}#{path}")
178
179
  request = build_request(defn[:method], uri, body)
179
- response = execute(uri, request)
180
180
 
181
- map_response(response, defn[:response])
181
+ BSV.logger&.debug { "[Protocol] #{defn[:method].upcase} #{uri}" }
182
+
183
+ response = execute(uri, request)
184
+
185
+ build_response(response, defn[:response])
182
186
  end
183
187
 
184
188
  private
@@ -313,38 +317,47 @@ module BSV
313
317
  end
314
318
  end
315
319
 
316
- # Maps an HTTP response to a Result type, applying the response handler
317
- # on 2xx bodies.
320
+ # Wraps an HTTP response in a +ProtocolResponse+, applying the response
321
+ # handler on 2xx bodies.
318
322
  #
319
- # All non-2xx results carry +status_code:+ in their +metadata+ hash so
320
- # that facades can construct domain exceptions with the original HTTP code.
323
+ # On 2xx responses, the handler is applied and the result stored in +data+.
324
+ # If the handler raises +JSON::ParserError+ or +TypeError+, the response
325
+ # is marked as an error with the exception message.
326
+ #
327
+ # On non-2xx responses, the raw body is stored as +error_message+.
321
328
  #
322
329
  # @param response [Net::HTTPResponse]
323
330
  # @param handler [Symbol, #call]
324
- # @return [Result::Success, Result::Error, Result::NotFound]
325
- def map_response(response, handler)
326
- code = response.code.to_i
327
-
328
- case code
329
- when 200..299
330
- data = apply_handler(response.body, handler)
331
- return data if data.is_a?(Result::Error)
332
-
333
- Result::Success.new(data: data)
334
- when 404
335
- Result::NotFound.new(message: response.body, metadata: { status_code: code })
336
- when 429, 500..599
337
- Result::Error.new(message: response.body, retryable: true, metadata: { status_code: code })
338
- else
339
- Result::Error.new(message: response.body, retryable: false, metadata: { status_code: code })
331
+ # @return [ProtocolResponse]
332
+ def build_response(response, handler)
333
+ result = if response.is_a?(Net::HTTPSuccess)
334
+ begin
335
+ data = apply_handler(response.body, handler)
336
+ ProtocolResponse.new(response, data: data)
337
+ rescue JSON::ParserError, TypeError => e
338
+ ProtocolResponse.new(response, http_success: false,
339
+ error_message: "JSON/response error: #{e.message}")
340
+ end
341
+ else
342
+ ProtocolResponse.new(response, error_message: response.body)
343
+ end
344
+
345
+ BSV.logger&.debug do
346
+ "[Protocol] HTTP #{response.code} http_success=#{result.http_success?}" \
347
+ "#{" error=#{result.error_message[0, 80]}" if result.error_message}"
340
348
  end
349
+
350
+ result
341
351
  end
342
352
 
343
353
  # Applies the response handler to a raw body string.
344
354
  #
355
+ # Exceptions from JSON parsing or type mismatches propagate to the caller
356
+ # (+build_response+) which handles them uniformly.
357
+ #
345
358
  # @param body [String, nil]
346
359
  # @param handler [Symbol, #call]
347
- # @return [Object, Result::Error]
360
+ # @return [Object]
348
361
  def apply_handler(body, handler)
349
362
  return body if body.nil?
350
363
 
@@ -365,8 +378,6 @@ module BSV
365
378
 
366
379
  handler.call(body)
367
380
  end
368
- rescue JSON::ParserError, TypeError => e
369
- Result::Error.new(message: "JSON/response error: #{e.message}", retryable: false)
370
381
  end
371
382
  end
372
383
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module BSV
6
+ module Network
7
+ class ProtocolResponse
8
+ attr_reader :data, :error_message
9
+
10
+ def initialize(http_response, data: nil, http_success: nil, error_message: nil)
11
+ @http_response = http_response
12
+ @data = data
13
+ @http_success = http_success.nil? ? http_response.is_a?(Net::HTTPSuccess) : http_success
14
+ @error_message = error_message
15
+ freeze
16
+ end
17
+
18
+ # Delegated from Net::HTTPResponse (nil-safe)
19
+ def body
20
+ @http_response&.body
21
+ end
22
+
23
+ def code
24
+ @http_response&.code
25
+ end
26
+
27
+ def content_type
28
+ @http_response&.content_type
29
+ end
30
+
31
+ # Two layers of status predicates:
32
+ #
33
+ # HTTP logical status — did the operation succeed? Driven by the +http_success:+
34
+ # parameter, which defaults to the RFC 9110 success class check
35
+ # (+Net::HTTPSuccess+, i.e. 2xx) but can be overridden by escape
36
+ # hatches that reinterpret HTTP status (e.g. ARC REJECTED on 2xx
37
+ # sets http_success: false; WoC is_utxo 404 sets http_success: true).
38
+ #
39
+ # Transport status — what did the HTTP layer actually return?
40
+ # These always reflect the +Net::HTTPResponse+ class hierarchy
41
+ # regardless of the +http_success:+ override. +http_not_found?+ maps
42
+ # directly to +Net::HTTPNotFound+ (404). +retryable?+ is a
43
+ # domain composite: 429 (+Net::HTTPTooManyRequests+) or 5xx
44
+ # (+Net::HTTPServerError+) — not an RFC 9110 category itself,
45
+ # but a useful signal for retry logic.
46
+
47
+ def http_success?
48
+ @http_success
49
+ end
50
+
51
+ def http_not_found?
52
+ @http_response.is_a?(Net::HTTPNotFound)
53
+ end
54
+
55
+ def retryable?
56
+ @http_response.is_a?(Net::HTTPTooManyRequests) ||
57
+ @http_response.is_a?(Net::HTTPServerError)
58
+ end
59
+
60
+ # Canonical form (placeholder — delegates to data until shapes are defined)
61
+ def canonical
62
+ data
63
+ end
64
+
65
+ # Compatibility: chain trackers + MCP tools call .message
66
+ alias message error_message
67
+
68
+ # Derive new response with overrides (same HTTP response, different interpretation)
69
+ def with(**overrides)
70
+ derived = self.class.new(
71
+ @http_response,
72
+ data: overrides.fetch(:data, @data),
73
+ http_success: overrides.fetch(:http_success, @http_success),
74
+ error_message: overrides.fetch(:error_message, @error_message)
75
+ )
76
+
77
+ BSV.logger&.debug do
78
+ changes = overrides.keys.map { |k| "#{k}=#{overrides[k].inspect[0, 40]}" }.join(' ')
79
+ "[ProtocolResponse] with(#{changes})"
80
+ end
81
+
82
+ derived
83
+ end
84
+ end
85
+ end
86
+ end
@@ -11,9 +11,7 @@ module BSV
11
11
  # Extends Protocol with five endpoints and two escape hatches for broadcast
12
12
  # logic: EF format preference, rejection detection, and custom headers.
13
13
  #
14
- # The protocol returns Result objects (never raises). The facade layer
15
- # (Phase D) is responsible for translating Results to BroadcastResponse /
16
- # BroadcastError as needed by consumer code.
14
+ # The protocol returns ProtocolResponse objects (never raises).
17
15
  #
18
16
  # == Example
19
17
  #
@@ -22,8 +20,8 @@ module BSV
22
20
  # api_key: 'my-api-key'
23
21
  # )
24
22
  # result = arc.call(:broadcast, tx)
25
- # result.success? # => true
26
- # result.data[:txid] # => "abc123..."
23
+ # result.http_success? # => true
24
+ # result.data['txid'] # => "abc123..."
27
25
  #
28
26
  # @see https://docs.gorillapool.io/arc/api.html ARC API v1 documentation
29
27
  class ARC < Protocol
@@ -77,7 +75,7 @@ module BSV
77
75
  # @param callback_url [String, nil] per-call callback URL override
78
76
  # @param callback_token [String, nil] per-call callback token override
79
77
  # @param callback_batch [Boolean, nil] when truthy, sends X-CallbackBatch header
80
- # @return [Result::Success, Result::Error]
78
+ # @return [ProtocolResponse]
81
79
  def call_broadcast(tx, wait_for: nil, skip_fee_validation: nil,
82
80
  skip_script_validation: nil, skip_tx_validation: nil,
83
81
  callback_url: nil, callback_token: nil, callback_batch: nil, **)
@@ -98,7 +96,7 @@ module BSV
98
96
  parse_single_broadcast_response(response)
99
97
  end
100
98
 
101
- # Broadcast-many escape hatch: batch broadcast with per-item rejection detection.
99
+ # Broadcast-many escape hatch: batch broadcast with raw JSON array result.
102
100
  #
103
101
  # @param txs [Array<#to_ef_hex, #to_hex, String>]
104
102
  # @param wait_for [String, nil]
@@ -108,11 +106,11 @@ module BSV
108
106
  # @param callback_url [String, nil]
109
107
  # @param callback_token [String, nil]
110
108
  # @param callback_batch [Boolean, nil]
111
- # @return [Result::Success, Result::Error]
109
+ # @return [ProtocolResponse]
112
110
  def call_broadcast_many(txs, wait_for: nil, skip_fee_validation: nil,
113
111
  skip_script_validation: nil, skip_tx_validation: nil,
114
112
  callback_url: nil, callback_token: nil, callback_batch: nil, **)
115
- return Result::Success.new(data: []) if txs.empty?
113
+ return ProtocolResponse.new(nil, data: [], http_success: true) if txs.empty?
116
114
 
117
115
  body = JSON.generate(txs.map { |tx| { rawTx: resolve_tx_hex(tx) } })
118
116
 
@@ -188,159 +186,98 @@ module BSV
188
186
  end
189
187
 
190
188
  # Parse and validate a single-transaction ARC response.
189
+ #
190
+ # @return [ProtocolResponse]
191
191
  def parse_single_broadcast_response(response)
192
192
  code = response.code.to_i
193
193
  body = safe_parse_json(response.body)
194
194
 
195
195
  unless body.is_a?(Hash)
196
- return Result::Error.new(
197
- message: "HTTP #{code}",
198
- retryable: retryable_code?(code),
199
- metadata: { status_code: code }
200
- )
196
+ return ProtocolResponse.new(response, http_success: false,
197
+ error_message: "ARC returned #{body.class}, expected Hash")
201
198
  end
202
199
 
203
200
  unless (200..299).cover?(code)
204
- return Result::Error.new(
205
- message: body['detail'] || body['title'] || "HTTP #{code}",
206
- retryable: retryable_code?(code),
207
- metadata: { status_code: code, arc_status: body['txStatus'].to_s.upcase, txid: body['txid'] }
201
+ return ProtocolResponse.new(
202
+ response,
203
+ http_success: false,
204
+ error_message: body['detail'] || body['title'] || "HTTP #{code}"
208
205
  )
209
206
  end
210
207
 
211
208
  if rejected_status?(body)
212
- return Result::Error.new(
213
- message: body['detail'] || body['title'] || body['txStatus'],
214
- retryable: false,
215
- metadata: { status_code: code, arc_status: body['txStatus'].to_s.upcase, txid: body['txid'] }
209
+ return ProtocolResponse.new(
210
+ response,
211
+ http_success: false,
212
+ error_message: body['detail'] || body['title'] || body['txStatus'],
213
+ data: body
216
214
  )
217
215
  end
218
216
 
219
217
  unless body['txid']
220
- return Result::Error.new(
221
- message: 'ARC returned a malformed 2xx response',
222
- retryable: false,
223
- metadata: { status_code: code }
218
+ return ProtocolResponse.new(
219
+ response,
220
+ http_success: false,
221
+ error_message: body['detail'] || 'ARC returned a malformed 2xx response'
224
222
  )
225
223
  end
226
224
 
227
- Result::Success.new(
228
- data: arc_data_from(body),
229
- metadata: { arc_status: body['txStatus'].to_s.upcase }
230
- )
225
+ ProtocolResponse.new(response, data: body)
231
226
  end
232
227
 
233
228
  # Parse and validate a batch ARC response. HTTP-level errors return a
234
- # single Result::Error; per-item rejections are embedded in the data array.
229
+ # single error ProtocolResponse; success data is a raw JSON array of hashes.
230
+ #
231
+ # @return [ProtocolResponse]
235
232
  def parse_batch_broadcast_response(response)
236
233
  code = response.code.to_i
237
234
  body = safe_parse_json(response.body)
238
235
 
239
236
  unless (200..299).cover?(code)
240
- return Result::Error.new(
241
- message: body.is_a?(Hash) ? (body['detail'] || body['title'] || "HTTP #{code}") : "HTTP #{code}",
242
- retryable: retryable_code?(code),
243
- metadata: { status_code: code }
237
+ return ProtocolResponse.new(
238
+ response,
239
+ http_success: false,
240
+ error_message: body.is_a?(Hash) ? (body['detail'] || body['title'] || "HTTP #{code}") : "HTTP #{code}"
244
241
  )
245
242
  end
246
243
 
247
244
  unless body.is_a?(Array)
248
- return Result::Error.new(
249
- message: 'ARC returned a malformed batch response',
250
- retryable: false,
251
- metadata: { status_code: code }
245
+ return ProtocolResponse.new(
246
+ response,
247
+ http_success: false,
248
+ error_message: 'ARC returned a malformed batch response'
252
249
  )
253
250
  end
254
251
 
255
- items = body.map { |item| build_item_result(item) }
256
- Result::Success.new(data: items, metadata: {})
252
+ ProtocolResponse.new(response, data: body)
257
253
  end
258
254
 
259
- # Build a per-item result for a batch response entry.
260
- def build_item_result(item)
261
- unless item.is_a?(Hash)
262
- return Result::Error.new(
263
- message: 'malformed batch item',
264
- retryable: false,
265
- metadata: {}
266
- )
267
- end
268
-
269
- if rejected_status?(item)
270
- Result::Error.new(
271
- message: item['detail'] || item['title'] || item['txStatus'],
272
- retryable: false,
273
- metadata: { status_code: 200, arc_status: item['txStatus'].to_s.upcase, txid: item['txid'] }
274
- )
275
- elsif !item['txid']
276
- Result::Error.new(
277
- message: 'ARC returned a malformed 2xx response',
278
- retryable: false,
279
- metadata: { status_code: 200 }
280
- )
281
- else
282
- Result::Success.new(
283
- data: arc_data_from(item),
284
- metadata: { arc_status: item['txStatus'].to_s.upcase }
285
- )
286
- end
287
- end
288
-
289
- # Escape hatch for get_tx_status: returns a normalised data hash using the
290
- # same field set as broadcast responses rather than the raw parsed JSON.
291
- # Also checks for rejection status and missing txid (malformed 2xx).
255
+ # Escape hatch for get_tx_status: checks for rejection status and missing
256
+ # txid (malformed 2xx). Returns raw JSON data (string keys).
292
257
  #
293
258
  # @param txid [String] ARC API boundary: display-order hex transaction ID to query
294
- # @return [Result::Success, Result::Error, Result::NotFound]
259
+ # @return [ProtocolResponse]
295
260
  def call_get_tx_status(txid, **)
296
261
  response = default_call(:get_tx_status, txid)
297
- return response unless response.is_a?(Result::Success)
262
+ return response unless response.http_success?
298
263
 
299
264
  body = response.data
300
265
 
301
266
  if rejected_status?(body)
302
- return Result::Error.new(
303
- message: body['detail'] || body['title'] || body['txStatus'],
304
- retryable: false,
305
- metadata: { arc_status: body['txStatus'].to_s.upcase, txid: body['txid'], status_code: 200 }
267
+ return response.with(
268
+ http_success: false,
269
+ error_message: body['detail'] || body['title'] || body['txStatus']
306
270
  )
307
271
  end
308
272
 
309
273
  unless body['txid']
310
- return Result::Error.new(
311
- message: 'ARC returned a malformed 2xx response',
312
- retryable: false,
313
- metadata: { status_code: 200 }
274
+ return response.with(
275
+ http_success: false,
276
+ error_message: 'ARC returned a malformed 2xx response'
314
277
  )
315
278
  end
316
279
 
317
- Result::Success.new(
318
- data: arc_data_from(body),
319
- metadata: { arc_status: body['txStatus'].to_s.upcase }
320
- )
321
- end
322
-
323
- # Build the normalised ARC data hash from a parsed JSON response body.
324
- # Includes all 8 fields that BroadcastResponse expects.
325
- #
326
- # @param body [Hash] parsed ARC JSON response
327
- # @return [Hash]
328
- def arc_data_from(body)
329
- {
330
- txid: body['txid'], # ARC API boundary: display-order hex from the ARC JSON response
331
- tx_status: body['txStatus'],
332
- message: body['title'],
333
- extra_info: body['extraInfo'],
334
- block_hash: body['blockHash'],
335
- block_height: body['blockHeight'],
336
- timestamp: body['timestamp'],
337
- competing_txs: body['competingTxs']
338
- }
339
- end
340
-
341
- # Determine whether a status code indicates a retryable failure.
342
- def retryable_code?(code)
343
- code == 429 || (500..599).cover?(code)
280
+ response
344
281
  end
345
282
 
346
283
  # Determine whether an ARC response body represents a rejected transaction.
@@ -88,16 +88,16 @@ module BSV
88
88
  #
89
89
  # @param pos_txid [String, nil] transaction ID (positional form)
90
90
  # @param txid [String, nil] transaction ID (keyword form)
91
- # @return [Result::Success<String>, Result::Error, Result::NotFound]
91
+ # @return [ProtocolResponse]
92
92
  def call_get_tx(pos_txid = nil, txid: nil)
93
93
  resolved = pos_txid || txid
94
94
  raise ArgumentError, 'txid is required' if resolved.nil? || resolved.empty?
95
95
 
96
96
  result = default_call(:get_tx, resolved)
97
- return result unless result.success?
98
- return Result::Error.new(message: 'empty response body') if result.data.nil? || result.data.empty?
97
+ return result unless result.http_success?
98
+ return result.with(http_success: false, error_message: 'empty response body') if result.data.nil? || result.data.empty?
99
99
 
100
- Result::Success.new(data: result.data.unpack1('H*'))
100
+ result.with(data: result.data.unpack1('H*'))
101
101
  end
102
102
 
103
103
  # Normalises the spend status response from the Ordinals API.
@@ -111,22 +111,22 @@ module BSV
111
111
  #
112
112
  # @param pos_outpoint [String, nil] outpoint in +"txid_vout"+ format (positional form)
113
113
  # @param outpoint [String, nil] outpoint in +"txid_vout"+ format (keyword form)
114
- # @return [Result::Success<Hash>, Result::Error, Result::NotFound]
114
+ # @return [ProtocolResponse]
115
115
  def call_get_spend(pos_outpoint = nil, outpoint: nil)
116
116
  resolved = pos_outpoint || outpoint
117
117
  raise ArgumentError, 'outpoint is required' if resolved.nil? || resolved.empty?
118
118
 
119
119
  result = default_call(:get_spend, resolved)
120
- return result unless result.success?
120
+ return result unless result.http_success?
121
121
 
122
122
  spending_txid = JSON.parse(result.data).to_s.strip
123
123
  if spending_txid.empty?
124
- Result::Success.new(data: { spent: false })
124
+ result.with(data: { spent: false })
125
125
  else
126
- Result::Success.new(data: { spent: true, spending_txid: spending_txid })
126
+ result.with(data: { spent: true, spending_txid: spending_txid })
127
127
  end
128
- rescue JSON::ParserError => e
129
- Result::Error.new(message: "spend response parse error: #{e.message}")
128
+ rescue JSON::ParserError, TypeError => e
129
+ result.with(http_success: false, error_message: "spend response parse error: #{e.message}")
130
130
  end
131
131
  end
132
132
  end