bsv-sdk 0.13.0 → 0.15.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.
@@ -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.15.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.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.12'
26
+ - !ruby/object:Gem::Dependency
27
+ name: secp256k1-native
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.16'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.16'
26
40
  description: A Ruby library for interacting with the BSV Blockchain — keys, scripts,
27
41
  transactions, and more.
28
42
  executables:
@@ -67,7 +81,6 @@ files:
67
81
  - lib/bsv/mcp/tools/fetch_utxos.rb
68
82
  - lib/bsv/mcp/tools/generate_key.rb
69
83
  - lib/bsv/mcp/tools/helpers.rb
70
- - lib/bsv/messages.rb
71
84
  - lib/bsv/network.rb
72
85
  - lib/bsv/network/arc.rb
73
86
  - lib/bsv/network/broadcast_error.rb
@@ -143,6 +156,7 @@ files:
143
156
  - lib/bsv/script/opcodes.rb
144
157
  - lib/bsv/script/push_drop_template.rb
145
158
  - lib/bsv/script/script.rb
159
+ - lib/bsv/secp256k1_native.bundle
146
160
  - lib/bsv/transaction.rb
147
161
  - lib/bsv/transaction/beef.rb
148
162
  - lib/bsv/transaction/chain_tracker.rb
@@ -163,9 +177,6 @@ files:
163
177
  - lib/bsv/transaction/var_int.rb
164
178
  - lib/bsv/transaction/verification_error.rb
165
179
  - lib/bsv/version.rb
166
- - lib/bsv/wallet.rb
167
- - lib/bsv/wallet/insufficient_funds_error.rb
168
- - lib/bsv/wallet/wallet.rb
169
180
  - lib/bsv/wire_format.rb
170
181
  homepage: https://github.com/sgbett/bsv-ruby-sdk
171
182
  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