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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/lib/bsv/auth/certificate.rb +6 -2
- 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 +5 -2
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +4 -4
- data/lib/bsv/mcp/tools/check_balance.rb +2 -2
- data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
- data/lib/bsv/mcp/tools/helpers.rb +2 -2
- data/lib/bsv/network/broadcast_error.rb +2 -0
- data/lib/bsv/network/broadcast_response.rb +4 -1
- data/lib/bsv/network/protocol.rb +56 -4
- data/lib/bsv/network/protocols/arc.rb +10 -6
- 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 +18 -4
- data/lib/bsv/network/protocols/woc_rest.rb +166 -85
- 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 -4
- data/lib/bsv/overlay/topic_broadcaster.rb +2 -2
- data/lib/bsv/overlay/types.rb +2 -0
- data/lib/bsv/primitives/hex.rb +64 -0
- data/lib/bsv/registry/client.rb +10 -8
- data/lib/bsv/registry/types.rb +2 -0
- data/lib/bsv/script/interpreter/interpreter.rb +7 -0
- data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
- data/lib/bsv/transaction/beef.rb +223 -147
- data/lib/bsv/transaction/merkle_path.rb +54 -38
- data/lib/bsv/transaction/transaction.rb +103 -40
- data/lib/bsv/transaction/transaction_input.rb +23 -18
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/interface/brc100.rb +5 -2
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +2 -0
- data/lib/bsv/wallet/proto_wallet.rb +6 -0
- data/lib/bsv/wire_format.rb +40 -14
- data/lib/bsv-sdk.rb +14 -0
- metadata +4 -3
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
|
|
@@ -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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
53
|
-
# @param
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
p.protocol Protocols::
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
49
|
-
# @param
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
54
|
-
# @param network
|
|
55
|
-
# @param
|
|
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(**
|
|
61
|
-
when :test, :testnet then testnet(**
|
|
62
|
-
when :stn then stn(**
|
|
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(**
|
|
87
|
+
testnet ? testnet(**kwargs) : mainnet(**kwargs)
|
|
67
88
|
end
|
|
68
89
|
end
|
|
69
90
|
end
|
data/lib/bsv/network/utxo.rb
CHANGED
|
@@ -5,10 +5,16 @@ module BSV
|
|
|
5
5
|
class UTXO
|
|
6
6
|
attr_reader :tx_hash, :tx_pos, :satoshis, :height
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
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&.
|
|
342
|
+
last&.dtxid
|
|
342
343
|
rescue StandardError
|
|
343
344
|
nil
|
|
344
345
|
end
|
|
345
346
|
|
|
346
|
-
|
|
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
|
|
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]
|
data/lib/bsv/overlay/types.rb
CHANGED
|
@@ -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
|
data/lib/bsv/primitives/hex.rb
CHANGED
|
@@ -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
|
data/lib/bsv/registry/client.rb
CHANGED
|
@@ -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
|
-
|
|
409
|
-
return nil
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
441
|
-
return nil
|
|
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 =
|
|
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)
|
data/lib/bsv/registry/types.rb
CHANGED
|
@@ -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
|