@bsv/sdk 1.4.0 → 1.4.1

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 (58) hide show
  1. package/dist/cjs/mod.js +2 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/src/identity/IdentityClient.js +258 -0
  5. package/dist/cjs/src/identity/IdentityClient.js.map +1 -0
  6. package/dist/cjs/src/identity/index.js +19 -0
  7. package/dist/cjs/src/identity/index.js.map +1 -0
  8. package/dist/cjs/src/identity/types/index.js +30 -0
  9. package/dist/cjs/src/identity/types/index.js.map +1 -0
  10. package/dist/cjs/src/registry/RegistryClient.js +392 -0
  11. package/dist/cjs/src/registry/RegistryClient.js.map +1 -0
  12. package/dist/cjs/src/registry/index.js +19 -0
  13. package/dist/cjs/src/registry/index.js.map +1 -0
  14. package/dist/cjs/src/registry/types/index.js +3 -0
  15. package/dist/cjs/src/registry/types/index.js.map +1 -0
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/mod.js +2 -0
  18. package/dist/esm/mod.js.map +1 -1
  19. package/dist/esm/src/identity/IdentityClient.js +255 -0
  20. package/dist/esm/src/identity/IdentityClient.js.map +1 -0
  21. package/dist/esm/src/identity/index.js +3 -0
  22. package/dist/esm/src/identity/index.js.map +1 -0
  23. package/dist/esm/src/identity/types/index.js +27 -0
  24. package/dist/esm/src/identity/types/index.js.map +1 -0
  25. package/dist/esm/src/registry/RegistryClient.js +388 -0
  26. package/dist/esm/src/registry/RegistryClient.js.map +1 -0
  27. package/dist/esm/src/registry/index.js +3 -0
  28. package/dist/esm/src/registry/index.js.map +1 -0
  29. package/dist/esm/src/registry/types/index.js +2 -0
  30. package/dist/esm/src/registry/types/index.js.map +1 -0
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/mod.d.ts +2 -0
  33. package/dist/types/mod.d.ts.map +1 -1
  34. package/dist/types/src/identity/IdentityClient.d.ts +50 -0
  35. package/dist/types/src/identity/IdentityClient.d.ts.map +1 -0
  36. package/dist/types/src/identity/index.d.ts +3 -0
  37. package/dist/types/src/identity/index.d.ts.map +1 -0
  38. package/dist/types/src/identity/types/index.d.ts +30 -0
  39. package/dist/types/src/identity/types/index.d.ts.map +1 -0
  40. package/dist/types/src/registry/RegistryClient.d.ts +94 -0
  41. package/dist/types/src/registry/RegistryClient.d.ts.map +1 -0
  42. package/dist/types/src/registry/index.d.ts +3 -0
  43. package/dist/types/src/registry/index.d.ts.map +1 -0
  44. package/dist/types/src/registry/types/index.d.ts +86 -0
  45. package/dist/types/src/registry/types/index.d.ts.map +1 -0
  46. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  47. package/dist/umd/bundle.js +1 -1
  48. package/mod.ts +3 -1
  49. package/package.json +2 -2
  50. package/src/identity/IdentityClient.ts +305 -0
  51. package/src/identity/README.md +93 -0
  52. package/src/identity/__tests/IdentityClient.test.ts +278 -0
  53. package/src/identity/index.ts +2 -0
  54. package/src/identity/types/index.ts +46 -0
  55. package/src/registry/RegistryClient.ts +493 -0
  56. package/src/registry/__tests/RegistryClient.test.ts +444 -0
  57. package/src/registry/index.ts +2 -0
  58. package/src/registry/types/index.ts +101 -0
