bsv-sdk 0.19.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/README.md +2 -2
  4. data/lib/bsv/auth/transport.rb +1 -1
  5. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
  6. data/lib/bsv/network/protocol.rb +4 -5
  7. data/lib/bsv/network/protocols/arc.rb +4 -30
  8. data/lib/bsv/network/protocols/arcade.rb +163 -0
  9. data/lib/bsv/network/protocols/chaintracks.rb +6 -3
  10. data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
  11. data/lib/bsv/network/protocols.rb +1 -0
  12. data/lib/bsv/network/provider.rb +7 -9
  13. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  14. data/lib/bsv/network/util.rb +44 -0
  15. data/lib/bsv/network.rb +1 -0
  16. data/lib/bsv/overlay/lookup_resolver.rb +0 -1
  17. data/lib/bsv/overlay/topic_broadcaster.rb +0 -1
  18. data/lib/bsv/primitives/curve.rb +1 -11
  19. data/lib/bsv/primitives/ecies.rb +1 -8
  20. data/lib/bsv/primitives/hex.rb +1 -1
  21. data/lib/bsv/script/script.rb +1 -1
  22. data/lib/bsv/transaction/beef.rb +0 -2
  23. data/lib/bsv/transaction/chain_tracker.rb +74 -13
  24. data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +3 -3
  25. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  26. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  27. data/lib/bsv/transaction/merkle_path.rb +0 -2
  28. data/lib/bsv/version.rb +1 -1
  29. data/lib/bsv/wallet/errors.rb +65 -21
  30. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  31. data/lib/bsv/wallet/proto_wallet.rb +15 -8
  32. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  33. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  34. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  35. data/lib/bsv/wallet/serializer/common.rb +207 -0
  36. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  37. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  38. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  39. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  40. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  41. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  42. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  43. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  44. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  45. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  46. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  47. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  48. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  49. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  50. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  51. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  52. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  53. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  54. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  55. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  56. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  57. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  58. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  59. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  60. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  61. data/lib/bsv/wallet/serializer/status.rb +85 -0
  62. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  63. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  64. data/lib/bsv/wallet/serializer.rb +180 -0
  65. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  66. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  67. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  68. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  69. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  70. data/lib/bsv/wallet/wire/calls.rb +79 -0
  71. data/lib/bsv/wallet/wire/frame.rb +181 -0
  72. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  73. data/lib/bsv/wallet/wire/validation.rb +213 -0
  74. data/lib/bsv/wallet/wire.rb +13 -0
  75. data/lib/bsv/wallet.rb +17 -0
  76. metadata +47 -3
  77. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffaea2263b4744cda53ec2ad10f1f842fbd19b6baf07ed0268788a2411a5f42a
4
- data.tar.gz: 34ea44a136351a85aa373e6cd8e089dd0cf52d273bf162b3f029f8fd1091448c
3
+ metadata.gz: d666b0e5af3155dbe92f4298f5448a5792275848f6e3c08fcc1bbfa09d9a20bd
4
+ data.tar.gz: c4f3beba29031b049afe90a967b5d982ee473cea57ee077c09ba9c263385c067
5
5
  SHA512:
