@bsv/sdk 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -82,7 +82,7 @@ export class HTTPSOverlayBroadcastFacilitator implements OverlayBroadcastFacilit
82
82
 
83
83
  constructor (httpClient = fetch, allowHTTP: boolean = false) {
84
84
  this.httpClient = httpClient
85
- this.allowHTTP = false
85
+ this.allowHTTP = allowHTTP
86
86
  }
87
87
 
88
88
  async send (url: string, taggedBEEF: TaggedBEEF): Promise<STEAK> {
@@ -1,13 +1,11 @@
1
1
  import {
2
2
  WalletInterface,
3
3
  WalletProtocol,
4
- WalletClient
5
- ,
6
- PubKeyHex
4
+ WalletClient,
5
+ PubKeyHex,
6
+ SecurityLevel
7
7
  } from '../wallet/index.js'
8
- import {
9
- Utils
10
- } from '../primitives/index.js'
8
+ import { Utils } from '../primitives/index.js'
11
9
  import {
12
10
  Transaction,
13
11
  BroadcastResponse,
@@ -17,10 +15,7 @@ import {
17
15
  LookupResolver,
18
16
  TopicBroadcaster
19
17
  } from '../overlay-tools/index.js'
20
- import {
21
- PushDrop,
22
- LockingScript
23
- } from '../script/index.js'
18
+ import { PushDrop, LockingScript } from '../script/index.js'
24
19
  import {
25
20
  CertificateFieldDescriptor,
26
21
  DefinitionData,
@@ -33,18 +28,18 @@ const REGISTRANT_TOKEN_AMOUNT = 1
33
28
 
34
29
  /**
35
30
  * RegistryClient manages on-chain registry definitions for three types:
36
- * - BasketMap (basket-based items)
37
- * - ProtoMap (protocol-based items)
38
- * - CertMap (certificate-based items)
31
+ * - basket (basket-based items)
32
+ * - protocol (protocol-based items)
33
+ * - certificate (certificate-based items)
39
34
  *
40
35
  * It provides methods to:
41
36
  * - Register new definitions using pushdrop-based UTXOs.
42
- * - Resolve existing definitions using a overlay lookup service.
37
+ * - Resolve existing definitions using a lookup service.
43
38
  * - List registry entries associated with the operator's wallet.
44
39
  * - Revoke an existing registry entry by spending its UTXO.
45
40
  *
46
41
  * Registry operators use this client to establish and manage
47
- * canonical references for baskets, protocols, and certificates types.
42
+ * canonical references for baskets, protocols, and certificate types.
48
43
  */
49
44
  export class RegistryClient {
50
45
  private network: 'mainnet' | 'testnet'
@@ -59,24 +54,21 @@ export class RegistryClient {
59
54
  * Registry operators (i.e., identity key owners) can create these definitions
60
55
  * to establish canonical references for basket IDs, protocol specs, or certificate schemas.
61
56
  *
62
- * @param data - The structured information needed to register an item of kind 'basket', 'protocol', or 'certificate'.
57
+ * @param data - Structured information about a 'basket', 'protocol', or 'certificate'.
63
58
  * @returns A promise with the broadcast result or failure.
64
59
  */
65
60
  async registerDefinition (data: DefinitionData): Promise<BroadcastResponse | BroadcastFailure> {
66
61
  const registryOperator = (await this.wallet.getPublicKey({ identityKey: true })).publicKey
67
62
  const pushdrop = new PushDrop(this.wallet)
68
63
 
69
- // Build the array of fields, depending on the definition type
64
+ // Convert definition data into PushDrop fields
70
65
  const fields = this.buildPushDropFields(data, registryOperator)
71
- const protocol = this.getWalletProtocol(data.definitionType)
72
66
 
73
- // Lock the fields
74
- const lockingScript = await pushdrop.lock(
75
- fields,
76
- protocol,
77
- '1',
78
- 'self'
79
- )
67
+ // Convert the user-friendly definitionType to the actual wallet protocol
68
+ const protocol = this.mapDefinitionTypeToWalletProtocol(data.definitionType)
69
+
70
+ // Lock the fields into a pushdrop-based UTXO
71
+ const lockingScript = await pushdrop.lock(fields, protocol, '1', 'anyone', true)
80
72
 
81
73
  // Create a transaction
82
74
  const { tx } = await this.wallet.createAction({
@@ -85,21 +77,24 @@ export class RegistryClient {
85
77
  {
86
78
  satoshis: REGISTRANT_TOKEN_AMOUNT,
87
79
  lockingScript: lockingScript.toHex(),
88
- outputDescription: `New ${data.definitionType} registration token`
80
+ outputDescription: `New ${data.definitionType} registration token`,
81
+ basket: this.mapDefinitionTypeToBasketName(data.definitionType)
89
82
  }
90
- ]
83
+ ],
84
+ options: {
85
+ randomizeOutputs: false
86
+ }
91
87
  })
92
88
 
93
89
  if (tx === undefined) {
94
90
  throw new Error(`Failed to create ${data.definitionType} registration transaction!`)
95
91
  }
96
92
 
97
- // Broadcast
98
-
93
+ // Broadcast to the relevant topic
99
94
  const broadcaster = new TopicBroadcaster(
100
- [this.getBroadcastTopic(data.definitionType)],
95
+ [this.mapDefinitionTypeToTopic(data.definitionType)],
101
96
  {
102
- networkPreset: this.network ??= (await (this.wallet.getNetwork({}))).network
97
+ networkPreset: this.network ??= (await this.wallet.getNetwork({})).network
103
98
  }
104
99
  )
105
100
  return await broadcaster.broadcast(Transaction.fromAtomicBEEF(tx))
@@ -112,7 +107,7 @@ export class RegistryClient {
112
107
  * - For "basket", the query is of type BasketMapQuery:
113
108
  * { basketID?: string; name?: string; registryOperators?: string[]; }
114
109
  * - For "protocol", the query is of type ProtoMapQuery:
115
- * { name?: string; registryOperators?: string[]; protocolID?: string; securityLevel?: number; }
110
+ * { name?: string; registryOperators?: string[]; protocolID?: WalletProtocol; }
116
111
  * - For "certificate", the query is of type CertMapQuery:
117
112
  * { type?: string; name?: string; registryOperators?: string[]; }
118
113
  *
@@ -125,14 +120,10 @@ export class RegistryClient {
125
120
  query: RegistryQueryMapping[T]
126
121
  ): Promise<DefinitionData[]> {
127
122
  const resolver = new LookupResolver()
123
+ const serviceName = this.mapDefinitionTypeToServiceName(definitionType)
128
124
 
129
- // The service name depends on the kind
130
- const serviceName = this.getServiceName(definitionType)
131
- const result = await resolver.query({
132
- service: serviceName,
133
- query
134
- })
135
-
125
+ // Make the lookup query
126
+ const result = await resolver.query({ service: serviceName, query })
136
127
  if (result.type !== 'output-list') {
137
128
  return []
138
129
  }
@@ -140,11 +131,12 @@ export class RegistryClient {
140
131
  const parsedRegistryRecords: DefinitionData[] = []
141
132
  for (const output of result.outputs) {
142
133
  try {
143
- const parsedTx = Transaction.fromAtomicBEEF(output.beef)
144
- const record = await this.parseLockingScript(definitionType, parsedTx.outputs[output.outputIndex].lockingScript)
134
+ const parsedTx = Transaction.fromBEEF(output.beef)
135
+ const lockingScript = parsedTx.outputs[output.outputIndex].lockingScript
136
+ const record = await this.parseLockingScript(definitionType, lockingScript)
145
137
  parsedRegistryRecords.push(record)
146
138
  } catch {
147
- // skip
139
+ // Skip invalid or non-pushdrop outputs
148
140
  }
149
141
  }
150
142
  return parsedRegistryRecords
@@ -159,29 +151,35 @@ export class RegistryClient {
159
151
  * @returns A promise that resolves to an array of RegistryRecord objects.
160
152
  */
161
153
  async listOwnRegistryEntries (definitionType: DefinitionType): Promise<RegistryRecord[]> {
162
- const relevantBasketName = this.getBasketName(definitionType)
163
- const { outputs } = await this.wallet.listOutputs({
154
+ const relevantBasketName = this.mapDefinitionTypeToBasketName(definitionType)
155
+ const { outputs, BEEF } = await this.wallet.listOutputs({
164
156
  basket: relevantBasketName,
165
- include: 'locking scripts'
157
+ include: 'entire transactions'
166
158
  })
167
- const results: RegistryRecord[] = []
168
159
 
160
+ const results: RegistryRecord[] = []
169
161
  for (const output of outputs) {
170
- if (output.spendable) {
162
+ if (!output.spendable) {
171
163
  continue
172
164
  }
173
165
  try {
174
- const record = await this.parseLockingScript(definitionType, LockingScript.fromHex(output.lockingScript as string))
175
166
  const [txid, outputIndex] = output.outpoint.split('.')
167
+ const tx = Transaction.fromBEEF(BEEF as number[])
168
+ const lockingScript: LockingScript = tx.outputs[outputIndex].lockingScript
169
+ const record = await this.parseLockingScript(
170
+ definitionType,
171
+ lockingScript
172
+ )
176
173
  results.push({
177
174
  ...record,
178
175
  txid,
179
176
  outputIndex: Number(outputIndex),
180
177
  satoshis: output.satoshis,
181
- lockingScript: output.lockingScript as string
178
+ lockingScript: lockingScript.toHex(),
179
+ beef: BEEF as number[]
182
180
  })
183
181
  } catch {
184
- // ignore parse errors
182
+ // Ignore parse errors
185
183
  }
186
184
  }
187
185
 
@@ -191,29 +189,17 @@ export class RegistryClient {
191
189
  /**
192
190
  * Revokes a registry record by spending its associated UTXO.
193
191
  *
194
- * This function creates a transaction that spends the UTXO corresponding to the provided registry record,
195
- * revoking the registry entry. It prepares an unlocker using the appropriate wallet protocol,
196
- * builds a signable transaction, signs it, and then broadcasts the finalized transaction.
197
- *
198
- * @param registryRecord - The registry record to revoke. It must include a valid txid, outputIndex, and lockingScript.
199
- * @returns A promise that resolves with either a BroadcastResponse upon success or a BroadcastFailure on error.
200
- * @throws If required fields are missing or if transaction creation/signing fails.
192
+ * @param registryRecord - Must have valid txid, outputIndex, and lockingScript.
193
+ * @returns Broadcast success/failure.
201
194
  */
202
195
  async revokeOwnRegistryEntry (
203
196
  registryRecord: RegistryRecord
204
197
  ): Promise<BroadcastResponse | BroadcastFailure> {
205
198
  if (registryRecord.txid === undefined || typeof registryRecord.outputIndex === 'undefined' || registryRecord.lockingScript === undefined) {
206
- throw new Error('Invalid record. Missing txid, outputIndex, or lockingScript.')
199
+ throw new Error('Invalid registry record. Missing txid, outputIndex, or lockingScript.')
207
200
  }
208
201
 
209
- // Prepare the unlocker
210
- const pushdrop = new PushDrop(this.wallet)
211
- const unlocker = await pushdrop.unlock(
212
- this.getWalletProtocol(registryRecord.definitionType),
213
- '1',
214
- 'anyone'
215
- )
216
-
202
+ // Create a descriptive label for the item we’re revoking
217
203
  const itemIdentifier =
218
204
  registryRecord.definitionType === 'basket'
219
205
  ? registryRecord.basketID
@@ -223,13 +209,10 @@ export class RegistryClient {
223
209
  ? (registryRecord.name !== undefined ? registryRecord.name : registryRecord.type)
224
210
  : 'unknown'
225
211
 
226
- const description = `Revoke ${registryRecord.definitionType} item: ${String(itemIdentifier)}`
227
-
228
- // Create a new transaction that spends the UTXO
229
212
  const outpoint = `${registryRecord.txid}.${registryRecord.outputIndex}`
230
213
  const { signableTransaction } = await this.wallet.createAction({
231
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
232
- description,
214
+ description: `Revoke ${registryRecord.definitionType} item: ${itemIdentifier}`,
215
+ inputBEEF: registryRecord.beef,
233
216
  inputs: [
234
217
  {
235
218
  outpoint,
@@ -243,17 +226,33 @@ export class RegistryClient {
243
226
  throw new Error('Failed to create signable transaction.')
244
227
  }
245
228
 
246
- // Build a transaction object, sign with the unlock script
247
- const tx = Transaction.fromBEEF(signableTransaction.tx)
248
- const finalUnlockScript = await unlocker.sign(tx, registryRecord.outputIndex)
229
+ const partialTx = Transaction.fromBEEF(signableTransaction.tx)
249
230
 
250
- // Complete the signing
231
+ // Prepare the unlocker
232
+ const pushdrop = new PushDrop(this.wallet)
233
+ const unlocker = await pushdrop.unlock(
234
+ this.mapDefinitionTypeToWalletProtocol(registryRecord.definitionType),
235
+ '1',
236
+ 'anyone',
237
+ 'all',
238
+ false,
239
+ registryRecord.satoshis,
240
+ LockingScript.fromHex(registryRecord.lockingScript)
241
+ )
242
+
243
+ // Convert to Transaction, apply signature
244
+ const finalUnlockScript = await unlocker.sign(partialTx, registryRecord.outputIndex)
245
+
246
+ // Complete signing with the final unlock script
251
247
  const { tx: signedTx } = await this.wallet.signAction({
252
248
  reference: signableTransaction.reference,
253
249
  spends: {
254
250
  [registryRecord.outputIndex]: {
255
251
  unlockingScript: finalUnlockScript.toHex()
256
252
  }
253
+ },
254
+ options: {
255
+ acceptDelayedBroadcast: false
257
256
  }
258
257
  })
259
258
 
@@ -263,21 +262,25 @@ export class RegistryClient {
263
262
 
264
263
  // Broadcast
265
264
  const broadcaster = new TopicBroadcaster(
266
- [this.getBroadcastTopic(registryRecord.definitionType)],
265
+ [this.mapDefinitionTypeToTopic(registryRecord.definitionType)],
267
266
  {
268
- networkPreset: this.network ??= (await (this.wallet.getNetwork({}))).network
267
+ networkPreset: this.network ??= (await this.wallet.getNetwork({})).network
269
268
  }
270
269
  )
271
270
  return await broadcaster.broadcast(Transaction.fromAtomicBEEF(signedTx))
272
271
  }
273
272
 
274
273
  // --------------------------------------------------------------------------
275
- // INTERNAL HELPER METHODS
274
+ // INTERNAL UTILITY METHODS
276
275
  // --------------------------------------------------------------------------
277
276
 
277
+ /**
278
+ * Convert definition data into an array of pushdrop fields (strings).
279
+ * Each definition type has a slightly different shape.
280
+ */
278
281
  private buildPushDropFields (
279
282
  data: DefinitionData,
280
- registryOperator: string
283
+ registryOperator: PubKeyHex
281
284
  ): number[][] {
282
285
  let fields: string[]
283
286
 
@@ -293,8 +296,7 @@ export class RegistryClient {
293
296
  break
294
297
  case 'protocol':
295
298
  fields = [
296
- data.securityLevel.toString(),
297
- data.protocolID,
299
+ JSON.stringify(data.protocolID),
298
300
  data.name,
299
301
  data.iconURL,
300
302
  data.description,
@@ -312,17 +314,17 @@ export class RegistryClient {
312
314
  ]
313
315
  break
314
316
  default:
315
- throw new Error('Invalid registry kind specified')
317
+ throw new Error('Unsupported definition type')
316
318
  }
317
319
 
318
- // Append the registry operator to all cases.
320
+ // Append the operator's public identity key last
319
321
  fields.push(registryOperator)
320
322
 
321
323
  return fields.map(field => Utils.toArray(field))
322
324
  }
323
325
 
324
326
  /**
325
- * Decodes a pushdrop locking script for a given registry kind,
327
+ * Decodes a pushdrop locking script for a given definition type,
326
328
  * returning a typed record with the appropriate fields.
327
329
  */
328
330
  private async parseLockingScript (
@@ -335,15 +337,17 @@ export class RegistryClient {
335
337
  }
336
338
 
337
339
  let registryOperator: PubKeyHex
338
- let data: DefinitionData
340
+ let parsedData: DefinitionData
341
+
339
342
  switch (definitionType) {
340
343
  case 'basket': {
341
- if (decoded.fields.length !== 6) {
344
+ if (decoded.fields.length !== 7) {
342
345
  throw new Error('Unexpected field count for basket type.')
343
346
  }
344
347
  const [basketID, name, iconURL, description, docURL, operator] = decoded.fields
345
348
  registryOperator = Utils.toUTF8(operator)
346
- data = {
349
+
350
+ parsedData = {
347
351
  definitionType: 'basket',
348
352
  basketID: Utils.toUTF8(basketID),
349
353
  name: Utils.toUTF8(name),
@@ -353,12 +357,12 @@ export class RegistryClient {
353
357
  }
354
358
  break
355
359
  }
360
+
356
361
  case 'protocol': {
357
362
  if (decoded.fields.length !== 7) {
358
- throw new Error('Unexpected field count for proto type.')
363
+ throw new Error('Unexpected field count for protocol type.')
359
364
  }
360
365
  const [
361
- securityLevel,
362
366
  protocolID,
363
367
  name,
364
368
  iconURL,
@@ -367,10 +371,10 @@ export class RegistryClient {
367
371
  operator
368
372
  ] = decoded.fields
369
373
  registryOperator = Utils.toUTF8(operator)
370
- data = {
374
+
375
+ parsedData = {
371
376
  definitionType: 'protocol',
372
- securityLevel: parseInt(Utils.toUTF8(securityLevel), 10) as 0 | 1 | 2,
373
- protocolID: Utils.toUTF8(protocolID),
377
+ protocolID: deserializeWalletProtocol(Utils.toUTF8(protocolID)),
374
378
  name: Utils.toUTF8(name),
375
379
  iconURL: Utils.toUTF8(iconURL),
376
380
  description: Utils.toUTF8(description),
@@ -378,8 +382,9 @@ export class RegistryClient {
378
382
  }
379
383
  break
380
384
  }
385
+
381
386
  case 'certificate': {
382
- if (decoded.fields.length !== 7) {
387
+ if (decoded.fields.length !== 8) {
383
388
  throw new Error('Unexpected field count for certificate type.')
384
389
  }
385
390
  const [
@@ -391,17 +396,16 @@ export class RegistryClient {
391
396
  fieldsJSON,
392
397
  operator
393
398
  ] = decoded.fields
394
-
395
399
  registryOperator = Utils.toUTF8(operator)
396
400
 
397
- let parsedFields: Record<string, CertificateFieldDescriptor>
401
+ let parsedFields: Record<string, CertificateFieldDescriptor> = {}
398
402
  try {
399
403
  parsedFields = JSON.parse(Utils.toUTF8(fieldsJSON))
400
404
  } catch {
401
- parsedFields = {}
405
+ // If there's a JSON parse error, assume empty
402
406
  }
403
407
 
404
- data = {
408
+ parsedData = {
405
409
  definitionType: 'certificate',
406
410
  type: Utils.toUTF8(certType),
407
411
  name: Utils.toUTF8(name),
@@ -412,25 +416,25 @@ export class RegistryClient {
412
416
  }
413
417
  break
414
418
  }
419
+
415
420
  default:
416
- throw new Error('Invalid registry kind for parsing.')
421
+ throw new Error(`Unsupported definition type: ${definitionType as string}`)
417
422
  }
418
423
 
424
+ // Enforce that the pushdrop belongs to the CURRENT identity key
419
425
  const currentIdentityKey = (await this.wallet.getPublicKey({ identityKey: true })).publicKey
420
426
  if (registryOperator !== currentIdentityKey) {
421
427
  throw new Error('This registry token does not belong to the current wallet.')
422
428
  }
423
429
 
424
- return {
425
- ...data,
426
- registryOperator
427
- }
430
+ // Return the typed data plus the operator key
431
+ return { ...parsedData, registryOperator }
428
432
  }
429
433
 
430
434
  /**
431
- * Returns the (protocolID, keyID) used for pushdrop based on the registry kind.
435
+ * Convert our definitionType to the wallet protocol format ([protocolID, keyID]).
432
436
  */
433
- private getWalletProtocol (definitionType: DefinitionType): WalletProtocol {
437
+ private mapDefinitionTypeToWalletProtocol (definitionType: DefinitionType): WalletProtocol {
434
438
  switch (definitionType) {
435
439
  case 'basket':
436
440
  return [1, 'basketmap']
@@ -439,14 +443,14 @@ export class RegistryClient {
439
443
  case 'certificate':
440
444
  return [1, 'certmap']
441
445
  default:
442
- throw new Error(`Unknown registry type: ${definitionType as string}`)
446
+ throw new Error(`Unknown definition type: ${definitionType as string}`)
443
447
  }
444
448
  }
445
449
 
446
450
  /**
447
- * Returns the name of the basket used by the wallet
451
+ * Convert 'basket'|'protocol'|'certificate' to the basket name used by the wallet.
448
452
  */
449
- private getBasketName (definitionType: DefinitionType): string {
453
+ private mapDefinitionTypeToBasketName (definitionType: DefinitionType): string {
450
454
  switch (definitionType) {
451
455
  case 'basket':
452
456
  return 'basketmap'
@@ -455,14 +459,14 @@ export class RegistryClient {
455
459
  case 'certificate':
456
460
  return 'certmap'
457
461
  default:
458
- throw new Error(`Unknown basket type: ${definitionType as string}`)
462
+ throw new Error(`Unknown definition type: ${definitionType as string}`)
459
463
  }
460
464
  }
461
465
 
462
466
  /**
463
- * Returns the broadcast topic to be used with SHIPBroadcaster.
467
+ * Convert 'basket'|'protocol'|'certificate' to the broadcast topic name.
464
468
  */
465
- private getBroadcastTopic (definitionType: DefinitionType): string {
469
+ private mapDefinitionTypeToTopic (definitionType: DefinitionType): string {
466
470
  switch (definitionType) {
467
471
  case 'basket':
468
472
  return 'tm_basketmap'
@@ -471,14 +475,14 @@ export class RegistryClient {
471
475
  case 'certificate':
472
476
  return 'tm_certmap'
473
477
  default:
474
- throw new Error(`Unknown topic type: ${definitionType as string}`)
478
+ throw new Error(`Unknown definition type: ${definitionType as string}`)
475
479
  }
476
480
  }
477
481
 
478
482
  /**
479
- * Returns the lookup service name to use.
483
+ * Convert 'basket'|'protocol'|'certificate' to the lookup service name.
480
484
  */
481
- private getServiceName (definitionType: DefinitionType): string {
485
+ private mapDefinitionTypeToServiceName (definitionType: DefinitionType): string {
482
486
  switch (definitionType) {
483
487
  case 'basket':
484
488
  return 'ls_basketmap'
@@ -487,7 +491,31 @@ export class RegistryClient {
487
491
  case 'certificate':
488
492
  return 'ls_certmap'
489
493
  default:
490
- throw new Error(`Unknown service type: ${definitionType as string}`)
494
+ throw new Error(`Unknown definition type: ${definitionType as string}`)
491
495
  }
492
496
  }
493
497
  }
498
+
499
+ export function deserializeWalletProtocol (str: string): WalletProtocol {
500
+ // Parse the JSON string back into a JavaScript value.
501
+ const parsed = JSON.parse(str)
502
+
503
+ // Validate that the parsed value is an array with exactly two elements.
504
+ if (!Array.isArray(parsed) || parsed.length !== 2) {
505
+ throw new Error('Invalid wallet protocol format.')
506
+ }
507
+
508
+ const [security, protocolString] = parsed
509
+
510
+ // Validate that the security level is one of the allowed numbers.
511
+ if (![0, 1, 2].includes(security)) {
512
+ throw new Error('Invalid security level.')
513
+ }
514
+
515
+ // Validate that the protocol string is a string and its length is within the allowed bounds.
516
+ if (typeof protocolString !== 'string') {
517
+ throw new Error('Invalid protocolID')
518
+ }
519
+
520
+ return [security as SecurityLevel, protocolString]
521
+ }