@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.
Files changed (56) hide show
  1. package/dist/cjs/mod.js.map +1 -1
  2. package/dist/cjs/package.json +2 -2
  3. package/dist/cjs/src/Advertisement.js +3 -0
  4. package/dist/cjs/src/Advertisement.js.map +1 -0
  5. package/dist/cjs/src/Engine.js +108 -64
  6. package/dist/cjs/src/Engine.js.map +1 -1
  7. package/dist/cjs/src/GASP.js +20 -15
  8. package/dist/cjs/src/GASP.js.map +1 -1
  9. package/dist/cjs/src/storage/knex/KnexStorage.js +11 -5
  10. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  11. package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js +18 -0
  12. package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -0
  13. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  14. package/dist/esm/mod.js.map +1 -1
  15. package/dist/esm/src/Advertisement.js +2 -0
  16. package/dist/esm/src/Advertisement.js.map +1 -0
  17. package/dist/esm/src/Engine.js +107 -64
  18. package/dist/esm/src/Engine.js.map +1 -1
  19. package/dist/esm/src/GASP.js +20 -15
  20. package/dist/esm/src/GASP.js.map +1 -1
  21. package/dist/esm/src/storage/knex/KnexStorage.js +11 -5
  22. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  23. package/dist/esm/src/storage/knex/migrations/2024-07-10-001-block-height.js +13 -0
  24. package/dist/esm/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -0
  25. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  26. package/dist/types/mod.d.ts +2 -0
  27. package/dist/types/mod.d.ts.map +1 -1
  28. package/dist/types/src/Advertisement.d.ts +9 -0
  29. package/dist/types/src/Advertisement.d.ts.map +1 -0
  30. package/dist/types/src/Advertiser.d.ts +16 -24
  31. package/dist/types/src/Advertiser.d.ts.map +1 -1
  32. package/dist/types/src/Engine.d.ts +6 -8
  33. package/dist/types/src/Engine.d.ts.map +1 -1
  34. package/dist/types/src/GASP.d.ts +1 -0
  35. package/dist/types/src/GASP.d.ts.map +1 -1
  36. package/dist/types/src/Output.d.ts +1 -0
  37. package/dist/types/src/Output.d.ts.map +1 -1
  38. package/dist/types/src/storage/Storage.d.ts +8 -0
  39. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  40. package/dist/types/src/storage/knex/KnexStorage.d.ts +1 -0
  41. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  42. package/dist/types/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts +4 -0
  43. package/dist/types/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts.map +1 -0
  44. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  45. package/mod.ts +2 -0
  46. package/package.json +2 -2
  47. package/src/Advertisement.ts +8 -0
  48. package/src/Advertiser.ts +17 -25
  49. package/src/Engine.ts +112 -75
  50. package/src/Output.ts +1 -0
  51. package/src/storage/Storage.ts +10 -0
  52. package/src/storage/knex/KnexStorage.ts +12 -5
  53. package/src/storage/knex/migrations/2024-07-10-001-block-height.ts +15 -0
  54. package/src/GASP.ts +0 -420
  55. package/src/SHIPAdvertisement.ts +0 -8
  56. 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: SHIPAdvertisement[] = []
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: SHIPAdvertisement) => {
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.findAllSHIPAdvertisements()
427
- const currentSLAPAdvertisements = await advertiser.findAllSLAPAdvertisements()
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 existingSHIPTopics = new Set(currentSHIPAdvertisements.map(ad => ad.topic))
434
- const existingSLAPServices = new Set(currentSLAPAdvertisements.map(ad => ad.service))
433
+ const existingTopics = new Set(currentSHIPAdvertisements.map(ad => ad.topicOrService))
434
+ const existingServices = new Set(currentSLAPAdvertisements.map(ad => ad.topicOrService))
435
435
 
436
- const shipToCreate = Array.from(requiredSHIPAdvertisements).filter(topic => !existingSHIPTopics.has(topic))
437
- const shipToRevoke = currentSHIPAdvertisements.filter(ad => !requiredSHIPAdvertisements.has(ad.topic))
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
- const slapToCreate = Array.from(requiredSLAPAdvertisements).filter(service => !existingSLAPServices.has(service))
440
- const slapToRevoke = currentSLAPAdvertisements.filter(ad => !requiredSLAPAdvertisements.has(ad.service))
441
-
442
- // Step 4: Update Advertisements
443
- for (const topic of shipToCreate) {
444
- try {
445
- const taggedBEEF = await advertiser.createSHIPAdvertisement(topic)
446
- await this.submit(taggedBEEF)
447
- } catch (error) {
448
- console.error('Failed to create SHIP advertisement:', error)
449
- }
450
- }
451
-
452
- for (const service of slapToCreate) {
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
- for (const ad of slapToRevoke) {
471
- try {
472
- const taggedBEEF = await advertiser.revokeAdvertisement(ad)
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 (TODO: timestamp or block height, we need to decide on sync timing semantics) in the request. It constructs a response that includes a list of these UTXOs
543
- * and the (timestamp or block height, TODO...) from the initial request.
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 (timestamp or block height, TODO...) since the last sync.
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 timestamp.
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 output = await this.storage.findOutput(txid, outputIndex)
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
- if (output === undefined || output === null) {
576
- throw new Error('No matching output found!')
577
- }
570
+ const rootTx = Transaction.fromBEEF(output.beef)
571
+ let correctTx: Transaction | undefined
578
572
 
579
- const tx = Transaction.fromBEEF(output.beef)
580
- const rawTx = tx.toHex()
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
- const node: GASPNode = {
583
- rawTx,
584
- graphID,
585
- outputIndex
586
- }
587
- if (tx.merklePath !== undefined) {
588
- node.proof = tx.merklePath.toHex()
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
- return node
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
- // transaction already has a proof
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
- if (tx.merklePath)
769
- // Already have a proof for this output's transaction.
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 == undefined || outputs.length === 0) {
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
@@ -26,4 +26,5 @@ export type Output = {
26
26
  txid: string
27
27
  outputIndex: number
28
28
  }>
29
+ blockHeight?: number
29
30
  }
@@ -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
- // TODO: Create new migration for outputs that adds either created_at or block height tagging for proven outpoints if necessary.
63
- // if (since !== undefined) {
64
- // const sinceDate = new Date(since).toISOString()
65
- // await query.andWhere('created_at', '>=', sinceDate)
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
+ }