@qntx/spark 0.2.0 → 0.3.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,7 @@ 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
70
 
71
71
  Unilateral exit: each pre-signed transaction carries a hard deadline — once its parent confirms
72
72
  and its relative timelock passes, it must confirm within 50 blocks; otherwise the operators'
@@ -463,8 +463,15 @@ type SparkClient = {
463
463
  }>;
464
464
  readonly transferId?: string;
465
465
  }) => 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>>;
466
+ /**
467
+ * Claims the transfer `transferId` once it becomes claimable and returns the new leaves —
468
+ * re-querying the coordinator between retries the way upstream `TransferService.claimTransfer`
469
+ * does (five attempts, 1/2/4/8 s). `[]` means this identity's receiver leg already completed. A
470
+ * terminal status (`EXPIRED`, `RETURNED`, …) throws immediately; `signal` aborts the waits.
471
+ */
472
+ readonly claim: (transferId: string, options?: {
473
+ readonly signal?: AbortSignal;
474
+ }) => Promise<ReadonlyArray<SparkLeaf>>;
468
475
  /**
469
476
  * Claims every pending incoming transfer; returns the new leaves plus one failure entry per
470
477
  * transfer that could not be claimed — a single poisoned transfer does not abort the rest. A
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
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 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-rHuqmVV3.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. */
@@ -68,6 +68,14 @@ export declare function encodeTokenIdentifier(tokenIdentifier: Uint8Array, netwo
68
68
  */
69
69
  export declare function decodeTokenIdentifier(text: string): SparkTokenIdentifierData;
70
70
  //#endregion
71
+ //#region src/core/uuid.d.ts
72
+ /**
73
+ * UUIDv7 (RFC 9562 §5.7): 48-bit millisecond timestamp, then 74 random bits with the version (0111)
74
+ * and variant (10) fields applied. Randomness and the clock are injected so the function stays
75
+ * platform-neutral and deterministic under test.
76
+ */
77
+ export declare function uuidv7(random: (length: number) => Uint8Array, now?: () => number): string;
78
+ //#endregion
71
79
  //#region src/invoice/create.d.ts
72
80
  type InvoiceOptions = {
73
81
  readonly signer: SparkSigner;
package/dist/index.mjs CHANGED
@@ -3586,6 +3586,16 @@ const CLAIMABLE_TRANSFER_STATUSES = [
3586
3586
  "RECEIVER_KEY_TWEAKED",
3587
3587
  "RECEIVER_REFUND_SIGNED"
3588
3588
  ];
3589
+ /**
3590
+ * Whole-transfer statuses where the sender leg has not finished: the transfer will become claimable
3591
+ * (or terminal) later, so a receiver claim must wait and re-query rather than fail.
3592
+ */
3593
+ const TRANSIENT_TRANSFER_STATUSES = [
3594
+ "SENDER_INITIATED",
3595
+ "SENDER_KEY_TWEAK_PENDING",
3596
+ "SENDER_INITIATED_COORDINATOR",
3597
+ "APPLYING_SENDER_KEY_TWEAK"
3598
+ ];
3589
3599
  function millis(timestamp) {
3590
3600
  if (timestamp === void 0) return;
3591
3601
  return timestampDate(timestamp).getTime();
@@ -4190,6 +4200,67 @@ async function claimTransfer(options) {
4190
4200
  if (response.transfer === void 0) throw new SparkError("claimTransfer: coordinator returned no transfer");
4191
4201
  return verifyClaimedLeaves(receiverLeaves(response.transfer.leaves, receiverId));
4192
4202
  }
4203
+ /** Upstream `DEFAULT_RETRY_CONFIG`: five attempts, 1 s base, doubling — 1/2/4/8 s between tries. */
4204
+ const CLAIM_RETRY_DELAYS_MS = [
4205
+ 1e3,
4206
+ 2e3,
4207
+ 4e3,
4208
+ 8e3
4209
+ ];
4210
+ /** Resolves after `ms` — injected so tests control the pace — or rejects promptly on abort. */
4211
+ async function abortableWait(ms, sleep, signal) {
4212
+ if (signal === void 0) {
4213
+ await sleep(ms);
4214
+ return;
4215
+ }
4216
+ if (signal.aborted) throw new SparkRequestError("claim aborted", { code: "canceled" });
4217
+ let onAbort;
4218
+ const aborted = new Promise((_resolve, reject) => {
4219
+ onAbort = () => {
4220
+ reject(new SparkRequestError("claim aborted", { code: "canceled" }));
4221
+ };
4222
+ signal.addEventListener("abort", onAbort, { once: true });
4223
+ });
4224
+ try {
4225
+ await Promise.race([sleep(ms), aborted]);
4226
+ } finally {
4227
+ if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
4228
+ }
4229
+ }
4230
+ /**
4231
+ * Receiver claim by transfer id (upstream `TransferService.claimTransfer` with `withRetry` and
4232
+ * re-query). Every attempt re-queries the transfer: a receiver-leg-complete row returns `[]`, a
4233
+ * claimable one is claimed through the caller's `claim` (which serializes the claim plus its chain
4234
+ * store under the client leaf lock), a still-transient status waits and retries, and a terminal or
4235
+ * unrecognized status fails immediately — retrying `EXPIRED`/`RETURNED` can only burn the budget. A
4236
+ * transfer absent from the coordinator's index is treated as transient: the receive event can land
4237
+ * seconds before the SSP marks the transfer claimable.
4238
+ */
4239
+ async function claimByTransferId(options) {
4240
+ const { spark, signer, network, transferId, sleep, signal, claim } = options;
4241
+ const self = await signer.identityPublicKey();
4242
+ for (let attempt = 0;; attempt += 1) {
4243
+ if (signal?.aborted === true) throw new SparkRequestError("claim aborted", { code: "canceled" });
4244
+ const [row] = await queryTransfersByIds({
4245
+ spark,
4246
+ transferIds: [transferId],
4247
+ network
4248
+ });
4249
+ const transfer = row === void 0 ? void 0 : mapTransfer(row);
4250
+ if (transfer !== void 0) {
4251
+ if (isReceiverLegComplete(transfer, self)) return [];
4252
+ if (CLAIMABLE_TRANSFER_STATUSES.includes(transfer.status)) try {
4253
+ return await claim(transfer);
4254
+ } catch (error) {
4255
+ if (!(error instanceof SparkRequestError) || error.code !== "already_exists") throw error;
4256
+ }
4257
+ else if (!TRANSIENT_TRANSFER_STATUSES.includes(transfer.status)) throw new SparkInputError(`claim: transfer ${transferId} status ${transfer.status} is not claimable`);
4258
+ }
4259
+ const delay = CLAIM_RETRY_DELAYS_MS[attempt];
4260
+ if (delay === void 0) throw new SparkRequestError(`claim: transfer ${transferId} did not become claimable`, { code: "deadline_exceeded" });
4261
+ await abortableWait(delay, sleep, signal);
4262
+ }
4263
+ }
4193
4264
  //#endregion
4194
4265
  //#region src/transfer/send.ts
4195
4266
  /**
@@ -5842,13 +5913,19 @@ function createTransfers(context) {
5842
5913
  })),
5843
5914
  ...sendOptions.transferId === void 0 ? {} : { transferId: sendOptions.transferId }
5844
5915
  }),
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;
5916
+ claim: async (transferId, claimOptions) => claimByTransferId({
5917
+ ...context.transferCoordinator,
5918
+ transferId,
5919
+ sleep: context.sleep ?? defaultSleep,
5920
+ ...claimOptions?.signal === void 0 ? {} : { signal: claimOptions.signal },
5921
+ claim: async (transfer) => context.leafLock.run(async () => {
5922
+ const claimed = await claimTransfer({
5923
+ ...context.transferCoordinator,
5924
+ transfer
5925
+ });
5926
+ await context.storeChainsFor(claimed);
5927
+ return claimed;
5928
+ })
5852
5929
  }),
5853
5930
  claimPending: async () => {
5854
5931
  const pending = await queryPendingTransfers({
@@ -7328,4 +7405,4 @@ function createMemorySparkStore() {
7328
7405
  };
7329
7406
  }
7330
7407
  //#endregion
7331
- export { EPHEMERAL_ANCHOR_SCRIPT, MIN_STATIC_REFUND_FEE_SATS, SparkCryptoError, SparkError, SparkInputError, SparkRequestError, SparkTokenBroadcastError, addressToScript, buildCpfpChild, createMemorySparkStore, createSatsInvoice, createSparkClient, createTokensInvoice, decodeBolt11, decodeSparkAddress, decodeTokenIdentifier, encodeSparkAddress, encodeTokenIdentifier, exitTransactions, isExpired, parseSparkInvoice, sparkNetworkConfig, verifySparkAddressSignature };
7408
+ export { EPHEMERAL_ANCHOR_SCRIPT, MIN_STATIC_REFUND_FEE_SATS, SparkCryptoError, SparkError, SparkInputError, SparkRequestError, SparkTokenBroadcastError, addressToScript, buildCpfpChild, createMemorySparkStore, createSatsInvoice, createSparkClient, createTokensInvoice, decodeBolt11, decodeSparkAddress, decodeTokenIdentifier, encodeSparkAddress, encodeTokenIdentifier, exitTransactions, isExpired, parseSparkInvoice, sparkNetworkConfig, uuidv7, verifySparkAddressSignature };
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-rHuqmVV3.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.0",
3
+ "version": "0.3.0",
4
4
  "description": "Spark Bitcoin L2 protocol for TypeScript: platform-neutral client, signer and crypto-backend interfaces.",
5
5
  "keywords": [
6
6
  "bitcoin",