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.
@@ -4,56 +4,78 @@ 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, and the GorillaPool Ordinals
8
- # API for transaction and merkle path lookups.
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.
9
10
  #
10
- # Mainnet composes three protocols:
11
+ # Mainnet composes four protocols:
11
12
  # - ARC at +https://arcade.gorillapool.io+
12
13
  # - Chaintracks at +https://arcade.gorillapool.io+
13
14
  # - Ordinals at +https://ordinals.gorillapool.io+
15
+ # - JungleBus at +https://junglebus.gorillapool.io+
14
16
  #
15
- # Testnet provides ARC only at +https://testnet.arcade.gorillapool.io+.
17
+ # Testnet provides ARC and Chaintracks at +https://testnet.arcade.gorillapool.io+.
16
18
  #
17
19
  # == Example
18
20
  #
19
21
  # provider = BSV::Network::Providers::GorillaPool.mainnet
20
22
  # provider.call(:broadcast, tx)
21
23
  #
22
- # provider = BSV::Network::Providers::GorillaPool.testnet(api_key: 'my-key')
24
+ # provider = BSV::Network::Providers::GorillaPool.mainnet(auth: { bearer: 'token' })
23
25
  # provider.call(:broadcast, tx)
26
+ #
27
+ # # Legacy api_key: shorthand — still supported
28
+ # provider = BSV::Network::Providers::GorillaPool.testnet(api_key: 'my-key')
24
29
  class GorillaPool
25
- # Returns a mainnet Provider configured with ARC, Chaintracks, and Ordinals.
30
+ # Default requests-per-second limit for unauthenticated use.
31
+ DEFAULT_RATE_LIMIT = 3
32
+
33
+ # Returns a mainnet Provider configured with ARC, Chaintracks, Ordinals, and JungleBus.
34
+ #
35
+ # Auth is forwarded to all four protocols so each can authenticate independently.
26
36
  #
27
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
37
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
38
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
39
+ # @param opts [Hash] keyword arguments forwarded to each protocol constructor
28
40
  # @return [Provider]
