@bsv/sdk 2.1.1 → 2.1.2

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 (53) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/identity/ContactsManager.js +44 -6
  3. package/dist/cjs/src/identity/ContactsManager.js.map +1 -1
  4. package/dist/cjs/src/identity/IdentityClient.js +106 -37
  5. package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
  6. package/dist/cjs/src/overlay-tools/LookupResolver.js +85 -58
  7. package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
  8. package/dist/cjs/src/primitives/Hash.js +173 -50
  9. package/dist/cjs/src/primitives/Hash.js.map +1 -1
  10. package/dist/cjs/src/primitives/SymmetricKey.js +123 -1
  11. package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/identity/ContactsManager.js +44 -6
  14. package/dist/esm/src/identity/ContactsManager.js.map +1 -1
  15. package/dist/esm/src/identity/IdentityClient.js +106 -37
  16. package/dist/esm/src/identity/IdentityClient.js.map +1 -1
  17. package/dist/esm/src/overlay-tools/LookupResolver.js +85 -58
  18. package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
  19. package/dist/esm/src/primitives/Hash.js +177 -50
  20. package/dist/esm/src/primitives/Hash.js.map +1 -1
  21. package/dist/esm/src/primitives/SymmetricKey.js +123 -1
  22. package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/src/identity/ContactsManager.d.ts +13 -2
  25. package/dist/types/src/identity/ContactsManager.d.ts.map +1 -1
  26. package/dist/types/src/identity/IdentityClient.d.ts +50 -24
  27. package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
  28. package/dist/types/src/overlay-tools/LookupResolver.d.ts +14 -1
  29. package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
  30. package/dist/types/src/primitives/Hash.d.ts +21 -16
  31. package/dist/types/src/primitives/Hash.d.ts.map +1 -1
  32. package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
  33. package/dist/types/src/wallet/Wallet.interfaces.d.ts +16 -1
  34. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
  35. package/dist/types/src/wallet/WalletClient.d.ts +1 -1
  36. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  37. package/dist/types/src/wallet/substrates/window.CWI.d.ts +1 -1
  38. package/dist/types/src/wallet/substrates/window.CWI.d.ts.map +1 -1
  39. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  40. package/dist/umd/bundle.js +3 -3
  41. package/package.json +1 -1
  42. package/src/identity/ContactsManager.ts +47 -6
  43. package/src/identity/IdentityClient.ts +137 -53
  44. package/src/identity/__tests/IdentityClient.additional.test.ts +150 -1
  45. package/src/identity/__tests/IdentityClient.test.ts +4 -4
  46. package/src/overlay-tools/LookupResolver.ts +94 -57
  47. package/src/primitives/Hash.ts +232 -96
  48. package/src/primitives/SymmetricKey.ts +145 -1
  49. package/src/primitives/__tests/Hash.additional.test.ts +65 -0
  50. package/src/primitives/__tests/Hash.test.ts +6 -1
  51. package/src/wallet/Wallet.interfaces.ts +16 -1
  52. package/src/wallet/WalletClient.ts +1 -1
  53. package/src/wallet/substrates/window.CWI.ts +1 -1
@@ -1,4 +1,5 @@
1
1
  import { Transaction } from '../transaction/index.js'
2
+ import { Beef } from '../transaction/Beef.js'
2
3
  import OverlayAdminTokenTemplate from './OverlayAdminTokenTemplate.js'
3
4
  import * as Utils from '../primitives/utils.js'
4
5
  import { getOverlayHostReputationTracker, HostReputationTracker } from './HostReputationTracker.js'
@@ -35,6 +36,8 @@ export type LookupAnswer =
35
36
  beef: number[]
36
37
  outputIndex: number
37
38
  context?: number[]
39
+ /** Optional txid hint. When present, consumers can skip re-parsing beef to derive the txid. */
40
+ txid?: string
38
41
  }>
39
42
  }
40
43
 
