Class: BSV::Identity::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/identity/client.rb

Overview

Client for resolving and publishing identity information on the BSV overlay network.

Wraps wallet discovery and overlay broadcasting to provide a high-level interface for the BSV Identity protocol:

All overlay dependencies (broadcaster, resolver) are injectable for testing.

Examples:

Resolve an identity

client = BSV::Identity::Client.new(wallet: my_wallet)
identities = client.resolve_by_identity_key(identity_key: pubkey_hex)

Publicly reveal certificate fields

client = BSV::Identity::Client.new(wallet: my_wallet, broadcaster: my_broadcaster)
result = client.publicly_reveal_attributes(certificate, fields_to_reveal: ['email'])

Constant Summary collapse

DEFAULT_VERIFIER =

Default certificate verifier — raises NotImplementedError because certificate verification depends on BSV::Auth::Certificate, which is a separate HLR.

lambda do |_certificate|
  raise NotImplementedError,
        'Certificate verification requires BSV::Auth::Certificate (not yet implemented)'
end

Instance Method Summary collapse

Constructor Details

#initialize(wallet:, options: nil, originator: nil, certificate_verifier: nil, broadcaster: nil, resolver: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • wallet (#discover_by_identity_key, #discover_by_attributes, #prove_certificate, #create_action, #get_network)

    BRC-100 wallet interface

  • options (ClientOptions, nil) (defaults to: nil)

    identity protocol options (default: ClientOptions::DEFAULT)

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

    optional FQDN of the originating application

  • certificate_verifier (#call, nil) (defaults to: nil)

    callable that verifies a certificate; defaults to a lambda that raises NotImplementedError

  • broadcaster (BSV::Overlay::TopicBroadcaster, nil) (defaults to: nil)

    injectable broadcaster; built from the wallet’s network preset when nil

  • resolver (BSV::Overlay::LookupResolver, nil) (defaults to: nil)

    injectable lookup resolver; built from the wallet’s network preset when nil



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/bsv/identity/client.rb', line 44

def initialize(
  wallet:,
  options: nil,
  originator: nil,
  certificate_verifier: nil,
  broadcaster: nil,
  resolver: nil
)
  @wallet               = wallet
  @options              = options || default_options
  @originator           = originator
  @certificate_verifier = certificate_verifier || DEFAULT_VERIFIER
  @broadcaster          = broadcaster
  @resolver             = resolver
end

Instance Method Details

#publicly_reveal_attributes(certificate, fields_to_reveal:) ⇒ BSV::Overlay::OverlayBroadcastResult

Publicly reveals selected fields from a certificate by creating an on-chain identity token and broadcasting it to the overlay network.

The certificate is first optionally verified via the injected verifier, then the wallet proves the selected fields to the “anyone” verifier (PrivateKey(1) public key). A PushDrop locking script is constructed from the certificate JSON plus keyring, and the resulting transaction is broadcast to tm_identity.

Parameters:

  • certificate (Hash)

    wallet certificate hash

  • fields_to_reveal (Array<String>)

    field names to include in the revelation

Returns:

Raises:

  • (ArgumentError)

    if the certificate has no fields or fields_to_reveal is empty

  • (RuntimeError)

    if certificate verification fails or create_action returns no tx



110
111
112
113
114
115
116
117
118
119
120
121
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/bsv/identity/client.rb', line 110

def publicly_reveal_attributes(certificate, fields_to_reveal:)
  fields = certificate[:fields] || certificate['fields'] || {}
  raise ArgumentError, 'Public reveal failed: Certificate has no fields to reveal!' if fields.empty?
  raise ArgumentError, 'Public reveal failed: You must reveal at least one field!' if fields_to_reveal.empty?

  verify_certificate(certificate)

  # Prove the certificate to the "anyone" verifier (PrivateKey(1) public key)
  anyone_pubkey = BSV::Script::PushDropTemplate::GENERATOR_PUBKEY_HEX
  prove_result  = @wallet.prove_certificate(
    { certificate: certificate, fields_to_reveal: fields_to_reveal, verifier: anyone_pubkey },
    originator: @originator
  )
  keyring = prove_result[:keyring_for_verifier]

  # Build the PushDrop payload with ONLY the revealed fields — never
  # broadcast the full certificate (encrypted values for unrevealed
  # fields must not be written on-chain).
  revealed_fields = fields_to_reveal.to_h do |name|
    [name, fields[name.to_s] || fields[name.to_sym]]
  end

  serial_number = certificate[:serial_number] || certificate['serial_number'] ||
                  certificate[:serialNumber] || certificate['serialNumber']
  revocation_outpoint = certificate[:revocation_outpoint] || certificate['revocation_outpoint'] ||
                        certificate[:revocationOutpoint] || certificate['revocationOutpoint']

  payload = JSON.generate(
    type: certificate[:type] || certificate['type'],
    serialNumber: serial_number,
    subject: certificate[:subject] || certificate['subject'],
    certifier: certificate[:certifier] || certificate['certifier'],
    revocationOutpoint: revocation_outpoint,
    fields: revealed_fields,
    keyring: keyring
  )

  # Construct the locking script via PushDropTemplate
  template = BSV::Script::PushDropTemplate.new(wallet: @wallet, originator: @originator)
  locking_script = template.lock(
    fields: [payload],
    protocol_id: @options.protocol_id,
    key_id: @options.key_id,
    counterparty: 'anyone'
  )

  # Create the transaction
  create_result = @wallet.create_action(
    {
      description: 'Create a new Identity Token',
      outputs: [
        {
          satoshis: @options.token_amount,
          locking_script: locking_script.to_hex,
          output_description: 'Identity Token'
        }
      ],
      options: { randomize_outputs: false }
    },
    originator: @originator
  )

  raise 'Public reveal failed: failed to create action!' if create_result[:tx].nil?

  tx = BSV::Transaction::Transaction.from_beef(create_result[:tx])
  broadcaster_for_action.broadcast(tx)
end

#resolve_by_attributes(attributes:, limit: nil, offset: nil) ⇒ Array<DisplayableIdentity>

Resolves displayable identities matching specific certificate attribute values.

Delegates to the wallet’s discover_by_attributes and maps each returned certificate through IdentityParser.parse.

Parameters:

  • attributes (Hash)

    field name/value pairs to match

  • limit (Integer, nil) (defaults to: nil)

    maximum number of certificates to return

  • offset (Integer, nil) (defaults to: nil)

    number of certificates to skip

Returns:



87
88
89
90
91
92
93
94
# File 'lib/bsv/identity/client.rb', line 87

def resolve_by_attributes(attributes:, limit: nil, offset: nil)
  args = { attributes: attributes }
  args[:limit]  = limit  unless limit.nil?
  args[:offset] = offset unless offset.nil?

  result = @wallet.discover_by_attributes(args, originator: @originator)
  parse_certificates(result)
end

#resolve_by_identity_key(identity_key:, limit: nil, offset: nil) ⇒ Array<DisplayableIdentity>

Resolves displayable identities issued to a given identity key.

Delegates to the wallet’s discover_by_identity_key and maps each returned certificate through IdentityParser.parse.

Parameters:

  • identity_key (String)

    compressed public key hex

  • limit (Integer, nil) (defaults to: nil)

    maximum number of certificates to return

  • offset (Integer, nil) (defaults to: nil)

    number of certificates to skip

Returns:



69
70
71
72
73
74
75
76
# File 'lib/bsv/identity/client.rb', line 69

def resolve_by_identity_key(identity_key:, limit: nil, offset: nil)
  args = { identity_key: identity_key }
  args[:limit]  = limit  unless limit.nil?
  args[:offset] = offset unless offset.nil?

  result = @wallet.discover_by_identity_key(args, originator: @originator)
  parse_certificates(result)
end

#revoke_certificate_revelation(serial_number) ⇒ void

This method returns an undefined value.

Revokes a publicly revealed certificate by spending the identity token.

Queries the ls_identity lookup service for the revelation output identified by serial_number, then creates a spending transaction via the wallet and broadcasts it to tm_identity.

Parameters:

  • serial_number (String)

    Base64 serial number of the certificate revelation to revoke

Raises:

  • (RuntimeError)

    if the revelation cannot be found or the transaction cannot be created



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/bsv/identity/client.rb', line 187

def revoke_certificate_revelation(serial_number)
  question = BSV::Overlay::LookupQuestion.new(
    service: Constants::SERVICE,
    query: { serial_number: serial_number }
  )
  answer = resolver_for_action.query(question)

  raise 'Revoke failed: could not find revelation output' unless answer.type == 'output-list'
  raise 'Revoke failed: no outputs found for serial number' if answer.outputs.empty?

  output     = answer.outputs.first
  beef_bytes = output['beef'] || output[:beef]
  raise 'Revoke failed: overlay response missing BEEF data' unless beef_bytes

  raw_idx = output['outputIndex'] || output[:output_index]
  raise 'Revoke failed: overlay response missing outputIndex' if raw_idx.nil?

  output_idx = raw_idx.to_i
  raise 'Revoke failed: invalid outputIndex from overlay' if output_idx.negative?

  beef = BSV::Transaction::Beef.from_binary(beef_bytes)
  tx   = beef.transactions.last&.transaction
  raise 'Revoke failed: no transaction found in BEEF' unless tx
  raise 'Revoke failed: outputIndex out of range' if output_idx >= tx.outputs.length

  txid = tx.txid_hex
  outpoint = "#{txid}.#{output_idx}"

  # Create a spending transaction; use unlocking_script_length so the wallet
  # produces a signable transaction that can then be signed and broadcast.
  create_result = @wallet.create_action(
    {
      description: 'Spend certificate revelation token',
      input_beef: beef_bytes,
      inputs: [
        {
          input_description: 'Revelation token',
          outpoint: outpoint,
          unlocking_script_length: BSV::Script::PushDropTemplate::Unlocker::ESTIMATED_LENGTH
        }
      ],
      options: { randomize_outputs: false, no_send: true }
    },
    originator: @originator
  )

  raise 'Revoke failed: failed to create signable transaction' if create_result[:signable_transaction].nil?

  signable = create_result[:signable_transaction]
  partial_tx = BSV::Transaction::Transaction.from_beef(signable[:tx])

  # Unlock via PushDrop
  template = BSV::Script::PushDropTemplate.new(wallet: @wallet, originator: @originator)
  unlocker = template.unlock(
    protocol_id: @options.protocol_id,
    key_id: @options.key_id,
    counterparty: 'anyone'
  )
  # Sign the first (and only) input in the spending transaction.
  # output_idx is the index in the *source* tx; the spending tx input is always 0.
  spending_input_idx = 0
  unlocking_script = unlocker.sign(partial_tx, spending_input_idx)

  sign_result = @wallet.sign_action(
    {
      reference: signable[:reference],
      spends: { spending_input_idx => { unlocking_script: unlocking_script.to_hex } },
      options: { no_send: true }
    },
    originator: @originator
  )

  raise 'Revoke failed: failed to sign transaction' if sign_result[:tx].nil?

  signed_tx = BSV::Transaction::Transaction.from_beef(sign_result[:tx])
  broadcaster_for_action.broadcast(signed_tx)
end