@bsv/sdk 1.6.4 → 1.6.6

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 (39) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/kvstore/LocalKVStore.js +78 -52
  3. package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -1
  4. package/dist/cjs/src/storage/StorageDownloader.js +10 -11
  5. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  6. package/dist/cjs/src/transaction/chaintrackers/BlockHeadersService.js +91 -0
  7. package/dist/cjs/src/transaction/chaintrackers/BlockHeadersService.js.map +1 -0
  8. package/dist/cjs/src/transaction/chaintrackers/index.js +3 -1
  9. package/dist/cjs/src/transaction/chaintrackers/index.js.map +1 -1
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/kvstore/LocalKVStore.js +78 -52
  12. package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -1
  13. package/dist/esm/src/storage/StorageDownloader.js +10 -11
  14. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  15. package/dist/esm/src/transaction/chaintrackers/BlockHeadersService.js +90 -0
  16. package/dist/esm/src/transaction/chaintrackers/BlockHeadersService.js.map +1 -0
  17. package/dist/esm/src/transaction/chaintrackers/index.js +1 -0
  18. package/dist/esm/src/transaction/chaintrackers/index.js.map +1 -1
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/kvstore/LocalKVStore.d.ts +10 -2
  21. package/dist/types/src/kvstore/LocalKVStore.d.ts.map +1 -1
  22. package/dist/types/src/storage/StorageDownloader.d.ts +10 -11
  23. package/dist/types/src/storage/StorageDownloader.d.ts.map +1 -1
  24. package/dist/types/src/transaction/chaintrackers/BlockHeadersService.d.ts +46 -0
  25. package/dist/types/src/transaction/chaintrackers/BlockHeadersService.d.ts.map +1 -0
  26. package/dist/types/src/transaction/chaintrackers/index.d.ts +1 -0
  27. package/dist/types/src/transaction/chaintrackers/index.d.ts.map +1 -1
  28. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  29. package/dist/umd/bundle.js +1 -1
  30. package/docs/kvstore.md +6 -3
  31. package/docs/overlay-tools.md +2 -3
  32. package/docs/primitives.md +20 -53
  33. package/docs/storage.md +35 -2
  34. package/docs/transaction.md +1 -1
  35. package/package.json +1 -1
  36. package/src/kvstore/LocalKVStore.ts +93 -55
  37. package/src/storage/StorageDownloader.ts +10 -11
  38. package/src/transaction/chaintrackers/BlockHeadersService.ts +138 -0
  39. package/src/transaction/chaintrackers/index.ts +1 -0
package/docs/kvstore.md CHANGED
@@ -14,7 +14,8 @@ Allows setting, getting, and removing key-value pairs, with optional encryption.
14
14
 
15
15
  ```ts
16
16
  export default class LocalKVStore {
17
- constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string)
17
+ acceptDelayedBroadcast: boolean = false;
18
+ constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string, acceptDelayedBroadcast = false)
18
19
  async get(key: string, defaultValue: string | undefined = undefined): Promise<string | undefined>
19
20
  async set(key: string, value: string): Promise<OutpointString>
20
21
  async remove(key: string): Promise<string[]>
@@ -28,7 +29,7 @@ See also: [OutpointString](./wallet.md#type-outpointstring), [WalletClient](./wa
28
29
  Creates an instance of the localKVStore.
29
30
 
30
31
  ```ts
31
- constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string)
32
+ constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string, acceptDelayedBroadcast = false)
32
33
  ```
33
34
  See also: [WalletClient](./wallet.md#class-walletclient), [WalletInterface](./wallet.md#interface-walletinterface), [encrypt](./messages.md#variable-encrypt)
34
35
 
@@ -96,13 +97,15 @@ Argument Details
96
97
 
97
98
  #### Method set
98
99
 
99
- Sets or updates the value associated with a given key.
100
+ Sets or updates the value associated with a given key atomically.
100
101
  If the key already exists (one or more outputs found), it spends the existing output(s)
101
102
  and creates a new one with the updated value. If multiple outputs exist for the key,
102
103
  they are collapsed into a single new output.
103
104
  If the key does not exist, it creates a new output.
104
105
  Handles encryption if enabled.
105
106
  If signing the update/collapse transaction fails, it relinquishes the original outputs and starts over with a new chain.
107
+ Ensures atomicity by locking the key during the operation, preventing concurrent updates
108
+ to the same key from missing earlier changes.
106
109
 
107
110
  ```ts
