@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 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
- /** Verifies and claims a pending transfer addressed to this identity; returns the new leaves. */
467
- readonly claim: (transfer: SparkTransfer) => Promise<ReadonlyArray<SparkLeaf>>;
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 { CooperativeExitStatus as A, exitTransactions as C, CooperativeExitQuote as D, createMemorySparkStore as E, UnusedDepositAddress as F, SparkLeaf as I, SparkEvent as L, StaticDepositClaim as M, StaticDepositRefund as N, CooperativeExitResult as O, StaticDepositUtxo as P, ExitTransaction as S, SparkStore as T, LightningSendStatus as _, createSparkClient as a, LightningInvoice as b, buildCpfpChild as c, SparkTokenBalance as d, SparkTokenOutput as f, LightningSendOutcome as g, LightningReceiveStatus as h, SparkInvoiceStatus as i, MIN_STATIC_REFUND_FEE_SATS as j, CooperativeExitSpeed as k, SparkTransfer as l, SparkWalletSettings as m, SparkClient as n, StaticDepositClaimResult as o, SparkTokenOutputStatus as p, SparkClientOptions as r, FundingInput as s, ClaimPendingResult as t, SparkTransferLeaf as u, LightningSendStatusRequest as v, SparkExitChain as w, WatchtowerExitedLeaf as x, LightningPayment as y };
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 CooperativeExitStatus, C as exitTransactions, D as CooperativeExitQuote, E as createMemorySparkStore, F as UnusedDepositAddress, I as SparkLeaf, L as SparkEvent, M as StaticDepositClaim, N as StaticDepositRefund, O as CooperativeExitResult, P as StaticDepositUtxo, S as ExitTransaction, T as SparkStore, _ as LightningSendStatus, a as createSparkClient, b as LightningInvoice, c as buildCpfpChild, d as SparkTokenBalance, f as SparkTokenOutput, g as LightningSendOutcome, h as LightningReceiveStatus, i as SparkInvoiceStatus, j as MIN_STATIC_REFUND_FEE_SATS, k as CooperativeExitSpeed, l as SparkTransfer, m as SparkWalletSettings, n as SparkClient, o as StaticDepositClaimResult, p as SparkTokenOutputStatus, r as SparkClientOptions, s as FundingInput, t as ClaimPendingResult, u as SparkTransferLeaf, v as LightningSendStatusRequest, w as SparkExitChain, x as WatchtowerExitedLeaf, y as LightningPayment } from "./index-DISbAuPB.mjs";
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 SparkPayment = {
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?: SparkPayment | undefined;
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
- /** `query_all_transfers` — transfers where the identity is sender or receiver. */
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
- return (await options.spark.query_all_transfers({
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
- }, options.call)).transfers.map(mapTransfer);
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
- list: async (listOptions) => queryAllTransfers({
5825
- spark,
5826
- identityPublicKey: await signer.identityPublicKey(),
5827
- network,
5828
- ...listOptions?.limit === void 0 ? {} : { limit: listOptions.limit },
5829
- ...listOptions?.offset === void 0 ? {} : { offset: listOptions.offset }
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 (transfer) => context.leafLock.run(async () => {
5846
- const claimed = await claimTransfer({
5847
- ...context.transferCoordinator,
5848
- transfer
5849
- });
5850
- await context.storeChainsFor(claimed);
5851
- return claimed;
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
@@ -1,4 +1,4 @@
1
- import { n as SparkClient } from "./index-DISbAuPB.mjs";
1
+ import { n as SparkClient } from "./index-Bq6pNclo.mjs";
2
2
  //#region src/server/store.d.ts
3
3
  type LightningAddressUser = {
4
4
  /** Lowercase LUD-16 local-part. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qntx/spark",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Spark Bitcoin L2 protocol for TypeScript: platform-neutral client, signer and crypto-backend interfaces.",
5
5
  "keywords": [
6
6
  "bitcoin",