bsv-sdk 0.12.1 → 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 +4 -4
- data/CHANGELOG.md +49 -0
- data/lib/bsv/auth/certificate.rb +4 -4
- data/lib/bsv/auth/verifiable_certificate.rb +1 -1
- data/lib/bsv/network/arc.rb +95 -224
- data/lib/bsv/network/protocol.rb +321 -0
- data/lib/bsv/network/protocols/arc.rb +351 -0
- data/lib/bsv/network/protocols/chaintracks.rb +39 -0
- data/lib/bsv/network/protocols/ordinals.rb +32 -0
- data/lib/bsv/network/protocols/taal_binary.rb +99 -0
- data/lib/bsv/network/protocols/woc_rest.rb +301 -0
- data/lib/bsv/network/protocols.rb +17 -0
- data/lib/bsv/network/provider.rb +123 -0
- data/lib/bsv/network/providers/gorilla_pool.rb +61 -0
- data/lib/bsv/network/providers/taal.rb +57 -0
- data/lib/bsv/network/providers/whats_on_chain.rb +72 -0
- data/lib/bsv/network/providers.rb +25 -0
- data/lib/bsv/network/result.rb +119 -0
- data/lib/bsv/network/whats_on_chain.rb +78 -40
- data/lib/bsv/network.rb +5 -0
- data/lib/bsv/overlay/admin_token_template.rb +2 -2
- data/lib/bsv/script/push_drop_template.rb +1 -1
- data/lib/bsv/transaction/chain_trackers/chaintracks.rb +45 -49
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +57 -50
- data/lib/bsv/transaction/chain_trackers.rb +3 -4
- data/lib/bsv/transaction/fee_models/live_policy.rb +3 -2
- 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 -5
- metadata +14 -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
|
@@ -1,39 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
4
|
|
|
7
5
|
module BSV
|
|
8
6
|
module Transaction
|
|
9
7
|
module ChainTrackers
|
|
10
8
|
# Chain tracker that verifies merkle roots using the WhatsOnChain API.
|
|
11
9
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
10
|
+
# Delegates all HTTP communication to {BSV::Network::Protocols::WoCREST}.
|
|
11
|
+
# The constructor signature and {ChainTracker} contract are preserved.
|
|
12
|
+
#
|
|
13
|
+
# Note: the WoC API key is sent as a raw +Authorization+ header value
|
|
14
|
+
# (not Bearer-prefixed) to match the existing WoC API convention.
|
|
14
15
|
#
|
|
15
16
|
# @example
|
|
16
17
|
# tracker = BSV::Transaction::ChainTrackers::WhatsOnChain.new
|
|
17
18
|
# tracker.valid_root_for_height?('abcd...', 800_000)
|
|
18
19
|
class WhatsOnChain < ChainTracker
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
testnet:
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
# Returns a WhatsOnChain chain tracker using the provider default.
|
|
21
|
+
#
|
|
22
|
+
# @param testnet [Boolean] when true, uses the testnet endpoint
|
|
23
|
+
# @param opts [Hash] forwarded to the underlying protocol (e.g. +api_key:+, +http_client:+)
|
|
24
|
+
# @return [WhatsOnChain]
|
|
25
|
+
def self.default(testnet: false, **opts)
|
|
26
|
+
provider = BSV::Network::Providers::WhatsOnChain.default(testnet: testnet, **opts)
|
|
27
|
+
new(protocol: provider.protocol_for(:valid_root))
|
|
28
|
+
end
|
|
28
29
|
|
|
29
|
-
# @param network [Symbol] :main, :mainnet, :test, :testnet
|
|
30
|
+
# @param network [Symbol] :main, :mainnet, :test, :testnet (legacy compat)
|
|
30
31
|
# @param api_key [String, nil] optional WoC API key
|
|
31
32
|
# @param http_client [#request, nil] injectable HTTP client for testing
|
|
32
|
-
|
|
33
|
+
# @param protocol [BSV::Network::Protocols::WoCREST, nil] pre-configured protocol
|
|
34
|
+
def initialize(network: :main, api_key: nil, http_client: nil, protocol: nil)
|
|
33
35
|
super()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
if protocol
|
|
37
|
+
@protocol = protocol
|
|
38
|
+
else
|
|
39
|
+
wrapped_client = api_key ? RawAuthClient.new(api_key, http_client) : http_client
|
|
40
|
+
provider = BSV::Network::Providers::WhatsOnChain.default(network: network, http_client: wrapped_client)
|
|
41
|
+
@protocol = provider.protocol_for(:valid_root)
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
# Verify that a merkle root is valid for the given block height.
|
|
@@ -41,51 +47,52 @@ module BSV
|
|
|
41
47
|
# @param root [String] merkle root as a hex string
|
|
42
48
|
# @param height [Integer] block height
|
|
43
49
|
# @return [Boolean]
|
|
50
|
+
# @raise [BSV::Network::ChainProviderError] on network or API error
|
|
44
51
|
def valid_root_for_height?(root, height)
|
|
45
|
-
|
|
46
|
-
return false if
|
|
52
|
+
result = @protocol.call(:valid_root, root, height)
|
|
53
|
+
return false if result.not_found?
|
|
54
|
+
|
|
55
|
+
if result.error?
|
|
56
|
+
raise BSV::Network::ChainProviderError.new(
|
|
57
|
+
result.message.to_s,
|
|
58
|
+
status_code: result.metadata[:status_code]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
47
61
|
|
|
48
|
-
data
|
|
49
|
-
data['merkleroot'].downcase == root.downcase
|
|
62
|
+
result.data == true
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
# Return the current blockchain height.
|
|
53
66
|
#
|
|
54
67
|
# @return [Integer]
|
|
68
|
+
# @raise [BSV::Network::ChainProviderError] on network or API error
|
|
55
69
|
def current_height
|
|
56
|
-
|
|
57
|
-
data
|
|
58
|
-
data['blocks']
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
# @param path [String] API path
|
|
64
|
-
# @param not_found_returns_nil [Boolean] if true, return nil on 404 instead of raising
|
|
65
|
-
# @return [Net::HTTPResponse, nil]
|
|
66
|
-
def get(path, not_found_returns_nil: true)
|
|
67
|
-
uri = URI("#{BASE_URL}#{path}")
|
|
68
|
-
request = Net::HTTP::Get.new(uri)
|
|
69
|
-
request['Authorization'] = @api_key if @api_key
|
|
70
|
-
|
|
71
|
-
response = execute(uri, request)
|
|
72
|
-
code = response.code.to_i
|
|
73
|
-
|
|
74
|
-
return nil if not_found_returns_nil && code == 404
|
|
75
|
-
return response if (200..299).cover?(code)
|
|
70
|
+
result = @protocol.call(:current_height)
|
|
71
|
+
return result.data if result.success?
|
|
76
72
|
|
|
77
73
|
raise BSV::Network::ChainProviderError.new(
|
|
78
|
-
|
|
79
|
-
status_code:
|
|
74
|
+
result.message.to_s,
|
|
75
|
+
status_code: result.metadata[:status_code]
|
|
80
76
|
)
|
|
81
77
|
end
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
# Wraps an injectable HTTP client to set a raw Authorization header value
|
|
80
|
+
# before forwarding the request. This preserves the WoC convention of
|
|
81
|
+
# sending the API key without a Bearer prefix.
|
|
82
|
+
class RawAuthClient
|
|
83
|
+
def initialize(api_key, inner_client)
|
|
84
|
+
@api_key = api_key
|
|
85
|
+
@inner_client = inner_client
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def request(uri, req)
|
|
89
|
+
req['Authorization'] = @api_key
|
|
90
|
+
if @inner_client
|
|
91
|
+
@inner_client.request(uri, req)
|
|
92
|
+
else
|
|
93
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
94
|
+
http.request(req)
|
|
95
|
+
end
|
|
89
96
|
end
|
|
90
97
|
end
|
|
91
98
|
end
|
|
@@ -10,11 +10,10 @@ module BSV
|
|
|
10
10
|
# Return a default chain tracker backed by the Arcade/GorillaPool Chaintracks API.
|
|
11
11
|
#
|
|
12
12
|
# @param testnet [Boolean] use the testnet endpoint when true
|
|
13
|
-
# @param
|
|
13
|
+
# @param opts [Hash] forwarded to the underlying tracker (e.g. +api_key:+)
|
|
14
14
|
# @return [Chaintracks]
|
|
15
|
-
def self.default(testnet: false,
|
|
16
|
-
|
|
17
|
-
Chaintracks.new(url: url, api_key: api_key)
|
|
15
|
+
def self.default(testnet: false, **opts)
|
|
16
|
+
Chaintracks.default(testnet: testnet, **opts)
|
|
18
17
|
end
|
|
19
18
|
end
|
|
20
19
|
end
|
|
@@ -32,7 +32,6 @@ module BSV
|
|
|
32
32
|
# @return [Integer] cache TTL in seconds
|
|
33
33
|
attr_reader :cache_ttl
|
|
34
34
|
|
|
35
|
-
DEFAULT_ARC_URL = BSV::MAINNET_URL
|
|
36
35
|
DEFAULT_FALLBACK_RATE = 100
|
|
37
36
|
|
|
38
37
|
# Returns a LivePolicy with sensible defaults (GorillaPool ARC,
|
|
@@ -41,7 +40,9 @@ module BSV
|
|
|
41
40
|
# @param api_key [String, nil] optional ARC API key
|
|
42
41
|
# @return [LivePolicy]
|
|
43
42
|
def self.default(api_key: nil)
|
|
44
|
-
|
|
43
|
+
provider = BSV::Network::Providers::GorillaPool.mainnet
|
|
44
|
+
arc_protocol = provider.protocol_for(:broadcast)
|
|
45
|
+
new(arc_url: arc_protocol.base_url, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# @param arc_url [String] ARC base URL (e.g. 'https://arcade.gorillapool.io')
|
|
@@ -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
|
@@ -3,19 +3,15 @@
|
|
|
3
3
|
require_relative 'bsv/version'
|
|
4
4
|
|
|
5
5
|
module BSV
|
|
6
|
-
MAINNET_URL = ENV['BSV_ARC_MAINNET_URL'] || 'https://arcade.gorillapool.io'
|
|
7
|
-
TESTNET_URL = ENV['BSV_ARC_TESTNET_URL'] || 'https://testnet.arcade.gorillapool.io'
|
|
8
|
-
|
|
9
6
|
autoload :Primitives, 'bsv/primitives'
|
|
10
7
|
autoload :Script, 'bsv/script'
|
|
11
8
|
autoload :Transaction, 'bsv/transaction'
|
|
12
9
|
autoload :Network, 'bsv/network'
|
|
13
|
-
autoload :Wallet, 'bsv/wallet'
|
|
14
10
|
autoload :Auth, 'bsv/auth'
|
|
15
11
|
autoload :Overlay, 'bsv/overlay'
|
|
16
12
|
autoload :Identity, 'bsv/identity'
|
|
17
13
|
autoload :Registry, 'bsv/registry'
|
|
18
14
|
autoload :MCP, 'bsv/mcp'
|
|
19
|
-
|
|
15
|
+
|
|
20
16
|
autoload :WireFormat, 'bsv/wire_format'
|
|
21
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.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
@@ -67,12 +67,24 @@ 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
|
|
74
73
|
- lib/bsv/network/broadcast_response.rb
|
|
75
74
|
- lib/bsv/network/chain_provider_error.rb
|
|
75
|
+
- lib/bsv/network/protocol.rb
|
|
76
|
+
- lib/bsv/network/protocols.rb
|
|
77
|
+
- lib/bsv/network/protocols/arc.rb
|
|
78
|
+
- lib/bsv/network/protocols/chaintracks.rb
|
|
79
|
+
- lib/bsv/network/protocols/ordinals.rb
|
|
80
|
+
- lib/bsv/network/protocols/taal_binary.rb
|
|
81
|
+
- lib/bsv/network/protocols/woc_rest.rb
|
|
82
|
+
- lib/bsv/network/provider.rb
|
|
83
|
+
- lib/bsv/network/providers.rb
|
|
84
|
+
- lib/bsv/network/providers/gorilla_pool.rb
|
|
85
|
+
- lib/bsv/network/providers/taal.rb
|
|
86
|
+
- lib/bsv/network/providers/whats_on_chain.rb
|
|
87
|
+
- lib/bsv/network/result.rb
|
|
76
88
|
- lib/bsv/network/utxo.rb
|
|
77
89
|
- lib/bsv/network/whats_on_chain.rb
|
|
78
90
|
- lib/bsv/overlay.rb
|
|
@@ -150,9 +162,6 @@ files:
|
|
|
150
162
|
- lib/bsv/transaction/var_int.rb
|
|
151
163
|
- lib/bsv/transaction/verification_error.rb
|
|
152
164
|
- lib/bsv/version.rb
|
|
153
|
-
- lib/bsv/wallet.rb
|
|
154
|
-
- lib/bsv/wallet/insufficient_funds_error.rb
|
|
155
|
-
- lib/bsv/wallet/wallet.rb
|
|
156
165
|
- lib/bsv/wire_format.rb
|
|
157
166
|
homepage: https://github.com/sgbett/bsv-ruby-sdk
|
|
158
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
|
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
|