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:



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 201

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



496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 496

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



415
416
417
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 415

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

#broadcast_enabled?Boolean

Returns true when a broadcaster has been configured.

Returns:

  • (Boolean)


98
99
100
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 98

def broadcast_enabled?
  !@broadcaster.nil?
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



122
123
124
125
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
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 122

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

  validate_create_action!(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



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

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

  super
end

#create_signature(args, originator: nil) ⇒ Object



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

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

  super
end

#decrypt(args, originator: nil) ⇒ Object



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

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:



633
634
635
636
637
638
639
640
641
642
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 633

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: […] }



612
613
614
615
616
617
618
619
620
621
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 612

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



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

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:



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

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 }



316
317
318
319
320
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 316

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’



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

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.



649
650
651
652
653
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 649

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



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

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:



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 288

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])
  store_action(tx, args, status: 'completed')
  { 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 }



461
462
463
464
465
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 461

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 }



228
229
230
231
232
233
234
235
236
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 228

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:



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 519

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 }



248
249
250
251
252
253
254
255
256
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 248

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:



547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 547

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:



589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 589

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:



265
266
267
268
269
270
271
272
273
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 265

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



655
656
657
658
659
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 655

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



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

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:



447
448
449
450
451
452
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 447

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:



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 170

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
  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



429
430
431
432
433
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 429

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



368
369
370
371
372
373
374
375
376
377
378
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
# File 'lib/bsv/wallet_interface/wallet_client.rb', line 368

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



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

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

  super
end

#verify_signature(args, originator: nil) ⇒ Object



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

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 }



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

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

  { authenticated: true }
end