@bsv/overlay 0.1.10 → 0.1.12

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 (36) hide show
  1. package/README.md +89 -4
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/src/Engine.js +402 -21
  4. package/dist/cjs/src/Engine.js.map +1 -1
  5. package/dist/cjs/src/GASP/OverlayGASPRemote.js +4 -6
  6. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  7. package/dist/cjs/src/GASP/OverlayGASPStorage.js +77 -152
  8. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  9. package/dist/cjs/src/SHIPAdvertisement.js +3 -0
  10. package/dist/cjs/src/SHIPAdvertisement.js.map +1 -0
  11. package/dist/cjs/src/SLAPAdvertisement.js +3 -0
  12. package/dist/cjs/src/SLAPAdvertisement.js.map +1 -0
  13. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  14. package/dist/esm/src/Engine.js +402 -20
  15. package/dist/esm/src/Engine.js.map +1 -1
  16. package/dist/esm/src/GASP/OverlayGASPRemote.js +4 -7
  17. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  18. package/dist/esm/src/GASP/OverlayGASPStorage.js +77 -151
  19. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  20. package/dist/esm/src/SHIPAdvertisement.js +2 -0
  21. package/dist/esm/src/SHIPAdvertisement.js.map +1 -0
  22. package/dist/esm/src/SLAPAdvertisement.js +2 -0
  23. package/dist/esm/src/SLAPAdvertisement.js.map +1 -0
  24. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  25. package/dist/types/src/Engine.d.ts +118 -4
  26. package/dist/types/src/Engine.d.ts.map +1 -1
  27. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +2 -3
  28. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  29. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +3 -29
  30. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  31. package/dist/types/src/SHIPAdvertisement.d.ts +9 -0
  32. package/dist/types/src/SHIPAdvertisement.d.ts.map +1 -0
  33. package/dist/types/src/SLAPAdvertisement.d.ts +9 -0
  34. package/dist/types/src/SLAPAdvertisement.d.ts.map +1 -0
  35. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  36. package/package.json +2 -2
package/README.md CHANGED
@@ -39,7 +39,7 @@ In your server's main file, you can set everything up. Create a new Engine to ru
39
39
  const express = require('express')
40
40
  const bodyparser = require('body-parser')
41
41
  const { Engine, KnexStorage, HelloTopicManager, HelloLookupService, HelloStorageEngine } = require('@bsv/overlay')
42
- const { WoChain } = require('@bsv/sdk')
42
+ import { WhatsOnChain, NodejsHttpClient, ARC, ArcConfig, MerklePath } from '@bsv/sdk'
43
43
  // Populate a Knexfile with your database credentials
44
44
  const knex = require('knex')(require('../knexfile.js'))
45
45
  const app = express()
@@ -58,7 +58,18 @@ const engine = new Engine(
58
58
  },
59
59
  new KnexStorageEngine({
60
60
  knex
61
- })
61
+ }),
62
+ new CombinatorialChainTracker([
63
+ new WhatsOnChain(
64
+ NODE_ENV === 'production' ? 'main' : 'test',
65
+ {
66
+ httpClient: new NodejsHttpClient(https)
67
+ })
68
+ ]),
69
+ HOSTING_DOMAIN as string,
70
+ SHIP_TRACKERS,
71
+ SLAP_TRACKERS,
72
+ new ARC('https://arc.taal.com', arcConfig)
62
73
  )
63
74
 
64
75
  // This allows the API to be used everywhere when CORS is enforced
