bsv-sdk 0.13.0 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1ead96687ff2a6118fde4ab72a7dc0b11c7d82db8b4b883910120bcc0cee0dc
4
- data.tar.gz: 5a089f6b80111ba4abf5472bfbd8067a028b35a73f90e504e2541376d0d00d55
3
+ metadata.gz: 4532a163439721455da80290d8b02342e597a01bb1e00b259d13343f8f9bd592
4
+ data.tar.gz: b7cfda4a8084b96c28f34afb6829fcba9f5be0dabb01fca536147bfe2c66bad8
5
5
  SHA512:
6
- metadata.gz: d05f8b3b8a584706323c631fb9ff005420c6d5cdc5081f76fbf6a73b15c12323b87d30f469e5fa186bfa30b51905ffe4bc0785b57bd77d72286ee3f8ce03dc1f
7
- data.tar.gz: b892a8add5e5faaad6bc837ab26de879419cc457d909be4f5b1fb812fd4a69fb5356529689081dffe1195caffdb2e259da6a073ea1ce6ff2f0d08068c73a67f1
6
+ metadata.gz: ed0b97db3cec8503eba509f232a463d8566373b34ccaf5fe7cd130a72e712cde17b083b4ce94ef64fa84786d6e6aec8965423ca6b8a7471ada70ebd522956e2a
7
+ data.tar.gz: 0747a93bef059a8eccf0f504e703d20ddc875f8c562db8f01d3af4050bf7562deb262eb4494db4d98b78665f91274ed77754fa76e99254b8592b7e0f1526aa13
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to the `bsv-sdk` 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.14.0 — 2026-04-22
9
+
10
+ ### Added
11
+ - `BSV::Network::WhatsOnChain` expanded with `current_height`,
12
+ `get_block_header(height)`, and `valid_root_for_height?(root, height)` —
13
+ a single WhatsOnChain instance now serves as a complete chain data source
14
+ (#596)
15
+ - BEEF-based SPV verification conformance tests (#607)
16
+
17
+ ### Changed
18
+ - `Transaction#verify` now raises `VerificationError` for all failure modes:
19
+ `:script_failure` (wraps `ScriptError` with cause chaining),
20
+ `:missing_source` (replaces `ArgumentError`), `:invalid_merkle_proof`,
21
+ `:insufficient_fee`, and `:output_overflow`. Code rescuing `ArgumentError`
22
+ or `ScriptError` from `verify` must switch to `VerificationError` (#608)
23
+ - Removed dead code: `BSV::Wallet::Wallet` (superseded by bsv-wallet gem's
24
+ `Client`), `BSV::Messages` (unused re-export alias), and duplicate
25
+ `BSV::Wallet::InsufficientFundsError` (#594)
26
+
27
+ ### Fixed
28
+ - WhatsOnChain `valid_root_for_height?` YARD doc now correctly documents
29
+ that 404 returns `false` rather than raising
30
+
8
31
  ## 0.13.0 — 2026-04-21
9
32
 
10
33
  ### Added
@@ -176,7 +176,7 @@ module BSV
176
176
  def verify(verifier_wallet = nil)
177
177
  raise ArgumentError, 'certificate has no signature to verify' if @signature.nil? || @signature.empty?
178
178
 
179
- verifier_wallet ||= BSV::Wallet::Client.new('anyone', storage: BSV::Wallet::Store::Memory.new)
179
+ verifier_wallet ||= BSV::Wallet::Client.new('anyone', storage: BSV::Wallet::Store::Memory.new, allow_memory_store: true)
180
180
  preimage = to_binary(include_signature: false)
181
181
  sig_bytes = [@signature].pack('H*').unpack('C*')
182
182
 
@@ -5,8 +5,11 @@ module BSV
5
5
  # WhatsOnChain chain data provider for reading transactions and UTXOs
6
6
  # from the BSV network.
7
7
  #
8
- # Any object responding to #fetch_utxos(address) and
9
- # #fetch_transaction(txid) can serve as a chain data provider;
8
+ # Any object responding to #fetch_utxos(address),
9
+ # #fetch_transaction(txid), #current_height,
10
+ # #get_block_header(height), and optionally
11
+ # #valid_root_for_height?(root_hex, height) can serve as a chain
12
+ # data source;
10
13
  # this class implements that contract by delegating to
11
14
  # Protocols::WoCREST.
12
15
  #
@@ -62,6 +65,42 @@ module BSV
62
65
  BSV::Transaction::Transaction.from_hex(result.data)
63
66
  end
64
67
 
68
+ # Return the current blockchain height.
69
+ # @return [Integer]
70
+ # @raise [BSV::Network::ChainProviderError] on network or API error
71
+ def current_height
72
+ result = @protocol.call(:current_height)
73
+ raise_on_error(result)
74
+
75
+ result.data
76
+ end
77
+
78
+ # Fetch the block header for a given height.
79
+ # @param height [Integer] block height
80
+ # @return [Hash] parsed block header JSON
81
+ # @raise [BSV::Network::ChainProviderError] on network or API error
82
+ def get_block_header(height)
83
+ result = @protocol.call(:get_block_header, height)
84
+ raise_on_error(result)
85
+
86
+ result.data
87
+ end
88
+
89
+ # Verify that a merkle root is valid for the given block height.
90
+ # Returns +false+ when the block is not found (404); raises on other errors.
91
+ # @param root [String] expected merkle root as hex
92
+ # @param height [Integer] block height
93
+ # @return [Boolean]
94
+ # @raise [BSV::Network::ChainProviderError] on network or non-404 API error
95
+ def valid_root_for_height?(root, height)
96
+ result = @protocol.call(:valid_root, root, height)
97
+ return false if result.not_found?
98
+
99
+ raise_on_error(result)
100
+
101
+ result.data == true
102
+ end
103
+
65
104
  private
66
105
 
67
106
  # Translates a non-success Protocol result into a raised ChainProviderError.
@@ -550,12 +550,30 @@ module BSV
550
550
  # provided fee model.
551
551
  # 4. Checks that total outputs do not exceed total inputs.
552
552
  #
553
+ # == Semantic Divergences from Reference SDKs
554
+ #
555
+ # This implementation raises +VerificationError+ for all failure modes.
556
+ # The TypeScript and Python SDKs return +false+ for script failures and
557
+ # output overflow. The Go SDK propagates errors (not booleans) for script
558
+ # failures, aligning with the Ruby approach.
559
+ #
560
+ # Rationale: raising provides structured error information (+#code+,
561
+ # +#message+, +#cause+) that a boolean cannot convey. Consumers can
562
+ # rescue +VerificationError+ and inspect +#code+ for specifics.
563
+ #
564
+ # Divergence summary:
565
+ # - +:output_overflow+ — Ruby raises; TS/Python return +false+; Go omits the check
566
+ # - +:script_failure+ — Ruby raises; TS/Python return +false+; Go also propagates errors
567
+ # - +:missing_source+ — Ruby raises; consistent with TS/Go/Python (all raise/error)
568
+ #
553
569
  # @param chain_tracker [ChainTracker] chain tracker for merkle root validation
554
570
  # @param fee_model [FeeModel, nil] optional fee model to validate the root transaction's fee
555
571
  # @return [true] on successful verification
556
- # @raise [ArgumentError] if a source transaction or unlocking script is missing
557
- # @raise [BSV::Script::ScriptError] if script execution fails
558
- # @raise [VerificationError] for merkle path failures, fee validation, or output overflow
572
+ # @raise [VerificationError] with code +:invalid_merkle_proof+ if a merkle proof is invalid
573
+ # @raise [VerificationError] with code +:insufficient_fee+ if the fee is below the model's threshold
574
+ # @raise [VerificationError] with code +:output_overflow+ if outputs exceed inputs
575
+ # @raise [VerificationError] with code +:script_failure+ if script execution fails
576
+ # @raise [VerificationError] with code +:missing_source+ if an input is missing required source data
559
577
  def verify(chain_tracker:, fee_model: nil)
560
578
  verified = {}
561
579
  queue = [self]
@@ -581,8 +599,26 @@ module BSV
581
599
 
582
600
  # Verify each input
583
601
  tx.inputs.each_with_index do |input, index|
602
+ # Populate source data from source_transaction when not already set.
603
+ # Matches the TS SDK pattern: sourceOutput is read directly from
604
+ # source_transaction.outputs[sourceOutputIndex] during verify.
605
+ if input.source_transaction
606
+ source_output = input.source_transaction.outputs[input.prev_tx_out_index]
607
+ if source_output
608
+ input.source_locking_script ||= source_output.locking_script
609
+ input.source_satoshis ||= source_output.satoshis
610
+ end
611
+ end
612
+
584
613
  verify_input_requirements(tx, input, index)
585
- tx.verify_input(index)
614
+ begin
615
+ tx.verify_input(index)
616
+ rescue BSV::Script::ScriptError => e
617
+ raise VerificationError.new(
618
+ :script_failure,
619
+ "script verification failed for input #{index} of transaction #{tx.txid_hex}: #{e.message}"
620
+ )
621
+ end
586
622
 
587
623
  # Enqueue source transaction for verification if not yet verified
588
624
  source_tx = input.source_transaction
@@ -742,9 +778,18 @@ module BSV
742
778
 
743
779
  def verify_input_requirements(tx, input, index)
744
780
  tx_id = tx.txid_hex
745
- raise ArgumentError, "input #{index} of transaction #{tx_id} has no unlocking script" if input.unlocking_script.nil?
746
- raise ArgumentError, "input #{index} of transaction #{tx_id} has no source locking script" if input.source_locking_script.nil?
747
- raise ArgumentError, "input #{index} of transaction #{tx_id} has no source satoshis" if input.source_satoshis.nil?
781
+ if input.unlocking_script.nil?
782
+ raise VerificationError.new(:missing_source,
783
+ "input #{index} of transaction #{tx_id} has no unlocking script")
784
+ end
785
+ if input.source_locking_script.nil?
786
+ raise VerificationError.new(:missing_source,
787
+ "input #{index} of transaction #{tx_id} has no source locking script")
788
+ end
789
+ return unless input.source_satoshis.nil?
790
+
791
+ raise VerificationError.new(:missing_source,
792
+ "input #{index} of transaction #{tx_id} has no source satoshis")
748
793
  end
749
794
 
750
795
  def verify_fee(fee_model)
@@ -7,6 +7,15 @@ module BSV
7
7
  # Carries a machine-readable code alongside a human-readable message,
8
8
  # matching the typed error pattern used by the Go SDK
9
9
  # (ErrInvalidMerklePath, ErrFeeTooLow, ErrScriptVerificationFailed).
10
+ #
11
+ # == Error Codes
12
+ #
13
+ # - +:invalid_merkle_proof+ — merkle path verification failed against the chain tracker
14
+ # - +:insufficient_fee+ — transaction fee is below the fee model's requirement
15
+ # - +:output_overflow+ — total outputs exceed total inputs
16
+ # - +:script_failure+ — script interpreter rejected an input (original +ScriptError+ in +#cause+)
17
+ # - +:missing_source+ — required input data (unlocking script, source locking script,
18
+ # or source satoshis) is absent
10
19
  class VerificationError < StandardError
11
20
  # @return [Symbol] the error code
12
21
  attr_reader :code
@@ -14,6 +23,8 @@ module BSV
14
23
  INVALID_MERKLE_PROOF = :invalid_merkle_proof
15
24
  INSUFFICIENT_FEE = :insufficient_fee
16
25
  OUTPUT_OVERFLOW = :output_overflow
26
+ SCRIPT_FAILURE = :script_failure
27
+ MISSING_SOURCE = :missing_source
17
28
 
18
29
  # @param code [Symbol] error code
19
30
  # @param message [String] human-readable description
data/lib/bsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0'
5
5
  end
data/lib/bsv-sdk.rb CHANGED
@@ -7,12 +7,11 @@ module BSV
7
7
  autoload :Script, 'bsv/script'
8
8
  autoload :Transaction, 'bsv/transaction'
9
9
  autoload :Network, 'bsv/network'
10
- require_relative 'bsv/wallet' # eager — BSV::Wallet may be pre-defined by bsv-wallet gemspec
11
10
  autoload :Auth, 'bsv/auth'
12
11
  autoload :Overlay, 'bsv/overlay'
13
12
  autoload :Identity, 'bsv/identity'
14
13
  autoload :Registry, 'bsv/registry'
15
14
  autoload :MCP, 'bsv/mcp'
16
- autoload :Messages, 'bsv/messages'
15
+
17
16
  autoload :WireFormat, 'bsv/wire_format'
18
17
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsv-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
@@ -67,7 +67,6 @@ files:
67
67
  - lib/bsv/mcp/tools/fetch_utxos.rb
68
68
  - lib/bsv/mcp/tools/generate_key.rb
69
69
  - lib/bsv/mcp/tools/helpers.rb
70
- - lib/bsv/messages.rb
71
70
  - lib/bsv/network.rb
72
71
  - lib/bsv/network/arc.rb
73
72
  - lib/bsv/network/broadcast_error.rb
@@ -163,9 +162,6 @@ files:
163
162
  - lib/bsv/transaction/var_int.rb
164
163
  - lib/bsv/transaction/verification_error.rb
165
164
  - lib/bsv/version.rb
166
- - lib/bsv/wallet.rb
167
- - lib/bsv/wallet/insufficient_funds_error.rb
168
- - lib/bsv/wallet/wallet.rb
169
165
  - lib/bsv/wire_format.rb
170
166
  homepage: https://github.com/sgbett/bsv-ruby-sdk
171
167
  licenses:
data/lib/bsv/messages.rb DELETED
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BSV
4
- # Namespace providing TS SDK naming parity for messaging primitives.
5
- #
6
- # Re-exports {BSV::Primitives::SignedMessage} and {BSV::Primitives::EncryptedMessage}
7
- # under the +BSV::Messages+ namespace, matching the structure of the TypeScript SDK
8
- # (ts-sdk/src/messages/index.ts).
9
- #
10
- # The canonical implementations remain in +BSV::Primitives+; this module is a
11
- # lightweight re-export only.
12
- module Messages
13
- SignedMessage = BSV::Primitives::SignedMessage
14
- EncryptedMessage = BSV::Primitives::EncryptedMessage
15
- end
16
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BSV
4
- module Wallet
5
- class InsufficientFundsError < StandardError
6
- attr_reader :required, :available
7
-
8
- def initialize(message = nil, required: nil, available: nil)
9
- @required = required
10
- @available = available
11
- super(message || "insufficient funds: need #{required}, have #{available}")
12
- end
13
- end
14
- end
15
- end
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BSV
4
- module Wallet
5
- # Holds a private key, sources UTXOs via a chain data provider, and
6
- # funds and signs P2PKH transactions.
7
- #
8
- # The provider is duck-typed — any object responding to
9
- # #fetch_utxos(address) and #fetch_transaction(txid) qualifies.
10
- class Wallet
11
- DUST_THRESHOLD = 1
12
-
13
- attr_reader :private_key, :provider
14
-
15
- def initialize(private_key:, provider:)
16
- @private_key = private_key
17
- @provider = provider
18
- end
19
-
20
- def address(network: :mainnet)
21
- @private_key.public_key.address(network: network)
22
- end
23
-
24
- def balance(network: :mainnet)
25
- @provider.fetch_utxos(address(network: network)).sum(&:satoshis)
26
- end
27
-
28
- def fund(tx, network: :mainnet, satoshis_per_byte: 0.1)
29
- utxos = @provider.fetch_utxos(address(network: network))
30
- output_total = tx.total_output_satoshis
31
-
32
- # Add a dummy change output so fee estimation accounts for its size
33
- dummy_change = BSV::Transaction::TransactionOutput.new(
34
- satoshis: 0, locking_script: locking_script
35
- )
36
- tx.add_output(dummy_change)
37
-
38
- input_total = tx.total_input_satoshis
39
- funded = false
40
-
41
- utxos.each do |utxo|
42
- tx.add_input(build_input(utxo))
43
- input_total += utxo.satoshis
44
-
45
- fee = tx.estimated_fee(satoshis_per_byte: satoshis_per_byte)
46
- if input_total >= output_total + fee
47
- funded = true
48
- break
49
- end
50
- end
51
-
52
- # Remove the dummy change output
53
- tx.outputs.delete(dummy_change)
54
-
55
- unless funded
56
- fee = tx.estimated_fee(satoshis_per_byte: satoshis_per_byte)
57
- raise InsufficientFundsError.new(required: output_total + fee, available: input_total)
58
- end
59
-
60
- add_change_if_needed(tx, input_total, output_total, satoshis_per_byte)
61
-
62
- tx
63
- end
64
-
65
- def sign(tx)
66
- tx.sign_all(@private_key)
67
- end
68
-
69
- def fund_and_sign(tx, network: :mainnet, satoshis_per_byte: 0.1)
70
- fund(tx, network: network, satoshis_per_byte: satoshis_per_byte)
71
- sign(tx)
72
- end
73
-
74
- private
75
-
76
- def locking_script
77
- @locking_script ||= BSV::Script::Script.p2pkh_lock(@private_key.public_key.hash160)
78
- end
79
-
80
- def build_input(utxo)
81
- input = BSV::Transaction::TransactionInput.new(
82
- prev_tx_id: BSV::Transaction::TransactionInput.txid_from_hex(utxo.tx_hash),
83
- prev_tx_out_index: utxo.tx_pos
84
- )
85
- input.source_satoshis = utxo.satoshis
86
- input.source_locking_script = locking_script
87
- input.unlocking_script_template = BSV::Transaction::P2PKH.new(@private_key)
88
- input
89
- end
90
-
91
- def add_change_if_needed(tx, input_total, output_total, satoshis_per_byte)
92
- fee_without_change = tx.estimated_fee(satoshis_per_byte: satoshis_per_byte)
93
- remainder = input_total - output_total - fee_without_change
94
-
95
- return if remainder < DUST_THRESHOLD
96
-
97
- change_output = BSV::Transaction::TransactionOutput.new(
98
- satoshis: remainder, locking_script: locking_script
99
- )
100
- tx.add_output(change_output)
101
-
102
- # Recalculate: adding the change output increases fee slightly
103
- new_fee = tx.estimated_fee(satoshis_per_byte: satoshis_per_byte)
104
- fee_increase = new_fee - fee_without_change
105
- final_change = remainder - fee_increase
106
-
107
- if final_change >= DUST_THRESHOLD
108
- tx.outputs.delete(change_output)
109
- tx.add_output(
110
- BSV::Transaction::TransactionOutput.new(
111
- satoshis: final_change, locking_script: locking_script
112
- )
113
- )
114
- else
115
- tx.outputs.delete(change_output) # change absorbed by fee
116
- end
117
- end
118
- end
119
- end
120
- end
data/lib/bsv/wallet.rb DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BSV
4
- module Wallet
5
- autoload :InsufficientFundsError, 'bsv/wallet/insufficient_funds_error'
6
- autoload :Wallet, 'bsv/wallet/wallet'
7
- end
8
- end