bsv-wallet 0.10.0 → 0.11.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 +37 -0
- data/lib/bsv/wallet/client/brc100/network.rb +26 -6
- data/lib/bsv/wallet/client/brc100/transaction.rb +13 -1
- data/lib/bsv/wallet/client/certificate_signature.rb +1 -1
- data/lib/bsv/wallet/client.rb +79 -5
- data/lib/bsv/wallet/store.rb +8 -0
- data/lib/bsv/wallet/testing/shared_examples_for_storage_adapter.rb +778 -0
- data/lib/bsv/wallet/testing/shared_examples_for_wallet_operations.rb +3701 -0
- data/lib/bsv/wallet/testing/store_conformance.rb +15 -0
- data/lib/bsv/wallet/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43a1c78810a0351eb11cf29575e9d2a9e8ffca2d5e844f8122bc28dc5dfffb02
|
|
4
|
+
data.tar.gz: 358f2f7514b5d44f1896ec3ca6898cfb2e195142639711e787f545d27eda6f03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c82bcf291d49346fd574d8bf442b690575f4b42d3c66e94a8262a32ceab596831d7c9b8c69a3617e5540f29e93417014f16979aa0e345eff0b48577de6ddf10e
|
|
7
|
+
data.tar.gz: 8a5adfbe6bb90ea76d8478d181f3a453d5a9c4a15ad3a1f7eb833ef9d1b2862b5305d2dcaab016f4b10d83f5f091338f1e66c9ba3fdf0858f25597304d345308
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ All notable changes to the `bsv-wallet` gem are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.11.0 — 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `chain_data_source:` constructor param on `Client` — injectable chain
|
|
12
|
+
data provider for local blockchain queries without a remote substrate
|
|
13
|
+
- `sync_utxos` restored — discovers on-chain UTXOs via the chain data
|
|
14
|
+
source and imports them into storage as `:spendable` with
|
|
15
|
+
`derivation_type: :identity`. Deduplicates transaction fetches and
|
|
16
|
+
validates UTXO API values against transaction outputs (#599)
|
|
17
|
+
- `get_height` restored with local chain data fallback — returns
|
|
18
|
+
`{ height: Integer }` without requiring a substrate (#597)
|
|
19
|
+
- `get_header_for_height` restored with local chain data fallback —
|
|
20
|
+
returns WoC block header JSON (#598)
|
|
21
|
+
- BEEF SPV merkle root verification restored in `internalize_action`
|
|
22
|
+
when a chain data source with `valid_root_for_height?` is available;
|
|
23
|
+
error messages now distinguish SPV rejection from structural
|
|
24
|
+
invalidity (#600)
|
|
25
|
+
- Store conformance shared examples extracted to
|
|
26
|
+
`lib/bsv/wallet/testing/` — downstream adapter gems can now
|
|
27
|
+
`require 'bsv/wallet/testing/store_conformance'` for interface and
|
|
28
|
+
wallet-level test coverage (#591)
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- `MemoryStore` guarded in `Client.new` — raises `ArgumentError` unless
|
|
32
|
+
`allow_memory_store: true` is passed, preventing accidental data loss
|
|
33
|
+
in production
|
|
34
|
+
- Wallet-level dual-store specs refactored to use shared examples via
|
|
35
|
+
`it_behaves_like`; no test coverage lost (1281 examples preserved)
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- `sync_utxos` deduplicates UTXO response entries before processing
|
|
39
|
+
and caches fetched transactions by txid to minimise WoC API calls
|
|
40
|
+
- `internalize_action` chain tracker guard uses explicit nil check for
|
|
41
|
+
clarity
|
|
42
|
+
- Consistent error message wording across `get_height` and
|
|
43
|
+
`get_header_for_height`
|
|
44
|
+
|
|
8
45
|
## 0.10.0 — 2026-04-21
|
|
9
46
|
|
|
10
47
|
### Added
|
|
@@ -7,26 +7,46 @@ module BSV
|
|
|
7
7
|
module Network
|
|
8
8
|
# Returns the current blockchain height.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# Delegates to the substrate when configured. Falls back to the local
|
|
11
|
+
# chain data source when present. Raises {UnsupportedActionError} when
|
|
12
|
+
# neither is available.
|
|
11
13
|
#
|
|
12
14
|
# @return [Hash] { height: Integer }
|
|
13
15
|
def get_height(args = {}, originator: nil)
|
|
14
16
|
return @substrate.get_height(args, originator: originator) if @substrate
|
|
15
17
|
|
|
16
|
-
raise UnsupportedActionError, 'get_height requires a remote substrate'
|
|
18
|
+
raise UnsupportedActionError, 'get_height requires a chain_data_source or remote substrate' unless @chain_data_source
|
|
19
|
+
|
|
20
|
+
{ height: @chain_data_source.current_height }
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# Returns the block header at the given height.
|
|
20
24
|
#
|
|
21
|
-
#
|
|
25
|
+
# Delegates to the substrate when configured; falls back to the local
|
|
26
|
+
# chain data source otherwise.
|
|
27
|
+
#
|
|
28
|
+
# Note: BRC-100 specifies +{ header: String }+ (80-byte raw hex), but the
|
|
29
|
+
# local fallback returns the richer WoC JSON hash (containing +hash+,
|
|
30
|
+
# +merkleroot+, +previousblockhash+, +time+, +nonce+, +bits+,
|
|
31
|
+
# +version+, and +height+) under the +header+ key. This is strictly more
|
|
32
|
+
# useful for local callers and avoids error-prone byte-order reassembly.
|
|
33
|
+
#
|
|
34
|
+
# *Warning:* the WalletWire binary serialiser expects +header+ to be a
|
|
35
|
+
# hex string. This local fallback should not be used behind a wire
|
|
36
|
+
# transport without first converting the JSON hash to raw 80-byte hex.
|
|
22
37
|
#
|
|
23
38
|
# @param args [Hash]
|
|
24
|
-
# @option args [Integer] :height block height
|
|
25
|
-
# @return [Hash] { header:
|
|
39
|
+
# @option args [Integer] :height block height (must be >= 0)
|
|
40
|
+
# @return [Hash] { header: Hash } WoC block header JSON
|
|
26
41
|
def get_header_for_height(args, originator: nil)
|
|
27
42
|
return @substrate.get_header_for_height(args, originator: originator) if @substrate
|
|
28
43
|
|
|
29
|
-
raise UnsupportedActionError, 'get_header_for_height requires a remote substrate'
|
|
44
|
+
raise UnsupportedActionError, 'get_header_for_height requires a chain_data_source or remote substrate' unless @chain_data_source
|
|
45
|
+
|
|
46
|
+
height = args[:height]
|
|
47
|
+
raise InvalidParameterError.new('height', 'a non-negative Integer') unless height.is_a?(Integer) && !height.negative?
|
|
48
|
+
|
|
49
|
+
{ header: @chain_data_source.get_block_header(height) }
|
|
30
50
|
end
|
|
31
51
|
|
|
32
52
|
# Returns the network this wallet is configured for.
|
|
@@ -175,7 +175,19 @@ module BSV
|
|
|
175
175
|
beef_binary = args[:tx].pack('C*')
|
|
176
176
|
beef = BSV::Transaction::Beef.from_binary(beef_binary)
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
# Use the chain data source for SPV merkle root verification when available;
|
|
179
|
+
# fall back to structural-only verification otherwise.
|
|
180
|
+
chain_tracker = @chain_data_source if @chain_data_source.respond_to?(:valid_root_for_height?)
|
|
181
|
+
unless beef.verify(chain_tracker)
|
|
182
|
+
# Distinguish SPV failures from structural invalidity so callers
|
|
183
|
+
# can tell whether the problem is the BEEF itself or the merkle root.
|
|
184
|
+
message = if chain_tracker && beef.verify(nil)
|
|
185
|
+
'BEEF verification failed: merkle root not confirmed by chain tracker'
|
|
186
|
+
else
|
|
187
|
+
'BEEF verification failed: the bundle is structurally invalid'
|
|
188
|
+
end
|
|
189
|
+
raise WalletError, message
|
|
190
|
+
end
|
|
179
191
|
|
|
180
192
|
tx = extract_subject_transaction(beef)
|
|
181
193
|
|
|
@@ -56,7 +56,7 @@ module BSV
|
|
|
56
56
|
# a fresh +Client.new('anyone', storage: Store::Memory.new)+
|
|
57
57
|
# @return [true] when the signature verifies
|
|
58
58
|
# @raise [InvalidError] otherwise
|
|
59
|
-
def verify!(cert, verifier: Client.new('anyone', storage: Store::Memory.new))
|
|
59
|
+
def verify!(cert, verifier: Client.new('anyone', storage: Store::Memory.new, allow_memory_store: true))
|
|
60
60
|
signature_hex = cert[:signature]
|
|
61
61
|
raise InvalidError, 'signature is missing' if signature_hex.nil? || signature_hex.empty?
|
|
62
62
|
|
data/lib/bsv/wallet/client.rb
CHANGED
|
@@ -50,9 +50,13 @@ module BSV
|
|
|
50
50
|
# @return [Interface, nil] the optional substrate for remote wallet delegation
|
|
51
51
|
attr_reader :substrate
|
|
52
52
|
|
|
53
|
+
# @return [#current_height, #get_block_header, #fetch_utxos, #fetch_transaction, nil]
|
|
54
|
+
# optional chain data source for SPV and block header lookups
|
|
55
|
+
attr_reader :chain_data_source
|
|
56
|
+
|
|
53
57
|
# @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
|
|
54
|
-
# @param storage [Store] persistence adapter (default: Store::File.new)
|
|
55
|
-
#
|
|
58
|
+
# @param storage [Store] persistence adapter (default: Store::File.new)
|
|
59
|
+
# @param allow_memory_store [Boolean] set +true+ to suppress the MemoryStore safety guard
|
|
56
60
|
# @param network [String] 'mainnet' (default) or 'testnet'
|
|
57
61
|
# @param proof_store [ProofStore, nil] merkle proof store (default: LocalProofStore backed by storage)
|
|
58
62
|
# @param http_client [#request, nil] injectable HTTP client for certificate issuance
|
|
@@ -62,6 +66,8 @@ module BSV
|
|
|
62
66
|
# @param broadcaster [#broadcast, nil] optional broadcaster
|
|
63
67
|
# @param broadcast_queue [BroadcastQueue, nil] optional broadcast queue; defaults to BroadcastQueue::Inline
|
|
64
68
|
# @param substrate [Interface, nil] optional remote wallet substrate
|
|
69
|
+
# @param chain_data_source [#current_height, #get_block_header, #fetch_utxos, #fetch_transaction, nil]
|
|
70
|
+
# optional chain data source for SPV and block header lookups
|
|
65
71
|
def initialize(
|
|
66
72
|
key,
|
|
67
73
|
storage: Store::File.new,
|
|
@@ -73,10 +79,19 @@ module BSV
|
|
|
73
79
|
change_generator: nil,
|
|
74
80
|
broadcaster: nil,
|
|
75
81
|
broadcast_queue: nil,
|
|
76
|
-
substrate: nil
|
|
82
|
+
substrate: nil,
|
|
83
|
+
chain_data_source: nil,
|
|
84
|
+
allow_memory_store: false
|
|
77
85
|
)
|
|
86
|
+
if storage.is_a?(Store::Memory) && !storage.is_a?(Store::File) && !allow_memory_store
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
'MemoryStore is not a safe storage adapter for wallets. ' \
|
|
89
|
+
'See: https://sgbett.github.io/bsv-ruby-sdk/gems/wallet/#storage-adapters'
|
|
90
|
+
end
|
|
91
|
+
|
|
78
92
|
@key_deriver = key.is_a?(KeyDeriver) ? key : KeyDeriver.new(key)
|
|
79
93
|
@substrate = substrate
|
|
94
|
+
@chain_data_source = chain_data_source
|
|
80
95
|
@storage = storage
|
|
81
96
|
@network = network
|
|
82
97
|
@proof_store = proof_store || LocalProofStore.new(storage)
|
|
@@ -98,9 +113,68 @@ module BSV
|
|
|
98
113
|
@broadcast_queue.broadcast_enabled?
|
|
99
114
|
end
|
|
100
115
|
|
|
101
|
-
#
|
|
116
|
+
# Discovers UTXOs on-chain for the wallet's identity address and imports
|
|
117
|
+
# any that are not already in local storage.
|
|
118
|
+
#
|
|
119
|
+
# Requires either a substrate or a chain_data_source. When both are present,
|
|
120
|
+
# the substrate takes priority.
|
|
121
|
+
#
|
|
122
|
+
# @return [Integer] number of UTXOs imported (0 if nothing new)
|
|
123
|
+
# @raise [UnsupportedActionError] when neither substrate nor chain_data_source is set
|
|
124
|
+
# @raise [WalletError] when a fetched transaction has an out-of-bounds tx_pos
|
|
102
125
|
def sync_utxos
|
|
103
|
-
|
|
126
|
+
return @substrate.sync_utxos if @substrate
|
|
127
|
+
|
|
128
|
+
raise UnsupportedActionError, 'sync_utxos requires a chain_data_source or remote substrate' unless @chain_data_source
|
|
129
|
+
|
|
130
|
+
address = identity_address
|
|
131
|
+
utxos = @chain_data_source.fetch_utxos(address)
|
|
132
|
+
return 0 if utxos.empty?
|
|
133
|
+
|
|
134
|
+
# Group UTXOs by tx_hash to minimise WoC API calls — rate limiting
|
|
135
|
+
# is aggressive and penalties are harsh, so one fetch per transaction
|
|
136
|
+
# is far better than one fetch per UTXO.
|
|
137
|
+
tx_cache = {}
|
|
138
|
+
new_utxos = utxos.uniq { |u| "#{u.tx_hash}.#{u.tx_pos}" }
|
|
139
|
+
.reject { |u| output_exists?("#{u.tx_hash}.#{u.tx_pos}") }
|
|
140
|
+
return 0 if new_utxos.empty?
|
|
141
|
+
|
|
142
|
+
new_utxos.each do |utxo|
|
|
143
|
+
tx = tx_cache[utxo.tx_hash] ||= @chain_data_source.fetch_transaction(utxo.tx_hash)
|
|
144
|
+
|
|
145
|
+
pos = utxo.tx_pos
|
|
146
|
+
unless pos.is_a?(Integer) && pos >= 0 && pos < tx.outputs.length
|
|
147
|
+
raise WalletError, "Invalid tx_pos #{pos.inspect} for #{utxo.tx_hash} (#{tx.outputs.length} outputs)"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
output = tx.outputs[pos]
|
|
151
|
+
output_satoshis = output.satoshis
|
|
152
|
+
|
|
153
|
+
# The transaction output is the authoritative source for satoshis —
|
|
154
|
+
# a mismatch with the UTXO API would produce invalid sighashes.
|
|
155
|
+
if !utxo.satoshis.nil? && utxo.satoshis != output_satoshis
|
|
156
|
+
raise WalletError,
|
|
157
|
+
"UTXO value mismatch for #{utxo.tx_hash}.#{pos}: " \
|
|
158
|
+
"chain reported #{utxo.satoshis}, tx output is #{output_satoshis}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
locking_script_hex = output.locking_script.to_hex
|
|
162
|
+
outpoint = "#{utxo.tx_hash}.#{utxo.tx_pos}"
|
|
163
|
+
|
|
164
|
+
@storage.store_output({
|
|
165
|
+
outpoint: outpoint,
|
|
166
|
+
satoshis: output_satoshis,
|
|
167
|
+
locking_script: locking_script_hex,
|
|
168
|
+
basket: 'default',
|
|
169
|
+
tags: [],
|
|
170
|
+
derivation_type: :identity,
|
|
171
|
+
state: :spendable,
|
|
172
|
+
source_tx_hex: tx.to_hex
|
|
173
|
+
})
|
|
174
|
+
@storage.store_transaction(utxo.tx_hash, tx.to_hex)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
new_utxos.length
|
|
104
178
|
end
|
|
105
179
|
|
|
106
180
|
# --- UTXO Pool & Settings ---
|
data/lib/bsv/wallet/store.rb
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
module BSV
|
|
4
4
|
module Wallet
|
|
5
5
|
# Storage implementations. See {Interface::Store} for the contract.
|
|
6
|
+
#
|
|
7
|
+
# The recommended adapter is {Store::File} (JSON file-backed) or
|
|
8
|
+
# +bsv-wallet-postgres+ for production use.
|
|
9
|
+
#
|
|
10
|
+
# {Store::Memory} is available but will raise when passed to
|
|
11
|
+
# +Client.new+ unless +allow_memory_store: true+ is set — data is
|
|
12
|
+
# lost on process exit, including derived key material needed to
|
|
13
|
+
# spend change outputs.
|
|
6
14
|
module Store
|
|
7
15
|
autoload :Memory, 'bsv/wallet/store/memory'
|
|
8
16
|
autoload :File, 'bsv/wallet/store/file'
|