Class: BSV::Wallet::WalletClient

Inherits:
ProtoWallet show all
Defined in:
lib/bsv/wallet_interface/wallet_client.rb

Overview

BRC-100 transaction operations for the wallet interface.

Implements the 7 transaction-related BRC-100 methods on top of ProtoWallet: create_action, sign_action, abort_action, list_actions, list_outputs, relinquish_output, and internalize_action.

Transactions are built using the SDK’s Transaction::Transaction class. Completed actions and tracked outputs are persisted via a StorageAdapter (defaults to MemoryStore).

Examples:

Create a simple transaction

client = BSV::Wallet::WalletClient.new(private_key)
result = client.create_action({
  description: 'Pay invoice',
  outputs: [{ locking_script: '76a914...88ac', satoshis: 1000,
              output_description: 'Payment' }]
})

Constant Summary collapse

ANCESTOR_DEPTH_CAP =

Maximum ancestor depth to traverse when wiring source transactions. Guards against stack overflow on pathologically deep or cyclic chains.

64
STALE_CHECK_INTERVAL =

Rate-limits stale pending recovery to avoid O(n) scans on every auto-fund call. Skips if called again within this interval.

30

Instance Attribute Summary collapse

Attributes inherited from ProtoWallet

#key_deriver

Instance Method Summary collapse

Constructor Details

#initialize(key, storage: FileStore.new, network: 'mainnet', chain_provider: NullChainProvider.new, proof_store: nil, http_client: nil, fee_estimator: nil, coin_selector: nil, change_generator: nil, broadcaster: nil, broadcast_queue: nil, substrate: nil) ⇒ WalletClient

Returns a new instance of WalletClient.

