bsv-sdk 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/lib/bsv/auth/get_verifiable_certificates.rb +6 -6
- data/lib/bsv/auth/peer.rb +10 -4
- data/lib/bsv/auth/session_manager.rb +81 -5
- data/lib/bsv/identity/client.rb +4 -2
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +2 -2
- data/lib/bsv/mcp/tools/check_balance.rb +2 -2
- data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
- data/lib/bsv/network/broadcast_error.rb +1 -0
- data/lib/bsv/network/broadcast_response.rb +3 -1
- data/lib/bsv/network/protocol.rb +56 -4
- data/lib/bsv/network/protocols/arc.rb +6 -3
- data/lib/bsv/network/protocols/chaintracks.rb +6 -2
- data/lib/bsv/network/protocols/jungle_bus.rb +52 -0
- data/lib/bsv/network/protocols/ordinals.rb +110 -8
- data/lib/bsv/network/protocols/taal_binary.rb +17 -4
- data/lib/bsv/network/protocols/woc_rest.rb +164 -84
- data/lib/bsv/network/protocols.rb +1 -0
- data/lib/bsv/network/provider.rb +36 -5
- data/lib/bsv/network/providers/gorilla_pool.rb +42 -20
- data/lib/bsv/network/providers/taal.rb +38 -15
- data/lib/bsv/network/providers/whats_on_chain.rb +42 -21
- data/lib/bsv/network/utxo.rb +8 -2
- data/lib/bsv/overlay/lookup_resolver.rb +5 -5
- data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
- data/lib/bsv/overlay/types.rb +1 -0
- data/lib/bsv/registry/client.rb +8 -8
- data/lib/bsv/registry/types.rb +1 -0
- data/lib/bsv/transaction/beef.rb +139 -102
- data/lib/bsv/transaction/transaction.rb +31 -19
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wire_format.rb +40 -14
- metadata +4 -3
|
@@ -1,31 +1,133 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module BSV
|
|
4
6
|
module Network
|
|
5
7
|
module Protocols
|
|
6
8
|
# Ordinals implements the GorillaPool Ordinals API as a Protocol subclass.
|
|
7
9
|
#
|
|
8
|
-
# Provides raw transaction
|
|
9
|
-
#
|
|
10
|
+
# Provides raw transaction lookup, Merkle path (proof) retrieval, UTXO
|
|
11
|
+
# queries, balance lookups, spend status, and chain tip via the GorillaPool
|
|
12
|
+
# Ordinals REST API.
|
|
13
|
+
#
|
|
14
|
+
# == get_tx vs get_tx_details
|
|
15
|
+
#
|
|
16
|
+
# +get_tx+ returns the raw transaction as a hex string (escape hatch
|
|
17
|
+
# converts binary response body to hex). +get_tx_details+ returns the full
|
|
18
|
+
# parsed transaction metadata as a JSON object.
|
|
19
|
+
#
|
|
20
|
+
# == get_balance
|
|
21
|
+
#
|
|
22
|
+
# The API returns the balance as a quoted integer string (e.g. +"1000000"+),
|
|
23
|
+
# not a bare JSON number. The lambda handler parses this to an Integer.
|
|
24
|
+
#
|
|
25
|
+
# == get_spend
|
|
26
|
+
#
|
|
27
|
+
# The API returns an empty quoted string +""+ for unspent outputs and a
|
|
28
|
+
# quoted txid string for spent outputs. The escape hatch normalises these
|
|
29
|
+
# to +{ spent: false }+ or +{ spent: true, spending_txid: "..." }+.
|
|
30
|
+
# The outpoint must be passed as +"txid_vout"+ (underscore-separated).
|
|
10
31
|
#
|
|
11
32
|
# == Usage
|
|
12
33
|
#
|
|
13
34
|
# ord = BSV::Network::Protocols::Ordinals.new(base_url: 'https://ordinals.gorillapool.io')
|
|
14
35
|
# result = ord.call(:get_tx, 'abc123...')
|
|
15
|
-
# result.data # => "01000000..." (
|
|
36
|
+
# result.data # => "01000000..." (hex string)
|
|
16
37
|
#
|
|
17
38
|
# result = ord.call(:get_merkle_path, 'abc123...')
|
|
18
|
-
# result.data # =>
|
|
39
|
+
# result.data # => binary TSC proof bytes
|
|
40
|
+
#
|
|
41
|
+
# result = ord.call(:get_spend, 'txid_0')
|
|
42
|
+
# result.data # => { spent: false } or { spent: true, spending_txid: "..." }
|
|
43
|
+
#
|
|
44
|
+
# @see https://ordinals.gorillapool.io/api/docs/ Ordinals API documentation
|
|
19
45
|
class Ordinals < Protocol
|
|
20
|
-
|
|
21
|
-
endpoint :
|
|
46
|
+
# Transaction — raw binary body; escape hatch converts to hex
|
|
47
|
+
endpoint :get_tx, :get, '/api/tx/{txid}/raw'
|
|
48
|
+
|
|
49
|
+
# Transaction — full metadata as JSON
|
|
50
|
+
endpoint :get_tx_details, :get, '/api/tx/{txid}', response: :json
|
|
51
|
+
|
|
52
|
+
# Transaction — confirmation status
|
|
53
|
+
endpoint :get_tx_status, :get, '/api/tx/{txid}/status', response: :json
|
|
54
|
+
|
|
55
|
+
# Merkle path — binary TSC proof body; no JSON parsing
|
|
56
|
+
endpoint :get_merkle_path, :get, '/api/tx/{txid}/proof'
|
|
57
|
+
|
|
58
|
+
# UTXOs for an address — JSON array
|
|
59
|
+
endpoint :get_utxos, :get, '/api/txos/address/{address}/unspent', response: :json_array
|
|
60
|
+
|
|
61
|
+
# Balance for an address — quoted integer string; lambda parses to Integer
|
|
62
|
+
endpoint :get_balance, :get, '/api/txos/address/{address}/balance',
|
|
63
|
+
response: ->(body) { JSON.parse(body).to_i }
|
|
64
|
+
|
|
65
|
+
# Spend status for an outpoint (format: "txid_vout") — escape hatch normalises
|
|
66
|
+
endpoint :get_spend, :get, '/api/spends/{outpoint}'
|
|
67
|
+
|
|
68
|
+
# Chain tip — latest block metadata as JSON
|
|
69
|
+
endpoint :get_chain_tip, :get, '/api/blocks/tip', response: :json
|
|
22
70
|
|
|
23
71
|
# @param base_url [String] base URL for the Ordinals API
|
|
24
|
-
# @param api_key [String, nil]
|
|
72
|
+
# @param api_key [String, nil] legacy Bearer API key shorthand — use +auth:+ for new code
|
|
73
|
+
# @param auth [Hash, Symbol, nil] auth config; takes precedence over +api_key:+
|
|
25
74
|
# @param http_client [Object, nil] injectable HTTP client for testing
|
|
26
|
-
def initialize(base_url:, api_key: nil, http_client: nil)
|
|
75
|
+
def initialize(base_url:, api_key: nil, auth: nil, http_client: nil)
|
|
27
76
|
super
|
|
28
77
|
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Fetches the raw transaction and converts the binary response body to hex.
|
|
82
|
+
#
|
|
83
|
+
# The Ordinals API serves +/api/tx/{txid}/raw+ as binary data, not hex text.
|
|
84
|
+
# This escape hatch unpacks the binary body to a lowercase hex string,
|
|
85
|
+
# matching the convention used by other protocol +get_tx+ implementations.
|
|
86
|
+
#
|
|
87
|
+
# Accepts +txid+ as a positional argument or as a +txid:+ keyword.
|
|
88
|
+
#
|
|
89
|
+
# @param pos_txid [String, nil] transaction ID (positional form)
|
|
90
|
+
# @param txid [String, nil] transaction ID (keyword form)
|
|
91
|
+
# @return [Result::Success<String>, Result::Error, Result::NotFound]
|
|
92
|
+
def call_get_tx(pos_txid = nil, txid: nil)
|
|
93
|
+
resolved = pos_txid || txid
|
|
94
|
+
raise ArgumentError, 'txid is required' if resolved.nil? || resolved.empty?
|
|
95
|
+
|
|
96
|
+
result = default_call(:get_tx, resolved)
|
|
97
|
+
return result unless result.success?
|
|
98
|
+
return Result::Error.new(message: 'empty response body') if result.data.nil? || result.data.empty?
|
|
99
|
+
|
|
100
|
+
Result::Success.new(data: result.data.unpack1('H*'))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Normalises the spend status response from the Ordinals API.
|
|
104
|
+
#
|
|
105
|
+
# The API returns a quoted empty string +""+ when the output is unspent,
|
|
106
|
+
# and a quoted txid string +"abc123..."+ when it is spent. This escape
|
|
107
|
+
# hatch parses the JSON string body and returns a structured hash.
|
|
108
|
+
#
|
|
109
|
+
# The +outpoint+ argument must use underscore-separated format: +"txid_vout"+.
|
|
110
|
+
# Accepts +outpoint+ as a positional argument or as an +outpoint:+ keyword.
|
|
111
|
+
#
|
|
112
|
+
# @param pos_outpoint [String, nil] outpoint in +"txid_vout"+ format (positional form)
|
|
113
|
+
# @param outpoint [String, nil] outpoint in +"txid_vout"+ format (keyword form)
|
|
114
|
+
# @return [Result::Success<Hash>, Result::Error, Result::NotFound]
|
|
115
|
+
def call_get_spend(pos_outpoint = nil, outpoint: nil)
|
|
116
|
+
resolved = pos_outpoint || outpoint
|
|
117
|
+
raise ArgumentError, 'outpoint is required' if resolved.nil? || resolved.empty?
|
|
118
|
+
|
|
119
|
+
result = default_call(:get_spend, resolved)
|
|
120
|
+
return result unless result.success?
|
|
121
|
+
|
|
122
|
+
spending_txid = JSON.parse(result.data).to_s.strip
|
|
123
|
+
if spending_txid.empty?
|
|
124
|
+
Result::Success.new(data: { spent: false })
|
|
125
|
+
else
|
|
126
|
+
Result::Success.new(data: { spent: true, spending_txid: spending_txid })
|
|
127
|
+
end
|
|
128
|
+
rescue JSON::ParserError => e
|
|
129
|
+
Result::Error.new(message: "spend response parse error: #{e.message}")
|
|
130
|
+
end
|
|
29
131
|
end
|
|
30
132
|
end
|
|
31
133
|
end
|
|
@@ -8,7 +8,7 @@ module BSV
|
|
|
8
8
|
#
|
|
9
9
|
# TAAL quirks handled here:
|
|
10
10
|
# - Content-Type is +application/octet-stream+ (not JSON)
|
|
11
|
-
# - Authorization header
|
|
11
|
+
# - Authorization header is applied via the standard +apply_auth+ mechanism
|
|
12
12
|
# - A response containing +txn-already-known+ in the error field is treated
|
|
13
13
|
# as success (the transaction is already in the mempool — idempotent)
|
|
14
14
|
#
|
|
@@ -16,13 +16,26 @@ module BSV
|
|
|
16
16
|
#
|
|
17
17
|
# protocol = BSV::Network::Protocols::TAALBinary.new(
|
|
18
18
|
# base_url: 'https://api.taal.com',
|
|
19
|
-
# api_key: 'mainnet_your_key_here'
|
|
19
|
+
# auth: { api_key: 'mainnet_your_key_here' }
|
|
20
20
|
# )
|
|
21
21
|
# result = protocol.call(:broadcast, tx)
|
|
22
22
|
# puts result.data[:txid] if result.success?
|
|
23
|
+
#
|
|
24
|
+
# @see https://docs.taal.com TAAL API documentation
|
|
23
25
|
class TAALBinary < BSV::Network::Protocol
|
|
24
26
|
endpoint :broadcast, :post, '/api/v1/broadcast', response: :json
|
|
25
27
|
|
|
28
|
+
# @param base_url [String] base URL for the TAAL binary API
|
|
29
|
+
# @param api_key [String, nil] legacy API key shorthand (no Bearer prefix) — use +auth:+ for new code
|
|
30
|
+
# @param auth [Hash, Symbol, nil] auth config; takes precedence over +api_key:+
|
|
31
|
+
# @param http_client [Object, nil] injectable HTTP client for testing
|
|
32
|
+
def initialize(base_url:, api_key: nil, auth: nil, http_client: nil)
|
|
33
|
+
# Translate legacy api_key: to auth: { api_key: } so the base class sends
|
|
34
|
+
# the raw key without a Bearer prefix, matching TAAL's expected auth format.
|
|
35
|
+
resolved_auth = auth || (api_key ? { api_key: api_key } : nil)
|
|
36
|
+
super(base_url: base_url, auth: resolved_auth, http_client: http_client)
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
private
|
|
27
40
|
|
|
28
41
|
# Escape hatch for broadcast: sends raw binary transaction bytes with
|
|
@@ -35,8 +48,8 @@ module BSV
|
|
|
35
48
|
|
|
36
49
|
uri = URI("#{@base_url}/api/v1/broadcast")
|
|
37
50
|
request = Net::HTTP::Post.new(uri)
|
|
38
|
-
request['Content-Type']
|
|
39
|
-
request
|
|
51
|
+
request['Content-Type'] = 'application/octet-stream'
|
|
52
|
+
apply_auth(request)
|
|
40
53
|
|
|
41
54
|
request.body = body
|
|
42
55
|
|
|
@@ -7,9 +7,9 @@ module BSV
|
|
|
7
7
|
module Protocols
|
|
8
8
|
# WoCREST implements the WhatsOnChain REST API as a Protocol subclass.
|
|
9
9
|
#
|
|
10
|
-
# Provides
|
|
11
|
-
# UTXOs, scripts, address queries, broadcast, and health.
|
|
12
|
-
# handle WoC-specific body formats and field remapping.
|
|
10
|
+
# Provides endpoints covering chain info, block headers, transactions,
|
|
11
|
+
# UTXOs, scripts, address queries, broadcast, search, stats, and health.
|
|
12
|
+
# Escape hatches handle WoC-specific body formats and field remapping.
|
|
13
13
|
#
|
|
14
14
|
# == Network resolution
|
|
15
15
|
#
|
|
@@ -22,6 +22,8 @@ module BSV
|
|
|
22
22
|
# woc = BSV::Network::Protocols::WoCREST.new(network: :main)
|
|
23
23
|
# result = woc.call(:get_tx, 'abc123...')
|
|
24
24
|
# puts result.data if result.success?
|
|
25
|
+
#
|
|
26
|
+
# @see https://developers.whatsonchain.com/ WhatsOnChain API documentation
|
|
25
27
|
class WoCREST < Protocol
|
|
26
28
|
NETWORKS = {
|
|
27
29
|
'main' => 'main',
|
|
@@ -40,6 +42,9 @@ module BSV
|
|
|
40
42
|
endpoint :get_chain_info, :get, '/chain/info', response: :json
|
|
41
43
|
endpoint :get_block_header, :get, '/block/{height}/header', response: :json
|
|
42
44
|
endpoint :get_block_headers, :get, '/block/headers', response: :json_array
|
|
45
|
+
endpoint :get_circulating_supply, :get, '/circulatingsupply'
|
|
46
|
+
endpoint :get_chain_tips, :get, '/chain/tips', response: :json_array
|
|
47
|
+
endpoint :get_peer_info, :get, '/peer/info', response: :json_array
|
|
43
48
|
|
|
44
49
|
# Transaction
|
|
45
50
|
endpoint :get_tx, :get, '/tx/{txid}/hex'
|
|
@@ -51,15 +56,26 @@ module BSV
|
|
|
51
56
|
endpoint :decode_tx, :post, '/tx/decode', response: :json
|
|
52
57
|
endpoint :get_tx_status, :post, '/txs/status', response: :json
|
|
53
58
|
endpoint :get_tx_hex_bulk, :post, '/txs/hex', response: :json
|
|
59
|
+
endpoint :get_tx_binary, :get, '/tx/{txid}/bin'
|
|
60
|
+
endpoint :get_tx_by_block_index, :get, '/block/height/{height}/txindex/{txindex}', response: :json
|
|
61
|
+
endpoint :get_tx_propagation, :get, '/tx/hash/{txid}/propagation', response: :json
|
|
62
|
+
endpoint :get_bulk_tx_details, :post, '/txs', response: :json
|
|
63
|
+
endpoint :get_bulk_output_scripts, :post, '/txs/vouts/hex', response: :json
|
|
54
64
|
|
|
55
65
|
# UTXO / spent status
|
|
56
66
|
endpoint :get_utxos, :get, '/address/{address}/confirmed/unspent',
|
|
57
67
|
response: :json_array
|
|
58
|
-
endpoint :get_utxos_all, :get, '/address/{address}/unspent',
|
|
68
|
+
endpoint :get_utxos_all, :get, '/address/{address}/unspent/all',
|
|
59
69
|
response: :json_array
|
|
60
70
|
endpoint :is_utxo, :get, '/tx/{txid}/{vout}/spent', response: :json
|
|
61
71
|
endpoint :is_utxo_bulk, :post, '/utxos/spent', response: :json_array
|
|
62
72
|
endpoint :valid_root, :get, '/block/{height}/header', response: :json
|
|
73
|
+
endpoint :get_unconfirmed_utxos, :get, '/address/{address}/unconfirmed/unspent',
|
|
74
|
+
response: :json_array
|
|
75
|
+
endpoint :get_confirmed_spent, :get, '/tx/{txid}/{vout}/confirmed/spent', response: :json
|
|
76
|
+
endpoint :get_unconfirmed_spent, :get, '/tx/{txid}/{vout}/unconfirmed/spent', response: :json
|
|
77
|
+
endpoint :get_bulk_address_utxos, :post, '/addresses/confirmed/unspent', response: :json
|
|
78
|
+
endpoint :get_bulk_address_unconfirmed_utxos, :post, '/addresses/unconfirmed/unspent', response: :json
|
|
63
79
|
|
|
64
80
|
# Script
|
|
65
81
|
endpoint :get_script_unspent, :get, '/script/{script_hash}/confirmed/unspent',
|
|
@@ -69,6 +85,9 @@ module BSV
|
|
|
69
85
|
endpoint :get_script_all_unspent, :get, '/script/{script_hash}/unspent/all',
|
|
70
86
|
response: :json_array
|
|
71
87
|
endpoint :get_script_unspent_bulk, :post, '/scripts/confirmed/unspent', response: :json
|
|
88
|
+
endpoint :get_script_unconfirmed_unspent, :get, '/script/{script_hash}/unconfirmed/unspent',
|
|
89
|
+
response: :json_array
|
|
90
|
+
endpoint :get_bulk_script_unconfirmed_unspent, :post, '/scripts/unconfirmed/unspent', response: :json
|
|
72
91
|
|
|
73
92
|
# Address balance / history
|
|
74
93
|
endpoint :get_balance, :get, '/address/{address}/confirmed/balance',
|
|
@@ -83,35 +102,46 @@ module BSV
|
|
|
83
102
|
endpoint :get_exchange_rate, :get, '/exchangerate', response: :json
|
|
84
103
|
endpoint :get_fee_recommendation, :get, '/feerecommendation', response: :json
|
|
85
104
|
endpoint :get_mempool_info, :get, '/mempool/info', response: :json
|
|
105
|
+
endpoint :get_exchange_rate_historical, :get, '/exchangerate/historical', response: :json_array
|
|
106
|
+
endpoint :get_mempool_raw, :get, '/mempool/raw', response: :json_array
|
|
107
|
+
|
|
108
|
+
# Search
|
|
109
|
+
endpoint :search_links, :post, '/search/links', response: :json
|
|
110
|
+
|
|
111
|
+
# Stats
|
|
112
|
+
endpoint :get_block_stats, :get, '/block/height/{height}/stats', response: :json
|
|
113
|
+
endpoint :get_block_stats_by_hash, :get, '/block/hash/{hash}/stats', response: :json
|
|
114
|
+
endpoint :get_miner_block_stats, :get, '/miner/blocks/stats', response: :json
|
|
115
|
+
endpoint :get_miner_fees, :get, '/miner/fees', response: :json
|
|
116
|
+
endpoint :get_miner_summary, :get, '/miner/summary/stats', response: :json
|
|
117
|
+
endpoint :get_block_tag_count, :get, '/block/tagcount/height/{height}/stats', response: :json
|
|
86
118
|
|
|
87
119
|
# Health
|
|
88
|
-
endpoint :health, :get, '/
|
|
120
|
+
endpoint :health, :get, '/woc'
|
|
89
121
|
|
|
90
122
|
attr_reader :network_name
|
|
91
123
|
|
|
92
124
|
# @param base_url [String] base URL for the WoC API; may contain
|
|
93
125
|
# +{network}+ which will be interpolated with the resolved network name
|
|
94
|
-
# @param network
|
|
95
|
-
# @param api_key
|
|
126
|
+
# @param network [Symbol, String] :main, :mainnet, :test, :testnet, :stn
|
|
127
|
+
# @param api_key [String, nil] legacy API key — sends +Authorization: key+
|
|
128
|
+
# (no Bearer prefix, matching WoC's expected auth format)
|
|
129
|
+
# @param auth [Hash, Symbol, nil] auth config hash forwarded to Protocol;
|
|
130
|
+
# when provided, takes precedence over +api_key:+
|
|
96
131
|
# @param http_client [Object, nil] injectable HTTP client for testing
|
|
97
|
-
def initialize(base_url:, network: :main, api_key: nil, http_client: nil)
|
|
132
|
+
def initialize(base_url:, network: :main, api_key: nil, auth: nil, http_client: nil)
|
|
98
133
|
@network_name = resolve_network(network)
|
|
134
|
+
# Translate legacy api_key: to auth: { api_key: } so the base class sends
|
|
135
|
+
# the raw key without a Bearer prefix, matching WoC's expected auth format.
|
|
136
|
+
resolved_auth = auth || (api_key ? { api_key: api_key } : nil)
|
|
99
137
|
super(
|
|
100
138
|
base_url: base_url,
|
|
101
|
-
|
|
139
|
+
auth: resolved_auth,
|
|
102
140
|
network: @network_name,
|
|
103
141
|
http_client: http_client
|
|
104
142
|
)
|
|
105
143
|
end
|
|
106
144
|
|
|
107
|
-
# WoC expects a raw Authorization header (no Bearer prefix).
|
|
108
|
-
# Override the base class which adds "Bearer ".
|
|
109
|
-
def build_request(http_method, uri, body)
|
|
110
|
-
request = super
|
|
111
|
-
request['Authorization'] = @api_key if @api_key
|
|
112
|
-
request
|
|
113
|
-
end
|
|
114
|
-
|
|
115
145
|
private
|
|
116
146
|
|
|
117
147
|
# Resolves network aliases to the WoC URL segment string.
|
|
@@ -125,77 +155,40 @@ module BSV
|
|
|
125
155
|
end
|
|
126
156
|
end
|
|
127
157
|
|
|
128
|
-
# Fetches confirmed UTXOs for an address and remaps the +value+ field
|
|
129
|
-
# to +satoshis+ to match the SDK's UTXO convention.
|
|
130
|
-
#
|
|
131
|
-
# WoC returns entries with +{ tx_hash, tx_pos, value, height }+;
|
|
132
|
-
# callers and facades expect +satoshis+ in place of +value+.
|
|
133
|
-
#
|
|
134
|
-
# @param address [String] BSV address
|
|
135
|
-
# @return [Result::Success, Result::Error, Result::NotFound]
|
|
136
|
-
def call_get_utxos(address)
|
|
137
|
-
result = default_call(:get_utxos, address)
|
|
138
|
-
return result unless result.success?
|
|
139
|
-
|
|
140
|
-
Result::Success.new(data: remap_utxo_entries(result.data))
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Fetches all UTXOs (confirmed and unconfirmed) for an address and
|
|
144
|
-
# remaps the +value+ field to +satoshis+. Uses the legacy +/unspent+
|
|
145
|
-
# endpoint rather than +/confirmed/unspent+.
|
|
146
|
-
#
|
|
147
|
-
# @param address [String] BSV address
|
|
148
|
-
# @return [Result::Success, Result::Error, Result::NotFound]
|
|
149
|
-
def call_get_utxos_all(address)
|
|
150
|
-
result = default_call(:get_utxos_all, address)
|
|
151
|
-
return result unless result.success?
|
|
152
|
-
|
|
153
|
-
Result::Success.new(data: remap_utxo_entries(result.data))
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Remaps WoC UTXO entries from +{ 'value' => n }+ to +{ satoshis: n }+.
|
|
157
|
-
#
|
|
158
|
-
# @param entries [Array<Hash>]
|
|
159
|
-
# @return [Array<Hash>]
|
|
160
|
-
def remap_utxo_entries(entries)
|
|
161
|
-
entries.map do |entry|
|
|
162
|
-
{
|
|
163
|
-
tx_hash: entry['tx_hash'],
|
|
164
|
-
tx_pos: entry['tx_pos'],
|
|
165
|
-
satoshis: entry['value'],
|
|
166
|
-
height: entry['height']
|
|
167
|
-
}
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
158
|
# Checks whether a specific output is unspent by querying the WoC
|
|
172
159
|
# spent-status endpoint.
|
|
173
160
|
#
|
|
174
|
-
# WoC returns
|
|
161
|
+
# WoC returns 200 with spending transaction details when an output
|
|
162
|
+
# has been spent, or 404 when the output is unspent (no spending
|
|
163
|
+
# transaction found). This escape hatch maps both cases to a
|
|
164
|
+
# boolean: +true+ = unspent, +false+ = spent.
|
|
165
|
+
#
|
|
175
166
|
# The +script_hash:+ keyword is accepted for future fallback support
|
|
176
167
|
# but not used in this implementation.
|
|
177
168
|
#
|
|
178
169
|
# @param txid [String] WoC API boundary: display-order hex transaction ID
|
|
179
170
|
# @param vout [Integer] output index
|
|
180
171
|
# @param script_hash [String, nil] ignored
|
|
181
|
-
# @return [Result::Success<Boolean>, Result::Error
|
|
172
|
+
# @return [Result::Success<Boolean>, Result::Error]
|
|
182
173
|
def call_is_utxo(txid, vout, script_hash: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
183
174
|
result = default_call(:is_utxo, txid, vout)
|
|
184
|
-
return result unless result.success?
|
|
185
175
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
176
|
+
# 404 = no spending tx found = output is unspent
|
|
177
|
+
return Result::Success.new(data: true) if result.not_found?
|
|
178
|
+
|
|
179
|
+
# Non-success, non-404 = genuine error
|
|
180
|
+
return result unless result.success?
|
|
190
181
|
|
|
191
|
-
|
|
192
|
-
Result::Success.new(data:
|
|
182
|
+
# 200 with spending tx details = output is spent
|
|
183
|
+
Result::Success.new(data: false)
|
|
193
184
|
end
|
|
194
185
|
|
|
195
186
|
# Bulk-checks whether a set of outputs are unspent.
|
|
196
187
|
#
|
|
197
|
-
# WoC expects
|
|
198
|
-
# array of entries, each
|
|
188
|
+
# WoC expects +{ "utxos": [{ "txid": "...", "vout": N }, ...] }+ as the
|
|
189
|
+
# request body. It returns an array of entries, each with:
|
|
190
|
+
# +{ "utxo": { "txid": "...", "vout": N }, "spentIn": {...} | nil, "error": "" }+
|
|
191
|
+
# When +spentIn+ is present and non-empty the output is spent.
|
|
199
192
|
# Entries absent from the response (unknown outputs) are treated as spent.
|
|
200
193
|
#
|
|
201
194
|
# @param outpoints [Array<Hash>] array of +{ txid:, vout: }+ hashes
|
|
@@ -205,22 +198,25 @@ module BSV
|
|
|
205
198
|
def call_is_utxo_bulk(outpoints)
|
|
206
199
|
return Result::Success.new(data: {}) if outpoints.empty?
|
|
207
200
|
|
|
208
|
-
body = JSON.generate(outpoints.map { |op| { 'txid' => op[:txid].to_s, 'vout' => op[:vout].to_i } })
|
|
201
|
+
body = JSON.generate(utxos: outpoints.map { |op| { 'txid' => op[:txid].to_s, 'vout' => op[:vout].to_i } })
|
|
209
202
|
result = default_call(:is_utxo_bulk, body: body)
|
|
210
203
|
return result unless result.success?
|
|
211
204
|
|
|
212
|
-
# Build a lookup from the response
|
|
213
|
-
|
|
214
|
-
result.data.
|
|
215
|
-
next unless entry.is_a?(Hash) && entry.key?('
|
|
205
|
+
# Build a lookup from the response entries
|
|
206
|
+
# spentIn present and non-empty → spent (false); absent or empty → unspent (true)
|
|
207
|
+
normalised = result.data.each_with_object({}) do |entry, h|
|
|
208
|
+
next unless entry.is_a?(Hash) && entry.key?('utxo')
|
|
216
209
|
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
utxo = entry['utxo']
|
|
211
|
+
key = "#{utxo['txid']}.#{utxo['vout']}"
|
|
212
|
+
spent_in = entry['spentIn']
|
|
213
|
+
h[key] = spent_in.nil? || !spent_in.is_a?(Hash) || spent_in.empty?
|
|
219
214
|
end
|
|
220
215
|
|
|
221
|
-
|
|
216
|
+
# Unknown outpoints (absent from response) default to spent (false)
|
|
217
|
+
outpoints.each do |op|
|
|
222
218
|
key = "#{op[:txid]}.#{op[:vout]}"
|
|
223
|
-
|
|
219
|
+
normalised[key] = false unless normalised.key?(key)
|
|
224
220
|
end
|
|
225
221
|
|
|
226
222
|
Result::Success.new(data: normalised)
|
|
@@ -260,26 +256,110 @@ module BSV
|
|
|
260
256
|
|
|
261
257
|
# Fetches raw hex for multiple transactions in a single request.
|
|
262
258
|
#
|
|
263
|
-
# WoC expects
|
|
259
|
+
# WoC expects +{ "txids": [...] }+ as the request body.
|
|
264
260
|
#
|
|
265
261
|
# @param txids [Array<String>] list of transaction IDs
|
|
266
262
|
# @return [Result::Success<Array>, Result::Error]
|
|
267
263
|
def call_get_tx_hex_bulk(txids)
|
|
268
|
-
body = JSON.generate(txids)
|
|
264
|
+
body = JSON.generate(txids: txids)
|
|
269
265
|
default_call(:get_tx_hex_bulk, body: body)
|
|
270
266
|
end
|
|
271
267
|
|
|
272
268
|
# Fetches confirmed UTXOs for multiple script hashes in a single request.
|
|
273
269
|
#
|
|
274
|
-
# WoC expects
|
|
270
|
+
# WoC expects +{ "scripts": [...] }+ as the request body.
|
|
275
271
|
#
|
|
276
272
|
# @param script_hashes [Array<String>] list of script hashes
|
|
277
273
|
# @return [Result::Success<Hash>, Result::Error]
|
|
278
274
|
def call_get_script_unspent_bulk(script_hashes)
|
|
279
|
-
body = JSON.generate(script_hashes)
|
|
275
|
+
body = JSON.generate(scripts: script_hashes)
|
|
280
276
|
default_call(:get_script_unspent_bulk, body: body)
|
|
281
277
|
end
|
|
282
278
|
|
|
279
|
+
# Fetches full transaction details for multiple transactions.
|
|
280
|
+
#
|
|
281
|
+
# WoC expects +{ "txids": [...] }+ as the request body.
|
|
282
|
+
#
|
|
283
|
+
# @param txids [Array<String>] list of transaction IDs
|
|
284
|
+
# @return [Result::Success<Array>, Result::Error]
|
|
285
|
+
def call_get_bulk_tx_details(txids)
|
|
286
|
+
body = JSON.generate(txids: txids)
|
|
287
|
+
default_call(:get_bulk_tx_details, body: body)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Fetches output scripts for specific vouts across multiple transactions.
|
|
291
|
+
#
|
|
292
|
+
# WoC expects +{ "txids": [{ "txid": "...", "vouts": [0, 1] }, ...] }+ as the request body.
|
|
293
|
+
#
|
|
294
|
+
# @param tx_vouts [Array<Hash>] array of +{ txid:, vouts: [Integer] }+ hashes
|
|
295
|
+
# @return [Result::Success<Array>, Result::Error]
|
|
296
|
+
def call_get_bulk_output_scripts(tx_vouts)
|
|
297
|
+
body = JSON.generate(txids: tx_vouts.map { |tv| { 'txid' => tv[:txid].to_s, 'vouts' => tv[:vouts] } })
|
|
298
|
+
default_call(:get_bulk_output_scripts, body: body)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Fetches confirmed UTXOs for multiple addresses in a single request.
|
|
302
|
+
#
|
|
303
|
+
# WoC expects +{ "addresses": [...] }+ as the request body.
|
|
304
|
+
#
|
|
305
|
+
# @param addresses [Array<String>] list of addresses
|
|
306
|
+
# @return [Result::Success<Array>, Result::Error]
|
|
307
|
+
def call_get_bulk_address_utxos(addresses)
|
|
308
|
+
body = JSON.generate(addresses: addresses)
|
|
309
|
+
default_call(:get_bulk_address_utxos, body: body)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Fetches unconfirmed UTXOs for multiple addresses in a single request.
|
|
313
|
+
#
|
|
314
|
+
# WoC expects +{ "addresses": [...] }+ as the request body.
|
|
315
|
+
#
|
|
316
|
+
# @param addresses [Array<String>] list of addresses
|
|
317
|
+
# @return [Result::Success<Array>, Result::Error]
|
|
318
|
+
def call_get_bulk_address_unconfirmed_utxos(addresses)
|
|
319
|
+
body = JSON.generate(addresses: addresses)
|
|
320
|
+
default_call(:get_bulk_address_unconfirmed_utxos, body: body)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Fetches unconfirmed UTXOs for multiple script hashes in a single request.
|
|
324
|
+
#
|
|
325
|
+
# WoC expects +{ "scripts": [...] }+ as the request body.
|
|
326
|
+
#
|
|
327
|
+
# @param script_hashes [Array<String>] list of script hashes
|
|
328
|
+
# @return [Result::Success<Hash>, Result::Error]
|
|
329
|
+
def call_get_bulk_script_unconfirmed_unspent(script_hashes)
|
|
330
|
+
body = JSON.generate(scripts: script_hashes)
|
|
331
|
+
default_call(:get_bulk_script_unconfirmed_unspent, body: body)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Searches WhatsOnChain for links matching a query.
|
|
335
|
+
#
|
|
336
|
+
# WoC expects +{ "query": "..." }+ as the request body.
|
|
337
|
+
#
|
|
338
|
+
# @param query [String] search term
|
|
339
|
+
# @return [Result::Success<Hash>, Result::Error]
|
|
340
|
+
def call_search_links(query)
|
|
341
|
+
body = JSON.generate(query: query)
|
|
342
|
+
default_call(:search_links, body: body)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Fetches status for multiple transactions.
|
|
346
|
+
#
|
|
347
|
+
# WoC expects +{ "txids": [...] }+ as the request body. When called
|
|
348
|
+
# with a raw +body:+ keyword the body is forwarded as-is, which
|
|
349
|
+
# preserves backwards-compatibility with callers that build the body
|
|
350
|
+
# themselves.
|
|
351
|
+
#
|
|
352
|
+
# @param txids [Array<String>, nil] list of transaction IDs (positional)
|
|
353
|
+
# @param body [String, nil] pre-serialised request body (keyword)
|
|
354
|
+
# @return [Result::Success<Array>, Result::Error]
|
|
355
|
+
# @raise [ArgumentError] when neither txids nor body is provided
|
|
356
|
+
def call_get_tx_status(txids = nil, body: nil)
|
|
357
|
+
raise ArgumentError, 'provide txids array or body: keyword' if txids.nil? && body.nil?
|
|
358
|
+
|
|
359
|
+
raw_body = body || JSON.generate(txids: txids)
|
|
360
|
+
default_call(:get_tx_status, body: raw_body)
|
|
361
|
+
end
|
|
362
|
+
|
|
283
363
|
# Verifies that a merkle root matches the one recorded for a given
|
|
284
364
|
# block height. Uses the get_block_header endpoint internally.
|
|
285
365
|
#
|
|
@@ -9,6 +9,7 @@ module BSV
|
|
|
9
9
|
module Protocols
|
|
10
10
|
autoload :ARC, 'bsv/network/protocols/arc'
|
|
11
11
|
autoload :Chaintracks, 'bsv/network/protocols/chaintracks'
|
|
12
|
+
autoload :JungleBus, 'bsv/network/protocols/jungle_bus'
|
|
12
13
|
autoload :Ordinals, 'bsv/network/protocols/ordinals'
|
|
13
14
|
autoload :TAALBinary, 'bsv/network/protocols/taal_binary'
|
|
14
15
|
autoload :WoCREST, 'bsv/network/protocols/woc_rest'
|
data/lib/bsv/network/provider.rb
CHANGED
|
@@ -22,17 +22,30 @@ module BSV
|
|
|
22
22
|
# result = gorillapool.call(:broadcast, tx)
|
|
23
23
|
# result.success? # => true
|
|
24
24
|
class Provider
|
|
25
|
-
attr_reader :name
|
|
25
|
+
attr_reader :name, :auth, :rate_limit
|
|
26
26
|
|
|
27
|
-
# @param name
|
|
28
|
-
# @param
|
|
29
|
-
|
|
27
|
+
# @param name [String] human-readable provider name (e.g. 'GorillaPool')
|
|
28
|
+
# @param auth [Hash, Symbol] authentication config or +:none+ (default: +:none+).
|
|
29
|
+
# An empty hash or +nil+ is treated as +:none+.
|
|
30
|
+
# @param rate_limit [Numeric, nil] maximum requests per second (+nil+ = unlimited)
|
|
31
|
+
# @param block [Proc] optional configuration block — yields +self+
|
|
32
|
+
def initialize(name, auth: :none, rate_limit: nil, &block)
|
|
30
33
|
@name = name
|
|
34
|
+
@auth = normalise_auth(auth)
|
|
35
|
+
@rate_limit = rate_limit
|
|
31
36
|
@protocols = []
|
|
32
37
|
@command_index = {}
|
|
33
38
|
block&.call(self)
|
|
34
39
|
end
|
|
35
40
|
|
|
41
|
+
# Returns +true+ when the provider is configured with authentication
|
|
42
|
+
# credentials (i.e. +auth+ is not +:none+ and not an empty hash).
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def authenticated?
|
|
46
|
+
@auth != :none
|
|
47
|
+
end
|
|
48
|
+
|
|
36
49
|
# Registers a protocol class with the provider.
|
|
37
50
|
#
|
|
38
51
|
# The class is instantiated with the supplied +kwargs+. Its commands are
|
|
@@ -100,7 +113,9 @@ module BSV
|
|
|
100
113
|
# @return [String]
|
|
101
114
|
def to_s
|
|
102
115
|
protocol_summary = @protocols.map { |p| p.class.name&.split('::')&.last || p.class.to_s }.join(', ')
|
|
103
|
-
|
|
116
|
+
auth_status = authenticated? ? 'authenticated' : 'unauthenticated'
|
|
117
|
+
rate_part = @rate_limit.nil? ? '' : " rate_limit=#{@rate_limit}"
|
|
118
|
+
"#<#{self.class} name=#{@name.inspect} auth=#{auth_status}#{rate_part} protocols=[#{protocol_summary}]>"
|
|
104
119
|
end
|
|
105
120
|
alias inspect to_s
|
|
106
121
|
|
|
@@ -118,6 +133,22 @@ module BSV
|
|
|
118
133
|
|
|
119
134
|
instance.call(sym, *args, **kwargs)
|
|
120
135
|
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# Normalises the +auth+ argument so that +nil+ and empty hashes are
|
|
140
|
+
# stored as +:none+, giving a single canonical sentinel value for
|
|
141
|
+
# "no authentication".
|
|
142
|
+
#
|
|
143
|
+
# @param auth [Hash, Symbol, nil]
|
|
144
|
+
# @return [Hash, Symbol]
|
|
145
|
+
def normalise_auth(auth)
|
|
146
|
+
return :none if auth.nil?
|
|
147
|
+
return :none if auth == :none
|
|
148
|
+
return :none if auth.is_a?(Hash) && (auth.empty? || (auth[:bearer].nil? && auth[:api_key].nil?))
|
|
149
|
+
|
|
150
|
+
auth
|
|
151
|
+
end
|
|
121
152
|
end
|
|
122
153
|
end
|
|
123
154
|
end
|