bsv-sdk 0.12.1 → 0.14.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 +49 -0
- data/lib/bsv/auth/certificate.rb +4 -4
- data/lib/bsv/auth/verifiable_certificate.rb +1 -1
- data/lib/bsv/network/arc.rb +95 -224
- data/lib/bsv/network/protocol.rb +321 -0
- data/lib/bsv/network/protocols/arc.rb +351 -0
- data/lib/bsv/network/protocols/chaintracks.rb +39 -0
- data/lib/bsv/network/protocols/ordinals.rb +32 -0
- data/lib/bsv/network/protocols/taal_binary.rb +99 -0
- data/lib/bsv/network/protocols/woc_rest.rb +301 -0
- data/lib/bsv/network/protocols.rb +17 -0
- data/lib/bsv/network/provider.rb +123 -0
- data/lib/bsv/network/providers/gorilla_pool.rb +61 -0
- data/lib/bsv/network/providers/taal.rb +57 -0
- data/lib/bsv/network/providers/whats_on_chain.rb +72 -0
- data/lib/bsv/network/providers.rb +25 -0
- data/lib/bsv/network/result.rb +119 -0
- data/lib/bsv/network/whats_on_chain.rb +78 -40
- data/lib/bsv/network.rb +5 -0
- data/lib/bsv/overlay/admin_token_template.rb +2 -2
- data/lib/bsv/script/push_drop_template.rb +1 -1
- data/lib/bsv/transaction/chain_trackers/chaintracks.rb +45 -49
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +57 -50
- data/lib/bsv/transaction/chain_trackers.rb +3 -4
- data/lib/bsv/transaction/fee_models/live_policy.rb +3 -2
- data/lib/bsv/transaction/transaction.rb +52 -7
- data/lib/bsv/transaction/verification_error.rb +11 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv-sdk.rb +1 -5
- metadata +14 -5
- data/lib/bsv/messages.rb +0 -16
- data/lib/bsv/wallet/insufficient_funds_error.rb +0 -15
- data/lib/bsv/wallet/wallet.rb +0 -120
- data/lib/bsv/wallet.rb +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4532a163439721455da80290d8b02342e597a01bb1e00b259d13343f8f9bd592
|
|
4
|
+
data.tar.gz: b7cfda4a8084b96c28f34afb6829fcba9f5be0dabb01fca536147bfe2c66bad8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed0b97db3cec8503eba509f232a463d8566373b34ccaf5fe7cd130a72e712cde17b083b4ce94ef64fa84786d6e6aec8965423ca6b8a7471ada70ebd522956e2a
|
|
7
|
+
data.tar.gz: 0747a93bef059a8eccf0f504e703d20ddc875f8c562db8f01d3af4050bf7562deb262eb4494db4d98b78665f91274ed77754fa76e99254b8592b7e0f1526aa13
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,55 @@ 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.14.0 — 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `BSV::Network::WhatsOnChain` expanded with `current_height`,
|
|
12
|
+
`get_block_header(height)`, and `valid_root_for_height?(root, height)` —
|
|
13
|
+
a single WhatsOnChain instance now serves as a complete chain data source
|
|
14
|
+
(#596)
|
|
15
|
+
- BEEF-based SPV verification conformance tests (#607)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `Transaction#verify` now raises `VerificationError` for all failure modes:
|
|
19
|
+
`:script_failure` (wraps `ScriptError` with cause chaining),
|
|
20
|
+
`:missing_source` (replaces `ArgumentError`), `:invalid_merkle_proof`,
|
|
21
|
+
`:insufficient_fee`, and `:output_overflow`. Code rescuing `ArgumentError`
|
|
22
|
+
or `ScriptError` from `verify` must switch to `VerificationError` (#608)
|
|
23
|
+
- Removed dead code: `BSV::Wallet::Wallet` (superseded by bsv-wallet gem's
|
|
24
|
+
`Client`), `BSV::Messages` (unused re-export alias), and duplicate
|
|
25
|
+
`BSV::Wallet::InsufficientFundsError` (#594)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- WhatsOnChain `valid_root_for_height?` YARD doc now correctly documents
|
|
29
|
+
that 404 returns `false` rather than raising
|
|
30
|
+
|
|
31
|
+
## 0.13.0 — 2026-04-21
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- Protocol layer: base class with declarative DSL, HTTP dispatch, and Result value types
|
|
35
|
+
- WoCREST protocol with transaction detail, script query, exchange rate, fee, mempool, SPV, wallet, and address commands
|
|
36
|
+
- ARC protocol with broadcast escape hatches and enriched response data
|
|
37
|
+
- TAALBinary protocol with binary broadcast support
|
|
38
|
+
- Chaintracks and Ordinals protocols with base_url override
|
|
39
|
+
- Provider configuration container with block DSL and `.default(testnet:)` pattern
|
|
40
|
+
- Provider introspection and capability matrix
|
|
41
|
+
- Concrete provider defaults for GorillaPool, WhatsOnChain, and TAAL
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- Facades (ARC, WhatsOnChain, ChainTracker) hollowed out to delegate via Protocol
|
|
45
|
+
- Removed `BSV::MAINNET_URL`/`TESTNET_URL` constants and ENV var reading — provider defaults are the single source of truth
|
|
46
|
+
- Removed `BASE_URL` from protocols — `base_url:` is now mandatory
|
|
47
|
+
- Wallet namespace reorganisation: `Store` and `BroadcastQueue` collaborators namespaced under `Client`
|
|
48
|
+
- `ProtoWallet` and `WalletClient` replaced with `Client`
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Ruby 2.7 kwargs compatibility in `Protocol#call` and TAALBinary spec doubles
|
|
52
|
+
- Nil-guard on WoCREST broadcast and ARC status metadata
|
|
53
|
+
- ARC status rejection checks and TAALBinary nil-txid guard
|
|
54
|
+
- `BSV::Wallet::Wallet` autoload failure in multi-gem setup
|
|
55
|
+
- SimpleCov coverage gate now requires `COVERAGE=true` explicitly
|
|
56
|
+
|
|
8
57
|
## 0.12.1 — 2026-04-16
|
|
9
58
|
|
|
10
59
|
### Fixed
|
data/lib/bsv/auth/certificate.rb
CHANGED
|
@@ -21,7 +21,7 @@ module BSV
|
|
|
21
21
|
#
|
|
22
22
|
# Wallet parameters are duck-typed — any object responding to
|
|
23
23
|
# +create_signature+, +verify_signature+, and +get_public_key+ is accepted.
|
|
24
|
-
# No direct dependency on +BSV::Wallet::
|
|
24
|
+
# No direct dependency on +BSV::Wallet::Client+ is introduced here.
|
|
25
25
|
#
|
|
26
26
|
# @see https://hub.bsvblockchain.org/brc/wallet/0052 BRC-52
|
|
27
27
|
class Certificate
|
|
@@ -166,17 +166,17 @@ module BSV
|
|
|
166
166
|
|
|
167
167
|
# Verify the certificate's signature.
|
|
168
168
|
#
|
|
169
|
-
# Uses a fresh +'anyone'+
|
|
169
|
+
# Uses a fresh +'anyone'+ Client as the verifier, which matches the
|
|
170
170
|
# TS SDK behaviour. If no signature is present, raises +ArgumentError+.
|
|
171
171
|
#
|
|
172
172
|
# @param verifier_wallet [#verify_signature, nil] wallet to verify with;
|
|
173
|
-
# defaults to +BSV::Wallet::
|
|
173
|
+
# defaults to +BSV::Wallet::Client.new('anyone', storage: BSV::Wallet::Store::Memory.new)+
|
|
174
174
|
# @return [Boolean] +true+ if the signature is valid
|
|
175
175
|
# @raise [ArgumentError] if the certificate has no signature
|
|
176
176
|
def verify(verifier_wallet = nil)
|
|
177
177
|
raise ArgumentError, 'certificate has no signature to verify' if @signature.nil? || @signature.empty?
|
|
178
178
|
|
|
179
|
-
verifier_wallet ||= BSV::Wallet::
|
|
179
|
+
verifier_wallet ||= BSV::Wallet::Client.new('anyone', storage: BSV::Wallet::Store::Memory.new, allow_memory_store: true)
|
|
180
180
|
preimage = to_binary(include_signature: false)
|
|
181
181
|
sig_bytes = [@signature].pack('H*').unpack('C*')
|
|
182
182
|
|
|
@@ -21,7 +21,7 @@ module BSV
|
|
|
21
21
|
# - Counterparty: the certificate +subject+ public key
|
|
22
22
|
#
|
|
23
23
|
# Wallet parameters are duck-typed — any object responding to +:decrypt+
|
|
24
|
-
# is accepted. No direct dependency on +BSV::Wallet::
|
|
24
|
+
# is accepted. No direct dependency on +BSV::Wallet::Client+ is
|
|
25
25
|
# introduced here.
|
|
26
26
|
#
|
|
27
27
|
# @see BSV::Auth::Certificate base class
|
data/lib/bsv/network/arc.rb
CHANGED
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require 'securerandom'
|
|
7
|
-
|
|
8
3
|
module BSV
|
|
9
4
|
module Network
|
|
10
5
|
# ARC broadcaster for submitting transactions to the BSV network.
|
|
11
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
12
|
# Any object responding to #broadcast(tx) can serve as a broadcaster;
|
|
13
13
|
# this class implements that contract using the ARC API.
|
|
14
|
-
#
|
|
15
|
-
# The HTTP client is injectable for testability. It must respond to
|
|
16
|
-
# #request(uri, request) and return an object with #code and #body.
|
|
17
14
|
class ARC
|
|
18
15
|
# Returns an ARC instance pointed at the GorillaPool public ARC endpoint.
|
|
19
16
|
#
|
|
20
17
|
# @param testnet [Boolean] when true, uses the GorillaPool testnet endpoint
|
|
21
|
-
# @param
|
|
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
22
|
# @return [ARC]
|
|
23
|
-
def self.default(testnet: false, **opts)
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
)
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
# ARC response statuses that indicate the transaction was NOT accepted.
|
|
29
|
-
# Matches the TypeScript SDK's ARC broadcaster failure set (issue #305,
|
|
30
|
-
# finding F5.13). Prior to this fix, Ruby only recognised REJECTED and
|
|
31
|
-
# DOUBLE_SPEND_ATTEMPTED, silently treating INVALID / MALFORMED /
|
|
32
|
-
# MINED_IN_STALE_BLOCK responses as successful broadcasts.
|
|
33
36
|
REJECTED_STATUSES = %w[
|
|
34
37
|
REJECTED
|
|
35
38
|
DOUBLE_SPEND_ATTEMPTED
|
|
@@ -38,10 +41,11 @@ module BSV
|
|
|
38
41
|
MINED_IN_STALE_BLOCK
|
|
39
42
|
].freeze
|
|
40
43
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
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.
|
|
45
49
|
# @param api_key [String, nil] optional bearer token for Authorization
|
|
46
50
|
# @param deployment_id [String, nil] optional deployment identifier for
|
|
47
51
|
# the +XDeployment-ID+ header; defaults to a per-instance random value
|
|
@@ -50,246 +54,113 @@ module BSV
|
|
|
50
54
|
# @param callback_token [String, nil] optional +X-CallbackToken+ for
|
|
51
55
|
# ARC status callback authentication
|
|
52
56
|
# @param http_client [#request, nil] injectable HTTP client for testing
|
|
53
|
-
def initialize(
|
|
57
|
+
def initialize(url_or_protocol, api_key: nil, deployment_id: nil, callback_url: nil,
|
|
54
58
|
callback_token: nil, http_client: nil)
|
|
55
|
-
@
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
72
|
end
|
|
62
73
|
|
|
63
74
|
# Submit a transaction to ARC.
|
|
64
75
|
#
|
|
65
|
-
# The transaction is encoded as Extended Format (BRC-30) hex when every
|
|
66
|
-
# input has +source_satoshis+ and +source_locking_script+ populated,
|
|
67
|
-
# which lets ARC validate sighashes without fetching parents. Falls back
|
|
68
|
-
# to plain raw-tx hex when EF is unavailable.
|
|
69
|
-
#
|
|
70
76
|
# @param tx [Transaction] the transaction to broadcast
|
|
71
|
-
# @param wait_for [String, nil] ARC wait condition
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# connection open until the transaction reaches the requested
|
|
75
|
-
# state (or times out). Defaults to nil (no wait).
|
|
76
|
-
# @param skip_fee_validation [Boolean, nil] when truthy, sends the
|
|
77
|
-
# +X-SkipFeeValidation: true+ header, asking ARC to bypass its
|
|
78
|
-
# minimum-fee check. Useful for zero-fee data transactions or
|
|
79
|
-
# during local testing. Defaults to nil (fee validation applies).
|
|
80
|
-
# @param skip_script_validation [Boolean, nil] when truthy, sends the
|
|
81
|
-
# +X-SkipScriptValidation: true+ header, asking ARC to bypass
|
|
82
|
-
# script correctness checks. Defaults to nil (script validation
|
|
83
|
-
# applies).
|
|
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
|
|
84
80
|
# @return [BroadcastResponse]
|
|
85
81
|
# @raise [BroadcastError] when ARC returns a non-2xx HTTP status or a
|
|
86
82
|
# rejected/orphan +txStatus+
|
|
87
83
|
def broadcast(tx, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
handle_broadcast_response(response)
|
|
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)
|
|
96
91
|
end
|
|
97
92
|
|
|
98
93
|
# Submit multiple transactions to ARC in a single batch request.
|
|
99
94
|
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# Returns a mixed array of {BroadcastResponse} and {BroadcastError} objects
|
|
104
|
-
# — one element per submitted transaction in the same order. Per-transaction
|
|
105
|
-
# rejections are returned as {BroadcastError} values rather than raised, so
|
|
106
|
-
# callers can inspect the full result set even when some transactions fail.
|
|
107
|
-
# Only HTTP-level errors (non-2xx) raise a {BroadcastError} for the whole
|
|
108
|
-
# batch.
|
|
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.
|
|
109
98
|
#
|
|
110
99
|
# @param txs [Array<Transaction>] transactions to broadcast
|
|
111
|
-
# @param wait_for [String, nil] ARC wait condition
|
|
112
|
-
# @param skip_fee_validation [Boolean, nil]
|
|
113
|
-
#
|
|
114
|
-
# @param skip_script_validation [Boolean, nil] when truthy, sends
|
|
115
|
-
# +X-SkipScriptValidation: true+ for the batch request
|
|
100
|
+
# @param wait_for [String, nil] ARC wait condition
|
|
101
|
+
# @param skip_fee_validation [Boolean, nil]
|
|
102
|
+
# @param skip_script_validation [Boolean, nil]
|
|
116
103
|
# @return [Array<BroadcastResponse, BroadcastError>]
|
|
117
|
-
# @raise [BroadcastError] when ARC returns a non-2xx HTTP status
|
|
118
|
-
# malformed (non-array) response body
|
|
104
|
+
# @raise [BroadcastError] when ARC returns a non-2xx HTTP status
|
|
119
105
|
def broadcast_many(txs, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
request.body = JSON.generate(txs.map { |tx| { rawTx: raw_tx_hex(tx) } })
|
|
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
|
+
)
|
|
127
112
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
119
|
end
|
|
131
120
|
|
|
132
121
|
# Query the status of a previously submitted transaction.
|
|
133
|
-
#
|
|
122
|
+
#
|
|
123
|
+
# @param txid [String] transaction ID to query
|
|
124
|
+
# @return [BroadcastResponse]
|
|
125
|
+
# @raise [BroadcastError] on failure
|
|
134
126
|
def status(txid)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
request['XDeployment-ID'] = @deployment_id
|
|
138
|
-
apply_auth_header(request)
|
|
139
|
-
|
|
140
|
-
response = execute(uri, request)
|
|
141
|
-
handle_broadcast_response(response)
|
|
127
|
+
result = @protocol.call(:get_tx_status, txid)
|
|
128
|
+
result_to_response!(result)
|
|
142
129
|
end
|
|
143
130
|
|
|
144
131
|
private
|
|
145
132
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rescue ArgumentError
|
|
152
|
-
tx.to_hex
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def build_post_request(uri, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
|
|
156
|
-
request = Net::HTTP::Post.new(uri)
|
|
157
|
-
request['Content-Type'] = 'application/json'
|
|
158
|
-
request['XDeployment-ID'] = @deployment_id
|
|
159
|
-
request['X-WaitFor'] = wait_for if wait_for
|
|
160
|
-
request['X-CallbackUrl'] = @callback_url if @callback_url
|
|
161
|
-
request['X-CallbackToken'] = @callback_token if @callback_token
|
|
162
|
-
request['X-SkipFeeValidation'] = 'true' if skip_fee_validation
|
|
163
|
-
request['X-SkipScriptValidation'] = 'true' if skip_script_validation
|
|
164
|
-
apply_auth_header(request)
|
|
165
|
-
request
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def apply_auth_header(request)
|
|
169
|
-
request['Authorization'] = "Bearer #{@api_key}" if @api_key
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def execute(uri, request)
|
|
173
|
-
if @http_client
|
|
174
|
-
@http_client.request(uri, request)
|
|
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)
|
|
175
138
|
else
|
|
176
|
-
|
|
177
|
-
http.request(request)
|
|
178
|
-
end
|
|
139
|
+
raise broadcast_error_from_result(result)
|
|
179
140
|
end
|
|
180
141
|
end
|
|
181
142
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
txid: body['txid']
|
|
191
|
-
)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
if rejected_status?(body)
|
|
195
|
-
raise BroadcastError.new(
|
|
196
|
-
body['detail'] || body['title'] || body['txStatus'],
|
|
197
|
-
status_code: code,
|
|
198
|
-
txid: body['txid'],
|
|
199
|
-
arc_status: body['txStatus'].to_s.upcase
|
|
200
|
-
)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# A 2xx response without a txid is a malformed ARC reply —
|
|
204
|
-
# `parse_json` falls back to `{'detail' => raw}` on non-JSON,
|
|
205
|
-
# which would otherwise produce a `BroadcastResponse` full of
|
|
206
|
-
# `nil`s and `success? => true`. That's the same silent
|
|
207
|
-
# success-as-failure class of bug F5.13 closed for explicit
|
|
208
|
-
# error statuses; closing it here for shape corruption too.
|
|
209
|
-
unless body['txid']
|
|
210
|
-
raise BroadcastError.new(
|
|
211
|
-
'ARC returned a malformed 2xx response',
|
|
212
|
-
status_code: code
|
|
213
|
-
)
|
|
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)
|
|
214
151
|
end
|
|
215
|
-
|
|
216
|
-
build_response(body)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def rejected_status?(body)
|
|
220
|
-
# Case-insensitive match — the TypeScript reference
|
|
221
|
-
# (`ts-sdk/src/transaction/broadcasters/ARC.ts:155-166`) explicitly
|
|
222
|
-
# `.toUpperCase()`s both fields before membership / substring checks.
|
|
223
|
-
# ARC has a documented history of emitting values outside its own
|
|
224
|
-
# OpenAPI enum (e.g. `txStatus: "success"` for orphans in TS issue
|
|
225
|
-
# #105), so case normalisation is the defensive choice.
|
|
226
|
-
tx_status = body['txStatus'].to_s.upcase
|
|
227
|
-
return true if REJECTED_STATUSES.include?(tx_status)
|
|
228
|
-
return true if tx_status.include?(ORPHAN_MARKER)
|
|
229
|
-
|
|
230
|
-
extra_info = body['extraInfo'].to_s.upcase
|
|
231
|
-
return true if extra_info.include?(ORPHAN_MARKER)
|
|
232
|
-
|
|
233
|
-
false
|
|
234
152
|
end
|
|
235
153
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
txid: body['txid'],
|
|
245
|
-
tx_status: body['txStatus'],
|
|
246
|
-
message: body['title'],
|
|
247
|
-
extra_info: body['extraInfo'],
|
|
248
|
-
block_hash: body['blockHash'],
|
|
249
|
-
block_height: body['blockHeight'],
|
|
250
|
-
timestamp: body['timestamp'],
|
|
251
|
-
competing_txs: body['competingTxs']
|
|
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]
|
|
252
162
|
)
|
|
253
163
|
end
|
|
254
|
-
|
|
255
|
-
def handle_batch_response(response)
|
|
256
|
-
code = response.code.to_i
|
|
257
|
-
body = parse_json(response.body)
|
|
258
|
-
|
|
259
|
-
unless (200..299).cover?(code)
|
|
260
|
-
raise BroadcastError.new(
|
|
261
|
-
body['detail'] || body['title'] || "HTTP #{code}",
|
|
262
|
-
status_code: code
|
|
263
|
-
)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
unless body.is_a?(Array)
|
|
267
|
-
raise BroadcastError.new(
|
|
268
|
-
'ARC returned a malformed batch response',
|
|
269
|
-
status_code: code
|
|
270
|
-
)
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
body.map { |item| build_response_or_error(item) }
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def build_response_or_error(body)
|
|
277
|
-
if rejected_status?(body)
|
|
278
|
-
BroadcastError.new(
|
|
279
|
-
body['detail'] || body['title'] || body['txStatus'],
|
|
280
|
-
status_code: 200,
|
|
281
|
-
txid: body['txid'],
|
|
282
|
-
arc_status: body['txStatus'].to_s.upcase
|
|
283
|
-
)
|
|
284
|
-
elsif !body['txid']
|
|
285
|
-
BroadcastError.new(
|
|
286
|
-
'ARC returned a malformed 2xx response',
|
|
287
|
-
status_code: 200
|
|
288
|
-
)
|
|
289
|
-
else
|
|
290
|
-
build_response(body)
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
164
|
end
|
|
294
165
|
end
|
|
295
166
|
end
|