@bsv/sdk 2.0.11 → 2.0.13

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.
Files changed (106) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
  7. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
  8. package/dist/cjs/src/primitives/PrivateKey.js +3 -3
  9. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  10. package/dist/cjs/src/script/Spend.js +17 -9
  11. package/dist/cjs/src/script/Spend.js.map +1 -1
  12. package/dist/cjs/src/storage/StorageDownloader.js +6 -6
  13. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  14. package/dist/cjs/src/storage/StorageUtils.js +1 -1
  15. package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
  16. package/dist/cjs/src/transaction/MerklePath.js +168 -27
  17. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  18. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  19. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  20. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  21. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  22. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  23. package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
  24. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
  25. package/dist/esm/src/primitives/PrivateKey.js +3 -3
  26. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  27. package/dist/esm/src/script/Spend.js +17 -9
  28. package/dist/esm/src/script/Spend.js.map +1 -1
  29. package/dist/esm/src/storage/StorageDownloader.js +6 -6
  30. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  31. package/dist/esm/src/storage/StorageUtils.js +1 -1
  32. package/dist/esm/src/storage/StorageUtils.js.map +1 -1
  33. package/dist/esm/src/transaction/MerklePath.js +168 -27
  34. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  35. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  36. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  37. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  38. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  39. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  40. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
  41. package/dist/types/src/script/Spend.d.ts.map +1 -1
  42. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  43. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  44. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  45. package/dist/umd/bundle.js +3 -3
  46. package/dist/umd/bundle.js.map +1 -1
  47. package/docs/reference/storage.md +1 -1
  48. package/docs/reference/transaction.md +40 -0
  49. package/package.json +1 -1
  50. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  51. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  52. package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
  53. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  54. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  55. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  56. package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
  57. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  58. package/src/overlay-tools/HostReputationTracker.ts +17 -14
  59. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  60. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  61. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  62. package/src/primitives/PrivateKey.ts +3 -3
  63. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  64. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  65. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  66. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  67. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  68. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  69. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  70. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  71. package/src/primitives/__tests/Signature.test.ts +333 -0
  72. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  73. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  74. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  75. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  76. package/src/script/Spend.ts +19 -11
  77. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  78. package/src/script/__tests/Script.additional.test.ts +100 -0
  79. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  80. package/src/script/__tests/Spend.additional.test.ts +837 -0
  81. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  82. package/src/storage/StorageDownloader.ts +6 -6
  83. package/src/storage/StorageUtils.ts +1 -1
  84. package/src/transaction/MerklePath.ts +196 -36
  85. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  86. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  87. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  88. package/src/transaction/__tests/MerklePath.test.ts +232 -21
  89. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  90. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  91. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  92. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  93. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  94. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  95. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  96. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  97. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  98. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  99. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  100. package/src/wallet/__tests/WERR.test.ts +212 -0
  101. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  102. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  103. package/src/wallet/__tests/WalletError.test.ts +290 -0
  104. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  105. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  106. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,767 @@