@@ -65,7 +68,7 @@ export interface LookupQueryOptions {
65
68
  */
66
69
  export interface LookupAnswerProgress {
67
70
  type: 'output-list'
68
- outputs: Array<{ beef: number[], outputIndex: number, context?: number[] }>
71
+ outputs: Array<{ beef: number[], outputIndex: number, context?: number[], txid?: string }>
69
72
  /** Parallel array of resolved tx ids for each output (same index as `outputs`). */
70
73
  txIds: string[]
71
74
  /** True only for the final emission, after every in-flight host has settled. */
@@ -196,44 +199,61 @@ export class HTTPSOverlayLookupFacilitator implements OverlayLookupFacilitator {
196
199
 
197
200
  if (!response.ok) throw new Error(`Failed to facilitate lookup (HTTP ${response.status})`)
198
201
  if (response.headers.get('content-type') === 'application/octet-stream') {
199
- const payload = await response.arrayBuffer()
200
- const r = new Utils.Reader([...new Uint8Array(payload)])
201
- const nOutpoints = r.readVarIntNum()
202
- const outpoints: Array<{ txid: string, outputIndex: number, context?: number[] }> = []
203
- for (let i = 0; i < nOutpoints; i++) {
204
- const txid = Utils.toHex(r.read(32))
205
- const outputIndex = r.readVarIntNum()
206
- const contextLength = r.readVarIntNum()
207
- let context
208
- if (contextLength > 0) {
209
- context = r.read(contextLength)
210
- }
211
- outpoints.push({
212
- txid,
213
- outputIndex,
214
- context
215
- })
216
- }
217
- const beef = r.read()
218
- return {
219
- type: 'output-list',
220
- outputs: outpoints.map(x => ({
221
- outputIndex: x.outputIndex,
222
- context: x.context,
223
- beef: Transaction.fromBEEF(beef, x.txid).toBEEF()
224
- }))
225
- }
226
- } else {
227
- return await response.json()
202
+ return await this.parseOctetStreamLookup(response)
228
203
  }
204
+ return await response.json()
229
205
  } catch (e) {
230
206
  // Normalize timeouts to a consistent error message
231
- if ((e as { name?: string })?.name === 'AbortError') throw new Error('Request timed out')
207
+ if ((e as { name?: string })?.name === 'AbortError') {
208
+ throw new Error('Request timed out')
209
+ }
232
210
  throw e
233
211
  } finally {
234
212
  clearTimeout(timer)
235
213
  }
236
214
  }
215
+
216
+ /** Parse the aggregated octet-stream lookup response into an output-list LookupAnswer. */
217
+ private async parseOctetStreamLookup (response: Response): Promise<LookupAnswer> {
218
+ const payload = await response.arrayBuffer()
219
+ const r = new Utils.Reader([...new Uint8Array(payload)])
220
+ const nOutpoints = r.readVarIntNum()
221
+ const outpoints: Array<{ txid: string, outputIndex: number, context?: number[] }> = []
222
+ for (let i = 0; i < nOutpoints; i++) {
223
+ const txid = Utils.toHex(r.read(32))
224
+ const outputIndex = r.readVarIntNum()
225
+ const contextLength = r.readVarIntNum()
226
+ const context = contextLength > 0 ? r.read(contextLength) : undefined
227
+ outpoints.push({ txid, outputIndex, context })
228
+ }
229
+ const beef = r.read()
230
+ const beefObj = Beef.fromBinary(beef)
231
+ const outputs = await this.extractAtomicOutputs(outpoints, beefObj)
232
+ return { type: 'output-list', outputs }
233
+ }
234
+
235
+ /** Memoize per-txid atomic BEEF extraction, yielding to the event loop between outputs. */
236
+ private async extractAtomicOutputs (
237
+ outpoints: Array<{ txid: string, outputIndex: number, context?: number[] }>,
238
+ beefObj: Beef
239
+ ): Promise<Array<{ outputIndex: number, context?: number[], beef: number[], txid: string }>> {
240
+ const beefByTxid = new Map<string, number[]>()
241
+ const outputs: Array<{ outputIndex: number, context?: number[], beef: number[], txid: string }> = new Array(outpoints.length)
242
+ for (let idx = 0; idx < outpoints.length; idx++) {
243
+ const x = outpoints[idx]
244
+ let beefBytes = beefByTxid.get(x.txid)
245
+ if (beefBytes === undefined) {
246
+ beefBytes = beefObj.toBinaryAtomic(x.txid)
247
+ beefByTxid.set(x.txid, beefBytes)
248
+ }
249
+ outputs[idx] = { outputIndex: x.outputIndex, context: x.context, beef: beefBytes, txid: x.txid }
250
+ // Yield to event loop so UI animations and other JS don't starve.
251
+ if (idx > 0 && idx < outpoints.length - 1) {
252
+ await new Promise<void>((resolve) => setTimeout(resolve, 0))
253
+ }
254
+ }
255
+ return outputs
256
+ }
237
257
  }
238
258
 
