@bsv/overlay 0.1.28 → 0.2.1
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/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Engine.js +91 -12
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +89 -11
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-05-18-001-initial.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -1
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts +1 -1
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/LookupService.d.ts +67 -48
- package/dist/types/src/LookupService.d.ts.map +1 -1
- package/dist/types/src/Output.d.ts +2 -2
- package/dist/types/src/Output.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts +2 -2
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2024-05-18-001-initial.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/mod.ts +1 -1
- package/package.json +2 -2
- package/src/Engine.ts +133 -60
- package/src/GASP/OverlayGASPRemote.ts +6 -6
- package/src/GASP/OverlayGASPStorage.ts +17 -17
- package/src/LookupService.ts +112 -49
- package/src/Output.ts +1 -1
- package/src/__tests/Engine.test.ts +38 -29
- package/src/storage/Storage.ts +0 -1
- package/src/storage/knex/KnexStorage.ts +12 -12
- package/src/storage/knex/all-migrations.ts +1 -1
- package/src/storage/knex/migrations/2024-05-18-001-initial.ts +2 -2
- package/src/storage/knex/migrations/2024-07-10-001-block-height.ts +2 -2
- package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +2 -2
- package/src/storage/knex/migrations/2024-07-18-001-indexes.ts +2 -2
package/src/Engine.ts
CHANGED
|
@@ -47,7 +47,7 @@ export class Engine {
|
|
|
47
47
|
* @param {OverlayBroadcastFacilitator} overlayBroadcastFacilitator - Facilitator for propagation to other Overlay Services.
|
|
48
48
|
* @param {typeof console} logger - The place where log entries are written.
|
|
49
49
|
*/
|
|
50
|
-
constructor(
|
|
50
|
+
constructor (
|
|
51
51
|
public managers: { [key: string]: TopicManager },
|
|
52
52
|
public lookupServices: { [key: string]: LookupService },
|
|
53
53
|
public storage: Storage,
|
|
@@ -76,14 +76,14 @@ export class Engine {
|
|
|
76
76
|
if (managerName === 'tm_ship' && this.shipTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
|
|
77
77
|
// Combine tm_ship trackers with preexisting entries if any
|
|
78
78
|
const combinedSet = new Set([
|
|
79
|
-
...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName]
|
|
79
|
+
...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName] : []),
|
|
80
80
|
...this.shipTrackers
|
|
81
81
|
])
|
|
82
82
|
this.syncConfiguration[managerName] = Array.from(combinedSet)
|
|
83
83
|
} else if (managerName === 'tm_slap' && this.slapTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
|
|
84
84
|
// Combine tm_slap trackers with preexisting entries if any
|
|
85
85
|
const combinedSet = new Set([
|
|
86
|
-
...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName]
|
|
86
|
+
...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName] : []),
|
|
87
87
|
...this.slapTrackers
|
|
88
88
|
])
|
|
89
89
|
this.syncConfiguration[managerName] = Array.from(combinedSet)
|
|
@@ -97,13 +97,13 @@ export class Engine {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Helper functions for logging timings
|
|
100
|
-
private startTime(label: string): void {
|
|
100
|
+
private startTime (label: string): void {
|
|
101
101
|
if (this.logTime) {
|
|
102
102
|
this.logger.time(`${this.logPrefix} ${label}`)
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
private endTime(label: string): void {
|
|
106
|
+
private endTime (label: string): void {
|
|
107
107
|
if (this.logTime) {
|
|
108
108
|
this.logger.timeEnd(`${this.logPrefix} ${label}`)
|
|
109
109
|
}
|
|
@@ -114,12 +114,12 @@ 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
|
-
*
|
|
117
|
+
*
|
|
118
118
|
* The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
|
|
119
|
-
*
|
|
119
|
+
*
|
|
120
120
|
* @returns {Promise<STEAK>} The submitted transaction execution acknowledgement
|
|
121
121
|
*/
|
|
122
|
-
async submit(taggedBEEF: TaggedBEEF, onSteakReady?: (steak: STEAK) => void, mode: 'historical-tx' | 'current-tx' = 'current-tx'): Promise<STEAK> {
|
|
122
|
+
async submit (taggedBEEF: TaggedBEEF, onSteakReady?: (steak: STEAK) => void, mode: 'historical-tx' | 'current-tx' = 'current-tx'): Promise<STEAK> {
|
|
123
123
|
for (const t of taggedBEEF.topics) {
|
|
124
124
|
if (this.managers[t] === undefined || this.managers[t] === null) {
|
|
125
125
|
throw new Error(`This server does not support this topic: ${t}`)
|
|
@@ -181,7 +181,53 @@ export class Engine {
|
|
|
181
181
|
await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic)
|
|
182
182
|
await Promise.all(Object.values(this.lookupServices).map(async l => {
|
|
183
183
|
try {
|
|
184
|
-
|
|
184
|
+
if (typeof l.outputSpent === 'function') {
|
|
185
|
+
if (l.spendNotificationMode === 'txid') {
|
|
186
|
+
await l.outputSpent({
|
|
187
|
+
mode: 'txid',
|
|
188
|
+
spendingTxid: txid,
|
|
189
|
+
txid: output.txid,
|
|
190
|
+
outputIndex: output.outputIndex,
|
|
191
|
+
topic
|
|
192
|
+
})
|
|
193
|
+
} else if (l.spendNotificationMode === 'script') {
|
|
194
|
+
const inputIndex = tx.inputs.findIndex(i => {
|
|
195
|
+
let realSource = i.sourceTXID
|
|
196
|
+
if (!realSource) {
|
|
197
|
+
realSource = i.sourceTransaction?.id('hex')
|
|
198
|
+
}
|
|
199
|
+
return realSource === output.txid && i.sourceOutputIndex === output.outputIndex
|
|
200
|
+
})
|
|
201
|
+
if (!inputIndex) {
|
|
202
|
+
throw new Error('Could not find input index')
|
|
203
|
+
}
|
|
204
|
+
await l.outputSpent({
|
|
205
|
+
mode: 'script',
|
|
206
|
+
spendingTxid: txid,
|
|
207
|
+
inputIndex,
|
|
208
|
+
sequenceNumber: tx.inputs[inputIndex].sequence ?? 0xffffffff,
|
|
209
|
+
unlockingScript: tx.inputs[inputIndex].unlockingScript!,
|
|
210
|
+
txid: output.txid,
|
|
211
|
+
outputIndex: output.outputIndex,
|
|
212
|
+
topic
|
|
213
|
+
})
|
|
214
|
+
} else if (l.spendNotificationMode === 'whole-tx') {
|
|
215
|
+
await l.outputSpent({
|
|
216
|
+
mode: 'whole-tx',
|
|
217
|
+
spendingAtomicBEEF: tx.toAtomicBEEF(),
|
|
218
|
+
txid: output.txid,
|
|
219
|
+
outputIndex: output.outputIndex,
|
|
220
|
+
topic
|
|
221
|
+
})
|
|
222
|
+
} else { // none
|
|
223
|
+
await l.outputSpent({
|
|
224
|
+
mode: 'none',
|
|
225
|
+
txid: output.txid,
|
|
226
|
+
outputIndex: output.outputIndex,
|
|
227
|
+
topic
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
185
231
|
} catch (error) {
|
|
186
232
|
this.logger.error('Error in lookup service for outputSpent:', error)
|
|
187
233
|
}
|
|
@@ -244,7 +290,7 @@ export class Engine {
|
|
|
244
290
|
|
|
245
291
|
const outputsToMarkStale: Array<{
|
|
246
292
|
txid: string
|
|
247
|
-
previousOutputIndex: number
|
|
293
|
+
previousOutputIndex: number
|
|
248
294
|
inputIndex: number
|
|
249
295
|
}> = []
|
|
250
296
|
|
|
@@ -262,12 +308,13 @@ export class Engine {
|
|
|
262
308
|
|
|
263
309
|
// For each of the previous UTXOs for this topic, if the UTXO was not included in the list of UTXOs identified for retention, then it will be marked as stale.
|
|
264
310
|
for (const inputIndex of previousCoins) {
|
|
265
|
-
const previousTXID = tx.inputs[inputIndex].sourceTXID
|
|
311
|
+
const previousTXID = tx.inputs[inputIndex].sourceTXID ?? tx.inputs[inputIndex].sourceTransaction?.id('hex')
|
|
312
|
+
if (typeof previousTXID !== 'string') continue
|
|
266
313
|
const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
|
|
267
314
|
if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
|
|
268
315
|
outputsToMarkStale.push({
|
|
269
316
|
txid: previousTXID,
|
|
270
|
-
previousOutputIndex
|
|
317
|
+
previousOutputIndex,
|
|
271
318
|
inputIndex
|
|
272
319
|
})
|
|
273
320
|
} else {
|
|
@@ -294,12 +341,13 @@ export class Engine {
|
|
|
294
341
|
// Handle admittance and notification of incoming UTXOs
|
|
295
342
|
const newUTXOs: Array<{ txid: string, outputIndex: number }> = []
|
|
296
343
|
await Promise.all(outputsToAdmit.map(async outputIndex => {
|
|
344
|
+
if (typeof tx.outputs[outputIndex].satoshis !== 'number') return
|
|
297
345
|
this.startTime(`insertNewOutput_${txid.substring(0, 10)}`)
|
|
298
346
|
await this.storage.insertOutput({
|
|
299
347
|
txid,
|
|
300
348
|
outputIndex,
|
|
301
349
|
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
|
|
302
|
-
satoshis: tx.outputs[outputIndex].satoshis
|
|
350
|
+
satoshis: tx.outputs[outputIndex].satoshis,
|
|
303
351
|
topic,
|
|
304
352
|
spent: false,
|
|
305
353
|
beef: taggedBEEF.beef,
|
|
@@ -310,7 +358,31 @@ export class Engine {
|
|
|
310
358
|
newUTXOs.push({ txid, outputIndex })
|
|
311
359
|
|
|
312
360
|
this.startTime(`notifyLookupService${txid.substring(0, 10)}`)
|
|
313
|
-
await Promise.all(Object.values(this.lookupServices).map(async l =>
|
|
361
|
+
await Promise.all(Object.values(this.lookupServices).map(async l => {
|
|
362
|
+
if (l.admissionMode === 'locking-script') {
|
|
363
|
+
if (
|
|
364
|
+
typeof tx.outputs[outputIndex].lockingScript !== 'object' ||
|
|
365
|
+
typeof tx.outputs[outputIndex].satoshis !== 'number'
|
|
366
|
+
) {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
await l.outputAdmittedByTopic({
|
|
370
|
+
mode: 'locking-script',
|
|
371
|
+
txid,
|
|
372
|
+
outputIndex,
|
|
373
|
+
lockingScript: tx.outputs[outputIndex].lockingScript,
|
|
374
|
+
satoshis: tx.outputs[outputIndex].satoshis,
|
|
375
|
+
topic
|
|
376
|
+
})
|
|
377
|
+
} else {
|
|
378
|
+
await l.outputAdmittedByTopic({
|
|
379
|
+
mode: 'whole-tx',
|
|
380
|
+
atomicBEEF: tx.toAtomicBEEF(),
|
|
381
|
+
outputIndex,
|
|
382
|
+
topic
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
}))
|
|
314
386
|
this.endTime(`notifyLookupService${txid.substring(0, 10)}`)
|
|
315
387
|
}))
|
|
316
388
|
|
|
@@ -376,7 +448,7 @@ export class Engine {
|
|
|
376
448
|
* @param LookupQuestion — The question to ask the Overlay Services Engine
|
|
377
449
|
* @returns The answer to the question
|
|
378
450
|
*/
|
|
379
|
-
async lookup(lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
|
|
451
|
+
async lookup (lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
|
|
380
452
|
// Validate a lookup service for the provider is found
|
|
381
453
|
const lookupService = this.lookupServices[lookupQuestion.service]
|
|
382
454
|
if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service} `)
|
|
@@ -417,7 +489,7 @@ export class Engine {
|
|
|
417
489
|
}
|
|
418
490
|
|
|
419
491
|
/**
|
|
420
|
-
* Ensures alignment between the current SHIP/SLAP advertisements and the
|
|
492
|
+
* Ensures alignment between the current SHIP/SLAP advertisements and the
|
|
421
493
|
* configured Topic Managers and Lookup Services in the engine.
|
|
422
494
|
*
|
|
423
495
|
* This method performs the following actions:
|
|
@@ -435,7 +507,7 @@ export class Engine {
|
|
|
435
507
|
* @throws Will throw an error if there are issues during the advertisement synchronization process.
|
|
436
508
|
* @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
|
|
437
509
|
*/
|
|
438
|
-
async syncAdvertisements(): Promise<void> {
|
|
510
|
+
async syncAdvertisements (): Promise<void> {
|
|
439
511
|
if (
|
|
440
512
|
this.advertiser === undefined ||
|
|
441
513
|
typeof this.hostingURL !== 'string' ||
|
|
@@ -501,7 +573,7 @@ export class Engine {
|
|
|
501
573
|
*
|
|
502
574
|
* @throws Error if the overlay service engine is not configured for topical synchronization.
|
|
503
575
|
*/
|
|
504
|
-
async startGASPSync(): Promise<void> {
|
|
576
|
+
async startGASPSync (): Promise<void> {
|
|
505
577
|
if (this.syncConfiguration === undefined) {
|
|
506
578
|
throw new Error('Overlay Service Engine not configured for topical synchronization!')
|
|
507
579
|
}
|
|
@@ -595,7 +667,7 @@ export class Engine {
|
|
|
595
667
|
* @param topic - The topic for which UTXOs are being requested.
|
|
596
668
|
* @returns A promise that resolves to a GASPInitialResponse containing the list of UTXOs and the provided min block height.
|
|
597
669
|
*/
|
|
598
|
-
async provideForeignSyncResponse(initialRequest: GASPInitialRequest, topic: string): Promise<GASPInitialResponse> {
|
|
670
|
+
async provideForeignSyncResponse (initialRequest: GASPInitialRequest, topic: string): Promise<GASPInitialResponse> {
|
|
599
671
|
const UTXOs = await this.storage.findUTXOsForTopic(topic, initialRequest.since)
|
|
600
672
|
|
|
601
673
|
return {
|
|
@@ -618,7 +690,7 @@ export class Engine {
|
|
|
618
690
|
* @returns A promise that resolves to a GASPNode containing the raw transaction and other optional data.
|
|
619
691
|
* @throws An error if no output is found for the given transaction ID and output index.
|
|
620
692
|
*/
|
|
621
|
-
async provideForeignGASPNode(graphID: string, txid: string, outputIndex: number): Promise<GASPNode> {
|
|
693
|
+
async provideForeignGASPNode (graphID: string, txid: string, outputIndex: number): Promise<GASPNode> {
|
|
622
694
|
const hydrator = async (output: Output | null): Promise<GASPNode> => {
|
|
623
695
|
if (output?.beef === undefined) {
|
|
624
696
|
throw new Error('No matching output found!')
|
|
@@ -696,7 +768,7 @@ export class Engine {
|
|
|
696
768
|
*
|
|
697
769
|
* @returns {Promise<Output | undefined>} - A promise that resolves to the output history if found, or undefined if not.
|
|
698
770
|
*/
|
|
699
|
-
async getUTXOHistory(
|
|
771
|
+
async getUTXOHistory (
|
|
700
772
|
output: Output,
|
|
701
773
|
historySelector?: ((beef: number[], outputIndex: number, currentDepth: number) => Promise<boolean>) | number,
|
|
702
774
|
currentDepth = 0
|
|
@@ -777,7 +849,7 @@ export class Engine {
|
|
|
777
849
|
* @param output - The UTXO to be deleted.
|
|
778
850
|
* @returns {Promise<void>} - A promise that resolves when the deletion process is complete.
|
|
779
851
|
*/
|
|
780
|
-
private async deleteUTXODeep(output: Output): Promise<void> {
|
|
852
|
+
private async deleteUTXODeep (output: Output): Promise<void> {
|
|
781
853
|
try {
|
|
782
854
|
// Delete the current output IFF there are no references to it
|
|
783
855
|
if (output.consumedBy.length === 0) {
|
|
@@ -786,7 +858,7 @@ export class Engine {
|
|
|
786
858
|
// Notify the lookup services of the UTXO being deleted
|
|
787
859
|
for (const l of Object.values(this.lookupServices)) {
|
|
788
860
|
try {
|
|
789
|
-
await l.
|
|
861
|
+
await l.outputNoLongerRetainedInHistory?.(
|
|
790
862
|
output.txid,
|
|
791
863
|
output.outputIndex,
|
|
792
864
|
output.topic
|
|
@@ -833,7 +905,7 @@ export class Engine {
|
|
|
833
905
|
* @param txid BE hex string double hash of transaction proven by proof.
|
|
834
906
|
* @param proof for txid
|
|
835
907
|
*/
|
|
836
|
-
private updateInputProofs(tx: Transaction, txid: string, proof: MerklePath): void {
|
|
908
|
+
private updateInputProofs (tx: Transaction, txid: string, proof: MerklePath): void {
|
|
837
909
|
if (tx.merklePath !== undefined) {
|
|
838
910
|
// Update the merkle path to handle potential reorgs
|
|
839
911
|
tx.merklePath = proof
|
|
@@ -844,7 +916,8 @@ export class Engine {
|
|
|
844
916
|
} else {
|
|
845
917
|
for (const input of tx.inputs) {
|
|
846
918
|
// All inputs must have sourceTransactions
|
|
847
|
-
const stx = input.sourceTransaction
|
|
919
|
+
const stx = input.sourceTransaction
|
|
920
|
+
if (typeof stx !== 'object') continue
|
|
848
921
|
this.updateInputProofs(stx, txid, proof)
|
|
849
922
|
}
|
|
850
923
|
}
|
|
@@ -857,7 +930,7 @@ export class Engine {
|
|
|
857
930
|
* @param txid - The txid for which proof is a valid merkle path.
|
|
858
931
|
* @param proof - The merklePath proving txid is a mined transaction hash
|
|
859
932
|
*/
|
|
860
|
-
private async updateMerkleProof(output: Output, txid: string, proof: MerklePath): Promise<void> {
|
|
933
|
+
private async updateMerkleProof (output: Output, txid: string, proof: MerklePath): Promise<void> {
|
|
861
934
|
if (output.beef === undefined) {
|
|
862
935
|
throw new Error('Output must have associated transaction BEEF!')
|
|
863
936
|
}
|
|
@@ -891,7 +964,7 @@ export class Engine {
|
|
|
891
964
|
* @param proof - Merkle proof containing the Merkle path and other relevant data to verify the transaction.
|
|
892
965
|
* @param blockHeight - The block height associated with the incoming merkle proof.
|
|
893
966
|
*/
|
|
894
|
-
async handleNewMerkleProof(txid: string, proof: MerklePath, blockHeight?: number): Promise<void> {
|
|
967
|
+
async handleNewMerkleProof (txid: string, proof: MerklePath, blockHeight?: number): Promise<void> {
|
|
895
968
|
const outputs = await this.storage.findOutputsForTransaction(txid, true)
|
|
896
969
|
|
|
897
970
|
if (outputs === undefined || outputs.length === 0) {
|
|
@@ -914,21 +987,21 @@ export class Engine {
|
|
|
914
987
|
* @public
|
|
915
988
|
* @returns {Promise<Record<string, { name: string; shortDescription: string; iconURL?: string; version?: string; informationURL?: string; }>>} - Supported topic managers and their metadata
|
|
916
989
|
*/
|
|
917
|
-
async listTopicManagers(): Promise<Record<string, {
|
|
918
|
-
name: string
|
|
919
|
-
shortDescription: string
|
|
920
|
-
iconURL?: string
|
|
921
|
-
version?: string
|
|
922
|
-
informationURL?: string
|
|
990
|
+
async listTopicManagers (): Promise<Record<string, {
|
|
991
|
+
name: string
|
|
992
|
+
shortDescription: string
|
|
993
|
+
iconURL?: string
|
|
994
|
+
version?: string
|
|
995
|
+
informationURL?: string
|
|
923
996
|
}>> {
|
|
924
|
-
|
|
925
|
-
name: string
|
|
926
|
-
shortDescription: string
|
|
927
|
-
iconURL?: string
|
|
928
|
-
version?: string
|
|
929
|
-
informationURL?: string
|
|
997
|
+
const result: Record<string, {
|
|
998
|
+
name: string
|
|
999
|
+
shortDescription: string
|
|
1000
|
+
iconURL?: string
|
|
1001
|
+
version?: string
|
|
1002
|
+
informationURL?: string
|
|
930
1003
|
}> = {}
|
|
931
|
-
for (
|
|
1004
|
+
for (const t in this.managers) {
|
|
932
1005
|
try {
|
|
933
1006
|
result[t] = await this.managers[t].getMetaData()
|
|
934
1007
|
} catch (e) {
|
|
@@ -947,21 +1020,21 @@ export class Engine {
|
|
|
947
1020
|
* @public
|
|
948
1021
|
* @returns {Promise<Record<string, { name: string; shortDescription: string; iconURL?: string; version?: string; informationURL?: string; }>>} - Supported lookup services and their metadata
|
|
949
1022
|
*/
|
|
950
|
-
async listLookupServiceProviders(): Promise<Record<string, {
|
|
951
|
-
name: string
|
|
952
|
-
shortDescription: string
|
|
953
|
-
iconURL?: string
|
|
954
|
-
version?: string
|
|
955
|
-
informationURL?: string
|
|
1023
|
+
async listLookupServiceProviders (): Promise<Record<string, {
|
|
1024
|
+
name: string
|
|
1025
|
+
shortDescription: string
|
|
1026
|
+
iconURL?: string
|
|
1027
|
+
version?: string
|
|
1028
|
+
informationURL?: string
|
|
956
1029
|
}>> {
|
|
957
|
-
|
|
958
|
-
name: string
|
|
959
|
-
shortDescription: string
|
|
960
|
-
iconURL?: string
|
|
961
|
-
version?: string
|
|
962
|
-
informationURL?: string
|
|
1030
|
+
const result: Record<string, {
|
|
1031
|
+
name: string
|
|
1032
|
+
shortDescription: string
|
|
1033
|
+
iconURL?: string
|
|
1034
|
+
version?: string
|
|
1035
|
+
informationURL?: string
|
|
963
1036
|
}> = {}
|
|
964
|
-
for (
|
|
1037
|
+
for (const ls in this.lookupServices) {
|
|
965
1038
|
try {
|
|
966
1039
|
result[ls] = await this.lookupServices[ls].getMetaData()
|
|
967
1040
|
} catch (e) {
|
|
@@ -980,7 +1053,7 @@ export class Engine {
|
|
|
980
1053
|
* @public
|
|
981
1054
|
* @returns {Promise<string>} - the documentation for the topic manager
|
|
982
1055
|
*/
|
|
983
|
-
async getDocumentationForTopicManager(manager: any): Promise<string> {
|
|
1056
|
+
async getDocumentationForTopicManager (manager: any): Promise<string> {
|
|
984
1057
|
const documentation = await this.managers[manager]?.getDocumentation?.()
|
|
985
1058
|
return documentation !== undefined ? documentation : 'No documentation found!'
|
|
986
1059
|
}
|
|
@@ -990,7 +1063,7 @@ export class Engine {
|
|
|
990
1063
|
* @public
|
|
991
1064
|
* @returns {Promise<string>} - the documentation for the lookup service
|
|
992
1065
|
*/
|
|
993
|
-
async getDocumentationForLookupServiceProvider(provider: any): Promise<string> {
|
|
1066
|
+
async getDocumentationForLookupServiceProvider (provider: any): Promise<string> {
|
|
994
1067
|
const documentation = await this.lookupServices[provider]?.getDocumentation?.()
|
|
995
1068
|
return documentation !== undefined ? documentation : 'No documentation found!'
|
|
996
1069
|
}
|
|
@@ -1005,12 +1078,12 @@ export class Engine {
|
|
|
1005
1078
|
* @param url - The URL string to validate
|
|
1006
1079
|
* @returns {boolean} - Returns `false` if the URL violates any of the conditions `true` otherwise
|
|
1007
1080
|
*/
|
|
1008
|
-
private isValidUrl(url: string): boolean {
|
|
1081
|
+
private isValidUrl (url: string): boolean {
|
|
1009
1082
|
try {
|
|
1010
1083
|
const parsedUrl = new URL(url)
|
|
1011
1084
|
|
|
1012
1085
|
// Disallow http:
|
|
1013
|
-
if (parsedUrl.protocol ===
|
|
1086
|
+
if (parsedUrl.protocol === 'http:') {
|
|
1014
1087
|
return false
|
|
1015
1088
|
}
|
|
1016
1089
|
|
|
@@ -1026,9 +1099,9 @@ export class Engine {
|
|
|
1026
1099
|
const nonRoutableIpv4Patterns = [
|
|
1027
1100
|
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Loopback IPs
|
|
1028
1101
|
/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 10.x.x.x private IPs
|
|
1029
|
-
/^192\.168\.\d{1,3}\.\d{1,3}$/,
|
|
1102
|
+
/^192\.168\.\d{1,3}\.\d{1,3}$/, // 192.168.x.x private IPs
|
|
1030
1103
|
/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.x.x to 172.31.x.x private IPs
|
|
1031
|
-
/^0\.0\.0\.0$/
|
|
1104
|
+
/^0\.0\.0\.0$/ // Non-routable address
|
|
1032
1105
|
]
|
|
1033
1106
|
|
|
1034
1107
|
// Check for IPv4 matches
|
|
@@ -1037,7 +1110,7 @@ export class Engine {
|
|
|
1037
1110
|
}
|
|
1038
1111
|
|
|
1039
1112
|
// Check for non-routable IPv6 addresses explicitly
|
|
1040
|
-
if (ipAddress ===
|
|
1113
|
+
if (ipAddress === '[::1]') {
|
|
1041
1114
|
return false
|
|
1042
1115
|
}
|
|
1043
1116
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { GASPInitialReply, GASPInitialRequest, GASPInitialResponse, GASPNode, GASPNodeResponse, GASPRemote } from
|
|
1
|
+
import { GASPInitialReply, GASPInitialRequest, GASPInitialResponse, GASPNode, GASPNodeResponse, GASPRemote } from '@bsv/gasp'
|
|
2
2
|
|
|
3
3
|
export class OverlayGASPRemote implements GASPRemote {
|
|
4
|
-
constructor(public endpointURL: string, public topic: string) { }
|
|
4
|
+
constructor (public endpointURL: string, public topic: string) { }
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
|
|
8
8
|
* @param request
|
|
9
9
|
* @returns
|
|
10
10
|
*/
|
|
11
|
-
async getInitialResponse(request: GASPInitialRequest): Promise<GASPInitialResponse> {
|
|
11
|
+
async getInitialResponse (request: GASPInitialRequest): Promise<GASPInitialResponse> {
|
|
12
12
|
// Send out an HTTP request to the URL (current host for topic)
|
|
13
13
|
// Include the topic in the request
|
|
14
14
|
// Parse out response and return correct format
|
|
@@ -50,7 +50,7 @@ export class OverlayGASPRemote implements GASPRemote {
|
|
|
50
50
|
* @param metadata
|
|
51
51
|
* @returns
|
|
52
52
|
*/
|
|
53
|
-
async requestNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
|
|
53
|
+
async requestNode (graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
|
|
54
54
|
// Send an HTTP request with the provided info and get back a gaspNode
|
|
55
55
|
const url = `${this.endpointURL}/requestForeignGASPNode`
|
|
56
56
|
const body = {
|
|
@@ -95,13 +95,13 @@ export class OverlayGASPRemote implements GASPRemote {
|
|
|
95
95
|
// ---- Now optional methods ----
|
|
96
96
|
|
|
97
97
|
// When are only syncing to them
|
|
98
|
-
async getInitialReply(response: GASPInitialResponse): Promise<GASPInitialReply> {
|
|
98
|
+
async getInitialReply (response: GASPInitialResponse): Promise<GASPInitialReply> {
|
|
99
99
|
throw new Error('Function not supported!')
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// Only used when supporting bidirectional sync.
|
|
103
103
|
// Overlay services does not support this.
|
|
104
|
-
async submitNode(node: GASPNode): Promise<void | GASPNodeResponse> {
|
|
104
|
+
async submitNode (node: GASPNode): Promise<void | GASPNodeResponse> {
|
|
105
105
|
throw new Error('Node submission not supported!')
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -22,14 +22,14 @@ export interface GraphNode {
|
|
|
22
22
|
export class OverlayGASPStorage implements GASPStorage {
|
|
23
23
|
readonly temporaryGraphNodeRefs: Record<string, GraphNode> = {}
|
|
24
24
|
|
|
25
|
-
constructor(public topic: string, public engine: Engine, public maxNodesInGraph?: number) { }
|
|
25
|
+
constructor (public topic: string, public engine: Engine, public maxNodesInGraph?: number) { }
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
*
|
|
29
29
|
* @param since
|
|
30
30
|
* @returns
|
|
31
31
|
*/
|
|
32
|
-
async findKnownUTXOs(since: number): Promise<Array<{ txid: string, outputIndex: number }>> {
|
|
32
|
+
async findKnownUTXOs (since: number): Promise<Array<{ txid: string, outputIndex: number }>> {
|
|
33
33
|
const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since)
|
|
34
34
|
return UTXOs.map(output => ({
|
|
35
35
|
txid: output.txid,
|
|
@@ -39,13 +39,13 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* For a given txid and output index, returns the associated transaction, a merkle proof if the transaction is in a block, and metadata if if requested. If no metadata is requested, metadata hashes on inputs are not returned.
|
|
42
|
-
* @param graphID
|
|
43
|
-
* @param txid
|
|
44
|
-
* @param outputIndex
|
|
45
|
-
* @param metadata
|
|
46
|
-
* @returns
|
|
42
|
+
* @param graphID
|
|
43
|
+
* @param txid
|
|
44
|
+
* @param outputIndex
|
|
45
|
+
* @param metadata
|
|
46
|
+
* @returns
|
|
47
47
|
*/
|
|
48
|
-
async hydrateGASPNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
|
|
48
|
+
async hydrateGASPNode (graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
|
|
49
49
|
const output = await this.engine.storage.findOutput(txid, outputIndex, undefined, undefined, true)
|
|
50
50
|
|
|
51
51
|
if (output?.beef === undefined) {
|
|
@@ -72,7 +72,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
72
72
|
* @param tx The node for which needed inputs should be found.
|
|
73
73
|
* @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
|
|
74
74
|
*/
|
|
75
|
-
async findNeededInputs(tx: GASPNode): Promise<GASPNodeResponse | undefined> {
|
|
75
|
+
async findNeededInputs (tx: GASPNode): Promise<GASPNodeResponse | undefined> {
|
|
76
76
|
// If there is no Merkle proof, we always need the inputs
|
|
77
77
|
const response: GASPNodeResponse = {
|
|
78
78
|
requestedInputs: {}
|
|
@@ -120,7 +120,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
120
120
|
* Ensures that no inputs are requested from foreign nodes before sending any GASP response
|
|
121
121
|
* Also terminates graphs if the response would be empty.
|
|
122
122
|
*/
|
|
123
|
-
private async stripAlreadyKnownInputs(response: GASPNodeResponse | undefined): Promise<GASPNodeResponse | undefined> {
|
|
123
|
+
private async stripAlreadyKnownInputs (response: GASPNodeResponse | undefined): Promise<GASPNodeResponse | undefined> {
|
|
124
124
|
if (typeof response === 'undefined') {
|
|
125
125
|
return response
|
|
126
126
|
}
|
|
@@ -144,7 +144,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
144
144
|
* @param spentBy Unless this is the same node identified by the graph ID, denotes the TXID and input index for the node which spent this one, in 36-byte format.
|
|
145
145
|
* @throws If the node cannot be appended to the graph, either because the graph ID is for a graph the recipient does not want or because the graph has grown to be too large before being finalized.
|
|
146
146
|
*/
|
|
147
|
-
async appendToGraph(tx: GASPNode, spentBy?: string | undefined): Promise<void> {
|
|
147
|
+
async appendToGraph (tx: GASPNode, spentBy?: string | undefined): Promise<void> {
|
|
148
148
|
if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
|
|
149
149
|
throw new Error('The max number of nodes in transaction graph has been reached!')
|
|
150
150
|
}
|
|
@@ -194,7 +194,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
194
194
|
* @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
|
|
195
195
|
* @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
|
|
196
196
|
*/
|
|
197
|
-
async validateGraphAnchor(graphID: string): Promise<void> {
|
|
197
|
+
async validateGraphAnchor (graphID: string): Promise<void> {
|
|
198
198
|
const rootNode = this.temporaryGraphNodeRefs[graphID]
|
|
199
199
|
if (rootNode === undefined) {
|
|
200
200
|
throw new Error(`Graph node with ID ${graphID} not found`)
|
|
@@ -246,7 +246,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
246
246
|
* Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
|
|
247
247
|
* @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
|
|
248
248
|
*/
|
|
249
|
-
async discardGraph(graphID: string): Promise<void> {
|
|
249
|
+
async discardGraph (graphID: string): Promise<void> {
|
|
250
250
|
for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
|
|
251
251
|
if (graphRef.graphID === graphID) {
|
|
252
252
|
// Delete child node
|
|
@@ -260,7 +260,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
260
260
|
* Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
|
|
261
261
|
* @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
|
|
262
262
|
*/
|
|
263
|
-
async finalizeGraph(graphID: string): Promise<void> {
|
|
263
|
+
async finalizeGraph (graphID: string): Promise<void> {
|
|
264
264
|
const beefs = this.computeOrderedBEEFsForGraph(graphID)
|
|
265
265
|
|
|
266
266
|
// Submit all historical BEEFs in order, finalizing the graph for the current UTXO
|
|
@@ -277,11 +277,11 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
277
277
|
* @param {string} graphID — The ID of the graph for which BEEFs are required
|
|
278
278
|
* @returns Ordered BEEFs for the graph
|
|
279
279
|
*/
|
|
280
|
-
private computeOrderedBEEFsForGraph(graphID: string): number[][] {
|
|
280
|
+
private computeOrderedBEEFsForGraph (graphID: string): number[][] {
|
|
281
281
|
const beefs: number[][] = []
|
|
282
282
|
const hydrator = (node: GraphNode): void => {
|
|
283
283
|
const currentBEEF = this.getBEEFForNode(node)
|
|
284
|
-
if (beefs.
|
|
284
|
+
if (!beefs.includes(currentBEEF)) {
|
|
285
285
|
beefs.unshift(currentBEEF)
|
|
286
286
|
}
|
|
287
287
|
|
|
@@ -305,7 +305,7 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
305
305
|
* @param node Graph node for which BEEF is needed.
|
|
306
306
|
* @returns BEEF array, including all proofs on inputs.
|
|
307
307
|
*/
|
|
308
|
-
private getBEEFForNode(node: GraphNode): number[] {
|
|
308
|
+
private getBEEFForNode (node: GraphNode): number[] {
|
|
309
309
|
// Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
|
|
310
310
|
const hydrator = (node: GraphNode): Transaction => {
|
|
311
311
|
const tx = Transaction.fromHex(node.rawTx)
|