@breeztech/breez-sdk-spark 0.13.10-dev → 0.13.12-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 +157 -0
- package/bundler/breez_sdk_spark_wasm.js +1 -1
- package/bundler/breez_sdk_spark_wasm_bg.js +419 -41
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
- package/deno/breez_sdk_spark_wasm.d.ts +157 -0
- package/deno/breez_sdk_spark_wasm.js +419 -41
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
- package/nodejs/breez_sdk_spark_wasm.d.ts +157 -0
- package/nodejs/breez_sdk_spark_wasm.js +428 -41
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
- package/nodejs/index.js +56 -0
- package/nodejs/index.mjs +9 -0
- package/nodejs/mysql-session-manager/errors.cjs +13 -0
- package/nodejs/mysql-session-manager/index.cjs +144 -0
- package/nodejs/mysql-session-manager/migrations.cjs +102 -0
- package/nodejs/mysql-session-manager/package.json +9 -0
- package/nodejs/mysql-storage/errors.cjs +19 -0
- package/nodejs/mysql-storage/index.cjs +1367 -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 +980 -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 +941 -0
- package/nodejs/mysql-tree-store/migrations.cjs +221 -0
- package/nodejs/mysql-tree-store/package.json +9 -0
- package/nodejs/package.json +5 -0
- package/nodejs/postgres-session-manager/errors.cjs +13 -0
- package/nodejs/postgres-session-manager/index.cjs +165 -0
- package/nodejs/postgres-session-manager/migrations.cjs +126 -0
- package/nodejs/postgres-session-manager/package.json +9 -0
- package/nodejs/postgres-storage/index.cjs +147 -92
- package/nodejs/postgres-storage/migrations.cjs +85 -4
- package/nodejs/postgres-token-store/index.cjs +178 -102
- package/nodejs/postgres-token-store/migrations.cjs +92 -3
- package/nodejs/postgres-tree-store/index.cjs +168 -83
- package/nodejs/postgres-tree-store/migrations.cjs +80 -3
- package/package.json +1 -1
- package/ssr/index.js +53 -0
- package/web/breez_sdk_spark_wasm.d.ts +184 -7
- package/web/breez_sdk_spark_wasm.js +419 -41
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +27 -7
|
@@ -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;
|
|
@@ -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
|
|
@@ -301,7 +348,8 @@ class PostgresTokenStore {
|
|
|
301
348
|
*/
|
|
302
349
|
async getTokenBalances() {
|
|
303
350
|
try {
|
|
304
|
-
const result = await this.pool.query(
|
|
351
|
+
const result = await this.pool.query(
|
|
352
|
+
`
|
|
305
353
|
SELECT m.identifier, m.issuer_public_key, m.name, m.ticker, m.decimals,
|
|
306
354
|
m.max_supply, m.is_freezable, m.creation_entity_public_key,
|
|
307
355
|
COALESCE(SUM(
|
|
@@ -312,11 +360,16 @@ class PostgresTokenStore {
|
|
|
312
360
|
END
|
|
313
361
|
), 0)::text AS balance
|
|
314
362
|
FROM token_metadata m
|
|
315
|
-
JOIN token_outputs o
|
|
316
|
-
|
|
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
|
|
317
368
|
GROUP BY m.identifier, m.issuer_public_key, m.name, m.ticker,
|
|
318
369
|
m.decimals, m.max_supply, m.is_freezable, m.creation_entity_public_key
|
|
319
|
-
|
|
370
|
+
`,
|
|
371
|
+
[this.identity]
|
|
372
|
+
);
|
|
320
373
|
return result.rows.map((row) => ({
|
|
321
374
|
metadata: {
|
|
322
375
|
identifier: row.identifier,
|
|
@@ -349,9 +402,13 @@ class PostgresTokenStore {
|
|
|
349
402
|
o.prev_tx_hash, o.prev_tx_vout, o.reservation_id,
|
|
350
403
|
r.purpose
|
|
351
404
|
FROM token_metadata m
|
|
352
|
-
LEFT JOIN token_outputs o
|
|
353
|
-
|
|
354
|
-
|
|
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]
|
|
355
412
|
);
|
|
356
413
|
|
|
357
414
|
const map = new Map();
|
|
@@ -422,11 +479,13 @@ class PostgresTokenStore {
|
|
|
422
479
|
o.prev_tx_hash, o.prev_tx_vout, o.reservation_id,
|
|
423
480
|
r.purpose
|
|
424
481
|
FROM token_metadata m
|
|
425
|
-
LEFT JOIN token_outputs o
|
|
426
|
-
|
|
427
|
-
|
|
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}
|
|
428
487
|
ORDER BY o.token_amount::NUMERIC ASC`,
|
|
429
|
-
[param]
|
|
488
|
+
[param, this.identity]
|
|
430
489
|
);
|
|
431
490
|
|
|
432
491
|
if (result.rows.length === 0) {
|
|
@@ -473,18 +532,14 @@ class PostgresTokenStore {
|
|
|
473
532
|
*/
|
|
474
533
|
async insertTokenOutputs(tokenOutputs) {
|
|
475
534
|
try {
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
await client.query("BEGIN");
|
|
479
|
-
|
|
535
|
+
await this._withTransaction(async (client) => {
|
|
480
536
|
await this._upsertMetadata(client, tokenOutputs.metadata);
|
|
481
537
|
|
|
482
|
-
// Remove inserted output IDs from spent markers (output returned to us)
|
|
483
538
|
const outputIds = tokenOutputs.outputs.map((o) => o.output.id);
|
|
484
539
|
if (outputIds.length > 0) {
|
|
485
540
|
await client.query(
|
|
486
|
-
"DELETE FROM token_spent_outputs WHERE output_id = ANY($
|
|
487
|
-
[outputIds]
|
|
541
|
+
"DELETE FROM token_spent_outputs WHERE user_id = $1 AND output_id = ANY($2)",
|
|
542
|
+
[this.identity, outputIds]
|
|
488
543
|
);
|
|
489
544
|
}
|
|
490
545
|
|
|
@@ -495,14 +550,7 @@ class PostgresTokenStore {
|
|
|
495
550
|
output
|
|
496
551
|
);
|
|
497
552
|
}
|
|
498
|
-
|
|
499
|
-
await client.query("COMMIT");
|
|
500
|
-
} catch (error) {
|
|
501
|
-
await client.query("ROLLBACK").catch(() => {});
|
|
502
|
-
throw error;
|
|
503
|
-
} finally {
|
|
504
|
-
client.release();
|
|
505
|
-
}
|
|
553
|
+
});
|
|
506
554
|
} catch (error) {
|
|
507
555
|
if (error instanceof TokenStoreError) throw error;
|
|
508
556
|
throw new TokenStoreError(
|
|
@@ -544,8 +592,8 @@ class PostgresTokenStore {
|
|
|
544
592
|
|
|
545
593
|
// Get metadata
|
|
546
594
|
const metadataResult = await client.query(
|
|
547
|
-
"SELECT * FROM token_metadata WHERE identifier = $
|
|
548
|
-
[tokenIdentifier]
|
|
595
|
+
"SELECT * FROM token_metadata WHERE user_id = $1 AND identifier = $2",
|
|
596
|
+
[this.identity, tokenIdentifier]
|
|
549
597
|
);
|
|
550
598
|
|
|
551
599
|
if (metadataResult.rows.length === 0) {
|
|
@@ -563,8 +611,10 @@ class PostgresTokenStore {
|
|
|
563
611
|
o.token_public_key, o.token_amount, o.token_identifier,
|
|
564
612
|
o.prev_tx_hash, o.prev_tx_vout
|
|
565
613
|
FROM token_outputs o
|
|
566
|
-
WHERE o.
|
|
567
|
-
|
|
614
|
+
WHERE o.user_id = $1
|
|
615
|
+
AND o.token_identifier = $2
|
|
616
|
+
AND o.reservation_id IS NULL`,
|
|
617
|
+
[this.identity, tokenIdentifier]
|
|
568
618
|
);
|
|
569
619
|
|
|
570
620
|
let outputs = outputRows.rows.map((row) => this._outputFromRow(row));
|
|
@@ -650,16 +700,16 @@ class PostgresTokenStore {
|
|
|
650
700
|
const reservationId = this._generateId();
|
|
651
701
|
|
|
652
702
|
await client.query(
|
|
653
|
-
"INSERT INTO token_reservations (id, purpose) VALUES ($1, $2)",
|
|
654
|
-
[reservationId, purpose]
|
|
703
|
+
"INSERT INTO token_reservations (user_id, id, purpose) VALUES ($1, $2, $3)",
|
|
704
|
+
[this.identity, reservationId, purpose]
|
|
655
705
|
);
|
|
656
706
|
|
|
657
707
|
// Set reservation_id on selected outputs
|
|
658
708
|
const selectedIds = selectedOutputs.map((o) => o.output.id);
|
|
659
709
|
if (selectedIds.length > 0) {
|
|
660
710
|
await client.query(
|
|
661
|
-
"UPDATE token_outputs SET reservation_id = $1 WHERE id = ANY($2)",
|
|
662
|
-
[reservationId, selectedIds]
|
|
711
|
+
"UPDATE token_outputs SET reservation_id = $1 WHERE user_id = $3 AND id = ANY($2)",
|
|
712
|
+
[reservationId, selectedIds, this.identity]
|
|
663
713
|
);
|
|
664
714
|
}
|
|
665
715
|
|
|
@@ -687,16 +737,18 @@ class PostgresTokenStore {
|
|
|
687
737
|
async cancelReservation(id) {
|
|
688
738
|
try {
|
|
689
739
|
await this._withTransaction(async (client) => {
|
|
690
|
-
// Clear reservation_id from outputs
|
|
740
|
+
// Clear reservation_id from outputs first — the composite FK uses NO
|
|
741
|
+
// ACTION (column-list SET NULL is PG15+ and a whole-row SET NULL would
|
|
742
|
+
// null user_id, which is NOT NULL).
|
|
691
743
|
await client.query(
|
|
692
|
-
"UPDATE token_outputs SET reservation_id = NULL WHERE reservation_id = $
|
|
693
|
-
[id]
|
|
744
|
+
"UPDATE token_outputs SET reservation_id = NULL WHERE user_id = $1 AND reservation_id = $2",
|
|
745
|
+
[this.identity, id]
|
|
694
746
|
);
|
|
695
747
|
|
|
696
748
|
// Delete the reservation
|
|
697
749
|
await client.query(
|
|
698
|
-
"DELETE FROM token_reservations WHERE id = $
|
|
699
|
-
[id]
|
|
750
|
+
"DELETE FROM token_reservations WHERE user_id = $1 AND id = $2",
|
|
751
|
+
[this.identity, id]
|
|
700
752
|
);
|
|
701
753
|
});
|
|
702
754
|
} catch (error) {
|
|
@@ -721,8 +773,8 @@ class PostgresTokenStore {
|
|
|
721
773
|
await this._withWriteTransaction(async (client) => {
|
|
722
774
|
// Get reservation purpose
|
|
723
775
|
const reservationResult = await client.query(
|
|
724
|
-
"SELECT purpose FROM token_reservations WHERE id = $
|
|
725
|
-
[id]
|
|
776
|
+
"SELECT purpose FROM token_reservations WHERE user_id = $1 AND id = $2",
|
|
777
|
+
[this.identity, id]
|
|
726
778
|
);
|
|
727
779
|
if (reservationResult.rows.length === 0) {
|
|
728
780
|
return; // Non-existing reservation
|
|
@@ -731,45 +783,53 @@ class PostgresTokenStore {
|
|
|
731
783
|
|
|
732
784
|
// Get reserved output IDs and mark them as spent
|
|
733
785
|
const reservedOutputsResult = await client.query(
|
|
734
|
-
"SELECT id FROM token_outputs WHERE reservation_id = $
|
|
735
|
-
[id]
|
|
786
|
+
"SELECT id FROM token_outputs WHERE user_id = $1 AND reservation_id = $2",
|
|
787
|
+
[this.identity, id]
|
|
736
788
|
);
|
|
737
789
|
const reservedOutputIds = reservedOutputsResult.rows.map((r) => r.id);
|
|
738
790
|
|
|
739
791
|
if (reservedOutputIds.length > 0) {
|
|
740
792
|
await client.query(
|
|
741
|
-
`INSERT INTO token_spent_outputs (output_id)
|
|
742
|
-
SELECT
|
|
793
|
+
`INSERT INTO token_spent_outputs (user_id, output_id)
|
|
794
|
+
SELECT $2, output_id FROM UNNEST($1::text[]) AS t(output_id)
|
|
743
795
|
ON CONFLICT DO NOTHING`,
|
|
744
|
-
[reservedOutputIds]
|
|
796
|
+
[reservedOutputIds, this.identity]
|
|
745
797
|
);
|
|
746
798
|
}
|
|
747
799
|
|
|
748
800
|
// Delete reserved outputs
|
|
749
801
|
await client.query(
|
|
750
|
-
"DELETE FROM token_outputs WHERE reservation_id = $
|
|
751
|
-
[id]
|
|
802
|
+
"DELETE FROM token_outputs WHERE user_id = $1 AND reservation_id = $2",
|
|
803
|
+
[this.identity, id]
|
|
752
804
|
);
|
|
753
805
|
|
|
754
806
|
// Delete the reservation
|
|
755
807
|
await client.query(
|
|
756
|
-
"DELETE FROM token_reservations WHERE id = $
|
|
757
|
-
[id]
|
|
808
|
+
"DELETE FROM token_reservations WHERE user_id = $1 AND id = $2",
|
|
809
|
+
[this.identity, id]
|
|
758
810
|
);
|
|
759
811
|
|
|
760
|
-
// If this was a swap reservation, update last_completed_at
|
|
812
|
+
// If this was a swap reservation, update last_completed_at. UPSERT so a
|
|
813
|
+
// tenant that joined after migration 2 (and thus has no row) gets one.
|
|
761
814
|
if (isSwap) {
|
|
762
815
|
await client.query(
|
|
763
|
-
|
|
816
|
+
`INSERT INTO token_swap_status (user_id, last_completed_at)
|
|
817
|
+
VALUES ($1, NOW())
|
|
818
|
+
ON CONFLICT (user_id) DO UPDATE
|
|
819
|
+
SET last_completed_at = EXCLUDED.last_completed_at`,
|
|
820
|
+
[this.identity]
|
|
764
821
|
);
|
|
765
822
|
}
|
|
766
823
|
|
|
767
|
-
// Clean up orphaned metadata
|
|
824
|
+
// Clean up orphaned metadata (per-tenant)
|
|
768
825
|
await client.query(
|
|
769
826
|
`DELETE FROM token_metadata
|
|
770
|
-
WHERE
|
|
771
|
-
|
|
772
|
-
|
|
827
|
+
WHERE user_id = $1
|
|
828
|
+
AND identifier NOT IN (
|
|
829
|
+
SELECT DISTINCT token_identifier
|
|
830
|
+
FROM token_outputs WHERE user_id = $1
|
|
831
|
+
)`,
|
|
832
|
+
[this.identity]
|
|
773
833
|
);
|
|
774
834
|
});
|
|
775
835
|
} catch (error) {
|
|
@@ -815,15 +875,27 @@ class PostgresTokenStore {
|
|
|
815
875
|
}
|
|
816
876
|
|
|
817
877
|
/**
|
|
818
|
-
* Delete reservations that have exceeded the timeout.
|
|
819
|
-
*
|
|
820
|
-
*
|
|
878
|
+
* Delete reservations that have exceeded the timeout. Releases outputs by
|
|
879
|
+
* clearing reservation_id explicitly, then deletes the parents — the
|
|
880
|
+
* composite FK uses NO ACTION (column-list SET NULL is PG15+ and a
|
|
881
|
+
* whole-row SET NULL would null user_id, NOT NULL).
|
|
821
882
|
*/
|
|
822
883
|
async _cleanupStaleReservations(client) {
|
|
884
|
+
await client.query(
|
|
885
|
+
`UPDATE token_outputs SET reservation_id = NULL
|
|
886
|
+
WHERE user_id = $2
|
|
887
|
+
AND reservation_id IN (
|
|
888
|
+
SELECT id FROM token_reservations
|
|
889
|
+
WHERE user_id = $2
|
|
890
|
+
AND created_at < NOW() - make_interval(secs => $1)
|
|
891
|
+
)`,
|
|
892
|
+
[RESERVATION_TIMEOUT_SECS, this.identity]
|
|
893
|
+
);
|
|
823
894
|
await client.query(
|
|
824
895
|
`DELETE FROM token_reservations
|
|
825
|
-
WHERE
|
|
826
|
-
|
|
896
|
+
WHERE user_id = $2
|
|
897
|
+
AND created_at < NOW() - make_interval(secs => $1)`,
|
|
898
|
+
[RESERVATION_TIMEOUT_SECS, this.identity]
|
|
827
899
|
);
|
|
828
900
|
}
|
|
829
901
|
|
|
@@ -833,10 +905,10 @@ class PostgresTokenStore {
|
|
|
833
905
|
async _upsertMetadata(client, metadata) {
|
|
834
906
|
await client.query(
|
|
835
907
|
`INSERT INTO token_metadata
|
|
836
|
-
(identifier, issuer_public_key, name, ticker, decimals, max_supply,
|
|
908
|
+
(user_id, identifier, issuer_public_key, name, ticker, decimals, max_supply,
|
|
837
909
|
is_freezable, creation_entity_public_key)
|
|
838
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
839
|
-
ON CONFLICT (identifier) DO UPDATE SET
|
|
910
|
+
VALUES ($9, $1, $2, $3, $4, $5, $6, $7, $8)
|
|
911
|
+
ON CONFLICT (user_id, identifier) DO UPDATE SET
|
|
840
912
|
issuer_public_key = EXCLUDED.issuer_public_key,
|
|
841
913
|
name = EXCLUDED.name,
|
|
842
914
|
ticker = EXCLUDED.ticker,
|
|
@@ -853,6 +925,7 @@ class PostgresTokenStore {
|
|
|
853
925
|
metadata.maxSupply,
|
|
854
926
|
metadata.isFreezable,
|
|
855
927
|
metadata.creationEntityPublicKey || null,
|
|
928
|
+
this.identity,
|
|
856
929
|
]
|
|
857
930
|
);
|
|
858
931
|
}
|
|
@@ -863,11 +936,11 @@ class PostgresTokenStore {
|
|
|
863
936
|
async _insertSingleOutput(client, tokenIdentifier, output) {
|
|
864
937
|
await client.query(
|
|
865
938
|
`INSERT INTO token_outputs
|
|
866
|
-
(id, token_identifier, owner_public_key, revocation_commitment,
|
|
939
|
+
(user_id, id, token_identifier, owner_public_key, revocation_commitment,
|
|
867
940
|
withdraw_bond_sats, withdraw_relative_block_locktime,
|
|
868
941
|
token_public_key, token_amount, prev_tx_hash, prev_tx_vout, added_at)
|
|
869
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
|
|
870
|
-
ON CONFLICT (id) DO NOTHING`,
|
|
942
|
+
VALUES ($11, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
|
|
943
|
+
ON CONFLICT (user_id, id) DO NOTHING`,
|
|
871
944
|
[
|
|
872
945
|
output.output.id,
|
|
873
946
|
tokenIdentifier,
|
|
@@ -879,6 +952,7 @@ class PostgresTokenStore {
|
|
|
879
952
|
output.output.tokenAmount,
|
|
880
953
|
output.prevTxHash,
|
|
881
954
|
output.prevTxVout,
|
|
955
|
+
this.identity,
|
|
882
956
|
]
|
|
883
957
|
);
|
|
884
958
|
}
|
|
@@ -930,28 +1004,30 @@ class PostgresTokenStore {
|
|
|
930
1004
|
* @param {number} config.maxPoolSize - Maximum number of connections in the pool
|
|
931
1005
|
* @param {number} config.createTimeoutSecs - Timeout in seconds for establishing a new connection
|
|
932
1006
|
* @param {number} config.recycleTimeoutSecs - Timeout in seconds before recycling an idle connection
|
|
1007
|
+
* @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey scoping reads/writes
|
|
933
1008
|
* @param {object} [logger] - Optional logger
|
|
934
1009
|
* @returns {Promise<PostgresTokenStore>}
|
|
935
1010
|
*/
|
|
936
|
-
async function createPostgresTokenStore(config, logger = null) {
|
|
1011
|
+
async function createPostgresTokenStore(config, identity, logger = null) {
|
|
937
1012
|
const pool = new pg.Pool({
|
|
938
1013
|
connectionString: config.connectionString,
|
|
939
1014
|
max: config.maxPoolSize,
|
|
940
1015
|
connectionTimeoutMillis: config.createTimeoutSecs * 1000,
|
|
941
1016
|
idleTimeoutMillis: config.recycleTimeoutSecs * 1000,
|
|
942
1017
|
});
|
|
943
|
-
return createPostgresTokenStoreWithPool(pool, logger);
|
|
1018
|
+
return createPostgresTokenStoreWithPool(pool, identity, logger);
|
|
944
1019
|
}
|
|
945
1020
|
|
|
946
1021
|
/**
|
|
947
1022
|
* Create a PostgresTokenStore instance from an existing pg.Pool.
|
|
948
1023
|
*
|
|
949
1024
|
* @param {pg.Pool} pool - An existing connection pool
|
|
1025
|
+
* @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey scoping reads/writes
|
|
950
1026
|
* @param {object} [logger] - Optional logger
|
|
951
1027
|
* @returns {Promise<PostgresTokenStore>}
|
|
952
1028
|
*/
|
|
953
|
-
async function createPostgresTokenStoreWithPool(pool, logger = null) {
|
|
954
|
-
const store = new PostgresTokenStore(pool, logger);
|
|
1029
|
+
async function createPostgresTokenStoreWithPool(pool, identity, logger = null) {
|
|
1030
|
+
const store = new PostgresTokenStore(pool, identity, logger);
|
|
955
1031
|
await store.initialize();
|
|
956
1032
|
return store;
|
|
957
1033
|
}
|