bsv-sdk 0.2.0 → 0.3.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 +16 -0
- data/lib/bsv/network/broadcast_response.rb +1 -2
- data/lib/bsv/primitives/bsm.rb +2 -6
- data/lib/bsv/primitives/curve.rb +1 -2
- data/lib/bsv/primitives/encrypted_message.rb +100 -0
- data/lib/bsv/primitives/extended_key.rb +1 -2
- data/lib/bsv/primitives/key_shares.rb +83 -0
- data/lib/bsv/primitives/mnemonic.rb +1 -3
- data/lib/bsv/primitives/point_in_finite_field.rb +72 -0
- data/lib/bsv/primitives/polynomial.rb +95 -0
- data/lib/bsv/primitives/private_key.rb +101 -5
- data/lib/bsv/primitives/signed_message.rb +104 -0
- data/lib/bsv/primitives/symmetric_key.rb +128 -0
- data/lib/bsv/primitives.rb +18 -12
- data/lib/bsv/script/interpreter/interpreter.rb +1 -3
- data/lib/bsv/script/interpreter/operations/bitwise.rb +1 -3
- data/lib/bsv/script/interpreter/operations/crypto.rb +3 -9
- data/lib/bsv/script/interpreter/operations/flow_control.rb +2 -6
- data/lib/bsv/script/interpreter/operations/splice.rb +1 -3
- data/lib/bsv/script/interpreter/script_number.rb +2 -7
- data/lib/bsv/script/script.rb +256 -1
- data/lib/bsv/transaction/beef.rb +8 -11
- data/lib/bsv/transaction/transaction.rb +131 -59
- data/lib/bsv/transaction/transaction_input.rb +1 -2
- data/lib/bsv/transaction/transaction_output.rb +1 -2
- data/lib/bsv/transaction/var_int.rb +4 -16
- data/lib/bsv/transaction.rb +14 -14
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet_interface/errors/invalid_hmac_error.rb +11 -0
- data/lib/bsv/wallet_interface/errors/invalid_parameter_error.rb +14 -0
- data/lib/bsv/wallet_interface/errors/invalid_signature_error.rb +11 -0
- data/lib/bsv/wallet_interface/errors/unsupported_action_error.rb +11 -0
- data/lib/bsv/wallet_interface/errors/wallet_error.rb +14 -0
- data/lib/bsv/wallet_interface/interface.rb +384 -0
- data/lib/bsv/wallet_interface/key_deriver.rb +142 -0
- data/lib/bsv/wallet_interface/memory_store.rb +115 -0
- data/lib/bsv/wallet_interface/proto_wallet.rb +361 -0
- data/lib/bsv/wallet_interface/storage_adapter.rb +51 -0
- data/lib/bsv/wallet_interface/validators.rb +126 -0
- data/lib/bsv/wallet_interface/version.rb +7 -0
- data/lib/bsv/wallet_interface/wallet_client.rb +486 -0
- data/lib/bsv/wallet_interface.rb +25 -0
- data/lib/bsv-wallet.rb +4 -0
- metadata +24 -3
- /data/{LICENCE → LICENSE} +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module BSV
|
|
7
|
+
module Wallet
|
|
8
|
+
# BRC-100 transaction operations for the wallet interface.
|
|
9
|
+
#
|
|
10
|
+
# Implements the 7 transaction-related BRC-100 methods on top of
|
|
11
|
+
# {ProtoWallet}: create_action, sign_action, abort_action, list_actions,
|
|
12
|
+
# list_outputs, relinquish_output, and internalize_action.
|
|
13
|
+
#
|
|
14
|
+
# Transactions are built using the SDK's {BSV::Transaction::Transaction}
|
|
15
|
+
# class. Completed actions and tracked outputs are persisted via a
|
|
16
|
+
# {StorageAdapter} (defaults to {MemoryStore}).
|
|
17
|
+
#
|
|
18
|
+
# @example Create a simple transaction
|
|
19
|
+
# client = BSV::Wallet::WalletClient.new(private_key)
|
|
20
|
+
# result = client.create_action({
|
|
21
|
+
# description: 'Pay invoice',
|
|
22
|
+
# outputs: [{ locking_script: '76a914...88ac', satoshis: 1000,
|
|
23
|
+
# output_description: 'Payment' }]
|
|
24
|
+
# })
|
|
25
|
+
class WalletClient < ProtoWallet
|
|
26
|
+
# @return [StorageAdapter] the underlying persistence adapter
|
|
27
|
+
attr_reader :storage
|
|
28
|
+
|
|
29
|
+
# @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
|
|
30
|
+
# @param storage [StorageAdapter] persistence adapter (default: MemoryStore)
|
|
31
|
+
def initialize(key, storage: MemoryStore.new)
|
|
32
|
+
super(key)
|
|
33
|
+
@storage = storage
|
|
34
|
+
@pending = {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# --- Transaction Operations ---
|
|
38
|
+
|
|
39
|
+
# Creates a new Bitcoin transaction.
|
|
40
|
+
#
|
|
41
|
+
# If all inputs carry unlocking_script values, the transaction is
|
|
42
|
+
# finalised immediately and returned with :txid and :tx (BEEF bytes).
|
|
43
|
+
# If any input specifies only unlocking_script_length, the transaction
|
|
44
|
+
# is held pending and returned as a signable_transaction for external
|
|
45
|
+
# signing via {#sign_action}.
|
|
46
|
+
#
|
|
47
|
+
# @param args [Hash] transaction parameters
|
|
48
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
49
|
+
# @return [Hash] finalised result or signable_transaction
|
|
50
|
+
def create_action(args, _originator: nil)
|
|
51
|
+
validate_create_action!(args)
|
|
52
|
+
beef = parse_input_beef(args[:input_beef])
|
|
53
|
+
tx = build_transaction(args, beef)
|
|
54
|
+
|
|
55
|
+
if needs_signing?(args[:inputs])
|
|
56
|
+
create_signable(tx, args, beef)
|
|
57
|
+
else
|
|
58
|
+
finalize_action(tx, args)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Signs a previously created signable transaction.
|
|
63
|
+
#
|
|
64
|
+
# @param args [Hash]
|
|
65
|
+
# @option args [Hash] :spends map of input index (Integer or String) to
|
|
66
|
+
# { unlocking_script: hex, sequence_number: Integer }
|
|
67
|
+
# @option args [String] :reference base64 reference from create_action
|
|
68
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
69
|
+
# @return [Hash] with :txid and :tx (BEEF bytes)
|
|
70
|
+
def sign_action(args, _originator: nil)
|
|
71
|
+
reference = args[:reference]
|
|
72
|
+
pending = @pending[reference]
|
|
73
|
+
raise WalletError, 'Transaction not found for the given reference' unless pending
|
|
74
|
+
|
|
75
|
+
tx = pending[:tx]
|
|
76
|
+
apply_spends(tx, args[:spends])
|
|
77
|
+
@pending.delete(reference)
|
|
78
|
+
finalize_action(tx, pending[:args])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Aborts a pending signable transaction.
|
|
82
|
+
#
|
|
83
|
+
# @param args [Hash]
|
|
84
|
+
# @option args [String] :reference base64 reference to abort
|
|
85
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
86
|
+
# @return [Hash] { aborted: true }
|
|
87
|
+
def abort_action(args, _originator: nil)
|
|
88
|
+
reference = args[:reference]
|
|
89
|
+
raise WalletError, 'Transaction not found for the given reference' unless @pending.key?(reference)
|
|
90
|
+
|
|
91
|
+
@pending.delete(reference)
|
|
92
|
+
{ aborted: true }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Lists stored actions matching the given labels.
|
|
96
|
+
#
|
|
97
|
+
# @param args [Hash]
|
|
98
|
+
# @option args [Array<String>] :labels (required) labels to filter by
|
|
99
|
+
# @option args [String] :label_query_mode 'any' (default) or 'all'
|
|
100
|
+
# @option args [Integer] :limit max results (default 10)
|
|
101
|
+
# @option args [Integer] :offset results to skip (default 0)
|
|
102
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
103
|
+
# @return [Hash] { total_actions: Integer, actions: Array }
|
|
104
|
+
def list_actions(args, _originator: nil)
|
|
105
|
+
validate_list_actions!(args)
|
|
106
|
+
query = build_action_query(args)
|
|
107
|
+
total = @storage.count_actions(query)
|
|
108
|
+
actions = @storage.find_actions(query)
|
|
109
|
+
{ total_actions: total, actions: actions }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Lists spendable outputs in a basket.
|
|
113
|
+
#
|
|
114
|
+
# @param args [Hash]
|
|
115
|
+
# @option args [String] :basket (required) basket name
|
|
116
|
+
# @option args [Array<String>] :tags optional tag filter
|
|
117
|
+
# @option args [String] :tag_query_mode 'any' (default) or 'all'
|
|
118
|
+
# @option args [Integer] :limit max results (default 10)
|
|
119
|
+
# @option args [Integer] :offset results to skip (default 0)
|
|
120
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
121
|
+
# @return [Hash] { total_outputs: Integer, outputs: Array }
|
|
122
|
+
def list_outputs(args, _originator: nil)
|
|
123
|
+
validate_list_outputs!(args)
|
|
124
|
+
query = build_output_query(args)
|
|
125
|
+
total = @storage.count_outputs(query)
|
|
126
|
+
outputs = @storage.find_outputs(query)
|
|
127
|
+
{ total_outputs: total, outputs: outputs }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Removes an output from basket tracking.
|
|
131
|
+
#
|
|
132
|
+
# @param args [Hash]
|
|
133
|
+
# @option args [String] :basket basket name
|
|
134
|
+
# @option args [String] :output outpoint string
|
|
135
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
136
|
+
# @return [Hash] { relinquished: true }
|
|
137
|
+
def relinquish_output(args, _originator: nil)
|
|
138
|
+
Validators.validate_basket!(args[:basket])
|
|
139
|
+
Validators.validate_outpoint!(args[:output])
|
|
140
|
+
raise WalletError, 'Output not found' unless @storage.delete_output(args[:output])
|
|
141
|
+
|
|
142
|
+
{ relinquished: true }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Accepts an incoming transaction for wallet internalization.
|
|
146
|
+
#
|
|
147
|
+
# Parses the BEEF, locates the subject transaction, processes each
|
|
148
|
+
# specified output according to its protocol (wallet payment or basket
|
|
149
|
+
# insertion), and stores the action.
|
|
150
|
+
#
|
|
151
|
+
# @param args [Hash]
|
|
152
|
+
# @option args [Array<Integer>] :tx Atomic BEEF-formatted transaction as byte array
|
|
153
|
+
# @option args [Array<Hash>] :outputs output metadata
|
|
154
|
+
# @option args [String] :description 5-50 char description
|
|
155
|
+
# @option args [Array<String>] :labels optional labels
|
|
156
|
+
# @param _originator [String, nil] FQDN of the originating application
|
|
157
|
+
# @return [Hash] { accepted: true }
|
|
158
|
+
def internalize_action(args, _originator: nil)
|
|
159
|
+
validate_internalize_action!(args)
|
|
160
|
+
beef_binary = args[:tx].pack('C*')
|
|
161
|
+
beef = BSV::Transaction::Beef.from_binary(beef_binary)
|
|
162
|
+
tx = extract_subject_transaction(beef)
|
|
163
|
+
|
|
164
|
+
process_internalize_outputs(tx, args[:outputs])
|
|
165
|
+
store_action(tx, args, status: 'completed')
|
|
166
|
+
{ accepted: true }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# --- Validation ---
|
|
172
|
+
|
|
173
|
+
def validate_create_action!(args)
|
|
174
|
+
Validators.validate_description!(args[:description])
|
|
175
|
+
inputs_present = args[:inputs] && !args[:inputs].empty?
|
|
176
|
+
outputs_present = args[:outputs] && !args[:outputs].empty?
|
|
177
|
+
raise InvalidParameterError.new('inputs/outputs', 'at least one input or output') unless inputs_present || outputs_present
|
|
178
|
+
|
|
179
|
+
validate_action_inputs!(args[:inputs]) if args[:inputs]
|
|
180
|
+
validate_action_outputs!(args[:outputs]) if args[:outputs]
|
|
181
|
+
args[:labels]&.each { |l| Validators.validate_label!(l) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def validate_action_inputs!(inputs)
|
|
185
|
+
inputs.each do |input|
|
|
186
|
+
Validators.validate_outpoint!(input[:outpoint])
|
|
187
|
+
Validators.validate_description!(input[:input_description], 'input_description')
|
|
188
|
+
unless input[:unlocking_script] || input[:unlocking_script_length]
|
|
189
|
+
raise InvalidParameterError.new('unlocking_script',
|
|
190
|
+
'provided, or unlocking_script_length must be set')
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def validate_action_outputs!(outputs)
|
|
196
|
+
outputs.each do |output|
|
|
197
|
+
Validators.validate_hex_string!(output[:locking_script], 'locking_script')
|
|
198
|
+
Validators.validate_satoshis!(output[:satoshis])
|
|
199
|
+
Validators.validate_description!(output[:output_description], 'output_description')
|
|
200
|
+
Validators.validate_basket!(output[:basket]) if output[:basket]
|
|
201
|
+
output[:tags]&.each { |t| Validators.validate_tag!(t) }
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def validate_list_actions!(args)
|
|
206
|
+
raise InvalidParameterError.new('labels', 'a non-empty Array') unless args[:labels].is_a?(Array) && !args[:labels].empty?
|
|
207
|
+
|
|
208
|
+
args[:labels].each { |l| Validators.validate_label!(l) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def validate_list_outputs!(args)
|
|
212
|
+
Validators.validate_basket!(args[:basket])
|
|
213
|
+
args[:tags]&.each { |t| Validators.validate_tag!(t) }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def validate_internalize_action!(args)
|
|
217
|
+
raise InvalidParameterError.new('tx', 'a byte array') unless args[:tx].is_a?(Array)
|
|
218
|
+
raise InvalidParameterError.new('outputs', 'a non-empty Array') unless args[:outputs].is_a?(Array) && !args[:outputs].empty?
|
|
219
|
+
|
|
220
|
+
Validators.validate_description!(args[:description])
|
|
221
|
+
args[:labels]&.each { |l| Validators.validate_label!(l) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# --- Transaction building ---
|
|
225
|
+
|
|
226
|
+
def parse_input_beef(input_beef)
|
|
227
|
+
return unless input_beef
|
|
228
|
+
|
|
229
|
+
BSV::Transaction::Beef.from_binary(input_beef.pack('C*'))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def build_transaction(args, beef)
|
|
233
|
+
version = args.fetch(:version, 1)
|
|
234
|
+
lock_time = args.fetch(:lock_time, 0)
|
|
235
|
+
tx = BSV::Transaction::Transaction.new(version: version, lock_time: lock_time)
|
|
236
|
+
|
|
237
|
+
build_inputs(tx, args[:inputs], beef) if args[:inputs]
|
|
238
|
+
build_outputs(tx, args[:outputs]) if args[:outputs]
|
|
239
|
+
|
|
240
|
+
# Randomise output order unless explicitly disabled
|
|
241
|
+
shuffle_outputs(tx) if args[:inputs] && args[:outputs] && args.dig(:options, :randomize_outputs) != false
|
|
242
|
+
|
|
243
|
+
tx
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def build_inputs(tx, inputs, beef)
|
|
247
|
+
inputs.each do |spec|
|
|
248
|
+
txid_hex, index_str = spec[:outpoint].split('.')
|
|
249
|
+
output_index = index_str.to_i
|
|
250
|
+
seq = spec[:sequence_number] || 0xFFFFFFFF
|
|
251
|
+
|
|
252
|
+
input = BSV::Transaction::TransactionInput.new(
|
|
253
|
+
prev_tx_id: BSV::Transaction::TransactionInput.txid_from_hex(txid_hex),
|
|
254
|
+
prev_tx_out_index: output_index,
|
|
255
|
+
sequence: seq
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
wire_source(input, txid_hex, output_index, beef) if beef
|
|
259
|
+
|
|
260
|
+
input.unlocking_script = BSV::Script::Script.from_hex(spec[:unlocking_script]) if spec[:unlocking_script]
|
|
261
|
+
|
|
262
|
+
tx.add_input(input)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def wire_source(input, txid_hex, output_index, beef)
|
|
267
|
+
# find_transaction expects display byte order (32 raw bytes)
|
|
268
|
+
txid_display = [txid_hex].pack('H*')
|
|
269
|
+
source_beef_tx = beef.transactions.find { |bt| bt.transaction&.txid == txid_display }
|
|
270
|
+
return unless source_beef_tx
|
|
271
|
+
|
|
272
|
+
source_tx = source_beef_tx.transaction
|
|
273
|
+
input.source_transaction = source_tx
|
|
274
|
+
return unless source_tx.outputs[output_index]
|
|
275
|
+
|
|
276
|
+
input.source_satoshis = source_tx.outputs[output_index].satoshis
|
|
277
|
+
input.source_locking_script = source_tx.outputs[output_index].locking_script
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def build_outputs(tx, outputs)
|
|
281
|
+
outputs.each do |spec|
|
|
282
|
+
output = BSV::Transaction::TransactionOutput.new(
|
|
283
|
+
satoshis: spec[:satoshis],
|
|
284
|
+
locking_script: BSV::Script::Script.from_hex(spec[:locking_script])
|
|
285
|
+
)
|
|
286
|
+
# Tag the output with its spec so store_tracked_outputs can find the
|
|
287
|
+
# correct post-shuffle index, even when multiple outputs share the
|
|
288
|
+
# same locking script and satoshis.
|
|
289
|
+
output.instance_variable_set(:@_spec, spec)
|
|
290
|
+
tx.add_output(output)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def shuffle_outputs(tx)
|
|
295
|
+
shuffled = tx.outputs.shuffle
|
|
296
|
+
tx.outputs.clear
|
|
297
|
+
shuffled.each { |o| tx.add_output(o) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def needs_signing?(inputs)
|
|
301
|
+
return false unless inputs
|
|
302
|
+
|
|
303
|
+
inputs.any? { |i| i[:unlocking_script_length] && !i[:unlocking_script] }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# --- Finalisation ---
|
|
307
|
+
|
|
308
|
+
def create_signable(tx, args, beef)
|
|
309
|
+
reference = Base64.strict_encode64(SecureRandom.random_bytes(32))
|
|
310
|
+
@pending[reference] = { tx: tx, args: args, beef: beef }
|
|
311
|
+
tx_bytes = tx.to_binary
|
|
312
|
+
{ signable_transaction: { tx: tx_bytes.unpack('C*'), reference: reference } }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def finalize_action(tx, args)
|
|
316
|
+
txid = tx.txid_hex
|
|
317
|
+
status = args.dig(:options, :no_send) ? 'nosend' : 'completed'
|
|
318
|
+
|
|
319
|
+
store_action(tx, args, status: status)
|
|
320
|
+
store_tracked_outputs(txid, tx, args[:outputs])
|
|
321
|
+
|
|
322
|
+
beef_binary = tx.to_beef
|
|
323
|
+
result = { txid: txid, tx: beef_binary.unpack('C*') }
|
|
324
|
+
result[:no_send_change] = [] if args.dig(:options, :no_send)
|
|
325
|
+
result
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def apply_spends(tx, spends)
|
|
329
|
+
spends.each do |index, spend|
|
|
330
|
+
idx = index.is_a?(String) ? index.to_i : index
|
|
331
|
+
raise WalletError, "Input index #{idx} out of range" unless tx.inputs[idx]
|
|
332
|
+
|
|
333
|
+
tx.inputs[idx].unlocking_script = BSV::Script::Script.from_hex(spend[:unlocking_script])
|
|
334
|
+
# sequence is attr_reader only; re-set via instance_variable_set if provided
|
|
335
|
+
tx.inputs[idx].instance_variable_set(:@sequence, spend[:sequence_number]) if spend[:sequence_number]
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# --- Storage helpers ---
|
|
340
|
+
|
|
341
|
+
def store_action(tx, args, status: 'completed')
|
|
342
|
+
@storage.store_action({
|
|
343
|
+
txid: tx.txid_hex,
|
|
344
|
+
status: status,
|
|
345
|
+
description: args[:description],
|
|
346
|
+
labels: args[:labels] || [],
|
|
347
|
+
is_outgoing: true,
|
|
348
|
+
satoshis: tx.total_output_satoshis,
|
|
349
|
+
version: tx.version,
|
|
350
|
+
lock_time: tx.lock_time,
|
|
351
|
+
created_at: Time.now.utc.iso8601
|
|
352
|
+
})
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def store_tracked_outputs(txid, tx, output_specs)
|
|
356
|
+
return unless output_specs
|
|
357
|
+
|
|
358
|
+
output_specs.each do |spec|
|
|
359
|
+
next unless spec[:basket]
|
|
360
|
+
|
|
361
|
+
# Find the actual post-shuffle index by matching the TransactionOutput object.
|
|
362
|
+
# build_outputs stores a reference on each output via instance_variable_set(:@_spec)
|
|
363
|
+
# so we can reliably map even when multiple outputs share the same script/satoshis.
|
|
364
|
+
actual_idx = tx.outputs.index { |o| o.instance_variable_get(:@_spec).equal?(spec) }
|
|
365
|
+
next unless actual_idx
|
|
366
|
+
|
|
367
|
+
@storage.store_output({
|
|
368
|
+
outpoint: "#{txid}.#{actual_idx}",
|
|
369
|
+
satoshis: spec[:satoshis],
|
|
370
|
+
locking_script: spec[:locking_script],
|
|
371
|
+
basket: spec[:basket],
|
|
372
|
+
tags: spec[:tags] || [],
|
|
373
|
+
custom_instructions: spec[:custom_instructions],
|
|
374
|
+
spendable: true
|
|
375
|
+
})
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def build_action_query(args)
|
|
380
|
+
{
|
|
381
|
+
labels: args[:labels],
|
|
382
|
+
label_query_mode: args[:label_query_mode] || 'any',
|
|
383
|
+
limit: args[:limit] || 10,
|
|
384
|
+
offset: args[:offset] || 0
|
|
385
|
+
}
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def build_output_query(args)
|
|
389
|
+
query = {
|
|
390
|
+
basket: args[:basket],
|
|
391
|
+
limit: args[:limit] || 10,
|
|
392
|
+
offset: args[:offset] || 0
|
|
393
|
+
}
|
|
394
|
+
query[:tags] = args[:tags] if args[:tags]
|
|
395
|
+
query[:tag_query_mode] = args[:tag_query_mode] if args[:tag_query_mode]
|
|
396
|
+
query
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# --- Internalize helpers ---
|
|
400
|
+
|
|
401
|
+
def extract_subject_transaction(beef)
|
|
402
|
+
return find_by_subject_txid(beef) if beef.subject_txid
|
|
403
|
+
|
|
404
|
+
last_beef_tx = beef.transactions.reverse.find(&:transaction)
|
|
405
|
+
raise WalletError, 'No transaction found in BEEF' unless last_beef_tx
|
|
406
|
+
|
|
407
|
+
last_beef_tx.transaction
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def find_by_subject_txid(beef)
|
|
411
|
+
beef.find_atomic_transaction(beef.subject_txid) ||
|
|
412
|
+
raise(WalletError, 'Subject transaction not found in BEEF')
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def process_internalize_outputs(tx, output_specs)
|
|
416
|
+
txid = tx.txid_hex
|
|
417
|
+
|
|
418
|
+
output_specs.each do |spec|
|
|
419
|
+
output_index = spec[:output_index]
|
|
420
|
+
tx_output = tx.outputs[output_index]
|
|
421
|
+
raise WalletError, "Output index #{output_index} not found in transaction" unless tx_output
|
|
422
|
+
|
|
423
|
+
case spec[:protocol]
|
|
424
|
+
when 'wallet payment'
|
|
425
|
+
internalize_payment(txid, output_index, tx_output, spec[:payment_remittance])
|
|
426
|
+
when 'basket insertion'
|
|
427
|
+
internalize_basket(txid, output_index, tx_output, spec[:insertion_remittance])
|
|
428
|
+
else
|
|
429
|
+
raise InvalidParameterError.new('protocol', '"wallet payment" or "basket insertion"')
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def internalize_payment(txid, output_index, tx_output, remittance)
|
|
435
|
+
unless remittance
|
|
436
|
+
raise InvalidParameterError.new('payment_remittance',
|
|
437
|
+
'present for wallet payment protocol')
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
sender_key = remittance[:sender_identity_key]
|
|
441
|
+
prefix = remittance[:derivation_prefix]
|
|
442
|
+
suffix = remittance[:derivation_suffix]
|
|
443
|
+
|
|
444
|
+
# BRC-29: derive the expected P2PKH key for this payment
|
|
445
|
+
derived_pub = @key_deriver.derive_public_key(
|
|
446
|
+
[2, '3241645161d8'],
|
|
447
|
+
"#{prefix} #{suffix}",
|
|
448
|
+
sender_key,
|
|
449
|
+
for_self: true
|
|
450
|
+
)
|
|
451
|
+
expected_script = BSV::Script::Script.p2pkh_lock(derived_pub.hash160)
|
|
452
|
+
|
|
453
|
+
raise WalletError, 'Output script does not match derived payment key' unless tx_output.locking_script.to_binary == expected_script.to_binary
|
|
454
|
+
|
|
455
|
+
@storage.store_output({
|
|
456
|
+
outpoint: "#{txid}.#{output_index}",
|
|
457
|
+
satoshis: tx_output.satoshis,
|
|
458
|
+
locking_script: tx_output.locking_script.to_hex,
|
|
459
|
+
spendable: true,
|
|
460
|
+
sender_identity_key: sender_key,
|
|
461
|
+
derivation_prefix: prefix,
|
|
462
|
+
derivation_suffix: suffix
|
|
463
|
+
})
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def internalize_basket(txid, output_index, tx_output, remittance)
|
|
467
|
+
unless remittance
|
|
468
|
+
raise InvalidParameterError.new('insertion_remittance',
|
|
469
|
+
'present for basket insertion protocol')
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
Validators.validate_basket!(remittance[:basket])
|
|
473
|
+
|
|
474
|
+
@storage.store_output({
|
|
475
|
+
outpoint: "#{txid}.#{output_index}",
|
|
476
|
+
satoshis: tx_output.satoshis,
|
|
477
|
+
locking_script: tx_output.locking_script.to_hex,
|
|
478
|
+
basket: remittance[:basket],
|
|
479
|
+
tags: remittance[:tags] || [],
|
|
480
|
+
custom_instructions: remittance[:custom_instructions],
|
|
481
|
+
spendable: true
|
|
482
|
+
})
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module WalletInterface
|
|
5
|
+
autoload :VERSION, 'bsv/wallet_interface/version'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module Wallet
|
|
9
|
+
# BRC-100 Interface
|
|
10
|
+
autoload :Interface, 'bsv/wallet_interface/interface'
|
|
11
|
+
autoload :KeyDeriver, 'bsv/wallet_interface/key_deriver'
|
|
12
|
+
autoload :ProtoWallet, 'bsv/wallet_interface/proto_wallet'
|
|
13
|
+
autoload :Validators, 'bsv/wallet_interface/validators'
|
|
14
|
+
autoload :StorageAdapter, 'bsv/wallet_interface/storage_adapter'
|
|
15
|
+
autoload :MemoryStore, 'bsv/wallet_interface/memory_store'
|
|
16
|
+
autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
|
|
17
|
+
|
|
18
|
+
# Error classes
|
|
19
|
+
autoload :WalletError, 'bsv/wallet_interface/errors/wallet_error'
|
|
20
|
+
autoload :InvalidParameterError, 'bsv/wallet_interface/errors/invalid_parameter_error'
|
|
21
|
+
autoload :InvalidHmacError, 'bsv/wallet_interface/errors/invalid_hmac_error'
|
|
22
|
+
autoload :InvalidSignatureError, 'bsv/wallet_interface/errors/invalid_signature_error'
|
|
23
|
+
autoload :UnsupportedActionError, 'bsv/wallet_interface/errors/unsupported_action_error'
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/bsv-wallet.rb
ADDED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-03-
|
|
10
|
+
date: 2026-03-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: A Ruby library for interacting with the BSV Blockchain — keys, scripts,
|
|
13
13
|
transactions, and more.
|
|
@@ -16,10 +16,11 @@ extensions: []
|
|
|
16
16
|
extra_rdoc_files: []
|
|
17
17
|
files:
|
|
18
18
|
- CHANGELOG.md
|
|
19
|
-
-
|
|
19
|
+
- LICENSE
|
|
20
20
|
- README.md
|
|
21
21
|
- lib/bsv-attest.rb
|
|
22
22
|
- lib/bsv-sdk.rb
|
|
23
|
+
- lib/bsv-wallet.rb
|
|
23
24
|
- lib/bsv/attest.rb
|
|
24
25
|
- lib/bsv/attest/configuration.rb
|
|
25
26
|
- lib/bsv/attest/response.rb
|
|
@@ -39,13 +40,19 @@ files:
|
|
|
39
40
|
- lib/bsv/primitives/digest.rb
|
|
40
41
|
- lib/bsv/primitives/ecdsa.rb
|
|
41
42
|
- lib/bsv/primitives/ecies.rb
|
|
43
|
+
- lib/bsv/primitives/encrypted_message.rb
|
|
42
44
|
- lib/bsv/primitives/extended_key.rb
|
|
45
|
+
- lib/bsv/primitives/key_shares.rb
|
|
43
46
|
- lib/bsv/primitives/mnemonic.rb
|
|
44
47
|
- lib/bsv/primitives/mnemonic/wordlist.rb
|
|
48
|
+
- lib/bsv/primitives/point_in_finite_field.rb
|
|
49
|
+
- lib/bsv/primitives/polynomial.rb
|
|
45
50
|
- lib/bsv/primitives/private_key.rb
|
|
46
51
|
- lib/bsv/primitives/public_key.rb
|
|
47
52
|
- lib/bsv/primitives/schnorr.rb
|
|
48
53
|
- lib/bsv/primitives/signature.rb
|
|
54
|
+
- lib/bsv/primitives/signed_message.rb
|
|
55
|
+
- lib/bsv/primitives/symmetric_key.rb
|
|
49
56
|
- lib/bsv/script.rb
|
|
50
57
|
- lib/bsv/script/builder.rb
|
|
51
58
|
- lib/bsv/script/chunk.rb
|
|
@@ -83,6 +90,20 @@ files:
|
|
|
83
90
|
- lib/bsv/wallet.rb
|
|
84
91
|
- lib/bsv/wallet/insufficient_funds_error.rb
|
|
85
92
|
- lib/bsv/wallet/wallet.rb
|
|
93
|
+
- lib/bsv/wallet_interface.rb
|
|
94
|
+
- lib/bsv/wallet_interface/errors/invalid_hmac_error.rb
|
|
95
|
+
- lib/bsv/wallet_interface/errors/invalid_parameter_error.rb
|
|
96
|
+
- lib/bsv/wallet_interface/errors/invalid_signature_error.rb
|
|
97
|
+
- lib/bsv/wallet_interface/errors/unsupported_action_error.rb
|
|
98
|
+
- lib/bsv/wallet_interface/errors/wallet_error.rb
|
|
99
|
+
- lib/bsv/wallet_interface/interface.rb
|
|
100
|
+
- lib/bsv/wallet_interface/key_deriver.rb
|
|
101
|
+
- lib/bsv/wallet_interface/memory_store.rb
|
|
102
|
+
- lib/bsv/wallet_interface/proto_wallet.rb
|
|
103
|
+
- lib/bsv/wallet_interface/storage_adapter.rb
|
|
104
|
+
- lib/bsv/wallet_interface/validators.rb
|
|
105
|
+
- lib/bsv/wallet_interface/version.rb
|
|
106
|
+
- lib/bsv/wallet_interface/wallet_client.rb
|
|
86
107
|
homepage: https://github.com/sgbett/bsv-ruby-sdk
|
|
87
108
|
licenses:
|
|
88
109
|
- LicenseRef-OpenBSV
|
/data/{LICENCE → LICENSE}
RENAMED
|
File without changes
|