@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/overlay",
3
- "version": "2.2.0",
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.0.0",
58
+ "@types/node": "^26.1.1",
59
59
  "jest": "^30.4.2",
60
- "ts-jest": "^29.4.11",
61
- "ts-standard": "^12.0.2",
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
- "@bsv/sdk": "^2.1.3"
64
+ "oxlint": "^1.75.0",
65
+ "@bsv/sdk": "^2.2.0"
66
66
  },
67
67
  "dependencies": {
68
- "@bsv/gasp": "^1.3.0",
69
- "knex": "^3.2.10"
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": "ts-standard --fix src/**/*.ts",
84
- "lint:ci": "ts-standard src/**/*.ts",
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.merklePath !== undefined) {
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
- if (tx.id('hex') === txid) {
1963
- tx.merklePath = proof
1964
- } else {
1965
- for (const input of tx.inputs) {
1966
- // All inputs must have sourceTransactions
1967
- const stx = input.sourceTransaction
1968
- if (typeof stx !== 'object') continue
1969
- this.updateInputProofs(stx, txid, proof)
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
- if (tx.merklePath !== undefined) {
1988
- // Update the merkle path to handle potential reorgs
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
- console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}: ${e}`)
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!.set(100, [
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!.get(100)!)
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')!.tac
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!.find(r => r.txid === TXID_2)!
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')!.tac
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')!.basmRoot).toBe(root100)
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')!.tac
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!.find(r => r.txid === TXID_2)!.proven).toBe(true)
226
- expect(store.anchors.get('tm_test:101')!.tac).toBe(before)
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')!.basmRoot).toBe(ZERO)
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!.find(r => r.txid === TXID_2)!
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!.find(r => r.txid === TXID_2)!.proven).toBe(false)
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!.find(r => r.txid === TXID_1)!.proven).toBe(true)
277
- expect(store.anchors.get('tm_test:100')!.basmRoot).toBe(computeBasmRoot([{ txid: TXID_1, blockIndex: 0 }]))
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')!.tac
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!.find(r => r.txid === TXID_2)!.proven).toBe(true)
288
- expect(store.anchors.get('tm_test:101')!.tac).toBe(before)
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(Object.keys(newBEEF).length).toBe(0)
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).length).toBe(4)
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).length).toBe(4)
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).length).toBe(4)
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).length).toBe(4)
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
- await knex.raw(`
13
- INSERT IGNORE INTO transactions (txid, beef)
14
- SELECT txid, beef
15
- FROM outputs
16
- WHERE beef IS NOT NULL
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 using a JOIN
32
- await knex.raw(`
33
- UPDATE outputs
34
- JOIN transactions ON outputs.txid = transactions.txid
35
- SET outputs.beef = transactions.beef
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')