bsv-sdk 0.20.0 → 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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
  4. data/lib/bsv/network/protocols/arc.rb +4 -30
  5. data/lib/bsv/network/protocols/arcade.rb +163 -0
  6. data/lib/bsv/network/protocols/chaintracks.rb +6 -3
  7. data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
  8. data/lib/bsv/network/protocols.rb +1 -0
  9. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  10. data/lib/bsv/network/util.rb +44 -0
  11. data/lib/bsv/network.rb +1 -0
  12. data/lib/bsv/transaction/chain_tracker.rb +74 -13
  13. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  14. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  15. data/lib/bsv/version.rb +1 -1
  16. data/lib/bsv/wallet/errors.rb +65 -21
  17. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  18. data/lib/bsv/wallet/proto_wallet.rb +14 -1
  19. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  20. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  21. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  22. data/lib/bsv/wallet/serializer/common.rb +207 -0
  23. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  24. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  25. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  26. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  27. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  28. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  29. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  30. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  31. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  32. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  33. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  34. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  35. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  36. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  37. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  38. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  39. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  40. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  41. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  42. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  43. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  44. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  45. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  46. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  47. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  48. data/lib/bsv/wallet/serializer/status.rb +85 -0
  49. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  50. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  51. data/lib/bsv/wallet/serializer.rb +180 -0
  52. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  53. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  54. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  55. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  56. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  57. data/lib/bsv/wallet/wire/calls.rb +79 -0
  58. data/lib/bsv/wallet/wire/frame.rb +181 -0
  59. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  60. data/lib/bsv/wallet/wire/validation.rb +213 -0
  61. data/lib/bsv/wallet/wire.rb +13 -0
  62. data/lib/bsv/wallet.rb +17 -0
  63. metadata +46 -2
  64. 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: f7066943d0a9d1aa217f1ed97a3dc0390fd5c1bdbad845183bf7ccbc87f582f3
4
- data.tar.gz: e4914b71001219c5144433f2481097e141096fcbd82a969f655fe1afae242133
3
+ metadata.gz: d666b0e5af3155dbe92f4298f5448a5792275848f6e3c08fcc1bbfa09d9a20bd
4
+ data.tar.gz: c4f3beba29031b049afe90a967b5d982ee473cea57ee077c09ba9c263385c067
5
5
  SHA512:
6
- metadata.gz: a6497f1705a3e87d5e5cf2e4f7b20150440475463461e3ef15fdb31b33d4a602beb0b2fe30852ea6da3c19c18f44dc4348fbc596008dfd101c3e2ae9de9fccdb
7
- data.tar.gz: 21cdf72face3b0748442322a8b506f821432dc72d465a126b410e182e7ca4da17f29573f5b3c999c121ff2e5c4e5d284c790974f825bf547ec2ee36ff1417e5f
6
+ metadata.gz: ce7d22f5b2267f44909e871e15c619af28e0055db71f84a5fc7529c70c7c030d0314f466d328a2154c476530a6485b19588e09a96ce7ae3f406f29cfd6ec8569
7
+ data.tar.gz: 46cfee8000801ffd398d659b6174b9ac48cc2e4fc098c93e367634f06cfc9d9c659211ef51c452fd7c83262121e8923c6ab7240588fe01782c3002a6fa4f8075
data/CHANGELOG.md CHANGED
@@ -5,6 +5,88 @@ 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
+
8
90
  ## 0.20.0 — 2026-05-24
9
91
 
10
92
  ### Changed
@@ -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)],
@@ -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'
@@ -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
@@ -10,18 +10,21 @@ module BSV
10
10
  # chain tracker is the data source: an object the consumer provides
11
11
  # that can answer "is this merkle root valid for this block height?"
12
12
  #
13
- # The SDK is deliberately unopinionated about where that answer comes
14
- # from. A chain tracker backed by an in-memory hash is declarative.
15
- # One that fetches from the network on cache miss and writes to a
16
- # database is imperative. The verify methods don't care — they just
17
- # ask the question. All imperative behaviour (fetching, caching,
18
- # persisting) lives in the consumer's chain tracker implementation,
19
- # not in the SDK.
13
+ # This class is a working default implementation that wraps a
14
+ # {BSV::Network::Provider} and dispatches via {Provider#call}. The
15
+ # provider must serve the +:get_block_header+ and +:current_height+
16
+ # commands (e.g. a provider configured with {Protocols::JungleBus}).
17
+ #
18
+ # Subclasses may override either or both methods to supply their own
19
+ # data source (in-memory hash, database cache, etc.) without touching
20
+ # the provider at all. The +provider+ argument is optional precisely to
21
+ # preserve this pattern — a subclass that overrides both methods never
22
+ # reaches the provider-dispatch path.
20
23
  #
21
24
  # Any object responding to +valid_root_for_height?+ and
22
25
  # +current_height+ satisfies this interface. Inheriting from this
