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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/lib/bsv/auth/certificate.rb +4 -4
  4. data/lib/bsv/auth/verifiable_certificate.rb +1 -1
  5. data/lib/bsv/network/arc.rb +95 -224
  6. data/lib/bsv/network/protocol.rb +321 -0
  7. data/lib/bsv/network/protocols/arc.rb +351 -0
  8. data/lib/bsv/network/protocols/chaintracks.rb +39 -0
  9. data/lib/bsv/network/protocols/ordinals.rb +32 -0
  10. data/lib/bsv/network/protocols/taal_binary.rb +99 -0
  11. data/lib/bsv/network/protocols/woc_rest.rb +301 -0
  12. data/lib/bsv/network/protocols.rb +17 -0
  13. data/lib/bsv/network/provider.rb +123 -0
  14. data/lib/bsv/network/providers/gorilla_pool.rb +61 -0
  15. data/lib/bsv/network/providers/taal.rb +57 -0
  16. data/lib/bsv/network/providers/whats_on_chain.rb +72 -0
  17. data/lib/bsv/network/providers.rb +25 -0
  18. data/lib/bsv/network/result.rb +119 -0
  19. data/lib/bsv/network/whats_on_chain.rb +78 -40
  20. data/lib/bsv/network.rb +5 -0
  21. data/lib/bsv/overlay/admin_token_template.rb +2 -2
  22. data/lib/bsv/script/push_drop_template.rb +1 -1
  23. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +45 -49
  24. data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +57 -50
  25. data/lib/bsv/transaction/chain_trackers.rb +3 -4
  26. data/lib/bsv/transaction/fee_models/live_policy.rb +3 -2
  27. data/lib/bsv/transaction/transaction.rb +52 -7
  28. data/lib/bsv/transaction/verification_error.rb +11 -0
  29. data/lib/bsv/version.rb +1 -1
  30. data/lib/bsv-sdk.rb +1 -5
  31. metadata +14 -5
  32. data/lib/bsv/messages.rb +0 -16
  33. data/lib/bsv/wallet/insufficient_funds_error.rb +0 -15
  34. data/lib/bsv/wallet/wallet.rb +0 -120
  35. 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
- # Queries the WoC block header endpoint to retrieve the merkle root for a
13
- # given block height and compares it with the provided root.
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
- BASE_URL = 'https://api.whatsonchain.com'
20
-
21
- NETWORKS = {
22
- main: 'main',
23
- mainnet: 'main',
24
- test: 'test',
25
- testnet: 'test',
26
- stn: 'stn'
27
- }.freeze
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, or :stn
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
- def initialize(network: :main, api_key: nil, http_client: nil)
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
- @network = NETWORKS.fetch(network) { raise ArgumentError, "unknown network: #{network}" }
35
- @api_key = api_key
36
- @http_client = http_client
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
- response = get("/v1/bsv/#{@network}/block/#{height}/header")
46
- return false if response.nil?
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 = JSON.parse(response.body)
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
- response = get("/v1/bsv/#{@network}/chain/info", not_found_returns_nil: false)
57
- data = JSON.parse(response.body)
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
- response.body || "HTTP #{code}",
79
- status_code: code
74
+ result.message.to_s,
75
+ status_code: result.metadata[:status_code]
80
76
  )
81
77
  end
82
78
 
83
- def execute(uri, request)
84
- if @http_client
85
- @http_client.request(uri, request)
86
- else
87
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
88
- http.request(request)
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 api_key [String, nil] optional Bearer API key
13
+ # @param opts [Hash] forwarded to the underlying tracker (e.g. +api_key:+)
14
14
  # @return [Chaintracks]
15
- def self.default(testnet: false, api_key: nil)
16
- url = testnet ? Chaintracks::TESTNET_URL : Chaintracks::MAINNET_URL
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
- new(arc_url: DEFAULT_ARC_URL, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
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 [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.12.1'
4
+ VERSION = '0.14.0'
5
5
  end
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
- autoload :Messages, 'bsv/messages'
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.12.1
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
@@ -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