bsv-sdk 0.16.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/lib/bsv/auth/certificate.rb +6 -2
  4. data/lib/bsv/auth/get_verifiable_certificates.rb +6 -6
  5. data/lib/bsv/auth/peer.rb +10 -4
  6. data/lib/bsv/auth/session_manager.rb +81 -5
  7. data/lib/bsv/identity/client.rb +5 -2
  8. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +4 -4
  9. data/lib/bsv/mcp/tools/check_balance.rb +2 -2
  10. data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
  11. data/lib/bsv/mcp/tools/helpers.rb +2 -2
  12. data/lib/bsv/network/broadcast_error.rb +2 -0
  13. data/lib/bsv/network/broadcast_response.rb +4 -1
  14. data/lib/bsv/network/protocol.rb +56 -4
  15. data/lib/bsv/network/protocols/arc.rb +10 -6
  16. data/lib/bsv/network/protocols/chaintracks.rb +6 -2
  17. data/lib/bsv/network/protocols/jungle_bus.rb +52 -0
  18. data/lib/bsv/network/protocols/ordinals.rb +110 -8
  19. data/lib/bsv/network/protocols/taal_binary.rb +18 -4
  20. data/lib/bsv/network/protocols/woc_rest.rb +166 -85
  21. data/lib/bsv/network/protocols.rb +1 -0
  22. data/lib/bsv/network/provider.rb +36 -5
  23. data/lib/bsv/network/providers/gorilla_pool.rb +42 -20
  24. data/lib/bsv/network/providers/taal.rb +38 -15
  25. data/lib/bsv/network/providers/whats_on_chain.rb +42 -21
  26. data/lib/bsv/network/utxo.rb +8 -2
  27. data/lib/bsv/overlay/lookup_resolver.rb +5 -4
  28. data/lib/bsv/overlay/topic_broadcaster.rb +2 -2
  29. data/lib/bsv/overlay/types.rb +2 -0
  30. data/lib/bsv/primitives/hex.rb +64 -0
  31. data/lib/bsv/registry/client.rb +10 -8
  32. data/lib/bsv/registry/types.rb +2 -0
  33. data/lib/bsv/script/interpreter/interpreter.rb +7 -0
  34. data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
  35. data/lib/bsv/transaction/beef.rb +223 -147
  36. data/lib/bsv/transaction/merkle_path.rb +54 -38
  37. data/lib/bsv/transaction/transaction.rb +103 -40
  38. data/lib/bsv/transaction/transaction_input.rb +23 -18
  39. data/lib/bsv/version.rb +1 -1
  40. data/lib/bsv/wallet/interface/brc100.rb +5 -2
  41. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +2 -0
  42. data/lib/bsv/wallet/proto_wallet.rb +6 -0
  43. data/lib/bsv/wire_format.rb +40 -14
  44. data/lib/bsv-sdk.rb +14 -0
  45. metadata +4 -3
@@ -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 [String] human-readable provider name (e.g. 'GorillaPool')
28
- # @param block [Proc] optional configuration block yields +self+
29
- def initialize(name, &block)
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
- "#<#{self.class} name=#{@name.inspect} protocols=[#{protocol_summary}]>"
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
@@ -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,16 +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
- txid =
337
+ # Overlay API boundary: outpoint key uses display-order hex txid
338
+ dtxid_hex =
338
339
  begin
339
340
  beef = parse_beef(beef_data)
340
341
  last = beef&.transactions&.last
341
- last&.transaction&.txid
342
+ last&.dtxid
342
343
  rescue StandardError
343
344
  nil
344
345
  end
345
346
 
346
- txid ? "#{txid}.#{output_index}" : output.object_id.to_s
347
+ dtxid_hex ? "#{dtxid_hex}.#{output_index}" : output.object_id.to_s
347
348
  end
348
349
 
349
350
  # ---- Validation ----
@@ -110,7 +110,7 @@ module BSV
110
110
 
111
111
  OverlayBroadcastResult.new(
112
112
  status: 'success',
113
- txid: tx.txid_hex,
113
+ txid: tx.txid_hex, # Overlay API boundary: display-order hex txid for the broadcast result
114
114
  message: "Sent to #{successful.size} Overlay Service host(s)."
115
115
  )
116
116
  end
@@ -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]
@@ -82,6 +82,7 @@ module BSV
82
82
  # @return [String] result status ('success' or 'error')
83
83
  attr_reader :status
84
84
 