6
- metadata.gz: 6eb51aad42cb0ff70bd210c01ac8bada4d32ca38ce574ec9d63ef944d6e75f78902974ffcd35eb290260a81efc375d1bac9b40a82ab083bdbdfa5fb541fc10d4
7
- data.tar.gz: 87a040c985ef1d70b131db723c58367b67ce7b6d3cc68d42d70e37a74b573fabd9354dcffc521214671f49bb75d2635cbee89fcc4c3870b5da4292587f84854e
6
+ metadata.gz: ce7d22f5b2267f44909e871e15c619af28e0055db71f84a5fc7529c70c7c030d0314f466d328a2154c476530a6485b19588e09a96ce7ae3f406f29cfd6ec8569
7
+ data.tar.gz: 46cfee8000801ffd398d659b6174b9ac48cc2e4fc098c93e367634f06cfc9d9c659211ef51c452fd7c83262121e8923c6ab7240588fe01782c3002a6fa4f8075
data/CHANGELOG.md CHANGED
@@ -5,6 +5,95 @@ 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.22.0 — 2026-05-30
9
+
10
+ ### Added
11
+ - BRC-103 wire layer: `WalletWire` transport abstraction, `WalletWireTransceiver` (client), `WalletWireProcessor` (server), `Substrates::HTTPWalletWire` (binary HTTP transport), and `Substrates::HTTPWalletJSON` (JSON-over-HTTP, MetaNet Desktop compatible)
12
+ - Full BRC-103 serialisers for all 28 BRC-100 methods; wire format compatible with go-sdk byte-for-byte
13
+ - `BSV::Wallet.error_from_wire` helper rehydrates error frames into typed `BSV::Wallet::Error` subclasses (codes 1–7)
14
+ - New guide: `docs/guides/brc-103-wire.md` — end-to-end walkthrough of the wire layer
15
+ - Wire layer reference section added to `docs/sdk/wallet.md`
16
+ - `BSV::Transaction::ChainTracker.default(testnet: false)` — provider-routed chain tracker backed by GorillaPool + JungleBus by default; no credentials required (closes #783)
17
+ - `BSV::Network::Protocols::JungleBus` declares `:current_height` so the GorillaPool provider can satisfy chain-tip queries (#784)
18
+ - `BSV::Network::Providers::GorillaPool.testnet` now registers `Protocols::JungleBus` alongside `Protocols::Arcade` so chain-header lookups work on testnet
19
+
20
+ ### Changed
21
+ - `BSV::Transaction::ChainTracker` is now a working default implementation when constructed with a `Provider`; subclasses that override `valid_root_for_height?` and `current_height` continue to work unchanged (constructor now accepts an optional positional `provider` argument: `ChainTracker.new(provider)`, with `provider = nil` preserved for subclass-only use)
22
+
23
+ ### Removed (breaking)
24
+ - **`BSV::Transaction::ChainTrackers::Chaintracks`** is removed. Migrate to
25
+ `BSV::Transaction::ChainTracker.default` for general use (GorillaPool), or construct
26
+ a single-protocol Provider explicitly for own-server use:
27
+
28
+ ```ruby
29
+ own = BSV::Network::Provider.new('local') do |p|
30
+ p.protocol BSV::Network::Protocols::Chaintracks, base_url: 'http://my-server'
31
+ end
32
+ tracker = BSV::Transaction::ChainTracker.new(own)
33
+ ```
34
+
35
+ - **`BSV::Transaction::ChainTrackers.default`** (the namespace-level factory method) is
36
+ removed. Use `BSV::Transaction::ChainTracker.default` (the singular class-level
37
+ method) instead. The `ChainTrackers` namespace is now a pure autoload container for
38
+ concrete protocol-specific wrappers; `ChainTracker` (singular) is the porcelain entry
39
+ point.
40
+
41
+ - Closes #778 — GorillaPool chaintracks connectivity: chain data is now reachable
42
+ through the provider via JungleBus; the open question dissolves.
43
+
44
+ ### Note
45
+ - The default `ChainTracker` in the Ruby SDK resolves against GorillaPool/JungleBus,
46
+ which diverges from the TS SDK (WhatsOnChain) and Go/Python SDK defaults. This
47
+ reflects the broader pattern of GorillaPool being the default broadcast provider in
48
+ the Ruby SDK.
49
+
50
+ ## 0.21.0 — 2026-05-29 (not separately released — see 0.22.0)
51
+
52
+ ### Added
53
+ - `Protocols::Arcade` for `bsv-blockchain/arcade` services (run by GorillaPool at
54
+ `arcade.gorillapool.io`). Commands: `broadcast` (`POST /tx`), `get_tx_status`
55
+ (`GET /tx/{txid}`), `health` (`GET /health`). Distinct from `Protocols::ARC` —
56
+ paths, response bodies, and status taxonomies do not overlap. (#775)
57
+ - Shared `BSV::Network::Util` helpers: `safe_parse_json`, `resolve_tx_hex` —
58
+ available to all protocol classes without copy-paste. (#775)
59
+
60
+ ### Changed
61
+ - `ARC#call_broadcast` and `ARC#call_broadcast_many` now read their broadcast and
62
+ batch-broadcast paths from the endpoint table rather than hardcoding `/v1/tx` and
63
+ `/v1/txs`. Behaviour is unchanged; subclasses can now declare a different path. (#775)
64
+ - `LivePolicy.default` now points at `https://arc.taal.com` for fee-policy queries
65
+ rather than GorillaPool's Arcade endpoint, which does not expose `/v1/policy`. (#775)
66
+ - `broadcast_p2pkh` MCP tool updated to handle Arcade's `{"status": "submitted"}`
67
+ response shape from GorillaPool. (#775)
68
+
69
+ ### Breaking Changes
70
+ - **`Providers::GorillaPool` no longer registers `Protocols::ARC` or
71
+ `Protocols::Chaintracks`.** Mainnet now registers `Protocols::Arcade` instead.
72
+ Consumers calling `provider.call(:broadcast, ...)` against GorillaPool and
73
+ expecting an ARC-shaped response (`{txid, txStatus, ...}`) must migrate to
74
+ Arcade's response shape (`{status: "submitted"}`). No compatibility shim is
75
+ provided — pre-1.0 clean break. (#775)
76
+ - **`BSV_TESTNET_ARC_URL` environment variable renamed to `BSV_TESTNET_ARCADE_URL`.**
77
+ Update any scripts or CI configuration that set this variable. Default value:
78
+ `https://testnet.arcade.gorillapool.io`. (#775, #779)
79
+ - **`Providers::GorillaPool` no longer provides `:current_height` or
80
+ `:get_block_header` commands** (previously routed through the broken
81
+ `Protocols::Chaintracks` registration). A follow-up issue (#778) tracks correctly
82
+ wiring GorillaPool's chaintracks_server at its actual separate URL/port.
83
+
84
+ ### Removed
85
+ - `Protocols::Chaintracks` registration from `Providers::GorillaPool` (mainnet and
86
+ testnet). The registration was broken — GorillaPool's chaintracks_server runs as
87
+ a separate service on a separate port; the `/chaintracks/v2/` paths were returning
88
+ 404. See #778 for the follow-up to wire it correctly.
89
+
90
+ ## 0.20.0 — 2026-05-24
91
+
92
+ ### Changed
93
+ - Raise minimum Ruby version from 2.7 to 3.3; add Ruby 4.0 to CI matrix
94
+ - Remove Ruby 2.7 compatibility workarounds (Point#add fallback, OpenSSL.fixed_length_secure_compare fallback)
95
+ - Apply RuboCop modernisations unlocked by Ruby 3.3 target (anonymous forwarding, redundant `require 'set'`, `Hash#except`)
96
+
8
97
  ## 0.19.1 — 2026-05-13
9
98
 
10
99
  ### Fixed
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/sgbett/bsv-ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/sgbett/bsv-ruby-sdk/actions/workflows/ci.yml)
4
4
  [![codecov](https://codecov.io/gh/sgbett/bsv-ruby-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/sgbett/bsv-ruby-sdk)
5
5
  [![Gem Version](https://img.shields.io/gem/v/bsv-sdk)](https://rubygems.org/gems/bsv-sdk)
6
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-red)](https://rubygems.org/gems/bsv-sdk)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-red)](https://rubygems.org/gems/bsv-sdk)
7
7
 
8
8
  Welcome to the BSV Blockchain Libraries Project, the comprehensive Ruby SDK designed to provide an updated and unified layer for developing scalable applications on the BSV Blockchain. This SDK addresses the limitations of previous tools by offering a fresh, peer-to-peer approach, adhering to SPV, and ensuring privacy and scalability.
9
9
 
@@ -44,7 +44,7 @@ Elliptic curve operations (secp256k1) are provided by the [`secp256k1-native`](h
44
44
 
45
45
  ### Requirements
46
46
 
47
- - Ruby >= 2.7
47
+ - Ruby >= 3.3
48
48
  - No external dependencies beyond Ruby's standard library (`openssl` for hashing, HMAC, PBKDF2, and AES)
49
49
 
50
50
  ### Installation
@@ -37,7 +37,7 @@ module BSV
37
37
  #
38
38
  # @yieldparam message [Hash] the incoming auth message
39
39
  # @return [void]
40
- def on_data(&_block)
40
+ def on_data(&)
41
41
  raise NotImplementedError, "#{self.class}#on_data not implemented"
42
42
  end
43
43
  end
@@ -115,11 +115,13 @@ module BSV
115
115
  arc_result = arc.call(:broadcast, tx)
116
116
  return Helpers.error_response("Broadcast failed: #{arc_result.message}") unless arc_result.http_success?
117
117
 
118
+ data = arc_result.data || {}
118
119
  result = {
119
- txid: arc_result.data['txid'], # MCP tool boundary: display-order hex from ARC response
120
- tx_status: arc_result.data['txStatus'],
120
+ txid: data['txid'], # MCP tool boundary: display-order hex from Arcade response (present on re-submission)
121
+ tx_status: data['status'],
122
+ state: data['state'],
121
123
  hex: tx.to_hex
122
- }
124
+ }.compact
123
125
 
124
126
  ::MCP::Tool::Response.new(
125
127
  [::MCP::Content::Text.new(result.to_json)],
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
3
  require 'net/http'
5
4
  require 'json'
6
5
  require 'uri'
@@ -132,11 +131,11 @@ module BSV
132
131
  # Subscriptions are not callable; calling one raises +NotImplementedError+.
133
132
  #
134
133
  # @param command_name [Symbol, String] command to invoke
135
- # @param args [Array] positional arguments forwarded to path interpolation
134
+ # @param * [Array] positional arguments forwarded to path interpolation
136
135
  # @param kwargs [Hash] keyword arguments forwarded to path interpolation
137
136
  # @return [ProtocolResponse]
138
137
  # @raise [ArgumentError] when command_name is not registered
139
- def call(command_name, *args, **kwargs)
138
+ def call(command_name, *, **kwargs)
140
139
  name = command_name.to_sym
141
140
 
142
141
  if self.class.subscriptions.key?(name)
@@ -147,10 +146,10 @@ module BSV
147
146
  escape = :"call_#{name}"
148
147
  if respond_to?(escape, true)
149
148
  BSV.logger&.debug { "[Protocol] #{self.class.name} :#{name} → escape hatch" }
150
- return kwargs.empty? ? send(escape, *args) : send(escape, *args, **kwargs)
149
+ return kwargs.empty? ? send(escape, *) : send(escape, *, **kwargs)
151
150
  end
152
151
 
153
- default_call(name, *args, **kwargs)
152
+ default_call(name, *, **kwargs)
154
153
  end
155
154
 
156
155
  # Dispatches a command directly via HTTP, bypassing any escape hatch.
@@ -92,7 +92,7 @@ module BSV
92
92
  callback_batch: callback_batch
93
93
  )
94
94
 
95
- response = post_with_headers('/v1/tx', body, extra_headers)
95
+ response = post_with_headers(self.class.endpoints[:broadcast][:path], body, extra_headers)
96
96
  parse_single_broadcast_response(response)
97
97
  end
98
98
 
@@ -124,7 +124,7 @@ module BSV
124
124
  callback_batch: callback_batch
125
125
  )
126
126
 
127
- response = post_with_headers('/v1/txs', body, extra_headers)
127
+ response = post_with_headers(self.class.endpoints[:broadcast_many][:path], body, extra_headers)
128
128
  parse_batch_broadcast_response(response)
129
129
  end
130
130
 
@@ -135,30 +135,8 @@ module BSV
135
135
  request
136
136
  end
137
137
 
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
138
  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
-
158
- tx.to_ef_hex
159
- rescue ArgumentError => e
160
- BSV.logger&.debug { "[ARC] EF serialisation failed: #{e.message} — falling back to raw hex" }
161
- tx.to_hex
139
+ BSV::Network::Util.resolve_tx_hex(tx)
162
140
  end
163
141
 
164
142
  # Build the hash of ARC-specific extra headers.
@@ -296,12 +274,8 @@ module BSV
296
274
  false
297
275
  end
298
276
 
299
- # Parse JSON, returning a hash with a 'detail' key on parse failure.
300
- # When the raw input is nil or empty the detail is nil (not an empty string).
301
277
  def safe_parse_json(raw)
302
- JSON.parse(raw.to_s)
303
- rescue JSON::ParserError
304
- { 'detail' => (raw.to_s.empty? ? nil : raw.to_s) }
278
+ BSV::Network::Util.safe_parse_json(raw)
305
279
  end
306
280
  end
307
281
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module BSV
6
+ module Network
7
+ module Protocols
8
+ # Arcade protocol implementation for submitting transactions to the BSV network.
9
+ #
10
+ # Arcade is a sibling to ARC — both subclass Protocol directly. The request
11
+ # and response shapes diverge enough to rule out a shared abstract base:
12
+ # Arcade uses a narrower status taxonomy and a different broadcast response
13
+ # structure (no txid on fresh submission; idempotent re-submit returns
14
+ # status plus txid plus state).
15
+ #
16
+ # == Example
17
+ #
18
+ # arcade = BSV::Network::Protocols::Arcade.new(
19
+ # base_url: 'https://arcade.gorillapool.io'
20
+ # )
21
+ # result = arcade.call(:broadcast, tx)
22
+ # result.http_success? # => true
23
+ # result.data['status'] # => "submitted"
24
+ #
25
+ # @see https://github.com/bsv-blockchain/arcade Arcade repository
26
+ class Arcade < Protocol
27
+ # Arcade response statuses that indicate a transaction was NOT accepted.
28
+ # Narrower than ARC's taxonomy — Arcade has a single explicit rejection signal.
29
+ REJECTED_STATUSES = %w[REJECTED].freeze
30
+
31
+ endpoint :broadcast, :post, '/tx', response: :json
32
+ endpoint :get_tx_status, :get, '/tx/{txid}', response: :json
33
+ endpoint :health, :get, '/health', response: :json
34
+
35
+ # @param base_url [String] Arcade base URL
36
+ # @param api_key [String, nil] legacy bearer token shorthand
37
+ # @param auth [Hash, Symbol, nil] auth config; takes precedence over +api_key:+
38
+ # @param network [String, nil] network name for base URL interpolation
39
+ # @param http_client [#request, nil] injectable HTTP client for testing
40
+ # @param callback_url [String, nil] optional X-CallbackUrl header value
41
+ # @param callback_token [String, nil] optional X-CallbackToken header value
42
+ def initialize(base_url:, api_key: nil, auth: nil, network: nil, http_client: nil,
43
+ callback_url: nil, callback_token: nil)
44
+ super(base_url: base_url, api_key: api_key, auth: auth, network: network, http_client: http_client)
45
+ @callback_url = callback_url
46
+ @callback_token = callback_token
47
+ end
48
+
49
+ private
50
+
51
+ # Broadcast escape hatch: Arcade-specific headers and response shape.
52
+ #
53
+ # @param tx [#to_ef_hex, #to_hex, String] transaction object, hex string,
54
+ # or binary string
55
+ # @param callback_url [String, nil]
56
+ # @param callback_token [String, nil]
57
+ # @param full_status_updates [Boolean, nil]
58
+ # @param skip_fee_validation [Boolean, nil]
59
+ # @param skip_script_validation [Boolean, nil]
60
+ # @return [ProtocolResponse]
61
+ def call_broadcast(tx, callback_url: nil, callback_token: nil,
62
+ full_status_updates: nil, skip_fee_validation: nil,
63
+ skip_script_validation: nil, **)
64
+ hex = BSV::Network::Util.resolve_tx_hex(tx)
65
+ body = JSON.generate(rawTx: hex)
66
+
67
+ extra_headers = build_broadcast_headers(
68
+ callback_url: callback_url || @callback_url,
69
+ callback_token: callback_token || @callback_token,
70
+ full_status_updates: full_status_updates,
71
+ skip_fee_validation: skip_fee_validation,
72
+ skip_script_validation: skip_script_validation
73
+ )
74
+
75
+ path = self.class.endpoints[:broadcast][:path]
76
+ uri = URI("#{@base_url}#{path}")
77
+ request = build_request(:post, uri, body)
78
+ extra_headers.each { |k, v| request[k] = v }
79
+ response = execute(uri, request)
80
+
81
+ parse_broadcast_response(response)
82
+ end
83
+
84
+ # get_tx_status: pass-through to default_call plus rejection check.
85
+ #
86
+ # @param txid [String] display-order hex transaction ID
87
+ # @return [ProtocolResponse]
88
+ def call_get_tx_status(txid, **)
89
+ response = default_call(:get_tx_status, txid)
90
+ return response unless response.http_success?
91
+
92
+ body = response.data
93
+ return response unless body.is_a?(Hash)
94
+
95
+ if rejected_status?(body)
96
+ return response.with(
97
+ http_success: false,
98
+ error_message: body['txStatus'] || 'REJECTED'
99
+ )
100
+ end
101
+
102
+ response
103
+ end
104
+
105
+ def build_broadcast_headers(callback_url:, callback_token:, full_status_updates:,
106
+ skip_fee_validation:, skip_script_validation:)
107
+ headers = {}
108
+ headers['X-CallbackUrl'] = callback_url if callback_url
109
+ headers['X-CallbackToken'] = callback_token if callback_token
110
+ headers['X-FullStatusUpdates'] = 'true' if full_status_updates
111
+ headers['X-SkipFeeValidation'] = 'true' if skip_fee_validation
112
+ headers['X-SkipScriptValidation'] = 'true' if skip_script_validation
113
+ headers
114
+ end
115
+
116
+ def parse_broadcast_response(response)
117
+ code = response.code.to_i
118
+
119
+ if code == 503
120
+ retry_after = response['Retry-After']
121
+ msg = retry_after ? "Arcade backpressure; retry after #{retry_after}s" : 'Arcade backpressure'
122
+ return ProtocolResponse.new(response, http_success: false, error_message: msg)
123
+ end
124
+
125
+ unless (200..299).cover?(code)
126
+ body = BSV::Network::Util.safe_parse_json(response.body)
127
+ return ProtocolResponse.new(
128
+ response,
129
+ http_success: false,
130
+ error_message: body.is_a?(Hash) ? (body['reason'] || body['detail'] || "HTTP #{code}") : "HTTP #{code}"
131
+ )
132
+ end
133
+
134
+ body = BSV::Network::Util.safe_parse_json(response.body)
135
+
136
+ unless body.is_a?(Hash)
137
+ return ProtocolResponse.new(
138
+ response,
139
+ http_success: false,
140
+ error_message: 'Arcade returned a malformed 2xx response'
141
+ )
142
+ end
143
+
144
+ status = body['status']
145
+ unless status
146
+ return ProtocolResponse.new(
147
+ response,
148
+ http_success: false,
149
+ error_message: 'Arcade returned a malformed 2xx response'
150
+ )
151
+ end
152
+
153
+ ProtocolResponse.new(response, data: body)
154
+ end
155
+
156
+ def rejected_status?(body)
157
+ tx_status = body['txStatus'].to_s.upcase
158
+ REJECTED_STATUSES.include?(tx_status)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -16,15 +16,18 @@ module BSV
16
16
  #
17
17
  # == Usage
18
18
  #
19
- # ct = BSV::Network::Protocols::Chaintracks.new(base_url: 'https://arcade.gorillapool.io')
19
+ # ct = BSV::Network::Protocols::Chaintracks.new(base_url: 'http://localhost:8080')
20
20
  # result = ct.call(:current_height)
21
21
  # result.data # => 800000
22
22
  #
23
23
  # result = ct.call(:get_block_header, 800_000)
24
24
  # result.data # => { 'hash' => '...', 'height' => 800000, 'merkleRoot' => '...' }
25
25
  #
26
- # @note Chaintracks is an internal GorillaPool service; no public API documentation
27
- # is available.
26
+ # @note No public Chaintracks instance is hosted by major providers GorillaPool
27
+ # serves chain data via JungleBus instead. For general chain-tracking against
28
+ # GorillaPool, use the porcelain interface:
29
+ # +BSV::Transaction::ChainTracker.new(BSV::Network::Providers::GorillaPool.mainnet)+
30
+ # which routes through JungleBus automatically.
28
31
  class Chaintracks < Protocol
29
32
  endpoint :get_block_header, :get, '/chaintracks/v2/header/height/{height}', response: :json
30
33
  endpoint :current_height, :get, '/chaintracks/v2/tip',
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module BSV
4
6
  module Network
5
7
  module Protocols
@@ -39,6 +41,10 @@ module BSV
39
41
  # Block headers from a given height (supports ?limit=N, max 10000)
40
42
  endpoint :get_block_headers, :get, '/v1/block_header/list/{height}', response: :json_array
41
43
 
44
+ # Current chain tip height
45
+ endpoint :current_height, :get, '/v1/block_header/tip',
46
+ response: ->(body) { JSON.parse(body)['height'] }
47
+
42
48
  # @param base_url [String] base URL for the JungleBus API
43
49
  # @param api_key [String, nil] legacy API key shorthand — use +auth:+ for new code
44
50
  # @param auth [Hash, Symbol, nil] auth config; takes precedence over +api_key:+
@@ -7,6 +7,7 @@ module BSV
7
7
  # JungleBus, Ordinals, TAALBinary, and WoCREST.
8
8
  module Protocols
9
9
  autoload :ARC, 'bsv/network/protocols/arc'
10
+ autoload :Arcade, 'bsv/network/protocols/arcade'
10
11
  autoload :Chaintracks, 'bsv/network/protocols/chaintracks'
11
12
  autoload :JungleBus, 'bsv/network/protocols/jungle_bus'
12
13
  autoload :Ordinals, 'bsv/network/protocols/ordinals'
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module BSV
6
4
  module Network
7
5
  # Provider is a named configuration container that hosts one or more Protocol
@@ -56,10 +54,10 @@ module BSV
56
54
  # execution.
57
55
  #
58
56
  # @param klass [Class] a Protocol subclass
59
- # @param kwargs [Hash] keyword arguments forwarded to +klass.new+
57
+ # @param ** [Hash] keyword arguments forwarded to +klass.new+
60
58
  # @return [Protocol] the newly created protocol instance
61
- def protocol(klass, **kwargs)
62
- instance = klass.new(**kwargs)
59
+ def protocol(klass, **)
60
+ instance = klass.new(**)
63
61
  @protocols << instance
64
62
  klass.commands.each do |cmd|
65
63
  @command_index[cmd] ||= instance
@@ -122,16 +120,16 @@ module BSV
122
120
  # Dispatches a command to the first-registered protocol that serves it.
123
121
  #
124
122
  # @param command_name [Symbol, String] command to invoke
125
- # @param args [Array] positional arguments forwarded to the protocol
126
- # @param kwargs [Hash] keyword arguments forwarded to the protocol
123
+ # @param * [Array] positional arguments forwarded to the protocol
124
+ # @param ** [Hash] keyword arguments forwarded to the protocol
127
125
  # @return [ProtocolResponse]
128
126
  # @raise [ArgumentError] when no registered protocol serves the command
129
- def call(command_name, *args, **kwargs)
127
+ def call(command_name, *, **)
130
128
  sym = command_name.to_sym
131
129
  instance = @command_index[sym]
132
130
  raise ArgumentError, "#{@name} does not provide command :#{sym}" unless instance
133
131
 
134
- instance.call(sym, *args, **kwargs)
132
+ instance.call(sym, *, **)
135
133
  end
136
134
 
137
135
  private
@@ -4,17 +4,18 @@ module BSV
4
4
  module Network
5
5
  module Providers
6
6
  # GorillaPool returns pre-configured Provider instances using the GorillaPool
7
- # ARCADE infrastructure for ARC and Chaintracks, the GorillaPool Ordinals
8
- # API for transaction and merkle path lookups, and JungleBus for indexed
9
- # transaction data and block headers.
7
+ # Arcade infrastructure for broadcasting, the GorillaPool Ordinals API for
8
+ # transaction and merkle path lookups, and JungleBus for indexed transaction
9
+ # data and block headers.
10
10
  #
11
- # Mainnet composes four protocols:
12
- # - ARC at +https://arcade.gorillapool.io+
13
- # - Chaintracks at +https://arcade.gorillapool.io+
11
+ # Mainnet composes three protocols:
12
+ # - Arcade at +https://arcade.gorillapool.io+
14
13
  # - Ordinals at +https://ordinals.gorillapool.io+
15
14
  # - JungleBus at +https://junglebus.gorillapool.io+
16
15
  #
17
- # Testnet provides ARC and Chaintracks at +https://testnet.arcade.gorillapool.io+.
16
+ # Testnet composes two protocols:
17
+ # - Arcade at +https://testnet.arcade.gorillapool.io+
18
+ # - JungleBus at +https://testnet.junglebus.gorillapool.io+
18
19
  #
19
20
  # == Example
20
21
  #
@@ -30,9 +31,9 @@ module BSV
30
31
  # Default requests-per-second limit for unauthenticated use.
31
32
  DEFAULT_RATE_LIMIT = 3
32
33
 
33
- # Returns a mainnet Provider configured with ARC, Chaintracks, Ordinals, and JungleBus.
34
+ # Returns a mainnet Provider configured with Arcade, Ordinals, and JungleBus.
34
35
  #
35
- # Auth is forwarded to all four protocols so each can authenticate independently.
36
+ # Auth is forwarded to all three protocols so each can authenticate independently.
36
37
  #
37
38
  # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
38
39
  # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
@@ -42,16 +43,14 @@ module BSV
42
43
  resolved_auth = auth || (opts[:api_key] ? { bearer: opts[:api_key] } : :none)
43
44
  common = opts.slice(:api_key, :http_client).merge(auth: auth)
44
45
  Provider.new('GorillaPool', auth: resolved_auth, rate_limit: rate_limit) do |p|
45
- p.protocol Protocols::ARC, base_url: 'https://arcade.gorillapool.io', auth: auth, **opts
46
- p.protocol Protocols::Chaintracks, base_url: 'https://arcade.gorillapool.io', **common
47
- p.protocol Protocols::Ordinals, base_url: 'https://ordinals.gorillapool.io', **common
48
- p.protocol Protocols::JungleBus, base_url: 'https://junglebus.gorillapool.io', **common
46
+ p.protocol Protocols::Arcade, base_url: 'https://arcade.gorillapool.io', auth: auth, **opts
47
+ # TODO: re-register chaintracks_server at separate URL/port — see issue #778
48
+ p.protocol Protocols::Ordinals, base_url: 'https://ordinals.gorillapool.io', **common
49
+ p.protocol Protocols::JungleBus, base_url: 'https://junglebus.gorillapool.io', **common
49
50
  end
50
51
  end
51
52
 
52
- # Returns a testnet Provider configured with ARC and Chaintracks.
53
- #
54
- # Auth is forwarded to both protocols.
53
+ # Returns a testnet Provider configured with Arcade only.
55
54
  #
56
55
  # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
57
56
  # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
@@ -61,8 +60,9 @@ module BSV
61
60
  resolved_auth = auth || (opts[:api_key] ? { bearer: opts[:api_key] } : :none)
62
61
  common = opts.slice(:api_key, :http_client).merge(auth: auth)
63
62
  Provider.new('GorillaPool', auth: resolved_auth, rate_limit: rate_limit) do |p|
64
- p.protocol Protocols::ARC, base_url: 'https://testnet.arcade.gorillapool.io', auth: auth, **opts
65
- p.protocol Protocols::Chaintracks, base_url: 'https://testnet.arcade.gorillapool.io', **common
63
+ p.protocol Protocols::Arcade, base_url: 'https://testnet.arcade.gorillapool.io', auth: auth, **opts
64
+ p.protocol Protocols::JungleBus, base_url: 'https://testnet.junglebus.gorillapool.io', **common
65
+ # TODO: re-register chaintracks_server at separate URL/port — see issue #778
66
66
  end
67
67
  end
68
68
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module BSV
6
+ module Network
7
+ # Shared utility methods for protocol implementations.
8
+ module Util
9
+ # Parse JSON, returning a hash with a 'detail' key on parse failure.
10
+ # When the raw input is nil or empty the detail is nil (not an empty string).
11
+ def self.safe_parse_json(raw)
12
+ JSON.parse(raw.to_s)
13
+ rescue JSON::ParserError
14
+ { 'detail' => (raw.to_s.empty? ? nil : raw.to_s) }
15
+ end
16
+
17
+ # Coerce a transaction input to hex.
18
+ #
19
+ # Accepts (in order of preference):
20
+ # 1. Hex string — pass-through, zero conversion
21
+ # 2. Binary string — convert to hex
22
+ # 3. Transaction object — prefer EF hex (BRC-30), fall back to raw hex
23
+ #
24
+ # Detection uses content, not encoding: a string is hex if it has even
25
+ # length and contains only hex characters. This handles hex strings
26
+ # tagged as ASCII-8BIT (e.g. read from IO in binary mode).
27
+ #
28
+ # @param tx [String, #to_ef_hex, #to_hex] transaction in any supported form
29
+ # @return [String] hex-encoded transaction
30
+ def self.resolve_tx_hex(tx)
31
+ if tx.is_a?(String)
32
+ return tx if tx.match?(/\A[0-9a-fA-F]*\z/) && tx.length.even?
33
+
34
+ return tx.unpack1('H*')
35
+ end
36
+
37
+ tx.to_ef_hex
38
+ rescue ArgumentError => e
39
+ BSV.logger&.debug { "[Network::Util] EF serialisation failed: #{e.message} — falling back to raw hex" }
40
+ tx.to_hex
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/bsv/network.rb CHANGED
@@ -8,5 +8,6 @@ module BSV
8
8
  autoload :Providers, 'bsv/network/providers'
9
9
  autoload :Provider, 'bsv/network/provider'
10
10
  autoload :UTXO, 'bsv/network/utxo'
11
+ autoload :Util, 'bsv/network/util'
11
12
  end
12
13
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
3
  require 'uri'
5
4
 
6
5
  module BSV
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
3
  require 'json'
5
4
  require 'uri'
6
5