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 +4 -4
- data/CHANGELOG.md +28 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +4 -4
- data/lib/bsv/mcp/tools/check_balance.rb +2 -2
- data/lib/bsv/mcp/tools/fetch_tx.rb +2 -2
- data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
- data/lib/bsv/network/protocol.rb +41 -30
- data/lib/bsv/network/protocol_response.rb +86 -0
- data/lib/bsv/network/protocols/arc.rb +48 -111
- data/lib/bsv/network/protocols/ordinals.rb +10 -10
- data/lib/bsv/network/protocols/taal_binary.rb +11 -12
- data/lib/bsv/network/protocols/woc_rest.rb +25 -25
- data/lib/bsv/network/provider.rb +2 -2
- data/lib/bsv/network.rb +1 -6
- data/lib/bsv/transaction/chain_tracker.rb +30 -12
- data/lib/bsv/transaction/chain_trackers/chaintracks.rb +6 -14
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +6 -14
- data/lib/bsv/version.rb +1 -1
- metadata +2 -7
- data/lib/bsv/network/arc.rb +0 -26
- data/lib/bsv/network/broadcast_error.rb +0 -18
- data/lib/bsv/network/broadcast_response.rb +0 -31
- data/lib/bsv/network/chain_provider_error.rb +0 -14
- data/lib/bsv/network/result.rb +0 -119
- data/lib/bsv/network/whats_on_chain.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ade93923526aab66c548ffc2326e09e439e73a8403748afe3bcd2c1e6e04d58d
|
|
4
|
+
data.tar.gz: aff5ede00eb02991dca9daa171f0f21d04d975f6ad3f012a817cfc5b46710ceb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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[
|
|
120
|
-
tx_status: arc_result.data[
|
|
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.
|
|
66
|
-
code = utxo_result.
|
|
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.
|
|
56
|
-
code = fetch_result.
|
|
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.
|
|
57
|
-
code = utxo_result.
|
|
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)
|
data/lib/bsv/network/protocol.rb
CHANGED
|
@@ -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
|
|
24
|
-
# response
|
|
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 +
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
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
|
-
#
|
|
317
|
-
# on 2xx bodies.
|
|
320
|
+
# Wraps an HTTP response in a +ProtocolResponse+, applying the response
|
|
321
|
+
# handler on 2xx bodies.
|
|
318
322
|
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
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 [
|
|
325
|
-
def
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
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
|
|
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.
|
|
26
|
-
# result.data[
|
|
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 [
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
197
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
256
|
-
Result::Success.new(data: items, metadata: {})
|
|
252
|
+
ProtocolResponse.new(response, data: body)
|
|
257
253
|
end
|
|
258
254
|
|
|
259
|
-
#
|
|
260
|
-
|
|
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 [
|
|
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.
|
|
262
|
+
return response unless response.http_success?
|
|
298
263
|
|
|
299
264
|
body = response.data
|
|
300
265
|
|
|
301
266
|
if rejected_status?(body)
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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 [
|
|
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.
|
|
98
|
-
return
|
|
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
|
-
|
|
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 [
|
|
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.
|
|
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
|
-
|
|
124
|
+
result.with(data: { spent: false })
|
|
125
125
|
else
|
|
126
|
-
|
|
126
|
+
result.with(data: { spent: true, spending_txid: spending_txid })
|
|
127
127
|
end
|
|
128
|
-
rescue JSON::ParserError => e
|
|
129
|
-
|
|
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
|