@bsv/sdk 1.9.1 → 1.9.3

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 (44) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/kvstore/GlobalKVStore.js +116 -98
  3. package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -1
  4. package/dist/cjs/src/kvstore/types.js.map +1 -1
  5. package/dist/cjs/src/overlay-tools/index.js +1 -0
  6. package/dist/cjs/src/overlay-tools/index.js.map +1 -1
  7. package/dist/cjs/src/overlay-tools/withDoubleSpendRetry.js +55 -0
  8. package/dist/cjs/src/overlay-tools/withDoubleSpendRetry.js.map +1 -0
  9. package/dist/cjs/src/registry/RegistryClient.js +144 -25
  10. package/dist/cjs/src/registry/RegistryClient.js.map +1 -1
  11. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  12. package/dist/esm/src/kvstore/GlobalKVStore.js +117 -99
  13. package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -1
  14. package/dist/esm/src/kvstore/types.js.map +1 -1
  15. package/dist/esm/src/overlay-tools/index.js +1 -0
  16. package/dist/esm/src/overlay-tools/index.js.map +1 -1
  17. package/dist/esm/src/overlay-tools/withDoubleSpendRetry.js +48 -0
  18. package/dist/esm/src/overlay-tools/withDoubleSpendRetry.js.map +1 -0
  19. package/dist/esm/src/registry/RegistryClient.js +148 -26
  20. package/dist/esm/src/registry/RegistryClient.js.map +1 -1
  21. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  22. package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -1
  23. package/dist/types/src/kvstore/types.d.ts +2 -0
  24. package/dist/types/src/kvstore/types.d.ts.map +1 -1
  25. package/dist/types/src/overlay-tools/index.d.ts +1 -0
  26. package/dist/types/src/overlay-tools/index.d.ts.map +1 -1
  27. package/dist/types/src/overlay-tools/withDoubleSpendRetry.d.ts +14 -0
  28. package/dist/types/src/overlay-tools/withDoubleSpendRetry.d.ts.map +1 -0
  29. package/dist/types/src/registry/RegistryClient.d.ts +31 -5
  30. package/dist/types/src/registry/RegistryClient.d.ts.map +1 -1
  31. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  32. package/dist/umd/bundle.js +3 -3
  33. package/dist/umd/bundle.js.map +1 -1
  34. package/docs/reference/kvstore.md +9 -0
  35. package/docs/reference/overlay-tools.md +32 -0
  36. package/docs/reference/registry.md +34 -9
  37. package/package.json +1 -1
  38. package/src/kvstore/GlobalKVStore.ts +134 -114
  39. package/src/kvstore/__tests/GlobalKVStore.test.ts +11 -1
  40. package/src/kvstore/types.ts +2 -0
  41. package/src/overlay-tools/index.ts +1 -0
  42. package/src/overlay-tools/withDoubleSpendRetry.ts +71 -0
  43. package/src/registry/RegistryClient.ts +180 -36
  44. package/src/registry/__tests/RegistryClient.test.ts +211 -54
@@ -25,6 +25,7 @@ import {
25
25
  } from './types/index.js'
26
26
 
27
27
  const REGISTRANT_TOKEN_AMOUNT = 1
28
+ const REGISTRANT_KEY_ID = '1'
28
29
 
29
30
  /**
30
31
  * RegistryClient manages on-chain registry definitions for three types:
@@ -36,16 +37,47 @@ const REGISTRANT_TOKEN_AMOUNT = 1
36
37
  * - Register new definitions using pushdrop-based UTXOs.
37
38
  * - Resolve existing definitions using a lookup service.
38
39
  * - List registry entries associated with the operator's wallet.
39
- * - Revoke an existing registry entry by spending its UTXO.
40
+ * - Remove existing registry entries by spending their UTXOs.
41
+ * - Update existing registry entries.
40
42
  *
41
43
  * Registry operators use this client to establish and manage
42
44
  * canonical references for baskets, protocols, and certificate types.
43
45
  */
