@breeztech/breez-sdk-spark 0.16.1-dev1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/breez-sdk-spark.tgz +0 -0
- package/bundler/breez_sdk_spark_wasm.d.ts +94 -23
- package/bundler/breez_sdk_spark_wasm.js +1 -1
- package/bundler/breez_sdk_spark_wasm_bg.js +188 -46
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -10
- package/bundler/storage/index.js +153 -21
- package/deno/breez_sdk_spark_wasm.d.ts +94 -23
- package/deno/breez_sdk_spark_wasm.js +188 -46
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -10
- package/nodejs/breez_sdk_spark_wasm.d.ts +94 -23
- package/nodejs/breez_sdk_spark_wasm.js +189 -46
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -10
- package/nodejs/index.mjs +1 -0
- package/nodejs/mysql-storage/index.cjs +129 -14
- package/nodejs/mysql-storage/migrations.cjs +31 -1
- package/nodejs/postgres-storage/index.cjs +122 -12
- package/nodejs/postgres-storage/migrations.cjs +28 -1
- package/nodejs/storage/index.cjs +118 -12
- package/nodejs/storage/migrations.cjs +25 -1
- package/package.json +1 -1
- package/ssr/index.js +5 -0
- package/web/breez_sdk_spark_wasm.d.ts +109 -33
- package/web/breez_sdk_spark_wasm.js +188 -46
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -10
- package/web/storage/index.js +153 -21
|
@@ -105,6 +105,21 @@ function toBool(value) {
|
|
|
105
105
|
return value === 1 || value === "1" || value === true;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Maps a brz_cross_chain_swaps row to the camelCase StoredCrossChainSwap shape
|
|
110
|
+
* the SDK expects, coercing TINYINT(1) to a boolean.
|
|
111
|
+
*/
|
|
112
|
+
function crossChainSwapFromRow(row) {
|
|
113
|
+
return {
|
|
114
|
+
provider: row.provider,
|
|
115
|
+
id: row.id,
|
|
116
|
+
isTerminal: toBool(row.is_terminal),
|
|
117
|
+
updatedAt: Number(row.updated_at),
|
|
118
|
+
data: row.data,
|
|
119
|
+
secrets: row.secrets,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
class MysqlStorage {
|
|
109
124
|
/**
|
|
110
125
|
* @param {import('mysql2/promise').Pool} pool - Connection pool (may be shared with other tenants).
|
|
@@ -260,6 +275,17 @@ class MysqlStorage {
|
|
|
260
275
|
for (const paymentDetailsFilter of request.paymentDetailsFilter) {
|
|
261
276
|
const paymentDetailsClauses = [];
|
|
262
277
|
|
|
278
|
+
// Base type check: ensure payment type matches the filter type.
|
|
279
|
+
// Token payments are identified by spark = NULL AND a token row
|
|
280
|
+
// joined in via t.tx_hash.
|
|
281
|
+
if (paymentDetailsFilter.type === "spark") {
|
|
282
|
+
paymentDetailsClauses.push("p.spark = 1");
|
|
283
|
+
} else if (paymentDetailsFilter.type === "token") {
|
|
284
|
+
paymentDetailsClauses.push("p.spark IS NULL AND t.tx_hash IS NOT NULL");
|
|
285
|
+
} else if (paymentDetailsFilter.type === "lightning") {
|
|
286
|
+
paymentDetailsClauses.push("l.htlc_status IS NOT NULL");
|
|
287
|
+
}
|
|
288
|
+
|
|
263
289
|
const htlcAlias =
|
|
264
290
|
paymentDetailsFilter.type === "spark"
|
|
265
291
|
? "s"
|
|
@@ -284,23 +310,40 @@ class MysqlStorage {
|
|
|
284
310
|
params.push(...paymentDetailsFilter.htlcStatus);
|
|
285
311
|
}
|
|
286
312
|
|
|
313
|
+
// Conversion filter — same enum shape as the Rust storage backend:
|
|
314
|
+
// "ammRefundNeeded" → AMM conversion that needs a clawback
|
|
315
|
+
// "orchestraPending" → Orchestra order not yet terminal
|
|
316
|
+
// "boltzPending" → Boltz reverse swap not yet terminal
|
|
317
|
+
// (lives on the Lightning hold-invoice leg)
|
|
287
318
|
if (
|
|
288
319
|
(paymentDetailsFilter.type === "spark" ||
|
|
289
|
-
paymentDetailsFilter.type === "token"
|
|
290
|
-
|
|
320
|
+
paymentDetailsFilter.type === "token" ||
|
|
321
|
+
paymentDetailsFilter.type === "lightning") &&
|
|
322
|
+
paymentDetailsFilter.conversionFilter !== undefined
|
|
291
323
|
) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
let statusClause;
|
|
325
|
+
if (paymentDetailsFilter.conversionFilter === "ammRefundNeeded") {
|
|
326
|
+
statusClause =
|
|
327
|
+
"JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.type')) = 'amm' AND \
|
|
328
|
+
JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.status')) = 'refundNeeded'";
|
|
329
|
+
} else if (
|
|
330
|
+
paymentDetailsFilter.conversionFilter === "orchestraPending"
|
|
331
|
+
) {
|
|
332
|
+
statusClause =
|
|
333
|
+
"JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.type')) = 'orchestra' AND \
|
|
334
|
+
JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.status')) NOT IN ('completed', 'failed', 'refunded')";
|
|
335
|
+
} else if (
|
|
336
|
+
paymentDetailsFilter.conversionFilter === "boltzPending"
|
|
337
|
+
) {
|
|
338
|
+
statusClause =
|
|
339
|
+
"JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.type')) = 'boltz' AND \
|
|
340
|
+
JSON_UNQUOTE(JSON_EXTRACT(pm.conversion_info, '$.status')) NOT IN ('completed', 'failed', 'refunded')";
|
|
341
|
+
}
|
|
342
|
+
if (statusClause) {
|
|
343
|
+
paymentDetailsClauses.push(
|
|
344
|
+
`pm.conversion_info IS NOT NULL AND ${statusClause}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
304
347
|
}
|
|
305
348
|
|
|
306
349
|
if (
|
|
@@ -877,6 +920,10 @@ class MysqlStorage {
|
|
|
877
920
|
senderComment: row.lnurl_sender_comment || null,
|
|
878
921
|
};
|
|
879
922
|
}
|
|
923
|
+
|
|
924
|
+
if (row.conversion_info) {
|
|
925
|
+
details.conversionInfo = parseJson(row.conversion_info);
|
|
926
|
+
}
|
|
880
927
|
} else if (row.withdraw_tx_id) {
|
|
881
928
|
details = {
|
|
882
929
|
type: "withdraw",
|
|
@@ -1030,6 +1077,74 @@ class MysqlStorage {
|
|
|
1030
1077
|
}
|
|
1031
1078
|
}
|
|
1032
1079
|
|
|
1080
|
+
// ===== Cross-Chain Swap Operations =====
|
|
1081
|
+
|
|
1082
|
+
async setCrossChainSwap(swap) {
|
|
1083
|
+
try {
|
|
1084
|
+
await this.pool.query(
|
|
1085
|
+
`INSERT INTO brz_cross_chain_swaps
|
|
1086
|
+
(user_id, provider, id, is_terminal, updated_at, data, secrets)
|
|
1087
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1088
|
+
ON DUPLICATE KEY UPDATE
|
|
1089
|
+
is_terminal = VALUES(is_terminal),
|
|
1090
|
+
updated_at = VALUES(updated_at),
|
|
1091
|
+
data = VALUES(data),
|
|
1092
|
+
secrets = VALUES(secrets)`,
|
|
1093
|
+
[
|
|
1094
|
+
this.identity,
|
|
1095
|
+
swap.provider,
|
|
1096
|
+
swap.id,
|
|
1097
|
+
swap.isTerminal ? 1 : 0,
|
|
1098
|
+
swap.updatedAt,
|
|
1099
|
+
swap.data,
|
|
1100
|
+
swap.secrets,
|
|
1101
|
+
]
|
|
1102
|
+
);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
throw new StorageError(
|
|
1105
|
+
`Failed to set cross-chain swap: ${error.message}`,
|
|
1106
|
+
error
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async getCrossChainSwap(provider, id) {
|
|
1112
|
+
try {
|
|
1113
|
+
const [rows] = await this.pool.query(
|
|
1114
|
+
`SELECT provider, id, is_terminal, updated_at, data, secrets
|
|
1115
|
+
FROM brz_cross_chain_swaps
|
|
1116
|
+
WHERE user_id = ? AND provider = ? AND id = ?`,
|
|
1117
|
+
[this.identity, provider, id]
|
|
1118
|
+
);
|
|
1119
|
+
if (rows.length === 0) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
return crossChainSwapFromRow(rows[0]);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
throw new StorageError(
|
|
1125
|
+
`Failed to get cross-chain swap: ${error.message}`,
|
|
1126
|
+
error
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async listActiveCrossChainSwaps(provider) {
|
|
1132
|
+
try {
|
|
1133
|
+
const [rows] = await this.pool.query(
|
|
1134
|
+
`SELECT provider, id, is_terminal, updated_at, data, secrets
|
|
1135
|
+
FROM brz_cross_chain_swaps
|
|
1136
|
+
WHERE user_id = ? AND provider = ? AND is_terminal = FALSE`,
|
|
1137
|
+
[this.identity, provider]
|
|
1138
|
+
);
|
|
1139
|
+
return rows.map(crossChainSwapFromRow);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
throw new StorageError(
|
|
1142
|
+
`Failed to list active cross-chain swaps: ${error.message}`,
|
|
1143
|
+
error
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1033
1148
|
// ===== Sync Operations =====
|
|
1034
1149
|
|
|
1035
1150
|
async syncAddOutgoingChange(record) {
|
|
@@ -513,7 +513,7 @@ class MysqlMigrationManager {
|
|
|
513
513
|
// the schema matches brz_payment_details_lightning / _token / _spark. We
|
|
514
514
|
// can't safely backfill the new table from the dropped deposit_tx_id
|
|
515
515
|
// column: we never stored the original SSP output_index, and vout=0 is a
|
|
516
|
-
// valid output index
|
|
516
|
+
// valid output index, so defaulting would silently mislabel. Drop the
|
|
517
517
|
// column and leave the brz_payments row in place. The read path sees an
|
|
518
518
|
// unjoined deposit row as `details: None` until the resync re-fetches the
|
|
519
519
|
// SSP user_request and the upsert inserts the new details row.
|
|
@@ -532,6 +532,36 @@ class MysqlMigrationManager {
|
|
|
532
532
|
WHERE \`key\` = 'sync_offset' AND value IS NOT NULL`,
|
|
533
533
|
],
|
|
534
534
|
},
|
|
535
|
+
{
|
|
536
|
+
// Backfill the conversion_info `type` discriminator for the
|
|
537
|
+
// ConversionInfo enum refactor. All existing rows are AMM.
|
|
538
|
+
// Mirrors the Rust mysql migration of the same name.
|
|
539
|
+
name: "Backfill conversion_info type discriminator",
|
|
540
|
+
sql: [
|
|
541
|
+
`UPDATE brz_payment_metadata SET conversion_info = JSON_SET(conversion_info, '$.type', 'amm') WHERE conversion_info IS NOT NULL AND JSON_EXTRACT(conversion_info, '$.type') IS NULL`,
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
// Cross-chain swap rows, synced for cross-instance recovery. Shared
|
|
546
|
+
// across providers, discriminated by `provider`. Born multi-tenant
|
|
547
|
+
// (user_id in the PK). `data` is provider-opaque JSON; `secrets` is
|
|
548
|
+
// provider-opaque ciphertext (empty when the provider has none).
|
|
549
|
+
name: "Create brz_cross_chain_swaps table",
|
|
550
|
+
sql: [
|
|
551
|
+
`CREATE TABLE IF NOT EXISTS brz_cross_chain_swaps (
|
|
552
|
+
user_id VARBINARY(33) NOT NULL,
|
|
553
|
+
provider VARCHAR(64) NOT NULL,
|
|
554
|
+
id VARCHAR(255) NOT NULL,
|
|
555
|
+
is_terminal TINYINT(1) NOT NULL,
|
|
556
|
+
updated_at BIGINT NOT NULL,
|
|
557
|
+
data LONGTEXT NOT NULL,
|
|
558
|
+
secrets LONGTEXT NOT NULL,
|
|
559
|
+
PRIMARY KEY (user_id, provider, id),
|
|
560
|
+
INDEX brz_idx_cross_chain_swaps_user_provider_is_terminal
|
|
561
|
+
(user_id, provider, is_terminal)
|
|
562
|
+
)`,
|
|
563
|
+
],
|
|
564
|
+
},
|
|
535
565
|
];
|
|
536
566
|
}
|
|
537
567
|
}
|
|
@@ -239,6 +239,15 @@ class PostgresStorage {
|
|
|
239
239
|
for (const paymentDetailsFilter of request.paymentDetailsFilter) {
|
|
240
240
|
const paymentDetailsClauses = [];
|
|
241
241
|
|
|
242
|
+
// Base type check: ensure payment type matches the filter type
|
|
243
|
+
if (paymentDetailsFilter.type === "spark") {
|
|
244
|
+
paymentDetailsClauses.push("p.spark = true");
|
|
245
|
+
} else if (paymentDetailsFilter.type === "token") {
|
|
246
|
+
paymentDetailsClauses.push("p.spark IS NULL AND t.tx_hash IS NOT NULL");
|
|
247
|
+
} else if (paymentDetailsFilter.type === "lightning") {
|
|
248
|
+
paymentDetailsClauses.push("l.htlc_status IS NOT NULL");
|
|
249
|
+
}
|
|
250
|
+
|
|
242
251
|
// Filter by HTLC status (Spark or Lightning)
|
|
243
252
|
const htlcAlias =
|
|
244
253
|
paymentDetailsFilter.type === "spark"
|
|
@@ -266,24 +275,35 @@ class PostgresStorage {
|
|
|
266
275
|
params.push(...paymentDetailsFilter.htlcStatus);
|
|
267
276
|
}
|
|
268
277
|
|
|
269
|
-
// Filter by conversion
|
|
278
|
+
// Filter by conversion type + status
|
|
270
279
|
if (
|
|
271
280
|
(paymentDetailsFilter.type === "spark" ||
|
|
272
|
-
paymentDetailsFilter.type === "token"
|
|
273
|
-
|
|
281
|
+
paymentDetailsFilter.type === "token" ||
|
|
282
|
+
paymentDetailsFilter.type === "lightning") &&
|
|
283
|
+
paymentDetailsFilter.conversionFilter !== undefined
|
|
274
284
|
) {
|
|
285
|
+
// Lightning carries the Boltz conversion; its `conversion_info.type`
|
|
286
|
+
// is the authoritative discriminator, so no payment-type column check.
|
|
275
287
|
const typeCheck =
|
|
276
288
|
paymentDetailsFilter.type === "spark"
|
|
277
289
|
? "p.spark = true"
|
|
278
|
-
:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
290
|
+
: paymentDetailsFilter.type === "token"
|
|
291
|
+
? "p.spark IS NULL"
|
|
292
|
+
: null;
|
|
293
|
+
let statusClause;
|
|
294
|
+
if (paymentDetailsFilter.conversionFilter === "ammRefundNeeded") {
|
|
295
|
+
statusClause = "pm.conversion_info::jsonb->>'type' = 'amm' AND pm.conversion_info::jsonb->>'status' = 'refundNeeded'";
|
|
296
|
+
} else if (paymentDetailsFilter.conversionFilter === "orchestraPending") {
|
|
297
|
+
statusClause = "pm.conversion_info::jsonb->>'type' = 'orchestra' AND pm.conversion_info::jsonb->>'status' NOT IN ('completed', 'failed', 'refunded')";
|
|
298
|
+
} else if (paymentDetailsFilter.conversionFilter === "boltzPending") {
|
|
299
|
+
statusClause = "pm.conversion_info::jsonb->>'type' = 'boltz' AND pm.conversion_info::jsonb->>'status' NOT IN ('completed', 'failed', 'refunded')";
|
|
300
|
+
}
|
|
301
|
+
if (statusClause) {
|
|
302
|
+
const prefix = typeCheck ? `${typeCheck} AND ` : "";
|
|
303
|
+
paymentDetailsClauses.push(
|
|
304
|
+
`${prefix}pm.conversion_info IS NOT NULL AND ${statusClause}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
287
307
|
}
|
|
288
308
|
|
|
289
309
|
// Filter by token transaction hash
|
|
@@ -835,6 +855,13 @@ class PostgresStorage {
|
|
|
835
855
|
senderComment: row.lnurl_sender_comment || null,
|
|
836
856
|
};
|
|
837
857
|
}
|
|
858
|
+
|
|
859
|
+
if (row.conversion_info) {
|
|
860
|
+
details.conversionInfo =
|
|
861
|
+
typeof row.conversion_info === "string"
|
|
862
|
+
? JSON.parse(row.conversion_info)
|
|
863
|
+
: row.conversion_info;
|
|
864
|
+
}
|
|
838
865
|
} else if (row.withdraw_tx_id) {
|
|
839
866
|
details = {
|
|
840
867
|
type: "withdraw",
|
|
@@ -1017,6 +1044,74 @@ class PostgresStorage {
|
|
|
1017
1044
|
}
|
|
1018
1045
|
}
|
|
1019
1046
|
|
|
1047
|
+
// ===== Cross-Chain Swap Operations =====
|
|
1048
|
+
|
|
1049
|
+
async setCrossChainSwap(swap) {
|
|
1050
|
+
try {
|
|
1051
|
+
await this.pool.query(
|
|
1052
|
+
`INSERT INTO brz_cross_chain_swaps
|
|
1053
|
+
(user_id, provider, id, is_terminal, updated_at, data, secrets)
|
|
1054
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1055
|
+
ON CONFLICT(user_id, provider, id) DO UPDATE SET
|
|
1056
|
+
is_terminal = EXCLUDED.is_terminal,
|
|
1057
|
+
updated_at = EXCLUDED.updated_at,
|
|
1058
|
+
data = EXCLUDED.data,
|
|
1059
|
+
secrets = EXCLUDED.secrets`,
|
|
1060
|
+
[
|
|
1061
|
+
this.identity,
|
|
1062
|
+
swap.provider,
|
|
1063
|
+
swap.id,
|
|
1064
|
+
swap.isTerminal,
|
|
1065
|
+
swap.updatedAt,
|
|
1066
|
+
swap.data,
|
|
1067
|
+
swap.secrets,
|
|
1068
|
+
]
|
|
1069
|
+
);
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
throw new StorageError(
|
|
1072
|
+
`Failed to set cross-chain swap: ${error.message}`,
|
|
1073
|
+
error
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async getCrossChainSwap(provider, id) {
|
|
1079
|
+
try {
|
|
1080
|
+
const result = await this.pool.query(
|
|
1081
|
+
`SELECT provider, id, is_terminal, updated_at, data, secrets
|
|
1082
|
+
FROM brz_cross_chain_swaps
|
|
1083
|
+
WHERE user_id = $1 AND provider = $2 AND id = $3`,
|
|
1084
|
+
[this.identity, provider, id]
|
|
1085
|
+
);
|
|
1086
|
+
if (result.rows.length === 0) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
return crossChainSwapFromRow(result.rows[0]);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
throw new StorageError(
|
|
1092
|
+
`Failed to get cross-chain swap: ${error.message}`,
|
|
1093
|
+
error
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async listActiveCrossChainSwaps(provider) {
|
|
1099
|
+
try {
|
|
1100
|
+
const result = await this.pool.query(
|
|
1101
|
+
`SELECT provider, id, is_terminal, updated_at, data, secrets
|
|
1102
|
+
FROM brz_cross_chain_swaps
|
|
1103
|
+
WHERE user_id = $1 AND provider = $2 AND is_terminal = FALSE`,
|
|
1104
|
+
[this.identity, provider]
|
|
1105
|
+
);
|
|
1106
|
+
return result.rows.map(crossChainSwapFromRow);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new StorageError(
|
|
1109
|
+
`Failed to list active cross-chain swaps: ${error.message}`,
|
|
1110
|
+
error
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1020
1115
|
// ===== Sync Operations =====
|
|
1021
1116
|
|
|
1022
1117
|
async syncAddOutgoingChange(record) {
|
|
@@ -1443,6 +1538,21 @@ class PostgresStorage {
|
|
|
1443
1538
|
}
|
|
1444
1539
|
}
|
|
1445
1540
|
|
|
1541
|
+
/**
|
|
1542
|
+
* Maps a brz_cross_chain_swaps row to the camelCase StoredCrossChainSwap shape
|
|
1543
|
+
* the SDK expects.
|
|
1544
|
+
*/
|
|
1545
|
+
function crossChainSwapFromRow(row) {
|
|
1546
|
+
return {
|
|
1547
|
+
provider: row.provider,
|
|
1548
|
+
id: row.id,
|
|
1549
|
+
isTerminal: row.is_terminal,
|
|
1550
|
+
updatedAt: Number(row.updated_at),
|
|
1551
|
+
data: row.data,
|
|
1552
|
+
secrets: row.secrets,
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1446
1556
|
/**
|
|
1447
1557
|
* Creates a PostgresStorageConfig with the given connection string and default pool settings.
|
|
1448
1558
|
*
|
|
@@ -472,7 +472,7 @@ class PostgresMigrationManager {
|
|
|
472
472
|
// the schema matches brz_payment_details_lightning / _token / _spark. We
|
|
473
473
|
// can't safely backfill the new table from the dropped deposit_tx_id
|
|
474
474
|
// column: we never stored the original SSP output_index, and vout=0 is a
|
|
475
|
-
// valid output index
|
|
475
|
+
// valid output index, so defaulting would silently mislabel. Drop the
|
|
476
476
|
// column and leave the brz_payments row in place. The read path sees an
|
|
477
477
|
// unjoined deposit row as `details: None` until the resync re-fetches the
|
|
478
478
|
// SSP user_request and the upsert inserts the new details row.
|
|
@@ -491,6 +491,33 @@ class PostgresMigrationManager {
|
|
|
491
491
|
WHERE key = 'sync_offset' AND value IS NOT NULL`,
|
|
492
492
|
],
|
|
493
493
|
},
|
|
494
|
+
{
|
|
495
|
+
name: "Backfill conversion_info type discriminator",
|
|
496
|
+
sql: [
|
|
497
|
+
`UPDATE brz_payment_metadata SET conversion_info = conversion_info::jsonb || '{"type": "amm"}'::jsonb WHERE conversion_info IS NOT NULL AND conversion_info::jsonb->>'type' IS NULL`,
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
// Cross-chain swap rows, synced for cross-instance recovery. Shared
|
|
502
|
+
// across providers, discriminated by `provider`. Born multi-tenant
|
|
503
|
+
// (user_id in the PK). `data` is provider-opaque JSON; `secrets` is
|
|
504
|
+
// provider-opaque ciphertext (empty when the provider has none).
|
|
505
|
+
name: "Create brz_cross_chain_swaps table",
|
|
506
|
+
sql: [
|
|
507
|
+
`CREATE TABLE IF NOT EXISTS brz_cross_chain_swaps (
|
|
508
|
+
user_id BYTEA NOT NULL,
|
|
509
|
+
provider TEXT NOT NULL,
|
|
510
|
+
id TEXT NOT NULL,
|
|
511
|
+
is_terminal BOOLEAN NOT NULL,
|
|
512
|
+
updated_at BIGINT NOT NULL,
|
|
513
|
+
data TEXT NOT NULL,
|
|
514
|
+
secrets TEXT NOT NULL,
|
|
515
|
+
PRIMARY KEY (user_id, provider, id)
|
|
516
|
+
)`,
|
|
517
|
+
`CREATE INDEX brz_idx_cross_chain_swaps_user_provider_is_terminal
|
|
518
|
+
ON brz_cross_chain_swaps(user_id, provider, is_terminal)`,
|
|
519
|
+
],
|
|
520
|
+
},
|
|
494
521
|
];
|
|
495
522
|
}
|
|
496
523
|
}
|
package/nodejs/storage/index.cjs
CHANGED
|
@@ -206,6 +206,14 @@ class SqliteStorage {
|
|
|
206
206
|
const allPaymentDetailsClauses = [];
|
|
207
207
|
for (const paymentDetailsFilter of request.paymentDetailsFilter) {
|
|
208
208
|
const paymentDetailsClauses = [];
|
|
209
|
+
// Base type check: ensure payment type matches the filter type
|
|
210
|
+
if (paymentDetailsFilter.type === "spark") {
|
|
211
|
+
paymentDetailsClauses.push("p.spark = 1");
|
|
212
|
+
} else if (paymentDetailsFilter.type === "token") {
|
|
213
|
+
paymentDetailsClauses.push("p.spark IS NULL AND t.tx_hash IS NOT NULL");
|
|
214
|
+
} else if (paymentDetailsFilter.type === "lightning") {
|
|
215
|
+
paymentDetailsClauses.push("l.htlc_status IS NOT NULL");
|
|
216
|
+
}
|
|
209
217
|
// Filter by HTLC status (Spark or Lightning)
|
|
210
218
|
const htlcAlias =
|
|
211
219
|
paymentDetailsFilter.type === "spark" ? "s"
|
|
@@ -232,20 +240,35 @@ class SqliteStorage {
|
|
|
232
240
|
}
|
|
233
241
|
params.push(...paymentDetailsFilter.htlcStatus);
|
|
234
242
|
}
|
|
235
|
-
// Filter by
|
|
243
|
+
// Filter by conversion type + status
|
|
236
244
|
if (
|
|
237
|
-
(paymentDetailsFilter.type === "spark" ||
|
|
238
|
-
paymentDetailsFilter.
|
|
245
|
+
(paymentDetailsFilter.type === "spark" ||
|
|
246
|
+
paymentDetailsFilter.type === "token" ||
|
|
247
|
+
paymentDetailsFilter.type === "lightning") &&
|
|
248
|
+
paymentDetailsFilter.conversionFilter !== undefined
|
|
239
249
|
) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
// Lightning carries the Boltz conversion; its `conversion_info.type`
|
|
251
|
+
// is the authoritative discriminator, so no payment-type column check.
|
|
252
|
+
const typeCheck =
|
|
253
|
+
paymentDetailsFilter.type === "spark"
|
|
254
|
+
? "p.spark = 1"
|
|
255
|
+
: paymentDetailsFilter.type === "token"
|
|
256
|
+
? "p.spark IS NULL"
|
|
257
|
+
: null;
|
|
258
|
+
let statusClause;
|
|
259
|
+
if (paymentDetailsFilter.conversionFilter === "ammRefundNeeded") {
|
|
260
|
+
statusClause = "json_extract(pm.conversion_info, '$.type') = 'amm' AND json_extract(pm.conversion_info, '$.status') = 'refundNeeded'";
|
|
261
|
+
} else if (paymentDetailsFilter.conversionFilter === "orchestraPending") {
|
|
262
|
+
statusClause = "json_extract(pm.conversion_info, '$.type') = 'orchestra' AND json_extract(pm.conversion_info, '$.status') NOT IN ('completed', 'failed', 'refunded')";
|
|
263
|
+
} else if (paymentDetailsFilter.conversionFilter === "boltzPending") {
|
|
264
|
+
statusClause = "json_extract(pm.conversion_info, '$.type') = 'boltz' AND json_extract(pm.conversion_info, '$.status') NOT IN ('completed', 'failed', 'refunded')";
|
|
265
|
+
}
|
|
266
|
+
if (statusClause) {
|
|
267
|
+
const prefix = typeCheck ? `${typeCheck} AND ` : "";
|
|
268
|
+
paymentDetailsClauses.push(
|
|
269
|
+
`${prefix}pm.conversion_info IS NOT NULL AND ${statusClause}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
249
272
|
}
|
|
250
273
|
// Filter by token transaction hash
|
|
251
274
|
if (
|
|
@@ -813,6 +836,17 @@ class SqliteStorage {
|
|
|
813
836
|
senderComment: row.lnurl_sender_comment || null,
|
|
814
837
|
};
|
|
815
838
|
}
|
|
839
|
+
|
|
840
|
+
if (row.conversion_info) {
|
|
841
|
+
try {
|
|
842
|
+
details.conversionInfo = JSON.parse(row.conversion_info);
|
|
843
|
+
} catch (e) {
|
|
844
|
+
throw new StorageError(
|
|
845
|
+
`Failed to parse conversion_info JSON for payment ${row.id}: ${e.message}`,
|
|
846
|
+
e
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
816
850
|
} else if (row.withdraw_tx_id) {
|
|
817
851
|
details = {
|
|
818
852
|
type: "withdraw",
|
|
@@ -1378,6 +1412,78 @@ class SqliteStorage {
|
|
|
1378
1412
|
);
|
|
1379
1413
|
}
|
|
1380
1414
|
}
|
|
1415
|
+
|
|
1416
|
+
// ===== Cross-Chain Swap Operations =====
|
|
1417
|
+
|
|
1418
|
+
setCrossChainSwap(swap) {
|
|
1419
|
+
try {
|
|
1420
|
+
const stmt = this.db.prepare(`
|
|
1421
|
+
INSERT INTO cross_chain_swaps (provider, id, is_terminal, updated_at, data, secrets)
|
|
1422
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1423
|
+
ON CONFLICT(provider, id) DO UPDATE SET
|
|
1424
|
+
is_terminal = excluded.is_terminal,
|
|
1425
|
+
updated_at = excluded.updated_at,
|
|
1426
|
+
data = excluded.data,
|
|
1427
|
+
secrets = excluded.secrets
|
|
1428
|
+
`);
|
|
1429
|
+
stmt.run(
|
|
1430
|
+
swap.provider,
|
|
1431
|
+
swap.id,
|
|
1432
|
+
swap.isTerminal ? 1 : 0,
|
|
1433
|
+
swap.updatedAt,
|
|
1434
|
+
swap.data,
|
|
1435
|
+
swap.secrets
|
|
1436
|
+
);
|
|
1437
|
+
return Promise.resolve();
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
return Promise.reject(
|
|
1440
|
+
new StorageError(`Failed to set cross-chain swap: ${error.message}`, error)
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
getCrossChainSwap(provider, id) {
|
|
1446
|
+
try {
|
|
1447
|
+
const stmt = this.db.prepare(
|
|
1448
|
+
`SELECT provider, id, is_terminal, updated_at, data, secrets
|
|
1449
|
+
FROM cross_chain_swaps WHERE provider = ? AND id = ?`
|
|
1450
|
+
);
|
|
1451
|
+
const row = stmt.get(provider, id);
|
|
1452
|
+
return Promise.resolve(row ? crossChainSwapFromRow(row) : null);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
return Promise.reject(
|
|
1455
|
+
new StorageError(`Failed to get cross-chain swap: ${error.message}`, error)
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
listActiveCrossChainSwaps(provider) {
|
|
1461
|
+
try {
|
|
1462
|
+
const stmt = this.db.prepare(
|
|
1463
|
+
`SELECT provider, id, is_terminal, updated_at, data, secrets
|
|
1464
|
+
FROM cross_chain_swaps WHERE provider = ? AND is_terminal = 0`
|
|
1465
|
+
);
|
|
1466
|
+
const rows = stmt.all(provider);
|
|
1467
|
+
return Promise.resolve(rows.map(crossChainSwapFromRow));
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
return Promise.reject(
|
|
1470
|
+
new StorageError(`Failed to list active cross-chain swaps: ${error.message}`, error)
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/// Maps a `cross_chain_swaps` row to a StoredCrossChainSwap, parsing `data` and
|
|
1477
|
+
/// the is_terminal flag back into the camelCase shape the SDK expects.
|
|
1478
|
+
function crossChainSwapFromRow(row) {
|
|
1479
|
+
return {
|
|
1480
|
+
provider: row.provider,
|
|
1481
|
+
id: row.id,
|
|
1482
|
+
isTerminal: row.is_terminal !== 0,
|
|
1483
|
+
updatedAt: Number(row.updated_at),
|
|
1484
|
+
data: row.data,
|
|
1485
|
+
secrets: row.secrets,
|
|
1486
|
+
};
|
|
1381
1487
|
}
|
|
1382
1488
|
|
|
1383
1489
|
async function createDefaultStorage(dataDir, logger = null) {
|
|
@@ -438,7 +438,7 @@ class MigrationManager {
|
|
|
438
438
|
// the schema matches payment_details_lightning / _token / _spark. We can't
|
|
439
439
|
// safely backfill the new table from the dropped deposit_tx_id column: we
|
|
440
440
|
// never stored the original SSP output_index, and vout=0 is a valid output
|
|
441
|
-
// index
|
|
441
|
+
// index, so defaulting would silently mislabel. Drop the column and leave
|
|
442
442
|
// the payments row in place. The read path sees an unjoined deposit row
|
|
443
443
|
// as `details: None` until the resync re-fetches the SSP user_request and
|
|
444
444
|
// the upsert inserts the new details row.
|
|
@@ -456,6 +456,30 @@ class MigrationManager {
|
|
|
456
456
|
WHERE key = 'sync_offset' AND json_valid(value)`,
|
|
457
457
|
],
|
|
458
458
|
},
|
|
459
|
+
{
|
|
460
|
+
name: "Backfill conversion_info type discriminator",
|
|
461
|
+
sql: `UPDATE payment_metadata SET conversion_info = json_set(conversion_info, '$.type', 'amm') WHERE conversion_info IS NOT NULL AND json_extract(conversion_info, '$.type') IS NULL`,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
// Cross-chain swap rows, synced for cross-instance recovery. Shared
|
|
465
|
+
// across providers, discriminated by `provider`. `data` is
|
|
466
|
+
// provider-opaque JSON; `secrets` is provider-opaque ciphertext (empty
|
|
467
|
+
// when the provider has none).
|
|
468
|
+
name: "Create cross_chain_swaps table",
|
|
469
|
+
sql: [
|
|
470
|
+
`CREATE TABLE cross_chain_swaps (
|
|
471
|
+
provider TEXT NOT NULL,
|
|
472
|
+
id TEXT NOT NULL,
|
|
473
|
+
is_terminal INTEGER NOT NULL,
|
|
474
|
+
updated_at INTEGER NOT NULL,
|
|
475
|
+
data TEXT NOT NULL,
|
|
476
|
+
secrets TEXT NOT NULL,
|
|
477
|
+
PRIMARY KEY (provider, id)
|
|
478
|
+
)`,
|
|
479
|
+
`CREATE INDEX idx_cross_chain_swaps_provider_is_terminal
|
|
480
|
+
ON cross_chain_swaps(provider, is_terminal)`,
|
|
481
|
+
],
|
|
482
|
+
},
|
|
459
483
|
];
|
|
460
484
|
}
|
|
461
485
|
}
|
package/package.json
CHANGED
package/ssr/index.js
CHANGED
|
@@ -98,6 +98,11 @@ export function postgresStorage(...args) {
|
|
|
98
98
|
return _module.postgresStorage(...args);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export function start(...args) {
|
|
102
|
+
if (!_module) _notInitialized('start');
|
|
103
|
+
return _module.start(...args);
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
export function task_worker_entry_point(...args) {
|
|
102
107
|
if (!_module) _notInitialized('task_worker_entry_point');
|
|
103
108
|
return _module.task_worker_entry_point(...args);
|