bsv-sdk 0.2.1 → 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bsv/network/broadcast_response.rb +1 -2
  3. data/lib/bsv/primitives/bsm.rb +2 -6
  4. data/lib/bsv/primitives/curve.rb +1 -2
  5. data/lib/bsv/primitives/encrypted_message.rb +100 -0
  6. data/lib/bsv/primitives/extended_key.rb +1 -2
  7. data/lib/bsv/primitives/key_shares.rb +83 -0
  8. data/lib/bsv/primitives/mnemonic.rb +1 -3
  9. data/lib/bsv/primitives/point_in_finite_field.rb +72 -0
  10. data/lib/bsv/primitives/polynomial.rb +95 -0
  11. data/lib/bsv/primitives/private_key.rb +100 -3
  12. data/lib/bsv/primitives/signed_message.rb +104 -0
  13. data/lib/bsv/primitives/symmetric_key.rb +128 -0
  14. data/lib/bsv/primitives.rb +18 -12
  15. data/lib/bsv/script/interpreter/interpreter.rb +1 -3
  16. data/lib/bsv/script/interpreter/operations/bitwise.rb +1 -3
  17. data/lib/bsv/script/interpreter/operations/crypto.rb +3 -9
  18. data/lib/bsv/script/interpreter/operations/flow_control.rb +2 -6
  19. data/lib/bsv/script/interpreter/operations/splice.rb +1 -3
  20. data/lib/bsv/script/interpreter/script_number.rb +2 -7
  21. data/lib/bsv/script/script.rb +252 -1
  22. data/lib/bsv/transaction/beef.rb +1 -4
  23. data/lib/bsv/transaction/transaction.rb +123 -45
  24. data/lib/bsv/transaction/transaction_input.rb +1 -2
  25. data/lib/bsv/transaction/transaction_output.rb +1 -2
  26. data/lib/bsv/transaction/var_int.rb +4 -16
  27. data/lib/bsv/transaction.rb +14 -14
  28. data/lib/bsv/version.rb +1 -1
  29. data/lib/bsv/wallet_interface/errors/invalid_hmac_error.rb +11 -0
  30. data/lib/bsv/wallet_interface/errors/invalid_parameter_error.rb +14 -0
  31. data/lib/bsv/wallet_interface/errors/invalid_signature_error.rb +11 -0
  32. data/lib/bsv/wallet_interface/errors/unsupported_action_error.rb +11 -0
  33. data/lib/bsv/wallet_interface/errors/wallet_error.rb +14 -0
  34. data/lib/bsv/wallet_interface/interface.rb +384 -0
  35. data/lib/bsv/wallet_interface/key_deriver.rb +142 -0
  36. data/lib/bsv/wallet_interface/memory_store.rb +115 -0
  37. data/lib/bsv/wallet_interface/proto_wallet.rb +361 -0
  38. data/lib/bsv/wallet_interface/storage_adapter.rb +51 -0
  39. data/lib/bsv/wallet_interface/validators.rb +126 -0
  40. data/lib/bsv/wallet_interface/version.rb +7 -0
  41. data/lib/bsv/wallet_interface/wallet_client.rb +486 -0
  42. data/lib/bsv/wallet_interface.rb +25 -0
  43. data/lib/bsv-wallet.rb +4 -0
  44. metadata +24 -3
  45. /data/{LICENCE → LICENSE} +0 -0