239
259
  /**
@@ -296,13 +316,18 @@ export default class LookupResolver {
296
316
  timeout?: number,
297
317
  options?: LookupQueryOptions
298
318
  ): Promise<LookupAnswer> {
299
- let last: LookupAnswerProgress | null = null
300
319
  // Existing fast-but-narrow contract: return at the first cumulative emission
301
320
  // (the post-grace aggregate, or the final emission when every host settles
302
321
  // before the grace window). Callers wanting progressive enrichment use query$().
303
- for await (const partial of this.query$(question, timeout, options)) {
304
- last = partial
305
- break
322
+ // Take only the first emission, then explicitly close the iterator so the
323
+ // generator's `finally` block runs and clears any outstanding timers.
324
+ const iter = this.query$(question, timeout, options)[Symbol.asyncIterator]()
325
+ let last: LookupAnswerProgress | null = null
326
+ try {
327
+ const { value, done } = await iter.next()
328
+ if (done !== true && value != null) last = value
329
+ } finally {
330
+ await iter.return?.(undefined)
306
331
  }
307
332
  return {
308
333
  type: 'output-list',
@@ -401,31 +426,16 @@ export default class LookupResolver {
401
426
  let graceFired = false
402
427
  let emittedOnce = false
403
428
 
404
- const beefKey = (beef: number[] | undefined): string => {
405
- if (typeof beef !== 'object' || beef == null) return ''
406
- return beef.join(',')
407
- }
408
-
409
429
  const mergeAnswer = (answer: LookupAnswer): boolean => {
410
430
  let added = false
431
+ const now = Date.now()
411
432
  for (const output of answer.outputs) {
412
- const keyForBeef = beefKey(output.beef)
413
- let memo = this.txMemo.get(keyForBeef)
414
- const now = Date.now()
415
- if (typeof memo !== 'object' || memo === null || memo.expiresAt <= now) {
416
- try {
417
- const txId = Transaction.fromBEEF(output.beef).id('hex')
418
- memo = { txId, expiresAt: now + this.txMemoTtlMs }
419
- if (this.txMemo.size > 4096) this.evictOldest(this.txMemo)
420
- this.txMemo.set(keyForBeef, memo)
421
- } catch {
422
- continue
423
- }
424
- }
425
- const uniqKey = `${memo.txId}.${output.outputIndex}`
433
+ const txId = this.resolveTxIdForOutput(output, now)
434
+ if (txId === null) continue
435
+ const uniqKey = `${txId}.${output.outputIndex}`
426
436
  if (!outputsMap.has(uniqKey)) {
427
437
  outputsMap.set(uniqKey, output)
428
- txIds.push(memo.txId)
438
+ txIds.push(txId)
429
439
  added = true
430
440
  }
431
441
  }
@@ -603,7 +613,7 @@ export default class LookupResolver {
603
613
  resolve([...allHosts])
604
614
  }
605
615
  })
606
- .catch(() => { /* tracker failed; tracked by lookupHostWithTracking */ })
616
+ .catch(() => { /* tracker failure tracked in reputation */ })
607
617
  .finally(() => {
608
618
  pending--
609
619
  if (pending === 0 && !resolved) {
@@ -615,7 +625,34 @@ export default class LookupResolver {
615
625
  })
616
626
  }
617
627
 
618
- /** Evict an arbitrary “oldest” entry from a Map (iteration order). */
628
+ /**
629
+ * Resolve a txid for an aggregated lookup output. Uses the threaded-through `output.txid`
630
+ * fast path when present; otherwise memoizes Transaction.fromBEEF(beef).id('hex') keyed by
631
+ * the BEEF byte sequence. Returns null when the BEEF is unparseable.
632
+ */
633
+ private resolveTxIdForOutput (
634
+ output: { txid?: string, beef: number[], outputIndex: number, context?: number[] },
635
+ now: number
636
+ ): string | null {
637
+ if (typeof output.txid === 'string' && output.txid.length > 0) {
638
+ return output.txid
639
+ }
640
+ const keyForBeef = Array.isArray(output.beef) ? output.beef.join(',') : ''
641
+ const memo = this.txMemo.get(keyForBeef)
642
+ if (typeof memo === 'object' && memo !== null && memo.expiresAt > now) {
643
+ return memo.txId
644
+ }
645
+ try {
646
+ const txId = Transaction.fromBEEF(output.beef).id('hex')
647
+ if (this.txMemo.size > 4096) this.evictOldest(this.txMemo)
648
+ this.txMemo.set(keyForBeef, { txId, expiresAt: now + this.txMemoTtlMs })
649
+ return txId
650
+ } catch {
651
+ return null
652
+ }
653
+ }
654
+
655
+ /** Evict an arbitrary "oldest" entry from a Map (iteration order). */
619
656
  private evictOldest<T>(m: Map<string, T>): void {
620
657
  const firstKey = m.keys().next().value
621
658
  if (firstKey !== undefined) m.delete(firstKey)