Parameters:

  • key (BSV::Primitives::PrivateKey, String, KeyDeriver)

    signing key

  • storage (StorageAdapter) (defaults to: FileStore.new)

    persistence adapter (default: FileStore). Use storage: MemoryStore.new for tests.

  • network (String) (defaults to: 'mainnet')

    ‘mainnet’ (default) or ‘testnet’

  • chain_provider (ChainProvider) (defaults to: NullChainProvider.new)

    blockchain data provider (default: NullChainProvider)

  • proof_store (ProofStore, nil) (defaults to: nil)

    merkle proof store (default: LocalProofStore backed by storage)

  • http_client (#request, nil) (defaults to: nil)

    injectable HTTP client for certificate issuance

  • broadcaster (#broadcast, nil) (defaults to: nil)

    optional broadcaster; any object responding to #broadcast(tx)

  • broadcast_queue (BroadcastQueue, nil) (defaults to: nil)

    optional broadcast queue; defaults to InlineQueue

  • substrate (Interface, nil) (defaults to: nil)

    optional remote wallet substrate; when set, all Interface methods delegate to the substrate instead of using local storage and key derivation. Accepts any object implementing Interface (e.g. Substrates::HTTPWalletJSON, Substrates::WalletWireTransceiver).



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 64

def initialize(
  key,
  storage: FileStore.new,
  network: 'mainnet',
  chain_provider: NullChainProvider.new,
  proof_store: nil,
  http_client: nil,
  fee_estimator: nil,
  coin_selector: nil,
  change_generator: nil,
  broadcaster: nil,
  broadcast_queue: nil,
  substrate: nil
)
  super(key)
  @substrate = substrate
  @storage = storage
  @network = network
  @chain_provider = chain_provider
  @proof_store = proof_store || LocalProofStore.new(storage)
  @http_client = http_client
  @broadcaster = broadcaster
  @pending = {}
  @pending_by_txid = {}
  @injected_fee_estimator    = fee_estimator
  @injected_coin_selector    = coin_selector
  @injected_change_generator = change_generator
  @broadcast_queue = broadcast_queue || InlineQueue.new(
    storage: @storage,
    broadcaster: @broadcaster
  )
end

Instance Attribute Details

#broadcast_queueBroadcastQueue (readonly)

Returns the broadcast queue used to dispatch transactions.

Returns:

  • (BroadcastQueue)

    the broadcast queue used to dispatch transactions



46
47
48
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 46

def broadcast_queue
  @broadcast_queue
end

#broadcaster#broadcast? (readonly)

Returns the optional broadcaster (responds to #broadcast(tx)).

Returns:

  • (#broadcast, nil)

    the optional broadcaster (responds to #broadcast(tx))



43
44
45
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 43

def broadcaster
  @broadcaster
end

#chain_providerChainProvider (readonly)

Returns the blockchain data provider.

Returns:



34
35
36
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 34

def chain_provider
  @chain_provider
end

#networkString (readonly)

Returns the network (‘mainnet’ or ‘testnet’).

Returns:

  • (String)

    the network (‘mainnet’ or ‘testnet’)



37
38
39
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 37

def network
  @network
end

#proof_storeProofStore (readonly)

Returns the merkle proof persistence store.

Returns:

  • (ProofStore)

    the merkle proof persistence store



40
41
42
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 40

def proof_store
  @proof_store
end

#storageStorageAdapter (readonly)

Returns the underlying persistence adapter.

Returns:



31
32
33
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 31

def storage
  @storage
end

#substrateInterface? (readonly)

Returns the optional substrate for remote wallet delegation.

Returns:

  • (Interface, nil)

    the optional substrate for remote wallet delegation



49
50
51
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 49

def substrate
  @substrate
end

Instance Method Details

#abort_action(args, originator: nil) ⇒ Hash

Aborts a pending signable transaction.

If the pending entry holds UTXO references (stored by auto-fund), any outputs locked as :pending with that reference are released back to :spendable so they become eligible for future coin selection.

Parameters:

  • args (Hash)
  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :reference (String)

    base64 reference to abort

Returns:

  • (Hash)

    { aborted: true }

Raises:



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 211

def abort_action(args, originator: nil)
  return @substrate.abort_action(args, originator: originator) if @substrate

  reference = args[:reference]
  raise WalletError, 'Transaction not found for the given reference' unless @pending.key?(reference)

  pending_entry = @pending.delete(reference)
  txid = pending_entry[:tx]&.txid_hex
  @pending_by_txid.delete(txid) if txid
  rollback_pending_action(
    pending_entry[:locked_outpoints],
    pending_entry[:change_outpoints],
    txid,
    reference
  )
  { aborted: true }
end

#acquire_certificate(args, originator: nil) ⇒ Hash

Acquires an identity certificate via direct storage.

The ‘issuance’ protocol (which requires HTTP to a certifier URL) is not yet supported and raises UnsupportedActionError.

Parameters:

  • args (Hash)

Options Hash (args):

  • :type (String)

    certificate type (base64)

  • :certifier (String)

    certifier public key hex

  • :acquisition_protocol (String)

    ‘direct’ or ‘issuance’

  • :fields (Hash)

    certificate fields (field_name => value)

  • :serial_number (String)

    serial number (required for direct)

  • :revocation_outpoint (String)

    outpoint string (required for direct)

  • :signature (String)

    certifier signature hex (required for direct)

  • :keyring_revealer (String)

    pubkey hex or ‘certifier’ (required for direct)

  • :keyring_for_subject (Hash)

    field_name => base64 key (required for direct)

Returns:

  • (Hash)

    the stored certificate



507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 507

def acquire_certificate(args, originator: nil)
  return @substrate.acquire_certificate(args, originator: originator) if @substrate

  validate_acquire_certificate!(args)

  cert = if args[:acquisition_protocol] == 'issuance'
           acquire_via_issuance(args)
         else
           acquire_via_direct(args)
         end

  @storage.store_certificate(cert)
  cert_without_keyring(cert)
end

#balance(basket: nil) ⇒ Integer

Returns the total spendable satoshis across all baskets (or a named basket).

Includes every output in :spendable state — regardless of whether the wallet holds the signing key. This answers “how much does this wallet hold?”, not “how much can it auto-spend?”. Use #spendable_balance for the latter.

Parameters:

  • basket (String, nil) (defaults to: nil)

    the basket to total, or nil for all baskets

Returns:

  • (Integer)

    sum of all spendable output values



426
427
428
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 426

def balance(basket: nil)
  @storage.find_spendable_outputs(basket: basket).sum { |o| o[:satoshis].to_i }
end

#broadcast_enabled?Boolean

Returns true when broadcast is available.

Delegates to the broadcast queue so that queue-embedded broadcasters (e.g. SolidQueueAdapter.new(broadcaster: arc)) are recognised even when no broadcaster: was passed directly to WalletClient.

Returns:

  • (Boolean)


102
103
104
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 102

def broadcast_enabled?
  @broadcast_queue.broadcast_enabled?
end

#create_action(args, originator: nil) ⇒ Hash

Creates a new Bitcoin transaction.

When the :auto_fund option is true (and :outputs are provided but no :inputs), the wallet automatically selects UTXOs from the default basket, estimates fees, generates change, and returns a complete signed transaction (auto-fund mode).

Without :auto_fund, if all inputs carry unlocking_script values, the transaction is finalised immediately and returned with :txid and :tx (BEEF bytes). If any input specifies only unlocking_script_length, the transaction is held pending and returned as a signable_transaction for external signing via #sign_action.

Parameters:

  • args (Hash)

    transaction parameters

  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :auto_fund (Boolean)

    when true, automatically selects UTXOs and generates change; requires :outputs and no :inputs

Returns:

  • (Hash)

    finalised result or signable_transaction



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 126

def create_action(args, originator: nil)
  return @substrate.create_action(args, originator: originator) if @substrate

  validate_create_action!(args)
  validate_broadcast_configuration!(args)

  send_with_txids = Array(args.dig(:options, :send_with))

  outputs = args[:outputs] || []
  inputs  = args[:inputs]

  # When send_with is provided but no new transaction body is specified,
  # broadcast only the batched no_send transactions.
  if !send_with_txids.empty? && outputs.empty? && (inputs.nil? || inputs.empty?)
    raise WalletError, 'A broadcaster is required to use send_with' unless broadcast_enabled?

    return { send_with_results: broadcast_send_with(send_with_txids) }
  end

  if (inputs.nil? || inputs.empty?) && !outputs.empty? && (args[:auto_fund] || spendable_pool_eligible?)
    result = auto_fund_and_create(args, outputs)
    # If send_with was also specified, batch-broadcast those alongside the
    # current transaction's implicit broadcast.
    unless send_with_txids.empty?
      raise WalletError, 'A broadcaster is required to use send_with' unless broadcast_enabled?

      result[:send_with_results] = broadcast_send_with(send_with_txids)
    end
    return result
  end

  beef = parse_input_beef(args[:input_beef])
  tx = build_transaction(args, beef)

  if needs_signing?(args[:inputs])
    create_signable(tx, args, beef)
  else
    finalize_action(tx, args)
  end
end

#create_hmac(args, originator: nil) ⇒ Object



690
691
692
693
694
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 690

def create_hmac(args, originator: nil)
  return @substrate.create_hmac(args, originator: originator) if @substrate

  super
end

#create_signature(args, originator: nil) ⇒ Object



702
703
704
705
706
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 702

def create_signature(args, originator: nil)
  return @substrate.create_signature(args, originator: originator) if @substrate

  super
end

#decrypt(args, originator: nil) ⇒ Object



684
685
686
687
688
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 684

def decrypt(args, originator: nil)
  return @substrate.decrypt(args, originator: originator) if @substrate

  super
end

#discover_by_attributes(args, originator: nil) ⇒ Hash

Discovers certificates matching specific attribute values.

Searches stored certificates where field values match the given attributes. Only searches certificates belonging to this wallet.

Parameters:

  • args (Hash)

Options Hash (args):

  • :attributes (Hash)

    field_name => value pairs to match

  • :limit (Integer)

    max results (default 10)

  • :offset (Integer)

    number to skip (default 0)

Returns:

  • (Hash)

    { total_certificates:, certificates: […] }

Raises:



644
645
646
647
648
649
650
651
652
653
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 644

def discover_by_attributes(args, originator: nil)
  return @substrate.discover_by_attributes(args, originator: originator) if @substrate

  raise InvalidParameterError.new('attributes', 'a non-empty Hash') unless args[:attributes].is_a?(Hash) && !args[:attributes].empty?

  query = { attributes: args[:attributes], limit: args[:limit] || 10, offset: args[:offset] || 0 }
  total = @storage.count_certificates(query)
  certs = @storage.find_certificates(query)
  { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
end

#discover_by_identity_key(args, originator: nil) ⇒ Hash

Discovers certificates issued to a given identity key.

For a local wallet, searches stored certificates where the subject matches the given identity key.

Parameters:

  • args (Hash)

Options Hash (args):

  • :identity_key (String)

    public key hex to search

  • :limit (Integer)

    max results (default 10)

  • :offset (Integer)

    number to skip (default 0)

Returns:

  • (Hash)

    { total_certificates:, certificates: […] }



623
624
625
626
627
628
629
630
631
632
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 623

def discover_by_identity_key(args, originator: nil)
  return @substrate.discover_by_identity_key(args, originator: originator) if @substrate

  Validators.validate_pub_key_hex!(args[:identity_key], 'identity_key')

  query = { subject: args[:identity_key], limit: args[:limit] || 10, offset: args[:offset] || 0 }
  total = @storage.count_certificates(query)
  certs = @storage.find_certificates(query)
  { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
end

#encrypt(args, originator: nil) ⇒ Object



678
679
680
681
682
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 678

def encrypt(args, originator: nil)
  return @substrate.encrypt(args, originator: originator) if @substrate

  super
end

#get_header_for_height(args, originator: nil) ⇒ Hash

Returns the block header at the given height from the chain provider.

Parameters:

  • args (Hash)

Options Hash (args):

  • :height (Integer)

    block height

Returns:

  • (Hash)

    { header: String } 80-byte hex-encoded block header

Raises:



338
339
340
341
342
343
344
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 338

def get_header_for_height(args, originator: nil)
  return @substrate.get_header_for_height(args, originator: originator) if @substrate

  raise InvalidParameterError.new('height', 'a positive Integer') unless args[:height].is_a?(Integer) && args[:height].positive?

  { header: @chain_provider.get_header(args[:height]) }
end

#get_height(args = {}, originator: nil) ⇒ Hash

Returns the current blockchain height from the chain provider.

Parameters:

  • _args (Hash)

    unused (empty hash)

Returns:

  • (Hash)

    { height: Integer }



327
328
329
330
331
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 327

def get_height(args = {}, originator: nil)
  return @substrate.get_height(args, originator: originator) if @substrate

  { height: @chain_provider.get_height }
end

#get_network(args = {}, originator: nil) ⇒ Hash

Returns the network this wallet is configured for.

Parameters:

  • _args (Hash)

    unused (empty hash)

Returns:

  • (Hash)

    { network: String } ‘mainnet’ or ‘testnet’



350
351
352
353
354
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 350

def get_network(args = {}, originator: nil)
  return @substrate.get_network(args, originator: originator) if @substrate

  { network: @network }
end

#get_public_key(args, originator: nil) ⇒ Object

— ProtoWallet crypto method overrides for substrate delegation —

When a substrate is configured, these methods delegate to it rather than performing local key derivation and cryptographic operations.



660
661
662
663
664
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 660

def get_public_key(args, originator: nil)
  return @substrate.get_public_key(args, originator: originator) if @substrate

  super
end

#get_version(args = {}, originator: nil) ⇒ Hash

Returns the wallet version string.

Parameters:

  • _args (Hash)

    unused (empty hash)

Returns:

  • (Hash)

    { version: String } in vendor-major.minor.patch format



360
361
362
363
364
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 360

def get_version(args = {}, originator: nil)
  return @substrate.get_version(args, originator: originator) if @substrate

  { version: "bsv-wallet-#{BSV::Wallet::VERSION}" }
end

#internalize_action(args, originator: nil) ⇒ Hash

Accepts an incoming transaction for wallet internalization.

Parses the BEEF, locates the subject transaction, processes each specified output according to its protocol (wallet payment or basket insertion), and stores the action.

Parameters:

  • args (Hash)
  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :tx (Array<Integer>)

    Atomic BEEF-formatted transaction as byte array

  • :outputs (Array<Hash>)

    output metadata

  • :description (String)

    5-50 char description

  • :labels (Array<String>)

    optional labels

Returns:

  • (Hash)

    { accepted: true }

Raises:



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 298

def internalize_action(args, originator: nil)
  return @substrate.internalize_action(args, originator: originator) if @substrate

  validate_internalize_action!(args)
  beef_binary = args[:tx].pack('C*')
  beef = BSV::Transaction::Beef.from_binary(beef_binary)

  # F8.14: verify the BEEF bundle before trusting its contents.
  # Pass the chain provider if it supports SPV root verification;
  # otherwise fall back to structural validation via valid?.
  chain_tracker = @chain_provider.respond_to?(:valid_root_for_height?) ? @chain_provider : nil
  raise WalletError, 'BEEF verification failed: the bundle is structurally invalid' unless beef.verify(chain_tracker)

  tx = extract_subject_transaction(beef)

  store_proofs_from_beef(beef)
  @storage.store_transaction(tx.txid_hex, tx.to_hex)
  process_internalize_outputs(tx, args[:outputs])
  has_proof = !beef.find_bump(tx.txid).nil?
  store_action(tx, args, status: has_proof ? 'completed' : 'unproven')
  { accepted: true }
end

#is_authenticated(args = {}, originator: nil) ⇒ Hash

Checks whether the user is authenticated. For local wallets with a private key, this is always true.

Parameters:

  • _args (Hash)

    unused (empty hash)

Returns:

  • (Hash)

    { authenticated: Boolean }



472
473
474
475
476
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 472

def is_authenticated(args = {}, originator: nil)
  return @substrate.is_authenticated(args, originator: originator) if @substrate

  { authenticated: true }
end

#list_actions(args, originator: nil) ⇒ Hash

Lists stored actions matching the given labels.

Parameters:

  • args (Hash)
  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :labels (Array<String>) — default: required

    labels to filter by

  • :label_query_mode (String)

    ‘any’ (default) or ‘all’

  • :limit (Integer)

    max results (default 10)

  • :offset (Integer)

    results to skip (default 0)

Returns:

  • (Hash)

    { total_actions: Integer, actions: Array }



238
239
240
241
242
243
244
245
246
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 238

def list_actions(args, originator: nil)
  return @substrate.list_actions(args, originator: originator) if @substrate

  validate_list_actions!(args)
  query = build_action_query(args)
  total = @storage.count_actions(query)
  actions = @storage.find_actions(query)
  { total_actions: total, actions: strip_action_fields(actions, args) }
end

#list_certificates(args, originator: nil) ⇒ Hash

Lists identity certificates filtered by certifier and type.

Parameters:

  • args (Hash)

Options Hash (args):

  • :certifiers (Array<String>)

    certifier public keys

  • :types (Array<String>)

    certificate types

  • :limit (Integer)

    max results (default 10)

  • :offset (Integer)

    number to skip (default 0)

Returns:

  • (Hash)

    { total_certificates:, certificates: […] }

Raises:



530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 530

def list_certificates(args, originator: nil)
  return @substrate.list_certificates(args, originator: originator) if @substrate

  raise InvalidParameterError.new('certifiers', 'a non-empty Array') unless args[:certifiers].is_a?(Array) && !args[:certifiers].empty?
  raise InvalidParameterError.new('types', 'a non-empty Array') unless args[:types].is_a?(Array) && !args[:types].empty?

  query = {
    certifiers: args[:certifiers],
    types: args[:types],
    limit: args[:limit] || 10,
    offset: args[:offset] || 0
  }
  total = @storage.count_certificates(query)
  certs = @storage.find_certificates(query)
  { total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
end

#list_outputs(args, originator: nil) ⇒ Hash

Lists spendable outputs in a basket.

Parameters:

  • args (Hash)
  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :basket (String) — default: required

    basket name

  • :tags (Array<String>)

    optional tag filter

  • :tag_query_mode (String)

    ‘any’ (default) or ‘all’

  • :limit (Integer)

    max results (default 10)

  • :offset (Integer)

    results to skip (default 0)

Returns:

  • (Hash)

    { total_outputs: Integer, outputs: Array }



258
259
260
261
262
263
264
265
266
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 258

def list_outputs(args, originator: nil)
  return @substrate.list_outputs(args, originator: originator) if @substrate

  validate_list_outputs!(args)
  query = build_output_query(args)
  total = @storage.count_outputs(query)
  outputs = @storage.find_outputs(query)
  { total_outputs: total, outputs: strip_output_fields(outputs, args) }
end

#prove_certificate(args, originator: nil) ⇒ Hash

Proves select fields of an identity certificate to a verifier.

Encrypts each requested field’s keyring entry for the verifier using protocol-derived encryption (BRC-2), allowing the verifier to decrypt only the revealed fields.

Parameters:

  • args (Hash)

Options Hash (args):

  • :certificate (Hash)

    the certificate to prove

  • :fields_to_reveal (Array<String>)

    field names to reveal

  • :verifier (String)

    verifier public key hex

Returns:

  • (Hash)

    { keyring_for_verifier: { field_name => Array<Integer> } }

Raises:



558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 558

def prove_certificate(args, originator: nil)
  return @substrate.prove_certificate(args, originator: originator) if @substrate

  cert_arg = args[:certificate]
  fields_to_reveal = args[:fields_to_reveal]
  verifier = args[:verifier]

  raise InvalidParameterError.new('certificate', 'a Hash') unless cert_arg.is_a?(Hash)
  raise InvalidParameterError.new('fields_to_reveal', 'a non-empty Array') unless fields_to_reveal.is_a?(Array) && !fields_to_reveal.empty?

  Validators.validate_pub_key_hex!(verifier, 'verifier')

  # Look up the full certificate (with keyring) from storage
  stored = find_stored_certificate(cert_arg)
  raise WalletError, 'Certificate not found in wallet' unless stored
  raise WalletError, 'Certificate has no keyring' unless stored[:keyring]

  keyring_for_verifier = {}
  fields_to_reveal.each do |field_name|
    key_value = stored[:keyring][field_name] || stored[:keyring][field_name.to_sym]
    raise WalletError, "Keyring entry not found for field '#{field_name}'" unless key_value

    # Encrypt the keyring entry for the verifier
    encrypted = encrypt({
                          plaintext: key_value.bytes,
                          protocol_id: [2, 'certificate field encryption'],
                          key_id: "#{cert_arg[:serial_number]} #{field_name}",
                          counterparty: verifier
                        })
    keyring_for_verifier[field_name] = encrypted[:ciphertext]
  end

  { keyring_for_verifier: keyring_for_verifier }
end

#relinquish_certificate(args, originator: nil) ⇒ Hash

Removes a certificate from the wallet.

Parameters:

  • args (Hash)

Options Hash (args):

  • :type (String)

    certificate type

  • :serial_number (String)

    serial number

  • :certifier (String)

    certifier public key hex

Returns:

  • (Hash)

    { relinquished: true }

Raises:



600
601
602
603
604
605
606
607
608
609
610
611
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 600

def relinquish_certificate(args, originator: nil)
  return @substrate.relinquish_certificate(args, originator: originator) if @substrate

  deleted = @storage.delete_certificate(
    type: args[:type],
    serial_number: args[:serial_number],
    certifier: args[:certifier]
  )
  raise WalletError, 'Certificate not found' unless deleted

  { relinquished: true }
end

#relinquish_output(args, originator: nil) ⇒ Hash

Removes an output from basket tracking.

Parameters:

  • args (Hash)
  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :basket (String)

    basket name

  • :output (String)

    outpoint string

Returns:

  • (Hash)

    { relinquished: true }

Raises:



275
276
277
278
279
280
281
282
283
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 275

def relinquish_output(args, originator: nil)
  return @substrate.relinquish_output(args, originator: originator) if @substrate

  Validators.validate_basket!(args[:basket])
  Validators.validate_outpoint!(args[:output])
  raise WalletError, 'Output not found' unless @storage.delete_output(args[:output])

  { relinquished: true }
end

#reveal_counterparty_key_linkage(args, originator: nil) ⇒ Object



666
667
668
669
670
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 666

def reveal_counterparty_key_linkage(args, originator: nil)
  return @substrate.reveal_counterparty_key_linkage(args, originator: originator) if @substrate

  super
end

#reveal_specific_key_linkage(args, originator: nil) ⇒ Object



672
673
674
675
676
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 672

def reveal_specific_key_linkage(args, originator: nil)
  return @substrate.reveal_specific_key_linkage(args, originator: originator) if @substrate

  super
end

#set_wallet_change_params(count:, satoshis:) ⇒ Object

Configures the target UTXO pool parameters for change generation.

When set, the auto-fund path will use these parameters to decide how many change outputs to produce:

- If the pool is below +:count+, more change outputs are generated
  (up to +max_outputs+) to build up the UTXO pool.
- If the pool is at or above +:count+, fewer outputs are generated
  (1-2) to avoid fragmenting the pool unnecessarily.

Parameters:

  • count (Integer)

    desired number of spendable UTXOs in ‘default’ basket

  • satoshis (Integer)

    desired average value per UTXO in satoshis

Raises:



458
459
460
461
462
463
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 458

def set_wallet_change_params(count:, satoshis:)
  raise InvalidParameterError.new('count', 'a positive Integer') unless count.is_a?(Integer) && count.positive?
  raise InvalidParameterError.new('satoshis', 'a positive Integer') unless satoshis.is_a?(Integer) && satoshis.positive?

  @storage.store_setting('change_params', { count: count, satoshis: satoshis })
end

#sign_action(args, originator: nil) ⇒ Hash

Signs a previously created signable transaction.

Parameters:

  • args (Hash)
  • _originator (String, nil)

    FQDN of the originating application

Options Hash (args):

  • :spends (Hash)

    map of input index (Integer or String) to { unlocking_script: hex, sequence_number: Integer }

  • :reference (String)

    base64 reference from create_action

Returns:

  • (Hash)

    with :txid and :tx (BEEF bytes)

Raises:



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 175

def sign_action(args, originator: nil)
  return @substrate.sign_action(args, originator: originator) if @substrate

  reference = args[:reference]
  pending = @pending[reference]
  raise WalletError, 'Transaction not found for the given reference' unless pending

  tx = pending[:tx]
  apply_spends(tx, args[:spends])
  @pending.delete(reference)

  # Merge sign_action's own options over the original create_action args so
  # callers can supply accept_delayed_broadcast at sign time.
  merged_args = if args[:options]
                  pending[:args].merge(options: (pending[:args][:options] || {}).merge(args[:options]))
                else
                  pending[:args]
                end

  # Re-validate broadcast configuration on merged args — options may have
  # been flipped between create_action and sign_action (e.g. no_send toggled).
  validate_broadcast_configuration!(merged_args)

  finalize_action(tx, merged_args)
end

#spendable_balance(basket: nil) ⇒ Integer

Returns the total satoshis of outputs the wallet can automatically spend.

Only outputs carrying full BRC-29 derivation metadata (derivation_prefix, derivation_suffix, sender_identity_key) are counted — these are the outputs the wallet can sign without external input. Basket-only outputs (e.g. tokens without derivation data) contribute to #balance but not here.

Parameters:

  • basket (String, nil) (defaults to: nil)

    restrict to a named basket, or nil for all

Returns:

  • (Integer)

    total auto-spendable satoshis



440
441
442
443
444
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 440

def spendable_balance(basket: nil)
  @storage.find_spendable_outputs(basket: basket)
          .select { |o| (o[:derivation_prefix] && o[:derivation_suffix] && o[:sender_identity_key]) || o[:derivation_type]&.to_s == 'identity' }
          .sum { |o| o[:satoshis].to_i }
end

#sync_utxosInteger

Discovers on-chain UTXOs for the wallet’s identity address and imports any that are not already present in storage.

Each imported output is stored in the ‘default’ basket with state :spendable and derivation_type: :identity. The :identity derivation type signals to the auto-fund signing path that the root private key should be used directly (these outputs lack BRC-29 derivation metadata).

The method is idempotent — calling it twice with the same UTXO set produces no duplicates.

Returns:

  • (Integer)

    number of new UTXOs imported



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 379

def sync_utxos
  address = identity_address
  utxos = @chain_provider.get_utxos(address)
  return 0 if utxos.empty?

  imported = 0
  utxos.each do |utxo|
    outpoint = "#{utxo[:tx_hash]}.#{utxo[:tx_pos]}"
    next if output_exists?(outpoint)

    tx_hex = @chain_provider.get_transaction(utxo[:tx_hash])
    tx = BSV::Transaction::Transaction.from_hex(tx_hex)

    pos = utxo[:tx_pos]
    unless pos.is_a?(Integer) && pos >= 0 && pos < tx.outputs.length
      raise WalletError, "Invalid tx_pos #{pos.inspect} for #{utxo[:tx_hash]} (#{tx.outputs.length} outputs)"
    end

    locking_script_hex = tx.outputs[pos].locking_script.to_hex

    @storage.store_output({
                            outpoint: outpoint,
                            satoshis: utxo[:value],
                            locking_script: locking_script_hex,
                            basket: 'default',
                            tags: [],
                            derivation_type: :identity,
                            state: :spendable,
                            source_tx_hex: tx_hex
                          })
    @storage.store_transaction(utxo[:tx_hash], tx_hex)
    imported += 1
  end

  imported
end

#verify_hmac(args, originator: nil) ⇒ Object



696
697
698
699
700
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 696

def verify_hmac(args, originator: nil)
  return @substrate.verify_hmac(args, originator: originator) if @substrate

  super
end

#verify_signature(args, originator: nil) ⇒ Object



708
709
710
711
712
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 708

def verify_signature(args, originator: nil)
  return @substrate.verify_signature(args, originator: originator) if @substrate

  super
end

#wait_for_authentication(args = {}, originator: nil) ⇒ Hash

Waits until the user is authenticated. For local wallets, returns immediately.

Parameters:

  • _args (Hash)

    unused (empty hash)

Returns:

  • (Hash)

    { authenticated: true }



483
484
485
486
487
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 483

def wait_for_authentication(args = {}, originator: nil)
  return @substrate.wait_for_authentication(args, originator: originator) if @substrate

  { authenticated: true }
end