@@ -133,8 +144,18 @@ app.get(`/getDocumentationForLookupServiceProvider`, async (req, res) => {
133
144
  // Submit transactions and facilitate lookup requests
134
145
  app.post(`/submit`, async (req, res) => {
135
146
  try {
136
- const result = await engine.submit(req.body)
137
- return res.status(200).json(result)
147
+ // Parse out the topics and construct the tagged BEEF
148
+ const topics = JSON.parse(req.headers['x-topics'] as string)
149
+ const taggedBEEF: TaggedBEEF = {
150
+ beef: Array.from(req.body as number[]),
151
+ topics
152
+ }
153
+
154
+ // Using a callback function, we can just return once our steak is ready
155
+ // instead of having to wait for all the broadcasts to occur.
156
+ await engine.submit(taggedBEEF, (steak: STEAK) => {
157
+ return res.status(200).json(steak)
158
+ })
138
159
  } catch (error) {
139
160
  return res.status(400).json({
140
161
  status: 'error',
@@ -156,6 +177,70 @@ app.post(`/lookup`, async (req, res) => {
156
177
  }
157
178
  })
158
179
 
180
+ app.post('/arc-ingest', (req, res) => {
181
+ (async () => {
182
+ try {
183
+ const merklePath = MerklePath.fromHex(req.body.merklePath)
184
+ await engine.handleNewMerkleProof(req.body.txid, merklePath, req.body.blockHeight)
185
+ return res.status(200).json({ status: 'success', message: 'transaction status updated' })
186
+ } catch (error) {
187
+ console.error(error)
188
+ return res.status(400).json({
189
+ status: 'error',
190
+ message: error instanceof Error ? error.message : 'An unknown error occurred'
191
+ })
192
+ }
193
+ })().catch(() => {
194
+ res.status(500).json({
195
+ status: 'error',
196
+ message: 'Unexpected error'
197
+ })
198
+ })
199
+ })
200
+
201
+ app.post('/requestSyncResponse', (req, res) => {
202
+ (async () => {
203
+ try {
204
+ const topic = req.headers['x-bsv-topic'] as string
205
+ const response = await engine.provideForeignSyncResponse(req.body, topic)
206
+ return res.status(200).json(response)
207
+ } catch (error) {
208
+ console.error(error)
209
+ return res.status(400).json({
210
+ status: 'error',
211
+ message: error instanceof Error ? error.message : 'An unknown error occurred'
212
+ })
213
+ }
214
+ })().catch(() => {
215
+ res.status(500).json({
216
+ status: 'error',
217
+ message: 'Unexpected error'
218
+ })
219
+ })
220
+ })
221
+
222
+ app.post('/requestForeignGASPNode', (req, res) => {
223
+ (async () => {
224
+ try {
225
+ console.log(req.body)
226
+ const { graphID, txid, outputIndex, metadata } = req.body
227
+ const response = await engine.provideForeignGASPNode(graphID, txid, outputIndex)
228
+ return res.status(200).json(response)
229
+ } catch (error) {
230
+ console.error(error)
231
+ return res.status(400).json({
232
+ status: 'error',
233
+ message: error instanceof Error ? error.message : 'An unknown error occurred'
234
+ })
235
+ }
236
+ })().catch(() => {
237
+ res.status(500).json({
238
+ status: 'error',
239
+ message: 'Unexpected error'
240
+ })
241
+ })
242
+ })
243
+
159
244
  // 404, all other routes are not found.
160
245
  app.use((req, res) => {
161
246
  console.log('404', req.url)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/overlay",
3
- "version": "0.1.9",
3
+ "version": "0.1.8",
4
4
  "type": "commonjs",
5
5
  "description": "BSV Blockchain Overlay Services Engine",
6
6
  "files": [
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Engine = void 0;
3
+ exports.OverlayGASPStorage = exports.OverlayGASPRemote = exports.Engine = void 0;
4
4
  const sdk_1 = require("@bsv/sdk");
5
5
  const gasp_1 = require("@bsv/gasp");
6
- const OverlayGASPRemote_js_1 = require("./GASP/OverlayGASPRemote.js");
7
- const OverlayGASPStorage_js_1 = require("./GASP/OverlayGASPStorage.js");
8
6
  /**
9
7
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
10
8
  */
@@ -14,7 +12,7 @@ class Engine {
14
12
  * @param {[key: string]: TopicManager} managers - manages topic admittance
15
13
  * @param {[key: string]: LookupService} lookupServices - manages UTXO lookups
16
14
  * @param {Storage} storage - for interacting with internally-managed persistent data
17
- * @param {ChainTracker | 'scripts only'} chainTracker - Verifies SPV data associated with transactions
15
+ * @param {ChainTracker} chainTracker - Verifies SPV data associated with transactions
18
16
  * @param {string} [hostingURL] - The URL this engine is hosted at. Required if going to support peer-discovery with an advertiser.
19
17
  * @param {Broadcaster} [Broadcaster] - broadcaster used for broadcasting the incoming transaction
20
18
  * @param {Advertiser} [Advertiser] - handles SHIP and SLAP advertisements for peer-discovery
@@ -86,6 +84,7 @@ class Engine {
86
84
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
87
85
  * @param {function(STEAK): void} [onSTEAKReady] - Optional callback function invoked when the STEAK is ready.
88
86
  * @param {string} mode — Indicates the submission behavior, whether historical or current. Historical transactions are not broadcast or propagated.
87
+ * @param {boolean} log
89
88
  *
90
89
  * The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
91
90
  *
@@ -104,9 +103,9 @@ class Engine {
104
103
  this.startTime(`submit_${txid}`);
105
104
  this.startTime(`chainTracker_${txid.substring(0, 10)}`);
106
105
  const txValid = await tx.verify(this.chainTracker);
106
+ this.endTime(`chainTracker_${txid.substring(0, 10)}`);
107
107
  if (!txValid)
108
108
  throw new Error('Unable to verify SPV information.');
109
- this.endTime(`chainTracker_${txid.substring(0, 10)}`);
110
109
  const steak = {};
111
110
  let admissableOutputs = { outputsToAdmit: [], coinsToRetain: [] };
112
111
  const previousCoins = [];
@@ -124,22 +123,20 @@ class Engine {
124
123
  return;
125
124
  }
126
125
  // Check if any input of this transaction is a previous UTXO
127
- const outputPromises = tx.inputs.map(async (input, i) => {
126
+ const outputPromises = tx.inputs.map(input => {
128
127
  var _a;
129
- const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : (_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex');
130
- if (previousTXID !== undefined) {
131
- const output = this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
132
- if (output !== undefined && output !== null) {
133
- previousCoins.push(i);
134
- return await Promise.resolve(output);
135
- }
136
- }
137
- return await Promise.resolve(null);
128
+ const previousTXID = input.sourceTXID || ((_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex'));
129
+ return this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
138
130
  });
139
131
  this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`);
140
132
  const outputs = await Promise.all(outputPromises);
141
133
  this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`);
142
- const markSpentPromises = outputs.map(async (output) => {
134
+ outputs.forEach((output, i) => {
135
+ if (output !== undefined && output !== null) {
136
+ previousCoins.push(i);
137
+ }
138
+ });
139
+ const markSpentPromises = outputs.map(async (output, i) => {
143
140
  if (output !== undefined && output !== null) {
144
141
  try {
145
142
  await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic);
@@ -239,7 +236,7 @@ class Engine {
239
236
  this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
240
237
  newUTXOs.push({ txid, outputIndex });
241
238
  this.startTime(`notifyLookupService${txid.substring(0, 10)}`);
242
- await Promise.all(Object.values(this.lookupServices).map(async (l) => { var _a; return await ((_a = l.outputAdded) === null || _a === void 0 ? void 0 : _a.call(l, txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)); }));
239
+ await Promise.all(Object.values(this.lookupServices).map(l => { var _a; return (_a = l.outputAdded) === null || _a === void 0 ? void 0 : _a.call(l, txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic); }));
243
240
  this.endTime(`notifyLookupService${txid.substring(0, 10)}`);
244
241
  }));
245
242
  this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
@@ -263,10 +260,10 @@ class Engine {
263
260
  if (this.advertiser === undefined || mode === 'historical-tx') {
264
261
  return steak;
265
262
  }
266
- this.startTime(`transactionPropagation_${txid.substring(0, 10)}`);
263
+ this.startTime(`transactionPropgation_${txid.substring(0, 10)}`);
267
264
  // Propagate transaction to other nodes according to synchronization agreements
268
265
  // 1. Find nodes that host the topics associated with admissable outputs
269
- // We want to figure out which topics we actually care about (because their associated outputs were admitted)
266
+ // We want to figure out which topics we actually care about(because their associated outputs were admitted)
270
267
  // AND if the topic was not admitted we want to remove it from the list of topics we care about.
271
268
  const relevantTopics = taggedBEEF.topics.filter(topic => steak[topic] !== undefined && steak[topic].outputsToAdmit.length !== 0);
272
269
  // TODO: Cache ship/slap lookup with expiry (every 5min)
@@ -521,7 +518,7 @@ class Engine {
521
518
  if (Array.isArray(syncEndpoints)) {
522
519
  await Promise.all(syncEndpoints.map(async (endpoint) => {
523
520
  // Sync to each host that is associated with this topic
524
- const gasp = new gasp_1.GASP(new OverlayGASPStorage_js_1.OverlayGASPStorage(topic, this), new OverlayGASPRemote_js_1.OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}]`, true);
521
+ const gasp = new gasp_1.GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}]`, true);
525
522
  await gasp.sync();
526
523
  }));
527
524
  }
@@ -867,5 +864,389 @@ There is currently a bug with the test runner that prevents importing and using
867
864
  Thus, all non-type exports have been moved to Engine.
868
865
  */
869
866
  // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPRemote.ts]-----
870
- // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPStorage.ts]-----
867
+ class OverlayGASPRemote {
868
+ constructor(endpointURL, topic) {
869
+ this.endpointURL = endpointURL;
870
+ this.topic = topic;
871
+ }
872
+ /**
873
+ * Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
874
+ * @param request
875
+ * @returns
876
+ */
877
+ async getInitialResponse(request) {
878
+ // Send out an HTTP request to the URL (current host for topic)
879
+ // Include the topic in the request
880
+ // Parse out response and return correct format
881
+ const url = `${this.endpointURL}/requestSyncResponse`;
882
+ const response = await fetch(url, {
883
+ method: 'POST',
884
+ headers: {
885
+ 'Content-Type': 'application/json',
886
+ 'X-BSV-Topic': this.topic
887
+ },
888
+ body: JSON.stringify(request)
889
+ });
890
+ if (!response.ok) {
891
+ throw new Error(`HTTP error! Status: ${response.status}`);
892
+ }
893
+ const result = await response.json();
894
+ // Validate and return the response in the correct format
895
+ if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
896
+ throw new Error('Invalid response format');
897
+ }
898
+ return {
899
+ UTXOList: result.UTXOList.map((utxo) => ({
900
+ txid: utxo.txid,
901
+ outputIndex: utxo.outputIndex
902
+ })),
903
+ since: result.since
904
+ };
905
+ }
906
+ /**
907
+ * Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
908
+ * @param graphID
909
+ * @param txid
910
+ * @param outputIndex
911
+ * @param metadata
912
+ * @returns
913
+ */
914
+ async requestNode(graphID, txid, outputIndex, metadata) {
915
+ // Send an HTTP request with the provided info and get back a gaspNode
916
+ const url = `${this.endpointURL}/requestForeignGASPNode`;
917
+ const body = {
918
+ graphID,
919
+ txid,
920
+ outputIndex,
921
+ metadata
922
+ };
923
+ const response = await fetch(url, {
924
+ method: 'POST',
925
+ headers: {
926
+ 'Content-Type': 'application/json'
927
+ },
928
+ body: JSON.stringify(body)
929
+ });
930
+ if (!response.ok) {
931
+ throw new Error(`HTTP error! Status: ${response.status}`);
932
+ }
933
+ const result = await response.json();
934
+ // Validate and return the response in the correct format
935
+ if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
936
+ throw new Error('Invalid response format');
937
+ }
938
+ const gaspNode = {
939
+ graphID: result.graphID,
940
+ rawTx: result.rawTx,
941
+ outputIndex: result.outputIndex,
942
+ proof: result.proof,
943
+ txMetadata: result.txMetadata,
944
+ outputMetadata: result.outputMetadata,
945
+ inputs: result.inputs
946
+ };
947
+ return gaspNode;
948
+ }
949
+ // ---- Now optional methods ----
950
+ // When are only syncing to them
951
+ async getInitialReply(response) {
952
+ throw new Error('Function not supported!');
953
+ }
954
+ // Only used when supporting bidirectional sync.
955
+ // Overlay services does not support this.
956
+ async submitNode(node) {
957
+ throw new Error('Node submission not supported!');
958
+ }
959
+ }
960
+ exports.OverlayGASPRemote = OverlayGASPRemote;
961
+ class OverlayGASPStorage {
962
+ constructor(topic, engine, maxNodesInGraph) {
963
+ this.topic = topic;
964
+ this.engine = engine;
965
+ this.maxNodesInGraph = maxNodesInGraph;
966
+ this.temporaryGraphNodeRefs = {};
967
+ }
968
+ /**
969
+ *
970
+ * @param since
971
+ * @returns
972
+ */
973
+ async findKnownUTXOs(since) {
974
+ const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since);
975
+ return UTXOs.map(output => ({
976
+ txid: output.txid,
977
+ outputIndex: output.outputIndex
978
+ }));
979
+ }
980
+ /**
981
+ * 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.
982
+ * @param graphID
983
+ * @param txid
984
+ * @param outputIndex
985
+ * @param metadata
986
+ * @returns
987
+ */
988
+ async hydrateGASPNode(graphID, txid, outputIndex, metadata) {
989
+ const output = await this.engine.storage.findOutput(txid, outputIndex, undefined, undefined, true);
990
+ if ((output === null || output === void 0 ? void 0 : output.beef) === undefined) {
991
+ throw new Error('No matching output found!');
992
+ }
993
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
994
+ const rawTx = tx.toHex();
995
+ const node = {
996
+ rawTx,
997
+ graphID,
998
+ outputIndex
999
+ };
1000
+ if (tx.merklePath !== undefined) {
1001
+ node.proof = tx.merklePath.toHex();
1002
+ }
1003
+ return node;
1004
+ }
1005
+ /**
1006
+ * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
1007
+ * @param tx The node for which needed inputs should be found.
1008
+ * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
1009
+ */
1010
+ async findNeededInputs(tx) {
1011
+ var _a, _b, _c;
1012
+ // If there is no Merkle proof, we always need the inputs
1013
+ const response = {
1014
+ requestedInputs: {}
1015
+ };
1016
+ const parsedTx = sdk_1.Transaction.fromHex(tx.rawTx);
1017
+ if (tx.proof === undefined) {
1018
+ for (const input of parsedTx.inputs) {
1019
+ response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
1020
+ metadata: false
1021
+ };
1022
+ }
1023
+ return await this.stripAlreadyKnownInputs(response);
1024
+ }
1025
+ // Attempt to check if the current transaction is admissible
1026
+ parsedTx.merklePath = sdk_1.MerklePath.fromHex(tx.proof);
1027
+ const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), []);
1028
+ if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
1029
+ // The transaction is admissible, no further inputs are needed
1030
+ }
1031
+ else {
1032
+ // The transaction is not admissible, get inputs needed for further verification
1033
+ // TopicManagers should implement a function to identify which inputs are needed.
1034
+ if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
1035
+ try {
1036
+ const neededInputs = (_c = await ((_b = (_a = this.engine.managers[this.topic]).identifyNeededInputs) === null || _b === void 0 ? void 0 : _b.call(_a, parsedTx.toBEEF()))) !== null && _c !== void 0 ? _c : [];
1037
+ for (const input of neededInputs) {
1038
+ response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
1039
+ metadata: false
1040
+ };
1041
+ }
1042
+ return await this.stripAlreadyKnownInputs(response);
1043
+ }
1044
+ catch (e) {
1045
+ console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`);
1046
+ // Cut off the graph in case of an error here.
1047
+ }
1048
+ }
1049
+ // By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
1050
+ }
1051
+ // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
1052
+ }
1053
+ /**
1054
+ * Ensures that no inputs are requested from foreign nodes before sending any GASP response
1055
+ * Also terminates graphs if the response would be empty.
1056
+ */
1057
+ async stripAlreadyKnownInputs(response) {
1058
+ if (typeof response === 'undefined') {
1059
+ return response;
1060
+ }
1061
+ for (const inputNodeId of Object.keys(response.requestedInputs)) {
1062
+ const [txid, outputIndex] = inputNodeId.split('.');
1063
+ const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic);
1064
+ if (found !== null && found !== undefined) {
1065
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1066
+ delete response.requestedInputs[inputNodeId];
1067
+ }
1068
+ }
1069
+ if (Object.keys(response.requestedInputs).length === 0) {
1070
+ return undefined;
1071
+ }
1072
+ return response;
1073
+ }
1074
+ /**
1075
+ * Appends a new node to a temporary graph.
1076
+ * @param tx The node to append to this graph.
1077
+ * @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.
1078
+ * @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.
1079
+ */
1080
+ async appendToGraph(tx, spentBy) {
1081
+ if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
1082
+ throw new Error('The max number of nodes in transaction graph has been reached!');
1083
+ }
1084
+ const parsedTx = sdk_1.Transaction.fromHex(tx.rawTx);
1085
+ const txid = parsedTx.id('hex');
1086
+ if (tx.proof !== undefined) {
1087
+ parsedTx.merklePath = sdk_1.MerklePath.fromHex(tx.proof);
1088
+ }
1089
+ // Given the passed in node, append to the temp graph
1090
+ // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
1091
+ const newGraphNode = {
1092
+ txid,
1093
+ graphID: tx.graphID,
1094
+ rawTx: tx.rawTx,
1095
+ outputIndex: tx.outputIndex,
1096
+ proof: tx.proof,
1097
+ txMetadata: tx.txMetadata,
1098
+ outputMetadata: tx.outputMetadata,
1099
+ inputs: tx.inputs,
1100
+ children: []
1101
+ };
1102
+ // If spentBy is undefined, then we know it's the root node.
1103
+ if (spentBy === undefined) {
1104
+ this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode;
1105
+ }
1106
+ else {
1107
+ // Find the parent node based on spentBy
1108
+ const parentNode = this.temporaryGraphNodeRefs[spentBy];
1109
+ if (parentNode !== undefined) {
1110
+ // Set parent-child relationship
1111
+ parentNode.children.push(newGraphNode);
1112
+ newGraphNode.parent = parentNode;
1113
+ this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode;
1114
+ }
1115
+ else {
1116
+ throw new Error(`Parent node with GraphID ${spentBy} not found`);
1117
+ }
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Checks whether the given graph, in its current state, makes reference only to transactions that are proven in the blockchain, or already known by the recipient to be valid.
1122
+ * Additionally, in a breadth-first manner (ensuring that all inputs for any given node are processed before nodes that spend them), it ensures that the root node remains valid according to the rules of the overlay's topic manager,
1123
+ * while considering any coins which the Manager had previously indicated were either valid or invalid.
1124
+ * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1125
+ * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
1126
+ */
1127
+ async validateGraphAnchor(graphID) {
1128
+ var _a;
1129
+ const rootNode = this.temporaryGraphNodeRefs[graphID];
1130
+ if (rootNode === undefined) {
1131
+ throw new Error(`Graph node with ID ${graphID} not found`);
1132
+ }
1133
+ // Check that the root node is Bitcoin-valid.
1134
+ const beef = this.getBEEFForNode(rootNode);
1135
+ const spvTx = sdk_1.Transaction.fromBEEF(beef);
1136
+ const isBitcoinValid = await spvTx.verify(this.engine.chainTracker);
1137
+ if (!isBitcoinValid) {
1138
+ throw new Error('The graph is not well-anchored according to the rules of Bitcoin.');
1139
+ }
1140
+ // Then, ensure the node is Overlay-valid.
1141
+ const beefs = this.computeOrderedBEEFsForGraph(graphID);
1142
+ // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
1143
+ const coins = new Set();
1144
+ // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
1145
+ // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
1146
+ for (const beef of beefs) {
1147
+ // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
1148
+ const previousCoins = [];
1149
+ const tx = sdk_1.Transaction.fromBEEF(beef);
1150
+ for (const inputIndex in tx.inputs) {
1151
+ const input = tx.inputs[inputIndex];
1152
+ const sourceTXID = input.sourceTXID || ((_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex'));
1153
+ const coin = `${sourceTXID}.${input.sourceOutputIndex}`;
1154
+ if (coins.has(coin)) {
1155
+ previousCoins.push(Number(inputIndex));
1156
+ }
1157
+ }
1158
+ const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins);
1159
+ // Every admitted output is now a coin.
1160
+ for (const outputIndex of admittanceInstructions.outputsToAdmit) {
1161
+ coins.add(`${tx.id('hex')}.${outputIndex}`);
1162
+ }
1163
+ }
1164
+ // After sending through all the graph's BEEFs...
1165
+ // If the root node is now a coin, we have acceptance by the overlay.
1166
+ // Otherwise, throw.
1167
+ if (!coins.has(graphID)) {
1168
+ throw new Error('This graph did not result in topical admittance of the root node. Rejecting.');
1169
+ }
1170
+ }
1171
+ /**
1172
+ * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
1173
+ * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1174
+ */
1175
+ async discardGraph(graphID) {
1176
+ for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
1177
+ if (graphRef.graphID === graphID) {
1178
+ // Delete child node
1179
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1180
+ delete this.temporaryGraphNodeRefs[nodeId];
1181
+ }
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
1186
+ * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
1187
+ */
1188
+ async finalizeGraph(graphID) {
1189
+ const beefs = this.computeOrderedBEEFsForGraph(graphID);
1190
+ // Submit all historical BEEFs in order, finalizing the graph for the current UTXO
1191
+ for (const beef of beefs) {
1192
+ await this.engine.submit({
1193
+ beef,
1194
+ topics: [this.topic]
1195
+ }, () => { }, 'historical-tx');
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Computes an ordered set of BEEFs for the graph with the given graph IDs
1200
+ * @param {string} graphID — The ID of the graph for which BEEFs are required
1201
+ * @returns Ordered BEEFs for the graph
1202
+ */
1203
+ computeOrderedBEEFsForGraph(graphID) {
1204
+ const beefs = [];
1205
+ const hydrator = (node) => {
1206
+ const currentBEEF = this.getBEEFForNode(node);
1207
+ if (beefs.indexOf(currentBEEF) === -1) {
1208
+ beefs.unshift(currentBEEF);
1209
+ }
1210
+ for (const child of node.children) {
1211
+ // Continue backwards to the earliest nodes, adding them onto the beginning
1212
+ hydrator(child);
1213
+ }
1214
+ };
1215
+ // Start the hydrator with the root node
1216
+ const foundRoot = this.temporaryGraphNodeRefs[graphID];
1217
+ if (!foundRoot) {
1218
+ throw new Error('Unable to find root node in graph for finalization!');
1219
+ }
1220
+ hydrator(foundRoot);
1221
+ return beefs;
1222
+ }
1223
+ /**
1224
+ * Computes a full BEEF for a given graph node, based on the temporary graph store.
1225
+ * @param node Graph node for which BEEF is needed.
1226
+ * @returns BEEF array, including all proofs on inputs.
1227
+ */
1228
+ getBEEFForNode(node) {
1229
+ // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
1230
+ const hydrator = (node) => {
1231
+ const tx = sdk_1.Transaction.fromHex(node.rawTx);
1232
+ if (node.proof) {
1233
+ tx.merklePath = sdk_1.MerklePath.fromHex(node.proof);
1234
+ return tx; // Transaction with proof, end of the line.
1235
+ }
1236
+ // For each input, look it up and recurse.
1237
+ for (const inputIndex in tx.inputs) {
1238
+ const input = tx.inputs[inputIndex];
1239
+ const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID}.${input.sourceOutputIndex}`];
1240
+ if (!foundNode) {
1241
+ throw new Error('Required input node for unproven parent not found in temporary graph store. Ensure, for every parent of any given already-proven node (kept for Overlay-specific historical reasons), that a proof is also provided on those inputs. While implicitly they are valid by virtue of their descendents being proven in the blockchain, BEEF serialization will still fail when winding forward the topical UTXO set histories during sync.');
1242
+ }
1243
+ tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode);
1244
+ }
1245
+ return tx;
1246
+ };
1247
+ const finalTX = hydrator(node);
1248
+ return finalTX.toBEEF();
1249
+ }
1250
+ }
1251
+ exports.OverlayGASPStorage = OverlayGASPStorage;
871
1252
  //# sourceMappingURL=Engine.js.map