@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,750 @@
1
+ /**
2
+ * RegistryClient additional tests.
3
+ *
4
+ * Focuses on branches not covered by the primary test file:
5
+ * - updateDefinition (all paths)
6
+ * - removeDefinition itemIdentifier for unknown definitionType
7
+ * - listOwnRegistryEntries: non-spendable skip, parse errors, empty BEEF
8
+ * - resolve: protocol and certificate parsing, JSON parse error in certificate fields
9
+ * - deserializeWalletProtocol: all validation error paths
10
+ * - getIdentityKey: caching behaviour
11
+ * - getNetwork: caching behaviour
12
+ */
13
+
14
+ import { RegistryClient, deserializeWalletProtocol } from '../RegistryClient'
15
+ import { WalletInterface } from '../../wallet/index.js'
16
+ import { TopicBroadcaster, LookupResolver } from '../../overlay-tools/index.js'
17
+ import { PushDrop } from '../../script/index.js'
18
+ import {
19
+ DefinitionType,
20
+ DefinitionData,
21
+ ProtocolDefinitionData,
22
+ CertificateDefinitionData,
23
+ RegistryRecord,
24
+ CertificateFieldDescriptor
25
+ } from '../types/index.js'
26
+
27
+ // -------------------- Module-level mocks -------------------- //
28
+
29
+ const mockBroadcast = jest.fn().mockResolvedValue('broadcastSuccess')
30
+
31
+ jest.mock('../../overlay-tools/index.js', () => ({
32
+ TopicBroadcaster: jest.fn().mockImplementation(() => ({
33
+ broadcast: mockBroadcast
34
+ })),
35
+ LookupResolver: jest.fn().mockImplementation(() => ({
36
+ query: jest.fn()
37
+ }))
38
+ }))
39
+
40
+ jest.mock('../../script/index.js', () => {
41
+ const actual = jest.requireActual('../../script/index.js')
42
+ return {
43
+ ...actual,
44
+ PushDrop: Object.assign(
45
+ jest.fn().mockImplementation(() => ({
46
+ lock: jest.fn().mockResolvedValue({ toHex: () => 'mockLockHex' }),
47
+ unlock: jest.fn().mockReturnValue({
48
+ sign: jest.fn().mockResolvedValue({ toHex: () => 'mockUnlockHex' })
49
+ })
50
+ })),
51
+ { decode: jest.fn() }
52
+ ),
53
+ LockingScript: {
54
+ fromHex: jest.fn().mockImplementation((hex: string) => ({ hex }))
55
+ }
56
+ }
57
+ })
58
+
59
+ jest.mock('../../transaction/index.js', () => ({
60
+ Transaction: {
61
+ fromAtomicBEEF: jest.fn().mockImplementation(() => ({
62
+ outputs: [
63
+ { lockingScript: 'mockLS0' },
64
+ { lockingScript: 'mockLS1' }
65
+ ]
66
+ })),
67
+ fromBEEF: jest.fn().mockImplementation(() => ({
68
+ outputs: [
69
+ { lockingScript: { toHex: () => 'mockLSHex0' } },
70
+ { lockingScript: { toHex: () => 'mockLSHex1' } },
71
+ { lockingScript: { toHex: () => 'mockLSHex2' } }
72
+ ]
73
+ }))
74
+ }
75
+ }))
76
+
77
+ jest.mock('../../primitives/index.js', () => ({
78
+ Utils: {
79
+ toArray: jest.fn().mockImplementation((str: string) =>
80
+ Array.from(str).map((c) => c.charCodeAt(0))
81
+ ),
82
+ toUTF8: jest.fn().mockImplementation((arr: number[] | string) => {
83
+ if (Array.isArray(arr)) return arr.map((n) => String.fromCharCode(n)).join('')
84
+ return arr
85
+ })
86
+ }
87
+ }))
88
+
89
+ // -------------------- Test helpers -------------------- //
90
+
91
+ const TEST_ORIGINATOR = 'test.additional.origin'
92
+ const MOCK_PUB_KEY = 'mockPublicKey'
93
+
94
+ function buildWalletMock (): jest.Mocked<Partial<WalletInterface>> {
95
+ return {
96
+ getPublicKey: jest.fn().mockResolvedValue({ publicKey: MOCK_PUB_KEY }),
97
+ createAction: jest.fn().mockResolvedValue({
98
+ tx: [1, 2, 3],
99
+ signableTransaction: { tx: [1, 2, 3], reference: 'ref123' }
100
+ }),
101
+ signAction: jest.fn().mockResolvedValue({ tx: [7, 8, 9] }),
102
+ listOutputs: jest.fn().mockResolvedValue({ outputs: [], BEEF: [] }),
103
+ getNetwork: jest.fn().mockResolvedValue({ network: 'testnet' })
104
+ }
105
+ }
106
+
107
+ function buildClient (wallet: Partial<WalletInterface>): RegistryClient {
108
+ const client = new RegistryClient(wallet as WalletInterface, {}, TEST_ORIGINATOR)
109
+ ;(client as any).resolver = {
110
+ query: jest.fn().mockResolvedValue({ type: 'output-list', outputs: [] })
111
+ }
112
+ return client
113
+ }
114
+
115
+ const baseRegistryRecord: RegistryRecord = {
116
+ definitionType: 'basket',
117
+ basketID: 'testBasket',
118
+ name: 'Test Basket',
119
+ iconURL: 'https://icon.com',
120
+ description: 'A basket',
121
+ documentationURL: 'https://docs.com',
122
+ txid: 'txid123',
123
+ outputIndex: 0,
124
+ satoshis: 1,
125
+ lockingScript: 'lockhex',
126
+ registryOperator: MOCK_PUB_KEY,
127
+ beef: [1, 2, 3]
128
+ }
129
+
130
+ // -------------------- deserializeWalletProtocol -------------------- //
131
+
132
+ describe('deserializeWalletProtocol', () => {
133
+ it('parses a valid protocol string', () => {
134
+ const result = deserializeWalletProtocol(JSON.stringify([1, 'my-protocol']))
135
+ expect(result).toEqual([1, 'my-protocol'])
136
+ })
137
+
138
+ it('throws for non-array input', () => {
139
+ expect(() => deserializeWalletProtocol('"not-array"')).toThrow(
140
+ 'Invalid wallet protocol format.'
141
+ )
142
+ })
143
+
144
+ it('throws for array with wrong length', () => {
145
+ expect(() => deserializeWalletProtocol(JSON.stringify([1]))).toThrow(
146
+ 'Invalid wallet protocol format.'
147
+ )
148
+ })
149
+
150
+ it('throws for invalid security level', () => {
151
+ expect(() => deserializeWalletProtocol(JSON.stringify([3, 'proto']))).toThrow(
152
+ 'Invalid security level.'
153
+ )
154
+ })
155
+
156
+ it('accepts security level 0', () => {
157
+ const result = deserializeWalletProtocol(JSON.stringify([0, 'proto']))
158
+ expect(result[0]).toBe(0)
159
+ })
160
+
161
+ it('accepts security level 2', () => {
162
+ const result = deserializeWalletProtocol(JSON.stringify([2, 'proto']))
163
+ expect(result[0]).toBe(2)
164
+ })
165
+
166
+ it('throws for non-string protocol string', () => {
167
+ expect(() => deserializeWalletProtocol(JSON.stringify([1, 42]))).toThrow(
168
+ 'Invalid protocolID'
169
+ )
170
+ })
171
+
172
+ it('throws for completely invalid JSON', () => {
173
+ expect(() => deserializeWalletProtocol('not-json')).toThrow()
174
+ })
175
+ })
176
+
177
+ // -------------------- getIdentityKey caching -------------------- //
178
+
179
+ describe('RegistryClient.getIdentityKey – caching', () => {
180
+ it('calls getPublicKey only once on repeated calls', async () => {
181
+ const wallet = buildWalletMock()
182
+ const client = buildClient(wallet)
183
+
184
+ // Trigger two operations that each call getIdentityKey internally
185
+ await client.registerDefinition({
186
+ definitionType: 'basket',
187
+ basketID: 'b1',
188
+ name: 'Basket 1',
189
+ iconURL: 'https://icon.com',
190
+ description: 'desc',
191
+ documentationURL: 'https://docs.com'
192
+ })
193
+ await client.registerDefinition({
194
+ definitionType: 'basket',
195
+ basketID: 'b2',
196
+ name: 'Basket 2',
197
+ iconURL: 'https://icon.com',
198
+ description: 'desc',
199
+ documentationURL: 'https://docs.com'
200
+ })
201
+
202
+ // getPublicKey should only have been called once (identity key was cached)
203
+ expect(wallet.getPublicKey).toHaveBeenCalledTimes(1)
204
+ })
205
+ })
206
+
207
+ // -------------------- getNetwork caching -------------------- //
208
+
209
+ describe('RegistryClient.getNetwork – caching', () => {
210
+ it('calls wallet.getNetwork only once even across multiple operations', async () => {
211
+ const wallet = buildWalletMock()
212
+ const client = buildClient(wallet)
213
+
214
+ await client.registerDefinition({
215
+ definitionType: 'basket',
216
+ basketID: 'b1',
217
+ name: 'Basket 1',
218
+ iconURL: 'https://icon.com',
219
+ description: 'desc',
220
+ documentationURL: 'https://docs.com'
221
+ })
222
+ await client.registerDefinition({
223
+ definitionType: 'basket',
224
+ basketID: 'b2',
225
+ name: 'Basket 2',
226
+ iconURL: 'https://icon.com',
227
+ description: 'desc',
228
+ documentationURL: 'https://docs.com'
229
+ })
230
+
231
+ expect(wallet.getNetwork).toHaveBeenCalledTimes(1)
232
+ })
233
+ })
234
+
235
+ // -------------------- updateDefinition -------------------- //
236
+
237
+ describe('RegistryClient.updateDefinition', () => {
238
+ let wallet: jest.Mocked<Partial<WalletInterface>>
239
+ let client: RegistryClient
240
+
241
+ beforeEach(() => {
242
+ wallet = buildWalletMock()
243
+ client = buildClient(wallet)
244
+ jest.clearAllMocks()
245
+ mockBroadcast.mockClear()
246
+ mockBroadcast.mockResolvedValue('broadcastSuccess')
247
+ })
248
+
249
+ it('throws if txid is missing from the record', async () => {
250
+ const record = { ...baseRegistryRecord, txid: undefined }
251
+ const updated: DefinitionData = { ...baseRegistryRecord }
252
+ await expect(client.updateDefinition(record as any, updated)).rejects.toThrow(
253
+ 'Invalid registry record. Missing txid, outputIndex, or lockingScript.'
254
+ )
255
+ })
256
+
257
+ it('throws if outputIndex is missing from the record', async () => {
258
+ const record = { ...baseRegistryRecord, outputIndex: undefined }
259
+ const updated: DefinitionData = { ...baseRegistryRecord }
260
+ await expect(client.updateDefinition(record as any, updated)).rejects.toThrow(
261
+ 'Invalid registry record. Missing txid, outputIndex, or lockingScript.'
262
+ )
263
+ })
264
+
265
+ it('throws if lockingScript is missing from the record', async () => {
266
+ const record = { ...baseRegistryRecord, lockingScript: undefined }
267
+ const updated: DefinitionData = { ...baseRegistryRecord }
268
+ await expect(client.updateDefinition(record as any, updated)).rejects.toThrow(
269
+ 'Invalid registry record. Missing txid, outputIndex, or lockingScript.'
270
+ )
271
+ })
272
+
273
+ it('throws if definitionType does not match updatedData', async () => {
274
+ const record = { ...baseRegistryRecord }
275
+ const updated: DefinitionData = {
276
+ definitionType: 'protocol',
277
+ protocolID: [1, 'p'],
278
+ name: 'P',
279
+ iconURL: 'u',
280
+ description: 'd',
281
+ documentationURL: 'doc'
282
+ }
283
+ await expect(client.updateDefinition(record, updated)).rejects.toThrow(
284
+ 'Cannot change definition type from basket to protocol'
285
+ )
286
+ })
287
+
288
+ it('throws if the registry record does not belong to the current wallet', async () => {
289
+ const record = { ...baseRegistryRecord, registryOperator: 'differentKey' }
290
+ const updated: DefinitionData = { ...baseRegistryRecord }
291
+ await expect(client.updateDefinition(record, updated)).rejects.toThrow(
292
+ 'This registry token does not belong to the current wallet.'
293
+ )
294
+ })
295
+
296
+ it('throws if createAction returns no signableTransaction', async () => {
297
+ ;(wallet.createAction as jest.Mock).mockResolvedValueOnce({
298
+ tx: [1, 2, 3],
299
+ signableTransaction: undefined
300
+ })
301
+ const record = { ...baseRegistryRecord }
302
+ const updated: DefinitionData = { ...baseRegistryRecord }
303
+ await expect(client.updateDefinition(record, updated)).rejects.toThrow(
304
+ 'Failed to create signable transaction.'
305
+ )
306
+ })
307
+
308
+ it('throws if signAction returns no tx', async () => {
309
+ ;(wallet.signAction as jest.Mock).mockResolvedValueOnce({ tx: undefined })
310
+ const record = { ...baseRegistryRecord }
311
+ const updated: DefinitionData = { ...baseRegistryRecord }
312
+ await expect(client.updateDefinition(record, updated)).rejects.toThrow(
313
+ 'Failed to finalize the transaction signature.'
314
+ )
315
+ })
316
+
317
+ it('successfully updates a basket definition and broadcasts', async () => {
318
+ const record = { ...baseRegistryRecord }
319
+ const updated: DefinitionData = {
320
+ definitionType: 'basket',
321
+ basketID: 'updatedBasket',
322
+ name: 'Updated Basket',
323
+ iconURL: 'https://newicon.com',
324
+ description: 'Updated description',
325
+ documentationURL: 'https://newdocs.com'
326
+ }
327
+
328
+ const result = await client.updateDefinition(record, updated)
329
+
330
+ expect(result).toBe('broadcastSuccess')
331
+ expect(wallet.createAction).toHaveBeenCalledWith(
332
+ expect.objectContaining({
333
+ description: 'Update basket item: testBasket',
334
+ inputs: [expect.objectContaining({ outpoint: 'txid123.0' })],
335
+ outputs: [expect.objectContaining({ basket: 'basketmap' })]
336
+ }),
337
+ TEST_ORIGINATOR
338
+ )
339
+ expect(wallet.signAction).toHaveBeenCalledWith(
340
+ expect.objectContaining({ reference: 'ref123' }),
341
+ TEST_ORIGINATOR
342
+ )
343
+ expect(TopicBroadcaster).toHaveBeenCalledWith(
344
+ ['tm_basketmap'],
345
+ expect.objectContaining({ networkPreset: 'testnet' })
346
+ )
347
+ })
348
+
349
+ it('successfully updates a protocol definition and uses protocol item identifier', async () => {
350
+ const protocolRecord: RegistryRecord = {
351
+ definitionType: 'protocol',
352
+ protocolID: [1, 'oldProto'],
353
+ name: 'Old Protocol',
354
+ iconURL: 'https://icon.com',
355
+ description: 'desc',
356
+ documentationURL: 'https://docs.com',
357
+ txid: 'protTxid',
358
+ outputIndex: 0,
359
+ satoshis: 1,
360
+ lockingScript: 'lockhex',
361
+ registryOperator: MOCK_PUB_KEY,
362
+ beef: [1, 2, 3]
363
+ }
364
+ const updated: ProtocolDefinitionData = {
365
+ definitionType: 'protocol',
366
+ protocolID: [1, 'newProto'],
367
+ name: 'New Protocol',
368
+ iconURL: 'https://icon.com',
369
+ description: 'Updated desc',
370
+ documentationURL: 'https://newdocs.com'
371
+ }
372
+
373
+ const result = await client.updateDefinition(protocolRecord, updated)
374
+
375
+ expect(result).toBe('broadcastSuccess')
376
+ expect(wallet.createAction).toHaveBeenCalledWith(
377
+ expect.objectContaining({
378
+ description: 'Update protocol item: Old Protocol'
379
+ }),
380
+ TEST_ORIGINATOR
381
+ )
382
+ expect(TopicBroadcaster).toHaveBeenCalledWith(['tm_protomap'], expect.anything())
383
+ })
384
+
385
+ it('uses certificate type when name is undefined in the item identifier', async () => {
386
+ const certRecord: RegistryRecord = {
387
+ definitionType: 'certificate',
388
+ type: 'certType123',
389
+ name: undefined as any,
390
+ iconURL: 'https://icon.com',
391
+ description: 'desc',
392
+ documentationURL: 'https://docs.com',
393
+ fields: {},
394
+ txid: 'certTxid',
395
+ outputIndex: 0,
396
+ satoshis: 1,
397
+ lockingScript: 'lockhex',
398
+ registryOperator: MOCK_PUB_KEY,
399
+ beef: [1, 2, 3]
400
+ }
401
+ const updated: CertificateDefinitionData = {
402
+ definitionType: 'certificate',
403
+ type: 'certType123',
404
+ name: 'New Cert Name',
405
+ iconURL: 'https://icon.com',
406
+ description: 'Updated',
407
+ documentationURL: 'https://docs.com',
408
+ fields: {}
409
+ }
410
+
411
+ const result = await client.updateDefinition(certRecord, updated)
412
+
413
+ expect(result).toBe('broadcastSuccess')
414
+ expect(wallet.createAction).toHaveBeenCalledWith(
415
+ expect.objectContaining({
416
+ description: 'Update certificate item: certType123'
417
+ }),
418
+ TEST_ORIGINATOR
419
+ )
420
+ })
421
+
422
+ it('uses certificate name when defined for the item identifier', async () => {
423
+ const certRecord: RegistryRecord = {
424
+ definitionType: 'certificate',
425
+ type: 'certType456',
426
+ name: 'Named Cert',
427
+ iconURL: 'https://icon.com',
428
+ description: 'desc',
429
+ documentationURL: 'https://docs.com',
430
+ fields: {},
431
+ txid: 'certTxid2',
432
+ outputIndex: 0,
433
+ satoshis: 1,
434
+ lockingScript: 'lockhex',
435
+ registryOperator: MOCK_PUB_KEY,
436
+ beef: [1, 2, 3]
437
+ }
438
+ const updated: CertificateDefinitionData = {
439
+ definitionType: 'certificate',
440
+ type: 'certType456',
441
+ name: 'Updated Cert',
442
+ iconURL: 'https://icon.com',
443
+ description: 'desc',
444
+ documentationURL: 'https://docs.com',
445
+ fields: {}
446
+ }
447
+
448
+ await client.updateDefinition(certRecord, updated)
449
+
450
+ expect(wallet.createAction).toHaveBeenCalledWith(
451
+ expect.objectContaining({
452
+ description: 'Update certificate item: Named Cert'
453
+ }),
454
+ TEST_ORIGINATOR
455
+ )
456
+ })
457
+
458
+ it('uses acceptDelayedBroadcast: true when configured', async () => {
459
+ const delayedClient = new RegistryClient(
460
+ wallet as WalletInterface,
461
+ { acceptDelayedBroadcast: true },
462
+ TEST_ORIGINATOR
463
+ )
464
+ ;(delayedClient as any).resolver = {
465
+ query: jest.fn().mockResolvedValue({ type: 'output-list', outputs: [] })
466
+ }
467
+
468
+ const record = { ...baseRegistryRecord }
469
+ const updated: DefinitionData = { ...baseRegistryRecord }
470
+ await delayedClient.updateDefinition(record, updated)
471
+
472
+ expect(wallet.createAction).toHaveBeenCalledWith(
473
+ expect.objectContaining({
474
+ options: expect.objectContaining({ acceptDelayedBroadcast: true })
475
+ }),
476
+ TEST_ORIGINATOR
477
+ )
478
+ expect(wallet.signAction).toHaveBeenCalledWith(
479
+ expect.objectContaining({
480
+ options: expect.objectContaining({ acceptDelayedBroadcast: true })
481
+ }),
482
+ TEST_ORIGINATOR
483
+ )
484
+ })
485
+
486
+ it('propagates broadcast errors', async () => {
487
+ mockBroadcast.mockRejectedValueOnce(new Error('Update broadcast failed!'))
488
+ const record = { ...baseRegistryRecord }
489
+ const updated: DefinitionData = { ...baseRegistryRecord }
490
+ await expect(client.updateDefinition(record, updated)).rejects.toThrow('Update broadcast failed!')
491
+ })
492
+ })
493
+
494
+ // -------------------- removeDefinition: unknown identifier -------------------- //
495
+
496
+ describe('RegistryClient.removeDefinition – itemIdentifier edge cases', () => {
497
+ let wallet: jest.Mocked<Partial<WalletInterface>>
498
+ let client: RegistryClient
499
+
500
+ beforeEach(() => {
501
+ wallet = buildWalletMock()
502
+ client = buildClient(wallet)
503
+ jest.clearAllMocks()
504
+ mockBroadcast.mockClear()
505
+ mockBroadcast.mockResolvedValue('broadcastSuccess')
506
+ })
507
+
508
+ it('uses basketID as itemIdentifier for basket records', async () => {
509
+ const record = { ...baseRegistryRecord }
510
+ await client.removeDefinition(record)
511
+ expect(wallet.createAction).toHaveBeenCalledWith(
512
+ expect.objectContaining({
513
+ description: 'Remove basket item: testBasket'
514
+ }),
515
+ TEST_ORIGINATOR
516
+ )
517
+ })
518
+ })
519
+
520
+ // -------------------- listOwnRegistryEntries edge cases -------------------- //
521
+
522
+ describe('RegistryClient.listOwnRegistryEntries – edge cases', () => {
523
+ let wallet: jest.Mocked<Partial<WalletInterface>>
524
+ let client: RegistryClient
525
+
526
+ beforeEach(() => {
527
+ wallet = buildWalletMock()
528
+ client = buildClient(wallet)
529
+ jest.clearAllMocks()
530
+ })
531
+
532
+ it('returns empty array when no outputs are returned', async () => {
533
+ ;(wallet.listOutputs as jest.Mock).mockResolvedValue({ outputs: [], BEEF: [] })
534
+ const results = await client.listOwnRegistryEntries('basket')
535
+ expect(results).toEqual([])
536
+ })
537
+
538
+ it('skips non-spendable outputs', async () => {
539
+ ;(wallet.listOutputs as jest.Mock).mockResolvedValue({
540
+ outputs: [
541
+ { outpoint: 'tx1.0', satoshis: 1, spendable: false },
542
+ { outpoint: 'tx2.0', satoshis: 1, spendable: false }
543
+ ],
544
+ BEEF: [1, 2, 3]
545
+ })
546
+ const results = await client.listOwnRegistryEntries('basket')
547
+ expect(results).toEqual([])
548
+ expect(PushDrop.decode).not.toHaveBeenCalled()
549
+ })
550
+
551
+ it('skips spendable outputs that fail to parse (catches error silently)', async () => {
552
+ ;(wallet.listOutputs as jest.Mock).mockResolvedValue({
553
+ outputs: [{ outpoint: 'badtx.0', satoshis: 1, spendable: true }],
554
+ BEEF: [1, 2, 3]
555
+ })
556
+ // Make decode throw to simulate parse failure
557
+ ;(PushDrop.decode as jest.Mock).mockImplementationOnce(() => {
558
+ throw new Error('Invalid script')
559
+ })
560
+ const results = await client.listOwnRegistryEntries('basket')
561
+ expect(results).toEqual([])
562
+ })
563
+
564
+ it('returns records for all valid spendable outputs', async () => {
565
+ ;(wallet.listOutputs as jest.Mock).mockResolvedValue({
566
+ outputs: [
567
+ { outpoint: 'tx1.0', satoshis: 1, spendable: true },
568
+ { outpoint: 'tx2.0', satoshis: 2, spendable: true }
569
+ ],
570
+ BEEF: [0, 1, 2, 3]
571
+ })
572
+ // Basket has 7 fields
573
+ ;(PushDrop.decode as jest.Mock).mockReturnValue({
574
+ fields: [
575
+ [98], // basketID: 'b'
576
+ [97], // name: 'a'
577
+ [115], // iconURL: 's'
578
+ [107], // description: 'k'
579
+ [101], // documentationURL: 'e'
580
+ [116], // operator: 't'
581
+ [111] // signature field
582
+ ]
583
+ })
584
+
585
+ const results = await client.listOwnRegistryEntries('basket')
586
+ expect(results).toHaveLength(2)
587
+ expect(results[0].txid).toBe('tx1')
588
+ expect(results[1].txid).toBe('tx2')
589
+ })
590
+
591
+ it('uses protomap basket for protocol definition type', async () => {
592
+ ;(wallet.listOutputs as jest.Mock).mockResolvedValue({ outputs: [], BEEF: [] })
593
+ await client.listOwnRegistryEntries('protocol')
594
+ expect(wallet.listOutputs).toHaveBeenCalledWith({
595
+ basket: 'protomap',
596
+ include: 'entire transactions'
597
+ })
598
+ })
599
+
600
+ it('uses certmap basket for certificate definition type', async () => {
601
+ ;(wallet.listOutputs as jest.Mock).mockResolvedValue({ outputs: [], BEEF: [] })
602
+ await client.listOwnRegistryEntries('certificate')
603
+ expect(wallet.listOutputs).toHaveBeenCalledWith({
604
+ basket: 'certmap',
605
+ include: 'entire transactions'
606
+ })
607
+ })
608
+ })
609
+
610
+ // -------------------- resolve: protocol and certificate parsing -------------------- //
611
+
612
+ describe('RegistryClient.resolve – protocol and certificate parsing', () => {
613
+ let wallet: jest.Mocked<Partial<WalletInterface>>
614
+ let client: RegistryClient
615
+
616
+ beforeEach(() => {
617
+ wallet = buildWalletMock()
618
+ client = buildClient(wallet)
619
+ jest.clearAllMocks()
620
+ })
621
+
622
+ it('parses a protocol output from the resolver', async () => {
623
+ ;(client as any).resolver.query = jest.fn().mockResolvedValue({
624
+ type: 'output-list',
625
+ outputs: [{ beef: [1, 2, 3], outputIndex: 0 }]
626
+ })
627
+
628
+ // protocol has 7 fields: protocolID, name, iconURL, description, docURL, operator, sig
629
+ ;(PushDrop.decode as jest.Mock).mockReturnValueOnce({
630
+ fields: [
631
+ Array.from('[1,"proto"]').map((c) => c.charCodeAt(0)), // protocolID JSON
632
+ [110, 97, 109, 101], // 'name'
633
+ [105, 99, 111, 110], // 'icon'
634
+ [100, 101, 115, 99], // 'desc'
635
+ [100, 111, 99], // 'doc'
636
+ [111, 112], // 'op' - operator
637
+ [115, 105, 103] // signature
638
+ ]
639
+ })
640
+
641
+ const result = await client.resolve('protocol', { name: 'proto' })
642
+ expect(result).toHaveLength(1)
643
+ expect(result[0].definitionType).toBe('protocol')
644
+ })
645
+
646
+ it('parses a certificate output from the resolver', async () => {
647
+ ;(client as any).resolver.query = jest.fn().mockResolvedValue({
648
+ type: 'output-list',
649
+ outputs: [{ beef: [1, 2, 3], outputIndex: 0 }]
650
+ })
651
+
652
+ // certificate has 8 fields: type, name, iconURL, desc, docURL, fieldsJSON, operator, sig
653
+ const fieldsJSON = JSON.stringify({ field1: { friendlyName: 'Field One', description: 'd', type: 'text', fieldIcon: 'i' } })
654
+ ;(PushDrop.decode as jest.Mock).mockReturnValueOnce({
655
+ fields: [
656
+ [116, 121, 112, 101], // 'type'
657
+ [110, 97, 109, 101], // 'name'
658
+ [105, 99, 111, 110], // 'icon'
659
+ [100, 101, 115, 99], // 'desc'
660
+ [100, 111, 99], // 'doc'
661
+ Array.from(fieldsJSON).map((c) => c.charCodeAt(0)), // fieldsJSON
662
+ [111, 112], // 'op' - operator
663
+ [115, 105, 103] // signature
664
+ ]
665
+ })
666
+
667
+ const result = await client.resolve('certificate', { type: 'type' })
668
+ expect(result).toHaveLength(1)
669
+ expect(result[0].definitionType).toBe('certificate')
670
+ })
671
+
672
+ it('uses empty fields object when certificate fieldsJSON is invalid JSON', async () => {
673
+ ;(client as any).resolver.query = jest.fn().mockResolvedValue({
674
+ type: 'output-list',
675
+ outputs: [{ beef: [1, 2, 3], outputIndex: 0 }]
676
+ })
677
+
678
+ // Invalid JSON for fieldsJSON
679
+ ;(PushDrop.decode as jest.Mock).mockReturnValueOnce({
680
+ fields: [
681
+ [116, 121, 112, 101], // 'type'
682
+ [110, 97, 109, 101], // 'name'
683
+ [105, 99, 111, 110], // 'icon'
684
+ [100, 101, 115, 99], // 'desc'
685
+ [100, 111, 99], // 'doc'
686
+ [123, 105, 110, 118], // '{inv' - invalid JSON
687
+ [111, 112], // operator
688
+ [115, 105, 103] // signature
689
+ ]
690
+ })
691
+
692
+ const result = await client.resolve('certificate', { type: 'type' })
693
+ expect(result).toHaveLength(1)
694
+ expect((result[0] as CertificateDefinitionData).fields).toEqual({})
695
+ })
696
+
697
+ it('skips outputs with wrong field count for basket (not 7)', async () => {
698
+ ;(client as any).resolver.query = jest.fn().mockResolvedValue({
699
+ type: 'output-list',
700
+ outputs: [{ beef: [1, 2, 3], outputIndex: 0 }]
701
+ })
702
+
703
+ // Return only 5 fields — should fail the basket field count check and be skipped
704
+ ;(PushDrop.decode as jest.Mock).mockReturnValueOnce({
705
+ fields: [[1], [2], [3], [4], [5]]
706
+ })
707
+
708
+ const result = await client.resolve('basket', {})
709
+ expect(result).toEqual([])
710
+ })
711
+
712
+ it('skips outputs with wrong field count for certificate (not 8)', async () => {
713
+ ;(client as any).resolver.query = jest.fn().mockResolvedValue({
714
+ type: 'output-list',
715
+ outputs: [{ beef: [1, 2, 3], outputIndex: 0 }]
716
+ })
717
+
718
+ // Return only 5 fields — should fail the certificate field count check
719
+ ;(PushDrop.decode as jest.Mock).mockReturnValueOnce({
720
+ fields: [[1], [2], [3], [4], [5]]
721
+ })
722
+
723
+ const result = await client.resolve('certificate', {})
724
+ expect(result).toEqual([])
725
+ })
726
+ })
727
+
728
+ // -------------------- registerDefinition: network preset used -------------------- //
729
+
730
+ describe('RegistryClient.registerDefinition – network preset', () => {
731
+ it('passes testnet to TopicBroadcaster when wallet returns testnet', async () => {
732
+ const wallet = buildWalletMock()
733
+ ;(wallet.getNetwork as jest.Mock).mockResolvedValue({ network: 'testnet' })
734
+ const client = buildClient(wallet)
735
+
736
+ await client.registerDefinition({
737
+ definitionType: 'basket',
738
+ basketID: 'b1',
739
+ name: 'Basket',
740
+ iconURL: 'u',
741
+ description: 'd',
742
+ documentationURL: 'doc'
743
+ })
744
+
745
+ expect(TopicBroadcaster).toHaveBeenCalledWith(
746
+ expect.anything(),
747
+ expect.objectContaining({ networkPreset: 'testnet' })
748
+ )
749
+ })
750
+ })