44
46
  export class RegistryClient {
45
- private network: 'mainnet' | 'testnet'
47
+ private network: 'mainnet' | 'testnet' | undefined
48
+ private readonly resolver: LookupResolver
49
+ private cachedIdentityKey: PubKeyHex | undefined
50
+ private readonly acceptDelayedBroadcast: boolean
51
+
46
52
  constructor (
47
- private readonly wallet: WalletInterface = new WalletClient()
48
- ) { }
53
+ private readonly wallet: WalletInterface = new WalletClient(),
54
+ options: { acceptDelayedBroadcast?: boolean, resolver?: LookupResolver } = {}
55
+ ) {
56
+ this.acceptDelayedBroadcast = options.acceptDelayedBroadcast ?? false
57
+ this.resolver = options.resolver ?? new LookupResolver()
58
+ }
59
+
60
+ /**
61
+ * Gets the wallet's identity key, caching it after the first call.
62
+ * @returns The public identity key as a hex string.
63
+ */
64
+ private async getIdentityKey (): Promise<PubKeyHex> {
65
+ if (this.cachedIdentityKey === undefined) {
66
+ this.cachedIdentityKey = (await this.wallet.getPublicKey({ identityKey: true })).publicKey
67
+ }
68
+ return this.cachedIdentityKey
69
+ }
70
+
71
+ /**
72
+ * Gets the network, initializing and caching it on first call.
73
+ * @returns The network type ('mainnet' or 'testnet').
74
+ */
75
+ private async getNetwork (): Promise<'mainnet' | 'testnet'> {
76
+ if (this.network === undefined) {
77
+ this.network = (await this.wallet.getNetwork({})).network
78
+ }
79
+ return this.network
80
+ }
49
81
 
50
82
  /**
51
83
  * Publishes a new on-chain definition for baskets, protocols, or certificates.
@@ -58,7 +90,7 @@ export class RegistryClient {
58
90
  * @returns A promise with the broadcast result or failure.
59
91
  */
60
92
  async registerDefinition (data: DefinitionData): Promise<BroadcastResponse | BroadcastFailure> {
61
- const registryOperator = (await this.wallet.getPublicKey({ identityKey: true })).publicKey
93
+ const registryOperator = await this.getIdentityKey()
62
94
  const pushdrop = new PushDrop(this.wallet)
63
95
 
64
96
  // Convert definition data into PushDrop fields
@@ -68,7 +100,7 @@ export class RegistryClient {
68
100
  const protocol = this.mapDefinitionTypeToWalletProtocol(data.definitionType)
69
101
 
70
102
  // Lock the fields into a pushdrop-based UTXO
71
- const lockingScript = await pushdrop.lock(fields, protocol, '1', 'anyone', true)
103
+ const lockingScript = await pushdrop.lock(fields, protocol, REGISTRANT_KEY_ID, 'anyone', true)
72
104
 
73
105
  // Create a transaction
74
106
  const { tx } = await this.wallet.createAction({
@@ -82,22 +114,23 @@ export class RegistryClient {
82
114
  }
83
115
  ],
84
116
  options: {
117
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast,
85
118
  randomizeOutputs: false
86
119
  }
87
120
  })
88
-
89
121
  if (tx === undefined) {
90
122
  throw new Error(`Failed to create ${data.definitionType} registration transaction!`)
91
123
  }
92
-
93
124
  // Broadcast to the relevant topic
94
125
  const broadcaster = new TopicBroadcaster(
95
126
  [this.mapDefinitionTypeToTopic(data.definitionType)],
96
127
  {
97
- networkPreset: this.network ??= (await this.wallet.getNetwork({})).network
128
+ networkPreset: await this.getNetwork(),
129
+ resolver: this.resolver
98
130
  }
99
131
  )
100
- return await broadcaster.broadcast(Transaction.fromAtomicBEEF(tx))
132
+ const result = await broadcaster.broadcast(Transaction.fromAtomicBEEF(tx))
133
+ return result
101
134
  }
102
135
 
103
136
  /**
@@ -119,11 +152,10 @@ export class RegistryClient {
119
152
  definitionType: T,
120
153
  query: RegistryQueryMapping[T]
121
154
  ): Promise<DefinitionData[]> {
122
- const resolver = new LookupResolver()
123
155
  const serviceName = this.mapDefinitionTypeToServiceName(definitionType)
124
156
 
125
157
  // Make the lookup query
126
- const result = await resolver.query({ service: serviceName, query })
158
+ const result = await this.resolver.query({ service: serviceName, query })
127
159
  if (result.type !== 'output-list') {
128
160
  return []
129
161
  }
@@ -164,8 +196,8 @@ export class RegistryClient {
164
196
  }
165
197
  try {
166
198
  const [txid, outputIndex] = output.outpoint.split('.')
167
- const tx = Transaction.fromBEEF(BEEF as number[])
168
- const lockingScript: LockingScript = tx.outputs[outputIndex].lockingScript
199
+ const tx = Transaction.fromBEEF(BEEF as number[], txid)
200
+ const lockingScript: LockingScript = tx.outputs[Number(outputIndex)].lockingScript
169
201
  const record = await this.parseLockingScript(
170
202
  definitionType,
171
203
  lockingScript
@@ -187,12 +219,12 @@ export class RegistryClient {
187
219
  }
188
220
 
189
221
  /**
190
- * Revokes a registry record by spending its associated UTXO.
222
+ * Removes a registry definition by spending its associated UTXO.
191
223
  *
192
- * @param registryRecord - Must have valid txid, outputIndex, and lockingScript.
224
+ * @param registryRecord - The registry record to remove (must have valid txid, outputIndex, and lockingScript).
193
225
  * @returns Broadcast success/failure.
194
226
  */