108
111
  async set(key: string, value: string): Promise<OutpointString>
@@ -272,6 +272,7 @@ Tagged BEEF
272
272
  export interface TaggedBEEF {
273
273
  beef: number[];
274
274
  topics: string[];
275
+ offChainValues?: number[];
275
276
  }
276
277
  ```
277
278
 
@@ -530,10 +531,8 @@ export type LookupAnswer = {
530
531
  outputs: Array<{
531
532
  beef: number[];
532
533
  outputIndex: number;
534
+ context?: number[];
533
535
  }>;
534
- } | {
535
- type: "freeform";
536
- result: unknown;
537
536
  }
538
537
  ```
539
538
 
@@ -93,8 +93,7 @@ export default class BigNumber {
93
93
  zeroBits(): number
94
94
  byteLength(): number { if (this._magnitude === 0n)
95
95
  return 0; return Math.ceil(this.bitLength() / 8); }
96
- toTwos(width: number): BigNumber { this.assert(width >= 0); const Bw = BigInt(width); let v = this._getSignedValue(); if (this._sign === 1 && this._magnitude !== 0n)
97
- v = (1n << Bw) + v; const m = (1n << Bw) - 1n; v &= m; const r = new BigNumber(0n); r._initializeState(v, 0); return r; }
96
+ toTwos(width: number): BigNumber
98
97
  fromTwos(width: number): BigNumber
99
98
  isNeg(): boolean
100
99
  neg(): BigNumber
@@ -132,15 +131,7 @@ export default class BigNumber {
132
131
  iushln(bits: number): this { this.assert(typeof bits === "number" && bits >= 0); if (bits === 0)
133
132
  return this; this._magnitude <<= BigInt(bits); this._finishInitialization(); return this.strip(); }
134
133
  ishln(bits: number): this
135
- iushrn(bits: number, hint?: number, extended?: BigNumber): this { this.assert(typeof bits === "number" && bits >= 0); if (bits === 0) {
136
- if (extended != null)
137
- extended._initializeState(0n, 0);
138
- return this;
139
- } if (extended != null) {
140
- const m = (1n << BigInt(bits)) - 1n;
141
- const sOut = this._magnitude & m;
142
- extended._initializeState(sOut, 0);
143
- } this._magnitude >>= BigInt(bits); this._finishInitialization(); return this.strip(); }
134
+ iushrn(bits: number, hint?: number, extended?: BigNumber): this
144
135
  ishrn(bits: number, hint?: number, extended?: BigNumber): this
145
136
  shln(bits: number): BigNumber
146
137
  ushln(bits: number): BigNumber
@@ -168,31 +159,8 @@ export default class BigNumber {
168
159
  a: BigNumber;
169
160
  b: BigNumber;
170
161
  gcd: BigNumber;
171
- } { this.assert(p._sign === 0, "p must not be negative"); this.assert(!p.isZero(), "p must not be zero"); let uV = this._getSignedValue(); let vV = p._magnitude; let a = 1n; let pa = 0n; let b = 0n; let pb = 1n; while (vV !== 0n) {
172
- const q = uV / vV;
173
- let t = vV;
174
- vV = uV % vV;
175
- uV = t;
176
- t = pa;
177
- pa = a - q * pa;
178
- a = t;
179
- t = pb;
180
- pb = b - q * pb;
181
- b = t;
182
- } const ra = new BigNumber(0n); ra._setValueFromSigned(a); const rb = new BigNumber(0n); rb._setValueFromSigned(b); const rg = new BigNumber(0n); rg._initializeState(uV < 0n ? -uV : uV, 0); return { a: ra, b: rb, gcd: rg }; }
183
- gcd(num: BigNumber): BigNumber { let u = this._magnitude; let v = num._magnitude; if (u === 0n) {
184
- const r = new BigNumber(0n);
185
- r._setValueFromSigned(v);
186
- return r.iabs();
187
- } if (v === 0n) {
188
- const r = new BigNumber(0n);
189
- r._setValueFromSigned(u);
190
- return r.iabs();
191
- } while (v !== 0n) {
192
- const t = u % v;
193
- u = v;
194
- v = t;
195
- } const res = new BigNumber(0n); res._initializeState(u, 0); return res; }
162
+ }
163
+ gcd(num: BigNumber): BigNumber
196
164
  invm(num: BigNumber): BigNumber
197
165
  isEven(): boolean
198
166
  isOdd(): boolean
@@ -3247,8 +3215,8 @@ export class Reader {
3247
3215
  public pos: number;
3248
3216
  constructor(bin: number[] = [], pos: number = 0)
3249
3217
  public eof(): boolean
3250
- public read(len = this.bin.length): number[]
3251
- public readReverse(len = this.bin.length): number[]
3218
+ public read(len = this.length): number[]
3219
+ public readReverse(len = this.length): number[]
3252
3220
  public readUInt8(): number
3253
3221
  public readInt8(): number
3254
3222
  public readUInt16BE(): number
@@ -5047,9 +5015,9 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
5047
5015
 
5048
5016
  ```ts
5049
5017
  exclusiveOR = function (block0: number[], block1: number[]): number[] {
5050
- let i;
5051
- const result = [];
5052
- for (i = 0; i < block0.length; i++) {
5018
+ const len = block0.length;
5019
+ const result = new Array(len);
5020
+ for (let i = 0; i < len; i++) {
5053
5021
  result[i] = block0[i] ^ block1[i];
5054
5022
  }
5055
5023
  return result;
@@ -5231,20 +5199,19 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
5231
5199
 
5232
5200
  ```ts
5233
5201
  multiply = function (block0: number[], block1: number[]): number[] {
5234
- let i;
5235
- let j;
5236
- let v = block1.slice();
5237
- let z = createZeroBlock(16);
5238
- for (i = 0; i < 16; i++) {
5239
- for (j = 7; j !== -1; j--) {
5240
- if (checkBit(block0, i, j) !== 0) {
5241
- z = exclusiveOR(z, v);
5202
+ const v = block1.slice();
5203
+ const z = createZeroBlock(16);
5204
+ for (let i = 0; i < 16; i++) {
5205
+ for (let j = 7; j >= 0; j--) {
5206
+ if ((block0[i] & (1 << j)) !== 0) {
5207
+ xorInto(z, v);
5242
5208
  }
5243
- if (checkBit(v, 15, 0) !== 0) {
5244
- v = exclusiveOR(rightShift(v), R);
5209
+ if ((v[15] & 1) !== 0) {
5210
+ rightShift(v);
5211
+ xorInto(v, R);
5245
5212
  }
5246
5213
  else {
5247
- v = rightShift(v);
5214
+ rightShift(v);
5248
5215
  }
5249
5216
  }
5250
5217
  }
@@ -5252,7 +5219,7 @@ multiply = function (block0: number[], block1: number[]): number[] {
5252
5219
  }
5253
5220
  ```
5254
5221
 
5255
- See also: [checkBit](./primitives.md#variable-checkbit), [exclusiveOR](./primitives.md#variable-exclusiveor), [rightShift](./primitives.md#variable-rightshift)
5222
+ See also: [rightShift](./primitives.md#variable-rightshift)
5256
5223
 
5257
5224
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5258
5225
 
package/docs/storage.md CHANGED
@@ -120,8 +120,6 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
120
120
 
121
121
  ### Class: StorageDownloader
122
122
 
123
- Locates HTTP URLs where content can be downloaded. It uses the passed or the default one.
124
-
125
123
  ```ts
126
124
  export class StorageDownloader {
127
125
  constructor(config?: DownloaderConfig)
@@ -132,6 +130,41 @@ export class StorageDownloader {
132
130
 
133
131
  See also: [DownloadResult](./storage.md#interface-downloadresult), [DownloaderConfig](./storage.md#interface-downloaderconfig)
134
132
 
133
+ #### Method download
134
+
135
+ Downloads the content from the UHRP URL after validating the hash for integrity.
136
+
137
+ ```ts
138
+ public async download(uhrpUrl: string): Promise<DownloadResult>
139
+ ```
140
+ See also: [DownloadResult](./storage.md#interface-downloadresult)
141
+
142
+ Returns
143
+
144
+ A promise that resolves to the downloaded content.
145
+
146
+ Argument Details
147
+
148
+ + **uhrpUrl**
149
+ + The UHRP URL to download.
150
+
151
+ #### Method resolve
152
+
153
+ Resolves the UHRP URL to a list of HTTP URLs where content can be downloaded.
154
+
155
+ ```ts
156
+ public async resolve(uhrpUrl: string): Promise<string[]>
157
+ ```
158
+
159
+ Returns
160
+
161
+ A promise that resolves to an array of HTTP URLs.
162
+
163
+ Argument Details
164
+
165
+ + **uhrpUrl**
166
+ + The UHRP URL to resolve.
167
+
135
168
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
136
169
 
137
170
  ---
@@ -1506,7 +1506,7 @@ export default class Transaction {
1506
1506
  static fromHex(hex: string): Transaction
1507
1507
  static fromHexEF(hex: string): Transaction
1508
1508
  static fromHexBEEF(hex: string, txid?: string): Transaction
1509
- constructor(version: number = 1, inputs: TransactionInput[] = [], outputs: TransactionOutput[] = [], lockTime: number = 0, metadata: Record<string, any> = {}, merklePath?: MerklePath)
1509
+ constructor(version: number = 1, inputs: TransactionInput[] = [], outputs: TransactionOutput[] = [], lockTime: number = 0, metadata: Record<string, any> = new Map(), merklePath?: MerklePath)
1510
1510
  addInput(input: TransactionInput): void
1511
1511
  addOutput(output: TransactionOutput): void
1512
1512
  addP2PKHOutput(address: number[] | string, satoshis?: number): void
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -38,6 +38,14 @@ export default class LocalKVStore {
38
38
  */
39
39
  private readonly originator?: string
40
40
 
41
+ acceptDelayedBroadcast: boolean = false
42
+
43
+ /**
44
+ * A map to store locks for each key to ensure atomic updates.
45
+ * @private
46
+ */
47
+ private readonly keyLocks: Map<string, Promise<void>> = new Map()
48
+
41
49
  /**
42
50
  * Creates an instance of the localKVStore.
43
51
  *
@@ -51,7 +59,8 @@ export default class LocalKVStore {
51
59
  wallet: WalletInterface = new WalletClient(),
52
60
  context = 'kvstore default',
53
61
  encrypt = true,
54
- originator?: string
62
+ originator?: string,
63
+ acceptDelayedBroadcast = false
55
64
  ) {
56
65
  if (typeof context !== 'string' || context.length < 1) {
57
66
  throw new Error('A context in which to operate is required.')
@@ -60,6 +69,7 @@ export default class LocalKVStore {
60
69
  this.context = context
61
70
  this.encrypt = encrypt
62
71
  this.originator = originator
72
+ this.acceptDelayedBroadcast = acceptDelayedBroadcast
63
73
  }
64
74
 
65
75
  private getProtocol (key: string): { protocolID: WalletProtocol, keyID: string } {
@@ -160,78 +170,106 @@ export default class LocalKVStore {
160
170
  }
161
171
 
162
172
  /**
163
- * Sets or updates the value associated with a given key.
173
+ * Sets or updates the value associated with a given key atomically.
164
174
  * If the key already exists (one or more outputs found), it spends the existing output(s)
165
175
  * and creates a new one with the updated value. If multiple outputs exist for the key,
166
176
  * they are collapsed into a single new output.
167
177
  * If the key does not exist, it creates a new output.
168
178
  * Handles encryption if enabled.
169
179
  * If signing the update/collapse transaction fails, it relinquishes the original outputs and starts over with a new chain.
180
+ * Ensures atomicity by locking the key during the operation, preventing concurrent updates
181
+ * to the same key from missing earlier changes.
170
182
  *
171
183
  * @param {string} key - The key to set or update.
172
184
  * @param {string} value - The value to associate with the key.
173
185
  * @returns {Promise<OutpointString>} A promise that resolves to the outpoint string (txid.vout) of the new or updated token output.
174
186
  */
175
187
  async set (key: string, value: string): Promise<OutpointString> {
176
- const current = await this.lookupValue(key, undefined, 10)
177
- if (current.value === value) {
178
- if (current.outpoint === undefined) { throw new Error('outpoint must be valid when value is valid and unchanged') }
179
- // Don't create a new transaction if the value doesn't need to change...
180
- return current.outpoint
181
- }
182
- const protocol = this.getProtocol(key)
183
- let valueAsArray = Utils.toArray(value, 'utf8')
184
- if (this.encrypt) {
185
- const { ciphertext } = await this.wallet.encrypt({
186
- ...protocol,
187
- plaintext: valueAsArray
188
- })
189
- valueAsArray = ciphertext
188
+ // Check if a lock exists for this key and wait for it to resolve
189
+ const existingLock = this.keyLocks.get(key)
190
+ if (existingLock != null) {
191
+ await existingLock
190
192
  }
191
- const pushdrop = new PushDrop(this.wallet, this.originator)
192
- const lockingScript = await pushdrop.lock(
193
- [valueAsArray],
194
- protocol.protocolID,
195
- protocol.keyID,
196
- 'self'
197
- )
198
- const { outputs, BEEF: inputBEEF } = current.lor
199
- let outpoint: OutpointString
193
+
194
+ let resolveNewLock: () => void = () => {}
195
+ const newLock = new Promise<void>((resolve) => {
196
+ resolveNewLock = resolve
197
+ })
198
+ this.keyLocks.set(key, newLock)
199
+
200
200
  try {
201
- const inputs = this.getInputs(outputs)
202
- const { txid, signableTransaction } = await this.wallet.createAction({
203
- description: `Update ${key} in ${this.context}`,
204
- inputBEEF,
205
- inputs,
206
- outputs: [{
207
- basket: this.context,
208
- tags: [key],
209
- lockingScript: lockingScript.toHex(),
210
- satoshis: 1,
211
- outputDescription: 'Key-value token'
212
- }],
213
- options: {
214
- acceptDelayedBroadcast: false,
215
- randomizeOutputs: false
201
+ const current = await this.lookupValue(key, undefined, 10)
202
+ if (current.value === value) {
203
+ if (current.outpoint === undefined) {
204
+ throw new Error('outpoint must be valid when value is valid and unchanged')
216
205
  }
217
- })
218
- if (outputs.length > 0 && typeof signableTransaction !== 'object') {
219
- throw new Error('Wallet did not return a signable transaction when expected.')
206
+ // Don't create a new transaction if the value doesn't need to change
207
+ return current.outpoint
220
208
  }
221
- if (signableTransaction == null) {
222
- outpoint = `${txid as string}.0`
223
- } else {
224
- const spends = await this.getSpends(key, outputs, pushdrop, signableTransaction.tx)
225
- const { txid } = await this.wallet.signAction({
226
- reference: signableTransaction.reference,
227
- spends
209
+
210
+ const protocol = this.getProtocol(key)
211
+ let valueAsArray = Utils.toArray(value, 'utf8')
212
+ if (this.encrypt) {
213
+ const { ciphertext } = await this.wallet.encrypt({
214
+ ...protocol,
215
+ plaintext: valueAsArray
228
216
  })
229
- outpoint = `${txid as string}.0`
217
+ valueAsArray = ciphertext
230
218
  }
231
- } catch (_) {
232
- throw new Error(`There are ${outputs.length} outputs with tag ${key} that cannot be unlocked.`)
219
+
220
+ const pushdrop = new PushDrop(this.wallet, this.originator)
221
+ const lockingScript = await pushdrop.lock(
222
+ [valueAsArray],
223
+ protocol.protocolID,
224
+ protocol.keyID,
225
+ 'self'
226
+ )
227
+
228
+ const { outputs, BEEF: inputBEEF } = current.lor
229
+ let outpoint: OutpointString
230
+ try {
231
+ const inputs = this.getInputs(outputs)
232
+ const { txid, signableTransaction } = await this.wallet.createAction({
233
+ description: `Update ${key} in ${this.context}`,
234
+ inputBEEF,
235
+ inputs,
236
+ outputs: [{
237
+ basket: this.context,
238
+ tags: [key],
239
+ lockingScript: lockingScript.toHex(),
240
+ satoshis: 1,
241
+ outputDescription: 'Key-value token'
242
+ }],
243
+ options: {
244
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast,
245
+ randomizeOutputs: false
246
+ }
247
+ })
248
+
249
+ if (outputs.length > 0 && typeof signableTransaction !== 'object') {
250
+ throw new Error('Wallet did not return a signable transaction when expected.')
251
+ }
252
+
253
+ if (signableTransaction == null) {
254
+ outpoint = `${txid as string}.0`
255
+ } else {
256
+ const spends = await this.getSpends(key, outputs, pushdrop, signableTransaction.tx)
257
+ const { txid } = await this.wallet.signAction({
258
+ reference: signableTransaction.reference,
259
+ spends
260
+ })
261
+ outpoint = `${txid as string}.0`
262
+ }
263
+ } catch (_) {
264
+ throw new Error(`There are ${outputs.length} outputs with tag ${key} that cannot be unlocked.`)
265
+ }
266
+
267
+ return outpoint
268
+ } finally {
269
+ // Release the lock by resolving the promise and removing it from the map
270
+ this.keyLocks.delete(key)
271
+ resolveNewLock()
233
272
  }
234
- return outpoint
235
273
  }
236
274
 
237
275
  /**
@@ -257,7 +295,7 @@ export default class LocalKVStore {
257
295
  inputBEEF,
258
296
  inputs,
259
297
  options: {
260
- acceptDelayedBroadcast: false
298
+ acceptDelayedBroadcast: this.acceptDelayedBroadcast
261
299
  }
262
300
  })
263
301
  if (typeof signableTransaction !== 'object') {
@@ -13,17 +13,6 @@ export interface DownloadResult {
13
13
  mimeType: string | null
14
14
  }
15
15
 
16
- /**
17
- * Locates HTTP URLs where content can be downloaded. It uses the passed or the default one.
18
- *
19
- * @param {Object} obj All parameters are passed in an object.
20
- * @param {String} obj.uhrpUrl The UHRP url to resolve.
21
- * @param {string} obj.confederacyHost HTTPS URL for for the with default setting.
22
- *
23
- * @return {Array<String>} An array of HTTP URLs where content can be downloaded.
24
- * @throws {Error} If UHRP url parameter invalid or is not an array
25
- * or there is an error retrieving url(s) stored in the UHRP token.
26
- */
27
16
  export class StorageDownloader {
28
17
  private readonly networkPreset?: 'mainnet' | 'testnet' | 'local' = 'mainnet'
29
18
 
@@ -31,6 +20,11 @@ export class StorageDownloader {
31
20
  this.networkPreset = config?.networkPreset
32
21
  }
33
22
 
23
+ /**
24
+ * Resolves the UHRP URL to a list of HTTP URLs where content can be downloaded.
25
+ * @param uhrpUrl The UHRP URL to resolve.
26
+ * @returns A promise that resolves to an array of HTTP URLs.
27
+ */
34
28
  public async resolve (uhrpUrl: string): Promise<string[]> {
35
29
  // Use UHRP lookup service
36
30
  const lookupResolver = new LookupResolver({ networkPreset: this.networkPreset })
@@ -54,6 +48,11 @@ export class StorageDownloader {
54
48
  return decodedResults
55
49
  }
56
50
 
51
+ /**
52
+ * Downloads the content from the UHRP URL after validating the hash for integrity.
53
+ * @param uhrpUrl The UHRP URL to download.
54
+ * @returns A promise that resolves to the downloaded content.
55
+ */
57
56
  public async download (uhrpUrl: string): Promise<DownloadResult> {
58
57
  if (!StorageUtils.isValidURL(uhrpUrl)) {
59
58
  throw new Error('Invalid parameter UHRP url')
@@ -0,0 +1,138 @@
1
+ import ChainTracker from '../ChainTracker.js'
2
+ import { HttpClient } from '../http/HttpClient.js'
3
+ import { defaultHttpClient } from '../http/DefaultHttpClient.js'
4
+
5
+ /** Configuration options for the BlockHeadersService ChainTracker. */
6
+ export interface BlockHeadersServiceConfig {
7
+ /** The HTTP client used to make requests to the API. */
8
+ httpClient?: HttpClient
9
+
10
+ /** The API key used to authenticate requests to the BlockHeadersService API. */
11
+ apiKey?: string
12
+ }
13
+
14
+ interface MerkleRootVerificationRequest {
15
+ blockHeight: number
16
+ merkleRoot: string
17
+ }
18
+
19
+ interface MerkleRootConfirmation {
20
+ blockHash: string
21
+ blockHeight: number
22
+ merkleRoot: string
23
+ confirmation: 'CONFIRMED' | 'UNCONFIRMED'
24
+ }
25
+
26
+ interface MerkleRootVerificationResponse {
27
+ confirmationState: 'CONFIRMED' | 'UNCONFIRMED'
28
+ confirmations: MerkleRootConfirmation[]
29
+ }
30
+
31
+ /**
32
+ * Represents a chain tracker based on a BlockHeadersService API.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const chainTracker = new BlockHeadersService('https://headers.spv.money', {
37
+ * apiKey: '17JxRHcJerGBEbusx56W8o1m8Js73TFGo'
38
+ * })
39
+ * ```
40
+ */
41
+ export class BlockHeadersService implements ChainTracker {
42
+ protected readonly baseUrl: string
43
+ protected readonly httpClient: HttpClient
44
+ protected readonly apiKey: string
45
+
46
+ /**
47
+ * Constructs an instance of the BlockHeadersService ChainTracker.
48
+ *
49
+ * @param {string} baseUrl - The base URL for the BlockHeadersService API (e.g. https://headers.spv.money)
50
+ * @param {BlockHeadersServiceConfig} config - Configuration options for the BlockHeadersService ChainTracker.
51
+ */
52
+ constructor(
53
+ baseUrl: string,
54
+ config: BlockHeadersServiceConfig = {}
55
+ ) {
56
+ const { httpClient, apiKey } = config
57
+ this.baseUrl = baseUrl
58
+ this.httpClient = httpClient ?? defaultHttpClient()
59
+ this.apiKey = apiKey ?? ''
60
+ }
61
+
62
+ /**
63
+ * Verifies if a given merkle root is valid for a specific block height.
64
+ *
65
+ * @param {string} root - The merkle root to verify.
66
+ * @param {number} height - The block height to check against.
67
+ * @returns {Promise<boolean>} - A promise that resolves to true if the merkle root is valid for the specified block height, false otherwise.
68
+ */
69
+ async isValidRootForHeight(root: string, height: number): Promise<boolean> {
70
+ const requestOptions = {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ 'Accept': 'application/json',
75
+ 'Authorization': `Bearer ${this.apiKey}`
76
+ },
77
+ data: [
78
+ {
79
+ blockHeight: height,
80
+ merkleRoot: root
81
+ }
82
+ ] as MerkleRootVerificationRequest[]
83
+ }
84
+
85
+ try {
86
+ const response = await this.httpClient.request<MerkleRootVerificationResponse>(
87
+ `${this.baseUrl}/api/v1/chain/merkleroot/verify`,
88
+ requestOptions
89
+ )
90
+
91
+ if (response.ok) {
92
+ return response.data.confirmationState === 'CONFIRMED'
93
+ } else {
94
+ throw new Error(
95
+ `Failed to verify merkleroot for height ${height} because of an error: ${JSON.stringify(response.data)}`
96
+ )
97
+ }
98
+ } catch (error) {
99
+ throw new Error(
100
+ `Failed to verify merkleroot for height ${height} because of an error: ${error instanceof Error ? error.message : String(error)}`
101
+ )
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Gets the current block height from the BlockHeadersService API.
107
+ *
108
+ * @returns {Promise<number>} - A promise that resolves to the current block height.
109
+ */
110
+ async currentHeight(): Promise<number> {
111
+ const requestOptions = {
112
+ method: 'GET',
113
+ headers: {
114
+ 'Accept': 'application/json',
115
+ 'Authorization': `Bearer ${this.apiKey}`
116
+ }
117
+ }
118
+
119
+ try {
120
+ const response = await this.httpClient.request<{ height: number }>(
121
+ `${this.baseUrl}/api/v1/chain/tip/longest`,
122
+ requestOptions
123
+ )
124
+
125
+ if (response.ok && response.data && typeof response.data.height === 'number') {
126
+ return response.data.height
127
+ } else {
128
+ throw new Error(
129
+ `Failed to get current height because of an error: ${JSON.stringify(response.data)}`
130
+ )
131
+ }
132
+ } catch (error) {
133
+ throw new Error(
134
+ `Failed to get current height because of an error: ${error instanceof Error ? error.message : String(error)}`
135
+ )
136
+ }
137
+ }
138
+ }
@@ -1,3 +1,4 @@
1
1
  export { default as WhatsOnChain } from './WhatsOnChain.js'
2
2
  export type { WhatsOnChainConfig } from './WhatsOnChain.js'
3
+ export { BlockHeadersService } from './BlockHeadersService.js'
3
4
  export { defaultChainTracker } from './DefaultChainTracker.js'