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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +14 -2
- data/lib/bsv/auth/certificate.rb +1 -1
- data/lib/bsv/network/whats_on_chain.rb +41 -2
- data/lib/bsv/primitives/base58.rb +2 -1
- data/lib/bsv/primitives/curve.rb +37 -12
- data/lib/bsv/primitives/ecdsa.rb +4 -4
- data/lib/bsv/primitives/openssl_ec_shim.rb +32 -5
- data/lib/bsv/primitives/private_key.rb +2 -2
- data/lib/bsv/primitives/public_key.rb +1 -1
- data/lib/bsv/primitives/schnorr.rb +4 -4
- data/lib/bsv/primitives/secp256k1.rb +4 -595
- data/lib/bsv/primitives/signature.rb +2 -0
- data/lib/bsv/primitives/signed_message.rb +6 -5
- data/lib/bsv/secp256k1_native.bundle +0 -0
- data/lib/bsv/transaction/transaction.rb +52 -7
- data/lib/bsv/transaction/verification_error.rb +11 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv-sdk.rb +1 -2
- metadata +16 -5
- data/lib/bsv/messages.rb +0 -16
- data/lib/bsv/wallet/insufficient_funds_error.rb +0 -15
- data/lib/bsv/wallet/wallet.rb +0 -120
- data/lib/bsv/wallet.rb +0 -8
|
@@ -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 [
|
|
557
|
-
# @raise [
|
|
558
|
-
# @raise [VerificationError]
|
|
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
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
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
|
-
|
|
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.
|
|
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
|
data/lib/bsv/wallet/wallet.rb
DELETED
|
@@ -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
|