bsv-sdk 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61435802c338981fb3f6fd8d5881aac9aa83b1cdb261f02dc2a9e2ffa1eb8fa7
4
- data.tar.gz: '0159ac16ce996bc45503c6342e0ec763d82285968d9b614c209b4c38fc8f2980'
3
+ metadata.gz: b45240189e3a6531b7273ee4df823490c1f892ae332257743b74783f2e88ef87
4
+ data.tar.gz: b3ffa3feb075fb232b87a50149af07139ef01d1d80edb03c7e595f758fe01a84
5
5
  SHA512:
6
- metadata.gz: 17c9ee9dee1b8454aff19753196dbbc9a04463bdca7da03c6c0769d23b9603efb9ef9f4d6fd57b4dc426a99077d5780c9bb5e978e10e6675809301fc9fea1178
7
- data.tar.gz: 1a1bbbee8c570c28c8c608320e1db986527d1e847e5c2cb1943c2b35ec10f3fd6de983bea3eb0026b4216caecee2771c246a0ba3eebc497f2bce88325a58002c
6
+ metadata.gz: 28cb3358d0483ad4574e1ef3a57c6f85f45e4404de0a70faea6e8b431b9c47655b490e73f4779ca74a617e35bcdadd65eb7f5e2cbd9310199a3af494af4a9187
7
+ data.tar.gz: 5a4f40bf35ba4dc93a260c6729871a165ebb4e468b802f664371bcf0574d3aacf23d91f8ca9634a739af6cd4443e8b264b9256a2888ddfd725289f98a606b3a9
@@ -23,11 +23,20 @@ module BSV
23
23
  end
24
24
 
25
25
  # Submit a transaction to ARC.
26
- # Returns BroadcastResponse on success, raises BroadcastError on failure.
27
- def broadcast(tx)
26
+ #
27
+ # @param tx [Transaction] the transaction to broadcast
28
+ # @param wait_for [String, nil] ARC wait condition — one of
29
+ # 'RECEIVED', 'STORED', 'ANNOUNCED_TO_NETWORK',
30
+ # 'SEEN_ON_NETWORK', or 'MINED'. When set, ARC holds the
31
+ # connection open until the transaction reaches the requested
32
+ # state (or times out). Defaults to nil (no wait).
33
+ # @return [BroadcastResponse]
34
+ # @raise [BroadcastError]
35
+ def broadcast(tx, wait_for: nil)
28
36
  uri = URI("#{@url}/v1/tx")
29
37
  request = Net::HTTP::Post.new(uri)
30
38
  request['Content-Type'] = 'application/octet-stream'
39
+ request['X-WaitFor'] = wait_for if wait_for
31
40
  apply_auth_header(request)
32
41
  request.body = tx.to_binary
33
42
 
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.3.0'
4
+ VERSION = '0.3.1'
5
5
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Duck-typed interface for blockchain data providers.
6
+ #
7
+ # Include this module in chain provider adapters and override all methods.
8
+ # The default implementations raise NotImplementedError.
9
+ #
10
+ # @example Custom provider
11
+ # class MyChainProvider
12
+ # include BSV::Wallet::ChainProvider
13
+ #
14
+ # def get_height
15
+ # # query your node/API
16
+ # end
17
+ #
18
+ # def get_header(height)
19
+ # # return 80-byte hex block header
20
+ # end
21
+ # end
22
+ module ChainProvider
23
+ # Returns the current blockchain height.
24
+ # @return [Integer]
25
+ def get_height
26
+ raise NotImplementedError, "#{self.class}#get_height not implemented"
27
+ end
28
+
29
+ # Returns the block header at the given height.
30
+ # @param _height [Integer] block height
31
+ # @return [String] 80-byte hex-encoded block header
32
+ def get_header(_height)
33
+ raise NotImplementedError, "#{self.class}#get_header not implemented"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -16,8 +16,10 @@ module BSV
16
16
  def initialize(root_key)
17
17
  @root_key = if root_key == 'anyone'
18
18
  BSV::Primitives::PrivateKey.new(ANYONE_BN)
19
- else
19
+ elsif root_key.is_a?(BSV::Primitives::PrivateKey)
20
20
  root_key
21
+ else
22
+ raise ArgumentError, "expected a BSV::Primitives::PrivateKey or 'anyone', got #{root_key.class}"
21
23
  end
22
24
  end
23
25
 