85
+ # Overlay API boundary: display-order hex txid echoed from the broadcast response.
85
86
  # @return [String, nil] transaction identifier (present on success)
86
87
  attr_reader :txid
87
88
 
@@ -101,6 +102,7 @@ module BSV
101
102
  # @param description [String, nil]
102
103
  def initialize(status:, txid: nil, message: nil, code: nil, description: nil)
103
104
  @status = status
105
+ BSV::Primitives::Hex.validate_dtxid_hex!(txid, name: 'overlay broadcast txid') if txid
104
106
  @txid = txid
105
107
  @message = message
106
108
  @code = code
@@ -72,6 +72,70 @@ module BSV
72
72
  def self.encode(bytes)
73
73
  bytes.unpack1('H*')
74
74
  end
75
+
76
+ # Validate that +value+ is a 32-byte wire-order transaction ID.
77
+ #
78
+ # @param value [String] expected 32-byte binary string
79
+ # @param name [String] label for the error message (e.g. +'prev_wtxid'+)
80
+ # @return [String] the input value (pass-through for chaining)
81
+ # @raise [ArgumentError] if +value+ is not a 32-byte binary string
82
+ def self.validate_wtxid!(value, name: 'wtxid')
83
+ unless value.is_a?(String) && value.bytesize == 32
84
+ hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
85
+ ' (looks like a hex txid — use wtxid_from_hex to convert)'
86
+ else
87
+ ''
88
+ end
89
+ size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
90
+ raise ArgumentError,
91
+ "expected 32-byte wire-order wtxid for #{name}, got #{size}#{hint}"
92
+ end
93
+ value
94
+ end
95
+
96
+ # Validate that +value+ is a 32-byte binary hash.
97
+ #
98
+ # General-purpose validator for any 32-byte hash (merkle nodes, roots,
99
+ # etc.) — not specific to transaction IDs. For txid-specific validation
100
+ # use {.validate_wtxid!} or {.validate_dtxid_hex!} instead.
101
+ #
102
+ # @param value [String] expected 32-byte binary string
103
+ # @param name [String] label for the error message
104
+ # @return [String] the input value (pass-through for chaining)
105
+ # @raise [ArgumentError] if +value+ is not a 32-byte binary string
106
+ def self.validate_hash32!(value, name: 'hash')
107
+ unless value.is_a?(String) && value.bytesize == 32
108
+ hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
109
+ ' (looks like hex — decode it first)'
110
+ else
111
+ ''
112
+ end
113
+ size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
114
+ raise ArgumentError,
115
+ "expected 32-byte hash for #{name}, got #{size}#{hint}"
116
+ end
117
+ value
118
+ end
119
+
120
+ # Validate that +value+ is a 64-character display-order hex transaction ID.
121
+ #
122
+ # @param value [String] expected 64-char hex string
123
+ # @param name [String] label for the error message (e.g. +'dtxid_hex'+)
124
+ # @return [String] the input value (pass-through for chaining)
125
+ # @raise [ArgumentError] if +value+ is not a 64-char hex string
126
+ def self.validate_dtxid_hex!(value, name: 'dtxid_hex')
127
+ unless value.is_a?(String) && value.length == 64 && value.match?(HEX_RE)
128
+ hint = if value.is_a?(String) && value.bytesize == 32 && !value.match?(HEX_RE)
129
+ ' (looks like binary bytes — use dtxid_hex or unpack to convert)'
130
+ else
131
+ ''
132
+ end
133
+ size = value.is_a?(String) ? "#{value.length}-char string" : value.class.to_s
134
+ raise ArgumentError,
135
+ "expected 64-char display-order hex for #{name}, got #{size}#{hint}"
136
+ end
137
+ value
138
+ end
75
139
  end
76
140
  end
77
141
  end
@@ -155,6 +155,7 @@ module BSV
155
155
  verify_ownership(registered_definition)
156
156
 
157
157
  definition_type = registered_definition.definition_type
158
+ # Registry API boundary: outpoint uses display-order hex txid from RegisteredDefinition
158
159
  outpoint = "#{registered_definition.txid}.#{registered_definition.output_index}"
159
160
 
160
161
  create_result = @wallet.create_action(
@@ -405,21 +406,21 @@ module BSV
405
406
  return nil if output_idx.negative? || beef_raw.nil?
406
407
 
407
408
  beef = BSV::Transaction::Beef.from_binary(beef_raw)
408
- tx = beef.transactions.last&.transaction
409
- 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)
410
411
 
