@bsv/overlay 0.1.4 → 0.1.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.
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Advertisement.js +3 -0
- package/dist/cjs/src/Advertisement.js.map +1 -0
- package/dist/cjs/src/Engine.js +108 -64
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP.js +20 -15
- package/dist/cjs/src/GASP.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +11 -5
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js +18 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/Advertisement.js +2 -0
- package/dist/esm/src/Advertisement.js.map +1 -0
- package/dist/esm/src/Engine.js +107 -64
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP.js +20 -15
- package/dist/esm/src/GASP.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +11 -5
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-07-10-001-block-height.js +13 -0
- package/dist/esm/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/Advertisement.d.ts +9 -0
- package/dist/types/src/Advertisement.d.ts.map +1 -0
- package/dist/types/src/Advertiser.d.ts +16 -24
- package/dist/types/src/Advertiser.d.ts.map +1 -1
- package/dist/types/src/Engine.d.ts +6 -8
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP.d.ts +1 -0
- package/dist/types/src/GASP.d.ts.map +1 -1
- package/dist/types/src/Output.d.ts +1 -0
- package/dist/types/src/Output.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +8 -0
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +1 -0
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/mod.ts +2 -0
- package/package.json +2 -2
- package/src/Advertisement.ts +8 -0
- package/src/Advertiser.ts +17 -25
- package/src/Engine.ts +112 -75
- package/src/Output.ts +1 -0
- package/src/storage/Storage.ts +10 -0
- package/src/storage/knex/KnexStorage.ts +12 -5
- package/src/storage/knex/migrations/2024-07-10-001-block-height.ts +15 -0
- package/src/GASP.ts +0 -420
- package/src/SHIPAdvertisement.ts +0 -8
- package/src/SLAPAdvertisement.ts +0 -8
package/src/Engine.ts
CHANGED
|
@@ -9,10 +9,10 @@ import { LookupQuestion } from './LookupQuestion.js'
|
|
|
9
9
|
import { LookupAnswer } from './LookupAnswer.js'
|
|
10
10
|
import { LookupFormula } from './LookupFormula.js'
|
|
11
11
|
import { Transaction, ChainTracker, MerklePath, Broadcaster, isBroadcastFailure } from '@bsv/sdk'
|
|
12
|
-
import { Advertiser } from './Advertiser.js'
|
|
13
|
-
import { SHIPAdvertisement } from './SHIPAdvertisement.js'
|
|
12
|
+
import { AdvertisementData, Advertiser } from './Advertiser.js'
|
|
14
13
|
import { GASP, GASPInitialReply, GASPInitialRequest, GASPInitialResponse, GASPNode, GASPNodeResponse, GASPRemote, GASPStorage } from '@bsv/gasp'
|
|
15
14
|
import { SyncConfiguration } from './SyncConfiguration.js'
|
|
15
|
+
import { Advertisement } from './Advertisement.js'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Am engine for running BSV Overlay Services (topic managers and lookup services).
|
|
@@ -272,7 +272,7 @@ export class Engine {
|
|
|
272
272
|
|
|
273
273
|
// Lookup will currently always return type output-list
|
|
274
274
|
if (lookupAnswer.type === 'output-list') {
|
|
275
|
-
const shipAdvertisements:
|
|
275
|
+
const shipAdvertisements: Advertisement[] = []
|
|
276
276
|
lookupAnswer.outputs.forEach(output => {
|
|
277
277
|
try {
|
|
278
278
|
// Parse out the advertisements using the provided parser
|
|
@@ -286,7 +286,7 @@ export class Engine {
|
|
|
286
286
|
}
|
|
287
287
|
})
|
|
288
288
|
if (shipAdvertisements.length > 0) {
|
|
289
|
-
shipAdvertisements.forEach((advertisement:
|
|
289
|
+
shipAdvertisements.forEach((advertisement: Advertisement) => {
|
|
290
290
|
if (!domainToTopicsMap.has(advertisement.domain)) {
|
|
291
291
|
domainToTopicsMap.set(advertisement.domain, new Set<string>())
|
|
292
292
|
}
|
|
@@ -423,57 +423,49 @@ export class Engine {
|
|
|
423
423
|
const configuredServices = Object.keys(this.lookupServices)
|
|
424
424
|
|
|
425
425
|
// Step 2: Fetch Existing Advertisements
|
|
426
|
-
const currentSHIPAdvertisements = await advertiser.
|
|
427
|
-
const currentSLAPAdvertisements = await advertiser.
|
|
426
|
+
const currentSHIPAdvertisements = await advertiser.findAllAdvertisements('SHIP')
|
|
427
|
+
const currentSLAPAdvertisements = await advertiser.findAllAdvertisements('SLAP')
|
|
428
428
|
|
|
429
429
|
// Step 3: Compare and Determine Actions
|
|
430
430
|
const requiredSHIPAdvertisements = new Set(configuredTopics)
|
|
431
431
|
const requiredSLAPAdvertisements = new Set(configuredServices)
|
|
432
432
|
|
|
433
|
-
const
|
|
434
|
-
const
|
|
433
|
+
const existingTopics = new Set(currentSHIPAdvertisements.map(ad => ad.topicOrService))
|
|
434
|
+
const existingServices = new Set(currentSLAPAdvertisements.map(ad => ad.topicOrService))
|
|
435
435
|
|
|
436
|
-
const
|
|
437
|
-
const
|
|
436
|
+
const shipsToCreate = Array.from(requiredSHIPAdvertisements).filter(topicOrService => !existingTopics.has(topicOrService))
|
|
437
|
+
const slapsToCreate = Array.from(requiredSLAPAdvertisements).filter(topicOrService => !existingServices.has(topicOrService))
|
|
438
|
+
const shipsToRevoke = currentSHIPAdvertisements.filter(ad => !requiredSHIPAdvertisements.has(ad.topicOrService))
|
|
439
|
+
const slapsToRevoke = currentSLAPAdvertisements.filter(ad => !requiredSLAPAdvertisements.has(ad.topicOrService))
|
|
438
440
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
const taggedBEEF = await advertiser.createSLAPAdvertisement(service)
|
|
455
|
-
await this.submit(taggedBEEF)
|
|
456
|
-
} catch (error) {
|
|
457
|
-
console.error('Failed to create SLAP advertisement:', error)
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
for (const ad of shipToRevoke) {
|
|
462
|
-
try {
|
|
463
|
-
const taggedBEEF = await advertiser.revokeAdvertisement(ad)
|
|
441
|
+
// Create needed SHIP/SLAP advertisements
|
|
442
|
+
try {
|
|
443
|
+
if (shipsToCreate.length > 0 || slapsToCreate.length > 0) {
|
|
444
|
+
const advertisementData: AdvertisementData[] = [
|
|
445
|
+
...shipsToCreate.map(topic => ({
|
|
446
|
+
protocol: 'SHIP' as 'SHIP',
|
|
447
|
+
topicOrServiceName: topic
|
|
448
|
+
})),
|
|
449
|
+
...slapsToCreate.map(service => ({
|
|
450
|
+
protocol: 'SLAP' as 'SLAP',
|
|
451
|
+
topicOrServiceName: service
|
|
452
|
+
}))
|
|
453
|
+
]
|
|
454
|
+
const taggedBEEF = await advertiser.createAdvertisements(advertisementData)
|
|
464
455
|
await this.submit(taggedBEEF)
|
|
465
|
-
} catch (error) {
|
|
466
|
-
console.error('Failed to revoke SHIP advertisement:', error)
|
|
467
456
|
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('Failed to create SHIP advertisement:', error)
|
|
468
459
|
}
|
|
469
460
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
461
|
+
// Revoke all advertisements to revoke
|
|
462
|
+
try {
|
|
463
|
+
if (shipsToRevoke.length > 0 || slapsToRevoke.length > 0) {
|
|
464
|
+
const taggedBEEF = await advertiser.revokeAdvertisements([...shipsToRevoke, ...slapsToRevoke])
|
|
473
465
|
await this.submit(taggedBEEF)
|
|
474
|
-
} catch (error) {
|
|
475
|
-
console.error('Failed to revoke SLAP advertisement:', error)
|
|
476
466
|
}
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error('Failed to revoke SHIP/SLAP advertisements:', error)
|
|
477
469
|
}
|
|
478
470
|
}
|
|
479
471
|
|
|
@@ -539,12 +531,12 @@ export class Engine {
|
|
|
539
531
|
* Given a GASP request, create an initial response.
|
|
540
532
|
*
|
|
541
533
|
* This method processes an initial synchronization request by finding the relevant UTXOs for the given topic
|
|
542
|
-
* since the provided
|
|
543
|
-
* and the
|
|
534
|
+
* since the provided block height in the request. It constructs a response that includes a list of these UTXOs
|
|
535
|
+
* and the min block height from the initial request.
|
|
544
536
|
*
|
|
545
|
-
* @param initialRequest - The GASP initial request containing the version and the
|
|
537
|
+
* @param initialRequest - The GASP initial request containing the version and the block height since the last sync.
|
|
546
538
|
* @param topic - The topic for which UTXOs are being requested.
|
|
547
|
-
* @returns A promise that resolves to a GASPInitialResponse containing the list of UTXOs and the provided
|
|
539
|
+
* @returns A promise that resolves to a GASPInitialResponse containing the list of UTXOs and the provided min block height.
|
|
548
540
|
*/
|
|
549
541
|
async provideForeignSyncResponse(initialRequest: GASPInitialRequest, topic: string): Promise<GASPInitialResponse> {
|
|
550
542
|
const UTXOs = await this.storage.findUTXOsForTopic(topic, initialRequest.since)
|
|
@@ -570,25 +562,66 @@ export class Engine {
|
|
|
570
562
|
* @throws An error if no output is found for the given transaction ID and output index.
|
|
571
563
|
*/
|
|
572
564
|
async provideForeignGASPNode(graphID: string, txid: string, outputIndex: number): Promise<GASPNode> {
|
|
573
|
-
const
|
|
565
|
+
const hydrator = async (output: Output | null): Promise<GASPNode> => {
|
|
566
|
+
if (output === undefined || output === null) {
|
|
567
|
+
throw new Error('No matching output found!')
|
|
568
|
+
}
|
|
574
569
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
570
|
+
const rootTx = Transaction.fromBEEF(output.beef)
|
|
571
|
+
let correctTx: Transaction | undefined
|
|
578
572
|
|
|
579
|
-
|
|
580
|
-
|
|
573
|
+
const searchInput = (tx: Transaction): void => {
|
|
574
|
+
if (tx.id('hex') === txid) {
|
|
575
|
+
correctTx = tx
|
|
576
|
+
} else {
|
|
577
|
+
// For each input, look it up and recurse.
|
|
578
|
+
for (const input of tx.inputs) {
|
|
579
|
+
// We should always have a source transaction
|
|
580
|
+
if (input.sourceTransaction !== undefined) {
|
|
581
|
+
searchInput(input.sourceTransaction)
|
|
582
|
+
} else {
|
|
583
|
+
throw new Error('Incomplete SPV data!')
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
581
588
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
+
searchInput(rootTx)
|
|
590
|
+
|
|
591
|
+
if (correctTx !== undefined) {
|
|
592
|
+
const rawTx = correctTx.toHex()
|
|
593
|
+
const node: GASPNode = {
|
|
594
|
+
rawTx,
|
|
595
|
+
graphID,
|
|
596
|
+
outputIndex
|
|
597
|
+
}
|
|
598
|
+
if (correctTx.merklePath !== undefined) {
|
|
599
|
+
node.proof = correctTx.merklePath.toHex()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return node
|
|
603
|
+
} else {
|
|
604
|
+
// Recursively try to find a matching output
|
|
605
|
+
let foundNode: GASPNode | undefined
|
|
606
|
+
for (const currentOutput of output.outputsConsumed) {
|
|
607
|
+
try {
|
|
608
|
+
const outputFound = await this.storage.findOutput(currentOutput.txid, currentOutput.outputIndex)
|
|
609
|
+
foundNode = await hydrator(outputFound)
|
|
610
|
+
break
|
|
611
|
+
} catch (error) {
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (foundNode !== undefined) {
|
|
616
|
+
return foundNode
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
throw new Error('Unable to find output associated with your request!')
|
|
589
620
|
}
|
|
590
621
|
|
|
591
|
-
|
|
622
|
+
const [rootTxid, rootOutputIndex] = graphID.split('.')
|
|
623
|
+
const output = await this.storage.findOutput(rootTxid, Number(rootOutputIndex))
|
|
624
|
+
return await hydrator(output)
|
|
592
625
|
}
|
|
593
626
|
|
|
594
627
|
/**
|
|
@@ -729,20 +762,19 @@ export class Engine {
|
|
|
729
762
|
|
|
730
763
|
/**
|
|
731
764
|
* Given a new transaction proof (txid, proof),
|
|
732
|
-
*
|
|
733
765
|
* update tx.merklePath if appropriate,
|
|
734
|
-
*
|
|
735
766
|
* and if not, recurse through all input sourceTransactions.
|
|
736
|
-
*
|
|
767
|
+
*
|
|
737
768
|
* @param tx transaction which may benefit from new proof.
|
|
738
769
|
* @param txid BE hex string double hash of transaction proven by proof.
|
|
739
770
|
* @param proof for txid
|
|
740
771
|
*/
|
|
741
|
-
private updateInputProofs(tx: Transaction, txid: string, proof: MerklePath) {
|
|
742
|
-
if (tx.merklePath)
|
|
743
|
-
//
|
|
772
|
+
private updateInputProofs(tx: Transaction, txid: string, proof: MerklePath): void {
|
|
773
|
+
if (tx.merklePath !== undefined) {
|
|
774
|
+
// Update the merkle path to handle potential reorgs
|
|
775
|
+
tx.merklePath = proof
|
|
744
776
|
return
|
|
745
|
-
|
|
777
|
+
}
|
|
746
778
|
if (tx.id('hex') === txid) {
|
|
747
779
|
tx.merklePath = proof
|
|
748
780
|
} else {
|
|
@@ -762,12 +794,12 @@ export class Engine {
|
|
|
762
794
|
* @param proof - The merklePath proving txid is a mined transaction hash
|
|
763
795
|
*/
|
|
764
796
|
private async updateMerkleProof(output: Output, txid: string, proof: MerklePath): Promise<void> {
|
|
765
|
-
|
|
766
797
|
const tx = Transaction.fromBEEF(output.beef)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
798
|
+
if (tx.merklePath !== undefined) {
|
|
799
|
+
// Update the merkle path to handle potential reorgs
|
|
800
|
+
tx.merklePath = proof
|
|
770
801
|
return
|
|
802
|
+
}
|
|
771
803
|
|
|
772
804
|
// recursively update all sourceTransactions proven by (txid,proof)
|
|
773
805
|
this.updateInputProofs(tx, txid, proof)
|
|
@@ -789,16 +821,23 @@ export class Engine {
|
|
|
789
821
|
*
|
|
790
822
|
* @param txid - Transaction ID of the associated outputs to prune.
|
|
791
823
|
* @param proof - Merkle proof containing the Merkle path and other relevant data to verify the transaction.
|
|
824
|
+
* @param blockHeight - The block height associated with the incoming merkle proof.
|
|
792
825
|
*/
|
|
793
|
-
async handleNewMerkleProof(txid: string, proof: MerklePath): Promise<void> {
|
|
826
|
+
async handleNewMerkleProof(txid: string, proof: MerklePath, blockHeight?: number): Promise<void> {
|
|
794
827
|
const outputs = await this.storage.findOutputsForTransaction(txid)
|
|
795
828
|
|
|
796
|
-
if (outputs
|
|
829
|
+
if (outputs === undefined || outputs.length === 0) {
|
|
797
830
|
throw new Error('Could not find matching transaction outputs for proof ingest!')
|
|
798
831
|
}
|
|
799
832
|
|
|
800
833
|
for (const output of outputs) {
|
|
801
834
|
await this.updateMerkleProof(output, txid, proof)
|
|
835
|
+
|
|
836
|
+
// Add the associated blockHeight
|
|
837
|
+
if (blockHeight !== undefined) {
|
|
838
|
+
output.blockHeight = blockHeight
|
|
839
|
+
await this.storage.updateOutputBlockHeight?.(output.txid, output.outputIndex, output.topic, blockHeight)
|
|
840
|
+
}
|
|
802
841
|
}
|
|
803
842
|
}
|
|
804
843
|
|
|
@@ -965,7 +1004,6 @@ export class OverlayGASPRemote implements GASPRemote {
|
|
|
965
1004
|
*/
|
|
966
1005
|
export interface GraphNode {
|
|
967
1006
|
txid: string
|
|
968
|
-
time: number
|
|
969
1007
|
graphID: string
|
|
970
1008
|
rawTx: string
|
|
971
1009
|
outputIndex: number
|
|
@@ -1117,7 +1155,6 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
1117
1155
|
// Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
|
|
1118
1156
|
const newGraphNode: GraphNode = {
|
|
1119
1157
|
txid,
|
|
1120
|
-
time: 0, // TODO: Determine required format for Time (either block height or timestamp, undefined / Infinity for unconfirmed transactions
|
|
1121
1158
|
graphID: tx.graphID,
|
|
1122
1159
|
rawTx: tx.rawTx,
|
|
1123
1160
|
outputIndex: tx.outputIndex,
|
package/src/Output.ts
CHANGED
package/src/storage/Storage.ts
CHANGED
|
@@ -80,6 +80,16 @@ export interface Storage {
|
|
|
80
80
|
*/
|
|
81
81
|
updateOutputBeef: (txid: string, outputIndex: number, topic: string, beef: number[]) => Promise<void>
|
|
82
82
|
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Updates the block height on an output
|
|
86
|
+
* @param txid — TXID of the output to update
|
|
87
|
+
* @param outputIndex — Index of the output to update
|
|
88
|
+
* @param topic— Topic in which the output should be updated
|
|
89
|
+
* @param blockHeight - height of the block the transaction associated with this output was included in
|
|
90
|
+
*/
|
|
91
|
+
updateOutputBlockHeight?: (txid: string, outputIndex: number, topic: string, blockHeight: number) => Promise<void>
|
|
92
|
+
|
|
83
93
|
/**
|
|
84
94
|
* Inserts record of the applied transaction
|
|
85
95
|
* @param tx — The transaction to insert
|
|
@@ -59,11 +59,10 @@ export class KnexStorage implements Storage {
|
|
|
59
59
|
async findUTXOsForTopic(topic: string, since?: number): Promise<Output[]> {
|
|
60
60
|
const query = this.knex('outputs').where({ topic, spent: false })
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// }
|
|
62
|
+
// If provided, additionally filters UTXOs by block height
|
|
63
|
+
if (since !== undefined) {
|
|
64
|
+
await query.andWhere('blockHeight', '>=', since)
|
|
65
|
+
}
|
|
67
66
|
|
|
68
67
|
const outputs = await query.select(
|
|
69
68
|
'txid', 'outputIndex', 'outputScript', 'topic', 'satoshis', 'beef', 'outputsConsumed', 'spent', 'consumedBy'
|
|
@@ -127,6 +126,14 @@ export class KnexStorage implements Storage {
|
|
|
127
126
|
}).update('beef', Buffer.from(beef))
|
|
128
127
|
}
|
|
129
128
|
|
|
129
|
+
async updateOutputBlockHeight(txid: string, outputIndex: number, topic: string, blockHeight: number): Promise<void> {
|
|
130
|
+
await this.knex('outputs').where({
|
|
131
|
+
txid,
|
|
132
|
+
outputIndex,
|
|
133
|
+
topic
|
|
134
|
+
}).update('blockHeight', blockHeight)
|
|
135
|
+
}
|
|
136
|
+
|
|
130
137
|
async insertAppliedTransaction(tx: { txid: string, topic: string }): Promise<void> {
|
|
131
138
|
await this.knex('applied_transactions').insert({
|
|
132
139
|
txid: tx.txid,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Knex } from 'knex'
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
// Add new column for blockHeight associated with outputs
|
|
5
|
+
await knex.schema.table('outputs', table => {
|
|
6
|
+
table.integer('blockHeight').unsigned().nullable()
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
// Remove the blockHeight column (rollback operation)
|
|
12
|
+
await knex.schema.table('outputs', table => {
|
|
13
|
+
table.dropColumn('blockHeight')
|
|
14
|
+
})
|
|
15
|
+
}
|