@@ -0,0 +1,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # BRC-100 Wallet Interface
6
+ #
7
+ # Defines the 28 methods of the standard BSV wallet-to-application interface.
8
+ # Include this module and override the methods your implementation supports.
9
+ # Unimplemented methods raise {UnsupportedActionError}.
10
+ module Interface
11
+ # rubocop:disable Lint/UnusedMethodArgument
12
+
13
+ # --- Transaction Operations ---
14
+
15
+ # Creates a new Bitcoin transaction with metadata and labels.
16
+ #
17
+ # @param args [Hash] transaction parameters
18
+ # @option args [String] :description (required) 5-50 char description
19
+ # @option args [Array<Integer>] :input_beef BEEF data for inputs
20
+ # @option args [Array<Hash>] :inputs input objects with :outpoint, :unlocking_script, :input_description
21
+ # @option args [Array<Hash>] :outputs output objects with :locking_script, :satoshis, :output_description
22
+ # @option args [Integer] :lock_time optional lock time
23
+ # @option args [Integer] :version optional transaction version
24
+ # @option args [Array<String>] :labels optional transaction labels
25
+ # @option args [Hash] :options optional processing options
26
+ # @param originator [String, nil] FQDN of the originating application
27
+ # @return [Hash] with :txid, :tx, :no_send_change, :send_with_results, :signable_transaction
28
+ def create_action(args, originator: nil)
29
+ raise UnsupportedActionError, 'create_action'
30
+ end
31
+
32
+ # Signs a previously created transaction.
33
+ #
34
+ # @param args [Hash] signing parameters
35
+ # @option args [Hash] :spends map of input indexes to unlocking scripts
36
+ # @option args [String] :reference base64 reference from create_action
37
+ # @option args [Hash] :options optional processing options
38
+ # @param originator [String, nil] FQDN of the originating application
39
+ # @return [Hash] with :txid, :tx, :send_with_results
40
+ def sign_action(args, originator: nil)
41
+ raise UnsupportedActionError, 'sign_action'
42
+ end
43
+
44
+ # Aborts an incomplete transaction.
45
+ #
46
+ # @param args [Hash]
47
+ # @option args [String] :reference base64 reference to abort
48
+ # @param originator [String, nil] FQDN of the originating application
49
+ # @return [Hash] { aborted: true }
50
+ def abort_action(args, originator: nil)
51
+ raise UnsupportedActionError, 'abort_action'
52
+ end
53
+
54
+ # Lists transactions matching the specified labels.
55
+ #
56
+ # @param args [Hash] query parameters
57
+ # @option args [Array<String>] :labels (required) labels to filter by
58
+ # @option args [String] :label_query_mode 'any' or 'all'
59
+ # @option args [Boolean] :include_labels include labels in results
60
+ # @option args [Boolean] :include_inputs include input details
61
+ # @option args [Boolean] :include_outputs include output details
62
+ # @option args [Integer] :limit max results (default 10, max 10000)
63
+ # @option args [Integer] :offset number to skip
64
+ # @param originator [String, nil] FQDN of the originating application
65
+ # @return [Hash] with :total_actions, :actions
66
+ def list_actions(args, originator: nil)
67
+ raise UnsupportedActionError, 'list_actions'
68
+ end
69
+
70
+ # Accepts an incoming transaction for internalization.
71
+ #
72
+ # @param args [Hash]
73
+ # @option args [Array<Integer>] :tx Atomic BEEF-formatted transaction
74
+ # @option args [Array<Hash>] :outputs metadata about outputs
75
+ # @option args [String] :description 5-50 char description
76
+ # @option args [Array<String>] :labels optional labels
77
+ # @param originator [String, nil] FQDN of the originating application
78
+ # @return [Hash] { accepted: true }
79
+ def internalize_action(args, originator: nil)
80
+ raise UnsupportedActionError, 'internalize_action'
81
+ end
82
+
83
+ # Lists spendable outputs in a basket.
84
+ #
85
+ # @param args [Hash]
86
+ # @option args [String] :basket (required) basket name
87
+ # @option args [Array<String>] :tags optional tag filter
88
+ # @option args [String] :tag_query_mode 'any' or 'all'
89
+ # @option args [String] :include 'locking scripts' or 'entire transactions'
90
+ # @option args [Integer] :limit max results
91
+ # @option args [Integer] :offset number to skip
92
+ # @param originator [String, nil] FQDN of the originating application
93
+ # @return [Hash] with :total_outputs, :outputs
94
+ def list_outputs(args, originator: nil)
95
+ raise UnsupportedActionError, 'list_outputs'
96
+ end
97
+
98
+ # Releases an output from basket tracking.
99
+ #
100
+ # @param args [Hash]
101
+ # @option args [String] :basket basket name
102
+ # @option args [String] :output outpoint string
103
+ # @param originator [String, nil] FQDN of the originating application
104
+ # @return [Hash] { relinquished: true }
105
+ def relinquish_output(args, originator: nil)
106
+ raise UnsupportedActionError, 'relinquish_output'
107
+ end
108
+
109
+ # --- Public Key Management ---
110
+
111
+ # Retrieves a derived or identity public key.
112
+ #
113
+ # @param args [Hash]
114
+ # @option args [Boolean] :identity_key if true, return the identity key
115
+ # @option args [Array] :protocol_id [security_level, protocol_name]
116
+ # @option args [String] :key_id key identifier
117
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
118
+ # @option args [Boolean] :for_self derive from own identity
119
+ # @option args [Boolean] :privileged privileged mode
120
+ # @option args [String] :privileged_reason reason for privileged access
121
+ # @param originator [String, nil] FQDN of the originating application
122
+ # @return [Hash] { public_key: String }
123
+ def get_public_key(args, originator: nil)
124
+ raise UnsupportedActionError, 'get_public_key'
125
+ end
126
+
127
+ # Reveals key linkage between self and a counterparty to a verifier.
128
+ #
129
+ # @param args [Hash]
130
+ # @option args [String] :counterparty counterparty public key hex
131
+ # @option args [String] :verifier verifier public key hex
132
+ # @option args [Boolean] :privileged privileged mode
133
+ # @option args [String] :privileged_reason reason for privileged access
134
+ # @param originator [String, nil] FQDN of the originating application
135
+ # @return [Hash] with :prover, :verifier, :counterparty, :revelation_time, :encrypted_linkage, :encrypted_linkage_proof
136
+ def reveal_counterparty_key_linkage(args, originator: nil)
137
+ raise UnsupportedActionError, 'reveal_counterparty_key_linkage'
138
+ end
139
+
140
+ # Reveals specific key linkage for a particular interaction.
141
+ #
142
+ # @param args [Hash]
143
+ # @option args [String] :counterparty counterparty public key hex
144
+ # @option args [String] :verifier verifier public key hex
145
+ # @option args [Array] :protocol_id [security_level, protocol_name]
146
+ # @option args [String] :key_id key identifier
147
+ # @option args [Boolean] :privileged privileged mode
148
+ # @option args [String] :privileged_reason reason for privileged access
149
+ # @param originator [String, nil] FQDN of the originating application
150
+ # @return [Hash] with :prover, :verifier, :counterparty, :protocol_id, :key_id, :encrypted_linkage, :encrypted_linkage_proof, :proof_type
151
+ def reveal_specific_key_linkage(args, originator: nil)
152
+ raise UnsupportedActionError, 'reveal_specific_key_linkage'
153
+ end
154
+
155
+ # --- Cryptography Operations ---
156
+
157
+ # Encrypts data using a derived symmetric key.
158
+ #
159
+ # @param args [Hash]
160
+ # @option args [Array<Integer>] :plaintext byte array to encrypt
161
+ # @option args [Array] :protocol_id [security_level, protocol_name]
162
+ # @option args [String] :key_id key identifier
163
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
164
+ # @option args [Boolean] :privileged privileged mode
165
+ # @param originator [String, nil] FQDN of the originating application
166
+ # @return [Hash] { ciphertext: Array<Integer> }
167
+ def encrypt(args, originator: nil)
168
+ raise UnsupportedActionError, 'encrypt'
169
+ end
170
+
171
+ # Decrypts data using a derived symmetric key.
172
+ #
173
+ # @param args [Hash]
174
+ # @option args [Array<Integer>] :ciphertext byte array to decrypt
175
+ # @option args [Array] :protocol_id [security_level, protocol_name]
176
+ # @option args [String] :key_id key identifier
177
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
178
+ # @option args [Boolean] :privileged privileged mode
179
+ # @param originator [String, nil] FQDN of the originating application
180
+ # @return [Hash] { plaintext: Array<Integer> }
181
+ def decrypt(args, originator: nil)
182
+ raise UnsupportedActionError, 'decrypt'
183
+ end
184
+
185
+ # Creates an HMAC using a derived symmetric key.
186
+ #
187
+ # @param args [Hash]
188
+ # @option args [Array<Integer>] :data byte array
189
+ # @option args [Array] :protocol_id [security_level, protocol_name]
190
+ # @option args [String] :key_id key identifier
191
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
192
+ # @option args [Boolean] :privileged privileged mode
193
+ # @param originator [String, nil] FQDN of the originating application
194
+ # @return [Hash] { hmac: Array<Integer> }
195
+ def create_hmac(args, originator: nil)
196
+ raise UnsupportedActionError, 'create_hmac'
197
+ end
198
+
199
+ # Verifies an HMAC using a derived symmetric key.
200
+ #
201
+ # @param args [Hash]
202
+ # @option args [Array<Integer>] :data byte array
203
+ # @option args [Array<Integer>] :hmac HMAC to verify
204
+ # @option args [Array] :protocol_id [security_level, protocol_name]
205
+ # @option args [String] :key_id key identifier
206
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
207
+ # @option args [Boolean] :privileged privileged mode
208
+ # @param originator [String, nil] FQDN of the originating application
209
+ # @return [Hash] { valid: true }
210
+ def verify_hmac(args, originator: nil)
211
+ raise UnsupportedActionError, 'verify_hmac'
212
+ end
213
+
214
+ # Creates a digital signature using a derived private key.
215
+ #
216
+ # @param args [Hash]
217
+ # @option args [Array<Integer>] :data data to sign
218
+ # @option args [Array<Integer>] :hash_to_directly_sign pre-hashed data to sign
219
+ # @option args [Array] :protocol_id [security_level, protocol_name]
220
+ # @option args [String] :key_id key identifier
221
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
222
+ # @option args [Boolean] :privileged privileged mode
223
+ # @param originator [String, nil] FQDN of the originating application
224
+ # @return [Hash] { signature: Array<Integer> }
225
+ def create_signature(args, originator: nil)
226
+ raise UnsupportedActionError, 'create_signature'
227
+ end
228
+
229
+ # Verifies a digital signature using a derived public key.
230
+ #
231
+ # @param args [Hash]
232
+ # @option args [Array<Integer>] :data original data
233
+ # @option args [Array<Integer>] :hash_to_directly_verify pre-hashed data
234
+ # @option args [Array<Integer>] :signature DER-encoded signature
235
+ # @option args [Array] :protocol_id [security_level, protocol_name]
236
+ # @option args [String] :key_id key identifier
237
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
238
+ # @option args [Boolean] :for_self verify own signature
239
+ # @option args [Boolean] :privileged privileged mode
240
+ # @param originator [String, nil] FQDN of the originating application
241
+ # @return [Hash] { valid: true }
242
+ def verify_signature(args, originator: nil)
243
+ raise UnsupportedActionError, 'verify_signature'
244
+ end
245
+
246
+ # --- Identity and Certificate Management ---
247
+
248
+ # Acquires an identity certificate.
249
+ #
250
+ # @param args [Hash]
251
+ # @option args [String] :type certificate type (base64)
252
+ # @option args [String] :certifier certifier public key hex
253
+ # @option args [String] :acquisition_protocol 'direct' or 'issuance'
254
+ # @option args [Hash] :fields certificate fields
255
+ # @param originator [String, nil] FQDN of the originating application
256
+ # @return [Hash] certificate data
257
+ def acquire_certificate(args, originator: nil)
258
+ raise UnsupportedActionError, 'acquire_certificate'
259
+ end
260
+
261
+ # Lists identity certificates.
262
+ #
263
+ # @param args [Hash]
264
+ # @option args [Array<String>] :certifiers certifier public keys
265
+ # @option args [Array<String>] :types certificate types
266
+ # @option args [Integer] :limit max results
267
+ # @option args [Integer] :offset number to skip
268
+ # @param originator [String, nil] FQDN of the originating application
269
+ # @return [Hash] with :total_certificates, :certificates
270
+ def list_certificates(args, originator: nil)
271
+ raise UnsupportedActionError, 'list_certificates'
272
+ end
273
+
274
+ # Proves select fields of a certificate to a verifier.
275
+ #
276
+ # @param args [Hash]
277
+ # @option args [Hash] :certificate the certificate to prove
278
+ # @option args [Array<String>] :fields_to_reveal field names to reveal
279
+ # @option args [String] :verifier verifier public key hex
280
+ # @param originator [String, nil] FQDN of the originating application
281
+ # @return [Hash] { keyring_for_verifier: Hash }
282
+ def prove_certificate(args, originator: nil)
283
+ raise UnsupportedActionError, 'prove_certificate'
284
+ end
285
+
286
+ # Removes a certificate from the wallet.
287
+ #
288
+ # @param args [Hash]
289
+ # @option args [String] :type certificate type
290
+ # @option args [String] :serial_number certificate serial number
291
+ # @option args [String] :certifier certifier public key hex
292
+ # @param originator [String, nil] FQDN of the originating application
293
+ # @return [Hash] { relinquished: true }
294
+ def relinquish_certificate(args, originator: nil)
295
+ raise UnsupportedActionError, 'relinquish_certificate'
296
+ end
297
+
298
+ # Discovers certificates by identity key.
299
+ #
300
+ # @param args [Hash]
301
+ # @option args [String] :identity_key public key hex to search
302
+ # @option args [Integer] :limit max results
303
+ # @option args [Integer] :offset number to skip
304
+ # @param originator [String, nil] FQDN of the originating application
305
+ # @return [Hash] with :total_certificates, :certificates
306
+ def discover_by_identity_key(args, originator: nil)
307
+ raise UnsupportedActionError, 'discover_by_identity_key'
308
+ end
309
+
310
+ # Discovers certificates by attributes.
311
+ #
312
+ # @param args [Hash]
313
+ # @option args [Hash] :attributes attribute name/value pairs to match
314
+ # @option args [Integer] :limit max results
315
+ # @option args [Integer] :offset number to skip
316
+ # @param originator [String, nil] FQDN of the originating application
317
+ # @return [Hash] with :total_certificates, :certificates
318
+ def discover_by_attributes(args, originator: nil)
319
+ raise UnsupportedActionError, 'discover_by_attributes'
320
+ end
321
+
322
+ # --- Blockchain and Network Data ---
323
+
324
+ # Returns the current blockchain height.
325
+ #
326
+ # @param args [Hash] empty hash
327
+ # @param originator [String, nil] FQDN of the originating application
328
+ # @return [Hash] { height: Integer }
329
+ def get_height(args = {}, originator: nil)
330
+ raise UnsupportedActionError, 'get_height'
331
+ end
332
+
333
+ # Returns the block header at a given height.
334
+ #
335
+ # @param args [Hash]
336
+ # @option args [Integer] :height block height
337
+ # @param originator [String, nil] FQDN of the originating application
338
+ # @return [Hash] { header: String }
339
+ def get_header_for_height(args, originator: nil)
340
+ raise UnsupportedActionError, 'get_header_for_height'
341
+ end
342
+
343
+ # Returns the network (mainnet or testnet).
344
+ #
345
+ # @param args [Hash] empty hash
346
+ # @param originator [String, nil] FQDN of the originating application
347
+ # @return [Hash] { network: String }
348
+ def get_network(args = {}, originator: nil)
349
+ raise UnsupportedActionError, 'get_network'
350
+ end
351
+
352
+ # Returns the wallet version string.
353
+ #
354
+ # @param args [Hash] empty hash
355
+ # @param originator [String, nil] FQDN of the originating application
356
+ # @return [Hash] { version: String }
357
+ def get_version(args = {}, originator: nil)
358
+ raise UnsupportedActionError, 'get_version'
359
+ end
360
+
361
+ # --- Authentication ---
362
+
363
+ # Checks if the user is authenticated.
364
+ #
365
+ # @param args [Hash] empty hash
366
+ # @param originator [String, nil] FQDN of the originating application
367
+ # @return [Hash] { authenticated: Boolean }
368
+ def is_authenticated(args = {}, originator: nil)
369
+ raise UnsupportedActionError, 'is_authenticated'
370
+ end
371
+
372
+ # Waits until the user is authenticated.
373
+ #
374
+ # @param args [Hash] empty hash
375
+ # @param originator [String, nil] FQDN of the originating application
376
+ # @return [Hash] { authenticated: true }
377
+ def wait_for_authentication(args = {}, originator: nil)
378
+ raise UnsupportedActionError, 'wait_for_authentication'
379
+ end
380
+
381
+ # rubocop:enable Lint/UnusedMethodArgument
382
+ end
383
+ end
384
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # BRC-42/43 key derivation for the wallet interface.
6
+ #
7
+ # Derives child keys from a root private key using BKDS (BSV Key Derivation
8
+ # Scheme). Supports protocol IDs, key IDs, counterparties, and security
9
+ # levels as defined in BRC-43.
10
+ class KeyDeriver
11
+ ANYONE_BN = OpenSSL::BN.new(1)
12
+
13
+ attr_reader :root_key
14
+
15
+ # @param root_key [BSV::Primitives::PrivateKey, String] a private key or 'anyone'
16
+ def initialize(root_key)
17
+ @root_key = if root_key == 'anyone'
18
+ BSV::Primitives::PrivateKey.new(ANYONE_BN)
19
+ else
20
+ root_key
21
+ end
22
+ end
23
+
24
+ # Returns the identity public key as a hex string.
25
+ # @return [String] 66-character compressed public key hex
26
+ def identity_key
27
+ @root_key.public_key.to_hex
28
+ end
29
+
30
+ # Derives a public key using BRC-42 key derivation.
31
+ #
32
+ # @param protocol_id [Array] [security_level, protocol_name]
33
+ # @param key_id [String] key identifier
34
+ # @param counterparty [String] public key hex, 'self', or 'anyone'
35
+ # @param for_self [Boolean] derive from own identity rather than counterparty's
36
+ # @return [BSV::Primitives::PublicKey]
37
+ def derive_public_key(protocol_id, key_id, counterparty, for_self: false)
38
+ Validators.validate_protocol_id!(protocol_id)
39
+ Validators.validate_key_id!(key_id)
40
+ invoice = compute_invoice_number(protocol_id, key_id)
41
+ counterparty_pub = resolve_counterparty(counterparty)
42
+
43
+ if for_self
44
+ @root_key.derive_child(counterparty_pub, invoice).public_key
45
+ else
46
+ counterparty_pub.derive_child(@root_key, invoice)
47
+ end
48
+ end
49
+
50
+ # Derives a private key using BRC-42 key derivation.
51
+ #
52
+ # @param protocol_id [Array] [security_level, protocol_name]
53
+ # @param key_id [String] key identifier
54
+ # @param counterparty [String] public key hex, 'self', or 'anyone'
55
+ # @return [BSV::Primitives::PrivateKey]
56
+ def derive_private_key(protocol_id, key_id, counterparty)
57
+ Validators.validate_protocol_id!(protocol_id)
58
+ Validators.validate_key_id!(key_id)
59
+ invoice = compute_invoice_number(protocol_id, key_id)
60
+ counterparty_pub = resolve_counterparty(counterparty)
61
+ @root_key.derive_child(counterparty_pub, invoice)
62
+ end
63
+
64
+ # Derives a symmetric key for encryption/HMAC operations.
65
+ #
66
+ # Uses ECDH between the derived private and public child keys to
67
+ # produce a shared secret, then uses the X-coordinate as the key.
68
+ #
69
+ # @param protocol_id [Array] [security_level, protocol_name]
70
+ # @param key_id [String] key identifier
71
+ # @param counterparty [String] public key hex, 'self', or 'anyone'
72
+ # @return [BSV::Primitives::SymmetricKey]
73
+ def derive_symmetric_key(protocol_id, key_id, counterparty)
74
+ Validators.validate_protocol_id!(protocol_id)
75
+ Validators.validate_key_id!(key_id)
76
+ invoice = compute_invoice_number(protocol_id, key_id)
77
+ counterparty_pub = resolve_counterparty(counterparty)
78
+
79
+ derived_private = @root_key.derive_child(counterparty_pub, invoice)
80
+ derived_public = counterparty_pub.derive_child(@root_key, invoice)
81
+
82
+ BSV::Primitives::SymmetricKey.from_ecdh(derived_private, derived_public)
83
+ end
84
+
85
+ # Reveals the ECDH shared secret between this wallet and a counterparty.
86
+ # Used for BRC-69 Method 1 (counterparty key linkage).
87
+ #
88
+ # @param counterparty [String] public key hex (not 'self')
89
+ # @return [String] compressed shared secret bytes
90
+ def reveal_counterparty_secret(counterparty)
91
+ raise InvalidParameterError.new('counterparty', 'not "self" for key linkage revelation') if counterparty == 'self'
92
+
93
+ counterparty_pub = resolve_counterparty(counterparty)
94
+ @root_key.derive_shared_secret(counterparty_pub).compressed
95
+ end
96
+
97
+ # Reveals the specific key offset for a particular derived key.
98
+ # Used for BRC-69 Method 2 (specific key linkage).
99
+ #
100
+ # @param counterparty [String] public key hex
101
+ # @param protocol_id [Array] [security_level, protocol_name]
102
+ # @param key_id [String] key identifier
103
+ # @return [String] HMAC-SHA256 bytes (the key offset)
104
+ def reveal_specific_secret(counterparty, protocol_id, key_id)
105
+ Validators.validate_protocol_id!(protocol_id)
106
+ Validators.validate_key_id!(key_id)
107
+ counterparty_pub = resolve_counterparty(counterparty)
108
+ shared = @root_key.derive_shared_secret(counterparty_pub)
109
+ invoice = compute_invoice_number(protocol_id, key_id)
110
+ BSV::Primitives::Digest.hmac_sha256(shared.compressed, invoice.encode('UTF-8'))
111
+ end
112
+
113
+ private
114
+
115
+ # Resolves a counterparty identifier to a PublicKey.
116
+ #
117
+ # @param counterparty [String] 'self', 'anyone', or a hex public key
118
+ # @return [BSV::Primitives::PublicKey]
119
+ def resolve_counterparty(counterparty)
120
+ case counterparty
121
+ when 'self'
122
+ @root_key.public_key
123
+ when 'anyone'
124
+ BSV::Primitives::PrivateKey.new(ANYONE_BN).public_key
125
+ else
126
+ Validators.validate_counterparty!(counterparty)
127
+ BSV::Primitives::PublicKey.from_hex(counterparty)
128
+ end
129
+ end
130
+
131
+ # Computes the invoice number from a protocol ID and key ID.
132
+ # Format: "#{security_level}-#{protocol_name}-#{key_id}"
133
+ #
134
+ # @param protocol_id [Array] [security_level, protocol_name]
135
+ # @param key_id [String]
136
+ # @return [String]
137
+ def compute_invoice_number(protocol_id, key_id)
138
+ "#{protocol_id[0]}-#{protocol_id[1]}-#{key_id}"
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # In-memory storage adapter for testing.
6
+ #
7
+ # Stores actions, outputs, and certificates in plain Ruby arrays.
8
+ # Not thread-safe; intended for test use only.
9
+ class MemoryStore
10
+ include StorageAdapter
11
+
12
+ def initialize
13
+ @actions = []
14
+ @outputs = []
15
+ @certificates = []
16
+ end
17
+
18
+ def store_action(action_data)
19
+ @actions << action_data
20
+ action_data
21
+ end
22
+
23
+ def find_actions(query)
24
+ apply_pagination(filter_actions(query), query)
25
+ end
26
+
27
+ def count_actions(query)
28
+ filter_actions(query).length
29
+ end
30
+
31
+ def store_output(output_data)
32
+ @outputs << output_data
33
+ output_data
34
+ end
35
+
36
+ def find_outputs(query)
37
+ apply_pagination(filter_outputs(query), query)
38
+ end
39
+
40
+ def count_outputs(query)
41
+ filter_outputs(query).length
42
+ end
43
+
44
+ def delete_output(outpoint)
45
+ idx = @outputs.index { |o| o[:outpoint] == outpoint }
46
+ return false unless idx
47
+
48
+ @outputs.delete_at(idx)
49
+ true
50
+ end
51
+
52
+ def store_certificate(cert_data)
53
+ @certificates << cert_data
54
+ cert_data
55
+ end
56
+
57
+ def find_certificates(query)
58
+ results = @certificates
59
+ results = results.select { |c| query[:certifiers].include?(c[:certifier]) } if query[:certifiers]
60
+ results = results.select { |c| query[:types].include?(c[:type]) } if query[:types]
61
+ apply_pagination(results, query)
62
+ end
63
+
64
+ def delete_certificate(type:, serial_number:, certifier:)
65
+ idx = @certificates.index do |c|
66
+ c[:type] == type && c[:serial_number] == serial_number && c[:certifier] == certifier
67
+ end
68
+ return false unless idx
69
+
70
+ @certificates.delete_at(idx)
71
+ true
72
+ end
73
+
74
+ private
75
+
76
+ def filter_actions(query)
77
+ results = @actions
78
+ return results unless query[:labels]
79
+
80
+ mode = query[:label_query_mode] || 'any'
81
+ results.select do |a|
82
+ action_labels = a[:labels] || []
83
+ if mode == 'all'
84
+ (query[:labels] - action_labels).empty?
85
+ else
86
+ (query[:labels] & action_labels).any?
87
+ end
88
+ end
89
+ end
90
+
91
+ def filter_outputs(query)
92
+ results = @outputs
93
+ results = results.select { |o| o[:basket] == query[:basket] } if query[:basket]
94
+ if query[:tags]
95
+ mode = query[:tag_query_mode] || 'any'
96
+ results = results.select do |o|
97
+ output_tags = o[:tags] || []
98
+ if mode == 'all'
99
+ (query[:tags] - output_tags).empty?
100
+ else
101
+ (query[:tags] & output_tags).any?
102
+ end
103
+ end
104
+ end
105
+ query[:include_spent] ? results : results.reject { |o| o[:spendable] == false }
106
+ end
107
+
108
+ def apply_pagination(results, query)
109
+ offset = query[:offset] || 0
110
+ limit = query[:limit] || 10
111
+ results[offset, limit] || []
112
+ end
113
+ end
114
+ end
115
+ end