@@ -55,10 +55,11 @@ module BSV
55
55
  end
56
56
 
57
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)
58
+ apply_pagination(filter_certificates(query), query)
59
+ end
60
+
61
+ def count_certificates(query)
62
+ filter_certificates(query).length
62
63
  end
63
64
 
64
65
  def delete_certificate(type:, serial_number:, certifier:)
@@ -105,6 +106,20 @@ module BSV
105
106
  query[:include_spent] ? results : results.reject { |o| o[:spendable] == false }
106
107
  end
107
108
 
109
+ def filter_certificates(query)
110
+ results = @certificates
111
+ results = results.select { |c| query[:certifiers].include?(c[:certifier]) } if query[:certifiers]
112
+ results = results.select { |c| query[:types].include?(c[:type]) } if query[:types]
113
+ results = results.select { |c| c[:subject] == query[:subject] } if query[:subject]
114
+ if query[:attributes]
115
+ results = results.select do |c|
116
+ fields = c[:fields] || {}
117
+ query[:attributes].all? { |k, v| fields[k] == v || fields[k.to_sym] == v }
118
+ end
119
+ end
120
+ results
121
+ end
122
+
108
123
  def apply_pagination(results, query)
109
124
  offset = query[:offset] || 0
110
125
  limit = query[:limit] || 10
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Default chain provider that raises for all blockchain queries.
6
+ #
7
+ # Used when a WalletClient is constructed without a chain provider,
8
+ # allowing the wallet to function for transaction and crypto operations
9
+ # without requiring a blockchain connection.
10
+ class NullChainProvider
11
+ include ChainProvider
12
+
13
+ def get_height
14
+ raise UnsupportedActionError, 'get_height (no chain provider configured)'
15
+ end
16
+
17
+ def get_header(_height)
18
+ raise UnsupportedActionError, 'get_header_for_height (no chain provider configured)'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -46,6 +46,10 @@ module BSV
46
46
  def count_outputs(_query)
47
47
  raise NotImplementedError, "#{self.class}#count_outputs not implemented"
48
48
  end
49
+
50
+ def count_certificates(_query)
51
+ raise NotImplementedError, "#{self.class}#count_certificates not implemented"
52
+ end
49
53
  end
50
54
  end
51
55
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BSV
4
4
  module WalletInterface
5
- VERSION = '0.1.0'
5
+ VERSION = '0.1.1'
6
6
  end
7
7
  end
@@ -26,11 +26,21 @@ module BSV
26
26
  # @return [StorageAdapter] the underlying persistence adapter
27
27
  attr_reader :storage
28
28
 
29
+ # @return [ChainProvider] the blockchain data provider
30
+ attr_reader :chain_provider
31
+
32
+ # @return [String] the network ('mainnet' or 'testnet')
33
+ attr_reader :network
34
+
29
35
  # @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
30
36
  # @param storage [StorageAdapter] persistence adapter (default: MemoryStore)
31
- def initialize(key, storage: MemoryStore.new)
37
+ # @param network [String] 'mainnet' (default) or 'testnet'
38
+ # @param chain_provider [ChainProvider] blockchain data provider (default: NullChainProvider)
39
+ def initialize(key, storage: MemoryStore.new, network: 'mainnet', chain_provider: NullChainProvider.new)
32
40
  super(key)
33
41
  @storage = storage
42
+ @network = network
43
+ @chain_provider = chain_provider
34
44
  @pending = {}
35
45
  end
36
46
 
@@ -166,6 +176,224 @@ module BSV
166
176
  { accepted: true }
167
177
  end
168
178
 