@@ -0,0 +1,444 @@
1
+ import { RegistryClient } from '../RegistryClient'
2
+ import { WalletInterface } from '../../wallet/index.js'
3
+ import { TopicBroadcaster, LookupResolver } from '../../overlay-tools/index.js'
4
+ import { PushDrop } from '../../script/index.js'
5
+ import {
6
+ DefinitionType,
7
+ DefinitionData,
8
+ BasketDefinitionData,
9
+ ProtocolDefinitionData,
10
+ CertificateDefinitionData,
11
+ RegistryRecord,
12
+ CertificateFieldDescriptor
13
+ } from '../types/index.js'
14
+
15
+ // -------------------- Mocks Setup -------------------- //
16
+
17
+ // 1) A top-level broadcast mock function
18
+ const mockBroadcast = jest.fn().mockResolvedValue('mockBroadcastSuccess')
19
+
20
+ jest.mock('../../overlay-tools/index.js', () => {
21
+ return {
22
+ TopicBroadcaster: jest.fn().mockImplementation(() => ({
23
+ broadcast: mockBroadcast
24
+ })),
25
+ LookupResolver: jest.fn().mockImplementation(() => ({
26
+ query: jest.fn()
27
+ }))
28
+ }
29
+ })
30
+
31
+ jest.mock('../../script/index.js', () => {
32
+ const actualScriptModule = jest.requireActual('../../script/index.js')
33
+ return {
34
+ ...actualScriptModule,
35
+ PushDrop: Object.assign(
36
+ jest.fn().mockImplementation(() => ({
37
+ lock: jest.fn().mockResolvedValue({ toHex: () => 'mockLockingScriptHex' }),
38
+ unlock: jest.fn().mockResolvedValue({
39
+ sign: jest.fn().mockResolvedValue({
40
+ toHex: () => 'mockUnlockingScriptHex'
41
+ })
42
+ })
43
+ })),
44
+ {
45
+ // Ensure decode is a Jest mock
46
+ decode: jest.fn()
47
+ }
48
+ ),
49
+ LockingScript: {
50
+ fromHex: jest.fn().mockImplementation((hex: string) => ({ hex }))
51
+ }
52
+ }
53
+ })
54
+
55
+ // Ensure `PushDrop.decode` is recognized as a Jest mock
56
+ ; (PushDrop as any).decode = jest.fn()
57
+
58
+ jest.mock('../../transaction/index.js', () => {
59
+ return {
60
+ Transaction: {
61
+ fromAtomicBEEF: jest.fn().mockImplementation((_tx: number[]) => ({
62
+ toHexBEEF: () => 'mockTxHexBEEF',
63
+ outputs: [
64
+ { lockingScript: 'mockLockingScriptObject0' },
65
+ { lockingScript: 'mockLockingScriptObject1' },
66
+ { lockingScript: 'mockLockingScriptObject2' }
67
+ ]
68
+ })),
69
+ fromBEEF: jest.fn().mockImplementation((_tx: number[]) => ({}))
70
+ }
71
+ }
72
+ })
73
+
74
+ jest.mock('../../primitives/index.js', () => {
75
+ return {
76
+ Utils: {
77
+ toArray: jest.fn().mockImplementation((str: string) =>
78
+ Array.from(str).map((c) => c.charCodeAt(0))
79
+ ),
80
+ toUTF8: jest.fn().mockImplementation((arr: number[] | string) => {
81
+ if (Array.isArray(arr)) {
82
+ return arr.map((n) => String.fromCharCode(n)).join('')
83
+ }
84
+ return arr
85
+ })
86
+ }
87
+ }
88
+ })
89
+
90
+ let walletMock: Partial<WalletInterface>
91
+
92
+ /**
93
+ * Build minimal valid DefinitionData for each type
94
+ */
95
+ function buildDefinitionData(type: DefinitionType): DefinitionData {
96
+ switch (type) {
97
+ case 'basket': {
98
+ const data: BasketDefinitionData = {
99
+ definitionType: 'basket',
100
+ basketID: 'someBasketId',
101
+ name: 'Test Basket',
102
+ iconURL: 'https://someiconurl.com',
103
+ description: 'Basket Description',
104
+ documentationURL: 'https://docs.basket.com'
105
+ }
106
+ return data
107
+ }
108
+ case 'protocol': {
109
+ const data: ProtocolDefinitionData = {
110
+ definitionType: 'protocol',
111
+ protocolID: 'someProtocolId',
112
+ securityLevel: 1,
113
+ name: 'Test Protocol',
114
+ iconURL: 'https://someiconurl.com',
115
+ description: 'Protocol Description',
116
+ documentationURL: 'https://docs.protocol.com'
117
+ }
118
+ return data
119
+ }
120
+ case 'certificate': {
121
+ const fields: Record<string, CertificateFieldDescriptor> = {
122
+ myField: {
123
+ friendlyName: 'Friendly Field Name',
124
+ description: 'some field description',
125
+ type: 'text',
126
+ fieldIcon: 'https://someiconurl.com/icons/myField.png'
127
+ }
128
+ }
129
+ const data: CertificateDefinitionData = {
130
+ definitionType: 'certificate',
131
+ type: 'someCertType',
132
+ name: 'Test Certificate',
133
+ iconURL: 'https://someiconurl.com',
134
+ description: 'Certificate Description',
135
+ documentationURL: 'https://docs.certificate.com',
136
+ fields
137
+ }
138
+ return data
139
+ }
140
+ default:
141
+ throw new Error(`Invalid test usage: unsupported DefinitionType "${type}"`)
142
+ }
143
+ }
144
+
145
+ describe('RegistryClient', () => {
146
+ let registryClient: RegistryClient
147
+
148
+ beforeEach(() => {
149
+ walletMock = {
150
+ getPublicKey: jest.fn().mockResolvedValue({ publicKey: 'mockPublicKey' }),
151
+ createAction: jest.fn().mockResolvedValue({
152
+ tx: [1, 2, 3],
153
+ signableTransaction: { tx: [1, 2, 3], reference: 'someRef' }
154
+ }),
155
+ signAction: jest.fn().mockResolvedValue({ tx: [4, 5, 6] }),
156
+ listOutputs: jest.fn().mockResolvedValue({ outputs: [] }),
157
+ getNetwork: jest.fn().mockResolvedValue({ network: 'main' })
158
+ }
159
+
160
+ registryClient = new RegistryClient(walletMock as WalletInterface)
161
+
162
+ // Clear all mock calls
163
+ jest.clearAllMocks()
164
+ mockBroadcast.mockClear()
165
+ })
166
+
167
+ // ------------------------------------------------------------------
168
+ // registerDefinition
169
+ // ------------------------------------------------------------------
170
+ describe('registerDefinition', () => {
171
+ it('should register a basket definition and broadcast with networkPreset=main', async () => {
172
+ const data = buildDefinitionData('basket')
173
+ const result = await registryClient.registerDefinition(data)
174
+ expect(result).toBe('mockBroadcastSuccess')
175
+
176
+ expect(walletMock.createAction).toHaveBeenCalledWith({
177
+ description: 'Register a new basket item',
178
+ outputs: expect.arrayContaining([
179
+ expect.objectContaining({
180
+ satoshis: 1,
181
+ outputDescription: 'New basket registration token'
182
+ })
183
+ ])
184
+ })
185
+ expect(TopicBroadcaster).toHaveBeenCalledWith(
186
+ ['tm_basketmap'],
187
+ { networkPreset: 'main' }
188
+ )
189
+ expect(mockBroadcast).toHaveBeenCalledTimes(1)
190
+ })
191
+
192
+ it('should register a protocol definition and broadcast with networkPreset=main', async () => {
193
+ const data = buildDefinitionData('protocol')
194
+ const result = await registryClient.registerDefinition(data)
195
+ expect(result).toBe('mockBroadcastSuccess')
196
+
197
+ expect(walletMock.createAction).toHaveBeenCalledWith({
198
+ description: 'Register a new protocol item',
199
+ outputs: expect.arrayContaining([
200
+ expect.objectContaining({
201
+ satoshis: 1,
202
+ outputDescription: 'New protocol registration token'
203
+ })
204
+ ])
205
+ })
206
+
207
+ expect(TopicBroadcaster).toHaveBeenCalledWith(
208
+ ['tm_protomap'],
209
+ { networkPreset: 'main' }
210
+ )
211
+ expect(mockBroadcast).toHaveBeenCalledTimes(1)
212
+ })
213
+
214
+ it('should register a certificate definition and broadcast with networkPreset=main', async () => {
215
+ const data = buildDefinitionData('certificate')
216
+ const result = await registryClient.registerDefinition(data)
217
+ expect(result).toBe('mockBroadcastSuccess')
218
+
219
+ expect(walletMock.createAction).toHaveBeenCalledWith({
220
+ description: 'Register a new certificate item',
221
+ outputs: expect.arrayContaining([
222
+ expect.objectContaining({
223
+ satoshis: 1,
224
+ outputDescription: 'New certificate registration token'
225
+ })
226
+ ])
227
+ })
228
+
229
+ expect(TopicBroadcaster).toHaveBeenCalledWith(
230
+ ['tm_certmap'],
231
+ { networkPreset: 'main' }
232
+ )
233
+ expect(mockBroadcast).toHaveBeenCalledTimes(1)
234
+ })
235
+
236
+ it('should throw if createAction returns undefined tx', async () => {
237
+ (walletMock.createAction as jest.Mock).mockResolvedValueOnce({
238
+ tx: undefined
239
+ })
240
+ const data = buildDefinitionData('basket')
241
+ await expect(registryClient.registerDefinition(data)).rejects.toThrow(
242
+ 'Failed to create basket registration transaction!'
243
+ )
244
+ })
245
+
246
+ it('should throw an error on invalid definition type', async () => {
247
+ const invalidData = { definitionType: 'invalidType' } as any as DefinitionData
248
+ await expect(registryClient.registerDefinition(invalidData)).rejects.toThrow(
249
+ 'Invalid registry kind specified'
250
+ )
251
+ })
252
+ })
253
+
254
+ // ------------------------------------------------------------------
255
+ // resolve
256
+ // ------------------------------------------------------------------
257
+ describe('resolve', () => {
258
+ it('should return empty array if resolver does not return output-list', async () => {
259
+ (LookupResolver as jest.Mock).mockImplementation(() => ({
260
+ query: jest.fn().mockResolvedValue({ type: 'unknown' })
261
+ }))
262
+
263
+ const result = await registryClient.resolve('basket', { name: 'foo' })
264
+ expect(result).toEqual([])
265
+ })
266
+
267
+ it('should parse outputs from resolver if type is output-list', async () => {
268
+ ; (LookupResolver as jest.Mock).mockImplementation(() => ({
269
+ query: jest.fn().mockResolvedValue({
270
+ type: 'output-list',
271
+ outputs: [
272
+ { beef: [9, 9, 9], outputIndex: 0 }
273
+ ]
274
+ })
275
+ }))
276
+
277
+ // Mock decode once, so the code that does `PushDrop.decode(...)` returns some fields
278
+ ; (PushDrop.decode as jest.Mock).mockReturnValue({
279
+ fields: [
280
+ [98], // 'b'
281
+ [97], // 'a'
282
+ [115], // 's'
283
+ [107], // 'k'
284
+ [101], // 'e'
285
+ [116] // 't' => operator
286
+ ]
287
+ })
288
+
289
+ // The final field must match the current wallet pubkey => 't'
290
+ ; (walletMock.getPublicKey as jest.Mock).mockResolvedValueOnce({ publicKey: 't' })
291
+
292
+ const result = await registryClient.resolve('basket', { basketID: 'whatever' })
293
+ expect(result).toHaveLength(1)
294
+ expect(result[0]).toMatchObject({
295
+ definitionType: 'basket',
296
+ basketID: 'b',
297
+ name: 'a',
298
+ iconURL: 's',
299
+ description: 'k',
300
+ documentationURL: 'e',
301
+ registryOperator: 't'
302
+ })
303
+ })
304
+
305
+ it('should skip outputs that fail parseLockingScript', async () => {
306
+ ; (LookupResolver as jest.Mock).mockImplementation(() => ({
307
+ query: jest.fn().mockResolvedValue({
308
+ type: 'output-list',
309
+ outputs: [
310
+ { beef: [1, 1, 1], outputIndex: 0 },
311
+ { beef: [2, 2, 2], outputIndex: 1 }
312
+ ]
313
+ })
314
+ }))
315
+
316
+ // We do two .mockReturnValueOnce calls so each decode returns something different.
317
+ ; (PushDrop.decode as jest.Mock)
318
+ .mockReturnValueOnce({ fields: [] })
319
+ .mockReturnValueOnce({ fields: [] })
320
+
321
+ const result = await registryClient.resolve('basket', { name: 'fooAgain' })
322
+ expect(result).toEqual([])
323
+ })
324
+ })
325
+
326
+ // ------------------------------------------------------------------
327
+ // listOwnRegistryEntries
328
+ // ------------------------------------------------------------------
329
+ describe('listOwnRegistryEntries', () => {
330
+ it('should parse and return registry records from wallet outputs', async () => {
331
+ // The wallet returns 3 outputs, but we skip any "spendable" or parse-failing ones
332
+ ; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
333
+ outputs: [
334
+ {
335
+ outpoint: 'abc123.0',
336
+ satoshis: 1000,
337
+ lockingScript: 'someLockingScriptHex',
338
+ spendable: false
339
+ },
340
+ {
341
+ outpoint: 'xyz999.1',
342
+ satoshis: 500,
343
+ lockingScript: 'badLockingScriptHex',
344
+ spendable: false
345
+ },
346
+ {
347
+ outpoint: 'skipMe.2',
348
+ satoshis: 200,
349
+ lockingScript: 'skipLockingScriptHex',
350
+ spendable: true
351
+ }
352
+ ]
353
+ })
354
+
355
+ // We decode the first output successfully => 6 fields => valid basket
356
+ // Then the second decode => parse fails
357
+ ; (PushDrop.decode as jest.Mock)
358
+ .mockReturnValueOnce({ fields: Array(6).fill([97]) })
359
+ .mockReturnValueOnce({ fields: [] })
360
+ ; (walletMock.getPublicKey as jest.Mock).mockResolvedValueOnce({ publicKey: 'a' })
361
+
362
+ const records = await registryClient.listOwnRegistryEntries('basket')
363
+ expect(walletMock.listOutputs).toHaveBeenCalledWith({
364
+ basket: 'basketmap',
365
+ include: 'locking scripts'
366
+ })
367
+ expect(records).toHaveLength(1)
368
+ expect(records[0]).toMatchObject({
369
+ definitionType: 'basket',
370
+ txid: 'abc123',
371
+ outputIndex: 0,
372
+ satoshis: 1000,
373
+ lockingScript: 'someLockingScriptHex'
374
+ })
375
+ })
376
+ })
377
+
378
+ // ------------------------------------------------------------------
379
+ // revokeOwnRegistryEntry
380
+ // ------------------------------------------------------------------
381
+ describe('revokeOwnRegistryEntry', () => {
382
+ let validRecord: RegistryRecord
383
+
384
+ beforeEach(() => {
385
+ validRecord = {
386
+ definitionType: 'basket',
387
+ basketID: 'myBasket',
388
+ name: 'whatever',
389
+ iconURL: 'url',
390
+ description: 'desc',
391
+ documentationURL: 'docURL',
392
+ txid: 'someTxId',
393
+ outputIndex: 0,
394
+ satoshis: 1000,
395
+ lockingScript: 'someLockingScriptHex',
396
+ registryOperator: 'mockPublicKey'
397
+ }
398
+ })
399
+
400
+ it('should revoke a record successfully (networkPreset=main)', async () => {
401
+ const result = await registryClient.revokeOwnRegistryEntry(validRecord)
402
+ expect(result).toBe('mockBroadcastSuccess')
403
+
404
+ expect(walletMock.createAction).toHaveBeenCalledWith({
405
+ description: 'Revoke basket item: myBasket',
406
+ inputs: [
407
+ {
408
+ outpoint: 'someTxId.0',
409
+ unlockingScriptLength: 73,
410
+ inputDescription: 'Revoking basket token'
411
+ }
412
+ ]
413
+ })
414
+
415
+ expect(TopicBroadcaster).toHaveBeenCalledWith(['tm_basketmap'], { networkPreset: 'main' })
416
+ expect(mockBroadcast).toHaveBeenCalled()
417
+ })
418
+
419
+ it('should throw if createAction returns no signableTransaction', async () => {
420
+ ; (walletMock.createAction as jest.Mock).mockResolvedValueOnce({
421
+ tx: [1, 2, 3],
422
+ signableTransaction: undefined
423
+ })
424
+ await expect(registryClient.revokeOwnRegistryEntry(validRecord)).rejects.toThrow(
425
+ 'Failed to create signable transaction.'
426
+ )
427
+ })
428
+
429
+ it('should throw if signAction returns no signedTx', async () => {
430
+ ; (walletMock.signAction as jest.Mock).mockResolvedValueOnce({ tx: undefined })
431
+ await expect(registryClient.revokeOwnRegistryEntry(validRecord)).rejects.toThrow(
432
+ 'Failed to finalize the transaction signature.'
433
+ )
434
+ })
435
+
436
+ it('should propagate broadcast errors', async () => {
437
+ mockBroadcast.mockRejectedValueOnce(new Error('Broadcast failure!'))
438
+
439
+ await expect(registryClient.revokeOwnRegistryEntry(validRecord)).rejects.toThrow(
440
+ 'Broadcast failure!'
441
+ )
442
+ })
443
+ })
444
+ })
@@ -0,0 +1,2 @@
1
+ export * from './RegistryClient.js'
2
+ export * from './types/index.js'
@@ -0,0 +1,101 @@
1
+ import { PubKeyHex } from '../../wallet/index.js'
2
+
3
+ /**
4
+ * Determines which category of registry item we are working with.
5
+ * - "basket" corresponds to BasketMap
6
+ * - "protocol" corresponds to ProtoMap
7
+ * - "certificate" corresponds to CertMap
8
+ */
9
+ export type DefinitionType = 'basket' | 'protocol' | 'certificate'
10
+
11
+ /**
12
+ * Describes a re-usable structure for certificate fields (used by CertMap).
13
+ */
14
+ export interface CertificateFieldDescriptor {
15
+ friendlyName: string
16
+ description: string
17
+ type: 'text' | 'imageURL' | 'other'
18
+ fieldIcon: string
19
+ }
20
+
21
+ /**
22
+ * Registry data for a Basket-style record (BasketMap).
23
+ */
24
+ export interface BasketDefinitionData {
25
+ definitionType: 'basket'
26
+ basketID: string
27
+ name: string
28
+ iconURL: string
29
+ description: string
30
+ documentationURL: string
31
+ registryOperator?: PubKeyHex
32
+ }
33
+
34
+ /**
35
+ * Registry data for a Proto-style record (ProtoMap).
36
+ */
37
+ export interface ProtocolDefinitionData {
38
+ definitionType: 'protocol'
39
+ protocolID: string
40
+ securityLevel: 0 | 1 | 2
41
+ name: string
42
+ iconURL: string
43
+ description: string
44
+ documentationURL: string
45
+ registryOperator?: PubKeyHex
46
+ }
47
+
48
+ /**
49
+ * Registry data for a Cert-style record (CertMap).
50
+ */
51
+ export interface CertificateDefinitionData {
52
+ definitionType: 'certificate'
53
+ type: string
54
+ name: string
55
+ iconURL: string
56
+ description: string
57
+ documentationURL: string
58
+ fields: Record<string, CertificateFieldDescriptor>
59
+ registryOperator?: PubKeyHex
60
+ }
61
+
62
+ export type DefinitionData =
63
+ | BasketDefinitionData
64
+ | ProtocolDefinitionData
65
+ | CertificateDefinitionData
66
+
67
+ export interface TokenData {
68
+ txid: string
69
+ outputIndex: number
70
+ satoshis: number
71
+ lockingScript: string
72
+ }
73
+
74
+ export type RegistryRecord = DefinitionData & TokenData
75
+
76
+ // Lookup Query Types (Note: can be shared types with lookup service)
77
+
78
+ interface BasketMapQuery {
79
+ basketID?: string
80
+ registryOperators?: string[]
81
+ name?: string
82
+ }
83
+
84
+ interface ProtoMapQuery {
85
+ name?: string
86
+ registryOperators?: string[]
87
+ protocolID?: string
88
+ securityLevel?: number
89
+ }
90
+
91
+ interface CertMapQuery {
92
+ type?: string
93
+ name?: string
94
+ registryOperators?: string[]
95
+ }
96
+
97
+ export interface RegistryQueryMapping {
98
+ basket: BasketMapQuery
99
+ protocol: ProtoMapQuery
100
+ certificate: CertMapQuery
101
+ }