195
- async revokeOwnRegistryEntry (
227
+ async removeDefinition (
196
228
  registryRecord: RegistryRecord
197
229
  ): Promise<BroadcastResponse | BroadcastFailure> {
198
230
  if (registryRecord.txid === undefined || typeof registryRecord.outputIndex === 'undefined' || registryRecord.lockingScript === undefined) {
@@ -200,12 +232,12 @@ export class RegistryClient {
200
232
  }
201
233
 
202
234
  // Check if the registry record belongs to the current user
203
- const currentIdentityKey = (await this.wallet.getPublicKey({ identityKey: true })).publicKey
235
+ const currentIdentityKey = await this.getIdentityKey()
204
236
  if (registryRecord.registryOperator !== currentIdentityKey) {
205
237
  throw new Error('This registry token does not belong to the current wallet.')
206
238
  }
207
239
 
208
- // Create a descriptive label for the item were revoking
240
+ // Create a descriptive label for the item we're removing
209
241
  const itemIdentifier =
210
242
  registryRecord.definitionType === 'basket'
211
243
  ? registryRecord.basketID
@@ -217,48 +249,159 @@ export class RegistryClient {
217
249
 
218
250
  const outpoint = `${registryRecord.txid}.${registryRecord.outputIndex}`
219
251
  const { signableTransaction } = await this.wallet.createAction({
220
- description: `Revoke ${registryRecord.definitionType} item: ${itemIdentifier}`,
252
+ description: `Remove ${registryRecord.definitionType} item: ${itemIdentifier}`,
221
253
  inputBEEF: registryRecord.beef,
222
254
  inputs: [
223
255
  {
224
256
  outpoint,
225
- unlockingScriptLength: 73,
226
- inputDescription: `Revoking ${registryRecord.definitionType} token`
257
+ unlockingScriptLength: 74,
258
+ inputDescription: `Removing ${registryRecord.definitionType} token`
227
259
  }
228
- ]
260
+ ],
261
+ options: {
262
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast,
263
+ randomizeOutputs: false
264
+ }
229
265
  })
230
266
 
231
267
  if (signableTransaction === undefined) {
232
268
  throw new Error('Failed to create signable transaction.')
233
269
  }
234
-
235
- const partialTx = Transaction.fromBEEF(signableTransaction.tx)
270
+ const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
236
271
 
237
272
  // Prepare the unlocker
238
273
  const pushdrop = new PushDrop(this.wallet)
239
- const unlocker = await pushdrop.unlock(
274
+ const unlocker = pushdrop.unlock(
240
275
  this.mapDefinitionTypeToWalletProtocol(registryRecord.definitionType),
241
- '1',
242
- 'anyone',
243
- 'all',
244
- false,
245
- registryRecord.satoshis,
246
- LockingScript.fromHex(registryRecord.lockingScript)
276
+ REGISTRANT_KEY_ID,
277
+ 'anyone'
247
278
  )
248
279
 
249
280
  // Convert to Transaction, apply signature
250
- const finalUnlockScript = await unlocker.sign(partialTx, registryRecord.outputIndex)
281
+ const finalUnlockScript = await unlocker.sign(partialTx, 0)
282
+
283
+ // Complete signing with the final unlock script
284
+ const { tx: signedTx } = await this.wallet.signAction({
285
+ reference: signableTransaction.reference,
286
+ spends: {
287
+ 0: {
288
+ unlockingScript: finalUnlockScript.toHex()
289
+ }
290
+ },
291
+ options: {
292
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast
293
+ }
294
+ })
295
+
296
+ if (signedTx === undefined) {
297
+ throw new Error('Failed to finalize the transaction signature.')
298
+ }
299
+ // Broadcast
300
+ const broadcaster = new TopicBroadcaster(
301
+ [this.mapDefinitionTypeToTopic(registryRecord.definitionType)],
302
+ {
303
+ networkPreset: await this.getNetwork(),
304
+ resolver: this.resolver
305
+ }
306
+ )
307
+ const result = await broadcaster.broadcast(Transaction.fromAtomicBEEF(signedTx))
308
+ return result
309
+ }
310
+
311
+ /**
312
+ * Updates an existing registry record by spending its UTXO and creating a new one with updated data.
313
+ *
314
+ * @param registryRecord - The existing registry record to update (must have valid txid, outputIndex, and lockingScript).
315
+ * @param updatedData - The new definition data to replace the old record.
316
+ * @returns Broadcast success/failure.
317
+ */
318
+ async updateDefinition (
319
+ registryRecord: RegistryRecord,
320
+ updatedData: DefinitionData
321
+ ): Promise<BroadcastResponse | BroadcastFailure> {
322
+ if (registryRecord.txid === undefined || typeof registryRecord.outputIndex === 'undefined' || registryRecord.lockingScript === undefined) {
323
+ throw new Error('Invalid registry record. Missing txid, outputIndex, or lockingScript.')
324
+ }
325
+
326
+ // Verify the updated data matches the record type
327
+ if (registryRecord.definitionType !== updatedData.definitionType) {
328
+ throw new Error(`Cannot change definition type from ${registryRecord.definitionType} to ${updatedData.definitionType}`)
329
+ }
330
+
331
+ // Check if the registry record belongs to the current user
332
+ const currentIdentityKey = await this.getIdentityKey()
333
+ if (registryRecord.registryOperator !== currentIdentityKey) {
334
+ throw new Error('This registry token does not belong to the current wallet.')
335
+ }
336
+
337
+ // Create a descriptive label for the item we're updating
338
+ const itemIdentifier =
339
+ registryRecord.definitionType === 'basket'
340
+ ? registryRecord.basketID
341
+ : registryRecord.definitionType === 'protocol'
342
+ ? registryRecord.name
343
+ : registryRecord.definitionType === 'certificate'
344
+ ? (registryRecord.name !== undefined ? registryRecord.name : registryRecord.type)
345
+ : 'unknown'
346
+
347
+ const pushdrop = new PushDrop(this.wallet)
348
+
349
+ // Build the new locking script with updated data
350
+ const fields = this.buildPushDropFields(updatedData, currentIdentityKey)
351
+ const protocol = this.mapDefinitionTypeToWalletProtocol(updatedData.definitionType)
352
+ const newLockingScript = await pushdrop.lock(fields, protocol, REGISTRANT_KEY_ID, 'anyone', true)
353
+
354
+ const outpoint = `${registryRecord.txid}.${registryRecord.outputIndex}`
355
+ const { signableTransaction } = await this.wallet.createAction({
356
+ description: `Update ${registryRecord.definitionType} item: ${itemIdentifier}`,
357
+ inputBEEF: registryRecord.beef,
358
+ inputs: [
359
+ {
360
+ outpoint,
361
+ unlockingScriptLength: 74,
362
+ inputDescription: `Updating ${registryRecord.definitionType} token`
363
+ }
364
+ ],
365
+ outputs: [
366
+ {
367
+ satoshis: REGISTRANT_TOKEN_AMOUNT,
368
+ lockingScript: newLockingScript.toHex(),
369
+ outputDescription: `Updated ${registryRecord.definitionType} registration token`,
370
+ basket: this.mapDefinitionTypeToBasketName(registryRecord.definitionType)
371
+ }
372
+ ],
373
+ options: {
374
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast,
375
+ randomizeOutputs: false
376
+ }
377
+ })
378
+
379
+ if (signableTransaction === undefined) {
380
+ throw new Error('Failed to create signable transaction.')
381
+ }
382
+
383
+ const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
384
+
385
+ // Prepare the unlocker for the input
386
+ const unlocker = pushdrop.unlock(
387
+ this.mapDefinitionTypeToWalletProtocol(registryRecord.definitionType),
388
+ REGISTRANT_KEY_ID,
389
+ 'anyone'
390
+ )
391
+
392
+ // Sign the input
393
+ const finalUnlockScript = await unlocker.sign(partialTx, 0)
251
394
 
252
395
  // Complete signing with the final unlock script
253
396
  const { tx: signedTx } = await this.wallet.signAction({
254
397
  reference: signableTransaction.reference,
255
398
  spends: {
256
- [registryRecord.outputIndex]: {
399
+ 0: {
257
400
  unlockingScript: finalUnlockScript.toHex()
258
401
  }
259
402
  },
260
403
  options: {
261
- acceptDelayedBroadcast: false
404
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast
262
405
  }
263
406
  })
264
407
 
@@ -270,7 +413,8 @@ export class RegistryClient {
270
413
  const broadcaster = new TopicBroadcaster(
271
414
  [this.mapDefinitionTypeToTopic(registryRecord.definitionType)],
272
415
  {
273
- networkPreset: this.network ??= (await this.wallet.getNetwork({})).network
416
+ networkPreset: await this.getNetwork(),
417
+ resolver: this.resolver
274
418
  }
275
419
  )
276
420
  return await broadcaster.broadcast(Transaction.fromAtomicBEEF(signedTx))