@bsv/overlay 2.2.0 → 2.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/README.md +4 -1
- package/dist/cjs/package.json +10 -10
- package/dist/cjs/src/Engine.js +14 -19
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +2 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js +36 -12
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +14 -19
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +2 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js +36 -12
- package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.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/tsconfig.types.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/Engine.ts +13 -18
- package/src/GASP/OverlayGASPStorage.ts +2 -1
- package/src/__tests/BASMChain.test.ts +26 -21
- package/src/__tests/Engine.test.ts +42 -6
- package/src/__tests/OverlayGASPStorage.test.ts +34 -0
- package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +36 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsv/overlay",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BSV Blockchain Overlay Services Engine",
|
|
6
6
|
"main": "dist/cjs/mod.js",
|
|
@@ -55,18 +55,18 @@
|
|
|
55
55
|
"homepage": "https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay#readme",
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/jest": "^30.0.0",
|
|
58
|
-
"@types/node": "^26.
|
|
58
|
+
"@types/node": "^26.1.1",
|
|
59
59
|
"jest": "^30.4.2",
|
|
60
|
-
"ts-jest": "^29.4.
|
|
61
|
-
"
|
|
62
|
-
"ts2md": "^0.2.0",
|
|
60
|
+
"ts-jest": "^29.4.12",
|
|
61
|
+
"ts2md": "^0.2.8",
|
|
63
62
|
"tsconfig-to-dual-package": "^1.2.0",
|
|
64
63
|
"typescript": "^6.0.3",
|
|
65
|
-
"
|
|
64
|
+
"oxlint": "^1.75.0",
|
|
65
|
+
"@bsv/sdk": "^2.2.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"
|
|
69
|
-
"
|
|
68
|
+
"knex": "^3.3.0",
|
|
69
|
+
"@bsv/gasp": "^1.3.1"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@bsv/sdk": "^2.1.6"
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"test": "npm run build && jest",
|
|
81
81
|
"test:watch": "npm run build && jest --watch",
|
|
82
82
|
"test:coverage": "npm run build && jest --coverage",
|
|
83
|
-
"lint": "
|
|
84
|
-
"lint:ci": "
|
|
83
|
+
"lint": "oxlint src",
|
|
84
|
+
"lint:ci": "oxlint src",
|
|
85
85
|
"build": "tsc -b && tsconfig-to-dual-package tsconfig.cjs.json",
|
|
86
86
|
"dev": "tsc -b -w",
|
|
87
87
|
"prepublish": "npm run build",
|
package/src/Engine.ts
CHANGED
|
@@ -919,7 +919,7 @@ export class Engine {
|
|
|
919
919
|
async lookup(lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
|
|
920
920
|
// Validate a lookup service for the provider is found
|
|
921
921
|
const lookupService = this.lookupServices[lookupQuestion.service]
|
|
922
|
-
if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}
|
|
922
|
+
if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`)
|
|
923
923
|
|
|
924
924
|
const lookupResult = await lookupService.lookup(lookupQuestion)
|
|
925
925
|
const hydrationContext = this.createUTXOHistoryHydrationContext()
|
|
@@ -1954,20 +1954,20 @@ export class Engine {
|
|
|
1954
1954
|
* @param proof for txid
|
|
1955
1955
|
*/
|
|
1956
1956
|
private updateInputProofs(tx: Transaction, txid: string, proof: MerklePath): void {
|
|
1957
|
-
if (tx.
|
|
1957
|
+
if (tx.id('hex') === txid) {
|
|
1958
1958
|
// Update the merkle path to handle potential reorgs
|
|
1959
1959
|
tx.merklePath = proof
|
|
1960
1960
|
return
|
|
1961
1961
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1962
|
+
// A mined transaction's source graph is no longer part of its BEEF. Do not
|
|
1963
|
+
// replace an unrelated transaction's proof with the proof for an ancestor.
|
|
1964
|
+
if (tx.merklePath !== undefined) return
|
|
1965
|
+
|
|
1966
|
+
for (const input of tx.inputs) {
|
|
1967
|
+
// All inputs must have sourceTransactions
|
|
1968
|
+
const stx = input.sourceTransaction
|
|
1969
|
+
if (typeof stx !== 'object') continue
|
|
1970
|
+
this.updateInputProofs(stx, txid, proof)
|
|
1971
1971
|
}
|
|
1972
1972
|
}
|
|
1973
1973
|
|
|
@@ -1984,13 +1984,8 @@ export class Engine {
|
|
|
1984
1984
|
}
|
|
1985
1985
|
|
|
1986
1986
|
const tx = Transaction.fromBEEF(output.beef)
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
tx.merklePath = proof
|
|
1990
|
-
return
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
// recursively update all sourceTransactions proven by (txid,proof)
|
|
1987
|
+
// Update this transaction, or recursively update the matching source
|
|
1988
|
+
// transaction. This also persists replacement proofs after a reorg.
|
|
1994
1989
|
this.updateInputProofs(tx, txid, proof)
|
|
1995
1990
|
|
|
1996
1991
|
// Update the output's BEEF in the storage DB
|
|
@@ -145,7 +145,8 @@ export class OverlayGASPStorage implements GASPStorage {
|
|
|
145
145
|
}
|
|
146
146
|
return await this.stripAlreadyKnownInputs(response)
|
|
147
147
|
} catch (e) {
|
|
148
|
-
|
|
148
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
149
|
+
console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}: ${message}`)
|
|
149
150
|
// Cut off the graph in case of an error here.
|
|
150
151
|
}
|
|
151
152
|
// By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
|
|
@@ -28,6 +28,11 @@ interface FakeStore {
|
|
|
28
28
|
applied?: AppliedRow[]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function must<T> (value: T | undefined, message: string): T {
|
|
32
|
+
if (value === undefined) throw new Error(message)
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
function makeStorage (store: FakeStore): any {
|
|
32
37
|
const key = (topic: string, height: number): string => `${topic}:${height}`
|
|
33
38
|
return {
|
|
@@ -111,11 +116,11 @@ describe('BRC-136 BASM anchor chain continuity', () => {
|
|
|
111
116
|
const store: FakeStore = { anchors: new Map(), admitted: new Map() }
|
|
112
117
|
|
|
113
118
|
// Genesis: topic admits two txs at height 100.
|
|
114
|
-
store.admitted
|
|
119
|
+
must(store.admitted, 'admitted transaction map').set(100, [
|
|
115
120
|
{ txid: TXID_1, blockIndex: 0 },
|
|
116
121
|
{ txid: TXID_2, blockIndex: 1 }
|
|
117
122
|
])
|
|
118
|
-
const genesisRoot = computeBasmRoot(store.admitted
|
|
123
|
+
const genesisRoot = computeBasmRoot(must(must(store.admitted, 'admitted transaction map').get(100), 'genesis admissions'))
|
|
119
124
|
const genesisTac = computeTac(ZERO, blockHashFor(100), genesisRoot)
|
|
120
125
|
store.anchors.set('tm_test:100', {
|
|
121
126
|
topic: 'tm_test',
|
|
@@ -137,7 +142,7 @@ describe('BRC-136 BASM anchor chain continuity', () => {
|
|
|
137
142
|
// 101..105 are empty anchors (zero root, zero count) chained off genesis.
|
|
138
143
|
let expectedTac = genesisTac
|
|
139
144
|
for (let h = 101; h <= 105; h++) {
|
|
140
|
-
const anchor = store.anchors.get(`tm_test:${h}`)
|
|
145
|
+
const anchor = must(store.anchors.get(`tm_test:${h}`), `anchor ${h}`)
|
|
141
146
|
expect(anchor.basmRoot).toBe(ZERO)
|
|
142
147
|
expect(anchor.admittedCount).toBe(0)
|
|
143
148
|
expectedTac = computeTac(expectedTac, blockHashFor(h), ZERO)
|
|
@@ -146,7 +151,7 @@ describe('BRC-136 BASM anchor chain continuity', () => {
|
|
|
146
151
|
|
|
147
152
|
// The tip TAC is a cumulative hash that still depends on the genesis block —
|
|
148
153
|
// i.e. it was NOT reset to a per-block value.
|
|
149
|
-
const tipTac = store.anchors.get('tm_test:105')
|
|
154
|
+
const tipTac = must(store.anchors.get('tm_test:105'), 'tip anchor').tac
|
|
150
155
|
const resetTac = computeTac(ZERO, blockHashFor(105), ZERO)
|
|
151
156
|
expect(tipTac).not.toBe(resetTac)
|
|
152
157
|
})
|
|
@@ -192,29 +197,29 @@ describe('BRC-136 BASM reorg handling', () => {
|
|
|
192
197
|
})
|
|
193
198
|
|
|
194
199
|
// TXID_2 is demoted: no longer proven, block metadata cleared.
|
|
195
|
-
const row = store.applied
|
|
200
|
+
const row = must(store.applied?.find(r => r.txid === TXID_2), 'orphaned applied row')
|
|
196
201
|
expect(row.proven).toBe(false)
|
|
197
202
|
expect(row.blockHeight).toBeUndefined()
|
|
198
203
|
// It survives as a receipt record (firstSeenHeight retained).
|
|
199
204
|
expect(row.firstSeenHeight).toBe(101)
|
|
200
205
|
|
|
201
206
|
// Anchor at 101 rebuilt: empty admitted set, canonical hash, recomputed TAC.
|
|
202
|
-
const anchor101 = store.anchors.get('tm_test:101')
|
|
207
|
+
const anchor101 = must(store.anchors.get('tm_test:101'), 'anchor 101')
|
|
203
208
|
expect(anchor101.admittedCount).toBe(0)
|
|
204
209
|
expect(anchor101.basmRoot).toBe(ZERO)
|
|
205
210
|
expect(anchor101.blockHash).toBe(H101_NEW)
|
|
206
|
-
const tac100 = store.anchors.get('tm_test:100')
|
|
211
|
+
const tac100 = must(store.anchors.get('tm_test:100'), 'anchor 100').tac
|
|
207
212
|
expect(anchor101.tac).toBe(computeTac(tac100, H101_NEW, ZERO))
|
|
208
213
|
|
|
209
214
|
// Height 100 is untouched.
|
|
210
215
|
const root100 = computeBasmRoot([{ txid: TXID_1, blockIndex: 0 }])
|
|
211
|
-
expect(store.anchors.get('tm_test:100')
|
|
216
|
+
expect(must(store.anchors.get('tm_test:100'), 'anchor 100').basmRoot).toBe(root100)
|
|
212
217
|
})
|
|
213
218
|
|
|
214
219
|
it('is idempotent: orphaned hashes matching no proven rows leave the chain unchanged', async () => {
|
|
215
220
|
const store = seedTwoBlockChain()
|
|
216
221
|
const engine = makeEngine(store, { currentHeight: 101 })
|
|
217
|
-
const before = store.anchors.get('tm_test:101')
|
|
222
|
+
const before = must(store.anchors.get('tm_test:101'), 'anchor 101').tac
|
|
218
223
|
|
|
219
224
|
await (engine as any).handleReorg({
|
|
220
225
|
orphanedBlockHashes: ['ffff000000000000000000000000000000000000000000000000000000000000'],
|
|
@@ -222,8 +227,8 @@ describe('BRC-136 BASM reorg handling', () => {
|
|
|
222
227
|
newTipHeight: 101
|
|
223
228
|
})
|
|
224
229
|
|
|
225
|
-
expect(store.applied
|
|
226
|
-
expect(store.anchors.get('tm_test:101')
|
|
230
|
+
expect(must(store.applied?.find(r => r.txid === TXID_2), 'applied row 2').proven).toBe(true)
|
|
231
|
+
expect(must(store.anchors.get('tm_test:101'), 'anchor 101').tac).toBe(before)
|
|
227
232
|
})
|
|
228
233
|
|
|
229
234
|
it('re-proving a demoted tx at a new height restores it to the admitted set', async () => {
|
|
@@ -235,10 +240,10 @@ describe('BRC-136 BASM reorg handling', () => {
|
|
|
235
240
|
rebuildFromHeight: 101,
|
|
236
241
|
newTipHeight: 101
|
|
237
242
|
})
|
|
238
|
-
expect(store.anchors.get('tm_test:101')
|
|
243
|
+
expect(must(store.anchors.get('tm_test:101'), 'anchor 101').basmRoot).toBe(ZERO)
|
|
239
244
|
|
|
240
245
|
// TXID_2 re-mined into the new canonical block at 101.
|
|
241
|
-
const row = store.applied
|
|
246
|
+
const row = must(store.applied?.find(r => r.txid === TXID_2), 'applied row 2')
|
|
242
247
|
row.proven = true
|
|
243
248
|
row.blockHeight = 101
|
|
244
249
|
row.blockHash = H101_NEW
|
|
@@ -251,7 +256,7 @@ describe('BRC-136 BASM reorg handling', () => {
|
|
|
251
256
|
newTipHeight: 101
|
|
252
257
|
})
|
|
253
258
|
|
|
254
|
-
const anchor101 = store.anchors.get('tm_test:101')
|
|
259
|
+
const anchor101 = must(store.anchors.get('tm_test:101'), 'anchor 101')
|
|
255
260
|
expect(anchor101.admittedCount).toBe(1)
|
|
256
261
|
expect(anchor101.basmRoot).toBe(computeBasmRoot([{ txid: TXID_2, blockIndex: 0 }]))
|
|
257
262
|
expect(anchor101.blockHash).toBe(H101_NEW)
|
|
@@ -268,23 +273,23 @@ describe('BRC-136 BASM reorg revalidation sweep', () => {
|
|
|
268
273
|
|
|
269
274
|
await (engine as any).revalidateRecentAnchors(3)
|
|
270
275
|
|
|
271
|
-
expect(store.applied
|
|
272
|
-
const anchor101 = store.anchors.get('tm_test:101')
|
|
276
|
+
expect(must(store.applied?.find(r => r.txid === TXID_2), 'applied row 2').proven).toBe(false)
|
|
277
|
+
const anchor101 = must(store.anchors.get('tm_test:101'), 'anchor 101')
|
|
273
278
|
expect(anchor101.basmRoot).toBe(ZERO)
|
|
274
279
|
expect(anchor101.blockHash).toBe(H101_NEW)
|
|
275
280
|
// Height 100 still valid and untouched.
|
|
276
|
-
expect(store.applied
|
|
277
|
-
expect(store.anchors.get('tm_test:100')
|
|
281
|
+
expect(must(store.applied?.find(r => r.txid === TXID_1), 'applied row 1').proven).toBe(true)
|
|
282
|
+
expect(must(store.anchors.get('tm_test:100'), 'anchor 100').basmRoot).toBe(computeBasmRoot([{ txid: TXID_1, blockIndex: 0 }]))
|
|
278
283
|
})
|
|
279
284
|
|
|
280
285
|
it('leaves the chain unchanged when every proof in the window still validates', async () => {
|
|
281
286
|
const store = seedTwoBlockChain()
|
|
282
287
|
const engine = makeEngine(store, { currentHeight: 101, isValidRootForHeight: async () => true })
|
|
283
|
-
const before = store.anchors.get('tm_test:101')
|
|
288
|
+
const before = must(store.anchors.get('tm_test:101'), 'anchor 101').tac
|
|
284
289
|
|
|
285
290
|
await (engine as any).revalidateRecentAnchors(3)
|
|
286
291
|
|
|
287
|
-
expect(store.applied
|
|
288
|
-
expect(store.anchors.get('tm_test:101')
|
|
292
|
+
expect(must(store.applied?.find(r => r.txid === TXID_2), 'applied row 2').proven).toBe(true)
|
|
293
|
+
expect(must(store.anchors.get('tm_test:101'), 'anchor 101').tac).toBe(before)
|
|
289
294
|
})
|
|
290
295
|
})
|
|
@@ -309,6 +309,37 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
309
309
|
})
|
|
310
310
|
})
|
|
311
311
|
describe('handleNewMerkleProof tests', () => {
|
|
312
|
+
it('persists a replacement proof when the stored BEEF is already mined', async () => {
|
|
313
|
+
const tx = Transaction.fromHexBEEF(beef37abad0)
|
|
314
|
+
const txid = tx.id('hex')
|
|
315
|
+
const output: Output = {
|
|
316
|
+
txid,
|
|
317
|
+
outputIndex: 0,
|
|
318
|
+
outputScript: tx.outputs[0].lockingScript.toBinary(),
|
|
319
|
+
topic: 'hello',
|
|
320
|
+
satoshis: tx.outputs[0].satoshis,
|
|
321
|
+
beef: tx.toBEEF(),
|
|
322
|
+
spent: false,
|
|
323
|
+
outputsConsumed: [],
|
|
324
|
+
consumedBy: [],
|
|
325
|
+
score: Date.now()
|
|
326
|
+
}
|
|
327
|
+
const updateTransactionBEEF = jest.fn(async () => undefined)
|
|
328
|
+
mockStorageEngine.findOutputsForTransaction = jest.fn(async () => [output])
|
|
329
|
+
mockStorageEngine.updateTransactionBEEF = updateTransactionBEEF
|
|
330
|
+
const engine = new Engine(
|
|
331
|
+
{ Hello: mockTopicManager },
|
|
332
|
+
{ Hello: mockLookupService },
|
|
333
|
+
mockStorageEngine,
|
|
334
|
+
mockChainTracker
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if (tx.merklePath === undefined) throw new Error('improper test setup')
|
|
338
|
+
await engine.handleNewMerkleProof(txid, tx.merklePath)
|
|
339
|
+
|
|
340
|
+
expect(updateTransactionBEEF).toHaveBeenCalledWith(txid, tx.toBEEF())
|
|
341
|
+
})
|
|
342
|
+
|
|
312
343
|
it('0 simple proof', async () => {
|
|
313
344
|
const beef = beef27c8f1
|
|
314
345
|
const txid = txid27c8f
|
|
@@ -423,7 +454,12 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
423
454
|
|
|
424
455
|
const mp37abad = Transaction.fromHexBEEF(beef37abad0).merklePath
|
|
425
456
|
await engine.handleNewMerkleProof(txid37abad, mp37abad)
|
|
426
|
-
expect(
|
|
457
|
+
expect(newBEEF[`${txid37abad}`]).toBe(beef37abad0)
|
|
458
|
+
expect(newBEEF[`${txid877734}`].length).toBeGreaterThan(beef8777340.length)
|
|
459
|
+
expect(newBEEF[`${txid509f5e}`].length).toBeGreaterThan(beef509f5e0.length)
|
|
460
|
+
expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
|
|
461
|
+
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
|
|
462
|
+
expect(Object.keys(newBEEF)).toHaveLength(5)
|
|
427
463
|
|
|
428
464
|
const mp877734 = Transaction.fromHexBEEF(beef8777340).merklePath
|
|
429
465
|
await engine.handleNewMerkleProof(txid877734, mp877734)
|
|
@@ -431,7 +467,7 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
431
467
|
expect(newBEEF[`${txid509f5e}`].length).toBeGreaterThan(beef509f5e0.length)
|
|
432
468
|
expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
|
|
433
469
|
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
|
|
434
|
-
expect(Object.keys(newBEEF)
|
|
470
|
+
expect(Object.keys(newBEEF)).toHaveLength(5)
|
|
435
471
|
|
|
436
472
|
const mp509f5e = Transaction.fromHexBEEF(beef509f5e0).merklePath
|
|
437
473
|
await engine.handleNewMerkleProof(txid509f5e, mp509f5e)
|
|
@@ -439,7 +475,7 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
439
475
|
expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
|
|
440
476
|
expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
|
|
441
477
|
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
|
|
442
|
-
expect(Object.keys(newBEEF)
|
|
478
|
+
expect(Object.keys(newBEEF)).toHaveLength(5)
|
|
443
479
|
|
|
444
480
|
const mp942620 = Transaction.fromHexBEEF(beef9426200).merklePath
|
|
445
481
|
await engine.handleNewMerkleProof(txid942620, mp942620)
|
|
@@ -447,7 +483,7 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
447
483
|
expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
|
|
448
484
|
expect(newBEEF[`${txid942620}`]).toBe(beef9426200)
|
|
449
485
|
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
|
|
450
|
-
expect(Object.keys(newBEEF)
|
|
486
|
+
expect(Object.keys(newBEEF)).toHaveLength(5)
|
|
451
487
|
|
|
452
488
|
const mp17d182 = Transaction.fromHexBEEF(beef17d1820).merklePath
|
|
453
489
|
await engine.handleNewMerkleProof(txid17d182, mp17d182)
|
|
@@ -455,7 +491,7 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
455
491
|
expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
|
|
456
492
|
expect(newBEEF[`${txid942620}`]).toBe(beef9426200)
|
|
457
493
|
expect(newBEEF[`${txid17d182}`]).toBe(beef17d1820)
|
|
458
|
-
expect(Object.keys(newBEEF)
|
|
494
|
+
expect(Object.keys(newBEEF)).toHaveLength(5)
|
|
459
495
|
})
|
|
460
496
|
})
|
|
461
497
|
|
|
@@ -963,7 +999,7 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
963
999
|
await expect(engine.lookup({
|
|
964
1000
|
service: 'HelloWorld',
|
|
965
1001
|
query: { name: 'Bob' }
|
|
966
|
-
})).rejects.toThrow()
|
|
1002
|
+
})).rejects.toThrow('Lookup service not found for provider: HelloWorld')
|
|
967
1003
|
})
|
|
968
1004
|
it('Calls the lookup function from the lookup service', async () => {
|
|
969
1005
|
// TODO: Make the default storage engine return something...?
|
|
@@ -125,6 +125,40 @@ describe('OverlayGASPStorage', () => {
|
|
|
125
125
|
requestedInputs: { 'neededTxid.1': { metadata: false } }
|
|
126
126
|
})
|
|
127
127
|
})
|
|
128
|
+
|
|
129
|
+
it.each([
|
|
130
|
+
[new Error('manager failed'), 'manager failed'],
|
|
131
|
+
['manager failed as a string', 'manager failed as a string']
|
|
132
|
+
])('terminates the graph and logs normalized identifyNeededInputs errors', async (failure, message) => {
|
|
133
|
+
const mockTx: GASPNode = {
|
|
134
|
+
rawTx: '001122',
|
|
135
|
+
proof: 'someproof',
|
|
136
|
+
graphID: 'txid123.0',
|
|
137
|
+
outputIndex: 0
|
|
138
|
+
}
|
|
139
|
+
const parsedTx = {
|
|
140
|
+
inputs: [],
|
|
141
|
+
toBEEF: jest.fn().mockReturnValue([1, 2, 3]),
|
|
142
|
+
id: jest.fn().mockReturnValue('txid123'),
|
|
143
|
+
merklePath: {}
|
|
144
|
+
}
|
|
145
|
+
Transaction.fromHex = jest.fn().mockReturnValue(parsedTx)
|
|
146
|
+
MerklePath.fromHex = jest.fn().mockReturnValue(parsedTx.merklePath)
|
|
147
|
+
mockEngine.managers['test-topic'] = {
|
|
148
|
+
identifyAdmissibleOutputs: jest.fn().mockResolvedValue({
|
|
149
|
+
outputsToAdmit: []
|
|
150
|
+
}),
|
|
151
|
+
identifyNeededInputs: jest.fn().mockRejectedValue(failure)
|
|
152
|
+
}
|
|
153
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
154
|
+
|
|
155
|
+
await expect(overlayStorage.findNeededInputs(mockTx)).resolves.toBeUndefined()
|
|
156
|
+
|
|
157
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
158
|
+
`An error occurred when identifying needed inputs for transaction: txid123.0: ${message}`
|
|
159
|
+
)
|
|
160
|
+
consoleError.mockRestore()
|
|
161
|
+
})
|
|
128
162
|
})
|
|
129
163
|
|
|
130
164
|
describe('discardGraph', () => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Knex } from 'knex'
|
|
2
2
|
|
|
3
3
|
export async function up (knex: Knex): Promise<void> {
|
|
4
|
+
const client = (knex.client.config.client || '').toLowerCase()
|
|
5
|
+
|
|
4
6
|
// Create the new transactions table
|
|
5
7
|
await knex.schema.createTable('transactions', table => {
|
|
6
8
|
table.increments()
|
|
@@ -9,12 +11,23 @@ export async function up (knex: Knex): Promise<void> {
|
|
|
9
11
|
})
|
|
10
12
|
|
|
11
13
|
// Move data from outputs table to the new transactions table and ensure deduplication
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
if (client.startsWith('mysql')) {
|
|
15
|
+
await knex.raw(`
|
|
16
|
+
INSERT IGNORE INTO transactions (txid, beef)
|
|
17
|
+
SELECT txid, beef
|
|
18
|
+
FROM outputs
|
|
19
|
+
WHERE beef IS NOT NULL
|
|
20
|
+
`)
|
|
21
|
+
} else {
|
|
22
|
+
// SQLite / PostgreSQL: INSERT IGNORE is MySQL-only syntax
|
|
23
|
+
await knex.raw(`
|
|
24
|
+
INSERT INTO transactions (txid, beef)
|
|
25
|
+
SELECT txid, beef
|
|
26
|
+
FROM outputs
|
|
27
|
+
WHERE beef IS NOT NULL
|
|
28
|
+
ON CONFLICT (txid) DO NOTHING
|
|
29
|
+
`)
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
// Drop the beef column from the outputs table
|
|
20
33
|
await knex.schema.table('outputs', table => {
|
|
@@ -23,17 +36,28 @@ export async function up (knex: Knex): Promise<void> {
|
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export async function down (knex: Knex): Promise<void> {
|
|
39
|
+
const client = (knex.client.config.client || '').toLowerCase()
|
|
40
|
+
|
|
26
41
|
// Add the beef column back to the outputs table
|
|
27
42
|
await knex.schema.table('outputs', table => {
|
|
28
43
|
table.binary('beef')
|
|
29
44
|
})
|
|
30
45
|
|
|
31
|
-
// Move data back from the transactions table to the outputs table
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
// Move data back from the transactions table to the outputs table
|
|
47
|
+
if (client.startsWith('mysql')) {
|
|
48
|
+
await knex.raw(`
|
|
49
|
+
UPDATE outputs
|
|
50
|
+
JOIN transactions ON outputs.txid = transactions.txid
|
|
51
|
+
SET outputs.beef = transactions.beef
|
|
52
|
+
`)
|
|
53
|
+
} else {
|
|
54
|
+
// SQLite / PostgreSQL: UPDATE ... JOIN is MySQL-only syntax
|
|
55
|
+
await knex.raw(`
|
|
56
|
+
UPDATE outputs
|
|
57
|
+
SET beef = (SELECT beef FROM transactions WHERE transactions.txid = outputs.txid)
|
|
58
|
+
WHERE txid IN (SELECT txid FROM transactions)
|
|
59
|
+
`)
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
// Drop the transactions table
|
|
39
63
|
await knex.schema.dropTable('transactions')
|