29
- def self.mainnet(**opts)
30
- common = opts.slice(:api_key, :http_client)
31
- Provider.new('GorillaPool') do |p|
32
- p.protocol Protocols::ARC, base_url: 'https://arcade.gorillapool.io', **opts
41
+ def self.mainnet(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
42
+ resolved_auth = auth || (opts[:api_key] ? { bearer: opts[:api_key] } : :none)
43
+ common = opts.slice(:api_key, :http_client).merge(auth: auth)
44
+ 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
33
46
  p.protocol Protocols::Chaintracks, base_url: 'https://arcade.gorillapool.io', **common
34
47
  p.protocol Protocols::Ordinals, base_url: 'https://ordinals.gorillapool.io', **common
48
+ p.protocol Protocols::JungleBus, base_url: 'https://junglebus.gorillapool.io', **common
35
49
  end
36
50
  end
37
51
 
38
52
  # Returns a testnet Provider configured with ARC and Chaintracks.
39
53
  #
40
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
54
+ # Auth is forwarded to both protocols.
55
+ #
56
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
57
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
58
+ # @param opts [Hash] keyword arguments forwarded to each protocol constructor
41
59
  # @return [Provider]
42
- def self.testnet(**opts)
43
- common = opts.slice(:api_key, :http_client)
44
- Provider.new('GorillaPool') do |p|
45
- p.protocol Protocols::ARC, base_url: 'https://testnet.arcade.gorillapool.io', **opts
60
+ def self.testnet(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
61
+ resolved_auth = auth || (opts[:api_key] ? { bearer: opts[:api_key] } : :none)
62
+ common = opts.slice(:api_key, :http_client).merge(auth: auth)
63
+ 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
46
65
  p.protocol Protocols::Chaintracks, base_url: 'https://testnet.arcade.gorillapool.io', **common
47
66
  end
48
67
  end
49
68
 
50
69
  # Returns a mainnet or testnet Provider depending on the +testnet:+ flag.
51
70
  #
52
- # @param testnet [Boolean] when true, returns the testnet Provider
53
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
71
+ # @param testnet [Boolean] when true, returns the testnet Provider
72
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
73
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
74
+ # @param opts [Hash] keyword arguments forwarded to each protocol constructor
54
75
  # @return [Provider]
55
- def self.default(testnet: false, **opts)
56
- testnet ? testnet(**opts) : mainnet(**opts)
76
+ def self.default(testnet: false, auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
77
+ kwargs = { auth: auth, rate_limit: rate_limit, **opts }
78
+ testnet ? testnet(**kwargs) : mainnet(**kwargs)
57
79
  end
58
80
  end
59
81
  end
@@ -14,42 +14,65 @@ module BSV
14
14
  # To use TAALBinary directly, call +provider.protocol_for(:broadcast)+ on the
15
15
  # TAALBinary instance via +provider.protocols.last+, or build a custom Provider.
16
16
  #
17
+ # TAAL requires an API key for production use. The default rate limit is +nil+
18
+ # (unconstrained) because the effective limit depends on the subscription tier.
19
+ #
17
20
  # There is no TAAL testnet default — TAAL does not publish a supported testnet ARC URL.
18
21
  #
19
22
  # == Example
20
23
  #
21
- # provider = BSV::Network::Providers::TAAL.mainnet(api_key: 'mainnet_...')
24
+ # provider = BSV::Network::Providers::TAAL.mainnet(auth: { bearer: 'mainnet_...' })
22
25
  # provider.call(:broadcast, tx)
26
+ #
27
+ # # Legacy api_key: shorthand — still supported
28
+ # provider = BSV::Network::Providers::TAAL.mainnet(api_key: 'mainnet_...')
23
29
  class TAAL
30
+ # Default requests-per-second limit.
31
+ # +nil+ because the effective limit depends on the TAAL subscription tier.
32
+ DEFAULT_RATE_LIMIT = nil
33
+
24
34
  # Returns a mainnet Provider configured with ARC and TAALBinary.
25
35
  #
26
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
36
+ # Auth is forwarded to both protocols. ARC uses Bearer tokens;
37
+ # TAALBinary uses raw API keys. Each protocol's constructor handles
38
+ # the translation appropriate to its endpoint.
39
+ #
40
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
41
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+ (nil)
42
+ # @param opts [Hash] keyword arguments forwarded to each protocol constructor
27
43
  # @return [Provider]
28
- def self.mainnet(**opts)
29
- common = opts.slice(:api_key, :http_client)
30
- Provider.new('TAAL') do |p|
31
- p.protocol Protocols::ARC, base_url: 'https://arc.taal.com', **opts
32
- p.protocol Protocols::TAALBinary, base_url: 'https://api.taal.com', **common
44
+ def self.mainnet(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
45
+ resolved_auth = auth || (opts[:api_key] ? { bearer: opts[:api_key] } : :none)
46
+ common = opts.slice(:api_key, :http_client).merge(auth: auth)
47
+ Provider.new('TAAL', auth: resolved_auth, rate_limit: rate_limit) do |p|
48
+ p.protocol Protocols::ARC, base_url: 'https://arc.taal.com', auth: auth, **opts
49
+ p.protocol Protocols::TAALBinary, base_url: 'https://api.taal.com', **common
33
50
  end
34
51
  end
35
52
 
36
53
  # Returns a testnet Provider configured with ARC only.
37
54
  #
38
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
55
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and ARC protocol
56
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+ (nil)
57
+ # @param opts [Hash] keyword arguments forwarded to the ARC protocol constructor
39
58
  # @return [Provider]
40
- def self.testnet(**opts)
41
- Provider.new('TAAL') do |p|
42
- p.protocol Protocols::ARC, base_url: 'https://arc-test.taal.com', **opts
59
+ def self.testnet(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
60
+ resolved_auth = auth || (opts[:api_key] ? { bearer: opts[:api_key] } : :none)
61
+ Provider.new('TAAL', auth: resolved_auth, rate_limit: rate_limit) do |p|
62
+ p.protocol Protocols::ARC, base_url: 'https://arc-test.taal.com', auth: auth, **opts
43
63
  end
44
64
  end
45
65
 
46
66
  # Returns a mainnet or testnet Provider depending on the +testnet:+ flag.
47
67
  #
48
- # @param testnet [Boolean] when true, returns the testnet Provider
49
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
68
+ # @param testnet [Boolean] when true, returns the testnet Provider
69
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and all protocols
70
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+ (nil)
71
+ # @param opts [Hash] keyword arguments forwarded to each protocol constructor
50
72
  # @return [Provider]
51
- def self.default(testnet: false, **opts)
52
- testnet ? testnet(**opts) : mainnet(**opts)
73
+ def self.default(testnet: false, auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
74
+ kwargs = { auth: auth, rate_limit: rate_limit, **opts }
75
+ testnet ? testnet(**kwargs) : mainnet(**kwargs)
53
76
  end
54
77
  end
55
78
  end
@@ -15,55 +15,76 @@ module BSV
15
15
  # provider = BSV::Network::Providers::WhatsOnChain.mainnet
16
16
  # provider.call(:get_tx, 'abc123...')
17
17
  #
18
- # provider = BSV::Network::Providers::WhatsOnChain.testnet(api_key: 'my-key')
18
+ # provider = BSV::Network::Providers::WhatsOnChain.mainnet(auth: { api_key: 'my-key' })
19
19
  # provider.call(:broadcast, tx)
20
+ #
21
+ # # Legacy api_key: shorthand — still supported
22
+ # provider = BSV::Network::Providers::WhatsOnChain.testnet(api_key: 'my-key')
20
23
  class WhatsOnChain
24
+ # Default requests-per-second limit for unauthenticated use.
25
+ DEFAULT_RATE_LIMIT = 3
26
+
21
27
  # Returns a mainnet Provider configured with WoCREST.
22
28
  #
23
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
29
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and WoCREST protocol
30
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
31
+ # @param opts [Hash] keyword arguments forwarded to the WoCREST protocol constructor
24
32
  # @return [Provider]
25
- def self.mainnet(**opts)
26
- Provider.new('WhatsOnChain') do |p|
27
- p.protocol Protocols::WoCREST, base_url: 'https://api.whatsonchain.com/v1/bsv/main', **opts
33
+ def self.mainnet(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
34
+ resolved_auth = auth || (opts[:api_key] ? { api_key: opts[:api_key] } : :none)
35
+ Provider.new('WhatsOnChain', auth: resolved_auth, rate_limit: rate_limit) do |p|
36
+ p.protocol Protocols::WoCREST, base_url: 'https://api.whatsonchain.com/v1/bsv/main',
37
+ auth: auth, **opts
28
38
  end
29
39
  end
30
40
 
31
41
  # Returns a testnet Provider configured with WoCREST.
32
42
  #
33
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
43
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and WoCREST protocol
44
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
45
+ # @param opts [Hash] keyword arguments forwarded to the WoCREST protocol constructor
34
46
  # @return [Provider]
35
- def self.testnet(**opts)
36
- Provider.new('WhatsOnChain') do |p|
37
- p.protocol Protocols::WoCREST, base_url: 'https://api.whatsonchain.com/v1/bsv/test', **opts
47
+ def self.testnet(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
48
+ resolved_auth = auth || (opts[:api_key] ? { api_key: opts[:api_key] } : :none)
49
+ Provider.new('WhatsOnChain', auth: resolved_auth, rate_limit: rate_limit) do |p|
50
+ p.protocol Protocols::WoCREST, base_url: 'https://api.whatsonchain.com/v1/bsv/test',
51
+ auth: auth, **opts
38
52
  end
39
53
  end
40
54
 
41
55
  # Returns a Provider for the BSV Scaling Test Network (STN).
42
56
  #
43
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
57
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and WoCREST protocol
58
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
59
+ # @param opts [Hash] keyword arguments forwarded to the WoCREST protocol constructor
44
60
  # @return [Provider]
45
- def self.stn(**opts)
46
- Provider.new('WhatsOnChain') do |p|
47
- p.protocol Protocols::WoCREST, base_url: 'https://api.whatsonchain.com/v1/bsv/stn', **opts
61
+ def self.stn(auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
62
+ resolved_auth = auth || (opts[:api_key] ? { api_key: opts[:api_key] } : :none)
63
+ Provider.new('WhatsOnChain', auth: resolved_auth, rate_limit: rate_limit) do |p|
64
+ p.protocol Protocols::WoCREST, base_url: 'https://api.whatsonchain.com/v1/bsv/stn',
65
+ auth: auth, **opts
48
66
  end
49
67
  end
50
68
 
51
69
  # Returns a Provider for the given network.
52
70
  #
53
- # @param testnet [Boolean] when true, returns the testnet Provider
54
- # @param network [Symbol, nil] explicit network (:main, :test, :stn) — overrides +testnet:+
55
- # @param opts [Hash] keyword arguments forwarded to each protocol constructor
71
+ # @param testnet [Boolean] when true, returns the testnet Provider
72
+ # @param network [Symbol, nil] explicit network (:main, :test, :stn) — overrides +testnet:+
73
+ # @param auth [Hash, Symbol, nil] auth config forwarded to Provider and WoCREST protocol
74
+ # @param rate_limit [Numeric, nil] requests per second; defaults to +DEFAULT_RATE_LIMIT+
75
+ # @param opts [Hash] keyword arguments forwarded to the WoCREST protocol constructor
56
76
  # @return [Provider]
57
- def self.default(testnet: false, network: nil, **opts)
77
+ def self.default(testnet: false, network: nil, auth: nil, rate_limit: DEFAULT_RATE_LIMIT, **opts)
78
+ kwargs = { auth: auth, rate_limit: rate_limit, **opts }
58
79
  if network
59
80
  case network.to_sym
60
- when :main, :mainnet then mainnet(**opts)
61
- when :test, :testnet then testnet(**opts)
62
- when :stn then stn(**opts)
81
+ when :main, :mainnet then mainnet(**kwargs)
82
+ when :test, :testnet then testnet(**kwargs)
83
+ when :stn then stn(**kwargs)
63
84
  else raise ArgumentError, "unknown network: #{network}"
64
85
  end
65
86
  else
66
- testnet ? testnet(**opts) : mainnet(**opts)
87
+ testnet ? testnet(**kwargs) : mainnet(**kwargs)
67
88
  end
68
89
  end
69
90
  end
@@ -5,10 +5,16 @@ module BSV
5
5
  class UTXO
6
6
  attr_reader :tx_hash, :tx_pos, :satoshis, :height
7
7
 
8
- def initialize(tx_hash:, tx_pos:, satoshis:, height: nil)
8
+ # @param tx_hash [String] transaction ID
9
+ # @param tx_pos [Integer] output index
10
+ # @param satoshis [Integer] output value in satoshis (accepts +value+ as alias)
11
+ # @param height [Integer, nil] block height (0 or nil = unconfirmed)
12
+ def initialize(tx_hash:, tx_pos:, satoshis: nil, value: nil, height: nil)
9
13
  @tx_hash = tx_hash
10
14
  @tx_pos = tx_pos
11
- @satoshis = satoshis
15
+ @satoshis = satoshis || value
16
+ raise ArgumentError, 'satoshis or value is required' if @satoshis.nil?
17
+
12
18
  @height = height
13
19
  end
14
20
 
@@ -203,7 +203,7 @@ module BSV
203
203
 
204
204
  # Get the last (subject) transaction — typically the most recent entry
205
205
  beef_tx = beef.transactions.last
206
- return nil unless beef_tx&.transaction
206
+ return nil if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
207
207
 
208
208
  tx = beef_tx.transaction
209
209
  txout = tx.outputs[output_index]
@@ -334,17 +334,17 @@ module BSV
334
334
  beef_data = output['beef'] || output[:beef]
335
335
  output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
336
336
 
337
- # Overlay API boundary: outpoint key uses display-order txid bytes (via Transaction#txid)
338
- txid =
337
+ # Overlay API boundary: outpoint key uses display-order hex txid
338
+ dtxid_hex =
339
339
  begin
340
340
  beef = parse_beef(beef_data)
341
341
  last = beef&.transactions&.last
342
- last&.transaction&.txid
342
+ last&.dtxid
343
343
  rescue StandardError
344
344
  nil
345
345
  end
346
346
 
347
- txid ? "#{txid}.#{output_index}" : output.object_id.to_s
347
+ dtxid_hex ? "#{dtxid_hex}.#{output_index}" : output.object_id.to_s
348
348
  end
349
349
 
350
350
  # ---- Validation ----
@@ -199,7 +199,7 @@ module BSV
199
199
  return nil unless beef
200
200
 
201
201
  beef_tx = beef.transactions.last
202
- return nil unless beef_tx&.transaction
202
+ return nil if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
203
203
 
204
204
  tx = beef_tx.transaction
205
205
  txout = tx.outputs[output_index]
@@ -102,6 +102,7 @@ module BSV
102
102
  # @param description [String, nil]
103
103
  def initialize(status:, txid: nil, message: nil, code: nil, description: nil)
104
104
  @status = status
105
+ BSV::Primitives::Hex.validate_dtxid_hex!(txid, name: 'overlay broadcast txid') if txid
105
106
  @txid = txid
106
107
  @message = message
107
108
  @code = code
@@ -406,21 +406,21 @@ module BSV
406
406
  return nil if output_idx.negative? || beef_raw.nil?
407
407
 
408
408
  beef = BSV::Transaction::Beef.from_binary(beef_raw)
409
- tx = beef.transactions.last&.transaction
410
- return nil unless tx
409
+ beef_tx = beef.transactions.last
410
+ return nil if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
411
411
 
412
- locking_script = tx.outputs[output_idx]&.locking_script
412
+ locking_script = beef_tx.transaction.outputs[output_idx]&.locking_script
413
413
  return nil unless locking_script
414
414
 
415
415
  definition_data = parse_locking_script(definition_type, locking_script)
416
416
 
417
417
  RegisteredDefinition.new(
418
418
  definition_data: definition_data,
419
- txid: tx.txid_hex, # Registry API boundary: display-order hex txid
419
+ txid: beef_tx.transaction.txid_hex, # Registry API boundary: display-order hex txid
420
420
  output_index: output_idx,
421
421
  locking_script: locking_script.to_hex,
422
422
  beef: beef_raw,
423
- satoshis: tx.outputs[output_idx].satoshis
423
+ satoshis: beef_tx.transaction.outputs[output_idx].satoshis
424
424
  )
425
425
  end
426
426
 
@@ -439,10 +439,10 @@ module BSV
439
439
  txid, output_idx_str = outpoint_str.split('.')
440
440
  output_idx = output_idx_str.to_i
441
441
 
442
- tx = beef.transactions.last&.transaction
443
- return nil unless tx
442
+ beef_tx = beef.transactions.last
443
+ return nil if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
444
444
 
445
- locking_script = tx.outputs[output_idx]&.locking_script
445
+ locking_script = beef_tx.transaction.outputs[output_idx]&.locking_script
446
446
  return nil unless locking_script
447
447
 
448
448
  definition_data = parse_locking_script(definition_type, locking_script)
@@ -217,6 +217,7 @@ module BSV
217
217
  def initialize(definition_data:, txid:, output_index:, locking_script:, beef:, satoshis: 1)
218
218
  @definition_data = definition_data
219
219
  @definition_type = definition_data.definition_type
220
+ BSV::Primitives::Hex.validate_dtxid_hex!(txid, name: 'registry definition txid')
220
221
  @txid = txid
221
222
  @output_index = output_index
222
223
  @locking_script = locking_script