@breeztech/breez-sdk-spark 0.22.2 → 0.23.0
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/breez-sdk-spark.tgz +0 -0
- package/bundler/breez_sdk_spark_wasm.d.ts +65 -5
- package/bundler/breez_sdk_spark_wasm_bg.js +66 -27
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/bundler/index.js +15 -2
- package/bundler/package.json +5 -0
- package/bundler/storage/index.js +6 -0
- package/bundler/tree-store/index.js +1518 -0
- package/bundler/tree-store/package.json +12 -0
- package/deno/breez_sdk_spark_wasm.d.ts +65 -5
- package/deno/breez_sdk_spark_wasm.js +66 -27
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/nodejs/breez_sdk_spark_wasm.d.ts +65 -5
- package/nodejs/breez_sdk_spark_wasm.js +66 -27
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/nodejs/index.js +11 -0
- package/nodejs/mysql-storage/index.cjs +9 -1
- package/nodejs/mysql-storage/migrations.cjs +6 -0
- package/nodejs/mysql-tree-store/index.cjs +296 -29
- package/nodejs/mysql-tree-store/migrations.cjs +198 -34
- package/nodejs/package.json +1 -0
- package/nodejs/postgres-storage/index.cjs +9 -1
- package/nodejs/postgres-storage/migrations.cjs +6 -0
- package/nodejs/postgres-tree-store/index.cjs +301 -40
- package/nodejs/postgres-tree-store/migrations.cjs +42 -0
- package/nodejs/storage/index.cjs +14 -3
- package/nodejs/storage/migrations.cjs +6 -0
- package/nodejs/tree-store/errors.cjs +13 -0
- package/nodejs/tree-store/index.cjs +1185 -0
- package/nodejs/tree-store/migrations.cjs +185 -0
- package/nodejs/tree-store/package.json +9 -0
- package/package.json +1 -1
- package/web/breez_sdk_spark_wasm.d.ts +74 -11
- package/web/breez_sdk_spark_wasm.js +66 -27
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/web/index.js +15 -2
- package/web/package.json +5 -0
- package/web/storage/index.js +6 -0
- package/web/tree-store/index.js +1518 -0
- package/web/tree-store/package.json +12 -0
|
@@ -58,22 +58,22 @@ const LEAF_UPSERT_CHUNK_SIZE = 1000;
|
|
|
58
58
|
* leaf is sufficient). $1 is the user id.
|
|
59
59
|
*/
|
|
60
60
|
const SLIM_LEAF_CANDIDATES_SQL = `
|
|
61
|
-
SELECT id,
|
|
61
|
+
SELECT id, value
|
|
62
62
|
FROM brz_tree_leaves
|
|
63
63
|
WHERE user_id = $1
|
|
64
64
|
AND status = 'Available'
|
|
65
65
|
AND is_missing_from_operators = FALSE
|
|
66
66
|
AND reservation_id IS NULL
|
|
67
67
|
AND (
|
|
68
|
-
|
|
68
|
+
value <= $2
|
|
69
69
|
OR id = (
|
|
70
70
|
SELECT id FROM brz_tree_leaves
|
|
71
71
|
WHERE user_id = $1
|
|
72
72
|
AND status = 'Available'
|
|
73
73
|
AND is_missing_from_operators = FALSE
|
|
74
74
|
AND reservation_id IS NULL
|
|
75
|
-
AND
|
|
76
|
-
ORDER BY
|
|
75
|
+
AND value > $2
|
|
76
|
+
ORDER BY value
|
|
77
77
|
LIMIT 1
|
|
78
78
|
)
|
|
79
79
|
)
|
|
@@ -94,6 +94,30 @@ function _identityLockKey(prefix, identity) {
|
|
|
94
94
|
return hash.digest().readBigInt64BE(0);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Pair a leaf with its ancestors (nearest first) by walking `parent_node_id`
|
|
99
|
+
* through `nodes`. Returns null if the leaf itself is absent; stops at a gap or
|
|
100
|
+
* cycle, returning a partial chain.
|
|
101
|
+
* @param {Map<string, object>} nodes
|
|
102
|
+
* @param {string} leafId
|
|
103
|
+
* @returns {{leaf: object, ancestors: Array<object>}|null}
|
|
104
|
+
*/
|
|
105
|
+
function assembleExitChain(nodes, leafId) {
|
|
106
|
+
const leaf = nodes.get(leafId);
|
|
107
|
+
if (!leaf) return null;
|
|
108
|
+
const ancestors = [];
|
|
109
|
+
const visited = new Set([leafId]);
|
|
110
|
+
let current = leaf.parent_node_id;
|
|
111
|
+
while (current != null && !visited.has(current)) {
|
|
112
|
+
visited.add(current);
|
|
113
|
+
const node = nodes.get(current);
|
|
114
|
+
if (!node) break;
|
|
115
|
+
ancestors.push(node);
|
|
116
|
+
current = node.parent_node_id;
|
|
117
|
+
}
|
|
118
|
+
return { leaf, ancestors };
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
class PostgresTreeStore {
|
|
98
122
|
/**
|
|
99
123
|
* @param {import('pg').Pool} pool
|
|
@@ -143,9 +167,8 @@ class PostgresTreeStore {
|
|
|
143
167
|
}
|
|
144
168
|
|
|
145
169
|
/**
|
|
146
|
-
* Run a function inside a transaction with the advisory lock.
|
|
147
|
-
*
|
|
148
|
-
* (`tryReserveLeaves`, `setLeaves`).
|
|
170
|
+
* Run a function inside a transaction with the advisory lock. Used by every
|
|
171
|
+
* operation that mutates the leaf set or its reservations.
|
|
149
172
|
* @param {function(import('pg').PoolClient): Promise<T>} fn
|
|
150
173
|
* @returns {Promise<T>}
|
|
151
174
|
* @template T
|
|
@@ -171,9 +194,8 @@ class PostgresTreeStore {
|
|
|
171
194
|
|
|
172
195
|
/**
|
|
173
196
|
* Run a function inside a transaction without the advisory lock. Used by
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* suffice and the global lock would only add contention.
|
|
197
|
+
* `addLeaves` and by read-only queries (`trySelectLeaves`), where MVCC +
|
|
198
|
+
* row-level locks suffice and the global lock would only add contention.
|
|
177
199
|
* @param {function(import('pg').PoolClient): Promise<T>} fn
|
|
178
200
|
* @returns {Promise<T>}
|
|
179
201
|
* @template T
|
|
@@ -205,13 +227,14 @@ class PostgresTreeStore {
|
|
|
205
227
|
return;
|
|
206
228
|
}
|
|
207
229
|
|
|
230
|
+
const leafNodes = leaves;
|
|
208
231
|
await this._withTransaction(async (client) => {
|
|
209
232
|
// Remove these leaves from spent_leaves table
|
|
210
|
-
const leafIds =
|
|
233
|
+
const leafIds = leafNodes.map((l) => l.id);
|
|
211
234
|
await this._batchRemoveSpentLeaves(client, leafIds);
|
|
212
235
|
|
|
213
236
|
// Batch upsert all leaves
|
|
214
|
-
await this._batchUpsertLeaves(client,
|
|
237
|
+
await this._batchUpsertLeaves(client, leafNodes, false, null);
|
|
215
238
|
});
|
|
216
239
|
} catch (error) {
|
|
217
240
|
if (error instanceof TreeStoreError) throw error;
|
|
@@ -222,6 +245,120 @@ class PostgresTreeStore {
|
|
|
222
245
|
}
|
|
223
246
|
}
|
|
224
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Store the ancestor chain of each pedigree, leaving the leaf pool and any
|
|
250
|
+
* spent marker untouched.
|
|
251
|
+
* @param {Array} pedigrees - Array of LeafPedigree { leaf, ancestors }
|
|
252
|
+
*/
|
|
253
|
+
async storeAncestors(pedigrees) {
|
|
254
|
+
try {
|
|
255
|
+
if (!pedigrees || pedigrees.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await this._withWriteTransaction(async (client) => {
|
|
260
|
+
// A leaf can be spent between its chain being resolved and this write, and
|
|
261
|
+
// a chain is only ever removed with its leaf. Writing one for a leaf that is
|
|
262
|
+
// already gone would leave it behind for good.
|
|
263
|
+
const stored = await client.query(
|
|
264
|
+
"SELECT id FROM brz_tree_leaves WHERE user_id = $1 AND id = ANY($2)",
|
|
265
|
+
[this.identity, pedigrees.map((p) => p.leaf.id)]
|
|
266
|
+
);
|
|
267
|
+
const storedLeafIds = new Set(stored.rows.map((row) => row.id));
|
|
268
|
+
|
|
269
|
+
await this._batchUpsertAncestors(
|
|
270
|
+
client,
|
|
271
|
+
pedigrees.filter((p) => storedLeafIds.has(p.leaf.id))
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error instanceof TreeStoreError) throw error;
|
|
276
|
+
throw new TreeStoreError(
|
|
277
|
+
`Failed to store ancestors: ${error.message}`,
|
|
278
|
+
error
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Ids of the stored leaves whose chain cannot back an exit: the leaf has a
|
|
285
|
+
* parent, and no ancestor row of its own holds that parent.
|
|
286
|
+
* @returns {Promise<Array<string>>}
|
|
287
|
+
*/
|
|
288
|
+
async leavesMissingExitChains() {
|
|
289
|
+
try {
|
|
290
|
+
const result = await this.pool.query(
|
|
291
|
+
// A stored chain runs from its leaf's parent to a root, so a leaf whose
|
|
292
|
+
// chain holds the parent it has now is exitable. The join binds all three
|
|
293
|
+
// primary key columns, making it one index probe per leaf. A leaf that is
|
|
294
|
+
// itself a root needs no chain.
|
|
295
|
+
`SELECT l.id
|
|
296
|
+
FROM brz_tree_leaves l
|
|
297
|
+
LEFT JOIN brz_tree_ancestors link
|
|
298
|
+
ON link.user_id = l.user_id AND link.leaf_id = l.id
|
|
299
|
+
AND link.id = l.parent_node_id
|
|
300
|
+
WHERE l.user_id = $1
|
|
301
|
+
AND l.parent_node_id IS NOT NULL
|
|
302
|
+
AND link.leaf_id IS NULL`,
|
|
303
|
+
[this.identity]
|
|
304
|
+
);
|
|
305
|
+
return result.rows.map((r) => r.id);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof TreeStoreError) throw error;
|
|
308
|
+
throw new TreeStoreError(
|
|
309
|
+
`Failed to get leaves missing exit chains: ${error.message}`,
|
|
310
|
+
error
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Reconstruct the exit chains for many leaves in one query, each as
|
|
317
|
+
* { leaf, ancestors } with ancestors nearest first. A leaf absent from the store
|
|
318
|
+
* is skipped; a chain that hits a gap comes back partial.
|
|
319
|
+
* @param {Array<string>} leafIds
|
|
320
|
+
* @returns {Promise<Array<{leaf: object, ancestors: Array<object>}>>}
|
|
321
|
+
*/
|
|
322
|
+
async getExitChains(leafIds) {
|
|
323
|
+
try {
|
|
324
|
+
if (!leafIds || leafIds.length === 0) return [];
|
|
325
|
+
// One query loads each requested leaf's own row plus its ancestor rows,
|
|
326
|
+
// both tagged by the owning leaf id (a leaf's own row is tagged with its
|
|
327
|
+
// own id). Reading them in two queries could pair a leaf with ancestors
|
|
328
|
+
// from a different snapshot. Grouping by that tag keeps each leaf's node
|
|
329
|
+
// set separate, so a node id stored under another leaf can never
|
|
330
|
+
// cross-contaminate this one.
|
|
331
|
+
const result_rows = await this.pool.query(
|
|
332
|
+
`SELECT leaf_id, data FROM brz_tree_ancestors WHERE user_id = $1 AND leaf_id = ANY($2)
|
|
333
|
+
UNION ALL
|
|
334
|
+
SELECT id AS leaf_id, data FROM brz_tree_leaves WHERE user_id = $1 AND id = ANY($2)`,
|
|
335
|
+
[this.identity, leafIds]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const nodesByLeaf = new Map();
|
|
339
|
+
for (const r of result_rows.rows) {
|
|
340
|
+
let nodes = nodesByLeaf.get(r.leaf_id);
|
|
341
|
+
if (!nodes) {
|
|
342
|
+
nodes = new Map();
|
|
343
|
+
nodesByLeaf.set(r.leaf_id, nodes);
|
|
344
|
+
}
|
|
345
|
+
nodes.set(r.data.id, r.data);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = [];
|
|
349
|
+
for (const id of leafIds) {
|
|
350
|
+
const nodes = nodesByLeaf.get(id);
|
|
351
|
+
if (!nodes) continue;
|
|
352
|
+
const pedigree = assembleExitChain(nodes, id);
|
|
353
|
+
if (pedigree) result.push(pedigree);
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error instanceof TreeStoreError) throw error;
|
|
358
|
+
throw new TreeStoreError(`Failed to get exit chains: ${error.message}`, error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
225
362
|
/**
|
|
226
363
|
* Get all leaves categorized by status.
|
|
227
364
|
* @returns {Promise<Object>} Leaves object with available, notAvailable, etc.
|
|
@@ -235,7 +372,7 @@ class PostgresTreeStore {
|
|
|
235
372
|
try {
|
|
236
373
|
const result = await this.pool.query(
|
|
237
374
|
`
|
|
238
|
-
SELECT COALESCE(SUM(
|
|
375
|
+
SELECT COALESCE(SUM(l.value), 0)::bigint AS balance
|
|
239
376
|
FROM brz_tree_leaves l
|
|
240
377
|
LEFT JOIN brz_tree_reservations r
|
|
241
378
|
ON l.reservation_id = r.id AND l.user_id = r.user_id
|
|
@@ -265,8 +402,8 @@ class PostgresTreeStore {
|
|
|
265
402
|
const result = await this.pool.query(
|
|
266
403
|
`
|
|
267
404
|
SELECT l.id AS id,
|
|
268
|
-
l.
|
|
269
|
-
l.
|
|
405
|
+
l.verifying_public_key AS verifying,
|
|
406
|
+
l.signing_public_key AS keyshare
|
|
270
407
|
FROM brz_tree_leaves l
|
|
271
408
|
LEFT JOIN brz_tree_reservations r
|
|
272
409
|
ON l.reservation_id = r.id AND l.user_id = r.user_id
|
|
@@ -306,6 +443,7 @@ class PostgresTreeStore {
|
|
|
306
443
|
|
|
307
444
|
for (const row of result.rows) {
|
|
308
445
|
const node = row.data;
|
|
446
|
+
const spendable = node.status === "Available";
|
|
309
447
|
|
|
310
448
|
if (row.purpose) {
|
|
311
449
|
if (row.purpose === "Payment") {
|
|
@@ -313,14 +451,12 @@ class PostgresTreeStore {
|
|
|
313
451
|
} else if (row.purpose === "Swap") {
|
|
314
452
|
reservedForSwap.push(node);
|
|
315
453
|
}
|
|
454
|
+
} else if (!spendable) {
|
|
455
|
+
notAvailable.push(node);
|
|
316
456
|
} else if (row.is_missing_from_operators) {
|
|
317
|
-
|
|
318
|
-
availableMissingFromOperators.push(node);
|
|
319
|
-
}
|
|
320
|
-
} else if (node.status === "Available") {
|
|
321
|
-
available.push(node);
|
|
457
|
+
availableMissingFromOperators.push(node);
|
|
322
458
|
} else {
|
|
323
|
-
|
|
459
|
+
available.push(node);
|
|
324
460
|
}
|
|
325
461
|
}
|
|
326
462
|
|
|
@@ -393,14 +529,29 @@ class PostgresTreeStore {
|
|
|
393
529
|
// Includes leaves released earlier in this transaction by
|
|
394
530
|
// _cleanupStaleReservations (which now NULLs reservation_id explicitly,
|
|
395
531
|
// since the composite FK uses NO ACTION).
|
|
396
|
-
await client.query(
|
|
397
|
-
"DELETE FROM brz_tree_leaves WHERE user_id = $1 AND reservation_id IS NULL AND added_at < $2",
|
|
532
|
+
const deleted = await client.query(
|
|
533
|
+
"DELETE FROM brz_tree_leaves WHERE user_id = $1 AND reservation_id IS NULL AND added_at < $2 RETURNING id",
|
|
398
534
|
[this.identity, refreshTimestamp]
|
|
399
535
|
);
|
|
400
536
|
|
|
401
537
|
// Upsert all leaves (filtering spent)
|
|
402
538
|
await this._batchUpsertLeaves(client, leaves, false, spentIds);
|
|
403
539
|
await this._batchUpsertLeaves(client, missingLeaves, true, spentIds);
|
|
540
|
+
|
|
541
|
+
// A leaf reported again in this same refresh is re-inserted above, so its
|
|
542
|
+
// ancestor rows must survive: only ids that do NOT reappear (truly gone,
|
|
543
|
+
// e.g. spent) get their ancestor rows dropped alongside them.
|
|
544
|
+
const survivingIds = new Set();
|
|
545
|
+
for (const leaf of leaves.concat(missingLeaves || [])) {
|
|
546
|
+
if (!spentIds.has(leaf.id)) survivingIds.add(leaf.id);
|
|
547
|
+
}
|
|
548
|
+
const goneIds = deleted.rows.map((r) => r.id).filter((id) => !survivingIds.has(id));
|
|
549
|
+
if (goneIds.length > 0) {
|
|
550
|
+
await client.query(
|
|
551
|
+
"DELETE FROM brz_tree_ancestors WHERE user_id = $1 AND leaf_id = ANY($2)",
|
|
552
|
+
[this.identity, goneIds]
|
|
553
|
+
);
|
|
554
|
+
}
|
|
404
555
|
});
|
|
405
556
|
} catch (error) {
|
|
406
557
|
if (error instanceof TreeStoreError) throw error;
|
|
@@ -427,14 +578,19 @@ class PostgresTreeStore {
|
|
|
427
578
|
async cancelReservation(id, leavesToKeep) {
|
|
428
579
|
try {
|
|
429
580
|
await this._withTransaction(async (client) => {
|
|
430
|
-
|
|
431
|
-
|
|
581
|
+
// Return leavesToKeep to the pool even when the reservation is already
|
|
582
|
+
// gone (e.g. released by stale cleanup): dropping them here would lose
|
|
583
|
+
// the leaves until the next refresh. The deletes no-op in that case.
|
|
584
|
+
// Only the leaves are re-inserted: a kept leaf's ancestor rows stay put
|
|
585
|
+
// (they are not touched below); a dropped leaf's are removed with it.
|
|
586
|
+
const keepIds = new Set((leavesToKeep || []).map((l) => l.id));
|
|
587
|
+
const reservedResult = await client.query(
|
|
588
|
+
"SELECT id FROM brz_tree_leaves WHERE user_id = $1 AND reservation_id = $2",
|
|
432
589
|
[this.identity, id]
|
|
433
590
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
591
|
+
const droppedIds = reservedResult.rows
|
|
592
|
+
.map((r) => r.id)
|
|
593
|
+
.filter((rid) => !keepIds.has(rid));
|
|
438
594
|
|
|
439
595
|
await client.query(
|
|
440
596
|
"DELETE FROM brz_tree_leaves WHERE user_id = $1 AND reservation_id = $2",
|
|
@@ -446,6 +602,13 @@ class PostgresTreeStore {
|
|
|
446
602
|
[this.identity, id]
|
|
447
603
|
);
|
|
448
604
|
|
|
605
|
+
if (droppedIds.length > 0) {
|
|
606
|
+
await client.query(
|
|
607
|
+
"DELETE FROM brz_tree_ancestors WHERE user_id = $1 AND leaf_id = ANY($2)",
|
|
608
|
+
[this.identity, droppedIds]
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
449
612
|
if (leavesToKeep && leavesToKeep.length > 0) {
|
|
450
613
|
await this._batchUpsertLeaves(client, leavesToKeep, false, null);
|
|
451
614
|
}
|
|
@@ -495,6 +658,14 @@ class PostgresTreeStore {
|
|
|
495
658
|
"DELETE FROM brz_tree_reservations WHERE user_id = $1 AND id = $2",
|
|
496
659
|
[this.identity, id]
|
|
497
660
|
);
|
|
661
|
+
// The spent leaves own these ancestor rows; remove them in the same
|
|
662
|
+
// transaction rather than leaving them to a separate reclaim pass.
|
|
663
|
+
if (reservedLeafIds.length > 0) {
|
|
664
|
+
await client.query(
|
|
665
|
+
"DELETE FROM brz_tree_ancestors WHERE user_id = $1 AND leaf_id = ANY($2)",
|
|
666
|
+
[this.identity, reservedLeafIds]
|
|
667
|
+
);
|
|
668
|
+
}
|
|
498
669
|
}
|
|
499
670
|
|
|
500
671
|
// Add new leaves if provided
|
|
@@ -541,7 +712,7 @@ class PostgresTreeStore {
|
|
|
541
712
|
// from the prefiltered set since the prefilter may exclude big leaves.
|
|
542
713
|
const totalResult = await client.query(
|
|
543
714
|
`
|
|
544
|
-
SELECT COALESCE(SUM(
|
|
715
|
+
SELECT COALESCE(SUM(value), 0)::bigint AS total
|
|
545
716
|
FROM brz_tree_leaves
|
|
546
717
|
WHERE user_id = $1
|
|
547
718
|
AND status = 'Available'
|
|
@@ -802,6 +973,14 @@ class PostgresTreeStore {
|
|
|
802
973
|
"DELETE FROM brz_tree_leaves WHERE user_id = $1 AND reservation_id = $2",
|
|
803
974
|
[this.identity, reservationId]
|
|
804
975
|
);
|
|
976
|
+
// The spent leaves own these ancestor rows; remove them now since there
|
|
977
|
+
// is no later reclaim pass.
|
|
978
|
+
if (oldLeafIds.length > 0) {
|
|
979
|
+
await client.query(
|
|
980
|
+
"DELETE FROM brz_tree_ancestors WHERE user_id = $1 AND leaf_id = ANY($2)",
|
|
981
|
+
[this.identity, oldLeafIds]
|
|
982
|
+
);
|
|
983
|
+
}
|
|
805
984
|
|
|
806
985
|
// Upsert change leaves to available pool
|
|
807
986
|
await this._batchUpsertLeaves(client, changeLeaves, false, null);
|
|
@@ -819,6 +998,8 @@ class PostgresTreeStore {
|
|
|
819
998
|
[this.identity, reservationId]
|
|
820
999
|
);
|
|
821
1000
|
|
|
1001
|
+
// Return value must be plain TreeNodes: the Rust side deserializes
|
|
1002
|
+
// Vec<TreeNode>.
|
|
822
1003
|
return {
|
|
823
1004
|
id: reservationId,
|
|
824
1005
|
leaves: reservedLeaves,
|
|
@@ -1023,37 +1204,117 @@ class PostgresTreeStore {
|
|
|
1023
1204
|
async _batchUpsertLeaves(client, leaves, isMissingFromOperators, skipIds) {
|
|
1024
1205
|
if (!leaves || leaves.length === 0) return;
|
|
1025
1206
|
|
|
1026
|
-
const
|
|
1207
|
+
const leafNodes = skipIds
|
|
1027
1208
|
? leaves.filter((l) => !skipIds.has(l.id))
|
|
1028
1209
|
: leaves;
|
|
1029
1210
|
|
|
1030
|
-
if (
|
|
1211
|
+
if (leafNodes.length === 0) return;
|
|
1212
|
+
|
|
1031
1213
|
|
|
1032
1214
|
// All chunks run inside the caller's transaction, and NOW() is the
|
|
1033
1215
|
// transaction timestamp, so every row still lands atomically with one
|
|
1034
1216
|
// shared added_at.
|
|
1035
|
-
for (let i = 0; i <
|
|
1036
|
-
const chunk =
|
|
1217
|
+
for (let i = 0; i < leafNodes.length; i += LEAF_UPSERT_CHUNK_SIZE) {
|
|
1218
|
+
const chunk = leafNodes.slice(i, i + LEAF_UPSERT_CHUNK_SIZE);
|
|
1037
1219
|
const ids = chunk.map((l) => l.id);
|
|
1038
1220
|
const statuses = chunk.map((l) => l.status);
|
|
1039
1221
|
const missingFlags = chunk.map(() => isMissingFromOperators);
|
|
1040
1222
|
const dataValues = chunk.map((l) => JSON.stringify(l));
|
|
1223
|
+
const values = chunk.map((l) => l.value);
|
|
1224
|
+
const parents = chunk.map((l) => l.parent_node_id ?? null);
|
|
1225
|
+
const verifyings = chunk.map((l) => l.verifying_public_key);
|
|
1226
|
+
const signings = chunk.map((l) => l.signing_keyshare.public_key);
|
|
1041
1227
|
|
|
1042
1228
|
await client.query(
|
|
1043
|
-
`INSERT INTO brz_tree_leaves
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1229
|
+
`INSERT INTO brz_tree_leaves
|
|
1230
|
+
(user_id, id, status, is_missing_from_operators, data, added_at,
|
|
1231
|
+
value, parent_node_id, verifying_public_key, signing_public_key)
|
|
1232
|
+
SELECT $5, id, status, missing, data::jsonb, NOW(),
|
|
1233
|
+
value, parent_node_id, verifying, signing
|
|
1234
|
+
FROM UNNEST($1::text[], $2::text[], $3::bool[], $4::text[],
|
|
1235
|
+
$6::bigint[], $7::text[], $8::text[], $9::text[])
|
|
1236
|
+
AS t(id, status, missing, data, value, parent_node_id, verifying, signing)
|
|
1047
1237
|
ON CONFLICT (user_id, id) DO UPDATE SET
|
|
1048
1238
|
status = EXCLUDED.status,
|
|
1049
1239
|
is_missing_from_operators = EXCLUDED.is_missing_from_operators,
|
|
1050
1240
|
data = EXCLUDED.data,
|
|
1051
|
-
added_at = NOW()
|
|
1052
|
-
|
|
1241
|
+
added_at = NOW(),
|
|
1242
|
+
value = EXCLUDED.value,
|
|
1243
|
+
parent_node_id = EXCLUDED.parent_node_id,
|
|
1244
|
+
verifying_public_key = EXCLUDED.verifying_public_key,
|
|
1245
|
+
signing_public_key = EXCLUDED.signing_public_key`,
|
|
1246
|
+
[
|
|
1247
|
+
ids,
|
|
1248
|
+
statuses,
|
|
1249
|
+
missingFlags,
|
|
1250
|
+
dataValues,
|
|
1251
|
+
this.identity,
|
|
1252
|
+
values,
|
|
1253
|
+
parents,
|
|
1254
|
+
verifyings,
|
|
1255
|
+
signings,
|
|
1256
|
+
]
|
|
1053
1257
|
);
|
|
1054
1258
|
}
|
|
1055
1259
|
}
|
|
1056
1260
|
|
|
1261
|
+
/**
|
|
1262
|
+
* Replaces the ancestor rows of every pedigree wholesale (delete then
|
|
1263
|
+
* insert), in two statements however many leaves there are. A pedigree
|
|
1264
|
+
* carrying no ancestors is skipped: an empty list means the chain is unknown,
|
|
1265
|
+
* not that the leaf has none, so a stored chain must survive being re-added
|
|
1266
|
+
* without one.
|
|
1267
|
+
*/
|
|
1268
|
+
async _batchUpsertAncestors(client, pedigrees) {
|
|
1269
|
+
const withAncestors = (pedigrees || []).filter(
|
|
1270
|
+
(p) => p.ancestors && p.ancestors.length > 0
|
|
1271
|
+
);
|
|
1272
|
+
if (withAncestors.length === 0) return;
|
|
1273
|
+
|
|
1274
|
+
await client.query(
|
|
1275
|
+
"DELETE FROM brz_tree_ancestors WHERE user_id = $1 AND leaf_id = ANY($2)",
|
|
1276
|
+
[this.identity, withAncestors.map((p) => p.leaf.id)]
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
const leafIds = [];
|
|
1280
|
+
const ids = [];
|
|
1281
|
+
const parents = [];
|
|
1282
|
+
const statuses = [];
|
|
1283
|
+
const dataValues = [];
|
|
1284
|
+
const values = [];
|
|
1285
|
+
const verifyings = [];
|
|
1286
|
+
for (const pedigree of withAncestors) {
|
|
1287
|
+
for (const node of pedigree.ancestors) {
|
|
1288
|
+
leafIds.push(pedigree.leaf.id);
|
|
1289
|
+
ids.push(node.id);
|
|
1290
|
+
parents.push(node.parent_node_id ?? null);
|
|
1291
|
+
statuses.push(node.status);
|
|
1292
|
+
dataValues.push(JSON.stringify(node));
|
|
1293
|
+
values.push(node.value);
|
|
1294
|
+
verifyings.push(node.verifying_public_key);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
await client.query(
|
|
1299
|
+
`INSERT INTO brz_tree_ancestors
|
|
1300
|
+
(user_id, leaf_id, id, parent_node_id, status, data, value, verifying_public_key)
|
|
1301
|
+
SELECT $1, leaf_id, id, parent_node_id, status, data::jsonb, value, verifying
|
|
1302
|
+
FROM UNNEST($2::text[], $3::text[], $4::text[], $5::text[], $6::text[],
|
|
1303
|
+
$7::bigint[], $8::text[])
|
|
1304
|
+
AS t(leaf_id, id, parent_node_id, status, data, value, verifying)`,
|
|
1305
|
+
[
|
|
1306
|
+
this.identity,
|
|
1307
|
+
leafIds,
|
|
1308
|
+
ids,
|
|
1309
|
+
parents,
|
|
1310
|
+
statuses,
|
|
1311
|
+
dataValues,
|
|
1312
|
+
values,
|
|
1313
|
+
verifyings,
|
|
1314
|
+
]
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1057
1318
|
/**
|
|
1058
1319
|
* Batch set reservation_id on leaves.
|
|
1059
1320
|
*/
|
|
@@ -304,6 +304,48 @@ class TreeStoreMigrationManager {
|
|
|
304
304
|
ADD PRIMARY KEY (user_id)`,
|
|
305
305
|
],
|
|
306
306
|
},
|
|
307
|
+
{
|
|
308
|
+
// Mirrors Rust migration 4 in spark-postgres/src/tree_store.rs.
|
|
309
|
+
// Ancestor chain: intermediate nodes a leaf's exit chain walks through,
|
|
310
|
+
// kept separate from the spendable leaf pool and carrying no pool
|
|
311
|
+
// metadata. Each row is owned by the leaf it belongs to (`leaf_id` is
|
|
312
|
+
// part of the primary key), so a node shared by several leaves' chains
|
|
313
|
+
// stores one row per leaf rather than one deduplicated row. Multi-tenant
|
|
314
|
+
// from creation.
|
|
315
|
+
name: "Add ancestor chain table",
|
|
316
|
+
sql: [
|
|
317
|
+
`CREATE TABLE IF NOT EXISTS brz_tree_ancestors (
|
|
318
|
+
user_id BYTEA NOT NULL,
|
|
319
|
+
leaf_id TEXT NOT NULL,
|
|
320
|
+
id TEXT NOT NULL,
|
|
321
|
+
parent_node_id TEXT,
|
|
322
|
+
status TEXT NOT NULL,
|
|
323
|
+
value BIGINT NOT NULL DEFAULT 0,
|
|
324
|
+
verifying_public_key TEXT NOT NULL DEFAULT '',
|
|
325
|
+
data JSONB NOT NULL,
|
|
326
|
+
PRIMARY KEY (user_id, leaf_id, id)
|
|
327
|
+
)`,
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
// Promote the four JSON fields that queries pull out of `data` into
|
|
332
|
+
// dedicated columns (value, parent_node_id, verifying_public_key,
|
|
333
|
+
// signing_public_key), then backfill from the existing blob. Balance,
|
|
334
|
+
// selection, exit-chain, and key-verification queries read the columns
|
|
335
|
+
// instead of re-parsing JSON. Everything else stays in `data`.
|
|
336
|
+
name: "Promote leaf JSON fields to columns",
|
|
337
|
+
sql: [
|
|
338
|
+
`ALTER TABLE brz_tree_leaves ADD COLUMN IF NOT EXISTS value BIGINT NOT NULL DEFAULT 0`,
|
|
339
|
+
`ALTER TABLE brz_tree_leaves ADD COLUMN IF NOT EXISTS parent_node_id TEXT`,
|
|
340
|
+
`ALTER TABLE brz_tree_leaves ADD COLUMN IF NOT EXISTS verifying_public_key TEXT NOT NULL DEFAULT ''`,
|
|
341
|
+
`ALTER TABLE brz_tree_leaves ADD COLUMN IF NOT EXISTS signing_public_key TEXT NOT NULL DEFAULT ''`,
|
|
342
|
+
`UPDATE brz_tree_leaves SET
|
|
343
|
+
value = (data->>'value')::bigint,
|
|
344
|
+
parent_node_id = data->>'parent_node_id',
|
|
345
|
+
verifying_public_key = data->>'verifying_public_key',
|
|
346
|
+
signing_public_key = data->'signing_keyshare'->>'public_key'`,
|
|
347
|
+
],
|
|
348
|
+
},
|
|
307
349
|
];
|
|
308
350
|
}
|
|
309
351
|
}
|
package/nodejs/storage/index.cjs
CHANGED
|
@@ -700,7 +700,7 @@ class SqliteStorage {
|
|
|
700
700
|
listDeposits() {
|
|
701
701
|
try {
|
|
702
702
|
const stmt = this.db.prepare(`
|
|
703
|
-
SELECT txid, vout, amount_sats, is_mature, claim_error, refund_tx, refund_tx_id
|
|
703
|
+
SELECT txid, vout, amount_sats, is_mature, claim_error, refund_tx, refund_tx_id, instant_claim_status
|
|
704
704
|
FROM unclaimed_deposits
|
|
705
705
|
`);
|
|
706
706
|
|
|
@@ -714,6 +714,9 @@ class SqliteStorage {
|
|
|
714
714
|
claimError: row.claim_error ? JSON.parse(row.claim_error) : null,
|
|
715
715
|
refundTx: row.refund_tx,
|
|
716
716
|
refundTxId: row.refund_tx_id,
|
|
717
|
+
instantClaimStatus: row.instant_claim_status
|
|
718
|
+
? JSON.parse(row.instant_claim_status)
|
|
719
|
+
: null,
|
|
717
720
|
}))
|
|
718
721
|
);
|
|
719
722
|
} catch (error) {
|
|
@@ -735,12 +738,20 @@ class SqliteStorage {
|
|
|
735
738
|
stmt.run(JSON.stringify(payload.error), txid, vout);
|
|
736
739
|
} else if (payload.type === "refund") {
|
|
737
740
|
const stmt = this.db.prepare(`
|
|
738
|
-
UPDATE unclaimed_deposits
|
|
739
|
-
SET refund_tx = ?, refund_tx_id = ?, claim_error = NULL
|
|
741
|
+
UPDATE unclaimed_deposits
|
|
742
|
+
SET refund_tx = ?, refund_tx_id = ?, claim_error = NULL
|
|
740
743
|
WHERE txid = ? AND vout = ?
|
|
741
744
|
`);
|
|
742
745
|
|
|
743
746
|
stmt.run(payload.refundTx, payload.refundTxid, txid, vout);
|
|
747
|
+
} else if (payload.type === "instantClaim") {
|
|
748
|
+
const stmt = this.db.prepare(`
|
|
749
|
+
UPDATE unclaimed_deposits
|
|
750
|
+
SET instant_claim_status = ?
|
|
751
|
+
WHERE txid = ? AND vout = ?
|
|
752
|
+
`);
|
|
753
|
+
|
|
754
|
+
stmt.run(JSON.stringify(payload.status), txid, vout);
|
|
744
755
|
} else {
|
|
745
756
|
return Promise.reject(
|
|
746
757
|
new StorageError(`Unknown payload type: ${payload.type}`)
|
|
@@ -480,6 +480,12 @@ class MigrationManager {
|
|
|
480
480
|
ON cross_chain_swaps(provider, is_terminal)`,
|
|
481
481
|
],
|
|
482
482
|
},
|
|
483
|
+
{
|
|
484
|
+
name: "Add instant claim status to unclaimed_deposits",
|
|
485
|
+
sql: [
|
|
486
|
+
`ALTER TABLE unclaimed_deposits ADD COLUMN instant_claim_status TEXT`,
|
|
487
|
+
],
|
|
488
|
+
},
|
|
483
489
|
];
|
|
484
490
|
}
|
|
485
491
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// errors.cjs - Tree store error wrapper with cause chain support
|
|
2
|
+
class TreeStoreError extends Error {
|
|
3
|
+
constructor(message, cause = null) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'TreeStoreError';
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
if (Error.captureStackTrace) {
|
|
8
|
+
Error.captureStackTrace(this, TreeStoreError);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { TreeStoreError };
|