23
26
  # class is optional — it exists to document the contract and provide
24
- # clear error messages when methods are missing.
27
+ # a ready-to-use provider-backed implementation.
25
28
  #
26
29
  # @example In-memory chain tracker (test / declarative)
27
30
  # class HashTracker < BSV::Transaction::ChainTracker
@@ -38,23 +41,81 @@ module BSV
38
41
  # header.merkle_root == root
39
42
  # end
40
43
  # end
44
+ #
45
+ # @example Provider-backed (default impl)
46
+ # tracker = BSV::Transaction::ChainTracker.default
47
+ # tracker.valid_root_for_height?('abcd...', 800_000)
48
+ #
49
+ # @example Testnet
50
+ # tracker = BSV::Transaction::ChainTracker.default(testnet: true)
51
+ # tracker.current_height
41
52
  class ChainTracker
53
+ # @return [BSV::Network::Provider, nil] the underlying provider, if any
54
+ attr_reader :provider
55
+
56
+ # Return a default ChainTracker backed by the GorillaPool provider.
57
+ #
58
+ # @param testnet [Boolean] when true, uses the testnet provider
59
+ # @return [ChainTracker]
60
+ def self.default(testnet: false)
61
+ new(BSV::Network::Providers::GorillaPool.default(testnet: testnet))
62
+ end
63
+
64
+ # @param provider [BSV::Network::Provider, nil] provider serving +:get_block_header+
65
+ # and +:current_height+. Optional when the subclass overrides both methods.
66
+ def initialize(provider = nil)
67
+ @provider = provider
68
+ end
69
+
42
70
  # Verify that a merkle root is valid for the given block height.
43
71
  #
72
+ # Dispatches +:get_block_header+ to the configured provider. Returns +false+
73
+ # on 404 (block not found). Normalises the merkle root field name from any
74
+ # of +merkleroot+, +merkleRoot+, or +merkle_root+.
75
+ #
44
76
  # @param root [String] merkle root as a hex string
45
77
  # @param height [Integer] block height
46
78
  # @return [Boolean] true if the root matches the block at the given height
47
- # @raise [NotImplementedError] if not overridden by a subclass
48
- def valid_root_for_height?(_root, _height)
49
- raise NotImplementedError, "#{self.class}#valid_root_for_height? must be implemented"
79
+ # @raise [RuntimeError] when no provider is configured
80
+ # @raise [RuntimeError] on network or API error
81
+ def valid_root_for_height?(root, height)
82
+ raise 'ChainTracker requires a provider when used directly' if @provider.nil?
83
+
84
+ result = @provider.call(:get_block_header, height)
85
+ return false if result.http_not_found?
86
+ raise result.error_message.to_s unless result.http_success?
87
+
88
+ actual = normalise_merkle_root(result.data)
89
+ return false unless actual
90
+
91
+ actual.casecmp(root).zero?
50
92
  end
51
93
 
52
94
  # Return the current blockchain height.
53
95
  #
96
+ # Dispatches +:current_height+ to the configured provider.
97
+ #
54
98
  # @return [Integer] the height of the chain tip
55
- # @raise [NotImplementedError] if not overridden by a subclass
99
+ # @raise [RuntimeError] when no provider is configured
100
+ # @raise [RuntimeError] on network or API error
56
101
  def current_height
57
- raise NotImplementedError, "#{self.class}#current_height must be implemented"
102
+ raise 'ChainTracker requires a provider when used directly' if @provider.nil?
103
+
104
+ result = @provider.call(:current_height)
105
+ return result.data if result.http_success?
106
+
107
+ raise result.error_message.to_s
108
+ end
109
+
110
+ private
111
+
112
+ # Field-name diversity belongs at the Protocols::* layer; this is a
113
+ # transitional shim until the wire protocols return canonical shapes.
114
+ # See #791.
115
+ def normalise_merkle_root(body)
116
+ return nil unless body.is_a?(Hash)
117
+
118
+ body['merkleroot'] || body['merkleRoot'] || body['merkle_root']
58
119
  end
59
120
  end
60
121
  end
@@ -5,16 +5,6 @@ module BSV
5
5
  # Namespace for chain tracker implementations.
6
6
  module ChainTrackers
7
7
  autoload :WhatsOnChain, 'bsv/transaction/chain_trackers/whats_on_chain'
8
- autoload :Chaintracks, 'bsv/transaction/chain_trackers/chaintracks'
9
-
10
- # Return a default chain tracker backed by the Arcade/GorillaPool Chaintracks API.
11
- #
12
- # @param testnet [Boolean] use the testnet endpoint when true
13
- # @param ** [Hash] forwarded to the underlying tracker (e.g. +api_key:+)
14
- # @return [Chaintracks]
15
- def self.default(testnet: false, **)
16
- Chaintracks.default(testnet: testnet, **)
17
- end
18
8
  end
19
9
  end
20
10
  end