@arkade-os/boltz-swap 0.3.39 → 0.3.41

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/dist/index.cjs CHANGED
@@ -56,6 +56,7 @@ __export(index_exports, {
56
56
  enrichSubmarineSwapInvoice: () => enrichSubmarineSwapInvoice,
57
57
  getInvoicePaymentHash: () => getInvoicePaymentHash,
58
58
  getInvoiceSatoshis: () => getInvoiceSatoshis,
59
+ hasSubmarineStatusReached: () => hasSubmarineStatusReached,
59
60
  isChainClaimableStatus: () => isChainClaimableStatus,
60
61
  isChainFailedStatus: () => isChainFailedStatus,
61
62
  isChainFinalStatus: () => isChainFinalStatus,
@@ -324,6 +325,22 @@ var isSubmarinePendingStatus = (status) => {
324
325
  var isSubmarineRefundableStatus = (status) => {
325
326
  return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status);
326
327
  };
328
+ var SUBMARINE_STATUS_PROGRESSION = [
329
+ "swap.created",
330
+ "invoice.set",
331
+ "transaction.mempool",
332
+ "transaction.confirmed",
333
+ "invoice.pending",
334
+ "invoice.paid",
335
+ "transaction.claim.pending",
336
+ "transaction.claimed"
337
+ ];
338
+ var hasSubmarineStatusReached = (status, target) => {
339
+ const progression = SUBMARINE_STATUS_PROGRESSION;
340
+ const statusIndex = progression.indexOf(status);
341
+ const targetIndex = progression.indexOf(target);
342
+ return statusIndex >= 0 && targetIndex >= 0 && statusIndex >= targetIndex;
343
+ };
327
344
  var isSubmarineSuccessStatus = (status) => {
328
345
  return status === "transaction.claimed";
329
346
  };
@@ -1408,10 +1425,10 @@ var import_light_bolt11_decoder = __toESM(require("light-bolt11-decoder"), 1);
1408
1425
  var import_sdk2 = require("@arkade-os/sdk");
1409
1426
  var decodeInvoice = (invoice) => {
1410
1427
  const decoded = import_light_bolt11_decoder.default.decode(invoice);
1411
- const millisats = Number(decoded.sections.find((s) => s.name === "amount")?.value ?? "0");
1428
+ const millisats = BigInt(decoded.sections.find((s) => s.name === "amount")?.value ?? "0");
1412
1429
  return {
1413
1430
  expiry: decoded.expiry ?? 3600,
1414
- amountSats: Math.floor(millisats / 1e3),
1431
+ amountSats: Number(millisats / 1000n),
1415
1432
  description: decoded.sections.find((s) => s.name === "description")?.value ?? "",
1416
1433
  paymentHash: decoded.sections.find((s) => s.name === "payment_hash")?.value ?? ""
1417
1434
  };
@@ -3047,6 +3064,19 @@ var createVHTLCScript = (args) => {
3047
3064
  const vhtlcAddress = vhtlcScript.address(hrp, serverXOnlyPublicKey).encode();
3048
3065
  return { vhtlcScript, vhtlcAddress };
3049
3066
  };
3067
+ var candidateServerPubkeys = (arkInfo) => {
3068
+ const current = import_base8.hex.encode(normalizeToXOnlyKey(import_base8.hex.decode(arkInfo.signerPubkey), "server"));
3069
+ const seen = /* @__PURE__ */ new Set([current]);
3070
+ const candidates = [current];
3071
+ for (const deprecated of arkInfo.deprecatedSigners ?? []) {
3072
+ if (!deprecated.pubkey) continue;
3073
+ const key = import_base8.hex.encode(normalizeToXOnlyKey(import_base8.hex.decode(deprecated.pubkey), "server"));
3074
+ if (seen.has(key)) continue;
3075
+ seen.add(key);
3076
+ candidates.push(key);
3077
+ }
3078
+ return candidates;
3079
+ };
3050
3080
  var joinBatch = async (arkProvider, identity, input, output, {
3051
3081
  forfeitPubkey,
3052
3082
  forfeitAddress,
@@ -3461,6 +3491,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
3461
3491
  ...args.description?.trim() ? { description: args.description.trim() } : {}
3462
3492
  };
3463
3493
  const swapResponse = await this.swapProvider.createReverseSwap(swapRequest);
3494
+ const decodedInvoice = decodeInvoice(swapResponse.invoice);
3495
+ if (decodedInvoice.paymentHash !== preimageHash) {
3496
+ throw new SwapError({ message: "Preimage hash does not match invoice payment hash" });
3497
+ }
3464
3498
  const pendingSwap = {
3465
3499
  id: swapResponse.id,
3466
3500
  type: "reverse",
@@ -3504,27 +3538,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
3504
3538
  "boltz",
3505
3539
  pendingSwap.id
3506
3540
  );
3507
- const serverXOnly = normalizeToXOnlyKey(
3508
- import_base9.hex.decode(arkInfo.signerPubkey),
3509
- "server",
3510
- pendingSwap.id
3511
- );
3512
- const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({
3513
- network: arkInfo.network,
3541
+ const { vhtlcScript, serverXOnlyPublicKey: serverXOnly } = this.resolveVHTLCForLockup({
3542
+ arkInfo,
3514
3543
  preimageHash: (0, import_sha23.sha256)(preimage),
3515
3544
  receiverPubkey: import_base9.hex.encode(receiverXOnly),
3516
3545
  senderPubkey: import_base9.hex.encode(senderXOnly),
3517
- serverPubkey: import_base9.hex.encode(serverXOnly),
3518
- timeoutBlockHeights: vhtlcTimeouts
3546
+ timeoutBlockHeights: vhtlcTimeouts,
3547
+ lockupAddress,
3548
+ swapId: pendingSwap.id
3519
3549
  });
3520
- if (!vhtlcScript.claimScript)
3521
- throw new Error(
3522
- `Swap ${pendingSwap.id}: failed to create VHTLC script for reverse swap`
3523
- );
3524
- if (vhtlcAddress !== lockupAddress)
3525
- throw new Error(
3526
- `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3527
- );
3528
3550
  let unspentVtxos = [];
3529
3551
  let rawVtxos = [];
3530
3552
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
@@ -3675,16 +3697,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
3675
3697
  this.swapProvider.monitorSwap(pendingSwap.id, onStatusUpdate).catch(reject);
3676
3698
  });
3677
3699
  }
3678
- // =========================================================================
3679
- // Lightning: Submarine swaps (send Arkade -> Lightning)
3680
- // =========================================================================
3681
- /**
3682
- * Sends a Lightning payment via a submarine swap (Arkade → Lightning).
3683
- * Creates the swap, sends funds, and waits for settlement. Auto-refunds on failure.
3684
- * @param args.invoice - BOLT11 Lightning invoice to pay.
3685
- * @returns The amount paid, preimage (proof of payment), and transaction ID.
3686
- * @throws {TransactionFailedError} If the payment fails (auto-refunds if possible).
3687
- */
3688
3700
  async sendLightningPayment(args) {
3689
3701
  const pendingSwap = await this.createSubmarineSwap(args);
3690
3702
  if (!pendingSwap.response.address)
@@ -3695,6 +3707,18 @@ var ArkadeSwaps = class _ArkadeSwaps {
3695
3707
  amount: pendingSwap.response.expectedAmount
3696
3708
  });
3697
3709
  try {
3710
+ if (args.waitFor === "funded") {
3711
+ if (!this.swapManager) {
3712
+ logger.warn(
3713
+ `Swap ${pendingSwap.id}: sendLightningPayment with waitFor "funded" but SwapManager is disabled \u2014 a failure after this promise resolves is only persisted as refundable, not auto-refunded; recover via restoreSwaps/recoverSubmarineFunds`
3714
+ );
3715
+ }
3716
+ await this.waitForSwapFunded(pendingSwap);
3717
+ return {
3718
+ amount: pendingSwap.response.expectedAmount,
3719
+ txid
3720
+ };
3721
+ }
3698
3722
  const { preimage } = await this.waitForSwapSettlement(pendingSwap);
3699
3723
  return {
3700
3724
  amount: pendingSwap.response.expectedAmount,
@@ -3766,11 +3790,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
3766
3790
  "our",
3767
3791
  swap.id
3768
3792
  );
3769
- const serverXOnlyPublicKey = normalizeToXOnlyKey(
3770
- import_base9.hex.decode(resolvedArkInfo.signerPubkey),
3771
- "server",
3772
- swap.id
3773
- );
3774
3793
  const { claimPublicKey, timeoutBlockHeights: vhtlcTimeouts } = swap.response;
3775
3794
  if (!claimPublicKey || !vhtlcTimeouts)
3776
3795
  throw new Error(`Swap ${swap.id}: incomplete submarine swap response`);
@@ -3779,20 +3798,19 @@ var ArkadeSwaps = class _ArkadeSwaps {
3779
3798
  "boltz",
3780
3799
  swap.id
3781
3800
  );
3782
- const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({
3783
- network: resolvedArkInfo.network,
3801
+ const lockupAddress = swap.response.address;
3802
+ if (!lockupAddress)
3803
+ throw new Error(`Swap ${swap.id}: missing lockup address in submarine swap response`);
3804
+ const { vhtlcScript, serverXOnlyPublicKey } = this.resolveVHTLCForLockup({
3805
+ arkInfo: resolvedArkInfo,
3784
3806
  preimageHash: import_base9.hex.decode(preimageHash),
3785
3807
  receiverPubkey: import_base9.hex.encode(boltzXOnlyPublicKey),
3786
3808
  senderPubkey: import_base9.hex.encode(ourXOnlyPublicKey),
3787
- serverPubkey: import_base9.hex.encode(serverXOnlyPublicKey),
3788
- timeoutBlockHeights: vhtlcTimeouts
3809
+ timeoutBlockHeights: vhtlcTimeouts,
3810
+ lockupAddress,
3811
+ swapId: swap.id
3789
3812
  });
3790
- if (!vhtlcScript.claimScript)
3791
- throw new Error(`Swap ${swap.id}: failed to create VHTLC script for submarine swap`);
3792
- if (vhtlcAddress !== swap.response.address)
3793
- throw new Error(
3794
- `VHTLC address mismatch for swap ${swap.id}: expected ${swap.response.address}, got ${vhtlcAddress}`
3795
- );
3813
+ const vhtlcAddress = lockupAddress;
3796
3814
  const vhtlcPkScriptHex = import_base9.hex.encode(vhtlcScript.pkScript);
3797
3815
  return {
3798
3816
  arkInfo: resolvedArkInfo,
@@ -4202,6 +4220,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4202
4220
  }
4203
4221
  /**
4204
4222
  * Waits for a submarine swap's Lightning payment to settle.
4223
+ * Resolves only at the terminal "transaction.claimed" status, once Boltz
4224
+ * has swept the HTLC. To resolve as soon as the payment is in flight, use
4225
+ * {@link waitForSwapFunded} instead.
4205
4226
  * @param pendingSwap - The submarine swap to monitor.
4206
4227
  * @returns The preimage from the settled Lightning payment (proof of payment).
4207
4228
  * @throws {SwapExpiredError} If the swap expires.
@@ -4209,19 +4230,59 @@ var ArkadeSwaps = class _ArkadeSwaps {
4209
4230
  * @throws {TransactionLockupFailedError} If the lockup transaction fails.
4210
4231
  */
4211
4232
  async waitForSwapSettlement(pendingSwap) {
4233
+ return this.waitForSubmarineSwap(pendingSwap);
4234
+ }
4235
+ /**
4236
+ * Waits until a submarine swap is funded: resolves as soon as the lockup
4237
+ * transaction is observed ("transaction.mempool" or any later status in
4238
+ * the lifecycle — statuses can be skipped since subscriptions report only
4239
+ * the current one). The sender's funds are committed and the swap is
4240
+ * refundable from this point, which is when most Lightning wallets show
4241
+ * a payment as "sent".
4242
+ *
4243
+ * Monitoring continues in the background until the swap reaches a
4244
+ * terminal status, persisting updates to the repository (the preimage on
4245
+ * claim, the refundable flag on failure), but this promise no longer
4246
+ * rejects once resolved — acting on a late failure is the caller's
4247
+ * responsibility (the SwapManager handles it automatically when enabled).
4248
+ * @param pendingSwap - The submarine swap to monitor.
4249
+ * @throws {SwapExpiredError} If the swap expires before funding.
4250
+ * @throws {InvoiceFailedToPayError} If Boltz fails to route the payment.
4251
+ * @throws {TransactionLockupFailedError} If the lockup transaction fails.
4252
+ */
4253
+ async waitForSwapFunded(pendingSwap) {
4254
+ await this.waitForSubmarineSwap(pendingSwap, "transaction.mempool");
4255
+ }
4256
+ /**
4257
+ * Shared wait machinery: monitors the swap and resolves at the terminal
4258
+ * "transaction.claimed" status (with the preimage) or, when `resolveAt`
4259
+ * is given, as soon as that status — or any later one in the successful
4260
+ * progression — is observed (without a preimage).
4261
+ */
4262
+ async waitForSubmarineSwap(pendingSwap, resolveAt) {
4212
4263
  return new Promise((resolve, reject) => {
4213
- let isResolved = false;
4264
+ let isFinal = false;
4265
+ let isSettled = false;
4214
4266
  const onStatusUpdate = async (status) => {
4215
- if (isResolved) return;
4216
- const saveStatus = (additionalFields) => updateSubmarineSwapStatus(
4217
- pendingSwap,
4218
- status,
4219
- this.savePendingSubmarineSwap.bind(this),
4220
- additionalFields
4221
- );
4267
+ if (isFinal) return;
4268
+ const saveStatus = async (additionalFields) => {
4269
+ try {
4270
+ await updateSubmarineSwapStatus(
4271
+ pendingSwap,
4272
+ status,
4273
+ this.savePendingSubmarineSwap.bind(this),
4274
+ additionalFields
4275
+ );
4276
+ } catch (error) {
4277
+ logger.error(
4278
+ `Swap ${pendingSwap.id}: failed to persist status "${status}": ${error}`
4279
+ );
4280
+ }
4281
+ };
4222
4282
  switch (status) {
4223
4283
  case "swap.expired":
4224
- isResolved = true;
4284
+ isFinal = true;
4285
+ isSettled = true;
4225
4286
  await saveStatus({ refundable: true });
4226
4287
  reject(
4227
4288
  new SwapExpiredError({
@@ -4231,7 +4292,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4231
4292
  );
4232
4293
  break;
4233
4294
  case "invoice.failedToPay":
4234
- isResolved = true;
4295
+ isFinal = true;
4296
+ isSettled = true;
4235
4297
  await saveStatus({ refundable: true });
4236
4298
  reject(
4237
4299
  new InvoiceFailedToPayError({
@@ -4241,7 +4303,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4241
4303
  );
4242
4304
  break;
4243
4305
  case "transaction.lockupFailed":
4244
- isResolved = true;
4306
+ isFinal = true;
4307
+ isSettled = true;
4245
4308
  await saveStatus({ refundable: true });
4246
4309
  reject(
4247
4310
  new TransactionLockupFailedError({
@@ -4251,23 +4314,47 @@ var ArkadeSwaps = class _ArkadeSwaps {
4251
4314
  );
4252
4315
  break;
4253
4316
  case "transaction.claimed": {
4254
- isResolved = true;
4255
- const { preimage } = await this.swapProvider.getSwapPreimage(
4256
- pendingSwap.id
4257
- );
4258
- await saveStatus({ preimage });
4259
- resolve({ preimage });
4317
+ isFinal = true;
4318
+ isSettled = true;
4319
+ try {
4320
+ const { preimage } = await this.swapProvider.getSwapPreimage(
4321
+ pendingSwap.id
4322
+ );
4323
+ await saveStatus({ preimage });
4324
+ resolve({ preimage });
4325
+ } catch (error) {
4326
+ logger.error(
4327
+ `Swap ${pendingSwap.id}: failed to fetch preimage on claim: ${error}`
4328
+ );
4329
+ reject(error);
4330
+ }
4260
4331
  break;
4261
4332
  }
4262
4333
  default:
4263
4334
  await saveStatus();
4335
+ if (resolveAt && hasSubmarineStatusReached(status, resolveAt)) {
4336
+ isSettled = true;
4337
+ resolve({ preimage: void 0 });
4338
+ }
4264
4339
  break;
4265
4340
  }
4266
4341
  };
4267
- this.swapProvider.monitorSwap(pendingSwap.id, onStatusUpdate).catch((error) => {
4268
- if (!isResolved) {
4269
- isResolved = true;
4342
+ this.swapProvider.monitorSwap(pendingSwap.id, (status) => {
4343
+ onStatusUpdate(status).catch(
4344
+ (error) => logger.error(
4345
+ `Swap ${pendingSwap.id}: error handling status "${status}": ${error}`
4346
+ )
4347
+ );
4348
+ }).catch((error) => {
4349
+ if (!isSettled) {
4350
+ isFinal = true;
4351
+ isSettled = true;
4270
4352
  reject(error);
4353
+ } else {
4354
+ isFinal = true;
4355
+ logger.warn(
4356
+ `Swap ${pendingSwap.id}: monitor failed after settlement: ${error}`
4357
+ );
4271
4358
  }
4272
4359
  });
4273
4360
  });
@@ -4505,29 +4592,26 @@ var ArkadeSwaps = class _ArkadeSwaps {
4505
4592
  "user",
4506
4593
  pendingSwap.id
4507
4594
  );
4508
- const serverXOnlyPublicKey = normalizeToXOnlyKey(
4509
- import_base9.hex.decode(arkInfo.signerPubkey),
4510
- "server",
4511
- pendingSwap.id
4512
- );
4513
4595
  const boltzXOnlyPublicKey = normalizeToXOnlyKey(
4514
4596
  import_base9.hex.decode(pendingSwap.response.lockupDetails.serverPublicKey),
4515
4597
  "boltz",
4516
4598
  pendingSwap.id
4517
4599
  );
4518
- const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4519
- network: arkInfo.network,
4520
- preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
4521
- serverPubkey: import_base9.hex.encode(serverXOnlyPublicKey),
4522
- senderPubkey: import_base9.hex.encode(ourXOnlyPublicKey),
4523
- receiverPubkey: import_base9.hex.encode(boltzXOnlyPublicKey),
4524
- timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
4525
- });
4526
- if (!vhtlcScript.refundScript)
4527
- throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
4528
- if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
4600
+ let vhtlcScript;
4601
+ let serverXOnlyPublicKey;
4602
+ try {
4603
+ ({ vhtlcScript, serverXOnlyPublicKey } = this.resolveVHTLCForLockup({
4604
+ arkInfo,
4605
+ preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
4606
+ senderPubkey: import_base9.hex.encode(ourXOnlyPublicKey),
4607
+ receiverPubkey: import_base9.hex.encode(boltzXOnlyPublicKey),
4608
+ timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts,
4609
+ lockupAddress: pendingSwap.response.lockupDetails.lockupAddress,
4610
+ swapId: pendingSwap.id
4611
+ }));
4612
+ } catch (error) {
4529
4613
  throw new SwapError({
4530
- message: "Unable to claim: invalid VHTLC address"
4614
+ message: error instanceof Error ? error.message : "Unable to refund: invalid VHTLC address"
4531
4615
  });
4532
4616
  }
4533
4617
  const { vtxos } = await this.indexerProvider.getVtxos({
@@ -4786,20 +4870,21 @@ var ArkadeSwaps = class _ArkadeSwaps {
4786
4870
  pendingSwap.response.claimDetails.serverPublicKey,
4787
4871
  "sender"
4788
4872
  );
4789
- const serverXOnlyPublicKey = normalizeToXOnlyKey(arkInfo.signerPubkey, "server");
4790
- const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4791
- network: arkInfo.network,
4792
- preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
4793
- serverPubkey: import_base9.hex.encode(serverXOnlyPublicKey),
4794
- senderPubkey: import_base9.hex.encode(senderXOnlyPublicKey),
4795
- receiverPubkey: import_base9.hex.encode(receiverXOnlyPublicKey),
4796
- timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
4797
- });
4798
- if (!vhtlcScript.claimScript)
4799
- throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
4800
- if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
4873
+ let vhtlcScript;
4874
+ let serverXOnlyPublicKey;
4875
+ try {
4876
+ ({ vhtlcScript, serverXOnlyPublicKey } = this.resolveVHTLCForLockup({
4877
+ arkInfo,
4878
+ preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
4879
+ senderPubkey: import_base9.hex.encode(senderXOnlyPublicKey),
4880
+ receiverPubkey: import_base9.hex.encode(receiverXOnlyPublicKey),
4881
+ timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts,
4882
+ lockupAddress: pendingSwap.response.claimDetails.lockupAddress,
4883
+ swapId: pendingSwap.id
4884
+ }));
4885
+ } catch (error) {
4801
4886
  throw new SwapError({
4802
- message: "Unable to claim: invalid VHTLC address"
4887
+ message: error instanceof Error ? error.message : "Unable to claim: invalid VHTLC address"
4803
4888
  });
4804
4889
  }
4805
4890
  let vtxo;
@@ -5161,6 +5246,55 @@ var ArkadeSwaps = class _ArkadeSwaps {
5161
5246
  createVHTLCScript(args) {
5162
5247
  return createVHTLCScript(args);
5163
5248
  }
5249
+ /**
5250
+ * Reconstruct a swap's VHTLC by matching the persisted `lockupAddress`
5251
+ * against the current and deprecated server signers, returning the matched
5252
+ * script together with the server key it was minted under.
5253
+ *
5254
+ * Recovery paths (claim/refund/lookup) must use this instead of building the
5255
+ * VHTLC from the current signer alone: a swap created before a planned arkd
5256
+ * signer rotation is locked to a now-deprecated signer, so the current key
5257
+ * would yield the wrong address and strand the funds. The returned
5258
+ * `serverXOnlyPublicKey` is the original (possibly deprecated) key and MUST
5259
+ * be threaded into downstream signing/verification.
5260
+ *
5261
+ * Throws a descriptive mismatch error when no candidate reproduces the
5262
+ * lockup address (e.g. the swap predates an already-pruned deprecated
5263
+ * signer) — replacing the previous current-signer-only equality check.
5264
+ */
5265
+ resolveVHTLCForLockup(args) {
5266
+ const candidates = candidateServerPubkeys(args.arkInfo);
5267
+ for (const serverPubkey of candidates) {
5268
+ const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({
5269
+ network: args.arkInfo.network,
5270
+ preimageHash: args.preimageHash,
5271
+ receiverPubkey: args.receiverPubkey,
5272
+ senderPubkey: args.senderPubkey,
5273
+ serverPubkey,
5274
+ timeoutBlockHeights: args.timeoutBlockHeights
5275
+ });
5276
+ if (vhtlcAddress !== args.lockupAddress) continue;
5277
+ if (!vhtlcScript.claimScript && !vhtlcScript.refundScript) {
5278
+ throw new Error(
5279
+ `Swap ${args.swapId}: VHTLC address matched but claim/refund script leaves are empty`
5280
+ );
5281
+ }
5282
+ return {
5283
+ vhtlcScript,
5284
+ // The matched (possibly deprecated) key must flow into downstream
5285
+ // signing/verification: arkd signs a deprecated-signer input with
5286
+ // the deprecated key, so the claim/refund leaf checks use it.
5287
+ serverXOnlyPublicKey: normalizeToXOnlyKey(
5288
+ import_base9.hex.decode(serverPubkey),
5289
+ "server",
5290
+ args.swapId
5291
+ )
5292
+ };
5293
+ }
5294
+ throw new Error(
5295
+ `Swap ${args.swapId}: VHTLC address mismatch. Expected ${args.lockupAddress}; no current or deprecated server signer (${candidates.length} candidate(s) tried) reproduced it`
5296
+ );
5297
+ }
5164
5298
  async getFees(from, to) {
5165
5299
  if (from && to) {
5166
5300
  return this.swapProvider.getChainFees(from, to);
@@ -5244,13 +5378,28 @@ var ArkadeSwaps = class _ArkadeSwaps {
5244
5378
  for (const swap of await this.getPendingSubmarineSwapsFromStorage()) {
5245
5379
  if (isSubmarineFinalStatus(swap.status)) continue;
5246
5380
  promises.push(
5247
- this.getSwapStatus(swap.id).then(
5248
- ({ status }) => updateSubmarineSwapStatus(
5381
+ this.getSwapStatus(swap.id).then(async ({ status }) => {
5382
+ let additionalFields;
5383
+ if (isSubmarineSuccessStatus(status) && !swap.preimage) {
5384
+ try {
5385
+ const { preimage } = await this.swapProvider.getSwapPreimage(
5386
+ swap.id
5387
+ );
5388
+ additionalFields = { preimage };
5389
+ } catch (error) {
5390
+ logger.warn(
5391
+ `Failed to fetch preimage for settled swap ${swap.id}:`,
5392
+ error
5393
+ );
5394
+ }
5395
+ }
5396
+ await updateSubmarineSwapStatus(
5249
5397
  swap,
5250
5398
  status,
5251
- this.savePendingSubmarineSwap.bind(this)
5252
- )
5253
- ).catch((error) => {
5399
+ this.savePendingSubmarineSwap.bind(this),
5400
+ additionalFields
5401
+ );
5402
+ }).catch((error) => {
5254
5403
  logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
5255
5404
  })
5256
5405
  );
@@ -5429,6 +5578,7 @@ var LONG_RUNNING_ARKADE_SWAPS_REQUEST_TYPES = /* @__PURE__ */ new Set([
5429
5578
  "RECOVER_ALL_SUBMARINE_FUNDS",
5430
5579
  "WAIT_AND_CLAIM",
5431
5580
  "WAIT_FOR_SWAP_SETTLEMENT",
5581
+ "WAIT_FOR_SWAP_FUNDED",
5432
5582
  "RESTORE_SWAPS",
5433
5583
  "WAIT_AND_CLAIM_CHAIN",
5434
5584
  "WAIT_AND_CLAIM_ARK",
@@ -5616,6 +5766,10 @@ var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
5616
5766
  payload: res
5617
5767
  });
5618
5768
  }
5769
+ case "WAIT_FOR_SWAP_FUNDED": {
5770
+ await this.handler.waitForSwapFunded(message.payload);
5771
+ return this.tagged({ id, type: "SWAP_FUNDED" });
5772
+ }
5619
5773
  case "RESTORE_SWAPS": {
5620
5774
  const res = await this.handler.restoreSwaps(message.payload);
5621
5775
  return this.tagged({
@@ -6317,6 +6471,18 @@ var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
6317
6471
  throw new Error("Cannot wait for swap settlement", { cause: e });
6318
6472
  }
6319
6473
  }
6474
+ async waitForSwapFunded(pendingSwap) {
6475
+ try {
6476
+ await this.sendMessage({
6477
+ id: (0, import_sdk10.getRandomId)(),
6478
+ tag: this.messageTag,
6479
+ type: "WAIT_FOR_SWAP_FUNDED",
6480
+ payload: pendingSwap
6481
+ });
6482
+ } catch (e) {
6483
+ throw new Error("Cannot wait for swap funding", { cause: e });
6484
+ }
6485
+ }
6320
6486
  async restoreSwaps(boltzFees) {
6321
6487
  try {
6322
6488
  const res = await this.sendMessage({
@@ -6905,6 +7071,7 @@ var InMemorySwapRepository = class {
6905
7071
  enrichSubmarineSwapInvoice,
6906
7072
  getInvoicePaymentHash,
6907
7073
  getInvoiceSatoshis,
7074
+ hasSubmarineStatusReached,
6908
7075
  isChainClaimableStatus,
6909
7076
  isChainFailedStatus,
6910
7077
  isChainFinalStatus,
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { Q as QuoteSwapOptions, I as IArkadeSwaps, V as VhtlcTimeouts } from './arkade-swaps-DG9UepoS.cjs';
2
- export { A as ArkadeSwaps } from './arkade-swaps-DG9UepoS.cjs';
3
- import { B as BoltzSwap, D as DecodedInvoice, a as BoltzChainSwap, b as BoltzReverseSwap, c as BoltzSubmarineSwap, A as ArkadeSwapsConfig, N as Network, C as CreateLightningInvoiceRequest, S as SendLightningPaymentRequest, F as FeesResponse, d as Chain, e as CreateLightningInvoiceResponse, f as SendLightningPaymentResponse, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, k as ArkToBtcResponse, l as BtcToArkResponse, m as ChainArkRefundOutcome, n as SwapRepository, o as SwapManagerClient, p as GetSwapsFilter } from './types-D97i1LFu.cjs';
4
- export { q as ArkadeSwapsCreateConfig, r as BoltzSwapProvider, s as BoltzSwapStatus, I as IncomingPaymentSubscription, P as PendingChainSwap, t as PendingReverseSwap, u as PendingSubmarineSwap, v as PendingSwap, w as SubmarineRecoveryStatus, x as SwapManager, y as SwapManagerCallbacks, z as SwapManagerConfig, E as SwapManagerEvents, V as Vtxo, H as isChainClaimableStatus, J as isChainFailedStatus, K as isChainFinalStatus, M as isChainPendingStatus, O as isChainRefundableStatus, Q as isChainSignableStatus, R as isChainSuccessStatus, T as isChainSwapClaimable, U as isChainSwapRefundable, W as isPendingChainSwap, X as isPendingReverseSwap, Y as isPendingSubmarineSwap, Z as isReverseClaimableStatus, _ as isReverseFailedStatus, $ as isReverseFinalStatus, a0 as isReversePendingStatus, a1 as isReverseSuccessStatus, a2 as isReverseSwapClaimable, a3 as isSubmarineFailedStatus, a4 as isSubmarineFinalStatus, a5 as isSubmarinePendingStatus, a6 as isSubmarineRefundableStatus, a7 as isSubmarineSuccessStatus, a8 as isSubmarineSwapRefundable } from './types-D97i1LFu.cjs';
1
+ import { Q as QuoteSwapOptions, I as IArkadeSwaps, V as VhtlcTimeouts } from './arkade-swaps-C3sUFr5f.cjs';
2
+ export { A as ArkadeSwaps } from './arkade-swaps-C3sUFr5f.cjs';
3
+ import { B as BoltzSwap, D as DecodedInvoice, a as BoltzChainSwap, b as BoltzReverseSwap, c as BoltzSubmarineSwap, A as ArkadeSwapsConfig, N as Network, C as CreateLightningInvoiceRequest, S as SendLightningPaymentRequest, F as FeesResponse, d as Chain, e as CreateLightningInvoiceResponse, O as OptimisticSendLightningPaymentResponse, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, j as ArkToBtcResponse, k as BtcToArkResponse, l as ChainArkRefundOutcome, m as SwapRepository, n as SwapManagerClient, o as SendLightningPaymentResponse, p as GetSwapsFilter } from './types-8NrCdOpS.cjs';
4
+ export { q as ArkadeSwapsCreateConfig, r as BoltzSwapProvider, s as BoltzSwapStatus, I as IncomingPaymentSubscription, P as PendingChainSwap, t as PendingReverseSwap, u as PendingSubmarineSwap, v as PendingSwap, w as SubmarineProgressionStatus, x as SubmarineRecoveryStatus, y as SwapManager, z as SwapManagerCallbacks, E as SwapManagerConfig, H as SwapManagerEvents, V as Vtxo, J as hasSubmarineStatusReached, K as isChainClaimableStatus, M as isChainFailedStatus, Q as isChainFinalStatus, R as isChainPendingStatus, T as isChainRefundableStatus, U as isChainSignableStatus, W as isChainSuccessStatus, X as isChainSwapClaimable, Y as isChainSwapRefundable, Z as isPendingChainSwap, _ as isPendingReverseSwap, $ as isPendingSubmarineSwap, a0 as isReverseClaimableStatus, a1 as isReverseFailedStatus, a2 as isReverseFinalStatus, a3 as isReversePendingStatus, a4 as isReverseSuccessStatus, a5 as isReverseSwapClaimable, a6 as isSubmarineFailedStatus, a7 as isSubmarineFinalStatus, a8 as isSubmarinePendingStatus, a9 as isSubmarineRefundableStatus, aa as isSubmarineSuccessStatus, ab as isSubmarineSwapRefundable } from './types-8NrCdOpS.cjs';
5
5
  import { Transaction } from '@scure/btc-signer';
6
6
  import { MessageHandler, RequestEnvelope, ArkInfo, ResponseEnvelope, IWallet, IReadonlyWallet, VHTLC, Identity, ArkTxInput } from '@arkade-os/sdk';
7
7
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
@@ -225,7 +225,8 @@ type RequestSendLightningPayment = RequestEnvelope & {
225
225
  };
226
226
  type ResponseSendLightningPayment = ResponseEnvelope & {
227
227
  type: "LIGHTNING_PAYMENT_SENT";
228
- payload: SendLightningPaymentResponse;
228
+ /** Strict SendLightningPaymentResponse unless the request used waitFor: "funded". */
229
+ payload: OptimisticSendLightningPaymentResponse;
229
230
  };
230
231
  type RequestCreateSubmarineSwap = RequestEnvelope & {
231
232
  type: "CREATE_SUBMARINE_SWAP";
@@ -309,6 +310,13 @@ type ResponseWaitForSwapSettlement = ResponseEnvelope & {
309
310
  preimage: string;
310
311
  };
311
312
  };
313
+ type RequestWaitForSwapFunded = RequestEnvelope & {
314
+ type: "WAIT_FOR_SWAP_FUNDED";
315
+ payload: BoltzSubmarineSwap;
316
+ };
317
+ type ResponseWaitForSwapFunded = ResponseEnvelope & {
318
+ type: "SWAP_FUNDED";
319
+ };
312
320
  type RequestRestoreSwaps = RequestEnvelope & {
313
321
  type: "RESTORE_SWAPS";
314
322
  payload?: FeesResponse;
@@ -653,8 +661,8 @@ type ResponseSwapManagerWaitForCompletion = ResponseEnvelope & {
653
661
  txid: string;
654
662
  };
655
663
  };
656
- type ArkadeSwapsUpdaterRequest = RequestInitArkSwaps | RequestCreateLightningInvoice | RequestSendLightningPayment | RequestCreateSubmarineSwap | RequestCreateReverseSwap | RequestClaimVhtlc | RequestRefundVhtlc | RequestInspectSubmarineRecovery | RequestScanRecoverableSubmarineSwaps | RequestRecoverSubmarineFunds | RequestRecoverAllSubmarineFunds | RequestWaitAndClaim | RequestWaitForSwapSettlement | RequestRestoreSwaps | RequestEnrichReverseSwapPreimage | RequestEnrichSubmarineSwapInvoice | RequestGetFees | RequestGetLimits | RequestGetSwapStatus | RequestGetPendingSubmarineSwaps | RequestGetPendingReverseSwaps | RequestGetPendingChainSwaps | RequestGetSwapHistory | RequestRefreshSwapsStatus | RequestArkToBtc | RequestBtcToArk | RequestCreateChainSwap | RequestWaitAndClaimChain | RequestWaitAndClaimArk | RequestWaitAndClaimBtc | RequestClaimArk | RequestClaimBtc | RequestRefundArk | RequestSignServerClaim | RequestVerifyChainSwap | RequestQuoteSwap | RequestGetSwapQuote | RequestAcceptSwapQuote | RequestSwapManagerStart | RequestSwapManagerStop | RequestSwapManagerAddSwap | RequestSwapManagerRemoveSwap | RequestSwapManagerGetPending | RequestSwapManagerHasSwap | RequestSwapManagerIsProcessing | RequestSwapManagerGetStats | RequestSwapManagerWaitForCompletion;
657
- type ArkadeSwapsUpdaterResponse = ResponseInitArkSwaps | ResponseCreateLightningInvoice | ResponseSendLightningPayment | ResponseCreateSubmarineSwap | ResponseCreateReverseSwap | ResponseClaimVhtlc | ResponseRefundVhtlc | ResponseInspectSubmarineRecovery | ResponseScanRecoverableSubmarineSwaps | ResponseRecoverSubmarineFunds | ResponseRecoverAllSubmarineFunds | ResponseWaitAndClaim | ResponseWaitForSwapSettlement | ResponseRestoreSwaps | ResponseEnrichReverseSwapPreimage | ResponseEnrichSubmarineSwapInvoice | ResponseGetFees | ResponseGetLimits | ResponseGetSwapStatus | ResponseGetPendingSubmarineSwaps | ResponseGetPendingReverseSwaps | ResponseGetPendingChainSwaps | ResponseGetSwapHistory | ResponseRefreshSwapsStatus | ResponseArkToBtc | ResponseBtcToArk | ResponseCreateChainSwap | ResponseWaitAndClaimChain | ResponseWaitAndClaimArk | ResponseWaitAndClaimBtc | ResponseClaimArk | ResponseClaimBtc | ResponseRefundArk | ResponseSignServerClaim | ResponseVerifyChainSwap | ResponseQuoteSwap | ResponseGetSwapQuote | ResponseAcceptSwapQuote | ResponseSwapManagerStart | ResponseSwapManagerStop | ResponseSwapManagerAddSwap | ResponseSwapManagerRemoveSwap | ResponseSwapManagerGetPending | ResponseSwapManagerHasSwap | ResponseSwapManagerIsProcessing | ResponseSwapManagerGetStats | ResponseSwapManagerWaitForCompletion;
664
+ type ArkadeSwapsUpdaterRequest = RequestInitArkSwaps | RequestCreateLightningInvoice | RequestSendLightningPayment | RequestCreateSubmarineSwap | RequestCreateReverseSwap | RequestClaimVhtlc | RequestRefundVhtlc | RequestInspectSubmarineRecovery | RequestScanRecoverableSubmarineSwaps | RequestRecoverSubmarineFunds | RequestRecoverAllSubmarineFunds | RequestWaitAndClaim | RequestWaitForSwapSettlement | RequestWaitForSwapFunded | RequestRestoreSwaps | RequestEnrichReverseSwapPreimage | RequestEnrichSubmarineSwapInvoice | RequestGetFees | RequestGetLimits | RequestGetSwapStatus | RequestGetPendingSubmarineSwaps | RequestGetPendingReverseSwaps | RequestGetPendingChainSwaps | RequestGetSwapHistory | RequestRefreshSwapsStatus | RequestArkToBtc | RequestBtcToArk | RequestCreateChainSwap | RequestWaitAndClaimChain | RequestWaitAndClaimArk | RequestWaitAndClaimBtc | RequestClaimArk | RequestClaimBtc | RequestRefundArk | RequestSignServerClaim | RequestVerifyChainSwap | RequestQuoteSwap | RequestGetSwapQuote | RequestAcceptSwapQuote | RequestSwapManagerStart | RequestSwapManagerStop | RequestSwapManagerAddSwap | RequestSwapManagerRemoveSwap | RequestSwapManagerGetPending | RequestSwapManagerHasSwap | RequestSwapManagerIsProcessing | RequestSwapManagerGetStats | RequestSwapManagerWaitForCompletion;
665
+ type ArkadeSwapsUpdaterResponse = ResponseInitArkSwaps | ResponseCreateLightningInvoice | ResponseSendLightningPayment | ResponseCreateSubmarineSwap | ResponseCreateReverseSwap | ResponseClaimVhtlc | ResponseRefundVhtlc | ResponseInspectSubmarineRecovery | ResponseScanRecoverableSubmarineSwaps | ResponseRecoverSubmarineFunds | ResponseRecoverAllSubmarineFunds | ResponseWaitAndClaim | ResponseWaitForSwapSettlement | ResponseWaitForSwapFunded | ResponseRestoreSwaps | ResponseEnrichReverseSwapPreimage | ResponseEnrichSubmarineSwapInvoice | ResponseGetFees | ResponseGetLimits | ResponseGetSwapStatus | ResponseGetPendingSubmarineSwaps | ResponseGetPendingReverseSwaps | ResponseGetPendingChainSwaps | ResponseGetSwapHistory | ResponseRefreshSwapsStatus | ResponseArkToBtc | ResponseBtcToArk | ResponseCreateChainSwap | ResponseWaitAndClaimChain | ResponseWaitAndClaimArk | ResponseWaitAndClaimBtc | ResponseClaimArk | ResponseClaimBtc | ResponseRefundArk | ResponseSignServerClaim | ResponseVerifyChainSwap | ResponseQuoteSwap | ResponseGetSwapQuote | ResponseAcceptSwapQuote | ResponseSwapManagerStart | ResponseSwapManagerStop | ResponseSwapManagerAddSwap | ResponseSwapManagerRemoveSwap | ResponseSwapManagerGetPending | ResponseSwapManagerHasSwap | ResponseSwapManagerIsProcessing | ResponseSwapManagerGetStats | ResponseSwapManagerWaitForCompletion;
658
666
  declare class ArkadeSwapsMessageHandler implements MessageHandler<ArkadeSwapsUpdaterRequest, ArkadeSwapsUpdaterResponse> {
659
667
  private readonly swapRepository;
660
668
  static messageTag: string;
@@ -709,7 +717,10 @@ declare class ServiceWorkerArkadeSwaps implements IArkadeSwaps {
709
717
  stopSwapManager(): Promise<void>;
710
718
  getSwapManager(): SwapManagerClient | null;
711
719
  createLightningInvoice(args: CreateLightningInvoiceRequest): Promise<CreateLightningInvoiceResponse>;
712
- sendLightningPayment(args: SendLightningPaymentRequest): Promise<SendLightningPaymentResponse>;
720
+ sendLightningPayment(args: SendLightningPaymentRequest & {
721
+ waitFor?: "settled";
722
+ }): Promise<SendLightningPaymentResponse>;
723
+ sendLightningPayment(args: SendLightningPaymentRequest): Promise<OptimisticSendLightningPaymentResponse>;
713
724
  createSubmarineSwap(args: SendLightningPaymentRequest): Promise<BoltzSubmarineSwap>;
714
725
  createReverseSwap(args: CreateLightningInvoiceRequest): Promise<BoltzReverseSwap>;
715
726
  claimVHTLC(pendingSwap: BoltzReverseSwap): Promise<void>;
@@ -724,6 +735,7 @@ declare class ServiceWorkerArkadeSwaps implements IArkadeSwaps {
724
735
  waitForSwapSettlement(pendingSwap: BoltzSubmarineSwap): Promise<{
725
736
  preimage: string;
726
737
  }>;
738
+ waitForSwapFunded(pendingSwap: BoltzSubmarineSwap): Promise<void>;
727
739
  restoreSwaps(boltzFees?: FeesResponse): Promise<{
728
740
  chainSwaps: BoltzChainSwap[];
729
741
  reverseSwaps: BoltzReverseSwap[];
@@ -879,4 +891,4 @@ declare class InMemorySwapRepository implements SwapRepository {
879
891
  [Symbol.asyncDispose](): Promise<void>;
880
892
  }
881
893
 
882
- export { ArkToBtcResponse, ArkadeSwapsMessageHandler as ArkadeLightningMessageHandler, ArkadeSwapsConfig, ArkadeSwapsMessageHandler, BoltzChainSwap, BoltzRefundError, BoltzReverseSwap, BoltzSubmarineSwap, BoltzSwap, BtcToArkResponse, Chain, ChainFeesResponse, CreateLightningInvoiceRequest, CreateLightningInvoiceResponse, DecodedInvoice, FeesResponse, InMemorySwapRepository, IndexedDbSwapRepository, InsufficientFundsError, InvoiceExpiredError, InvoiceFailedToPayError, LimitsResponse, type Logger, Network, NetworkError, PreimageFetchError, QuoteRejectedError, type QuoteRejectionReason, QuoteSwapOptions, SchemaError, SendLightningPaymentRequest, SendLightningPaymentResponse, ServiceWorkerArkadeSwaps as ServiceWorkerArkadeLightning, ServiceWorkerArkadeSwaps, SubmarineRecoveryInfo, SubmarineRecoveryResult, SubmarineRefundOutcome, SwapError, SwapExpiredError, SwapManagerClient, SwapNotFoundError, SwapRepository, type SwapSaver, TransactionFailedError, decodeInvoice, enrichReverseSwapPreimage, enrichSubmarineSwapInvoice, getInvoicePaymentHash, getInvoiceSatoshis, isValidArkAddress, logger, migrateToSwapRepository, saveSwap, setLogger, updateChainSwapStatus, updateReverseSwapStatus, updateSubmarineSwapStatus, verifySignatures };
894
+ export { ArkToBtcResponse, ArkadeSwapsMessageHandler as ArkadeLightningMessageHandler, ArkadeSwapsConfig, ArkadeSwapsMessageHandler, BoltzChainSwap, BoltzRefundError, BoltzReverseSwap, BoltzSubmarineSwap, BoltzSwap, BtcToArkResponse, Chain, ChainFeesResponse, CreateLightningInvoiceRequest, CreateLightningInvoiceResponse, DecodedInvoice, FeesResponse, InMemorySwapRepository, IndexedDbSwapRepository, InsufficientFundsError, InvoiceExpiredError, InvoiceFailedToPayError, LimitsResponse, type Logger, Network, NetworkError, OptimisticSendLightningPaymentResponse, PreimageFetchError, QuoteRejectedError, type QuoteRejectionReason, QuoteSwapOptions, SchemaError, SendLightningPaymentRequest, SendLightningPaymentResponse, ServiceWorkerArkadeSwaps as ServiceWorkerArkadeLightning, ServiceWorkerArkadeSwaps, SubmarineRecoveryInfo, SubmarineRecoveryResult, SubmarineRefundOutcome, SwapError, SwapExpiredError, SwapManagerClient, SwapNotFoundError, SwapRepository, type SwapSaver, TransactionFailedError, decodeInvoice, enrichReverseSwapPreimage, enrichSubmarineSwapInvoice, getInvoicePaymentHash, getInvoiceSatoshis, isValidArkAddress, logger, migrateToSwapRepository, saveSwap, setLogger, updateChainSwapStatus, updateReverseSwapStatus, updateSubmarineSwapStatus, verifySignatures };