bsv-sdk 0.18.0 → 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 +37 -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 +73 -119
- 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/protocols.rb +2 -3
- 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
- data/lib/bsv/wallet/proto_wallet.rb +44 -8
- 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,43 @@ 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
|
+
|
|
36
|
+
## 0.18.1 — 2026-05-09
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- ARC broadcast accepts hex and binary strings directly, in addition to transaction objects
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Hex string detection uses content inspection rather than string encoding
|
|
43
|
+
- Stale "Phase B" text removed from Protocols namespace module doc
|
|
44
|
+
|
|
8
45
|
## 0.18.0 — 2026-05-09
|
|
9
46
|
|
|
10
47
|
### Breaking Changes
|
|
@@ -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
|
|
@@ -68,7 +66,8 @@ module BSV
|
|
|
68
66
|
# Broadcast escape hatch: EF format preference, custom headers, rejection
|
|
69
67
|
# detection, and malformed 2xx detection.
|
|
70
68
|
#
|
|
71
|
-
# @param tx [
|
|
69
|
+
# @param tx [#to_ef_hex, #to_hex, String] transaction object, hex string,
|
|
70
|
+
# or binary string
|
|
72
71
|
# @param wait_for [String, nil] ARC wait condition
|
|
73
72
|
# @param skip_fee_validation [Boolean, nil]
|
|
74
73
|
# @param skip_script_validation [Boolean, nil]
|
|
@@ -76,11 +75,11 @@ module BSV
|
|
|
76
75
|
# @param callback_url [String, nil] per-call callback URL override
|
|
77
76
|
# @param callback_token [String, nil] per-call callback token override
|
|
78
77
|
# @param callback_batch [Boolean, nil] when truthy, sends X-CallbackBatch header
|
|
79
|
-
# @return [
|
|
78
|
+
# @return [ProtocolResponse]
|
|
80
79
|
def call_broadcast(tx, wait_for: nil, skip_fee_validation: nil,
|
|
81
80
|
skip_script_validation: nil, skip_tx_validation: nil,
|
|
82
81
|
callback_url: nil, callback_token: nil, callback_batch: nil, **)
|
|
83
|
-
hex =
|
|
82
|
+
hex = resolve_tx_hex(tx)
|
|
84
83
|
body = JSON.generate(rawTx: hex)
|
|
85
84
|
|
|
86
85
|
extra_headers = build_broadcast_headers(
|
|
@@ -97,9 +96,9 @@ module BSV
|
|
|
97
96
|
parse_single_broadcast_response(response)
|
|
98
97
|
end
|
|
99
98
|
|
|
100
|
-
# Broadcast-many escape hatch: batch broadcast with
|
|
99
|
+
# Broadcast-many escape hatch: batch broadcast with raw JSON array result.
|
|
101
100
|
#
|
|
102
|
-
# @param txs [Array
|
|
101
|
+
# @param txs [Array<#to_ef_hex, #to_hex, String>]
|
|
103
102
|
# @param wait_for [String, nil]
|
|
104
103
|
# @param skip_fee_validation [Boolean, nil]
|
|
105
104
|
# @param skip_script_validation [Boolean, nil]
|
|
@@ -107,13 +106,13 @@ module BSV
|
|
|
107
106
|
# @param callback_url [String, nil]
|
|
108
107
|
# @param callback_token [String, nil]
|
|
109
108
|
# @param callback_batch [Boolean, nil]
|
|
110
|
-
# @return [
|
|
109
|
+
# @return [ProtocolResponse]
|
|
111
110
|
def call_broadcast_many(txs, wait_for: nil, skip_fee_validation: nil,
|
|
112
111
|
skip_script_validation: nil, skip_tx_validation: nil,
|
|
113
112
|
callback_url: nil, callback_token: nil, callback_batch: nil, **)
|
|
114
|
-
return
|
|
113
|
+
return ProtocolResponse.new(nil, data: [], http_success: true) if txs.empty?
|
|
115
114
|
|
|
116
|
-
body = JSON.generate(txs.map { |tx| { rawTx:
|
|
115
|
+
body = JSON.generate(txs.map { |tx| { rawTx: resolve_tx_hex(tx) } })
|
|
117
116
|
|
|
118
117
|
extra_headers = build_broadcast_headers(
|
|
119
118
|
wait_for: wait_for,
|
|
@@ -136,10 +135,26 @@ module BSV
|
|
|
136
135
|
request
|
|
137
136
|
end
|
|
138
137
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
138
|
+
# Coerce a transaction input to hex for the ARC JSON body.
|
|
139
|
+
#
|
|
140
|
+
# Accepts (in order of preference):
|
|
141
|
+
# 1. Hex string — pass-through, zero conversion
|
|
142
|
+
# 2. Binary string — convert to hex
|
|
143
|
+
# 3. Transaction object — prefer EF hex (BRC-30), fall back to raw hex
|
|
144
|
+
#
|
|
145
|
+
# Detection uses content, not encoding: a string is hex if it has even
|
|
146
|
+
# length and contains only hex characters. This handles hex strings
|
|
147
|
+
# tagged as ASCII-8BIT (e.g. read from IO in binary mode).
|
|
148
|
+
#
|
|
149
|
+
# @param tx [String, #to_ef_hex, #to_hex] transaction in any supported form
|
|
150
|
+
# @return [String] hex-encoded transaction
|
|
151
|
+
def resolve_tx_hex(tx)
|
|
152
|
+
if tx.is_a?(String)
|
|
153
|
+
return tx if tx.match?(/\A[0-9a-fA-F]*\z/) && tx.length.even?
|
|
154
|
+
|
|
155
|
+
return tx.unpack1('H*')
|
|
156
|
+
end
|
|
157
|
+
|
|
143
158
|
tx.to_ef_hex
|
|
144
159
|
rescue ArgumentError => e
|
|
145
160
|
BSV.logger&.debug { "[ARC] EF serialisation failed: #{e.message} — falling back to raw hex" }
|
|
@@ -171,159 +186,98 @@ module BSV
|
|
|
171
186
|
end
|
|
172
187
|
|
|
173
188
|
# Parse and validate a single-transaction ARC response.
|
|
189
|
+
#
|
|
190
|
+
# @return [ProtocolResponse]
|
|
174
191
|
def parse_single_broadcast_response(response)
|
|
175
192
|
code = response.code.to_i
|
|
176
193
|
body = safe_parse_json(response.body)
|
|
177
194
|
|
|
178
195
|
unless body.is_a?(Hash)
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
retryable: retryable_code?(code),
|
|
182
|
-
metadata: { status_code: code }
|
|
183
|
-
)
|
|
196
|
+
return ProtocolResponse.new(response, http_success: false,
|
|
197
|
+
error_message: "ARC returned #{body.class}, expected Hash")
|
|
184
198
|
end
|
|
185
199
|
|
|
186
200
|
unless (200..299).cover?(code)
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
201
|
+
return ProtocolResponse.new(
|
|
202
|
+
response,
|
|
203
|
+
http_success: false,
|
|
204
|
+
error_message: body['detail'] || body['title'] || "HTTP #{code}"
|
|
191
205
|
)
|
|
192
206
|
end
|
|
193
207
|
|
|
194
208
|
if rejected_status?(body)
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
return ProtocolResponse.new(
|
|
210
|
+
response,
|
|
211
|
+
http_success: false,
|
|
212
|
+
error_message: body['detail'] || body['title'] || body['txStatus'],
|
|
213
|
+
data: body
|
|
199
214
|
)
|
|
200
215
|
end
|
|
201
216
|
|
|
202
217
|
unless body['txid']
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
return ProtocolResponse.new(
|
|
219
|
+
response,
|
|
220
|
+
http_success: false,
|
|
221
|
+
error_message: body['detail'] || 'ARC returned a malformed 2xx response'
|
|
207
222
|
)
|
|
208
223
|
end
|
|
209
224
|
|
|
210
|
-
|
|
211
|
-
data: arc_data_from(body),
|
|
212
|
-
metadata: { arc_status: body['txStatus'].to_s.upcase }
|
|
213
|
-
)
|
|
225
|
+
ProtocolResponse.new(response, data: body)
|
|
214
226
|
end
|
|
215
227
|
|
|
216
228
|
# Parse and validate a batch ARC response. HTTP-level errors return a
|
|
217
|
-
# single
|
|
229
|
+
# single error ProtocolResponse; success data is a raw JSON array of hashes.
|
|
230
|
+
#
|
|
231
|
+
# @return [ProtocolResponse]
|
|
218
232
|
def parse_batch_broadcast_response(response)
|
|
219
233
|
code = response.code.to_i
|
|
220
234
|
body = safe_parse_json(response.body)
|
|
221
235
|
|
|
222
236
|
unless (200..299).cover?(code)
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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}"
|
|
227
241
|
)
|
|
228
242
|
end
|
|
229
243
|
|
|
230
244
|
unless body.is_a?(Array)
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
return ProtocolResponse.new(
|
|
246
|
+
response,
|
|
247
|
+
http_success: false,
|
|
248
|
+
error_message: 'ARC returned a malformed batch response'
|
|
235
249
|
)
|
|
236
250
|
end
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
Result::Success.new(data: items, metadata: {})
|
|
252
|
+
ProtocolResponse.new(response, data: body)
|
|
240
253
|
end
|
|
241
254
|
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
unless item.is_a?(Hash)
|
|
245
|
-
return Result::Error.new(
|
|
246
|
-
message: 'malformed batch item',
|
|
247
|
-
retryable: false,
|
|
248
|
-
metadata: {}
|
|
249
|
-
)
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
if rejected_status?(item)
|
|
253
|
-
Result::Error.new(
|
|
254
|
-
message: item['detail'] || item['title'] || item['txStatus'],
|
|
255
|
-
retryable: false,
|
|
256
|
-
metadata: { status_code: 200, arc_status: item['txStatus'].to_s.upcase, txid: item['txid'] }
|
|
257
|
-
)
|
|
258
|
-
elsif !item['txid']
|
|
259
|
-
Result::Error.new(
|
|
260
|
-
message: 'ARC returned a malformed 2xx response',
|
|
261
|
-
retryable: false,
|
|
262
|
-
metadata: { status_code: 200 }
|
|
263
|
-
)
|
|
264
|
-
else
|
|
265
|
-
Result::Success.new(
|
|
266
|
-
data: arc_data_from(item),
|
|
267
|
-
metadata: { arc_status: item['txStatus'].to_s.upcase }
|
|
268
|
-
)
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Escape hatch for get_tx_status: returns a normalised data hash using the
|
|
273
|
-
# same field set as broadcast responses rather than the raw parsed JSON.
|
|
274
|
-
# 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).
|
|
275
257
|
#
|
|
276
258
|
# @param txid [String] ARC API boundary: display-order hex transaction ID to query
|
|
277
|
-
# @return [
|
|
259
|
+
# @return [ProtocolResponse]
|
|
278
260
|
def call_get_tx_status(txid, **)
|
|
279
261
|
response = default_call(:get_tx_status, txid)
|
|
280
|
-
return response unless response.
|
|
262
|
+
return response unless response.http_success?
|
|
281
263
|
|
|
282
264
|
body = response.data
|
|
283
265
|
|
|
284
266
|
if rejected_status?(body)
|
|
285
|
-
return
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
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']
|
|
289
270
|
)
|
|
290
271
|
end
|
|
291
272
|
|
|
292
273
|
unless body['txid']
|
|
293
|
-
return
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
metadata: { status_code: 200 }
|
|
274
|
+
return response.with(
|
|
275
|
+
http_success: false,
|
|
276
|
+
error_message: 'ARC returned a malformed 2xx response'
|
|
297
277
|
)
|
|
298
278
|
end
|
|
299
279
|
|
|
300
|
-
|
|
301
|
-
data: arc_data_from(body),
|
|
302
|
-
metadata: { arc_status: body['txStatus'].to_s.upcase }
|
|
303
|
-
)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# Build the normalised ARC data hash from a parsed JSON response body.
|
|
307
|
-
# Includes all 8 fields that BroadcastResponse expects.
|
|
308
|
-
#
|
|
309
|
-
# @param body [Hash] parsed ARC JSON response
|
|
310
|
-
# @return [Hash]
|
|
311
|
-
def arc_data_from(body)
|
|
312
|
-
{
|
|
313
|
-
txid: body['txid'], # ARC API boundary: display-order hex from the ARC JSON response
|
|
314
|
-
tx_status: body['txStatus'],
|
|
315
|
-
message: body['title'],
|
|
316
|
-
extra_info: body['extraInfo'],
|
|
317
|
-
block_hash: body['blockHash'],
|
|
318
|
-
block_height: body['blockHeight'],
|
|
319
|
-
timestamp: body['timestamp'],
|
|
320
|
-
competing_txs: body['competingTxs']
|
|
321
|
-
}
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
# Determine whether a status code indicates a retryable failure.
|
|
325
|
-
def retryable_code?(code)
|
|
326
|
-
code == 429 || (500..599).cover?(code)
|
|
280
|
+
response
|
|
327
281
|
end
|
|
328
282
|
|
|
329
283
|
# Determine whether an ARC response body represents a rejected transaction.
|