@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
|
@@ -0,0 +1,1366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommonJS implementation for Node.js MySQL Storage.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `postgres-storage/index.cjs` for MySQL 8.0+. SQL translation rules:
|
|
5
|
+
* - `$N` placeholders → `?`
|
|
6
|
+
* - `JSONB` operators (`::jsonb->>`, `@>`) → `JSON_EXTRACT`/`JSON_UNQUOTE`/`JSON_CONTAINS`
|
|
7
|
+
* - `ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col` →
|
|
8
|
+
* `ON DUPLICATE KEY UPDATE col = VALUES(col)`
|
|
9
|
+
* - `ON CONFLICT DO NOTHING` → `INSERT … ON DUPLICATE KEY UPDATE <pk> = <pk>`
|
|
10
|
+
* (avoid `INSERT IGNORE`: it silently swallows non-PK errors too)
|
|
11
|
+
* - `pg`'s `pool.query(sql, params)` returns `{ rows, rowCount }`; mysql2's
|
|
12
|
+
* `pool.query(sql, params)` returns `[rows, fields]` for SELECT and
|
|
13
|
+
* `[okPacket, fields]` (with `affectedRows`) for write operations.
|
|
14
|
+
* - Reserved words like `key` need backtick quoting in MySQL.
|
|
15
|
+
* - `pool.connect()` → `pool.getConnection()`; `client.query("BEGIN")`/COMMIT/ROLLBACK
|
|
16
|
+
* → `conn.beginTransaction()`/`conn.commit()`/`conn.rollback()`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let mysql;
|
|
20
|
+
try {
|
|
21
|
+
const mainModule = require.main;
|
|
22
|
+
if (mainModule) {
|
|
23
|
+
mysql = mainModule.require("mysql2/promise");
|
|
24
|
+
} else {
|
|
25
|
+
mysql = require("mysql2/promise");
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
try {
|
|
29
|
+
mysql = require("mysql2/promise");
|
|
30
|
+
} catch (fallbackError) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`mysql2 not found. Please install it in your project: npm install mysql2@^3.11.0\n` +
|
|
33
|
+
`Original error: ${error.message}\nFallback error: ${fallbackError.message}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { StorageError } = require("./errors.cjs");
|
|
39
|
+
const { MysqlMigrationManager } = require("./migrations.cjs");
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Base query for payment lookups. All columns are accessed by name in _rowToPayment.
|
|
43
|
+
* parent_payment_id is only used by getPaymentsByParentIds.
|
|
44
|
+
*/
|
|
45
|
+
const SELECT_PAYMENT_SQL = `
|
|
46
|
+
SELECT p.id,
|
|
47
|
+
p.payment_type,
|
|
48
|
+
p.status,
|
|
49
|
+
p.amount,
|
|
50
|
+
p.fees,
|
|
51
|
+
p.timestamp,
|
|
52
|
+
p.method,
|
|
53
|
+
p.withdraw_tx_id,
|
|
54
|
+
p.deposit_tx_id,
|
|
55
|
+
p.spark,
|
|
56
|
+
l.invoice AS lightning_invoice,
|
|
57
|
+
l.payment_hash AS lightning_payment_hash,
|
|
58
|
+
l.destination_pubkey AS lightning_destination_pubkey,
|
|
59
|
+
COALESCE(l.description, pm.lnurl_description) AS lightning_description,
|
|
60
|
+
l.preimage AS lightning_preimage,
|
|
61
|
+
l.htlc_status AS lightning_htlc_status,
|
|
62
|
+
l.htlc_expiry_time AS lightning_htlc_expiry_time,
|
|
63
|
+
pm.lnurl_pay_info,
|
|
64
|
+
pm.lnurl_withdraw_info,
|
|
65
|
+
pm.conversion_info,
|
|
66
|
+
pm.conversion_status,
|
|
67
|
+
t.metadata AS token_metadata,
|
|
68
|
+
t.tx_hash AS token_tx_hash,
|
|
69
|
+
t.tx_type AS token_tx_type,
|
|
70
|
+
t.invoice_details AS token_invoice_details,
|
|
71
|
+
s.invoice_details AS spark_invoice_details,
|
|
72
|
+
s.htlc_details AS spark_htlc_details,
|
|
73
|
+
lrm.nostr_zap_request AS lnurl_nostr_zap_request,
|
|
74
|
+
lrm.nostr_zap_receipt AS lnurl_nostr_zap_receipt,
|
|
75
|
+
lrm.sender_comment AS lnurl_sender_comment,
|
|
76
|
+
lrm.payment_hash AS lnurl_payment_hash,
|
|
77
|
+
pm.parent_payment_id
|
|
78
|
+
FROM payments p
|
|
79
|
+
LEFT JOIN payment_details_lightning l ON p.id = l.payment_id AND p.user_id = l.user_id
|
|
80
|
+
LEFT JOIN payment_details_token t ON p.id = t.payment_id AND p.user_id = t.user_id
|
|
81
|
+
LEFT JOIN payment_details_spark s ON p.id = s.payment_id AND p.user_id = s.user_id
|
|
82
|
+
LEFT JOIN payment_metadata pm ON p.id = pm.payment_id AND p.user_id = pm.user_id
|
|
83
|
+
LEFT JOIN lnurl_receive_metadata lrm ON l.payment_hash = lrm.payment_hash AND l.user_id = lrm.user_id`;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* mysql2 may return JSON columns as either parsed objects or raw strings
|
|
87
|
+
* depending on driver/server behavior. This helper normalizes both shapes.
|
|
88
|
+
*/
|
|
89
|
+
function parseJson(value) {
|
|
90
|
+
if (value == null) return null;
|
|
91
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Normalize MySQL's TINYINT(1) to a JS boolean. */
|
|
96
|
+
function toBool(value) {
|
|
97
|
+
if (value == null) return null;
|
|
98
|
+
if (typeof value === "boolean") return value;
|
|
99
|
+
return value === 1 || value === "1" || value === true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class MysqlStorage {
|
|
103
|
+
/**
|
|
104
|
+
* @param {import('mysql2/promise').Pool} pool - Connection pool (may be shared with other tenants).
|
|
105
|
+
* @param {Buffer|Uint8Array} identity - 33-byte secp256k1 compressed pubkey
|
|
106
|
+
* uniquely identifying this tenant. All reads and writes are scoped to it
|
|
107
|
+
* so that multiple instances with distinct identities can share one DB.
|
|
108
|
+
* @param {object} [logger]
|
|
109
|
+
*/
|
|
110
|
+
constructor(pool, identity, logger = null) {
|
|
111
|
+
if (!identity) {
|
|
112
|
+
throw new StorageError("MysqlStorage requires a tenant identity");
|
|
113
|
+
}
|
|
114
|
+
this.pool = pool;
|
|
115
|
+
this.identity = Buffer.from(identity);
|
|
116
|
+
this.logger = logger;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Initialize the database (run migrations). */
|
|
120
|
+
async initialize() {
|
|
121
|
+
try {
|
|
122
|
+
const migrationManager = new MysqlMigrationManager(this.logger);
|
|
123
|
+
await migrationManager.migrate(this.pool, this.identity);
|
|
124
|
+
return this;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new StorageError(
|
|
127
|
+
`Failed to initialize MySQL database: ${error.message}`,
|
|
128
|
+
error
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Close the pool. */
|
|
134
|
+
async close() {
|
|
135
|
+
if (this.pool) {
|
|
136
|
+
await this.pool.end();
|
|
137
|
+
this.pool = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run a function inside a transaction.
|
|
143
|
+
* @param {function(import('mysql2/promise').PoolConnection): Promise<T>} fn
|
|
144
|
+
* @returns {Promise<T>}
|
|
145
|
+
* @template T
|
|
146
|
+
*/
|
|
147
|
+
async _withTransaction(fn) {
|
|
148
|
+
const conn = await this.pool.getConnection();
|
|
149
|
+
try {
|
|
150
|
+
await conn.beginTransaction();
|
|
151
|
+
const result = await fn(conn);
|
|
152
|
+
await conn.commit();
|
|
153
|
+
return result;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
await conn.rollback().catch(() => {});
|
|
156
|
+
throw error;
|
|
157
|
+
} finally {
|
|
158
|
+
conn.release();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ===== Cache Operations =====
|
|
163
|
+
|
|
164
|
+
async getCachedItem(key) {
|
|
165
|
+
try {
|
|
166
|
+
const [rows] = await this.pool.query(
|
|
167
|
+
"SELECT value FROM settings WHERE user_id = ? AND `key` = ?",
|
|
168
|
+
[this.identity, key]
|
|
169
|
+
);
|
|
170
|
+
return rows.length > 0 ? rows[0].value : null;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new StorageError(
|
|
173
|
+
`Failed to get cached item '${key}': ${error.message}`,
|
|
174
|
+
error
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async setCachedItem(key, value) {
|
|
180
|
+
try {
|
|
181
|
+
await this.pool.query(
|
|
182
|
+
"INSERT INTO settings (user_id, `key`, value) VALUES (?, ?, ?) " +
|
|
183
|
+
"ON DUPLICATE KEY UPDATE value = VALUES(value)",
|
|
184
|
+
[this.identity, key, value]
|
|
185
|
+
);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new StorageError(
|
|
188
|
+
`Failed to set cached item '${key}': ${error.message}`,
|
|
189
|
+
error
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async deleteCachedItem(key) {
|
|
195
|
+
try {
|
|
196
|
+
await this.pool.query(
|
|
197
|
+
"DELETE FROM settings WHERE user_id = ? AND `key` = ?",
|
|
198
|
+
[this.identity, key]
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new StorageError(
|
|
202
|
+
`Failed to delete cached item '${key}': ${error.message}`,
|
|
203
|
+
error
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ===== Payment Operations =====
|
|
209
|
+
|
|
210
|
+
async listPayments(request) {
|
|
211
|
+
try {
|
|
212
|
+
const actualOffset = request.offset != null ? request.offset : 0;
|
|
213
|
+
const actualLimit =
|
|
214
|
+
request.limit != null ? request.limit : 4294967295;
|
|
215
|
+
|
|
216
|
+
// Tenant scoping is always the first WHERE clause; subsequent dynamic
|
|
217
|
+
// filters add more clauses and parameters.
|
|
218
|
+
const whereClauses = ["p.user_id = ?"];
|
|
219
|
+
const params = [this.identity];
|
|
220
|
+
|
|
221
|
+
if (request.typeFilter && request.typeFilter.length > 0) {
|
|
222
|
+
const placeholders = request.typeFilter.map(() => "?");
|
|
223
|
+
whereClauses.push(
|
|
224
|
+
`p.payment_type IN (${placeholders.join(", ")})`
|
|
225
|
+
);
|
|
226
|
+
params.push(...request.typeFilter);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (request.statusFilter && request.statusFilter.length > 0) {
|
|
230
|
+
const placeholders = request.statusFilter.map(() => "?");
|
|
231
|
+
whereClauses.push(`p.status IN (${placeholders.join(", ")})`);
|
|
232
|
+
params.push(...request.statusFilter);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (request.fromTimestamp != null) {
|
|
236
|
+
whereClauses.push("p.timestamp >= ?");
|
|
237
|
+
params.push(request.fromTimestamp);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (request.toTimestamp != null) {
|
|
241
|
+
whereClauses.push("p.timestamp < ?");
|
|
242
|
+
params.push(request.toTimestamp);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
request.paymentDetailsFilter &&
|
|
247
|
+
request.paymentDetailsFilter.length > 0
|
|
248
|
+
) {
|
|
249
|
+
const allPaymentDetailsClauses = [];
|
|
250
|
+
for (const paymentDetailsFilter of request.paymentDetailsFilter) {
|
|
251
|
+
const paymentDetailsClauses = [];
|
|
252
|
+
|
|
253
|
+
const htlcAlias =
|
|
254
|
+
paymentDetailsFilter.type === "spark"
|
|
255
|
+
? "s"
|
|
256
|
+
: paymentDetailsFilter.type === "lightning"
|
|
257
|
+
? "l"
|
|
258
|
+
: null;
|
|
259
|
+
if (
|
|
260
|
+
htlcAlias &&
|
|
261
|
+
paymentDetailsFilter.htlcStatus !== undefined &&
|
|
262
|
+
paymentDetailsFilter.htlcStatus.length > 0
|
|
263
|
+
) {
|
|
264
|
+
const placeholders = paymentDetailsFilter.htlcStatus.map(() => "?");
|
|
265
|
+
if (htlcAlias === "l") {
|
|
266
|
+
paymentDetailsClauses.push(
|
|
267
|
+
`l.htlc_status IN (${placeholders.join(", ")})`
|
|
268
|
+
);
|
|
269
|
+
} else {
|
|
270
|
+
paymentDetailsClauses.push(
|
|
271
|
+
`JSON_UNQUOTE(JSON_EXTRACT(s.htlc_details, '$.status')) IN (${placeholders.join(", ")})`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
params.push(...paymentDetailsFilter.htlcStatus);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (
|
|
278
|
+
(paymentDetailsFilter.type === "spark" ||
|
|
279
|
+
paymentDetailsFilter.type === "token") &&
|
|
280
|
+
paymentDetailsFilter.conversionRefundNeeded !== undefined
|
|
281
|
+
) {
|
|
282
|
+
const typeCheck =
|
|
283
|
+
paymentDetailsFilter.type === "spark"
|
|
284
|
+
? "p.spark = 1"
|
|
285
|
+
: "p.spark IS NULL";
|
|
286
|
+
const refundNeeded =
|
|
287
|
+
paymentDetailsFilter.conversionRefundNeeded === true
|
|
288
|
+
? "= 'refundNeeded'"
|
|
289
|
+
: "!= 'refundNeeded'";
|
|
290
|
+
paymentDetailsClauses.push(
|
|
291
|
+
`${typeCheck} AND pm.conversion_info IS NOT NULL AND
|
|
292
|
+
JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.status')) ${refundNeeded}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
paymentDetailsFilter.type === "token" &&
|
|
298
|
+
paymentDetailsFilter.txHash !== undefined
|
|
299
|
+
) {
|
|
300
|
+
paymentDetailsClauses.push("t.tx_hash = ?");
|
|
301
|
+
params.push(paymentDetailsFilter.txHash);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
paymentDetailsFilter.type === "token" &&
|
|
306
|
+
paymentDetailsFilter.txType !== undefined
|
|
307
|
+
) {
|
|
308
|
+
paymentDetailsClauses.push("t.tx_type = ?");
|
|
309
|
+
params.push(paymentDetailsFilter.txType);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (paymentDetailsClauses.length > 0) {
|
|
313
|
+
allPaymentDetailsClauses.push(
|
|
314
|
+
`(${paymentDetailsClauses.join(" AND ")})`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (allPaymentDetailsClauses.length > 0) {
|
|
320
|
+
whereClauses.push(`(${allPaymentDetailsClauses.join(" OR ")})`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (request.assetFilter) {
|
|
325
|
+
const assetFilter = request.assetFilter;
|
|
326
|
+
if (assetFilter.type === "bitcoin") {
|
|
327
|
+
whereClauses.push("t.metadata IS NULL");
|
|
328
|
+
} else if (assetFilter.type === "token") {
|
|
329
|
+
whereClauses.push("t.metadata IS NOT NULL");
|
|
330
|
+
if (assetFilter.tokenIdentifier) {
|
|
331
|
+
whereClauses.push(
|
|
332
|
+
"JSON_UNQUOTE(JSON_EXTRACT(t.metadata, '$.identifier')) = ?"
|
|
333
|
+
);
|
|
334
|
+
params.push(assetFilter.tokenIdentifier);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
whereClauses.push("pm.parent_payment_id IS NULL");
|
|
340
|
+
|
|
341
|
+
const whereSql =
|
|
342
|
+
whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
343
|
+
|
|
344
|
+
const orderDirection = request.sortAscending ? "ASC" : "DESC";
|
|
345
|
+
const query = `${SELECT_PAYMENT_SQL} ${whereSql} ORDER BY p.timestamp ${orderDirection} LIMIT ? OFFSET ?`;
|
|
346
|
+
|
|
347
|
+
params.push(actualLimit, actualOffset);
|
|
348
|
+
const [rows] = await this.pool.query(query, params);
|
|
349
|
+
return rows.map(this._rowToPayment.bind(this));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error instanceof StorageError) throw error;
|
|
352
|
+
throw new StorageError(
|
|
353
|
+
`Failed to list payments (request: ${JSON.stringify(request)}): ${error.message}`,
|
|
354
|
+
error
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async insertPayment(payment) {
|
|
360
|
+
try {
|
|
361
|
+
if (!payment) {
|
|
362
|
+
throw new StorageError("Payment cannot be null or undefined");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await this._withTransaction(async (conn) => {
|
|
366
|
+
const withdrawTxId =
|
|
367
|
+
payment.details?.type === "withdraw" ? payment.details.txId : null;
|
|
368
|
+
const depositTxId =
|
|
369
|
+
payment.details?.type === "deposit" ? payment.details.txId : null;
|
|
370
|
+
const spark = payment.details?.type === "spark" ? 1 : null;
|
|
371
|
+
|
|
372
|
+
await conn.query(
|
|
373
|
+
`INSERT INTO payments (user_id, id, payment_type, status, amount, fees, timestamp, method, withdraw_tx_id, deposit_tx_id, spark)
|
|
374
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
375
|
+
ON DUPLICATE KEY UPDATE
|
|
376
|
+
payment_type=VALUES(payment_type),
|
|
377
|
+
status=VALUES(status),
|
|
378
|
+
amount=VALUES(amount),
|
|
379
|
+
fees=VALUES(fees),
|
|
380
|
+
timestamp=VALUES(timestamp),
|
|
381
|
+
method=VALUES(method),
|
|
382
|
+
withdraw_tx_id=VALUES(withdraw_tx_id),
|
|
383
|
+
deposit_tx_id=VALUES(deposit_tx_id),
|
|
384
|
+
spark=VALUES(spark)`,
|
|
385
|
+
[
|
|
386
|
+
this.identity,
|
|
387
|
+
payment.id,
|
|
388
|
+
payment.paymentType,
|
|
389
|
+
payment.status,
|
|
390
|
+
payment.amount.toString(),
|
|
391
|
+
payment.fees.toString(),
|
|
392
|
+
payment.timestamp,
|
|
393
|
+
payment.method ? JSON.stringify(payment.method) : null,
|
|
394
|
+
withdrawTxId,
|
|
395
|
+
depositTxId,
|
|
396
|
+
spark,
|
|
397
|
+
]
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (
|
|
401
|
+
payment.details?.type === "spark" &&
|
|
402
|
+
(payment.details.invoiceDetails != null ||
|
|
403
|
+
payment.details.htlcDetails != null)
|
|
404
|
+
) {
|
|
405
|
+
await conn.query(
|
|
406
|
+
`INSERT INTO payment_details_spark (user_id, payment_id, invoice_details, htlc_details)
|
|
407
|
+
VALUES (?, ?, ?, ?)
|
|
408
|
+
ON DUPLICATE KEY UPDATE
|
|
409
|
+
invoice_details=COALESCE(VALUES(invoice_details), invoice_details),
|
|
410
|
+
htlc_details=COALESCE(VALUES(htlc_details), htlc_details)`,
|
|
411
|
+
[
|
|
412
|
+
this.identity,
|
|
413
|
+
payment.id,
|
|
414
|
+
payment.details.invoiceDetails
|
|
415
|
+
? JSON.stringify(payment.details.invoiceDetails)
|
|
416
|
+
: null,
|
|
417
|
+
payment.details.htlcDetails
|
|
418
|
+
? JSON.stringify(payment.details.htlcDetails)
|
|
419
|
+
: null,
|
|
420
|
+
]
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (payment.details?.type === "lightning") {
|
|
425
|
+
await conn.query(
|
|
426
|
+
`INSERT INTO payment_details_lightning
|
|
427
|
+
(user_id, payment_id, invoice, payment_hash, destination_pubkey, description, preimage, htlc_status, htlc_expiry_time)
|
|
428
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
429
|
+
ON DUPLICATE KEY UPDATE
|
|
430
|
+
invoice=VALUES(invoice),
|
|
431
|
+
payment_hash=VALUES(payment_hash),
|
|
432
|
+
destination_pubkey=VALUES(destination_pubkey),
|
|
433
|
+
description=VALUES(description),
|
|
434
|
+
preimage=COALESCE(VALUES(preimage), preimage),
|
|
435
|
+
htlc_status=COALESCE(VALUES(htlc_status), htlc_status),
|
|
436
|
+
htlc_expiry_time=COALESCE(VALUES(htlc_expiry_time), htlc_expiry_time)`,
|
|
437
|
+
[
|
|
438
|
+
this.identity,
|
|
439
|
+
payment.id,
|
|
440
|
+
payment.details.invoice,
|
|
441
|
+
payment.details.htlcDetails.paymentHash,
|
|
442
|
+
payment.details.destinationPubkey,
|
|
443
|
+
payment.details.description,
|
|
444
|
+
payment.details.htlcDetails?.preimage,
|
|
445
|
+
payment.details.htlcDetails?.status ?? null,
|
|
446
|
+
payment.details.htlcDetails?.expiryTime ?? 0,
|
|
447
|
+
]
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (payment.details?.type === "token") {
|
|
452
|
+
await conn.query(
|
|
453
|
+
`INSERT INTO payment_details_token
|
|
454
|
+
(user_id, payment_id, metadata, tx_hash, tx_type, invoice_details)
|
|
455
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
456
|
+
ON DUPLICATE KEY UPDATE
|
|
457
|
+
metadata=VALUES(metadata),
|
|
458
|
+
tx_hash=VALUES(tx_hash),
|
|
459
|
+
tx_type=VALUES(tx_type),
|
|
460
|
+
invoice_details=COALESCE(VALUES(invoice_details), invoice_details)`,
|
|
461
|
+
[
|
|
462
|
+
this.identity,
|
|
463
|
+
payment.id,
|
|
464
|
+
JSON.stringify(payment.details.metadata),
|
|
465
|
+
payment.details.txHash,
|
|
466
|
+
payment.details.txType,
|
|
467
|
+
payment.details.invoiceDetails
|
|
468
|
+
? JSON.stringify(payment.details.invoiceDetails)
|
|
469
|
+
: null,
|
|
470
|
+
]
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error instanceof StorageError) throw error;
|
|
476
|
+
throw new StorageError(
|
|
477
|
+
`Failed to insert payment '${payment.id}': ${error.message}`,
|
|
478
|
+
error
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async getPaymentById(id) {
|
|
484
|
+
try {
|
|
485
|
+
if (!id) {
|
|
486
|
+
throw new StorageError("Payment ID cannot be null or undefined");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const [rows] = await this.pool.query(
|
|
490
|
+
`${SELECT_PAYMENT_SQL} WHERE p.user_id = ? AND p.id = ?`,
|
|
491
|
+
[this.identity, id]
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
if (rows.length === 0) {
|
|
495
|
+
throw new StorageError(`Payment with id '${id}' not found`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return this._rowToPayment(rows[0]);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof StorageError) throw error;
|
|
501
|
+
throw new StorageError(
|
|
502
|
+
`Failed to get payment by id '${id || "unknown"}': ${error.message}`,
|
|
503
|
+
error
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async getPaymentByInvoice(invoice) {
|
|
509
|
+
try {
|
|
510
|
+
if (!invoice) {
|
|
511
|
+
throw new StorageError("Invoice cannot be null or undefined");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const [rows] = await this.pool.query(
|
|
515
|
+
`${SELECT_PAYMENT_SQL} WHERE p.user_id = ? AND l.invoice = ?`,
|
|
516
|
+
[this.identity, invoice]
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
if (rows.length === 0) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return this._rowToPayment(rows[0]);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (error instanceof StorageError) throw error;
|
|
526
|
+
throw new StorageError(
|
|
527
|
+
`Failed to get payment by invoice '${invoice}': ${error.message}`,
|
|
528
|
+
error
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async getPaymentsByParentIds(parentPaymentIds) {
|
|
534
|
+
try {
|
|
535
|
+
if (!parentPaymentIds || parentPaymentIds.length === 0) {
|
|
536
|
+
return {};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Early exit if no related payments exist for this tenant. mysql2 returns EXISTS as 0/1.
|
|
540
|
+
const [hasRelatedRows] = await this.pool.query(
|
|
541
|
+
"SELECT EXISTS(SELECT 1 FROM payment_metadata WHERE user_id = ? AND parent_payment_id IS NOT NULL LIMIT 1) AS has_related",
|
|
542
|
+
[this.identity]
|
|
543
|
+
);
|
|
544
|
+
if (!hasRelatedRows[0].has_related) {
|
|
545
|
+
return {};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const placeholders = parentPaymentIds.map(() => "?");
|
|
549
|
+
const query = `${SELECT_PAYMENT_SQL} WHERE p.user_id = ? AND pm.parent_payment_id IN (${placeholders.join(", ")}) ORDER BY p.timestamp ASC`;
|
|
550
|
+
|
|
551
|
+
const [rows] = await this.pool.query(query, [this.identity, ...parentPaymentIds]);
|
|
552
|
+
|
|
553
|
+
const result = {};
|
|
554
|
+
for (const row of rows) {
|
|
555
|
+
const parentId = row.parent_payment_id;
|
|
556
|
+
if (!result[parentId]) {
|
|
557
|
+
result[parentId] = [];
|
|
558
|
+
}
|
|
559
|
+
result[parentId].push(this._rowToPayment(row));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return result;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (error instanceof StorageError) throw error;
|
|
565
|
+
throw new StorageError(
|
|
566
|
+
`Failed to get payments by parent ids: ${error.message}`,
|
|
567
|
+
error
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async insertPaymentMetadata(paymentId, metadata) {
|
|
573
|
+
try {
|
|
574
|
+
await this.pool.query(
|
|
575
|
+
`INSERT INTO payment_metadata (user_id, payment_id, parent_payment_id, lnurl_pay_info, lnurl_withdraw_info, lnurl_description, conversion_info, conversion_status)
|
|
576
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
577
|
+
ON DUPLICATE KEY UPDATE
|
|
578
|
+
parent_payment_id = COALESCE(VALUES(parent_payment_id), parent_payment_id),
|
|
579
|
+
lnurl_pay_info = COALESCE(VALUES(lnurl_pay_info), lnurl_pay_info),
|
|
580
|
+
lnurl_withdraw_info = COALESCE(VALUES(lnurl_withdraw_info), lnurl_withdraw_info),
|
|
581
|
+
lnurl_description = COALESCE(VALUES(lnurl_description), lnurl_description),
|
|
582
|
+
conversion_info = COALESCE(VALUES(conversion_info), conversion_info),
|
|
583
|
+
conversion_status = COALESCE(VALUES(conversion_status), conversion_status)`,
|
|
584
|
+
[
|
|
585
|
+
this.identity,
|
|
586
|
+
paymentId,
|
|
587
|
+
metadata.parentPaymentId,
|
|
588
|
+
metadata.lnurlPayInfo
|
|
589
|
+
? JSON.stringify(metadata.lnurlPayInfo)
|
|
590
|
+
: null,
|
|
591
|
+
metadata.lnurlWithdrawInfo
|
|
592
|
+
? JSON.stringify(metadata.lnurlWithdrawInfo)
|
|
593
|
+
: null,
|
|
594
|
+
metadata.lnurlDescription,
|
|
595
|
+
metadata.conversionInfo
|
|
596
|
+
? JSON.stringify(metadata.conversionInfo)
|
|
597
|
+
: null,
|
|
598
|
+
metadata.conversionStatus ?? null,
|
|
599
|
+
]
|
|
600
|
+
);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
throw new StorageError(
|
|
603
|
+
`Failed to set payment metadata for '${paymentId}': ${error.message}`,
|
|
604
|
+
error
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ===== Deposit Operations =====
|
|
610
|
+
|
|
611
|
+
async addDeposit(txid, vout, amountSats, isMature) {
|
|
612
|
+
try {
|
|
613
|
+
await this.pool.query(
|
|
614
|
+
`INSERT INTO unclaimed_deposits (user_id, txid, vout, amount_sats, is_mature)
|
|
615
|
+
VALUES (?, ?, ?, ?, ?)
|
|
616
|
+
ON DUPLICATE KEY UPDATE is_mature = VALUES(is_mature), amount_sats = VALUES(amount_sats)`,
|
|
617
|
+
[this.identity, txid, vout, amountSats, isMature ? 1 : 0]
|
|
618
|
+
);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
throw new StorageError(
|
|
621
|
+
`Failed to add deposit '${txid}:${vout}': ${error.message}`,
|
|
622
|
+
error
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async deleteDeposit(txid, vout) {
|
|
628
|
+
try {
|
|
629
|
+
await this.pool.query(
|
|
630
|
+
"DELETE FROM unclaimed_deposits WHERE user_id = ? AND txid = ? AND vout = ?",
|
|
631
|
+
[this.identity, txid, vout]
|
|
632
|
+
);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw new StorageError(
|
|
635
|
+
`Failed to delete deposit '${txid}:${vout}': ${error.message}`,
|
|
636
|
+
error
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async listDeposits() {
|
|
642
|
+
try {
|
|
643
|
+
const [rows] = await this.pool.query(
|
|
644
|
+
"SELECT txid, vout, amount_sats, is_mature, claim_error, refund_tx, refund_tx_id FROM unclaimed_deposits WHERE user_id = ?",
|
|
645
|
+
[this.identity]
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
return rows.map((row) => ({
|
|
649
|
+
txid: row.txid,
|
|
650
|
+
vout: row.vout,
|
|
651
|
+
amountSats:
|
|
652
|
+
row.amount_sats != null ? BigInt(row.amount_sats) : BigInt(0),
|
|
653
|
+
isMature: toBool(row.is_mature) ?? true,
|
|
654
|
+
claimError: parseJson(row.claim_error),
|
|
655
|
+
refundTx: row.refund_tx,
|
|
656
|
+
refundTxId: row.refund_tx_id,
|
|
657
|
+
}));
|
|
658
|
+
} catch (error) {
|
|
659
|
+
throw new StorageError(
|
|
660
|
+
`Failed to list deposits: ${error.message}`,
|
|
661
|
+
error
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async updateDeposit(txid, vout, payload) {
|
|
667
|
+
try {
|
|
668
|
+
if (payload.type === "claimError") {
|
|
669
|
+
await this.pool.query(
|
|
670
|
+
`UPDATE unclaimed_deposits
|
|
671
|
+
SET claim_error = ?, refund_tx = NULL, refund_tx_id = NULL
|
|
672
|
+
WHERE user_id = ? AND txid = ? AND vout = ?`,
|
|
673
|
+
[JSON.stringify(payload.error), this.identity, txid, vout]
|
|
674
|
+
);
|
|
675
|
+
} else if (payload.type === "refund") {
|
|
676
|
+
await this.pool.query(
|
|
677
|
+
`UPDATE unclaimed_deposits
|
|
678
|
+
SET refund_tx = ?, refund_tx_id = ?, claim_error = NULL
|
|
679
|
+
WHERE user_id = ? AND txid = ? AND vout = ?`,
|
|
680
|
+
[payload.refundTx, payload.refundTxid, this.identity, txid, vout]
|
|
681
|
+
);
|
|
682
|
+
} else {
|
|
683
|
+
throw new StorageError(`Unknown payload type: ${payload.type}`);
|
|
684
|
+
}
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (error instanceof StorageError) throw error;
|
|
687
|
+
throw new StorageError(
|
|
688
|
+
`Failed to update deposit '${txid}:${vout}': ${error.message}`,
|
|
689
|
+
error
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async setLnurlMetadata(metadata) {
|
|
695
|
+
try {
|
|
696
|
+
await this._withTransaction(async (conn) => {
|
|
697
|
+
for (const item of metadata) {
|
|
698
|
+
await conn.query(
|
|
699
|
+
`INSERT INTO lnurl_receive_metadata (user_id, payment_hash, nostr_zap_request, nostr_zap_receipt, sender_comment)
|
|
700
|
+
VALUES (?, ?, ?, ?, ?)
|
|
701
|
+
ON DUPLICATE KEY UPDATE
|
|
702
|
+
nostr_zap_request = VALUES(nostr_zap_request),
|
|
703
|
+
nostr_zap_receipt = VALUES(nostr_zap_receipt),
|
|
704
|
+
sender_comment = VALUES(sender_comment)`,
|
|
705
|
+
[
|
|
706
|
+
this.identity,
|
|
707
|
+
item.paymentHash,
|
|
708
|
+
item.nostrZapRequest || null,
|
|
709
|
+
item.nostrZapReceipt || null,
|
|
710
|
+
item.senderComment || null,
|
|
711
|
+
]
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error instanceof StorageError) throw error;
|
|
717
|
+
throw new StorageError(
|
|
718
|
+
`Failed to add lnurl metadata: ${error.message}`,
|
|
719
|
+
error
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ===== Private Helper Methods =====
|
|
725
|
+
|
|
726
|
+
_rowToPayment(row) {
|
|
727
|
+
let details = null;
|
|
728
|
+
if (row.lightning_invoice) {
|
|
729
|
+
details = {
|
|
730
|
+
type: "lightning",
|
|
731
|
+
invoice: row.lightning_invoice,
|
|
732
|
+
destinationPubkey: row.lightning_destination_pubkey,
|
|
733
|
+
description: row.lightning_description,
|
|
734
|
+
htlcDetails: row.lightning_htlc_status
|
|
735
|
+
? {
|
|
736
|
+
paymentHash: row.lightning_payment_hash,
|
|
737
|
+
preimage: row.lightning_preimage || null,
|
|
738
|
+
expiryTime: Number(row.lightning_htlc_expiry_time) ?? 0,
|
|
739
|
+
status: row.lightning_htlc_status,
|
|
740
|
+
}
|
|
741
|
+
: (() => {
|
|
742
|
+
throw new StorageError(
|
|
743
|
+
`htlc_status is required for Lightning payment ${row.id}`
|
|
744
|
+
);
|
|
745
|
+
})(),
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
if (row.lnurl_pay_info) {
|
|
749
|
+
details.lnurlPayInfo = parseJson(row.lnurl_pay_info);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (row.lnurl_withdraw_info) {
|
|
753
|
+
details.lnurlWithdrawInfo = parseJson(row.lnurl_withdraw_info);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (row.lnurl_payment_hash) {
|
|
757
|
+
details.lnurlReceiveMetadata = {
|
|
758
|
+
nostrZapRequest: row.lnurl_nostr_zap_request || null,
|
|
759
|
+
nostrZapReceipt: row.lnurl_nostr_zap_receipt || null,
|
|
760
|
+
senderComment: row.lnurl_sender_comment || null,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
} else if (row.withdraw_tx_id) {
|
|
764
|
+
details = {
|
|
765
|
+
type: "withdraw",
|
|
766
|
+
txId: row.withdraw_tx_id,
|
|
767
|
+
};
|
|
768
|
+
} else if (row.deposit_tx_id) {
|
|
769
|
+
details = {
|
|
770
|
+
type: "deposit",
|
|
771
|
+
txId: row.deposit_tx_id,
|
|
772
|
+
};
|
|
773
|
+
} else if (toBool(row.spark)) {
|
|
774
|
+
details = {
|
|
775
|
+
type: "spark",
|
|
776
|
+
invoiceDetails: parseJson(row.spark_invoice_details),
|
|
777
|
+
htlcDetails: parseJson(row.spark_htlc_details),
|
|
778
|
+
conversionInfo: parseJson(row.conversion_info),
|
|
779
|
+
};
|
|
780
|
+
} else if (row.token_metadata) {
|
|
781
|
+
details = {
|
|
782
|
+
type: "token",
|
|
783
|
+
metadata: parseJson(row.token_metadata),
|
|
784
|
+
txHash: row.token_tx_hash,
|
|
785
|
+
txType: row.token_tx_type,
|
|
786
|
+
invoiceDetails: parseJson(row.token_invoice_details),
|
|
787
|
+
conversionInfo: parseJson(row.conversion_info),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
let method = null;
|
|
792
|
+
if (row.method) {
|
|
793
|
+
try {
|
|
794
|
+
method = parseJson(row.method);
|
|
795
|
+
} catch (e) {
|
|
796
|
+
throw new StorageError(
|
|
797
|
+
`Failed to parse payment method JSON for payment ${row.id}: ${e.message}`,
|
|
798
|
+
e
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
id: row.id,
|
|
805
|
+
paymentType: row.payment_type,
|
|
806
|
+
status: row.status,
|
|
807
|
+
amount: BigInt(row.amount),
|
|
808
|
+
fees: BigInt(row.fees),
|
|
809
|
+
timestamp: Number(row.timestamp),
|
|
810
|
+
method,
|
|
811
|
+
details,
|
|
812
|
+
conversionDetails: row.conversion_status
|
|
813
|
+
? { status: row.conversion_status, from: null, to: null }
|
|
814
|
+
: null,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ===== Contact Operations =====
|
|
819
|
+
|
|
820
|
+
async listContacts(request) {
|
|
821
|
+
try {
|
|
822
|
+
const offset = request.offset != null ? request.offset : 0;
|
|
823
|
+
const limit = request.limit != null ? request.limit : 4294967295;
|
|
824
|
+
|
|
825
|
+
const [rows] = await this.pool.query(
|
|
826
|
+
`SELECT id, name, payment_identifier, created_at, updated_at
|
|
827
|
+
FROM contacts
|
|
828
|
+
WHERE user_id = ?
|
|
829
|
+
ORDER BY name ASC
|
|
830
|
+
LIMIT ? OFFSET ?`,
|
|
831
|
+
[this.identity, limit, offset]
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
return rows.map((row) => ({
|
|
835
|
+
id: row.id,
|
|
836
|
+
name: row.name,
|
|
837
|
+
paymentIdentifier: row.payment_identifier,
|
|
838
|
+
createdAt: Number(row.created_at),
|
|
839
|
+
updatedAt: Number(row.updated_at),
|
|
840
|
+
}));
|
|
841
|
+
} catch (error) {
|
|
842
|
+
throw new StorageError(
|
|
843
|
+
`Failed to list contacts: ${error.message}`,
|
|
844
|
+
error
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async getContact(id) {
|
|
850
|
+
try {
|
|
851
|
+
const [rows] = await this.pool.query(
|
|
852
|
+
`SELECT id, name, payment_identifier, created_at, updated_at
|
|
853
|
+
FROM contacts
|
|
854
|
+
WHERE user_id = ? AND id = ?`,
|
|
855
|
+
[this.identity, id]
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
if (rows.length === 0) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const row = rows[0];
|
|
863
|
+
return {
|
|
864
|
+
id: row.id,
|
|
865
|
+
name: row.name,
|
|
866
|
+
paymentIdentifier: row.payment_identifier,
|
|
867
|
+
createdAt: Number(row.created_at),
|
|
868
|
+
updatedAt: Number(row.updated_at),
|
|
869
|
+
};
|
|
870
|
+
} catch (error) {
|
|
871
|
+
throw new StorageError(`Failed to get contact: ${error.message}`, error);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async insertContact(contact) {
|
|
876
|
+
try {
|
|
877
|
+
await this.pool.query(
|
|
878
|
+
`INSERT INTO contacts (user_id, id, name, payment_identifier, created_at, updated_at)
|
|
879
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
880
|
+
ON DUPLICATE KEY UPDATE
|
|
881
|
+
name = VALUES(name),
|
|
882
|
+
payment_identifier = VALUES(payment_identifier),
|
|
883
|
+
updated_at = VALUES(updated_at)`,
|
|
884
|
+
[
|
|
885
|
+
this.identity,
|
|
886
|
+
contact.id,
|
|
887
|
+
contact.name,
|
|
888
|
+
contact.paymentIdentifier,
|
|
889
|
+
contact.createdAt,
|
|
890
|
+
contact.updatedAt,
|
|
891
|
+
]
|
|
892
|
+
);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
throw new StorageError(
|
|
895
|
+
`Failed to insert contact: ${error.message}`,
|
|
896
|
+
error
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async deleteContact(id) {
|
|
902
|
+
try {
|
|
903
|
+
await this.pool.query(
|
|
904
|
+
"DELETE FROM contacts WHERE user_id = ? AND id = ?",
|
|
905
|
+
[this.identity, id]
|
|
906
|
+
);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
throw new StorageError(
|
|
909
|
+
`Failed to delete contact: ${error.message}`,
|
|
910
|
+
error
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ===== Sync Operations =====
|
|
916
|
+
|
|
917
|
+
async syncAddOutgoingChange(record) {
|
|
918
|
+
try {
|
|
919
|
+
return await this._withTransaction(async (conn) => {
|
|
920
|
+
// The local queue revision is per-tenant — two tenants don't share a queue.
|
|
921
|
+
const [revisionRows] = await conn.query(
|
|
922
|
+
"SELECT COALESCE(MAX(revision), 0) + 1 AS revision FROM sync_outgoing WHERE user_id = ?",
|
|
923
|
+
[this.identity]
|
|
924
|
+
);
|
|
925
|
+
const revision = BigInt(revisionRows[0].revision);
|
|
926
|
+
|
|
927
|
+
await conn.query(
|
|
928
|
+
`INSERT INTO sync_outgoing (
|
|
929
|
+
user_id,
|
|
930
|
+
record_type,
|
|
931
|
+
data_id,
|
|
932
|
+
schema_version,
|
|
933
|
+
commit_time,
|
|
934
|
+
updated_fields_json,
|
|
935
|
+
revision
|
|
936
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
937
|
+
[
|
|
938
|
+
this.identity,
|
|
939
|
+
record.id.type,
|
|
940
|
+
record.id.dataId,
|
|
941
|
+
record.schemaVersion,
|
|
942
|
+
Math.floor(Date.now() / 1000),
|
|
943
|
+
JSON.stringify(record.updatedFields),
|
|
944
|
+
revision.toString(),
|
|
945
|
+
]
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
return revision;
|
|
949
|
+
});
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (error instanceof StorageError) throw error;
|
|
952
|
+
throw new StorageError(
|
|
953
|
+
`Failed to add outgoing change: ${error.message}`,
|
|
954
|
+
error
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async syncCompleteOutgoingSync(record, localRevision) {
|
|
960
|
+
try {
|
|
961
|
+
await this._withTransaction(async (conn) => {
|
|
962
|
+
const [deleteResult] = await conn.query(
|
|
963
|
+
"DELETE FROM sync_outgoing WHERE user_id = ? AND record_type = ? AND data_id = ? AND revision = ?",
|
|
964
|
+
[this.identity, record.id.type, record.id.dataId, localRevision.toString()]
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
if (deleteResult.affectedRows === 0) {
|
|
968
|
+
const msg = `complete_outgoing_sync: DELETE from sync_outgoing matched 0 rows (type=${record.id.type}, data_id=${record.id.dataId}, revision=${localRevision})`;
|
|
969
|
+
if (this.logger && typeof this.logger.log === "function") {
|
|
970
|
+
this.logger.log({ line: msg, level: "warn" });
|
|
971
|
+
} else {
|
|
972
|
+
// eslint-disable-next-line no-console
|
|
973
|
+
console.warn(`[MysqlStorage] ${msg}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
await conn.query(
|
|
978
|
+
`INSERT INTO sync_state (
|
|
979
|
+
user_id,
|
|
980
|
+
record_type,
|
|
981
|
+
data_id,
|
|
982
|
+
revision,
|
|
983
|
+
schema_version,
|
|
984
|
+
commit_time,
|
|
985
|
+
data
|
|
986
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
987
|
+
ON DUPLICATE KEY UPDATE
|
|
988
|
+
schema_version = VALUES(schema_version),
|
|
989
|
+
commit_time = VALUES(commit_time),
|
|
990
|
+
data = VALUES(data),
|
|
991
|
+
revision = VALUES(revision)`,
|
|
992
|
+
[
|
|
993
|
+
this.identity,
|
|
994
|
+
record.id.type,
|
|
995
|
+
record.id.dataId,
|
|
996
|
+
record.revision.toString(),
|
|
997
|
+
record.schemaVersion,
|
|
998
|
+
Math.floor(Date.now() / 1000),
|
|
999
|
+
JSON.stringify(record.data),
|
|
1000
|
+
]
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
// Upsert this tenant's revision row; fresh tenants without a row get one.
|
|
1004
|
+
await conn.query(
|
|
1005
|
+
`INSERT INTO sync_revision (user_id, revision) VALUES (?, ?)
|
|
1006
|
+
ON DUPLICATE KEY UPDATE revision = GREATEST(revision, VALUES(revision))`,
|
|
1007
|
+
[this.identity, record.revision.toString()]
|
|
1008
|
+
);
|
|
1009
|
+
});
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (error instanceof StorageError) throw error;
|
|
1012
|
+
throw new StorageError(
|
|
1013
|
+
`Failed to complete outgoing sync: ${error.message}`,
|
|
1014
|
+
error
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async syncGetPendingOutgoingChanges(limit) {
|
|
1020
|
+
try {
|
|
1021
|
+
const [rows] = await this.pool.query(
|
|
1022
|
+
`SELECT
|
|
1023
|
+
o.record_type,
|
|
1024
|
+
o.data_id,
|
|
1025
|
+
o.schema_version,
|
|
1026
|
+
o.commit_time,
|
|
1027
|
+
o.updated_fields_json,
|
|
1028
|
+
o.revision,
|
|
1029
|
+
e.schema_version AS existing_schema_version,
|
|
1030
|
+
e.commit_time AS existing_commit_time,
|
|
1031
|
+
e.data AS existing_data,
|
|
1032
|
+
e.revision AS existing_revision
|
|
1033
|
+
FROM sync_outgoing o
|
|
1034
|
+
LEFT JOIN sync_state e ON
|
|
1035
|
+
o.record_type = e.record_type AND
|
|
1036
|
+
o.data_id = e.data_id AND
|
|
1037
|
+
o.user_id = e.user_id
|
|
1038
|
+
WHERE o.user_id = ?
|
|
1039
|
+
ORDER BY o.revision ASC
|
|
1040
|
+
LIMIT ?`,
|
|
1041
|
+
[this.identity, limit]
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
return rows.map((row) => {
|
|
1045
|
+
const change = {
|
|
1046
|
+
id: { type: row.record_type, dataId: row.data_id },
|
|
1047
|
+
schemaVersion: row.schema_version,
|
|
1048
|
+
updatedFields: parseJson(row.updated_fields_json),
|
|
1049
|
+
localRevision: BigInt(row.revision),
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
let parent = null;
|
|
1053
|
+
if (row.existing_data) {
|
|
1054
|
+
parent = {
|
|
1055
|
+
id: { type: row.record_type, dataId: row.data_id },
|
|
1056
|
+
revision: BigInt(row.existing_revision),
|
|
1057
|
+
schemaVersion: row.existing_schema_version,
|
|
1058
|
+
data: parseJson(row.existing_data),
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return { change, parent };
|
|
1063
|
+
});
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
throw new StorageError(
|
|
1066
|
+
`Failed to get pending outgoing changes: ${error.message}`,
|
|
1067
|
+
error
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async syncGetLastRevision() {
|
|
1073
|
+
try {
|
|
1074
|
+
// A tenant that hasn't synced anything yet may have no row; treat as 0.
|
|
1075
|
+
const [rows] = await this.pool.query(
|
|
1076
|
+
"SELECT revision FROM sync_revision WHERE user_id = ?",
|
|
1077
|
+
[this.identity]
|
|
1078
|
+
);
|
|
1079
|
+
return rows.length > 0 ? BigInt(rows[0].revision) : BigInt(0);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
throw new StorageError(
|
|
1082
|
+
`Failed to get last revision: ${error.message}`,
|
|
1083
|
+
error
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async syncInsertIncomingRecords(records) {
|
|
1089
|
+
try {
|
|
1090
|
+
if (!records || records.length === 0) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
await this._withTransaction(async (conn) => {
|
|
1095
|
+
for (const record of records) {
|
|
1096
|
+
await conn.query(
|
|
1097
|
+
`INSERT INTO sync_incoming (
|
|
1098
|
+
user_id,
|
|
1099
|
+
record_type,
|
|
1100
|
+
data_id,
|
|
1101
|
+
schema_version,
|
|
1102
|
+
commit_time,
|
|
1103
|
+
data,
|
|
1104
|
+
revision
|
|
1105
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1106
|
+
ON DUPLICATE KEY UPDATE
|
|
1107
|
+
schema_version = VALUES(schema_version),
|
|
1108
|
+
commit_time = VALUES(commit_time),
|
|
1109
|
+
data = VALUES(data)`,
|
|
1110
|
+
[
|
|
1111
|
+
this.identity,
|
|
1112
|
+
record.id.type,
|
|
1113
|
+
record.id.dataId,
|
|
1114
|
+
record.schemaVersion,
|
|
1115
|
+
Math.floor(Date.now() / 1000),
|
|
1116
|
+
JSON.stringify(record.data),
|
|
1117
|
+
record.revision.toString(),
|
|
1118
|
+
]
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
if (error instanceof StorageError) throw error;
|
|
1124
|
+
throw new StorageError(
|
|
1125
|
+
`Failed to insert incoming records: ${error.message}`,
|
|
1126
|
+
error
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async syncDeleteIncomingRecord(record) {
|
|
1132
|
+
try {
|
|
1133
|
+
await this.pool.query(
|
|
1134
|
+
`DELETE FROM sync_incoming
|
|
1135
|
+
WHERE user_id = ?
|
|
1136
|
+
AND record_type = ?
|
|
1137
|
+
AND data_id = ?
|
|
1138
|
+
AND revision = ?`,
|
|
1139
|
+
[this.identity, record.id.type, record.id.dataId, record.revision.toString()]
|
|
1140
|
+
);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
throw new StorageError(
|
|
1143
|
+
`Failed to delete incoming record: ${error.message}`,
|
|
1144
|
+
error
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async syncGetIncomingRecords(limit) {
|
|
1150
|
+
try {
|
|
1151
|
+
const [rows] = await this.pool.query(
|
|
1152
|
+
`SELECT i.record_type,
|
|
1153
|
+
i.data_id,
|
|
1154
|
+
i.schema_version,
|
|
1155
|
+
i.data,
|
|
1156
|
+
i.revision,
|
|
1157
|
+
e.schema_version AS existing_schema_version,
|
|
1158
|
+
e.commit_time AS existing_commit_time,
|
|
1159
|
+
e.data AS existing_data,
|
|
1160
|
+
e.revision AS existing_revision
|
|
1161
|
+
FROM sync_incoming i
|
|
1162
|
+
LEFT JOIN sync_state e ON i.record_type = e.record_type AND i.data_id = e.data_id AND i.user_id = e.user_id
|
|
1163
|
+
WHERE i.user_id = ?
|
|
1164
|
+
ORDER BY i.revision ASC
|
|
1165
|
+
LIMIT ?`,
|
|
1166
|
+
[this.identity, limit]
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
return rows.map((row) => {
|
|
1170
|
+
const newState = {
|
|
1171
|
+
id: { type: row.record_type, dataId: row.data_id },
|
|
1172
|
+
revision: BigInt(row.revision),
|
|
1173
|
+
schemaVersion: row.schema_version,
|
|
1174
|
+
data: parseJson(row.data),
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
let oldState = null;
|
|
1178
|
+
if (row.existing_data) {
|
|
1179
|
+
oldState = {
|
|
1180
|
+
id: { type: row.record_type, dataId: row.data_id },
|
|
1181
|
+
revision: BigInt(row.existing_revision),
|
|
1182
|
+
schemaVersion: row.existing_schema_version,
|
|
1183
|
+
data: parseJson(row.existing_data),
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return { newState, oldState };
|
|
1188
|
+
});
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
throw new StorageError(
|
|
1191
|
+
`Failed to get incoming records: ${error.message}`,
|
|
1192
|
+
error
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async syncGetLatestOutgoingChange() {
|
|
1198
|
+
try {
|
|
1199
|
+
const [rows] = await this.pool.query(
|
|
1200
|
+
`SELECT
|
|
1201
|
+
o.record_type,
|
|
1202
|
+
o.data_id,
|
|
1203
|
+
o.schema_version,
|
|
1204
|
+
o.commit_time,
|
|
1205
|
+
o.updated_fields_json,
|
|
1206
|
+
o.revision,
|
|
1207
|
+
e.schema_version AS existing_schema_version,
|
|
1208
|
+
e.commit_time AS existing_commit_time,
|
|
1209
|
+
e.data AS existing_data,
|
|
1210
|
+
e.revision AS existing_revision
|
|
1211
|
+
FROM sync_outgoing o
|
|
1212
|
+
LEFT JOIN sync_state e ON
|
|
1213
|
+
o.record_type = e.record_type AND
|
|
1214
|
+
o.data_id = e.data_id AND
|
|
1215
|
+
o.user_id = e.user_id
|
|
1216
|
+
WHERE o.user_id = ?
|
|
1217
|
+
ORDER BY o.revision DESC
|
|
1218
|
+
LIMIT 1`,
|
|
1219
|
+
[this.identity]
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
if (rows.length === 0) {
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const row = rows[0];
|
|
1227
|
+
|
|
1228
|
+
const change = {
|
|
1229
|
+
id: { type: row.record_type, dataId: row.data_id },
|
|
1230
|
+
schemaVersion: row.schema_version,
|
|
1231
|
+
updatedFields: parseJson(row.updated_fields_json),
|
|
1232
|
+
localRevision: BigInt(row.revision),
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
let parent = null;
|
|
1236
|
+
if (row.existing_data) {
|
|
1237
|
+
parent = {
|
|
1238
|
+
id: { type: row.record_type, dataId: row.data_id },
|
|
1239
|
+
revision: BigInt(row.existing_revision),
|
|
1240
|
+
schemaVersion: row.existing_schema_version,
|
|
1241
|
+
data: parseJson(row.existing_data),
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return { change, parent };
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
throw new StorageError(
|
|
1248
|
+
`Failed to get latest outgoing change: ${error.message}`,
|
|
1249
|
+
error
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async syncUpdateRecordFromIncoming(record) {
|
|
1255
|
+
try {
|
|
1256
|
+
await this._withTransaction(async (conn) => {
|
|
1257
|
+
await conn.query(
|
|
1258
|
+
`INSERT INTO sync_state (
|
|
1259
|
+
user_id,
|
|
1260
|
+
record_type,
|
|
1261
|
+
data_id,
|
|
1262
|
+
revision,
|
|
1263
|
+
schema_version,
|
|
1264
|
+
commit_time,
|
|
1265
|
+
data
|
|
1266
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1267
|
+
ON DUPLICATE KEY UPDATE
|
|
1268
|
+
schema_version = VALUES(schema_version),
|
|
1269
|
+
commit_time = VALUES(commit_time),
|
|
1270
|
+
data = VALUES(data),
|
|
1271
|
+
revision = VALUES(revision)`,
|
|
1272
|
+
[
|
|
1273
|
+
this.identity,
|
|
1274
|
+
record.id.type,
|
|
1275
|
+
record.id.dataId,
|
|
1276
|
+
record.revision.toString(),
|
|
1277
|
+
record.schemaVersion,
|
|
1278
|
+
Math.floor(Date.now() / 1000),
|
|
1279
|
+
JSON.stringify(record.data),
|
|
1280
|
+
]
|
|
1281
|
+
);
|
|
1282
|
+
|
|
1283
|
+
await conn.query(
|
|
1284
|
+
`INSERT INTO sync_revision (user_id, revision) VALUES (?, ?)
|
|
1285
|
+
ON DUPLICATE KEY UPDATE revision = GREATEST(revision, VALUES(revision))`,
|
|
1286
|
+
[this.identity, record.revision.toString()]
|
|
1287
|
+
);
|
|
1288
|
+
});
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (error instanceof StorageError) throw error;
|
|
1291
|
+
throw new StorageError(
|
|
1292
|
+
`Failed to update record from incoming: ${error.message}`,
|
|
1293
|
+
error
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Creates a MysqlStorageConfig with the given connection string and default pool settings.
|
|
1301
|
+
*
|
|
1302
|
+
* Default values:
|
|
1303
|
+
* - maxPoolSize: 10
|
|
1304
|
+
* - createTimeoutSecs: 0 (no timeout)
|
|
1305
|
+
* - recycleTimeoutSecs: 10 (10 seconds idle before disconnect)
|
|
1306
|
+
*
|
|
1307
|
+
* @param {string} connectionString - MySQL connection URL (mysql://user:pass@host:3306/db)
|
|
1308
|
+
* @returns {object} MySQL storage configuration
|
|
1309
|
+
*/
|
|
1310
|
+
function defaultMysqlStorageConfig(connectionString) {
|
|
1311
|
+
return {
|
|
1312
|
+
connectionString,
|
|
1313
|
+
maxPoolSize: 10,
|
|
1314
|
+
createTimeoutSecs: 0,
|
|
1315
|
+
recycleTimeoutSecs: 10,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Create a mysql2 pool from a config object.
|
|
1321
|
+
* The returned pool can be shared across multiple store implementations.
|
|
1322
|
+
*/
|
|
1323
|
+
function createMysqlPool(config) {
|
|
1324
|
+
return mysql.createPool({
|
|
1325
|
+
uri: config.connectionString,
|
|
1326
|
+
connectionLimit: config.maxPoolSize,
|
|
1327
|
+
connectTimeout: (config.createTimeoutSecs || 0) * 1000 || 10000,
|
|
1328
|
+
idleTimeout: (config.recycleTimeoutSecs || 0) * 1000 || 10000,
|
|
1329
|
+
waitForConnections: true,
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Create a MysqlStorage instance from an existing mysql2 pool.
|
|
1335
|
+
*
|
|
1336
|
+
* @param {import('mysql2/promise').Pool} pool - An existing connection pool
|
|
1337
|
+
* @param {Buffer|Uint8Array} identity - 33-byte tenant identity (secp256k1 pubkey)
|
|
1338
|
+
* @param {object} [logger]
|
|
1339
|
+
*/
|
|
1340
|
+
async function createMysqlStorageWithPool(pool, identity, logger = null) {
|
|
1341
|
+
const storage = new MysqlStorage(pool, identity, logger);
|
|
1342
|
+
await storage.initialize();
|
|
1343
|
+
return storage;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Create a MysqlStorage instance from a config object.
|
|
1348
|
+
* Use defaultMysqlStorageConfig to create a config with sensible defaults.
|
|
1349
|
+
*
|
|
1350
|
+
* @param {object} config - MySQL storage config
|
|
1351
|
+
* @param {Buffer|Uint8Array} identity - 33-byte tenant identity (secp256k1 pubkey)
|
|
1352
|
+
* @param {object} [logger]
|
|
1353
|
+
*/
|
|
1354
|
+
async function createMysqlStorage(config, identity, logger = null) {
|
|
1355
|
+
const pool = createMysqlPool(config);
|
|
1356
|
+
return createMysqlStorageWithPool(pool, identity, logger);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
module.exports = {
|
|
1360
|
+
MysqlStorage,
|
|
1361
|
+
createMysqlStorage,
|
|
1362
|
+
createMysqlPool,
|
|
1363
|
+
createMysqlStorageWithPool,
|
|
1364
|
+
defaultMysqlStorageConfig,
|
|
1365
|
+
StorageError,
|
|
1366
|
+
};
|