@breeztech/breez-sdk-spark 0.13.9-debug → 0.13.11-dev1
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 +1113 -1050
- package/bundler/breez_sdk_spark_wasm.js +5 -1
- package/bundler/breez_sdk_spark_wasm_bg.js +1493 -1628
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +14 -6
- package/deno/breez_sdk_spark_wasm.d.ts +1113 -1050
- package/deno/breez_sdk_spark_wasm.js +1394 -1284
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +14 -6
- package/nodejs/breez_sdk_spark_wasm.d.ts +1113 -1050
- package/nodejs/breez_sdk_spark_wasm.js +2527 -2654
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +14 -6
- package/nodejs/index.js +34 -0
- package/nodejs/index.mjs +5 -4
- package/nodejs/mysql-storage/errors.cjs +19 -0
- package/nodejs/mysql-storage/index.cjs +1366 -0
- package/nodejs/mysql-storage/migrations.cjs +387 -0
- package/nodejs/mysql-storage/package.json +9 -0
- package/nodejs/mysql-token-store/errors.cjs +9 -0
- package/nodejs/mysql-token-store/index.cjs +988 -0
- package/nodejs/mysql-token-store/migrations.cjs +255 -0
- package/nodejs/mysql-token-store/package.json +9 -0
- package/nodejs/mysql-tree-store/errors.cjs +9 -0
- package/nodejs/mysql-tree-store/index.cjs +939 -0
- package/nodejs/mysql-tree-store/migrations.cjs +221 -0
- package/nodejs/mysql-tree-store/package.json +9 -0
- package/nodejs/package.json +3 -0
- package/nodejs/postgres-storage/index.cjs +147 -92
- package/nodejs/postgres-storage/migrations.cjs +85 -4
- package/nodejs/postgres-token-store/index.cjs +186 -101
- package/nodejs/postgres-token-store/migrations.cjs +92 -3
- package/nodejs/postgres-tree-store/index.cjs +177 -93
- package/nodejs/postgres-tree-store/migrations.cjs +80 -3
- package/package.json +1 -1
- package/ssr/index.js +19 -14
- package/web/breez_sdk_spark_wasm.d.ts +1267 -1195
- package/web/breez_sdk_spark_wasm.js +2295 -2169
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +14 -6
|
@@ -25,10 +25,10 @@ const { TokenStoreError } = require("./errors.cjs");
|
|
|
25
25
|
const { TokenStoreMigrationManager } = require("./migrations.cjs");
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Domain prefix mixed into the per-tenant advisory-lock key. Distinct prefixes
|
|
29
|
+
* guarantee that locks from different stores (tree, token, …) never collide.
|
|
30
30
|
*/
|
|
31
|
-
const
|
|
31
|
+
const TOKEN_STORE_LOCK_PREFIX = "breez-spark-sdk:token:";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Spent markers are kept for this duration to support multiple SDK instances.
|
|
@@ -42,9 +42,37 @@ const SPENT_MARKER_CLEANUP_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
42
42
|
*/
|
|
43
43
|
const RESERVATION_TIMEOUT_SECS = 300; // 5 minutes
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Derive a stable per-tenant 64-bit advisory-lock key by hashing a domain
|
|
47
|
+
* prefix together with the identity pubkey and folding the first 8 bytes of
|
|
48
|
+
* the SHA-256 digest into a signed big-endian i64 — the type expected by
|
|
49
|
+
* `pg_advisory_xact_lock(bigint)`. The 64-bit space keeps cross-tenant
|
|
50
|
+
* collisions negligible (~1.2e-10 at 65k tenants).
|
|
51
|
+
*/
|
|
52
|
+
function _identityLockKey(prefix, identity) {
|
|
53
|
+
const crypto = require("crypto");
|
|
54
|
+
const hash = crypto.createHash("sha256");
|
|
55
|
+
hash.update(prefix);
|
|
56
|
+
hash.update(Buffer.from(identity));
|
|
57
|
+
return hash.digest().readBigInt64BE(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
class PostgresTokenStore {
|
|
46
|
-
|
|
61
|
+
/**
|
|
62
|
+
* @param {import('pg').Pool} pool
|
|
63
|
+
* @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey
|
|
64
|
+
* identifying the tenant. All reads and writes are scoped by this.
|
|
65
|
+
* @param {object} [logger]
|
|
66
|
+
*/
|
|
67
|
+
constructor(pool, identity, logger = null) {
|
|
68
|
+
if (!identity || identity.length !== 33) {
|
|
69
|
+
throw new TokenStoreError(
|
|
70
|
+
"tenant identity (33-byte secp256k1 pubkey) is required"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
47
73
|
this.pool = pool;
|
|
74
|
+
this.identity = Buffer.from(identity);
|
|
75
|
+
this.lockKey = _identityLockKey(TOKEN_STORE_LOCK_PREFIX, identity);
|
|
48
76
|
this.logger = logger;
|
|
49
77
|
}
|
|
50
78
|
|
|
@@ -54,7 +82,7 @@ class PostgresTokenStore {
|
|
|
54
82
|
async initialize() {
|
|
55
83
|
try {
|
|
56
84
|
const migrationManager = new TokenStoreMigrationManager(this.logger);
|
|
57
|
-
await migrationManager.migrate(this.pool);
|
|
85
|
+
await migrationManager.migrate(this.pool, this.identity);
|
|
58
86
|
return this;
|
|
59
87
|
} catch (error) {
|
|
60
88
|
throw new TokenStoreError(
|
|
@@ -86,7 +114,10 @@ class PostgresTokenStore {
|
|
|
86
114
|
const client = await this.pool.connect();
|
|
87
115
|
try {
|
|
88
116
|
await client.query("BEGIN");
|
|
89
|
-
|
|
117
|
+
// Per-tenant advisory lock: 64-bit key derived from a token-store domain
|
|
118
|
+
// prefix and the tenant identity, so different tenants don't serialize
|
|
119
|
+
// on each other and tree/token locks never collide.
|
|
120
|
+
await client.query("SELECT pg_advisory_xact_lock($1)", [this.lockKey]);
|
|
90
121
|
const result = await fn(client);
|
|
91
122
|
await client.query("COMMIT");
|
|
92
123
|
return result;
|
|
@@ -100,9 +131,9 @@ class PostgresTokenStore {
|
|
|
100
131
|
|
|
101
132
|
/**
|
|
102
133
|
* Run a function inside a transaction without the advisory lock. Used by
|
|
103
|
-
* operations scoped to a single reservation_id (`cancelReservation
|
|
104
|
-
*
|
|
105
|
-
*
|
|
134
|
+
* operations scoped to a single reservation_id (`cancelReservation`)
|
|
135
|
+
* where MVCC + row-level locks suffice and the global lock would only add
|
|
136
|
+
* contention.
|
|
106
137
|
* @param {function(import('pg').PoolClient): Promise<T>} fn
|
|
107
138
|
* @returns {Promise<T>}
|
|
108
139
|
* @template T
|
|
@@ -144,9 +175,16 @@ class PostgresTokenStore {
|
|
|
144
175
|
// Skip if swap is active or completed during this refresh
|
|
145
176
|
const swapCheckResult = await client.query(
|
|
146
177
|
`SELECT
|
|
147
|
-
EXISTS(
|
|
148
|
-
|
|
149
|
-
|
|
178
|
+
EXISTS(
|
|
179
|
+
SELECT 1 FROM token_reservations
|
|
180
|
+
WHERE user_id = $1 AND purpose = 'Swap'
|
|
181
|
+
) AS has_active_swap,
|
|
182
|
+
COALESCE(
|
|
183
|
+
(SELECT last_completed_at >= $2
|
|
184
|
+
FROM token_swap_status WHERE user_id = $1),
|
|
185
|
+
FALSE
|
|
186
|
+
) AS swap_completed`,
|
|
187
|
+
[this.identity, refreshTimestamp]
|
|
150
188
|
);
|
|
151
189
|
const { has_active_swap, swap_completed } = swapCheckResult.rows[0];
|
|
152
190
|
if (has_active_swap || swap_completed) {
|
|
@@ -156,21 +194,21 @@ class PostgresTokenStore {
|
|
|
156
194
|
// Clean up old spent markers
|
|
157
195
|
const cleanupCutoff = new Date(refreshTimestamp.getTime() - SPENT_MARKER_CLEANUP_THRESHOLD_MS);
|
|
158
196
|
await client.query(
|
|
159
|
-
"DELETE FROM token_spent_outputs WHERE spent_at < $
|
|
160
|
-
[cleanupCutoff]
|
|
197
|
+
"DELETE FROM token_spent_outputs WHERE user_id = $1 AND spent_at < $2",
|
|
198
|
+
[this.identity, cleanupCutoff]
|
|
161
199
|
);
|
|
162
200
|
|
|
163
201
|
// Get recent spent output IDs (spent_at >= refresh_timestamp)
|
|
164
202
|
const spentResult = await client.query(
|
|
165
|
-
"SELECT output_id FROM token_spent_outputs WHERE spent_at >= $
|
|
166
|
-
[refreshTimestamp]
|
|
203
|
+
"SELECT output_id FROM token_spent_outputs WHERE user_id = $1 AND spent_at >= $2",
|
|
204
|
+
[this.identity, refreshTimestamp]
|
|
167
205
|
);
|
|
168
206
|
const spentIds = new Set(spentResult.rows.map((r) => r.output_id));
|
|
169
207
|
|
|
170
208
|
// Delete non-reserved outputs added BEFORE the refresh started
|
|
171
209
|
await client.query(
|
|
172
|
-
"DELETE FROM token_outputs WHERE reservation_id IS NULL AND added_at < $
|
|
173
|
-
[refreshTimestamp]
|
|
210
|
+
"DELETE FROM token_outputs WHERE user_id = $1 AND reservation_id IS NULL AND added_at < $2",
|
|
211
|
+
[this.identity, refreshTimestamp]
|
|
174
212
|
);
|
|
175
213
|
|
|
176
214
|
// Build a set of all incoming output IDs for reconciliation
|
|
@@ -185,7 +223,10 @@ class PostgresTokenStore {
|
|
|
185
223
|
const reservedRows = await client.query(
|
|
186
224
|
`SELECT r.id, o.id AS output_id
|
|
187
225
|
FROM token_reservations r
|
|
188
|
-
JOIN token_outputs o
|
|
226
|
+
JOIN token_outputs o
|
|
227
|
+
ON o.reservation_id = r.id AND o.user_id = r.user_id
|
|
228
|
+
WHERE r.user_id = $1`,
|
|
229
|
+
[this.identity]
|
|
189
230
|
);
|
|
190
231
|
|
|
191
232
|
// Group reserved outputs by reservation ID
|
|
@@ -216,51 +257,57 @@ class PostgresTokenStore {
|
|
|
216
257
|
// Delete outputs whose reservations are being removed entirely
|
|
217
258
|
if (reservationsToDelete.length > 0) {
|
|
218
259
|
await client.query(
|
|
219
|
-
"DELETE FROM token_outputs WHERE reservation_id = ANY($
|
|
220
|
-
[reservationsToDelete]
|
|
260
|
+
"DELETE FROM token_outputs WHERE user_id = $1 AND reservation_id = ANY($2)",
|
|
261
|
+
[this.identity, reservationsToDelete]
|
|
221
262
|
);
|
|
222
263
|
await client.query(
|
|
223
|
-
"DELETE FROM token_reservations WHERE id = ANY($
|
|
224
|
-
[reservationsToDelete]
|
|
264
|
+
"DELETE FROM token_reservations WHERE user_id = $1 AND id = ANY($2)",
|
|
265
|
+
[this.identity, reservationsToDelete]
|
|
225
266
|
);
|
|
226
267
|
}
|
|
227
268
|
|
|
228
269
|
// Delete individual reserved outputs that no longer exist
|
|
229
270
|
if (outputsToRemoveFromReservation.length > 0) {
|
|
230
271
|
await client.query(
|
|
231
|
-
"DELETE FROM token_outputs WHERE id = ANY($
|
|
232
|
-
[outputsToRemoveFromReservation]
|
|
272
|
+
"DELETE FROM token_outputs WHERE user_id = $1 AND id = ANY($2)",
|
|
273
|
+
[this.identity, outputsToRemoveFromReservation]
|
|
233
274
|
);
|
|
234
275
|
|
|
235
276
|
// Check if any reservations are now empty
|
|
236
277
|
const emptyReservations = await client.query(
|
|
237
278
|
`SELECT r.id FROM token_reservations r
|
|
238
|
-
LEFT JOIN token_outputs o
|
|
239
|
-
|
|
279
|
+
LEFT JOIN token_outputs o
|
|
280
|
+
ON o.reservation_id = r.id AND o.user_id = r.user_id
|
|
281
|
+
WHERE r.user_id = $1 AND o.id IS NULL`,
|
|
282
|
+
[this.identity]
|
|
240
283
|
);
|
|
241
284
|
const emptyIds = emptyReservations.rows.map((r) => r.id);
|
|
242
285
|
if (emptyIds.length > 0) {
|
|
243
286
|
await client.query(
|
|
244
|
-
"DELETE FROM token_reservations WHERE id = ANY($
|
|
245
|
-
[emptyIds]
|
|
287
|
+
"DELETE FROM token_reservations WHERE user_id = $1 AND id = ANY($2)",
|
|
288
|
+
[this.identity, emptyIds]
|
|
246
289
|
);
|
|
247
290
|
}
|
|
248
291
|
}
|
|
249
292
|
|
|
250
293
|
// Collect IDs of currently reserved outputs (that survived reconciliation)
|
|
251
294
|
const reservedOutputIdsResult = await client.query(
|
|
252
|
-
"SELECT id FROM token_outputs WHERE reservation_id IS NOT NULL"
|
|
295
|
+
"SELECT id FROM token_outputs WHERE user_id = $1 AND reservation_id IS NOT NULL",
|
|
296
|
+
[this.identity]
|
|
253
297
|
);
|
|
254
298
|
const reservedOutputIds = new Set(
|
|
255
299
|
reservedOutputIdsResult.rows.map((r) => r.id)
|
|
256
300
|
);
|
|
257
301
|
|
|
258
|
-
// Delete orphan metadata
|
|
302
|
+
// Delete orphan metadata (per-tenant)
|
|
259
303
|
await client.query(
|
|
260
304
|
`DELETE FROM token_metadata
|
|
261
|
-
WHERE
|
|
262
|
-
|
|
263
|
-
|
|
305
|
+
WHERE user_id = $1
|
|
306
|
+
AND identifier NOT IN (
|
|
307
|
+
SELECT DISTINCT token_identifier
|
|
308
|
+
FROM token_outputs WHERE user_id = $1
|
|
309
|
+
)`,
|
|
310
|
+
[this.identity]
|
|
264
311
|
);
|
|
265
312
|
|
|
266
313
|
// Insert new metadata and outputs, excluding spent and reserved
|
|
@@ -295,12 +342,14 @@ class PostgresTokenStore {
|
|
|
295
342
|
/**
|
|
296
343
|
* Returns the spendable per-token balances aggregated server-side.
|
|
297
344
|
* Each entry includes full token metadata + the available + swap-reserved sum.
|
|
298
|
-
*
|
|
345
|
+
* Matches the in-memory default impl which returns all tokens that have
|
|
346
|
+
* at least one output (including zero spendable balance).
|
|
299
347
|
* @returns {Promise<Array<{metadata: object, balance: string}>>}
|
|
300
348
|
*/
|
|
301
349
|
async getTokenBalances() {
|
|
302
350
|
try {
|
|
303
|
-
const result = await this.pool.query(
|
|
351
|
+
const result = await this.pool.query(
|
|
352
|
+
`
|
|
304
353
|
SELECT m.identifier, m.issuer_public_key, m.name, m.ticker, m.decimals,
|
|
305
354
|
m.max_supply, m.is_freezable, m.creation_entity_public_key,
|
|
306
355
|
COALESCE(SUM(
|
|
@@ -311,18 +360,16 @@ class PostgresTokenStore {
|
|
|
311
360
|
END
|
|
312
361
|
), 0)::text AS balance
|
|
313
362
|
FROM token_metadata m
|
|
314
|
-
JOIN token_outputs o
|
|
315
|
-
|
|
363
|
+
JOIN token_outputs o
|
|
364
|
+
ON o.token_identifier = m.identifier AND o.user_id = m.user_id
|
|
365
|
+
LEFT JOIN token_reservations r
|
|
366
|
+
ON o.reservation_id = r.id AND o.user_id = r.user_id
|
|
367
|
+
WHERE m.user_id = $1
|
|
316
368
|
GROUP BY m.identifier, m.issuer_public_key, m.name, m.ticker,
|
|
317
369
|
m.decimals, m.max_supply, m.is_freezable, m.creation_entity_public_key
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
WHEN r.purpose = 'Swap' THEN o.token_amount::numeric
|
|
322
|
-
ELSE 0
|
|
323
|
-
END
|
|
324
|
-
), 0) > 0
|
|
325
|
-
`);
|
|
370
|
+
`,
|
|
371
|
+
[this.identity]
|
|
372
|
+
);
|
|
326
373
|
return result.rows.map((row) => ({
|
|
327
374
|
metadata: {
|
|
328
375
|
identifier: row.identifier,
|
|
@@ -355,9 +402,13 @@ class PostgresTokenStore {
|
|
|
355
402
|
o.prev_tx_hash, o.prev_tx_vout, o.reservation_id,
|
|
356
403
|
r.purpose
|
|
357
404
|
FROM token_metadata m
|
|
358
|
-
LEFT JOIN token_outputs o
|
|
359
|
-
|
|
360
|
-
|
|
405
|
+
LEFT JOIN token_outputs o
|
|
406
|
+
ON o.token_identifier = m.identifier AND o.user_id = m.user_id
|
|
407
|
+
LEFT JOIN token_reservations r
|
|
408
|
+
ON o.reservation_id = r.id AND o.user_id = r.user_id
|
|
409
|
+
WHERE m.user_id = $1
|
|
410
|
+
ORDER BY m.identifier, o.token_amount::NUMERIC ASC`,
|
|
411
|
+
[this.identity]
|
|
361
412
|
);
|
|
362
413
|
|
|
363
414
|
const map = new Map();
|
|
@@ -428,11 +479,13 @@ class PostgresTokenStore {
|
|
|
428
479
|
o.prev_tx_hash, o.prev_tx_vout, o.reservation_id,
|
|
429
480
|
r.purpose
|
|
430
481
|
FROM token_metadata m
|
|
431
|
-
LEFT JOIN token_outputs o
|
|
432
|
-
|
|
433
|
-
|
|
482
|
+
LEFT JOIN token_outputs o
|
|
483
|
+
ON o.token_identifier = m.identifier AND o.user_id = m.user_id
|
|
484
|
+
LEFT JOIN token_reservations r
|
|
485
|
+
ON o.reservation_id = r.id AND o.user_id = r.user_id
|
|
486
|
+
WHERE m.user_id = $2 AND ${whereClause}
|
|
434
487
|
ORDER BY o.token_amount::NUMERIC ASC`,
|
|
435
|
-
[param]
|
|
488
|
+
[param, this.identity]
|
|
436
489
|
);
|
|
437
490
|
|
|
438
491
|
if (result.rows.length === 0) {
|
|
@@ -489,8 +542,8 @@ class PostgresTokenStore {
|
|
|
489
542
|
const outputIds = tokenOutputs.outputs.map((o) => o.output.id);
|
|
490
543
|
if (outputIds.length > 0) {
|
|
491
544
|
await client.query(
|
|
492
|
-
"DELETE FROM token_spent_outputs WHERE output_id = ANY($
|
|
493
|
-
[outputIds]
|
|
545
|
+
"DELETE FROM token_spent_outputs WHERE user_id = $1 AND output_id = ANY($2)",
|
|
546
|
+
[this.identity, outputIds]
|
|
494
547
|
);
|
|
495
548
|
}
|
|
496
549
|
|
|
@@ -550,8 +603,8 @@ class PostgresTokenStore {
|
|
|
550
603
|
|
|
551
604
|
// Get metadata
|
|
552
605
|
const metadataResult = await client.query(
|
|
553
|
-
"SELECT * FROM token_metadata WHERE identifier = $
|
|
554
|
-
[tokenIdentifier]
|
|
606
|
+
"SELECT * FROM token_metadata WHERE user_id = $1 AND identifier = $2",
|
|
607
|
+
[this.identity, tokenIdentifier]
|
|
555
608
|
);
|
|
556
609
|
|
|
557
610
|
if (metadataResult.rows.length === 0) {
|
|
@@ -569,8 +622,10 @@ class PostgresTokenStore {
|
|
|
569
622
|
o.token_public_key, o.token_amount, o.token_identifier,
|
|
570
623
|
o.prev_tx_hash, o.prev_tx_vout
|
|
571
624
|
FROM token_outputs o
|
|
572
|
-
WHERE o.
|
|
573
|
-
|
|
625
|
+
WHERE o.user_id = $1
|
|
626
|
+
AND o.token_identifier = $2
|
|
627
|
+
AND o.reservation_id IS NULL`,
|
|
628
|
+
[this.identity, tokenIdentifier]
|
|
574
629
|
);
|
|
575
630
|
|
|
576
631
|
let outputs = outputRows.rows.map((row) => this._outputFromRow(row));
|
|
@@ -656,16 +711,16 @@ class PostgresTokenStore {
|
|
|
656
711
|
const reservationId = this._generateId();
|
|
657
712
|
|
|
658
713
|
await client.query(
|
|
659
|
-
"INSERT INTO token_reservations (id, purpose) VALUES ($1, $2)",
|
|
660
|
-
[reservationId, purpose]
|
|
714
|
+
"INSERT INTO token_reservations (user_id, id, purpose) VALUES ($1, $2, $3)",
|
|
715
|
+
[this.identity, reservationId, purpose]
|
|
661
716
|
);
|
|
662
717
|
|
|
663
718
|
// Set reservation_id on selected outputs
|
|
664
719
|
const selectedIds = selectedOutputs.map((o) => o.output.id);
|
|
665
720
|
if (selectedIds.length > 0) {
|
|
666
721
|
await client.query(
|
|
667
|
-
"UPDATE token_outputs SET reservation_id = $1 WHERE id = ANY($2)",
|
|
668
|
-
[reservationId, selectedIds]
|
|
722
|
+
"UPDATE token_outputs SET reservation_id = $1 WHERE user_id = $3 AND id = ANY($2)",
|
|
723
|
+
[reservationId, selectedIds, this.identity]
|
|
669
724
|
);
|
|
670
725
|
}
|
|
671
726
|
|
|
@@ -693,16 +748,18 @@ class PostgresTokenStore {
|
|
|
693
748
|
async cancelReservation(id) {
|
|
694
749
|
try {
|
|
695
750
|
await this._withTransaction(async (client) => {
|
|
696
|
-
// Clear reservation_id from outputs
|
|
751
|
+
// Clear reservation_id from outputs first — the composite FK uses NO
|
|
752
|
+
// ACTION (column-list SET NULL is PG15+ and a whole-row SET NULL would
|
|
753
|
+
// null user_id, which is NOT NULL).
|
|
697
754
|
await client.query(
|
|
698
|
-
"UPDATE token_outputs SET reservation_id = NULL WHERE reservation_id = $
|
|
699
|
-
[id]
|
|
755
|
+
"UPDATE token_outputs SET reservation_id = NULL WHERE user_id = $1 AND reservation_id = $2",
|
|
756
|
+
[this.identity, id]
|
|
700
757
|
);
|
|
701
758
|
|
|
702
759
|
// Delete the reservation
|
|
703
760
|
await client.query(
|
|
704
|
-
"DELETE FROM token_reservations WHERE id = $
|
|
705
|
-
[id]
|
|
761
|
+
"DELETE FROM token_reservations WHERE user_id = $1 AND id = $2",
|
|
762
|
+
[this.identity, id]
|
|
706
763
|
);
|
|
707
764
|
});
|
|
708
765
|
} catch (error) {
|
|
@@ -720,11 +777,15 @@ class PostgresTokenStore {
|
|
|
720
777
|
*/
|
|
721
778
|
async finalizeReservation(id) {
|
|
722
779
|
try {
|
|
723
|
-
|
|
780
|
+
// _withWriteTransaction acquires the advisory lock so this serializes
|
|
781
|
+
// against `setTokensOutputs`. Without it, a concurrent setTokensOutputs
|
|
782
|
+
// could read token_spent_outputs before our marker commits and re-insert
|
|
783
|
+
// the just-spent output as Available.
|
|
784
|
+
await this._withWriteTransaction(async (client) => {
|
|
724
785
|
// Get reservation purpose
|
|
725
786
|
const reservationResult = await client.query(
|
|
726
|
-
"SELECT purpose FROM token_reservations WHERE id = $
|
|
727
|
-
[id]
|
|
787
|
+
"SELECT purpose FROM token_reservations WHERE user_id = $1 AND id = $2",
|
|
788
|
+
[this.identity, id]
|
|
728
789
|
);
|
|
729
790
|
if (reservationResult.rows.length === 0) {
|
|
730
791
|
return; // Non-existing reservation
|
|
@@ -733,45 +794,53 @@ class PostgresTokenStore {
|
|
|
733
794
|
|
|
734
795
|
// Get reserved output IDs and mark them as spent
|
|
735
796
|
const reservedOutputsResult = await client.query(
|
|
736
|
-
"SELECT id FROM token_outputs WHERE reservation_id = $
|
|
737
|
-
[id]
|
|
797
|
+
"SELECT id FROM token_outputs WHERE user_id = $1 AND reservation_id = $2",
|
|
798
|
+
[this.identity, id]
|
|
738
799
|
);
|
|
739
800
|
const reservedOutputIds = reservedOutputsResult.rows.map((r) => r.id);
|
|
740
801
|
|
|
741
802
|
if (reservedOutputIds.length > 0) {
|
|
742
803
|
await client.query(
|
|
743
|
-
`INSERT INTO token_spent_outputs (output_id)
|
|
744
|
-
SELECT
|
|
804
|
+
`INSERT INTO token_spent_outputs (user_id, output_id)
|
|
805
|
+
SELECT $2, output_id FROM UNNEST($1::text[]) AS t(output_id)
|
|
745
806
|
ON CONFLICT DO NOTHING`,
|
|
746
|
-
[reservedOutputIds]
|
|
807
|
+
[reservedOutputIds, this.identity]
|
|
747
808
|
);
|
|
748
809
|
}
|
|
749
810
|
|
|
750
811
|
// Delete reserved outputs
|
|
751
812
|
await client.query(
|
|
752
|
-
"DELETE FROM token_outputs WHERE reservation_id = $
|
|
753
|
-
[id]
|
|
813
|
+
"DELETE FROM token_outputs WHERE user_id = $1 AND reservation_id = $2",
|
|
814
|
+
[this.identity, id]
|
|
754
815
|
);
|
|
755
816
|
|
|
756
817
|
// Delete the reservation
|
|
757
818
|
await client.query(
|
|
758
|
-
"DELETE FROM token_reservations WHERE id = $
|
|
759
|
-
[id]
|
|
819
|
+
"DELETE FROM token_reservations WHERE user_id = $1 AND id = $2",
|
|
820
|
+
[this.identity, id]
|
|
760
821
|
);
|
|
761
822
|
|
|
762
|
-
// If this was a swap reservation, update last_completed_at
|
|
823
|
+
// If this was a swap reservation, update last_completed_at. UPSERT so a
|
|
824
|
+
// tenant that joined after migration 2 (and thus has no row) gets one.
|
|
763
825
|
if (isSwap) {
|
|
764
826
|
await client.query(
|
|
765
|
-
|
|
827
|
+
`INSERT INTO token_swap_status (user_id, last_completed_at)
|
|
828
|
+
VALUES ($1, NOW())
|
|
829
|
+
ON CONFLICT (user_id) DO UPDATE
|
|
830
|
+
SET last_completed_at = EXCLUDED.last_completed_at`,
|
|
831
|
+
[this.identity]
|
|
766
832
|
);
|
|
767
833
|
}
|
|
768
834
|
|
|
769
|
-
// Clean up orphaned metadata
|
|
835
|
+
// Clean up orphaned metadata (per-tenant)
|
|
770
836
|
await client.query(
|
|
771
837
|
`DELETE FROM token_metadata
|
|
772
|
-
WHERE
|
|
773
|
-
|
|
774
|
-
|
|
838
|
+
WHERE user_id = $1
|
|
839
|
+
AND identifier NOT IN (
|
|
840
|
+
SELECT DISTINCT token_identifier
|
|
841
|
+
FROM token_outputs WHERE user_id = $1
|
|
842
|
+
)`,
|
|
843
|
+
[this.identity]
|
|
775
844
|
);
|
|
776
845
|
});
|
|
777
846
|
} catch (error) {
|
|
@@ -817,15 +886,27 @@ class PostgresTokenStore {
|
|
|
817
886
|
}
|
|
818
887
|
|
|
819
888
|
/**
|
|
820
|
-
* Delete reservations that have exceeded the timeout.
|
|
821
|
-
*
|
|
822
|
-
*
|
|
889
|
+
* Delete reservations that have exceeded the timeout. Releases outputs by
|
|
890
|
+
* clearing reservation_id explicitly, then deletes the parents — the
|
|
891
|
+
* composite FK uses NO ACTION (column-list SET NULL is PG15+ and a
|
|
892
|
+
* whole-row SET NULL would null user_id, NOT NULL).
|
|
823
893
|
*/
|
|
824
894
|
async _cleanupStaleReservations(client) {
|
|
895
|
+
await client.query(
|
|
896
|
+
`UPDATE token_outputs SET reservation_id = NULL
|
|
897
|
+
WHERE user_id = $2
|
|
898
|
+
AND reservation_id IN (
|
|
899
|
+
SELECT id FROM token_reservations
|
|
900
|
+
WHERE user_id = $2
|
|
901
|
+
AND created_at < NOW() - make_interval(secs => $1)
|
|
902
|
+
)`,
|
|
903
|
+
[RESERVATION_TIMEOUT_SECS, this.identity]
|
|
904
|
+
);
|
|
825
905
|
await client.query(
|
|
826
906
|
`DELETE FROM token_reservations
|
|
827
|
-
WHERE
|
|
828
|
-
|
|
907
|
+
WHERE user_id = $2
|
|
908
|
+
AND created_at < NOW() - make_interval(secs => $1)`,
|
|
909
|
+
[RESERVATION_TIMEOUT_SECS, this.identity]
|
|
829
910
|
);
|
|
830
911
|
}
|
|
831
912
|
|
|
@@ -835,10 +916,10 @@ class PostgresTokenStore {
|
|
|
835
916
|
async _upsertMetadata(client, metadata) {
|
|
836
917
|
await client.query(
|
|
837
918
|
`INSERT INTO token_metadata
|
|
838
|
-
(identifier, issuer_public_key, name, ticker, decimals, max_supply,
|
|
919
|
+
(user_id, identifier, issuer_public_key, name, ticker, decimals, max_supply,
|
|
839
920
|
is_freezable, creation_entity_public_key)
|
|
840
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
841
|
-
ON CONFLICT (identifier) DO UPDATE SET
|
|
921
|
+
VALUES ($9, $1, $2, $3, $4, $5, $6, $7, $8)
|
|
922
|
+
ON CONFLICT (user_id, identifier) DO UPDATE SET
|
|
842
923
|
issuer_public_key = EXCLUDED.issuer_public_key,
|
|
843
924
|
name = EXCLUDED.name,
|
|
844
925
|
ticker = EXCLUDED.ticker,
|
|
@@ -855,6 +936,7 @@ class PostgresTokenStore {
|
|
|
855
936
|
metadata.maxSupply,
|
|
856
937
|
metadata.isFreezable,
|
|
857
938
|
metadata.creationEntityPublicKey || null,
|
|
939
|
+
this.identity,
|
|
858
940
|
]
|
|
859
941
|
);
|
|
860
942
|
}
|
|
@@ -865,11 +947,11 @@ class PostgresTokenStore {
|
|
|
865
947
|
async _insertSingleOutput(client, tokenIdentifier, output) {
|
|
866
948
|
await client.query(
|
|
867
949
|
`INSERT INTO token_outputs
|
|
868
|
-
(id, token_identifier, owner_public_key, revocation_commitment,
|
|
950
|
+
(user_id, id, token_identifier, owner_public_key, revocation_commitment,
|
|
869
951
|
withdraw_bond_sats, withdraw_relative_block_locktime,
|
|
870
952
|
token_public_key, token_amount, prev_tx_hash, prev_tx_vout, added_at)
|
|
871
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
|
|
872
|
-
ON CONFLICT (id) DO NOTHING`,
|
|
953
|
+
VALUES ($11, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
|
|
954
|
+
ON CONFLICT (user_id, id) DO NOTHING`,
|
|
873
955
|
[
|
|
874
956
|
output.output.id,
|
|
875
957
|
tokenIdentifier,
|
|
@@ -881,6 +963,7 @@ class PostgresTokenStore {
|
|
|
881
963
|
output.output.tokenAmount,
|
|
882
964
|
output.prevTxHash,
|
|
883
965
|
output.prevTxVout,
|
|
966
|
+
this.identity,
|
|
884
967
|
]
|
|
885
968
|
);
|
|
886
969
|
}
|
|
@@ -932,28 +1015,30 @@ class PostgresTokenStore {
|
|
|
932
1015
|
* @param {number} config.maxPoolSize - Maximum number of connections in the pool
|
|
933
1016
|
* @param {number} config.createTimeoutSecs - Timeout in seconds for establishing a new connection
|
|
934
1017
|
* @param {number} config.recycleTimeoutSecs - Timeout in seconds before recycling an idle connection
|
|
1018
|
+
* @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey scoping reads/writes
|
|
935
1019
|
* @param {object} [logger] - Optional logger
|
|
936
1020
|
* @returns {Promise<PostgresTokenStore>}
|
|
937
1021
|
*/
|
|
938
|
-
async function createPostgresTokenStore(config, logger = null) {
|
|
1022
|
+
async function createPostgresTokenStore(config, identity, logger = null) {
|
|
939
1023
|
const pool = new pg.Pool({
|
|
940
1024
|
connectionString: config.connectionString,
|
|
941
1025
|
max: config.maxPoolSize,
|
|
942
1026
|
connectionTimeoutMillis: config.createTimeoutSecs * 1000,
|
|
943
1027
|
idleTimeoutMillis: config.recycleTimeoutSecs * 1000,
|
|
944
1028
|
});
|
|
945
|
-
return createPostgresTokenStoreWithPool(pool, logger);
|
|
1029
|
+
return createPostgresTokenStoreWithPool(pool, identity, logger);
|
|
946
1030
|
}
|
|
947
1031
|
|
|
948
1032
|
/**
|
|
949
1033
|
* Create a PostgresTokenStore instance from an existing pg.Pool.
|
|
950
1034
|
*
|
|
951
1035
|
* @param {pg.Pool} pool - An existing connection pool
|
|
1036
|
+
* @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey scoping reads/writes
|
|
952
1037
|
* @param {object} [logger] - Optional logger
|
|
953
1038
|
* @returns {Promise<PostgresTokenStore>}
|
|
954
1039
|
*/
|
|
955
|
-
async function createPostgresTokenStoreWithPool(pool, logger = null) {
|
|
956
|
-
const store = new PostgresTokenStore(pool, logger);
|
|
1040
|
+
async function createPostgresTokenStoreWithPool(pool, identity, logger = null) {
|
|
1041
|
+
const store = new PostgresTokenStore(pool, identity, logger);
|
|
957
1042
|
await store.initialize();
|
|
958
1043
|
return store;
|
|
959
1044
|
}
|