179
+ # --- Blockchain & Network Data ---
180
+
181
+ # Returns the current blockchain height from the chain provider.
182
+ #
183
+ # @param _args [Hash] unused (empty hash)
184
+ # @return [Hash] { height: Integer }
185
+ def get_height(_args = {}, _originator: nil)
186
+ { height: @chain_provider.get_height }
187
+ end
188
+
189
+ # Returns the block header at the given height from the chain provider.
190
+ #
191
+ # @param args [Hash]
192
+ # @option args [Integer] :height block height
193
+ # @return [Hash] { header: String } 80-byte hex-encoded block header
194
+ def get_header_for_height(args, _originator: nil)
195
+ raise InvalidParameterError.new('height', 'a positive Integer') unless args[:height].is_a?(Integer) && args[:height].positive?
196
+
197
+ { header: @chain_provider.get_header(args[:height]) }
198
+ end
199
+
200
+ # Returns the network this wallet is configured for.
201
+ #
202
+ # @param _args [Hash] unused (empty hash)
203
+ # @return [Hash] { network: String } 'mainnet' or 'testnet'
204
+ def get_network(_args = {}, _originator: nil)
205
+ { network: @network }
206
+ end
207
+
208
+ # Returns the wallet version string.
209
+ #
210
+ # @param _args [Hash] unused (empty hash)
211
+ # @return [Hash] { version: String } in vendor-major.minor.patch format
212
+ def get_version(_args = {}, _originator: nil)
213
+ { version: "bsv-wallet-#{BSV::WalletInterface::VERSION}" }
214
+ end
215
+
216
+ # --- Authentication ---
217
+
218
+ # Checks whether the user is authenticated.
219
+ # For local wallets with a private key, this is always true.
220
+ #
221
+ # @param _args [Hash] unused (empty hash)
222
+ # @return [Hash] { authenticated: Boolean }
223
+ def is_authenticated(_args = {}, _originator: nil)
224
+ { authenticated: true }
225
+ end
226
+
227
+ # Waits until the user is authenticated.
228
+ # For local wallets, returns immediately.
229
+ #
230
+ # @param _args [Hash] unused (empty hash)
231
+ # @return [Hash] { authenticated: true }
232
+ def wait_for_authentication(_args = {}, _originator: nil)
233
+ { authenticated: true }
234
+ end
235
+
236
+ # --- Identity and Certificate Management ---
237
+
238
+ # Acquires an identity certificate via direct storage.
239
+ #
240
+ # The 'issuance' protocol (which requires HTTP to a certifier URL) is
241
+ # not yet supported and raises {UnsupportedActionError}.
242
+ #
243
+ # @param args [Hash]
244
+ # @option args [String] :type certificate type (base64)
245
+ # @option args [String] :certifier certifier public key hex
246
+ # @option args [String] :acquisition_protocol 'direct' or 'issuance'
247
+ # @option args [Hash] :fields certificate fields (field_name => value)
248
+ # @option args [String] :serial_number serial number (required for direct)
249
+ # @option args [String] :revocation_outpoint outpoint string (required for direct)
250
+ # @option args [String] :signature certifier signature hex (required for direct)
251
+ # @option args [String] :keyring_revealer pubkey hex or 'certifier' (required for direct)
252
+ # @option args [Hash] :keyring_for_subject field_name => base64 key (required for direct)
253
+ # @return [Hash] the stored certificate
254
+ def acquire_certificate(args, _originator: nil)
255
+ validate_acquire_certificate!(args)
256
+
257
+ raise UnsupportedActionError, 'acquire_certificate with issuance protocol' if args[:acquisition_protocol] == 'issuance'
258
+
259
+ cert = {
260
+ type: args[:type],
261
+ subject: @key_deriver.identity_key,
262
+ serial_number: args[:serial_number],
263
+ certifier: args[:certifier],
264
+ revocation_outpoint: args[:revocation_outpoint],
265
+ signature: args[:signature],
266
+ fields: args[:fields],
267
+ keyring: args[:keyring_for_subject]
268
+ }
269
+
270
+ @storage.store_certificate(cert)
271
+ cert_without_keyring(cert)
272
+ end
273
+
274
+ # Lists identity certificates filtered by certifier and type.
275
+ #
276
+ # @param args [Hash]
277
+ # @option args [Array<String>] :certifiers certifier public keys
278
+ # @option args [Array<String>] :types certificate types
279
+ # @option args [Integer] :limit max results (default 10)
280
+ # @option args [Integer] :offset number to skip (default 0)
281
+ # @return [Hash] { total_certificates:, certificates: [...] }
282
+ def list_certificates(args, _originator: nil)
283
+ raise InvalidParameterError.new('certifiers', 'a non-empty Array') unless args[:certifiers].is_a?(Array) && !args[:certifiers].empty?
284
+ raise InvalidParameterError.new('types', 'a non-empty Array') unless args[:types].is_a?(Array) && !args[:types].empty?
285
+
286
+ query = {
287
+ certifiers: args[:certifiers],
288
+ types: args[:types],
289
+ limit: args[:limit] || 10,
290
+ offset: args[:offset] || 0
291
+ }
292
+ total = @storage.count_certificates(query)
293
+ certs = @storage.find_certificates(query)
294
+ { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
295
+ end
296
+
297
+ # Proves select fields of an identity certificate to a verifier.
298
+ #
299
+ # Encrypts each requested field's keyring entry for the verifier using
300
+ # protocol-derived encryption (BRC-2), allowing the verifier to decrypt
301
+ # only the revealed fields.
302
+ #
303
+ # @param args [Hash]
304
+ # @option args [Hash] :certificate the certificate to prove
305
+ # @option args [Array<String>] :fields_to_reveal field names to reveal
306
+ # @option args [String] :verifier verifier public key hex
307
+ # @return [Hash] { keyring_for_verifier: { field_name => Array<Integer> } }
308
+ def prove_certificate(args, _originator: nil)
309
+ cert_arg = args[:certificate]
310
+ fields_to_reveal = args[:fields_to_reveal]
311
+ verifier = args[:verifier]
312
+
313
+ raise InvalidParameterError.new('certificate', 'a Hash') unless cert_arg.is_a?(Hash)
314
+ raise InvalidParameterError.new('fields_to_reveal', 'a non-empty Array') unless fields_to_reveal.is_a?(Array) && !fields_to_reveal.empty?
315
+
316
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
317
+
318
+ # Look up the full certificate (with keyring) from storage
319
+ stored = find_stored_certificate(cert_arg)
320
+ raise WalletError, 'Certificate not found in wallet' unless stored
321
+ raise WalletError, 'Certificate has no keyring' unless stored[:keyring]
322
+
323
+ keyring_for_verifier = {}
324
+ fields_to_reveal.each do |field_name|
325
+ key_value = stored[:keyring][field_name] || stored[:keyring][field_name.to_sym]
326
+ raise WalletError, "Keyring entry not found for field '#{field_name}'" unless key_value
327
+
328
+ # Encrypt the keyring entry for the verifier
329
+ encrypted = encrypt({
330
+ plaintext: key_value.bytes,
331
+ protocol_id: [2, 'certificate field revelation'],
332
+ key_id: "#{cert_arg[:type]} #{cert_arg[:serial_number]} #{field_name}",
333
+ counterparty: verifier
334
+ })
335
+ keyring_for_verifier[field_name] = encrypted[:ciphertext]
336
+ end
337
+
338
+ { keyring_for_verifier: keyring_for_verifier }
339
+ end
340
+
341
+ # Removes a certificate from the wallet.
342
+ #
343
+ # @param args [Hash]
344
+ # @option args [String] :type certificate type
345
+ # @option args [String] :serial_number serial number
346
+ # @option args [String] :certifier certifier public key hex
347
+ # @return [Hash] { relinquished: true }
348
+ def relinquish_certificate(args, _originator: nil)
349
+ deleted = @storage.delete_certificate(
350
+ type: args[:type],
351
+ serial_number: args[:serial_number],
352
+ certifier: args[:certifier]
353
+ )
354
+ raise WalletError, 'Certificate not found' unless deleted
355
+
356
+ { relinquished: true }
357
+ end
358
+
359
+ # Discovers certificates issued to a given identity key.
360
+ #
361
+ # For a local wallet, searches stored certificates where the subject
362
+ # matches the given identity key.
363
+ #
364
+ # @param args [Hash]
365
+ # @option args [String] :identity_key public key hex to search
366
+ # @option args [Integer] :limit max results (default 10)
367
+ # @option args [Integer] :offset number to skip (default 0)
368
+ # @return [Hash] { total_certificates:, certificates: [...] }
369
+ def discover_by_identity_key(args, _originator: nil)
370
+ Validators.validate_pub_key_hex!(args[:identity_key], 'identity_key')
371
+
372
+ query = { subject: args[:identity_key], limit: args[:limit] || 10, offset: args[:offset] || 0 }
373
+ total = @storage.count_certificates(query)
374
+ certs = @storage.find_certificates(query)
375
+ { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
376
+ end
377
+
378
+ # Discovers certificates matching specific attribute values.
379
+ #
380
+ # Searches stored certificates where field values match the given
381
+ # attributes. Only searches certificates belonging to this wallet.
382
+ #
383
+ # @param args [Hash]
384
+ # @option args [Hash] :attributes field_name => value pairs to match
385
+ # @option args [Integer] :limit max results (default 10)
386
+ # @option args [Integer] :offset number to skip (default 0)
387
+ # @return [Hash] { total_certificates:, certificates: [...] }
388
+ def discover_by_attributes(args, _originator: nil)
389
+ raise InvalidParameterError.new('attributes', 'a non-empty Hash') unless args[:attributes].is_a?(Hash) && !args[:attributes].empty?
390
+
391
+ query = { attributes: args[:attributes], limit: args[:limit] || 10, offset: args[:offset] || 0 }
392
+ total = @storage.count_certificates(query)
393
+ certs = @storage.find_certificates(query)
394
+ { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
395
+ end
396
+
169
397
  private
170
398
 
171
399
  # --- Validation ---
@@ -481,6 +709,40 @@ module BSV
481
709
  spendable: true
482
710
  })
483
711
  end
712
+
713
+ # --- Certificate helpers ---
714
+
715
+ def validate_acquire_certificate!(args)
716
+ raise InvalidParameterError.new('type', 'a String') unless args[:type].is_a?(String)
717
+
718
+ Validators.validate_pub_key_hex!(args[:certifier], 'certifier')
719
+ raise InvalidParameterError.new('fields', 'a Hash') unless args[:fields].is_a?(Hash)
720
+
721
+ protocol = args[:acquisition_protocol]
722
+ raise InvalidParameterError.new('acquisition_protocol', '"direct" or "issuance"') unless %w[direct issuance].include?(protocol)
723
+
724
+ return unless protocol == 'direct'
725
+
726
+ raise InvalidParameterError.new('serial_number', 'present for direct acquisition') unless args[:serial_number]
727
+ raise InvalidParameterError.new('revocation_outpoint', 'present for direct acquisition') unless args[:revocation_outpoint]
728
+ raise InvalidParameterError.new('signature', 'present for direct acquisition') unless args[:signature]
729
+ raise InvalidParameterError.new('keyring_for_subject', 'a Hash for direct acquisition') unless args[:keyring_for_subject].is_a?(Hash)
730
+ end
731
+
732
+ def find_stored_certificate(cert_arg)
733
+ results = @storage.find_certificates({
734
+ certifiers: [cert_arg[:certifier]],
735
+ types: [cert_arg[:type]],
736
+ limit: 10_000
737
+ })
738
+ results.find { |c| c[:serial_number] == cert_arg[:serial_number] }
739
+ end
740
+
741
+ def cert_without_keyring(cert)
742
+ result = cert.dup
743
+ result.delete(:keyring)
744
+ result
745
+ end
484
746
  end
485
747
  end
486
748
  end
@@ -11,9 +11,11 @@ module BSV
11
11
  autoload :KeyDeriver, 'bsv/wallet_interface/key_deriver'
12
12
  autoload :ProtoWallet, 'bsv/wallet_interface/proto_wallet'
13
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'
14
+ autoload :StorageAdapter, 'bsv/wallet_interface/storage_adapter'
15
+ autoload :MemoryStore, 'bsv/wallet_interface/memory_store'
16
+ autoload :ChainProvider, 'bsv/wallet_interface/chain_provider'
17
+ autoload :NullChainProvider, 'bsv/wallet_interface/null_chain_provider'
18
+ autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
17
19
 
18
20
  # Error classes
19
21
  autoload :WalletError, 'bsv/wallet_interface/errors/wallet_error'
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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
@@ -91,6 +91,7 @@ files:
91
91
  - lib/bsv/wallet/insufficient_funds_error.rb
92
92
  - lib/bsv/wallet/wallet.rb
93
93
  - lib/bsv/wallet_interface.rb
94
+ - lib/bsv/wallet_interface/chain_provider.rb
94
95
  - lib/bsv/wallet_interface/errors/invalid_hmac_error.rb
95
96
  - lib/bsv/wallet_interface/errors/invalid_parameter_error.rb
96
97
  - lib/bsv/wallet_interface/errors/invalid_signature_error.rb
@@ -99,6 +100,7 @@ files:
99
100
  - lib/bsv/wallet_interface/interface.rb
100
101
  - lib/bsv/wallet_interface/key_deriver.rb
101
102
  - lib/bsv/wallet_interface/memory_store.rb
103
+ - lib/bsv/wallet_interface/null_chain_provider.rb
102
104
  - lib/bsv/wallet_interface/proto_wallet.rb
103
105
  - lib/bsv/wallet_interface/storage_adapter.rb
104
106
  - lib/bsv/wallet_interface/validators.rb