411
- locking_script = tx.outputs[output_idx]&.locking_script
412
+ locking_script = beef_tx.transaction.outputs[output_idx]&.locking_script
412
413
  return nil unless locking_script
413
414
 
414
415
  definition_data = parse_locking_script(definition_type, locking_script)
415
416
 
416
417
  RegisteredDefinition.new(
417
418
  definition_data: definition_data,
418
- txid: tx.txid_hex,
419
+ txid: beef_tx.transaction.txid_hex, # Registry API boundary: display-order hex txid
419
420
  output_index: output_idx,
420
421
  locking_script: locking_script.to_hex,
421
422
  beef: beef_raw,
422
- satoshis: tx.outputs[output_idx].satoshis
423
+ satoshis: beef_tx.transaction.outputs[output_idx].satoshis
423
424
  )
424
425
  end
425
426
 
@@ -434,13 +435,14 @@ module BSV
434
435
  outpoint_str = output[:outpoint] || output['outpoint']
435
436
  return nil unless outpoint_str
436
437
 
438
+ # Registry API boundary: outpoint string uses display-order hex txid by convention
437
439
  txid, output_idx_str = outpoint_str.split('.')
438
440
  output_idx = output_idx_str.to_i
439
441
 
440
- tx = beef.transactions.last&.transaction
441
- 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)
442
444
 
443
- locking_script = tx.outputs[output_idx]&.locking_script
445
+ locking_script = beef_tx.transaction.outputs[output_idx]&.locking_script
444
446
  return nil unless locking_script
445
447
 
446
448
  definition_data = parse_locking_script(definition_type, locking_script)
@@ -188,6 +188,7 @@ module BSV
188
188
  # @return [String] the definition type (see {DefinitionType})
189
189
  attr_reader :definition_type
190
190
 
191
+ # Registry API boundary: display-order hex txid from the outpoint string held in the registry token.
191
192
  # @return [String] transaction ID of the containing UTXO
192
193
  attr_reader :txid
193
194
 
@@ -216,6 +217,7 @@ module BSV
216
217
  def initialize(definition_data:, txid:, output_index:, locking_script:, beef:, satoshis: 1)
217
218
  @definition_data = definition_data
218
219
  @definition_type = definition_data.definition_type
220
+ BSV::Primitives::Hex.validate_dtxid_hex!(txid, name: 'registry definition txid')
219
221
  @txid = txid
220
222
  @output_index = output_index
221
223
  @locking_script = locking_script
@@ -89,10 +89,12 @@ module BSV
89
89
 
90
90
  def execute
91
91
  scripts = [@unlock_script, @lock_script]
92
+ script_names = %w[unlock_script lock_script]
92
93
 
93
94
  scripts.each_with_index do |script, script_idx|
94
95
  @current_script = script
95
96
  chunks = script.chunks
97
+ BSV.logger&.debug { "[Interpreter] === #{script_names[script_idx]} (#{chunks.length} chunks) ===" }
96
98
 
97
99
  chunks.each_with_index do |chunk, chunk_idx|
98
100
  @current_chunk_idx = chunk_idx
@@ -115,6 +117,7 @@ module BSV
115
117
  end
116
118
 
117
119
  check_final_stack
120
+ BSV.logger&.debug { "[Interpreter] final stack: #{@dstack.length} items -> success" }
118
121
  true
119
122
  end
120
123
 
@@ -156,6 +159,10 @@ module BSV
156
159
  return
157
160
  end
158
161
 
162
+ BSV.logger&.debug do
163
+ name = Opcodes.name_for(opcode) || format('0x%02x', opcode)
164
+ "[Interpreter] #{name} (stack: #{@dstack.length})"
165
+ end
159
166
  dispatch_opcode(opcode, chunk)
160
167
  end
161
168
 
@@ -126,7 +126,13 @@ module BSV
126
126
 
127
127
  pubkey = BSV::Primitives::PublicKey.from_bytes(pubkey_bytes)
128
128
  hash = @tx.sighash(@input_index, sighash_type, subscript: sub_script)
129
- pubkey.verify(hash, sig)
129
+ result = pubkey.verify(hash, sig)
130
+ BSV.logger&.debug do
131
+ pk_hex = pubkey_bytes.unpack1('H*')
132
+ "[Interpreter] CHECKSIG: sighash_type=0x#{format('%02x', sighash_type)} " \
133
+ "pubkey=#{pk_hex[0, 8]}...#{pk_hex[-4..]} result=#{result}"
134
+ end
135
+ result
130
136
  rescue ArgumentError
131
137
  false
132
138
  end