@bsv/overlay 0.3.0 → 0.4.0

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/src/Engine.ts CHANGED
@@ -114,12 +114,13 @@ export class Engine {
114
114
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
115
115
  * @param {function(STEAK): void} [onSTEAKReady] - Optional callback function invoked when the STEAK is ready.
116
116
  * @param {string} mode — Indicates the submission behavior, whether historical or current. Historical transactions are not broadcast or propagated.
117
+ * @param {number[]} offChainValues — Values necessary to evaluate topical admittance that are not stored on-chain.
117
118
  *
118
119
  * The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
119
120
  *
120
121
  * @returns {Promise<STEAK>} The submitted transaction execution acknowledgement
121
122
  */
122
- async submit (taggedBEEF: TaggedBEEF, onSteakReady?: (steak: STEAK) => void, mode: 'historical-tx' | 'current-tx' = 'current-tx'): Promise<STEAK> {
123
+ async submit (taggedBEEF: TaggedBEEF, onSteakReady?: (steak: STEAK) => void, mode: 'historical-tx' | 'current-tx' = 'current-tx', offChainValues?: number[]): Promise<STEAK> {
123
124
  for (const t of taggedBEEF.topics) {
124
125
  if (this.managers[t] === undefined || this.managers[t] === null) {
125
126
  throw new Error(`This server does not support this topic: ${t}`)
@@ -209,7 +210,8 @@ export class Engine {
209
210
  unlockingScript: tx.inputs[inputIndex].unlockingScript!,
210
211
  txid: output.txid,
211
212
  outputIndex: output.outputIndex,
212
- topic
213
+ topic,
214
+ offChainValues
213
215
  })
214
216
  } else if (l.spendNotificationMode === 'whole-tx') {
215
217
  await l.outputSpent({
@@ -217,7 +219,8 @@ export class Engine {
217
219
  spendingAtomicBEEF: tx.toAtomicBEEF(),
218
220
  txid: output.txid,
219
221
  outputIndex: output.outputIndex,
220
- topic
222
+ topic,
223
+ offChainValues
221
224
  })
222
225
  } else { // none
223
226
  await l.outputSpent({
@@ -243,7 +246,7 @@ export class Engine {
243
246
  const admissibleOutputsPromise = (async () => {
244
247
  try {
245
248
  this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
246
- admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
249
+ admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins, offChainValues)
247
250
  this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
248
251
  } catch (_) {
249
252
  steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
@@ -372,14 +375,16 @@ export class Engine {
372
375
  outputIndex,
373
376
  lockingScript: tx.outputs[outputIndex].lockingScript,
374
377
  satoshis: tx.outputs[outputIndex].satoshis,
375
- topic
378
+ topic,
379
+ offChainValues
376
380
  })
377
381
  } else {
378
382
  await l.outputAdmittedByTopic({
379
383
  mode: 'whole-tx',
380
384
  atomicBEEF: tx.toAtomicBEEF(),
381
385
  outputIndex,
382
- topic
386
+ topic,
387
+ offChainValues
383
388
  })
384
389
  }
385
390
  }))
@@ -453,16 +458,9 @@ export class Engine {
453
458
  const lookupService = this.lookupServices[lookupQuestion.service]
454
459
  if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service} `)
455
460
 
456
- let lookupResult = await lookupService.lookup(lookupQuestion)
457
- // Handle custom lookup service answers
458
- if ((lookupResult as LookupAnswer).type === 'freeform' || (lookupResult as LookupAnswer).type === 'output-list') {
459
- return lookupResult as LookupAnswer
460
- }
461
- lookupResult = lookupResult as LookupFormula
462
-
463
- const hydratedOutputs: Array<{ beef: number[], outputIndex: number }> = []
464
-
465
- for (const { txid, outputIndex, history } of lookupResult) {
461
+ const lookupResult = await lookupService.lookup(lookupQuestion)
462
+ const hydratedOutputs: Array<{ beef: number[], outputIndex: number, context?: number[] }> = []
463
+ for (const { txid, outputIndex, history, context } of lookupResult) {
466
464
  // Make sure this is an unspent output (UTXO)
467
465
  const UTXO = await this.storage.findOutput(
468
466
  txid,
@@ -478,7 +476,8 @@ export class Engine {
478
476
  if (output?.beef !== undefined) {
479
477
  hydratedOutputs.push({
480
478
  beef: output.beef,
481
- outputIndex: output.outputIndex
479
+ outputIndex: output.outputIndex,
480
+ context
482
481
  })
483
482
  }
484
483
  }
@@ -1,5 +1,5 @@
1
1
  import { GASPNode, GASPNodeResponse, GASPStorage } from '@bsv/gasp'
2
- import { MerklePath, Transaction } from '@bsv/sdk'
2
+ import { MerklePath, Transaction, Utils } from '@bsv/sdk'
3
3
  import { Engine } from '../Engine.js'
4
4
 
5
5
  /**
@@ -90,7 +90,7 @@ export class OverlayGASPStorage implements GASPStorage {
90
90
 
91
91
  // Attempt to check if the current transaction is admissible
92
92
  parsedTx.merklePath = MerklePath.fromHex(tx.proof)
93
- const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), [])
93
+ const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), [], typeof tx.txMetadata === 'string' ? Utils.toArray(tx.txMetadata) : undefined)
94
94
 
95
95
  if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
96
96
  // The transaction is admissible, no further inputs are needed
@@ -23,4 +23,9 @@ export type LookupFormula = Array<{
23
23
  * If not provided, no historical information will be included with the Lookup Answer, except that which may incidentally be required to fully anchor the Lookup Answer to the blockchain.
24
24
  */
25
25
  history?: ((beef: number[], outputIndex: number, currentDepth: number) => Promise<boolean>) | number
26
+
27
+ /**
28
+ * Context for the UTXO, derived from the off-chain values.
29
+ */
30
+ context?: number[]
26
31
  }>
@@ -18,12 +18,14 @@ export type OutputAdmittedByTopic =
18
18
  topic: string // topic into which it was admitted
19
19
  satoshis: number // value of the output
20
20
  lockingScript: Script // script in this output
21
+ offChainValues?: number[] // off-chain values associated with the output
21
22
  }
22
23
  | { // «whole-tx» mode
23
24
  mode: 'whole-tx'
24
25
  atomicBEEF: number[] // whole transaction (Atomic BEEF)
25
26
  outputIndex: number
26
27
  topic: string
28
+ offChainValues?: number[]
27
29
  }
28
30
 
29
31
  /* ---------------------------------------------------------------------------
@@ -52,6 +54,7 @@ export type OutputSpent =
52
54
  inputIndex: number
53
55
  unlockingScript: Script
54
56
  sequenceNumber: number
57
+ offChainValues?: number[]
55
58
  }
56
59
  | { // «whole-tx» – full spending TX
57
60
  mode: 'whole-tx'
@@ -59,6 +62,7 @@ export type OutputSpent =
59
62
  outputIndex: number
60
63
  topic: string
61
64
  spendingAtomicBEEF: number[]
65
+ offChainValues?: number[]
62
66
  }
63
67
 
64
68
  /* ---------------------------------------------------------------------------
@@ -121,7 +125,7 @@ export interface LookupService {
121
125
  /* -------------------------------------------------------------------------
122
126
  * Query API
123
127
  * ----------------------------------------------------------------------- */
124
- lookup: (question: LookupQuestion) => Promise<LookupAnswer | LookupFormula>
128
+ lookup: (question: LookupQuestion) => Promise<LookupFormula>
125
129
 
126
130
  /* -------------------------------------------------------------------------
127
131
  * Documentation helpers
@@ -9,13 +9,13 @@ export interface TopicManager {
9
9
  * Accepts the transaction in BEEF format and an array of those input indices which spend previously-admitted outputs from the same topic.
10
10
  * The transaction's BEEF structure will always contain the transactions associated with previous coins for reference (if any), regardless of whether the current transaction was directly proven.
11
11
  */
12
- identifyAdmissibleOutputs: (beef: number[], previousCoins: number[]) => Promise<AdmittanceInstructions>
12
+ identifyAdmissibleOutputs: (beef: number[], previousCoins: number[], offChainValues?: number[]) => Promise<AdmittanceInstructions>
13
13
 
14
14
  /**
15
15
  * Identifies and returns the inputs needed to anchor any topical outputs from this transaction to their associated previous history.
16
16
  * @throws - if there are no potentially valid topical outputs in this transaction
17
17
  */
18
- identifyNeededInputs?: (beef: number[]) => Promise<Array<{ txid: string, outputIndex: number }>>
18
+ identifyNeededInputs?: (beef: number[], offChainValues?: number[]) => Promise<Array<{ txid: string, outputIndex: number }>>
19
19
 
20
20
  /**
21
21
  * Returns a Markdown-formatted documentation string for the topic manager.
@@ -534,7 +534,7 @@ describe('BSV Overlay Services Engine', () => {
534
534
  beef: exampleBeef,
535
535
  topics: ['Hello']
536
536
  })
537
- expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(exampleBeef, [0])
537
+ expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(exampleBeef, [0], undefined)
538
538
  })
539
539
  describe('When previous UTXOs were retained by the topic manager', () => {
540
540
  it('Notifies all lookup services about the output being spent (not deleted, see the comment about this in deleteUTXODeep)', async () => {