@bsv/sdk 1.10.1 → 1.10.2
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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/identity/IdentityClient.js +124 -20
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/identity/IdentityClient.js +124 -20
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/identity/IdentityClient.d.ts +8 -0
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/package.json +1 -1
- package/src/identity/IdentityClient.ts +153 -29
- package/src/identity/__tests/IdentityClient.test.ts +289 -1
package/package.json
CHANGED
|
@@ -13,11 +13,16 @@ import {
|
|
|
13
13
|
} from '../wallet/index.js'
|
|
14
14
|
import { BroadcastFailure, BroadcastResponse, Transaction } from '../transaction/index.js'
|
|
15
15
|
import Certificate from '../auth/certificates/Certificate.js'
|
|
16
|
+
import { VerifiableCertificate } from '../auth/certificates/VerifiableCertificate.js'
|
|
16
17
|
import { PushDrop } from '../script/index.js'
|
|
17
18
|
import { PrivateKey, Utils } from '../primitives/index.js'
|
|
19
|
+
import ProtoWallet from '../wallet/ProtoWallet.js'
|
|
18
20
|
import { LookupResolver, SHIPBroadcaster, TopicBroadcaster, withDoubleSpendRetry } from '../overlay-tools/index.js'
|
|
19
21
|
import { ContactsManager, Contact } from './ContactsManager.js'
|
|
20
22
|
|
|
23
|
+
// Default SocialCert certifier for fallback resolution when no wallet is available
|
|
24
|
+
const DEFAULT_CERTIFIER = '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'
|
|
25
|
+
|
|
21
26
|
/**
|
|
22
27
|
* IdentityClient lets you discover who others are, and let the world know who you are.
|
|
23
28
|
*/
|
|
@@ -30,7 +35,7 @@ export class IdentityClient {
|
|
|
30
35
|
private readonly originator?: OriginatorDomainNameStringUnder250Bytes
|
|
31
36
|
) {
|
|
32
37
|
this.originator = originator
|
|
33
|
-
this.wallet = wallet ?? new WalletClient()
|
|
38
|
+
this.wallet = wallet ?? new WalletClient('auto', this.originator)
|
|
34
39
|
this.contactsManager = new ContactsManager(this.wallet, this.originator)
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -140,19 +145,33 @@ export class IdentityClient {
|
|
|
140
145
|
): Promise<DisplayableIdentity[]> {
|
|
141
146
|
if (overrideWithContacts) {
|
|
142
147
|
// Override results with personal contacts if available
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
try {
|
|
149
|
+
const contacts = await this.contactsManager.getContacts(args.identityKey)
|
|
150
|
+
if (contacts.length > 0) {
|
|
151
|
+
return contacts
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// Contacts require wallet - continue to try discovery
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return
|
|
155
|
-
|
|
158
|
+
try {
|
|
159
|
+
const { certificates } = await this.wallet.discoverByIdentityKey(
|
|
160
|
+
args,
|
|
161
|
+
this.originator
|
|
162
|
+
)
|
|
163
|
+
return certificates.map((cert) => {
|
|
164
|
+
return IdentityClient.parseIdentity(cert)
|
|
165
|
+
})
|
|
166
|
+
} catch (error: unknown) {
|
|
167
|
+
// Fallback to LookupResolver if no wallet is available
|
|
168
|
+
const errorMessage = error instanceof Error ? error.message : ''
|
|
169
|
+
if (errorMessage.includes('No wallet available')) {
|
|
170
|
+
const certificates = await this.resolveViaLookup({ identityKey: args.identityKey })
|
|
171
|
+
return certificates.map((cert) => IdentityClient.parseIdentity(cert))
|
|
172
|
+
}
|
|
173
|
+
throw error
|
|
174
|
+
}
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
/**
|
|
@@ -167,26 +186,131 @@ export class IdentityClient {
|
|
|
167
186
|
overrideWithContacts = true
|
|
168
187
|
): Promise<DisplayableIdentity[]> {
|
|
169
188
|
// Run both queries in parallel for better performance
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
: Promise.resolve([]),
|
|
174
|
-
this.wallet.discoverByAttributes(args, this.originator)
|
|
175
|
-
])
|
|
176
|
-
|
|
177
|
-
// Fast lookup by identityKey
|
|
178
|
-
const contactByKey = new Map<PubKeyHex, Contact>(
|
|
179
|
-
contacts.map((contact) => [contact.identityKey, contact] as const)
|
|
180
|
-
)
|
|
189
|
+
const contactsPromise = overrideWithContacts
|
|
190
|
+
? this.contactsManager.getContacts().catch(() => [] as Contact[])
|
|
191
|
+
: Promise.resolve([] as Contact[])
|
|
181
192
|
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
try {
|
|
194
|
+
const [contacts, certificatesResult] = await Promise.all([
|
|
195
|
+
contactsPromise,
|
|
196
|
+
this.wallet.discoverByAttributes(args, this.originator)
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
// Fast lookup by identityKey
|
|
200
|
+
const contactByKey = new Map<PubKeyHex, Contact>(
|
|
201
|
+
contacts.map((contact) => [contact.identityKey, contact] as const)
|
|
202
|
+
)
|
|
184
203
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
// Guard if certificates might be absent
|
|
205
|
+
const certs = certificatesResult?.certificates ?? []
|
|
206
|
+
|
|
207
|
+
// Parse certificates and substitute with contacts where available
|
|
208
|
+
return certs.map(
|
|
209
|
+
(cert) =>
|
|
210
|
+
contactByKey.get(cert.subject) ?? IdentityClient.parseIdentity(cert)
|
|
211
|
+
)
|
|
212
|
+
} catch (error: unknown) {
|
|
213
|
+
// Fallback to LookupResolver if no wallet is available
|
|
214
|
+
const errorMessage = error instanceof Error ? error.message : ''
|
|
215
|
+
if (errorMessage.includes('No wallet available')) {
|
|
216
|
+
const certificates = await this.resolveViaLookup({ attributes: args.attributes })
|
|
217
|
+
return certificates.map((cert) => IdentityClient.parseIdentity(cert))
|
|
218
|
+
}
|
|
219
|
+
throw error
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Fallback method to resolve identity certificates via LookupResolver when no wallet is available.
|
|
225
|
+
* Uses the ls_identity overlay with a default SocialCert certifier.
|
|
226
|
+
*
|
|
227
|
+
* @param query - Query parameters: either identityKey or attributes
|
|
228
|
+
* @returns {Promise<IdentityCertificate[]>} Array of identity certificates
|
|
229
|
+
*/
|
|
230
|
+
private async resolveViaLookup (
|
|
231
|
+
query: { identityKey?: PubKeyHex, attributes?: Record<string, string> }
|
|
232
|
+
): Promise<IdentityCertificate[]> {
|
|
233
|
+
const resolver = new LookupResolver({ networkPreset: 'mainnet' })
|
|
234
|
+
|
|
235
|
+
// Build the lookup query based on what was provided
|
|
236
|
+
const lookupQuery: Record<string, any> = {
|
|
237
|
+
certifiers: [DEFAULT_CERTIFIER]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (query.identityKey !== undefined) {
|
|
241
|
+
lookupQuery.identityKey = query.identityKey
|
|
242
|
+
} else if (query.attributes !== undefined) {
|
|
243
|
+
// For attribute search, use the 'any' matcher for flexible matching
|
|
244
|
+
const attributeValues = Object.values(query.attributes)
|
|
245
|
+
if (attributeValues.length === 1) {
|
|
246
|
+
lookupQuery.attributes = { any: attributeValues[0] }
|
|
247
|
+
} else {
|
|
248
|
+
lookupQuery.attributes = query.attributes
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lookupResult = await resolver.query({
|
|
253
|
+
service: 'ls_identity',
|
|
254
|
+
query: lookupQuery
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
if (lookupResult.type !== 'output-list' || lookupResult.outputs.length === 0) {
|
|
258
|
+
return []
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const certificates: IdentityCertificate[] = []
|
|
262
|
+
const anyoneWallet = new ProtoWallet('anyone')
|
|
263
|
+
|
|
264
|
+
for (const output of lookupResult.outputs) {
|
|
265
|
+
try {
|
|
266
|
+
const tx = Transaction.fromBEEF(output.beef)
|
|
267
|
+
const decodedOutput = PushDrop.decode(tx.outputs[output.outputIndex].lockingScript)
|
|
268
|
+
const certData = JSON.parse(Utils.toUTF8(decodedOutput.fields[0]))
|
|
269
|
+
|
|
270
|
+
const verifiableCert = new VerifiableCertificate(
|
|
271
|
+
certData.type,
|
|
272
|
+
certData.serialNumber,
|
|
273
|
+
certData.subject,
|
|
274
|
+
certData.certifier,
|
|
275
|
+
certData.revocationOutpoint,
|
|
276
|
+
certData.fields,
|
|
277
|
+
certData.keyring,
|
|
278
|
+
certData.signature
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
// Decrypt fields using 'anyone' wallet for publicly revealed certificates
|
|
282
|
+
const decryptedFields = await verifiableCert.decryptFields(anyoneWallet)
|
|
283
|
+
|
|
284
|
+
// Verify the certificate is valid
|
|
285
|
+
await verifiableCert.verify()
|
|
286
|
+
|
|
287
|
+
// Construct IdentityCertificate with default certifier info
|
|
288
|
+
const identityCert: IdentityCertificate = {
|
|
289
|
+
type: certData.type,
|
|
290
|
+
serialNumber: certData.serialNumber,
|
|
291
|
+
subject: certData.subject,
|
|
292
|
+
certifier: certData.certifier,
|
|
293
|
+
revocationOutpoint: certData.revocationOutpoint,
|
|
294
|
+
fields: certData.fields,
|
|
295
|
+
signature: certData.signature,
|
|
296
|
+
certifierInfo: {
|
|
297
|
+
name: 'SocialCert',
|
|
298
|
+
iconUrl: 'https://socialcert.net/favicon.ico',
|
|
299
|
+
description: 'Social identity verification',
|
|
300
|
+
trust: 5
|
|
301
|
+
},
|
|
302
|
+
publiclyRevealedKeyring: certData.keyring,
|
|
303
|
+
decryptedFields
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
certificates.push(identityCert)
|
|
307
|
+
} catch (e) {
|
|
308
|
+
// Skip invalid certificates and continue processing
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return certificates
|
|
190
314
|
}
|
|
191
315
|
|
|
192
316
|
/**
|
|
@@ -15,14 +15,41 @@ jest.mock('../../script', () => {
|
|
|
15
15
|
}
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
// Store mock functions for LookupResolver so tests can configure them
|
|
19
|
+
const mockLookupResolverQuery = jest.fn()
|
|
20
|
+
|
|
18
21
|
jest.mock('../../overlay-tools/index.js', () => {
|
|
19
22
|
return {
|
|
20
23
|
TopicBroadcaster: jest.fn().mockImplementation(() => ({
|
|
21
24
|
broadcast: jest.fn().mockResolvedValue('broadcastResult')
|
|
25
|
+
})),
|
|
26
|
+
LookupResolver: jest.fn().mockImplementation(() => ({
|
|
27
|
+
query: mockLookupResolverQuery
|
|
28
|
+
}))
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Mock VerifiableCertificate for resolveViaLookup tests
|
|
33
|
+
const mockDecryptFields = jest.fn()
|
|
34
|
+
const mockVerify = jest.fn()
|
|
35
|
+
|
|
36
|
+
jest.mock('../../auth/certificates/VerifiableCertificate.js', () => {
|
|
37
|
+
return {
|
|
38
|
+
VerifiableCertificate: jest.fn().mockImplementation(() => ({
|
|
39
|
+
decryptFields: mockDecryptFields,
|
|
40
|
+
verify: mockVerify
|
|
22
41
|
}))
|
|
23
42
|
}
|
|
24
43
|
})
|
|
25
44
|
|
|
45
|
+
// Mock ProtoWallet for resolveViaLookup tests
|
|
46
|
+
jest.mock('../../wallet/ProtoWallet.js', () => {
|
|
47
|
+
return {
|
|
48
|
+
__esModule: true,
|
|
49
|
+
default: jest.fn().mockImplementation(() => ({}))
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
26
53
|
jest.mock('../../transaction/index.js', () => {
|
|
27
54
|
return {
|
|
28
55
|
Transaction: {
|
|
@@ -267,6 +294,187 @@ describe('IdentityClient', () => {
|
|
|
267
294
|
// Wallet method should not be called when contact is found
|
|
268
295
|
expect(walletMock.discoverByIdentityKey).not.toHaveBeenCalled()
|
|
269
296
|
})
|
|
297
|
+
|
|
298
|
+
it('should fallback to LookupResolver when no wallet is available', async () => {
|
|
299
|
+
// Mock wallet methods to throw "No wallet available" error
|
|
300
|
+
walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
|
|
301
|
+
new Error('No wallet available over any communication substrate. Install a BSV wallet today!')
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
// Mock ContactsManager to also throw (since it requires wallet)
|
|
305
|
+
const mockContactsManager = identityClient['contactsManager']
|
|
306
|
+
mockContactsManager.getContacts = jest.fn().mockRejectedValue(
|
|
307
|
+
new Error('No wallet available')
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Setup mock certificate data that would come from lookup
|
|
311
|
+
const certData = {
|
|
312
|
+
type: KNOWN_IDENTITY_TYPES.xCert,
|
|
313
|
+
serialNumber: 'serial123',
|
|
314
|
+
subject: 'test-identity-key',
|
|
315
|
+
certifier: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17',
|
|
316
|
+
revocationOutpoint: 'outpoint',
|
|
317
|
+
fields: { userName: 'encrypted', profilePhoto: 'encrypted' },
|
|
318
|
+
keyring: { fieldKey: 'keyringValue' },
|
|
319
|
+
signature: 'sig123'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Mock LookupResolver to return output-list with certificate data
|
|
323
|
+
mockLookupResolverQuery.mockResolvedValue({
|
|
324
|
+
type: 'output-list',
|
|
325
|
+
outputs: [{
|
|
326
|
+
beef: new Uint8Array([1, 2, 3]),
|
|
327
|
+
outputIndex: 0
|
|
328
|
+
}]
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Mock PushDrop.decode to return certificate JSON
|
|
332
|
+
const { PushDrop } = require('../../script')
|
|
333
|
+
PushDrop.decode.mockReturnValue({
|
|
334
|
+
fields: [new TextEncoder().encode(JSON.stringify(certData))]
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Mock VerifiableCertificate methods
|
|
338
|
+
mockDecryptFields.mockResolvedValue({
|
|
339
|
+
userName: 'TestUser',
|
|
340
|
+
profilePhoto: 'test-photo-url'
|
|
341
|
+
})
|
|
342
|
+
mockVerify.mockResolvedValue(true)
|
|
343
|
+
|
|
344
|
+
const identities = await identityClient.resolveByIdentityKey(
|
|
345
|
+
{ identityKey: 'test-identity-key' },
|
|
346
|
+
false // Don't override with contacts
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
// Verify wallet was called and threw
|
|
350
|
+
expect(walletMock.discoverByIdentityKey).toHaveBeenCalled()
|
|
351
|
+
|
|
352
|
+
// Verify LookupResolver was queried
|
|
353
|
+
expect(mockLookupResolverQuery).toHaveBeenCalledWith({
|
|
354
|
+
service: 'ls_identity',
|
|
355
|
+
query: {
|
|
356
|
+
certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
|
|
357
|
+
identityKey: 'test-identity-key'
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Verify results were returned from fallback
|
|
362
|
+
expect(identities).toHaveLength(1)
|
|
363
|
+
expect(identities[0].name).toBe('TestUser')
|
|
364
|
+
expect(identities[0].identityKey).toBe('test-identity-key')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should return empty array when LookupResolver returns no results', async () => {
|
|
368
|
+
walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
|
|
369
|
+
new Error('No wallet available')
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
const mockContactsManager = identityClient['contactsManager']
|
|
373
|
+
mockContactsManager.getContacts = jest.fn().mockRejectedValue(
|
|
374
|
+
new Error('No wallet available')
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
// Mock LookupResolver to return empty results
|
|
378
|
+
mockLookupResolverQuery.mockResolvedValue({
|
|
379
|
+
type: 'output-list',
|
|
380
|
+
outputs: []
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
const identities = await identityClient.resolveByIdentityKey(
|
|
384
|
+
{ identityKey: 'nonexistent-key' },
|
|
385
|
+
false
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
expect(identities).toHaveLength(0)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should return empty array when LookupResolver returns non-output-list type', async () => {
|
|
392
|
+
walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
|
|
393
|
+
new Error('No wallet available')
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
const mockContactsManager = identityClient['contactsManager']
|
|
397
|
+
mockContactsManager.getContacts = jest.fn().mockRejectedValue(
|
|
398
|
+
new Error('No wallet available')
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// Mock LookupResolver to return different type
|
|
402
|
+
mockLookupResolverQuery.mockResolvedValue({
|
|
403
|
+
type: 'freeform',
|
|
404
|
+
result: {}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const identities = await identityClient.resolveByIdentityKey(
|
|
408
|
+
{ identityKey: 'some-key' },
|
|
409
|
+
false
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
expect(identities).toHaveLength(0)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('should skip invalid certificates and continue processing', async () => {
|
|
416
|
+
walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
|
|
417
|
+
new Error('No wallet available')
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
const mockContactsManager = identityClient['contactsManager']
|
|
421
|
+
mockContactsManager.getContacts = jest.fn().mockRejectedValue(
|
|
422
|
+
new Error('No wallet available')
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
// Mock LookupResolver to return multiple outputs
|
|
426
|
+
mockLookupResolverQuery.mockResolvedValue({
|
|
427
|
+
type: 'output-list',
|
|
428
|
+
outputs: [
|
|
429
|
+
{ beef: new Uint8Array([1]), outputIndex: 0 },
|
|
430
|
+
{ beef: new Uint8Array([2]), outputIndex: 0 }
|
|
431
|
+
]
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// First call throws (invalid cert), second succeeds
|
|
435
|
+
const { PushDrop } = require('../../script')
|
|
436
|
+
PushDrop.decode
|
|
437
|
+
.mockImplementationOnce(() => { throw new Error('Invalid script') })
|
|
438
|
+
.mockImplementationOnce(() => ({
|
|
439
|
+
fields: [new TextEncoder().encode(JSON.stringify({
|
|
440
|
+
type: KNOWN_IDENTITY_TYPES.xCert,
|
|
441
|
+
serialNumber: 'serial',
|
|
442
|
+
subject: 'valid-key',
|
|
443
|
+
certifier: 'certifier',
|
|
444
|
+
revocationOutpoint: 'outpoint',
|
|
445
|
+
fields: {},
|
|
446
|
+
keyring: {},
|
|
447
|
+
signature: 'sig'
|
|
448
|
+
}))]
|
|
449
|
+
}))
|
|
450
|
+
|
|
451
|
+
mockDecryptFields.mockResolvedValue({ userName: 'ValidUser' })
|
|
452
|
+
mockVerify.mockResolvedValue(true)
|
|
453
|
+
|
|
454
|
+
const identities = await identityClient.resolveByIdentityKey(
|
|
455
|
+
{ identityKey: 'some-key' },
|
|
456
|
+
false
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
// Should have 1 identity (the valid one)
|
|
460
|
+
expect(identities).toHaveLength(1)
|
|
461
|
+
expect(identities[0].name).toBe('ValidUser')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('should re-throw non-wallet errors', async () => {
|
|
465
|
+
walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
|
|
466
|
+
new Error('Network timeout')
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
const mockContactsManager = identityClient['contactsManager']
|
|
470
|
+
mockContactsManager.getContacts = jest.fn().mockRejectedValue(
|
|
471
|
+
new Error('No wallet available')
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
await expect(
|
|
475
|
+
identityClient.resolveByIdentityKey({ identityKey: 'test-key' }, false)
|
|
476
|
+
).rejects.toThrow('Network timeout')
|
|
477
|
+
})
|
|
270
478
|
})
|
|
271
479
|
|
|
272
480
|
it('should throw if createAction returns no tx', async () => {
|
|
@@ -367,7 +575,11 @@ describe('IdentityClient', () => {
|
|
|
367
575
|
{
|
|
368
576
|
name: 'Alice Smith',
|
|
369
577
|
identityKey: 'alice-key',
|
|
370
|
-
avatarURL: '',
|
|
578
|
+
avatarURL: '',
|
|
579
|
+
abbreviatedKey: 'alice-i...',
|
|
580
|
+
badgeIconURL: '',
|
|
581
|
+
badgeLabel: '',
|
|
582
|
+
badgeClickURL: ''
|
|
371
583
|
}
|
|
372
584
|
]
|
|
373
585
|
|
|
@@ -416,6 +628,82 @@ describe('IdentityClient', () => {
|
|
|
416
628
|
expect(identities[0].name).toBe('alice@example.com') // Should be discovered identity, not contact
|
|
417
629
|
expect(mockContactsManager.getContacts).not.toHaveBeenCalled() // Should not fetch contacts
|
|
418
630
|
})
|
|
631
|
+
|
|
632
|
+
it('should fallback to LookupResolver when no wallet is available for attribute search', async () => {
|
|
633
|
+
walletMock.discoverByAttributes = jest.fn().mockRejectedValue(
|
|
634
|
+
new Error('No wallet available')
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
// Setup mock certificate data
|
|
638
|
+
const certData = {
|
|
639
|
+
type: KNOWN_IDENTITY_TYPES.emailCert,
|
|
640
|
+
serialNumber: 'serial123',
|
|
641
|
+
subject: 'found-identity-key',
|
|
642
|
+
certifier: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17',
|
|
643
|
+
revocationOutpoint: 'outpoint',
|
|
644
|
+
fields: { email: 'encrypted' },
|
|
645
|
+
keyring: { fieldKey: 'keyringValue' },
|
|
646
|
+
signature: 'sig123'
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
mockLookupResolverQuery.mockResolvedValue({
|
|
650
|
+
type: 'output-list',
|
|
651
|
+
outputs: [{
|
|
652
|
+
beef: new Uint8Array([1, 2, 3]),
|
|
653
|
+
outputIndex: 0
|
|
654
|
+
}]
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
const { PushDrop } = require('../../script')
|
|
658
|
+
PushDrop.decode.mockReturnValue({
|
|
659
|
+
fields: [new TextEncoder().encode(JSON.stringify(certData))]
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
mockDecryptFields.mockResolvedValue({ email: 'found@example.com' })
|
|
663
|
+
mockVerify.mockResolvedValue(true)
|
|
664
|
+
|
|
665
|
+
const identities = await identityClient.resolveByAttributes(
|
|
666
|
+
{ attributes: { email: 'found@example.com' } },
|
|
667
|
+
false
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
// Verify LookupResolver was queried with attributes
|
|
671
|
+
expect(mockLookupResolverQuery).toHaveBeenCalledWith({
|
|
672
|
+
service: 'ls_identity',
|
|
673
|
+
query: {
|
|
674
|
+
certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
|
|
675
|
+
attributes: { any: 'found@example.com' }
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
expect(identities).toHaveLength(1)
|
|
680
|
+
expect(identities[0].name).toBe('found@example.com')
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it('should use multi-attribute query when multiple attributes provided', async () => {
|
|
684
|
+
walletMock.discoverByAttributes = jest.fn().mockRejectedValue(
|
|
685
|
+
new Error('No wallet available')
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
mockLookupResolverQuery.mockResolvedValue({
|
|
689
|
+
type: 'output-list',
|
|
690
|
+
outputs: []
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
await identityClient.resolveByAttributes(
|
|
694
|
+
{ attributes: { firstName: 'John', lastName: 'Doe' } },
|
|
695
|
+
false
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
// Verify query uses full attributes object when more than one
|
|
699
|
+
expect(mockLookupResolverQuery).toHaveBeenCalledWith({
|
|
700
|
+
service: 'ls_identity',
|
|
701
|
+
query: {
|
|
702
|
+
certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
|
|
703
|
+
attributes: { firstName: 'John', lastName: 'Doe' }
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
})
|
|
419
707
|
})
|
|
420
708
|
|
|
421
709
|
describe('parseIdentity', () => {
|