@qntx/spark 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/{index-DISbAuPB.d.mts → index-Bq6pNclo.d.mts} +60 -3
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +414 -19
- package/dist/server.d.mts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,14 @@ and amount; that retry either returns the committed transaction's hash or broadc
|
|
|
66
66
|
those inputs once. Retrying without it would select different outputs and pay twice.
|
|
67
67
|
|
|
68
68
|
Offline receive: once a transfer commits, the sender cannot cancel it and its expiry does not
|
|
69
|
-
return it — `transfers.claim`/`claimPending` can pick it up at any later time.
|
|
69
|
+
return it — `transfers.claim(transferId)`/`claimPending` can pick it up at any later time.
|
|
70
|
+
|
|
71
|
+
Transaction history: `transfers.history({ limit?, offset? })` pages transfers newest-first
|
|
72
|
+
(`TRANSFER`, `PREIMAGE_SWAP`, `COOPERATIVE_EXIT`, `UTXO_SWAP`), folding each entry with its SSP
|
|
73
|
+
`user_request` when the counterparty is the SSP — lightning invoices, payment hashes and
|
|
74
|
+
preimages, coop-exit fees and payout txids, static-deposit credits. Entries surface as
|
|
75
|
+
`SparkPayment` (`send`/`receive`, `pending`/`completed`/`failed`, `spark`/`lightning`/`deposit`/
|
|
76
|
+
`withdraw`); `nextOffset` is `undefined` at the end.
|
|
70
77
|
|
|
71
78
|
Unilateral exit: each pre-signed transaction carries a hard deadline — once its parent confirms
|
|
72
79
|
and its relative timelock passes, it must confirm within 50 blocks; otherwise the operators'
|
|
@@ -332,6 +332,43 @@ type SparkTransfer = {
|
|
|
332
332
|
readonly leaves: ReadonlyArray<SparkTransferLeaf>;
|
|
333
333
|
};
|
|
334
334
|
//#endregion
|
|
335
|
+
//#region src/transfer/payment.d.ts
|
|
336
|
+
/**
|
|
337
|
+
* One entry of `client.transfers.history`: a transfer folded with its SSP `user_request` into the
|
|
338
|
+
* wallet-facing payment shape. Mapping follows the Breez adaptor semantics; every field a consumer
|
|
339
|
+
* displays comes from authenticated SO data or the SSP record — never from host input.
|
|
340
|
+
*/
|
|
341
|
+
type SparkPayment = {
|
|
342
|
+
readonly id: string;
|
|
343
|
+
readonly direction: "send" | "receive";
|
|
344
|
+
readonly status: "pending" | "completed" | "failed";
|
|
345
|
+
readonly method: "spark" | "lightning" | "deposit" | "withdraw";
|
|
346
|
+
/** Sats excluding fees — the total minus the fee for outbound sends and exits. */
|
|
347
|
+
readonly amountSats: number;
|
|
348
|
+
readonly feeSats: number;
|
|
349
|
+
/** Milliseconds since the epoch; absent when the SO reports none. */
|
|
350
|
+
readonly createdAt: number | undefined;
|
|
351
|
+
readonly details: {
|
|
352
|
+
readonly kind: "spark";
|
|
353
|
+
readonly invoice?: string;
|
|
354
|
+
} | {
|
|
355
|
+
readonly kind: "lightning";
|
|
356
|
+
readonly invoice: string;
|
|
357
|
+
/** Payment hash, hex. */
|
|
358
|
+
readonly paymentHash: string;
|
|
359
|
+
/** Payment preimage, hex. */
|
|
360
|
+
readonly preimage?: string;
|
|
361
|
+
readonly description?: string;
|
|
362
|
+
} | {
|
|
363
|
+
readonly kind: "withdraw";
|
|
364
|
+
readonly txid?: string;
|
|
365
|
+
} | {
|
|
366
|
+
readonly kind: "deposit";
|
|
367
|
+
readonly txid: string;
|
|
368
|
+
readonly vout: number;
|
|
369
|
+
} | undefined;
|
|
370
|
+
};
|
|
371
|
+
//#endregion
|
|
335
372
|
//#region src/core/bitcoin/cpfp.d.ts
|
|
336
373
|
type FundingInput = {
|
|
337
374
|
/** Display-order hex txid of the confirmed funding output. */
|
|
@@ -430,6 +467,19 @@ type SparkClient = {
|
|
|
430
467
|
readonly limit?: number;
|
|
431
468
|
readonly offset?: number;
|
|
432
469
|
}) => Promise<ReadonlyArray<SparkTransfer>>;
|
|
470
|
+
/**
|
|
471
|
+
* Transfer history as wallet-facing payments: TRANSFER, PREIMAGE_SWAP, COOPERATIVE_EXIT and
|
|
472
|
+
* UTXO_SWAP records (newest first) folded with their SSP user requests when the counterparty is
|
|
473
|
+
* the SSP — one batched SSP call per page. `limit` is 1–100 (default 20); `nextOffset` is the
|
|
474
|
+
* offset of the next page, `undefined` at the end.
|
|
475
|
+
*/
|
|
476
|
+
readonly history: (options?: {
|
|
477
|
+
readonly limit?: number;
|
|
478
|
+
readonly offset?: number;
|
|
479
|
+
}) => Promise<{
|
|
480
|
+
readonly payments: ReadonlyArray<SparkPayment>;
|
|
481
|
+
readonly nextOffset: number | undefined;
|
|
482
|
+
}>;
|
|
433
483
|
/**
|
|
434
484
|
* Sends exactly `amountSats` to `receiver` (a `spark1…`/`sparkrt1…` address string or a 33-byte
|
|
435
485
|
* identity public key) via `start_transfer_v2` — selecting exact leaves, or swapping with the
|
|
@@ -463,8 +513,15 @@ type SparkClient = {
|
|
|
463
513
|
}>;
|
|
464
514
|
readonly transferId?: string;
|
|
465
515
|
}) => Promise<SparkTransfer>;
|
|
466
|
-
/**
|
|
467
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Claims the transfer `transferId` once it becomes claimable and returns the new leaves —
|
|
518
|
+
* re-querying the coordinator between retries the way upstream `TransferService.claimTransfer`
|
|
519
|
+
* does (five attempts, 1/2/4/8 s). `[]` means this identity's receiver leg already completed. A
|
|
520
|
+
* terminal status (`EXPIRED`, `RETURNED`, …) throws immediately; `signal` aborts the waits.
|
|
521
|
+
*/
|
|
522
|
+
readonly claim: (transferId: string, options?: {
|
|
523
|
+
readonly signal?: AbortSignal;
|
|
524
|
+
}) => Promise<ReadonlyArray<SparkLeaf>>;
|
|
468
525
|
/**
|
|
469
526
|
* Claims every pending incoming transfer; returns the new leaves plus one failure entry per
|
|
470
527
|
* transfer that could not be claimed — a single poisoned transfer does not abort the rest. A
|
|
@@ -816,4 +873,4 @@ type SparkClientOptions = {
|
|
|
816
873
|
*/
|
|
817
874
|
declare function createSparkClient(options: SparkClientOptions): SparkClient;
|
|
818
875
|
//#endregion
|
|
819
|
-
export {
|
|
876
|
+
export { CooperativeExitSpeed as A, ExitTransaction as C, createMemorySparkStore as D, SparkStore as E, StaticDepositUtxo as F, UnusedDepositAddress as I, SparkLeaf as L, MIN_STATIC_REFUND_FEE_SATS as M, StaticDepositClaim as N, CooperativeExitQuote as O, StaticDepositRefund as P, SparkEvent as R, WatchtowerExitedLeaf as S, SparkExitChain as T, LightningSendOutcome as _, createSparkClient as a, LightningPayment as b, buildCpfpChild as c, SparkTransferLeaf as d, SparkTokenBalance as f, LightningReceiveStatus as g, SparkWalletSettings as h, SparkInvoiceStatus as i, CooperativeExitStatus as j, CooperativeExitResult as k, SparkPayment as l, SparkTokenOutputStatus as m, SparkClient as n, StaticDepositClaimResult as o, SparkTokenOutput as p, SparkClientOptions as r, FundingInput as s, ClaimPendingResult as t, SparkTransfer as u, LightningSendStatus as v, exitTransactions as w, LightningInvoice as x, LightningSendStatusRequest as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { a as SparkTokenBroadcastError, i as SparkRequestError, n as SparkError, r as SparkInputError, t as SparkCryptoError } from "./index-D1fBaHTa.mjs";
|
|
2
2
|
import { a as sparkNetworkConfig, i as SparkOperatorConfig, n as SparkNetwork, r as SparkNetworkConfig } from "./lightning-address-CRULmi9v.mjs";
|
|
3
3
|
import { i as isExpired, n as Bolt11Network, r as decodeBolt11, t as Bolt11Invoice } from "./bolt11-DsNTTS_O.mjs";
|
|
4
|
-
import { A as
|
|
4
|
+
import { A as CooperativeExitSpeed, C as ExitTransaction, D as createMemorySparkStore, E as SparkStore, F as StaticDepositUtxo, I as UnusedDepositAddress, L as SparkLeaf, M as MIN_STATIC_REFUND_FEE_SATS, N as StaticDepositClaim, O as CooperativeExitQuote, P as StaticDepositRefund, R as SparkEvent, S as WatchtowerExitedLeaf, T as SparkExitChain, _ as LightningSendOutcome, a as createSparkClient, b as LightningPayment, c as buildCpfpChild, d as SparkTransferLeaf, f as SparkTokenBalance, g as LightningReceiveStatus, h as SparkWalletSettings, i as SparkInvoiceStatus, j as CooperativeExitStatus, k as CooperativeExitResult, l as SparkPayment, m as SparkTokenOutputStatus, n as SparkClient, o as StaticDepositClaimResult, p as SparkTokenOutput, r as SparkClientOptions, s as FundingInput, t as ClaimPendingResult, u as SparkTransfer, v as LightningSendStatus, w as exitTransactions, x as LightningInvoice, y as LightningSendStatusRequest } from "./index-Bq6pNclo.mjs";
|
|
5
5
|
import { n as EPHEMERAL_ANCHOR_SCRIPT, t as addressToScript, v as SparkSigner } from "./transactions-DjTZU8ev.mjs";
|
|
6
6
|
//#region src/core/invoice.d.ts
|
|
7
7
|
/** Payment carried by a Spark invoice: satoshis or BTKN tokens. */
|
|
8
|
-
type
|
|
8
|
+
type SparkInvoicePayment = {
|
|
9
9
|
readonly type: "sats";
|
|
10
10
|
readonly amount?: number | undefined;
|
|
11
11
|
} | {
|
|
@@ -20,7 +20,7 @@ type SparkInvoiceFields = {
|
|
|
20
20
|
readonly version: number;
|
|
21
21
|
/** UUID identifying the invoice (canonical lowercase form). */
|
|
22
22
|
readonly id: string;
|
|
23
|
-
readonly payment?:
|
|
23
|
+
readonly payment?: SparkInvoicePayment | undefined;
|
|
24
24
|
/** At most 120 UTF-8 bytes. */
|
|
25
25
|
readonly memo?: string | undefined;
|
|
26
26
|
/** 33-byte compressed secp256k1 public key. */
|
|
@@ -120,4 +120,4 @@ export declare function parseSparkInvoice(text: string, options: {
|
|
|
120
120
|
readonly now: number;
|
|
121
121
|
}): ParsedSparkInvoice;
|
|
122
122
|
//#endregion
|
|
123
|
-
export { type Bolt11Invoice, type Bolt11Network, type ClaimPendingResult, type CooperativeExitQuote, type CooperativeExitResult, type CooperativeExitSpeed, type CooperativeExitStatus, EPHEMERAL_ANCHOR_SCRIPT, type ExitTransaction, type FundingInput, type LightningInvoice, type LightningPayment, type LightningReceiveStatus, type LightningSendOutcome, type LightningSendStatus, type LightningSendStatusRequest, MIN_STATIC_REFUND_FEE_SATS, type ParsedSparkInvoice, type SparkAddress, type SparkClient, type SparkClientOptions, SparkCryptoError, SparkError, type SparkEvent, type SparkExitChain, SparkInputError, type SparkInvoiceFields, type SparkInvoiceStatus, type SparkLeaf, type SparkNetwork, type SparkNetworkConfig, type SparkOperatorConfig, type SparkPayment, SparkRequestError, type SparkStore, type SparkTokenBalance, SparkTokenBroadcastError, type SparkTokenIdentifierData, type SparkTokenOutput, type SparkTokenOutputStatus, type SparkTransfer, type SparkTransferLeaf, type SparkWalletSettings, type StaticDepositClaim, type StaticDepositClaimResult, type StaticDepositRefund, type StaticDepositUtxo, type UnusedDepositAddress, type WatchtowerExitedLeaf, addressToScript, buildCpfpChild, createMemorySparkStore, createSparkClient, decodeBolt11, exitTransactions, isExpired, sparkNetworkConfig };
|
|
123
|
+
export { type Bolt11Invoice, type Bolt11Network, type ClaimPendingResult, type CooperativeExitQuote, type CooperativeExitResult, type CooperativeExitSpeed, type CooperativeExitStatus, EPHEMERAL_ANCHOR_SCRIPT, type ExitTransaction, type FundingInput, type LightningInvoice, type LightningPayment, type LightningReceiveStatus, type LightningSendOutcome, type LightningSendStatus, type LightningSendStatusRequest, MIN_STATIC_REFUND_FEE_SATS, type ParsedSparkInvoice, type SparkAddress, type SparkClient, type SparkClientOptions, SparkCryptoError, SparkError, type SparkEvent, type SparkExitChain, SparkInputError, type SparkInvoiceFields, type SparkInvoicePayment, type SparkInvoiceStatus, type SparkLeaf, type SparkNetwork, type SparkNetworkConfig, type SparkOperatorConfig, type SparkPayment, SparkRequestError, type SparkStore, type SparkTokenBalance, SparkTokenBroadcastError, type SparkTokenIdentifierData, type SparkTokenOutput, type SparkTokenOutputStatus, type SparkTransfer, type SparkTransferLeaf, type SparkWalletSettings, type StaticDepositClaim, type StaticDepositClaimResult, type StaticDepositRefund, type StaticDepositUtxo, type UnusedDepositAddress, type WatchtowerExitedLeaf, addressToScript, buildCpfpChild, createMemorySparkStore, createSparkClient, decodeBolt11, exitTransactions, isExpired, sparkNetworkConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { base64, bech32m } from "@scure/base";
|
|
|
11
11
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
12
12
|
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
13
13
|
import { createGrpcWebTransport } from "@connectrpc/connect-web";
|
|
14
|
-
import { bytesToNumberBE, equalBytes, hexToBytes as hexToBytes$1, numberToBytesBE, numberToVarBytesBE } from "@noble/curves/utils.js";
|
|
14
|
+
import { bytesToHex as bytesToHex$1, bytesToNumberBE, equalBytes, hexToBytes as hexToBytes$1, numberToBytesBE, numberToVarBytesBE } from "@noble/curves/utils.js";
|
|
15
15
|
import { Address, OutScript, Transaction } from "@scure/btc-signer";
|
|
16
16
|
//#region src/core/address.ts
|
|
17
17
|
const BECH32M_LIMIT$1 = 1024;
|
|
@@ -1229,6 +1229,94 @@ fragment UserRequestFragment on UserRequest {
|
|
|
1229
1229
|
lightning_send_request_payment_preimage: payment_preimage
|
|
1230
1230
|
}
|
|
1231
1231
|
}`;
|
|
1232
|
+
const TRANSFERS = `
|
|
1233
|
+
query Transfers($transfer_spark_ids: [UUID!]!) {
|
|
1234
|
+
transfers(transfer_spark_ids: $transfer_spark_ids) {
|
|
1235
|
+
...TransferFragment
|
|
1236
|
+
transfer_user_request: user_request {
|
|
1237
|
+
...UserRequestFragment
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
fragment TransferFragment on Transfer {
|
|
1243
|
+
__typename
|
|
1244
|
+
transfer_spark_id: spark_id
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
fragment CurrencyAmountFragment on CurrencyAmount {
|
|
1248
|
+
__typename
|
|
1249
|
+
currency_amount_original_value: original_value
|
|
1250
|
+
currency_amount_original_unit: original_unit
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
fragment TransferSparkIdFragment on Transfer {
|
|
1254
|
+
__typename
|
|
1255
|
+
transfer_spark_id: spark_id
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
fragment UserRequestFragment on UserRequest {
|
|
1259
|
+
__typename
|
|
1260
|
+
... on ClaimStaticDeposit {
|
|
1261
|
+
claim_static_deposit_id: id
|
|
1262
|
+
claim_static_deposit_deposit_amount: deposit_amount {
|
|
1263
|
+
...CurrencyAmountFragment
|
|
1264
|
+
}
|
|
1265
|
+
claim_static_deposit_credit_amount: credit_amount {
|
|
1266
|
+
...CurrencyAmountFragment
|
|
1267
|
+
}
|
|
1268
|
+
claim_static_deposit_max_fee: max_fee {
|
|
1269
|
+
...CurrencyAmountFragment
|
|
1270
|
+
}
|
|
1271
|
+
claim_static_deposit_status: status
|
|
1272
|
+
claim_static_deposit_transaction_id: transaction_id
|
|
1273
|
+
claim_static_deposit_output_index: output_index
|
|
1274
|
+
claim_static_deposit_transfer_spark_id: transfer_spark_id
|
|
1275
|
+
}
|
|
1276
|
+
... on CoopExitRequest {
|
|
1277
|
+
coop_exit_request_id: id
|
|
1278
|
+
coop_exit_request_fee: fee {
|
|
1279
|
+
...CurrencyAmountFragment
|
|
1280
|
+
}
|
|
1281
|
+
coop_exit_request_l1_broadcast_fee: l1_broadcast_fee {
|
|
1282
|
+
...CurrencyAmountFragment
|
|
1283
|
+
}
|
|
1284
|
+
coop_exit_request_status: status
|
|
1285
|
+
coop_exit_request_coop_exit_txid: coop_exit_txid
|
|
1286
|
+
coop_exit_request_transfer: transfer {
|
|
1287
|
+
...TransferSparkIdFragment
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
... on LeavesSwapRequest {
|
|
1291
|
+
leaves_swap_request_id: id
|
|
1292
|
+
leaves_swap_request_status: status
|
|
1293
|
+
}
|
|
1294
|
+
... on LightningReceiveRequest {
|
|
1295
|
+
lightning_receive_request_id: id
|
|
1296
|
+
lightning_receive_request_invoice: invoice {
|
|
1297
|
+
__typename
|
|
1298
|
+
invoice_encoded_invoice: encoded_invoice
|
|
1299
|
+
invoice_payment_hash: payment_hash
|
|
1300
|
+
}
|
|
1301
|
+
lightning_receive_request_status: status
|
|
1302
|
+
lightning_receive_request_transfer: transfer {
|
|
1303
|
+
...TransferSparkIdFragment
|
|
1304
|
+
}
|
|
1305
|
+
lightning_receive_request_payment_preimage: payment_preimage
|
|
1306
|
+
}
|
|
1307
|
+
... on LightningSendRequest {
|
|
1308
|
+
lightning_send_request_id: id
|
|
1309
|
+
lightning_send_request_encoded_invoice: encoded_invoice
|
|
1310
|
+
lightning_send_request_fee: fee {
|
|
1311
|
+
...CurrencyAmountFragment
|
|
1312
|
+
}
|
|
1313
|
+
lightning_send_request_status: status
|
|
1314
|
+
lightning_send_request_transfer: transfer {
|
|
1315
|
+
...TransferSparkIdFragment
|
|
1316
|
+
}
|
|
1317
|
+
lightning_send_request_payment_preimage: payment_preimage
|
|
1318
|
+
}
|
|
1319
|
+
}`;
|
|
1232
1320
|
//#endregion
|
|
1233
1321
|
//#region src/ssp/parse.ts
|
|
1234
1322
|
/** A malformed SSP response is a server fault, not caller input. */
|
|
@@ -1251,6 +1339,11 @@ function asSats(value, field) {
|
|
|
1251
1339
|
if (n === void 0 || !Number.isSafeInteger(n) || n < 0) throw unexpectedResponse(field, "is not a non-negative integer");
|
|
1252
1340
|
return n;
|
|
1253
1341
|
}
|
|
1342
|
+
/** A GraphQL Int field used as an array/output index — a number, not a decimal string. */
|
|
1343
|
+
function asIndex(value, field) {
|
|
1344
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw unexpectedResponse(field, "is not a non-negative integer");
|
|
1345
|
+
return value;
|
|
1346
|
+
}
|
|
1254
1347
|
/** CurrencyAmount in SATOSHI → sats; any other unit is an unexpected response. */
|
|
1255
1348
|
function currencyAmountSats(value, field) {
|
|
1256
1349
|
const amount = asObject(value, field);
|
|
@@ -1371,6 +1464,75 @@ function parseCoopExitResult(request, field) {
|
|
|
1371
1464
|
rawConnectorTransaction: asHexBytes(request["coop_exit_request_raw_connector_transaction"], `${field}.coop_exit_request_raw_connector_transaction`)
|
|
1372
1465
|
};
|
|
1373
1466
|
}
|
|
1467
|
+
function optionalTxidHex(value, field) {
|
|
1468
|
+
if (value === void 0 || value === null) return;
|
|
1469
|
+
return asTxidHex(value, field);
|
|
1470
|
+
}
|
|
1471
|
+
/** The `transfers` query's per-item parse: id plus its user_request union member (or none). */
|
|
1472
|
+
function parseTransfersResponse(data) {
|
|
1473
|
+
const items = data["transfers"];
|
|
1474
|
+
if (!Array.isArray(items)) throw unexpectedResponse("transfers", "is not an array");
|
|
1475
|
+
return items.map((item, index) => {
|
|
1476
|
+
const field = `transfers[${index}]`;
|
|
1477
|
+
const entry = asObject(item, field);
|
|
1478
|
+
const request = entry["transfer_user_request"];
|
|
1479
|
+
return {
|
|
1480
|
+
transferId: asString(entry["transfer_spark_id"], `${field}.transfer_spark_id`),
|
|
1481
|
+
request: request === void 0 || request === null ? void 0 : parseUserRequest(asObject(request, `${field}.transfer_user_request`), `${field}.transfer_user_request`)
|
|
1482
|
+
};
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Discriminated `user_request` parse. An unknown `__typename` is a variant we do not model, not a
|
|
1487
|
+
* malformed response.
|
|
1488
|
+
*/
|
|
1489
|
+
function parseUserRequest(request, field) {
|
|
1490
|
+
switch (request["__typename"]) {
|
|
1491
|
+
case "LightningSendRequest": {
|
|
1492
|
+
const send = parseLightningSend(request, field);
|
|
1493
|
+
return {
|
|
1494
|
+
kind: "lightning-send",
|
|
1495
|
+
status: send.status,
|
|
1496
|
+
invoice: send.encodedInvoice,
|
|
1497
|
+
feeSats: send.feeSats,
|
|
1498
|
+
...send.paymentPreimage === void 0 ? {} : { preimage: send.paymentPreimage }
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
case "LightningReceiveRequest": {
|
|
1502
|
+
const receive = parseLightningReceive(request, field);
|
|
1503
|
+
return {
|
|
1504
|
+
kind: "lightning-receive",
|
|
1505
|
+
status: receive.status,
|
|
1506
|
+
invoice: receive.invoice,
|
|
1507
|
+
...receive.paymentPreimage === void 0 ? {} : { preimage: receive.paymentPreimage }
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
case "CoopExitRequest": return parseCoopExitRequest(request, field);
|
|
1511
|
+
case "ClaimStaticDeposit": return parseStaticDepositRequest(request, field);
|
|
1512
|
+
case "LeavesSwapRequest": return { kind: "leaves-swap" };
|
|
1513
|
+
default: return;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/** `coop_exit` for history: fee = `fee` + `l1_broadcast_fee`, status and payout txid. */
|
|
1517
|
+
function parseCoopExitRequest(request, field) {
|
|
1518
|
+
const txid = optionalTxidHex(request["coop_exit_request_coop_exit_txid"], `${field}.coop_exit_request_coop_exit_txid`);
|
|
1519
|
+
return {
|
|
1520
|
+
kind: "coop-exit",
|
|
1521
|
+
status: asString(request["coop_exit_request_status"], `${field}.coop_exit_request_status`),
|
|
1522
|
+
feeSats: currencyAmountSats(request["coop_exit_request_fee"], `${field}.coop_exit_request_fee`) + currencyAmountSats(request["coop_exit_request_l1_broadcast_fee"], `${field}.coop_exit_request_l1_broadcast_fee`),
|
|
1523
|
+
...txid === void 0 ? {} : { txid }
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
/** `claim_static_deposit` for history: the claim's txid, vout and credited amount. */
|
|
1527
|
+
function parseStaticDepositRequest(request, field) {
|
|
1528
|
+
return {
|
|
1529
|
+
kind: "static-deposit",
|
|
1530
|
+
txid: asTxidHex(request["claim_static_deposit_transaction_id"], `${field}.claim_static_deposit_transaction_id`),
|
|
1531
|
+
vout: asIndex(request["claim_static_deposit_output_index"], `${field}.claim_static_deposit_output_index`),
|
|
1532
|
+
depositAmountSats: currencyAmountSats(request["claim_static_deposit_deposit_amount"], `${field}.claim_static_deposit_deposit_amount`),
|
|
1533
|
+
creditAmountSats: currencyAmountSats(request["claim_static_deposit_credit_amount"], `${field}.claim_static_deposit_credit_amount`)
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1374
1536
|
//#endregion
|
|
1375
1537
|
//#region src/ssp/index.ts
|
|
1376
1538
|
/**
|
|
@@ -1501,6 +1663,11 @@ function createSspClient(options) {
|
|
|
1501
1663
|
}
|
|
1502
1664
|
}
|
|
1503
1665
|
return {
|
|
1666
|
+
async transfers(transferIds) {
|
|
1667
|
+
if (transferIds.length === 0) return [];
|
|
1668
|
+
if (transferIds.length > 100) throw new SparkInputError("transfers: at most 100 ids per call");
|
|
1669
|
+
return parseTransfersResponse(asObject(await readPost("Transfers", TRANSFERS, { transfer_spark_ids: [...transferIds] }), "transfers"));
|
|
1670
|
+
},
|
|
1504
1671
|
async swapFeeEstimate(amountSats) {
|
|
1505
1672
|
if (!Number.isSafeInteger(amountSats) || amountSats <= 0) throw new SparkInputError("amount sats must be a positive integer");
|
|
1506
1673
|
return currencyAmountSats(asObject((await readPost("LeavesSwapFeeEstimate", LEAVES_SWAP_FEE_ESTIMATE, { total_amount_sats: amountSats }))["leaves_swap_fee_estimate"], "leaves_swap_fee_estimate")["leaves_swap_fee_estimate_output_fee_estimate"], "leaves_swap_fee_estimate_output_fee_estimate");
|
|
@@ -3586,6 +3753,16 @@ const CLAIMABLE_TRANSFER_STATUSES = [
|
|
|
3586
3753
|
"RECEIVER_KEY_TWEAKED",
|
|
3587
3754
|
"RECEIVER_REFUND_SIGNED"
|
|
3588
3755
|
];
|
|
3756
|
+
/**
|
|
3757
|
+
* Whole-transfer statuses where the sender leg has not finished: the transfer will become claimable
|
|
3758
|
+
* (or terminal) later, so a receiver claim must wait and re-query rather than fail.
|
|
3759
|
+
*/
|
|
3760
|
+
const TRANSIENT_TRANSFER_STATUSES = [
|
|
3761
|
+
"SENDER_INITIATED",
|
|
3762
|
+
"SENDER_KEY_TWEAK_PENDING",
|
|
3763
|
+
"SENDER_INITIATED_COORDINATOR",
|
|
3764
|
+
"APPLYING_SENDER_KEY_TWEAK"
|
|
3765
|
+
];
|
|
3589
3766
|
function millis(timestamp) {
|
|
3590
3767
|
if (timestamp === void 0) return;
|
|
3591
3768
|
return timestampDate(timestamp).getTime();
|
|
@@ -3696,17 +3873,27 @@ async function queryTransfersByIds(options) {
|
|
|
3696
3873
|
network: sparkNetworkProto(options.network)
|
|
3697
3874
|
}, options.call)).transfers;
|
|
3698
3875
|
}
|
|
3699
|
-
/**
|
|
3876
|
+
/**
|
|
3877
|
+
* `query_all_transfers` — transfers where the identity is sender or receiver. `types` filters on
|
|
3878
|
+
* the server side (empty means all). `nextOffset` is the offset of the following page — `undefined`
|
|
3879
|
+
* when the SO reports `-1` (a short page ends the listing).
|
|
3880
|
+
*/
|
|
3700
3881
|
async function queryAllTransfers(options) {
|
|
3701
|
-
|
|
3882
|
+
const response = await options.spark.query_all_transfers({
|
|
3702
3883
|
participant: {
|
|
3703
3884
|
case: "senderOrReceiverIdentityPublicKey",
|
|
3704
3885
|
value: options.identityPublicKey
|
|
3705
3886
|
},
|
|
3706
3887
|
network: sparkNetworkProto(options.network),
|
|
3707
3888
|
limit: BigInt(options.limit ?? 100),
|
|
3708
|
-
offset: BigInt(options.offset ?? 0)
|
|
3709
|
-
|
|
3889
|
+
offset: BigInt(options.offset ?? 0),
|
|
3890
|
+
types: [...options.types ?? []]
|
|
3891
|
+
}, options.call);
|
|
3892
|
+
const nextOffset = Number(response.offset);
|
|
3893
|
+
return {
|
|
3894
|
+
transfers: response.transfers.map(mapTransfer),
|
|
3895
|
+
nextOffset: nextOffset < 0 ? void 0 : nextOffset
|
|
3896
|
+
};
|
|
3710
3897
|
}
|
|
3711
3898
|
/**
|
|
3712
3899
|
* Whether `transfer` is exactly the one we signed for: our id, same type, our sender key, and the
|
|
@@ -4190,6 +4377,172 @@ async function claimTransfer(options) {
|
|
|
4190
4377
|
if (response.transfer === void 0) throw new SparkError("claimTransfer: coordinator returned no transfer");
|
|
4191
4378
|
return verifyClaimedLeaves(receiverLeaves(response.transfer.leaves, receiverId));
|
|
4192
4379
|
}
|
|
4380
|
+
/** Upstream `DEFAULT_RETRY_CONFIG`: five attempts, 1 s base, doubling — 1/2/4/8 s between tries. */
|
|
4381
|
+
const CLAIM_RETRY_DELAYS_MS = [
|
|
4382
|
+
1e3,
|
|
4383
|
+
2e3,
|
|
4384
|
+
4e3,
|
|
4385
|
+
8e3
|
|
4386
|
+
];
|
|
4387
|
+
/** Resolves after `ms` — injected so tests control the pace — or rejects promptly on abort. */
|
|
4388
|
+
async function abortableWait(ms, sleep, signal) {
|
|
4389
|
+
if (signal === void 0) {
|
|
4390
|
+
await sleep(ms);
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
if (signal.aborted) throw new SparkRequestError("claim aborted", { code: "canceled" });
|
|
4394
|
+
let onAbort;
|
|
4395
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
4396
|
+
onAbort = () => {
|
|
4397
|
+
reject(new SparkRequestError("claim aborted", { code: "canceled" }));
|
|
4398
|
+
};
|
|
4399
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4400
|
+
});
|
|
4401
|
+
try {
|
|
4402
|
+
await Promise.race([sleep(ms), aborted]);
|
|
4403
|
+
} finally {
|
|
4404
|
+
if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Receiver claim by transfer id (upstream `TransferService.claimTransfer` with `withRetry` and
|
|
4409
|
+
* re-query). Every attempt re-queries the transfer: a receiver-leg-complete row returns `[]`, a
|
|
4410
|
+
* claimable one is claimed through the caller's `claim` (which serializes the claim plus its chain
|
|
4411
|
+
* store under the client leaf lock), a still-transient status waits and retries, and a terminal or
|
|
4412
|
+
* unrecognized status fails immediately — retrying `EXPIRED`/`RETURNED` can only burn the budget. A
|
|
4413
|
+
* transfer absent from the coordinator's index is treated as transient: the receive event can land
|
|
4414
|
+
* seconds before the SSP marks the transfer claimable.
|
|
4415
|
+
*/
|
|
4416
|
+
async function claimByTransferId(options) {
|
|
4417
|
+
const { spark, signer, network, transferId, sleep, signal, claim } = options;
|
|
4418
|
+
const self = await signer.identityPublicKey();
|
|
4419
|
+
for (let attempt = 0;; attempt += 1) {
|
|
4420
|
+
if (signal?.aborted === true) throw new SparkRequestError("claim aborted", { code: "canceled" });
|
|
4421
|
+
const [row] = await queryTransfersByIds({
|
|
4422
|
+
spark,
|
|
4423
|
+
transferIds: [transferId],
|
|
4424
|
+
network
|
|
4425
|
+
});
|
|
4426
|
+
const transfer = row === void 0 ? void 0 : mapTransfer(row);
|
|
4427
|
+
if (transfer !== void 0) {
|
|
4428
|
+
if (isReceiverLegComplete(transfer, self)) return [];
|
|
4429
|
+
if (CLAIMABLE_TRANSFER_STATUSES.includes(transfer.status)) try {
|
|
4430
|
+
return await claim(transfer);
|
|
4431
|
+
} catch (error) {
|
|
4432
|
+
if (!(error instanceof SparkRequestError) || error.code !== "already_exists") throw error;
|
|
4433
|
+
}
|
|
4434
|
+
else if (!TRANSIENT_TRANSFER_STATUSES.includes(transfer.status)) throw new SparkInputError(`claim: transfer ${transferId} status ${transfer.status} is not claimable`);
|
|
4435
|
+
}
|
|
4436
|
+
const delay = CLAIM_RETRY_DELAYS_MS[attempt];
|
|
4437
|
+
if (delay === void 0) throw new SparkRequestError(`claim: transfer ${transferId} did not become claimable`, { code: "deadline_exceeded" });
|
|
4438
|
+
await abortableWait(delay, sleep, signal);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
//#endregion
|
|
4442
|
+
//#region src/transfer/payment.ts
|
|
4443
|
+
/**
|
|
4444
|
+
* CoopExitRequest statuses, bucketed per Breez's `SparkCoopExitRequestStatus::acceptance`: accepted
|
|
4445
|
+
* means the SSP took the exit over — the SO-side transfer status keeps telling completion; terminal
|
|
4446
|
+
* failures are failed; anything else (incl. unknown values) is pending.
|
|
4447
|
+
*/
|
|
4448
|
+
const COOP_EXIT_ACCEPTED$1 = /* @__PURE__ */ new Set([
|
|
4449
|
+
"INBOUND_TRANSFER_CHECKED",
|
|
4450
|
+
"TX_SIGNED",
|
|
4451
|
+
"TX_BROADCASTED",
|
|
4452
|
+
"WAITING_ON_TX_CONFIRMATIONS",
|
|
4453
|
+
"COMPLETE_REQUEST_RECEIVED",
|
|
4454
|
+
"ON_CHAIN_TX_CONFIRMED",
|
|
4455
|
+
"SUCCEEDED"
|
|
4456
|
+
]);
|
|
4457
|
+
const COOP_EXIT_FAILED$1 = /* @__PURE__ */ new Set([
|
|
4458
|
+
"EXPIRED",
|
|
4459
|
+
"FAILED",
|
|
4460
|
+
"INBOUND_TRANSFER_CLAIMING_FAILED",
|
|
4461
|
+
"EXPIRING_FAILED",
|
|
4462
|
+
"FAILING_FAILED"
|
|
4463
|
+
]);
|
|
4464
|
+
/**
|
|
4465
|
+
* Whether the transfer's counterparty is the SSP: the receiver on sends (any edge of a
|
|
4466
|
+
* multi-receiver), the sender on receives. Shared by the history batch-fetch and the mapper.
|
|
4467
|
+
*/
|
|
4468
|
+
function sspCounterparty(transfer, identityPublicKey, sspIdentityPublicKey) {
|
|
4469
|
+
return equalBytes(transfer.senderIdentityPublicKey, identityPublicKey) ? equalBytes(transfer.receiverIdentityPublicKey, sspIdentityPublicKey) || transfer.receivers.some((receiver) => equalBytes(receiver.identityPublicKey, sspIdentityPublicKey)) : equalBytes(transfer.senderIdentityPublicKey, sspIdentityPublicKey);
|
|
4470
|
+
}
|
|
4471
|
+
function toSparkPayment(options) {
|
|
4472
|
+
const { transfer, request } = options;
|
|
4473
|
+
const direction = equalBytes(transfer.senderIdentityPublicKey, options.identityPublicKey) ? "send" : "receive";
|
|
4474
|
+
const isSsp = sspCounterparty(transfer, options.identityPublicKey, options.sspIdentityPublicKey);
|
|
4475
|
+
const method = transfer.type === "COOPERATIVE_EXIT" ? "withdraw" : transfer.type === "UTXO_SWAP" ? "deposit" : transfer.type === "PREIMAGE_SWAP" ? isSsp ? "lightning" : "spark" : "spark";
|
|
4476
|
+
let status;
|
|
4477
|
+
if (transfer.status === "COMPLETED") status = "completed";
|
|
4478
|
+
else if (direction === "send" && transfer.status === "SENDER_KEY_TWEAKED") status = "completed";
|
|
4479
|
+
else if (transfer.status === "EXPIRED" || transfer.status === "RETURNED") status = "failed";
|
|
4480
|
+
else status = "pending";
|
|
4481
|
+
let feeSats = 0;
|
|
4482
|
+
let amountSats = transfer.totalValueSats;
|
|
4483
|
+
let details;
|
|
4484
|
+
switch (method) {
|
|
4485
|
+
case "lightning":
|
|
4486
|
+
if (request?.kind === "lightning-send") {
|
|
4487
|
+
if (request.preimage !== void 0) status = "completed";
|
|
4488
|
+
({feeSats} = request);
|
|
4489
|
+
amountSats = Math.max(0, transfer.totalValueSats - feeSats);
|
|
4490
|
+
details = lightningDetails(request.invoice, request.preimage);
|
|
4491
|
+
} else if (request?.kind === "lightning-receive") details = lightningDetails(request.invoice, request.preimage);
|
|
4492
|
+
break;
|
|
4493
|
+
case "withdraw":
|
|
4494
|
+
if (request?.kind === "coop-exit") {
|
|
4495
|
+
({feeSats} = request);
|
|
4496
|
+
amountSats = Math.max(0, transfer.totalValueSats - feeSats);
|
|
4497
|
+
if (COOP_EXIT_FAILED$1.has(request.status)) status = "failed";
|
|
4498
|
+
else if (!COOP_EXIT_ACCEPTED$1.has(request.status)) status = "pending";
|
|
4499
|
+
details = {
|
|
4500
|
+
kind: "withdraw",
|
|
4501
|
+
...request.txid === void 0 ? {} : { txid: request.txid }
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
break;
|
|
4505
|
+
case "deposit":
|
|
4506
|
+
if (request?.kind === "static-deposit") {
|
|
4507
|
+
feeSats = Math.max(0, request.depositAmountSats - request.creditAmountSats);
|
|
4508
|
+
details = {
|
|
4509
|
+
kind: "deposit",
|
|
4510
|
+
txid: request.txid,
|
|
4511
|
+
vout: request.vout
|
|
4512
|
+
};
|
|
4513
|
+
}
|
|
4514
|
+
break;
|
|
4515
|
+
case "spark": details = {
|
|
4516
|
+
kind: "spark",
|
|
4517
|
+
...transfer.sparkInvoice === void 0 ? {} : { invoice: transfer.sparkInvoice }
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4520
|
+
if (method !== "spark" && details === void 0) status = "pending";
|
|
4521
|
+
return {
|
|
4522
|
+
id: transfer.id,
|
|
4523
|
+
direction,
|
|
4524
|
+
status,
|
|
4525
|
+
method,
|
|
4526
|
+
amountSats,
|
|
4527
|
+
feeSats,
|
|
4528
|
+
createdAt: transfer.createdTimeMs,
|
|
4529
|
+
details
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
function lightningDetails(invoice, preimage) {
|
|
4533
|
+
try {
|
|
4534
|
+
const decoded = decodeBolt11(invoice);
|
|
4535
|
+
return {
|
|
4536
|
+
kind: "lightning",
|
|
4537
|
+
invoice,
|
|
4538
|
+
paymentHash: bytesToHex$1(decoded.paymentHash),
|
|
4539
|
+
...decoded.description === void 0 ? {} : { description: decoded.description },
|
|
4540
|
+
...preimage === void 0 ? {} : { preimage: bytesToHex$1(preimage) }
|
|
4541
|
+
};
|
|
4542
|
+
} catch {
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4193
4546
|
//#endregion
|
|
4194
4547
|
//#region src/transfer/send.ts
|
|
4195
4548
|
/**
|
|
@@ -5821,13 +6174,49 @@ function createTransfers(context) {
|
|
|
5821
6174
|
network
|
|
5822
6175
|
})).transfers;
|
|
5823
6176
|
},
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
6177
|
+
history: async (historyOptions) => {
|
|
6178
|
+
const limit = historyOptions?.limit ?? 20;
|
|
6179
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new SparkInputError("history: limit must be an integer in 1–100");
|
|
6180
|
+
const offset = historyOptions?.offset ?? 0;
|
|
6181
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new SparkInputError("history: offset must be a non-negative integer");
|
|
6182
|
+
const identityPublicKey = await signer.identityPublicKey();
|
|
6183
|
+
const sspIdentityPublicKey = context.config.ssp.identityPublicKey;
|
|
6184
|
+
const { transfers, nextOffset } = await queryAllTransfers({
|
|
6185
|
+
spark,
|
|
6186
|
+
identityPublicKey,
|
|
6187
|
+
network,
|
|
6188
|
+
limit,
|
|
6189
|
+
offset,
|
|
6190
|
+
types: [
|
|
6191
|
+
TransferType.TRANSFER,
|
|
6192
|
+
TransferType.PREIMAGE_SWAP,
|
|
6193
|
+
TransferType.COOPERATIVE_EXIT,
|
|
6194
|
+
TransferType.UTXO_SWAP
|
|
6195
|
+
]
|
|
6196
|
+
});
|
|
6197
|
+
const sspIds = transfers.filter((transfer) => sspCounterparty(transfer, identityPublicKey, sspIdentityPublicKey)).map((transfer) => transfer.id);
|
|
6198
|
+
const requests = /* @__PURE__ */ new Map();
|
|
6199
|
+
for (const result of await context.sspClient.transfers(sspIds)) if (result.request !== void 0) requests.set(result.transferId, result.request);
|
|
6200
|
+
return {
|
|
6201
|
+
payments: transfers.map((transfer) => toSparkPayment({
|
|
6202
|
+
transfer,
|
|
6203
|
+
request: requests.get(transfer.id),
|
|
6204
|
+
identityPublicKey,
|
|
6205
|
+
sspIdentityPublicKey
|
|
6206
|
+
})),
|
|
6207
|
+
nextOffset
|
|
6208
|
+
};
|
|
6209
|
+
},
|
|
6210
|
+
list: async (listOptions) => {
|
|
6211
|
+
const { transfers } = await queryAllTransfers({
|
|
6212
|
+
spark,
|
|
6213
|
+
identityPublicKey: await signer.identityPublicKey(),
|
|
6214
|
+
network,
|
|
6215
|
+
...listOptions?.limit === void 0 ? {} : { limit: listOptions.limit },
|
|
6216
|
+
...listOptions?.offset === void 0 ? {} : { offset: listOptions.offset }
|
|
6217
|
+
});
|
|
6218
|
+
return transfers;
|
|
6219
|
+
},
|
|
5831
6220
|
send: async (sendOptions) => sendSats(context, {
|
|
5832
6221
|
receivers: [{
|
|
5833
6222
|
receiverIdentityPublicKey: context.receiverIdentityKey(sendOptions.receiver),
|
|
@@ -5842,13 +6231,19 @@ function createTransfers(context) {
|
|
|
5842
6231
|
})),
|
|
5843
6232
|
...sendOptions.transferId === void 0 ? {} : { transferId: sendOptions.transferId }
|
|
5844
6233
|
}),
|
|
5845
|
-
claim: async (
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
}
|
|
5850
|
-
|
|
5851
|
-
|
|
6234
|
+
claim: async (transferId, claimOptions) => claimByTransferId({
|
|
6235
|
+
...context.transferCoordinator,
|
|
6236
|
+
transferId,
|
|
6237
|
+
sleep: context.sleep ?? defaultSleep,
|
|
6238
|
+
...claimOptions?.signal === void 0 ? {} : { signal: claimOptions.signal },
|
|
6239
|
+
claim: async (transfer) => context.leafLock.run(async () => {
|
|
6240
|
+
const claimed = await claimTransfer({
|
|
6241
|
+
...context.transferCoordinator,
|
|
6242
|
+
transfer
|
|
6243
|
+
});
|
|
6244
|
+
await context.storeChainsFor(claimed);
|
|
6245
|
+
return claimed;
|
|
6246
|
+
})
|
|
5852
6247
|
}),
|
|
5853
6248
|
claimPending: async () => {
|
|
5854
6249
|
const pending = await queryPendingTransfers({
|
package/dist/server.d.mts
CHANGED