1
+ import { WalletCertificate, WalletInterface } from '../../wallet/index'
2
+ import { IdentityClient } from '../IdentityClient'
3
+ import { Certificate } from '../../auth/certificates/index.js'
4
+ import { KNOWN_IDENTITY_TYPES, defaultIdentity } from '../types/index.js'
5
+
6
+ // ----- Mocks for external dependencies -----
7
+ jest.mock('../../script', () => {
8
+ const mockPushDropInstance = {
9
+ lock: jest.fn().mockResolvedValue({
10
+ toHex: () => 'lockingScriptHex'
11
+ }),
12
+ unlock: jest.fn().mockReturnValue({
13
+ sign: jest.fn().mockResolvedValue({
14
+ toHex: () => 'unlockingScriptHex'
15
+ })
16
+ })
17
+ }
18
+
19
+ const mockPushDrop: any = jest.fn().mockImplementation(() => mockPushDropInstance)
20
+ mockPushDrop.decode = jest.fn().mockReturnValue({
21
+ fields: [new Uint8Array([1, 2, 3, 4])]
22
+ })
23
+
24
+ return {
25
+ PushDrop: mockPushDrop,
26
+ LockingScript: {
27
+ fromHex: jest.fn().mockImplementation((hex: string) => ({ toHex: () => hex }))
28
+ }
29
+ }
30
+ })
31
+
32
+ jest.mock('../../overlay-tools/index.js', () => {
33
+ return {
34
+ TopicBroadcaster: jest.fn().mockImplementation(() => ({
35
+ broadcast: jest.fn().mockResolvedValue('broadcastResult')
36
+ })),
37
+ SHIPBroadcaster: jest.fn().mockImplementation(() => ({
38
+ broadcast: jest.fn().mockResolvedValue('broadcastResult')
39
+ })),
40
+ LookupResolver: jest.fn().mockImplementation(() => ({
41
+ query: jest.fn().mockResolvedValue({
42
+ type: 'output-list',
43
+ outputs: [
44
+ {
45
+ beef: [1, 2, 3]
46
+ }
47
+ ]
48
+ })
49
+ })),
50
+ withDoubleSpendRetry: jest.fn().mockImplementation(async (fn: () => Promise<void>) => {
51
+ await fn()
52
+ })
53
+ }
54
+ })
55
+
56
+ jest.mock('../../transaction/index.js', () => {
57
+ return {
58
+ Transaction: {
59
+ fromAtomicBEEF: jest.fn().mockImplementation((_tx) => ({
60
+ toHexBEEF: () => 'transactionHex'
61
+ })),
62
+ fromBEEF: jest.fn().mockReturnValue({
63
+ id: jest.fn().mockReturnValue('mocktxid'),
64
+ outputs: [
65
+ {
66
+ lockingScript: {
67
+ toHex: () => 'mockLockingScript'
68
+ }
69
+ }
70
+ ]
71
+ })
72
+ }
73
+ }
74
+ })
75
+
76
+ jest.mock('../../primitives/index.js', () => {
77
+ return {
78
+ Utils: {
79
+ toBase64: jest.fn().mockReturnValue('mockKeyID'),
80
+ toArray: jest.fn().mockReturnValue(new Uint8Array()),
81
+ toUTF8: jest.fn().mockImplementation((data) => {
82
+ return new TextDecoder().decode(data)
83
+ }),
84
+ toHex: jest.fn().mockReturnValue('0102030405060708')
85
+ },
86
+ Random: jest.fn().mockReturnValue(new Uint8Array(32)),
87
+ PrivateKey: jest.fn().mockImplementation(() => ({
88
+ toPublicKey: jest.fn().mockReturnValue({
89
+ toString: jest.fn().mockReturnValue('mockPublicKeyString')
90
+ })
91
+ }))
92
+ }
93
+ })
94
+
95
+ // ----- Begin Test Suite -----
96
+ describe('IdentityClient (additional coverage)', () => {
97
+ let walletMock: Partial<WalletInterface>
98
+ let identityClient: IdentityClient
99
+
100
+ beforeEach(() => {
101
+ const localStorageMock = {
102
+ getItem: jest.fn(),
103
+ setItem: jest.fn(),
104
+ removeItem: jest.fn()
105
+ }
106
+ Object.defineProperty(global, 'localStorage', {
107
+ value: localStorageMock,
108
+ writable: true
109
+ })
110
+
111
+ walletMock = {
112
+ proveCertificate: jest.fn().mockResolvedValue({ keyringForVerifier: 'fakeKeyring' }),
113
+ createAction: jest.fn().mockResolvedValue({
114
+ tx: [1, 2, 3],
115
+ signableTransaction: { tx: [1, 2, 3], reference: 'ref' }
116
+ }),
117
+ listCertificates: jest.fn().mockResolvedValue({ certificates: [] }),
118
+ acquireCertificate: jest.fn().mockResolvedValue({
119
+ fields: { name: 'Alice' },
120
+ verify: jest.fn().mockResolvedValue(true)
121
+ }),
122
+ signAction: jest.fn().mockResolvedValue({ tx: [4, 5, 6] }),
123
+ getNetwork: jest.fn().mockResolvedValue({ network: 'testnet' }),
124
+ discoverByIdentityKey: jest.fn().mockResolvedValue({ certificates: [] }),
125
+ discoverByAttributes: jest.fn().mockResolvedValue({ certificates: [] }),
126
+ listOutputs: jest.fn().mockResolvedValue({ outputs: [], BEEF: [] }),
127
+ createHmac: jest.fn().mockResolvedValue({ hmac: new Uint8Array([1, 2, 3, 4]) }),
128
+ decrypt: jest.fn().mockResolvedValue({ plaintext: new Uint8Array() }),
129
+ encrypt: jest.fn().mockResolvedValue({ ciphertext: new Uint8Array([5, 6, 7, 8]) })
130
+ }
131
+
132
+ identityClient = new IdentityClient(walletMock as WalletInterface)
133
+ jest.clearAllMocks()
134
+ })
135
+
136
+ // ─── parseIdentity: remaining known cert types ──────────────────────────────
137
+
138
+ describe('parseIdentity — remaining known cert types', () => {
139
+ it('parses discordCert correctly', () => {
140
+ const cert = {
141
+ type: KNOWN_IDENTITY_TYPES.discordCert,
142
+ subject: 'discordSubject123',
143
+ decryptedFields: { userName: 'DiscordUser', profilePhoto: 'discord-photo.png' },
144
+ certifierInfo: { name: 'DiscordCertifier', iconUrl: 'discord-icon.png' }
145
+ }
146
+ const result = IdentityClient.parseIdentity(cert as any)
147
+ expect(result.name).toBe('DiscordUser')
148
+ expect(result.avatarURL).toBe('discord-photo.png')
149
+ expect(result.badgeLabel).toBe('Discord account certified by DiscordCertifier')
150
+ expect(result.badgeIconURL).toBe('discord-icon.png')
151
+ expect(result.badgeClickURL).toBe('https://socialcert.net')
152
+ expect(result.identityKey).toBe('discordSubject123')
153
+ expect(result.abbreviatedKey).toBe('discordSub...')
154
+ })
155
+
156
+ it('parses emailCert correctly', () => {
157
+ const cert = {
158
+ type: KNOWN_IDENTITY_TYPES.emailCert,
159
+ subject: 'emailSubjectABC',
160
+ decryptedFields: { email: 'user@example.com' },
161
+ certifierInfo: { name: 'EmailCertifier', iconUrl: 'email-icon.png' }
162
+ }
163
+ const result = IdentityClient.parseIdentity(cert as any)
164
+ expect(result.name).toBe('user@example.com')
165
+ // avatarURL is a hard-coded constant for email
166
+ expect(result.avatarURL).toBe('XUTZxep7BBghAJbSBwTjNfmcsDdRFs5EaGEgkESGSgjJVYgMEizu')
167
+ expect(result.badgeLabel).toBe('Email certified by EmailCertifier')
168
+ expect(result.badgeIconURL).toBe('email-icon.png')
169
+ expect(result.badgeClickURL).toBe('https://socialcert.net')
170
+ })
171
+
172
+ it('parses phoneCert correctly', () => {
173
+ const cert = {
174
+ type: KNOWN_IDENTITY_TYPES.phoneCert,
175
+ subject: 'phoneSubjectXYZ',
176
+ decryptedFields: { phoneNumber: '+15555551234' },
177
+ certifierInfo: { name: 'PhoneCertifier', iconUrl: 'phone-icon.png' }
178
+ }
179
+ const result = IdentityClient.parseIdentity(cert as any)
180
+ expect(result.name).toBe('+15555551234')
181
+ // avatarURL is a hard-coded constant for phone
182
+ expect(result.avatarURL).toBe('XUTLxtX3ELNUwRhLwL7kWNGbdnFM8WG2eSLv84J7654oH8HaJWrU')
183
+ expect(result.badgeLabel).toBe('Phone certified by PhoneCertifier')
184
+ expect(result.badgeClickURL).toBe('https://socialcert.net')
185
+ })
186
+
187
+ it('parses identiCert correctly', () => {
188
+ const cert = {
189
+ type: KNOWN_IDENTITY_TYPES.identiCert,
190
+ subject: 'identiSubjectFOO',
191
+ decryptedFields: { firstName: 'Jane', lastName: 'Doe', profilePhoto: 'id-photo.png' },
192
+ certifierInfo: { name: 'GovCertifier', iconUrl: 'gov-icon.png' }
193
+ }
194
+ const result = IdentityClient.parseIdentity(cert as any)
195
+ expect(result.name).toBe('Jane Doe')
196
+ expect(result.avatarURL).toBe('id-photo.png')
197
+ expect(result.badgeLabel).toBe('Government ID certified by GovCertifier')
198
+ expect(result.badgeClickURL).toBe('https://identicert.me')
199
+ })
200
+
201
+ it('parses registrant correctly', () => {
202
+ const cert = {
203
+ type: KNOWN_IDENTITY_TYPES.registrant,
204
+ subject: 'registrantSubject',
205
+ decryptedFields: { name: 'ACME Corp', icon: 'acme-icon.png' },
206
+ certifierInfo: { name: 'RegistryCertifier', iconUrl: 'registry-icon.png' }
207
+ }
208
+ const result = IdentityClient.parseIdentity(cert as any)
209
+ expect(result.name).toBe('ACME Corp')
210
+ expect(result.avatarURL).toBe('acme-icon.png')
211
+ expect(result.badgeLabel).toBe('Entity certified by RegistryCertifier')
212
+ expect(result.badgeClickURL).toBe('https://projectbabbage.com/docs/registrant')
213
+ })
214
+
215
+ it('parses coolCert with cool=true', () => {
216
+ const cert = {
217
+ type: KNOWN_IDENTITY_TYPES.coolCert,
218
+ subject: 'coolSubject001',
219
+ decryptedFields: { cool: 'true' },
220
+ certifierInfo: {}
221
+ }
222
+ const result = IdentityClient.parseIdentity(cert as any)
223
+ expect(result.name).toBe('Cool Person!')
224
+ })
225
+
226
+ it('parses coolCert with cool != true', () => {
227
+ const cert = {
228
+ type: KNOWN_IDENTITY_TYPES.coolCert,
229
+ subject: 'coolSubject002',
230
+ decryptedFields: { cool: 'false' },
231
+ certifierInfo: {}
232
+ }
233
+ const result = IdentityClient.parseIdentity(cert as any)
234
+ expect(result.name).toBe('Not cool!')
235
+ })
236
+
237
+ it('parses anyone identity type', () => {
238
+ const cert = {
239
+ type: KNOWN_IDENTITY_TYPES.anyone,
240
+ subject: 'anyoneSubjectAAA',
241
+ decryptedFields: {},
242
+ certifierInfo: {}
243
+ }
244
+ const result = IdentityClient.parseIdentity(cert as any)
245
+ expect(result.name).toBe('Anyone')
246
+ expect(result.avatarURL).toBe('XUT4bpQ6cpBaXi1oMzZsXfpkWGbtp2JTUYAoN7PzhStFJ6wLfoeR')
247
+ expect(result.badgeLabel).toBe('Represents the ability for anyone to access this information.')
248
+ expect(result.badgeClickURL).toBe('https://projectbabbage.com/docs/anyone-identity')
249
+ })
250
+
251
+ it('parses self identity type', () => {
252
+ const cert = {
253
+ type: KNOWN_IDENTITY_TYPES.self,
254
+ subject: 'selfSubjectBBB',
255
+ decryptedFields: {},
256
+ certifierInfo: {}
257
+ }
258
+ const result = IdentityClient.parseIdentity(cert as any)
259
+ expect(result.name).toBe('You')
260
+ expect(result.avatarURL).toBe('XUT9jHGk2qace148jeCX5rDsMftkSGYKmigLwU2PLLBc7Hm63VYR')
261
+ expect(result.badgeLabel).toBe('Represents your ability to access this information.')
262
+ expect(result.badgeClickURL).toBe('https://projectbabbage.com/docs/self-identity')
263
+ })
264
+
265
+ it('produces empty abbreviatedKey when subject is empty string', () => {
266
+ const cert = {
267
+ type: KNOWN_IDENTITY_TYPES.anyone,
268
+ subject: '',
269
+ decryptedFields: {},
270
+ certifierInfo: {}
271
+ }
272
+ const result = IdentityClient.parseIdentity(cert as any)
273
+ expect(result.abbreviatedKey).toBe('')
274
+ })
275
+
276
+ it('abbreviatedKey is subject.substring(0,10)+"..." when subject is non-empty', () => {
277
+ const cert = {
278
+ type: KNOWN_IDENTITY_TYPES.anyone,
279
+ subject: '0123456789ABCDEF',
280
+ decryptedFields: {},
281
+ certifierInfo: {}
282
+ }
283
+ const result = IdentityClient.parseIdentity(cert as any)
284
+ expect(result.abbreviatedKey).toBe('0123456789...')
285
+ })
286
+ })
287
+
288
+ // ─── parseIdentity: generic/unknown type — tryToParseGenericIdentity paths ──
289
+
290
+ describe('parseIdentity — generic/unknown type (tryToParseGenericIdentity)', () => {
291
+ it('uses decryptedFields.name when present', () => {
292
+ const cert = {
293
+ type: 'custom-type',
294
+ subject: 'sub1',
295
+ decryptedFields: { name: 'Custom Name' },
296
+ certifierInfo: { name: 'SomeCert', iconUrl: 'some-icon.png' }
297
+ }
298
+ const result = IdentityClient.parseIdentity(cert as any)
299
+ expect(result.name).toBe('Custom Name')
300
+ })
301
+
302
+ it('falls back to decryptedFields.userName when name is absent', () => {
303
+ const cert = {
304
+ type: 'custom-type',
305
+ subject: 'sub1',
306
+ decryptedFields: { userName: 'userNameValue' },
307
+ certifierInfo: { name: 'SomeCert', iconUrl: 'icon.png' }
308
+ }
309
+ const result = IdentityClient.parseIdentity(cert as any)
310
+ expect(result.name).toBe('userNameValue')
311
+ })
312
+
313
+ it('falls back to firstName + lastName when both present', () => {
314
+ const cert = {
315
+ type: 'custom-type',
316
+ subject: 'sub1',
317
+ decryptedFields: { firstName: 'John', lastName: 'Smith' },
318
+ certifierInfo: {}
319
+ }
320
+ const result = IdentityClient.parseIdentity(cert as any)
321
+ expect(result.name).toBe('John Smith')
322
+ })
323
+
324
+ it('uses only firstName when lastName is absent', () => {
325
+ const cert = {
326
+ type: 'custom-type',
327
+ subject: 'sub1',
328
+ decryptedFields: { firstName: 'OnlyFirst' },
329
+ certifierInfo: {}
330
+ }
331
+ const result = IdentityClient.parseIdentity(cert as any)
332
+ expect(result.name).toBe('OnlyFirst')
333
+ })
334
+
335
+ it('uses only lastName when firstName is absent', () => {
336
+ const cert = {
337
+ type: 'custom-type',
338
+ subject: 'sub1',
339
+ decryptedFields: { lastName: 'OnlyLast' },
340
+ certifierInfo: {}
341
+ }
342
+ const result = IdentityClient.parseIdentity(cert as any)
343
+ expect(result.name).toBe('OnlyLast')
344
+ })
345
+
346
+ it('falls back to email when no name/userName/firstName/lastName', () => {
347
+ const cert = {
348
+ type: 'custom-type',
349
+ subject: 'sub1',
350
+ decryptedFields: { email: 'generic@example.com' },
351
+ certifierInfo: {}
352
+ }
353
+ const result = IdentityClient.parseIdentity(cert as any)
354
+ expect(result.name).toBe('generic@example.com')
355
+ })
356
+
357
+ it('uses defaultIdentity.name when no name fields exist', () => {
358
+ const cert = {
359
+ type: 'custom-type',
360
+ subject: 'sub1',
361
+ decryptedFields: {},
362
+ certifierInfo: {}
363
+ }
364
+ const result = IdentityClient.parseIdentity(cert as any)
365
+ expect(result.name).toBe(defaultIdentity.name)
366
+ })
367
+
368
+ it('uses decryptedFields.profilePhoto for avatarURL', () => {
369
+ const cert = {
370
+ type: 'custom-type',
371
+ subject: 'sub1',
372
+ decryptedFields: { name: 'X', profilePhoto: 'profile.png' },
373
+ certifierInfo: {}
374
+ }
375
+ const result = IdentityClient.parseIdentity(cert as any)
376
+ expect(result.avatarURL).toBe('profile.png')
377
+ })
378
+
379
+ it('falls back to decryptedFields.avatar for avatarURL', () => {
380
+ const cert = {
381
+ type: 'custom-type',
382
+ subject: 'sub1',
383
+ decryptedFields: { name: 'X', avatar: 'avatar.png' },
384
+ certifierInfo: {}
385
+ }
386
+ const result = IdentityClient.parseIdentity(cert as any)
387
+ expect(result.avatarURL).toBe('avatar.png')
388
+ })
389
+
390
+ it('falls back to decryptedFields.icon for avatarURL', () => {
391
+ const cert = {
392
+ type: 'custom-type',
393
+ subject: 'sub1',
394
+ decryptedFields: { name: 'X', icon: 'icon.png' },
395
+ certifierInfo: {}
396
+ }
397
+ const result = IdentityClient.parseIdentity(cert as any)
398
+ expect(result.avatarURL).toBe('icon.png')
399
+ })
400
+
401
+ it('falls back to decryptedFields.photo for avatarURL', () => {
402
+ const cert = {
403
+ type: 'custom-type',
404
+ subject: 'sub1',
405
+ decryptedFields: { name: 'X', photo: 'photo.png' },
406
+ certifierInfo: {}
407
+ }
408
+ const result = IdentityClient.parseIdentity(cert as any)
409
+ expect(result.avatarURL).toBe('photo.png')
410
+ })
411
+
412
+ it('uses defaultIdentity.avatarURL when no avatar field is present', () => {
413
+ const cert = {
414
+ type: 'custom-type',
415
+ subject: 'sub1',
416
+ decryptedFields: { name: 'X' },
417
+ certifierInfo: {}
418
+ }
419
+ const result = IdentityClient.parseIdentity(cert as any)
420
+ expect(result.avatarURL).toBe(defaultIdentity.avatarURL)
421
+ })
422
+
423
+ it('generates badgeLabel from certifierInfo.name', () => {
424
+ const cert = {
425
+ type: 'my-cert-type',
426
+ subject: 'sub1',
427
+ decryptedFields: {},
428
+ certifierInfo: { name: 'MyCertifier', iconUrl: 'cert-icon.png' }
429
+ }
430
+ const result = IdentityClient.parseIdentity(cert as any)
431
+ expect(result.badgeLabel).toBe('my-cert-type certified by MyCertifier')
432
+ })
433
+
434
+ it('uses defaultIdentity.badgeLabel when certifierInfo.name is absent', () => {
435
+ const cert = {
436
+ type: 'my-cert-type',
437
+ subject: 'sub1',
438
+ decryptedFields: {},
439
+ certifierInfo: {}
440
+ }
441
+ const result = IdentityClient.parseIdentity(cert as any)
442
+ expect(result.badgeLabel).toBe(defaultIdentity.badgeLabel)
443
+ })
444
+
445
+ it('uses certifierInfo.iconUrl for badgeIconURL when present', () => {
446
+ const cert = {
447
+ type: 'my-cert-type',
448
+ subject: 'sub1',
449
+ decryptedFields: {},
450
+ certifierInfo: { name: 'Cert', iconUrl: 'specific-icon.png' }
451
+ }
452
+ const result = IdentityClient.parseIdentity(cert as any)
453
+ expect(result.badgeIconURL).toBe('specific-icon.png')
454
+ })
455
+
456
+ it('uses defaultIdentity.badgeIconURL when certifierInfo.iconUrl is absent', () => {
457
+ const cert = {
458
+ type: 'my-cert-type',
459
+ subject: 'sub1',
460
+ decryptedFields: {},
461
+ certifierInfo: { name: 'Cert' }
462
+ }
463
+ const result = IdentityClient.parseIdentity(cert as any)
464
+ expect(result.badgeIconURL).toBe(defaultIdentity.badgeIconURL)
465
+ })
466
+
467
+ it('always uses defaultIdentity.badgeClickURL', () => {
468
+ const cert = {
469
+ type: 'my-cert-type',
470
+ subject: 'sub1',
471
+ decryptedFields: {},
472
+ certifierInfo: {}
473
+ }
474
+ const result = IdentityClient.parseIdentity(cert as any)
475
+ expect(result.badgeClickURL).toBe(defaultIdentity.badgeClickURL)
476
+ })
477
+
478
+ it('handles null certifierInfo gracefully', () => {
479
+ const cert = {
480
+ type: 'my-cert-type',
481
+ subject: 'sub1',
482
+ decryptedFields: {},
483
+ certifierInfo: null
484
+ }
485
+ const result = IdentityClient.parseIdentity(cert as any)
486
+ expect(result.badgeLabel).toBe(defaultIdentity.badgeLabel)
487
+ expect(result.badgeIconURL).toBe(defaultIdentity.badgeIconURL)
488
+ })
489
+
490
+ it('treats empty-string field values as absent (hasValue returns false)', () => {
491
+ const cert = {
492
+ type: 'my-cert-type',
493
+ subject: 'sub1',
494
+ decryptedFields: {
495
+ name: '',
496
+ userName: '',
497
+ firstName: '',
498
+ lastName: '',
499
+ email: ''
500
+ },
501
+ certifierInfo: {}
502
+ }
503
+ const result = IdentityClient.parseIdentity(cert as any)
504
+ expect(result.name).toBe(defaultIdentity.name)
505
+ })
506
+ })
507
+
508
+ // ─── resolveByIdentityKey: overrideWithContacts = false ────────────────────
509
+
510
+ describe('resolveByIdentityKey with overrideWithContacts=false', () => {
511
+ it('skips contacts and returns parsed certificates directly', async () => {
512
+ const dummyCertificate = {
513
+ type: KNOWN_IDENTITY_TYPES.xCert,
514
+ subject: 'aliceKey123456789',
515
+ decryptedFields: { userName: 'Alice', profilePhoto: 'photo.png' },
516
+ certifierInfo: { name: 'CertX', iconUrl: 'icon.png' }
517
+ }
518
+ walletMock.discoverByIdentityKey = jest.fn().mockResolvedValue({ certificates: [dummyCertificate] })
519
+
520
+ const mockContactsManager = identityClient['contactsManager']
521
+ mockContactsManager.getContacts = jest.fn().mockResolvedValue([{ name: 'Alice Contact', identityKey: 'aliceKey123456789' }])
522
+
523
+ const result = await identityClient.resolveByIdentityKey({ identityKey: 'aliceKey123456789' }, false)
524
+
525
+ expect(result).toHaveLength(1)
526
+ expect(result[0].name).toBe('Alice') // from cert, not contact
527
+ expect(mockContactsManager.getContacts).not.toHaveBeenCalled()
528
+ })
529
+
530
+ it('returns empty array when no certificates found and contacts skipped', async () => {
531
+ walletMock.discoverByIdentityKey = jest.fn().mockResolvedValue({ certificates: [] })
532
+
533
+ const result = await identityClient.resolveByIdentityKey({ identityKey: 'unknown-key' }, false)
534
+ expect(result).toEqual([])
535
+ })
536
+
537
+ it('handles undefined certificates result gracefully', async () => {
538
+ walletMock.discoverByIdentityKey = jest.fn().mockResolvedValue({ certificates: undefined })
539
+
540
+ const result = await identityClient.resolveByIdentityKey({ identityKey: 'some-key' }, false)
541
+ expect(result).toEqual([])
542
+ })
543
+ })
544
+
545
+ // ─── resolveByAttributes: additional branches ──────────────────────────────
546
+
547
+ describe('resolveByAttributes additional branches', () => {
548
+ it('handles null/undefined certificates result gracefully', async () => {
549
+ walletMock.discoverByAttributes = jest.fn().mockResolvedValue(null)
550
+ const result = await identityClient.resolveByAttributes({ attributes: { name: 'Alice' } }, false)
551
+ expect(result).toEqual([])
552
+ })
553
+
554
+ it('maps contact for subject when contact exists in map', async () => {
555
+ const contact = {
556
+ name: 'Alice From Contact',
557
+ identityKey: 'matched-key',
558
+ avatarURL: 'contact-avatar.png',
559
+ abbreviatedKey: 'matched-ke...',
560
+ badgeIconURL: '',
561
+ badgeLabel: '',
562
+ badgeClickURL: ''
563
+ }
564
+ const discoveredCertificate = {
565
+ type: KNOWN_IDENTITY_TYPES.emailCert,
566
+ subject: 'matched-key',
567
+ decryptedFields: { email: 'alice@example.com' },
568
+ certifierInfo: { name: 'EmailCert', iconUrl: '' }
569
+ }
570
+ const mockContactsManager = identityClient['contactsManager']
571
+ mockContactsManager.getContacts = jest.fn().mockResolvedValue([contact])
572
+ walletMock.discoverByAttributes = jest.fn().mockResolvedValue({ certificates: [discoveredCertificate] })
573
+
574
+ const result = await identityClient.resolveByAttributes({ attributes: { email: 'alice@example.com' } })
575
+ expect(result[0].name).toBe('Alice From Contact')
576
+ })
577
+
578
+ it('falls through to parseIdentity when no matching contact for subject', async () => {
579
+ const contact = {
580
+ name: 'Bob From Contact',
581
+ identityKey: 'bob-key',
582
+ avatarURL: '',
583
+ abbreviatedKey: '',
584
+ badgeIconURL: '',
585
+ badgeLabel: '',
586
+ badgeClickURL: ''
587
+ }
588
+ const discoveredCertificate = {
589
+ type: KNOWN_IDENTITY_TYPES.emailCert,
590
+ subject: 'alice-different-key',
591
+ decryptedFields: { email: 'alice@example.com' },
592
+ certifierInfo: { name: 'EmailCert', iconUrl: '' }
593
+ }
594
+ const mockContactsManager = identityClient['contactsManager']
595
+ mockContactsManager.getContacts = jest.fn().mockResolvedValue([contact])
596
+ walletMock.discoverByAttributes = jest.fn().mockResolvedValue({ certificates: [discoveredCertificate] })
597
+
598
+ const result = await identityClient.resolveByAttributes({ attributes: { email: 'alice@example.com' } })
599
+ expect(result[0].name).toBe('alice@example.com')
600
+ })
601
+ })
602
+
603
+ // ─── revokeCertificateRevelation ────────────────────────────────────────────
604
+
605
+ describe('revokeCertificateRevelation', () => {
606
+ const { LookupResolver, SHIPBroadcaster, withDoubleSpendRetry } = jest.requireMock('../../overlay-tools/index.js')
607
+
608
+ beforeEach(() => {
609
+ jest.clearAllMocks()
610
+ })
611
+
612
+ it('throws when lookup result type is not output-list', async () => {
613
+ LookupResolver.mockImplementation(() => ({
614
+ query: jest.fn().mockResolvedValue({ type: 'freeform', result: 'some data' })
615
+ }))
616
+
617
+ await expect(
618
+ identityClient.revokeCertificateRevelation('serialXYZ')
619
+ ).rejects.toThrow('Failed to get lookup result')
620
+ })
621
+
622
+ it('completes successfully with valid lookup output', async () => {
623
+ LookupResolver.mockImplementation(() => ({
624
+ query: jest.fn().mockResolvedValue({
625
+ type: 'output-list',
626
+ outputs: [{ beef: [1, 2, 3] }]
627
+ })
628
+ }))
629
+
630
+ SHIPBroadcaster.mockImplementation(() => ({
631
+ broadcast: jest.fn().mockResolvedValue('broadcasted')
632
+ }))
633
+
634
+ withDoubleSpendRetry.mockImplementation(async (fn: () => Promise<void>) => {
635
+ await fn()
636
+ })
637
+
638
+ const { Transaction } = jest.requireMock('../../transaction/index.js')
639
+ Transaction.fromBEEF.mockReturnValue({
640
+ id: jest.fn().mockReturnValue('mocktxid'),
641
+ outputs: [{ lockingScript: { toHex: () => 'scriptHex' } }]
642
+ })
643
+
644
+ walletMock.createAction = jest.fn().mockResolvedValue({
645
+ signableTransaction: { tx: [1, 2, 3], reference: 'ref' },
646
+ tx: undefined
647
+ })
648
+ walletMock.signAction = jest.fn().mockResolvedValue({ tx: [4, 5, 6] })
649
+
650
+ await expect(
651
+ identityClient.revokeCertificateRevelation('serialABC')
652
+ ).resolves.toBeUndefined()
653
+ })
654
+
655
+ it('throws when signableTransaction is undefined', async () => {
656
+ LookupResolver.mockImplementation(() => ({
657
+ query: jest.fn().mockResolvedValue({
658
+ type: 'output-list',
659
+ outputs: [{ beef: [1, 2, 3] }]
660
+ })
661
+ }))
662
+
663
+ SHIPBroadcaster.mockImplementation(() => ({
664
+ broadcast: jest.fn()
665
+ }))
666
+
667
+ withDoubleSpendRetry.mockImplementation(async (fn: () => Promise<void>) => {
668
+ await fn()
669
+ })
670
+
671
+ const { Transaction } = jest.requireMock('../../transaction/index.js')
672
+ Transaction.fromBEEF.mockReturnValue({
673
+ id: jest.fn().mockReturnValue('mocktxid'),
674
+ outputs: [{ lockingScript: { toHex: () => 'scriptHex' } }]
675
+ })
676
+
677
+ walletMock.createAction = jest.fn().mockResolvedValue({
678
+ signableTransaction: undefined,
679
+ tx: undefined
680
+ })
681
+
682
+ await expect(
683
+ identityClient.revokeCertificateRevelation('serialDEF')
684
+ ).rejects.toThrow('Failed to create signable transaction')
685
+ })
686
+
687
+ it('throws when signed tx is undefined after signAction', async () => {
688
+ LookupResolver.mockImplementation(() => ({
689
+ query: jest.fn().mockResolvedValue({
690
+ type: 'output-list',
691
+ outputs: [{ beef: [1, 2, 3] }]
692
+ })
693
+ }))
694
+
695
+ SHIPBroadcaster.mockImplementation(() => ({
696
+ broadcast: jest.fn()
697
+ }))
698
+
699
+ withDoubleSpendRetry.mockImplementation(async (fn: () => Promise<void>) => {
700
+ await fn()
701
+ })
702
+
703
+ const { Transaction } = jest.requireMock('../../transaction/index.js')
704
+ Transaction.fromBEEF.mockReturnValue({
705
+ id: jest.fn().mockReturnValue('mocktxid'),
706
+ outputs: [{ lockingScript: { toHex: () => 'scriptHex' } }]
707
+ })
708
+
709
+ walletMock.createAction = jest.fn().mockResolvedValue({
710
+ signableTransaction: { tx: [1, 2, 3], reference: 'ref' },
711
+ tx: undefined
712
+ })
713
+ walletMock.signAction = jest.fn().mockResolvedValue({ tx: undefined })
714
+
715
+ await expect(
716
+ identityClient.revokeCertificateRevelation('serialGHI')
717
+ ).rejects.toThrow('Failed to sign transaction')
718
+ })
719
+ })
720
+
721
+ // ─── constructor defaults ───────────────────────────────────────────────────
722
+
723
+ describe('constructor', () => {
724
+ it('defaults to WalletClient when no wallet provided', () => {
725
+ // Should not throw — WalletClient is instantiated internally
726
+ expect(() => new IdentityClient()).not.toThrow()
727
+ })
728
+
729
+ it('accepts an originator parameter', () => {
730
+ const client = new IdentityClient(walletMock as WalletInterface, undefined, 'example.com')
731
+ expect(client).toBeInstanceOf(IdentityClient)
732
+ })
733
+ })
734
+
735
+ // ─── getContacts / saveContact / removeContact delegation ──────────────────
736
+
737
+ describe('contact delegation methods', () => {
738
+ it('getContacts delegates to contactsManager', async () => {
739
+ const mockContactsManager = identityClient['contactsManager']
740
+ const expected = [{ name: 'Test', identityKey: 'key1', avatarURL: '', abbreviatedKey: '', badgeIconURL: '', badgeLabel: '', badgeClickURL: '' }]
741
+ mockContactsManager.getContacts = jest.fn().mockResolvedValue(expected)
742
+
743
+ const result = await identityClient.getContacts('key1', true, 50)
744
+ expect(mockContactsManager.getContacts).toHaveBeenCalledWith('key1', true, 50)
745
+ expect(result).toBe(expected)
746
+ })
747
+
748
+ it('saveContact delegates to contactsManager', async () => {
749
+ const mockContactsManager = identityClient['contactsManager']
750
+ mockContactsManager.saveContact = jest.fn().mockResolvedValue(undefined)
751
+
752
+ const contact = { name: 'Alice', identityKey: 'key1', avatarURL: '', abbreviatedKey: '', badgeIconURL: '', badgeLabel: '', badgeClickURL: '' }
753
+ const metadata = { note: 'test' }
754
+ await identityClient.saveContact(contact, metadata)
755
+
756
+ expect(mockContactsManager.saveContact).toHaveBeenCalledWith(contact, metadata)
757
+ })
758
+
759
+ it('removeContact delegates to contactsManager', async () => {
760
+ const mockContactsManager = identityClient['contactsManager']
761
+ mockContactsManager.removeContact = jest.fn().mockResolvedValue(undefined)
762
+
763
+ await identityClient.removeContact('key-to-remove')
764
+ expect(mockContactsManager.removeContact).toHaveBeenCalledWith('key-to-remove')
765
+ })
766
+ })
767
+ })