@arkade-os/boltz-swap 0.3.14 → 0.3.16

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.
@@ -4,10 +4,10 @@ import {
4
4
  registerExpoSwapBackgroundTask,
5
5
  swapsPollProcessor,
6
6
  unregisterExpoSwapBackgroundTask
7
- } from "../chunk-2TWYSAFO.js";
7
+ } from "../chunk-CNH5DDKV.js";
8
8
  import {
9
9
  ArkadeSwaps
10
- } from "../chunk-AIVWXKNG.js";
10
+ } from "../chunk-RL4PDED5.js";
11
11
  import "../chunk-3RG5ZIWI.js";
12
12
 
13
13
  // src/expo/arkade-lightning.ts
@@ -56,7 +56,7 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
56
56
  await instance.seedSwapPollTask();
57
57
  if (config.background.minimumBackgroundInterval) {
58
58
  try {
59
- const { registerExpoSwapBackgroundTask: registerExpoSwapBackgroundTask2 } = await import("../background-5XMCGVMS.js");
59
+ const { registerExpoSwapBackgroundTask: registerExpoSwapBackgroundTask2 } = await import("../background-DX6SZGGJ.js");
60
60
  await registerExpoSwapBackgroundTask2(
61
61
  config.background.taskName,
62
62
  {
@@ -129,7 +129,7 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
129
129
  }
130
130
  await this.inner.dispose();
131
131
  try {
132
- const { unregisterExpoSwapBackgroundTask: unregisterExpoSwapBackgroundTask2 } = await import("../background-5XMCGVMS.js");
132
+ const { unregisterExpoSwapBackgroundTask: unregisterExpoSwapBackgroundTask2 } = await import("../background-DX6SZGGJ.js");
133
133
  await unregisterExpoSwapBackgroundTask2(this.taskName);
134
134
  } catch (err) {
135
135
  const message = err instanceof Error ? err.message : String(err);
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  ArkadeLightningMessageHandler: () => ArkadeSwapsMessageHandler,
34
34
  ArkadeSwaps: () => ArkadeSwaps,
35
35
  ArkadeSwapsMessageHandler: () => ArkadeSwapsMessageHandler,
36
+ BoltzRefundError: () => BoltzRefundError,
36
37
  BoltzSwapProvider: () => BoltzSwapProvider,
37
38
  IndexedDbSwapRepository: () => IndexedDbSwapRepository,
38
39
  InsufficientFundsError: () => InsufficientFundsError,
@@ -176,6 +177,13 @@ var TransactionRefundedError = class extends SwapError {
176
177
  this.name = "TransactionRefundedError";
177
178
  }
178
179
  };
180
+ var BoltzRefundError = class extends Error {
181
+ constructor(message, cause) {
182
+ super(message);
183
+ this.cause = cause;
184
+ this.name = "BoltzRefundError";
185
+ }
186
+ };
179
187
 
180
188
  // src/arkade-swaps.ts
181
189
  var import_sdk8 = require("@arkade-os/sdk");
@@ -314,7 +322,7 @@ var isGetReverseSwapTxIdResponse = (data) => {
314
322
  return data && typeof data === "object" && typeof data.id === "string" && typeof data.timeoutBlockHeight === "number";
315
323
  };
316
324
  var isGetSwapStatusResponse = (data) => {
317
- return data && typeof data === "object" && typeof data.status === "string" && (data.zeroConfRejected === void 0 || typeof data.zeroConfRejected === "boolean") && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && (data.transaction.eta === void 0 || typeof data.transaction.eta === "number") && (data.transaction.hex === void 0 || typeof data.transaction.hex === "string") && (data.transaction.preimage === void 0 || typeof data.transaction.preimage === "string"));
325
+ return data && typeof data === "object" && typeof data.status === "string" && (data.zeroConfRejected === void 0 || typeof data.zeroConfRejected === "boolean") && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && (data.transaction.confirmed === void 0 || typeof data.transaction.confirmed === "boolean") && (data.transaction.eta === void 0 || typeof data.transaction.eta === "number") && (data.transaction.hex === void 0 || typeof data.transaction.hex === "string") && (data.transaction.preimage === void 0 || typeof data.transaction.preimage === "string"));
318
326
  };
319
327
  var isGetSubmarinePairsResponse = (data) => {
320
328
  return data && typeof data === "object" && data.ARK && typeof data.ARK === "object" && data.ARK.BTC && typeof data.ARK.BTC === "object" && typeof data.ARK.BTC.hash === "string" && typeof data.ARK.BTC.rate === "number" && data.ARK.BTC.limits && typeof data.ARK.BTC.limits === "object" && typeof data.ARK.BTC.limits.maximal === "number" && typeof data.ARK.BTC.limits.minimal === "number" && typeof data.ARK.BTC.limits.maximalZeroConf === "number" && data.ARK.BTC.fees && typeof data.ARK.BTC.fees === "object" && typeof data.ARK.BTC.fees.percentage === "number" && typeof data.ARK.BTC.fees.minerFees === "number";
@@ -323,13 +331,13 @@ var isGetReversePairsResponse = (data) => {
323
331
  return data && typeof data === "object" && data.BTC && typeof data.BTC === "object" && data.BTC.ARK && typeof data.BTC.ARK === "object" && data.BTC.ARK.hash && typeof data.BTC.ARK.hash === "string" && typeof data.BTC.ARK.rate === "number" && data.BTC.ARK.limits && typeof data.BTC.ARK.limits === "object" && typeof data.BTC.ARK.limits.maximal === "number" && typeof data.BTC.ARK.limits.minimal === "number" && data.BTC.ARK.fees && typeof data.BTC.ARK.fees === "object" && typeof data.BTC.ARK.fees.percentage === "number" && typeof data.BTC.ARK.fees.minerFees === "object" && typeof data.BTC.ARK.fees.minerFees.claim === "number" && typeof data.BTC.ARK.fees.minerFees.lockup === "number";
324
332
  };
325
333
  var isCreateSubmarineSwapResponse = (data) => {
326
- return data && typeof data === "object" && typeof data.id === "string" && typeof data.address === "string" && typeof data.expectedAmount === "number" && typeof data.claimPublicKey === "string" && typeof data.acceptZeroConf === "boolean" && isTimeoutBlockHeights(data.timeoutBlockHeights);
334
+ return data && typeof data === "object" && typeof data.id === "string" && typeof data.expectedAmount === "number" && (data.address === void 0 || typeof data.address === "string") && (data.claimPublicKey === void 0 || typeof data.claimPublicKey === "string") && (data.acceptZeroConf === void 0 || typeof data.acceptZeroConf === "boolean") && (data.timeoutBlockHeight === void 0 || typeof data.timeoutBlockHeight === "number") && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights));
327
335
  };
328
336
  var isGetSwapPreimageResponse = (data) => {
329
337
  return data && typeof data === "object" && typeof data.preimage === "string";
330
338
  };
331
339
  var isCreateReverseSwapResponse = (data) => {
332
- return data && typeof data === "object" && typeof data.id === "string" && typeof data.invoice === "string" && typeof data.onchainAmount === "number" && typeof data.lockupAddress === "string" && typeof data.refundPublicKey === "string" && isTimeoutBlockHeights(data.timeoutBlockHeights);
340
+ return data && typeof data === "object" && typeof data.id === "string" && typeof data.invoice === "string" && (data.onchainAmount === void 0 || typeof data.onchainAmount === "number") && (data.lockupAddress === void 0 || typeof data.lockupAddress === "string") && (data.refundPublicKey === void 0 || typeof data.refundPublicKey === "string") && (data.timeoutBlockHeight === void 0 || typeof data.timeoutBlockHeight === "number") && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights));
333
341
  };
334
342
  var isRefundSubmarineSwapResponse = (data) => {
335
343
  return data && typeof data === "object" && typeof data.transaction === "string" && typeof data.checkpoint === "string";
@@ -368,19 +376,19 @@ var isLeaf = (data) => {
368
376
  return data && typeof data === "object" && typeof data.version === "number" && typeof data.output === "string";
369
377
  };
370
378
  var isTree = (data) => {
371
- return data && typeof data === "object" && isLeaf(data.claimLeaf) && isLeaf(data.refundLeaf) && isLeaf(data.refundWithoutBoltzLeaf) && isLeaf(data.unilateralClaimLeaf) && isLeaf(data.unilateralRefundLeaf) && isLeaf(data.unilateralRefundWithoutBoltzLeaf);
379
+ return data && typeof data === "object" && isLeaf(data.claimLeaf) && isLeaf(data.refundLeaf) && (data.covenantClaimLeaf === void 0 || isLeaf(data.covenantClaimLeaf)) && (data.refundWithoutBoltzLeaf === void 0 || isLeaf(data.refundWithoutBoltzLeaf)) && (data.unilateralClaimLeaf === void 0 || isLeaf(data.unilateralClaimLeaf)) && (data.unilateralRefundLeaf === void 0 || isLeaf(data.unilateralRefundLeaf)) && (data.unilateralRefundWithoutBoltzLeaf === void 0 || isLeaf(data.unilateralRefundWithoutBoltzLeaf));
372
380
  };
373
381
  var isDetails = (data) => {
374
- return data && typeof data === "object" && isTree(data.tree) && (data.amount === void 0 || typeof data.amount === "number") && typeof data.keyIndex === "number" && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && typeof data.transaction.vout === "number") && typeof data.lockupAddress === "string" && typeof data.serverPublicKey === "string" && typeof data.timeoutBlockHeight === "number" && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights)) && (data.preimageHash === void 0 || typeof data.preimageHash === "string");
382
+ return data && typeof data === "object" && isTree(data.tree) && (data.amount === void 0 || typeof data.amount === "number") && typeof data.keyIndex === "number" && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && typeof data.transaction.vout === "number") && typeof data.lockupAddress === "string" && typeof data.serverPublicKey === "string" && (data.timeoutBlockHeight === void 0 || typeof data.timeoutBlockHeight === "number") && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights)) && (data.preimageHash === void 0 || typeof data.preimageHash === "string");
375
383
  };
376
384
  var isRestoredChainSwap = (data) => {
377
- return data && typeof data === "object" && typeof data.id === "string" && data.type === "chain" && typeof data.status === "string" && typeof data.createdAt === "number" && (data.from === "ARK" || data.from === "BTC") && (data.to === "ARK" || data.to === "BTC") && typeof data.preimageHash === "string" && data.refundDetails && typeof data.refundDetails === "object" && isTree(data.refundDetails.tree) && typeof data.refundDetails.amount === "number" && typeof data.refundDetails.keyIndex === "number" && (data.refundDetails.transaction === void 0 || data.refundDetails.transaction && typeof data.refundDetails.transaction === "object" && typeof data.refundDetails.transaction.id === "string" && typeof data.refundDetails.transaction.vout === "number") && typeof data.refundDetails.lockupAddress === "string" && typeof data.refundDetails.serverPublicKey === "string" && typeof data.refundDetails.timeoutBlockHeight === "number";
385
+ return data && typeof data === "object" && typeof data.id === "string" && data.type === "chain" && typeof data.status === "string" && typeof data.createdAt === "number" && (data.from === "ARK" || data.from === "BTC") && (data.to === "ARK" || data.to === "BTC") && (data.preimageHash === void 0 || typeof data.preimageHash === "string") && (data.invoice === void 0 || typeof data.invoice === "string") && (data.refundDetails === void 0 || isDetails(data.refundDetails)) && (data.claimDetails === void 0 || isDetails(data.claimDetails));
378
386
  };
379
387
  var isRestoredSubmarineSwap = (data) => {
380
- return data && typeof data === "object" && data.to === "BTC" && typeof data.id === "string" && data.from === "ARK" && data.type === "submarine" && typeof data.createdAt === "number" && typeof data.preimageHash === "string" && typeof data.status === "string" && isDetails(data.refundDetails) && (data.invoice === void 0 || typeof data.invoice === "string");
388
+ return data && typeof data === "object" && data.to === "BTC" && typeof data.id === "string" && data.from === "ARK" && data.type === "submarine" && typeof data.createdAt === "number" && (data.preimageHash === void 0 || typeof data.preimageHash === "string") && typeof data.status === "string" && isDetails(data.refundDetails) && (data.invoice === void 0 || typeof data.invoice === "string");
381
389
  };
382
390
  var isRestoredReverseSwap = (data) => {
383
- return data && typeof data === "object" && data.to === "ARK" && typeof data.id === "string" && data.from === "BTC" && data.type === "reverse" && typeof data.createdAt === "number" && typeof data.preimageHash === "string" && typeof data.status === "string" && isDetails(data.claimDetails) && (data.invoice === void 0 || typeof data.invoice === "string");
391
+ return data && typeof data === "object" && data.to === "ARK" && typeof data.id === "string" && data.from === "BTC" && data.type === "reverse" && typeof data.createdAt === "number" && (data.preimageHash === void 0 || typeof data.preimageHash === "string") && typeof data.status === "string" && isDetails(data.claimDetails) && (data.invoice === void 0 || typeof data.invoice === "string");
384
392
  };
385
393
  var isCreateSwapsRestoreResponse = (data) => {
386
394
  return Array.isArray(data) && data.every(
@@ -459,6 +467,18 @@ var BoltzSwapProvider = class {
459
467
  max: response.ARK.BTC.limits.maximal
460
468
  };
461
469
  }
470
+ /** Returns the current BTC chain tip height from Boltz. */
471
+ async getChainHeight() {
472
+ const response = await this.request(
473
+ "/v2/chain/heights",
474
+ "GET"
475
+ );
476
+ if (typeof response?.BTC !== "number")
477
+ throw new SchemaError({
478
+ message: "error fetching chain heights"
479
+ });
480
+ return response.BTC;
481
+ }
462
482
  /** Gets the lockup transaction ID for a reverse swap. */
463
483
  async getReverseSwapTxId(id) {
464
484
  const res = await this.request(
@@ -2928,10 +2948,22 @@ var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnly
2928
2948
  `Expected one checkpoint transaction, got ${checkpointPtxs.length}`
2929
2949
  );
2930
2950
  const unsignedCheckpointTx = checkpointPtxs[0];
2931
- const {
2932
- transaction: boltzSignedRefundTx,
2933
- checkpoint: boltzSignedCheckpointTx
2934
- } = await refundFunc(swapId, unsignedRefundTx, unsignedCheckpointTx);
2951
+ let boltzSignedRefundTx;
2952
+ let boltzSignedCheckpointTx;
2953
+ try {
2954
+ const result = await refundFunc(
2955
+ swapId,
2956
+ unsignedRefundTx,
2957
+ unsignedCheckpointTx
2958
+ );
2959
+ boltzSignedRefundTx = result.transaction;
2960
+ boltzSignedCheckpointTx = result.checkpoint;
2961
+ } catch (error) {
2962
+ throw new BoltzRefundError(
2963
+ `Boltz rejected refund for swap ${swapId}`,
2964
+ error
2965
+ );
2966
+ }
2935
2967
  const boltzXOnlyPublicKeyHex = import_base8.hex.encode(boltzXOnlyPublicKey);
2936
2968
  const refundLeafHash = (0, import_payment3.tapLeafHash)(
2937
2969
  scriptFromTapLeafScript(input.tapLeafScript)
@@ -3272,7 +3304,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
3272
3304
  */
3273
3305
  async claimVHTLC(pendingSwap) {
3274
3306
  if (!pendingSwap.preimage)
3275
- throw new Error("Preimage is required to claim VHTLC");
3307
+ throw new Error(
3308
+ `Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
3309
+ );
3310
+ const { refundPublicKey, lockupAddress, timeoutBlockHeights } = pendingSwap.response;
3311
+ if (!refundPublicKey || !lockupAddress || !timeoutBlockHeights)
3312
+ throw new Error(
3313
+ `Swap ${pendingSwap.id}: incomplete reverse swap response`
3314
+ );
3276
3315
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
3277
3316
  const arkInfo = await this.arkProvider.getInfo();
3278
3317
  const address = await this.wallet.getAddress();
@@ -3282,7 +3321,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3282
3321
  pendingSwap.id
3283
3322
  );
3284
3323
  const senderXOnly = normalizeToXOnlyKey(
3285
- import_base9.hex.decode(pendingSwap.response.refundPublicKey),
3324
+ import_base9.hex.decode(refundPublicKey),
3286
3325
  "boltz",
3287
3326
  pendingSwap.id
3288
3327
  );
@@ -3297,20 +3336,26 @@ var ArkadeSwaps = class _ArkadeSwaps {
3297
3336
  receiverPubkey: import_base9.hex.encode(receiverXOnly),
3298
3337
  senderPubkey: import_base9.hex.encode(senderXOnly),
3299
3338
  serverPubkey: import_base9.hex.encode(serverXOnly),
3300
- timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
3339
+ timeoutBlockHeights
3301
3340
  });
3302
3341
  if (!vhtlcScript.claimScript)
3303
- throw new Error("Failed to create VHTLC script for reverse swap");
3304
- if (vhtlcAddress !== pendingSwap.response.lockupAddress)
3305
- throw new Error("Boltz is trying to scam us");
3342
+ throw new Error(
3343
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for reverse swap`
3344
+ );
3345
+ if (vhtlcAddress !== lockupAddress)
3346
+ throw new Error(
3347
+ `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3348
+ );
3306
3349
  const { vtxos } = await this.indexerProvider.getVtxos({
3307
3350
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3308
3351
  });
3309
3352
  if (vtxos.length === 0)
3310
- throw new Error("No spendable virtual coins found");
3353
+ throw new Error(
3354
+ `Swap ${pendingSwap.id}: no spendable virtual coins found`
3355
+ );
3311
3356
  const vtxo = vtxos[0];
3312
3357
  if (vtxo.isSpent) {
3313
- throw new Error("VHTLC is already spent");
3358
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3314
3359
  }
3315
3360
  const input = {
3316
3361
  ...vtxo,
@@ -3442,6 +3487,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
3442
3487
  */
3443
3488
  async sendLightningPayment(args) {
3444
3489
  const pendingSwap = await this.createSubmarineSwap(args);
3490
+ if (!pendingSwap.response.address)
3491
+ throw new Error(
3492
+ `Swap ${pendingSwap.id}: missing address in submarine swap response`
3493
+ );
3445
3494
  await this.savePendingSubmarineSwap(pendingSwap);
3446
3495
  const txid = await this.wallet.send({
3447
3496
  address: pendingSwap.response.address,
@@ -3511,7 +3560,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3511
3560
  async refundVHTLC(pendingSwap) {
3512
3561
  const preimageHash = pendingSwap.request.invoice ? getInvoicePaymentHash(pendingSwap.request.invoice) : pendingSwap.preimageHash;
3513
3562
  if (!preimageHash)
3514
- throw new Error("Preimage hash is required to refund VHTLC");
3563
+ throw new Error(
3564
+ `Swap ${pendingSwap.id}: preimage hash is required to refund VHTLC`
3565
+ );
3515
3566
  const arkInfo = await this.arkProvider.getInfo();
3516
3567
  const address = await this.wallet.getAddress();
3517
3568
  if (!address) throw new Error("Failed to get ark address from wallet");
@@ -3525,8 +3576,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
3525
3576
  "server",
3526
3577
  pendingSwap.id
3527
3578
  );
3579
+ const { claimPublicKey, timeoutBlockHeights } = pendingSwap.response;
3580
+ if (!claimPublicKey || !timeoutBlockHeights)
3581
+ throw new Error(
3582
+ `Swap ${pendingSwap.id}: incomplete submarine swap response`
3583
+ );
3528
3584
  const boltzXOnlyPublicKey = normalizeToXOnlyKey(
3529
- import_base9.hex.decode(pendingSwap.response.claimPublicKey),
3585
+ import_base9.hex.decode(claimPublicKey),
3530
3586
  "boltz",
3531
3587
  pendingSwap.id
3532
3588
  );
@@ -3536,10 +3592,12 @@ var ArkadeSwaps = class _ArkadeSwaps {
3536
3592
  receiverPubkey: import_base9.hex.encode(boltzXOnlyPublicKey),
3537
3593
  senderPubkey: import_base9.hex.encode(ourXOnlyPublicKey),
3538
3594
  serverPubkey: import_base9.hex.encode(serverXOnlyPublicKey),
3539
- timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
3595
+ timeoutBlockHeights
3540
3596
  });
3541
3597
  if (!vhtlcScript.claimScript)
3542
- throw new Error("Failed to create VHTLC script for submarine swap");
3598
+ throw new Error(
3599
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for submarine swap`
3600
+ );
3543
3601
  if (vhtlcAddress !== pendingSwap.response.address)
3544
3602
  throw new Error(
3545
3603
  `VHTLC address mismatch for swap ${pendingSwap.id}: expected ${pendingSwap.response.address}, got ${vhtlcAddress}`
@@ -3554,30 +3612,50 @@ var ArkadeSwaps = class _ArkadeSwaps {
3554
3612
  scripts: [vhtlcPkScriptHex]
3555
3613
  });
3556
3614
  throw new Error(
3557
- allVtxos.length > 0 ? "VHTLC is already spent" : `VHTLC not found for address ${pendingSwap.response.address}`
3615
+ allVtxos.length > 0 ? `Swap ${pendingSwap.id}: VHTLC is already spent` : `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.address}`
3558
3616
  );
3559
3617
  }
3560
3618
  const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3619
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
3620
+ const refundLocktime = BigInt(timeoutBlockHeights.refund);
3621
+ const currentBlockHeight = await this.swapProvider.getChainHeight();
3622
+ const cltvSatisfied = BigInt(currentBlockHeight) >= refundLocktime;
3561
3623
  let boltzCallCount = 0;
3624
+ let skippedCount = 0;
3562
3625
  for (const vtxo of spendableVtxos) {
3563
3626
  const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
3564
- const input = {
3565
- ...vtxo,
3566
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
3567
- tapTree: vhtlcScript.encode()
3568
- };
3569
3627
  const output = {
3570
3628
  amount: BigInt(vtxo.value),
3571
3629
  script: outputScript
3572
3630
  };
3573
- if (isRecoverableVtxo) {
3631
+ if (cltvSatisfied) {
3632
+ const input2 = {
3633
+ ...vtxo,
3634
+ tapLeafScript: refundWithoutReceiverLeaf,
3635
+ tapTree: vhtlcScript.encode()
3636
+ };
3574
3637
  await this.joinBatch(
3575
3638
  this.wallet.identity,
3576
- input,
3639
+ input2,
3577
3640
  output,
3578
- arkInfo
3641
+ arkInfo,
3642
+ isRecoverableVtxo
3643
+ );
3644
+ continue;
3645
+ }
3646
+ if (isRecoverableVtxo) {
3647
+ logger.error(
3648
+ `Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${timeoutBlockHeights.refund}, currentBlockHeight=${currentBlockHeight}). Refund will be retried after locktime.`
3579
3649
  );
3580
- } else {
3650
+ skippedCount++;
3651
+ continue;
3652
+ }
3653
+ const input = {
3654
+ ...vtxo,
3655
+ tapLeafScript: vhtlcScript.refund(),
3656
+ tapTree: vhtlcScript.encode()
3657
+ };
3658
+ try {
3581
3659
  if (boltzCallCount > 0) {
3582
3660
  await new Promise((r) => setTimeout(r, 2e3));
3583
3661
  }
@@ -3596,14 +3674,42 @@ var ArkadeSwaps = class _ArkadeSwaps {
3596
3674
  )
3597
3675
  );
3598
3676
  boltzCallCount++;
3677
+ } catch (error) {
3678
+ if (!(error instanceof BoltzRefundError)) {
3679
+ throw error;
3680
+ }
3681
+ const tipNow = await this.swapProvider.getChainHeight();
3682
+ if (BigInt(tipNow) < refundLocktime) {
3683
+ logger.error(
3684
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (currentBlockHeight=${tipNow}, locktime=${timeoutBlockHeights.refund}). Refund will be retried after locktime.`
3685
+ );
3686
+ skippedCount++;
3687
+ continue;
3688
+ }
3689
+ logger.warn(
3690
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
3691
+ );
3692
+ const fallbackInput = {
3693
+ ...vtxo,
3694
+ tapLeafScript: refundWithoutReceiverLeaf,
3695
+ tapTree: vhtlcScript.encode()
3696
+ };
3697
+ await this.joinBatch(
3698
+ this.wallet.identity,
3699
+ fallbackInput,
3700
+ output,
3701
+ arkInfo,
3702
+ false
3703
+ );
3599
3704
  }
3600
3705
  }
3706
+ const fullyRefunded = skippedCount === 0;
3601
3707
  await updateSubmarineSwapStatus(
3602
3708
  pendingSwap,
3603
3709
  pendingSwap.status,
3604
3710
  // Keep current status
3605
3711
  this.savePendingSubmarineSwap.bind(this),
3606
- { refundable: true, refunded: true }
3712
+ { refundable: true, refunded: fullyRefunded }
3607
3713
  );
3608
3714
  }
3609
3715
  /**
@@ -3806,14 +3912,22 @@ var ArkadeSwaps = class _ArkadeSwaps {
3806
3912
  */
3807
3913
  async claimBtc(pendingSwap) {
3808
3914
  if (!pendingSwap.toAddress)
3809
- throw new Error("Destination address is required");
3915
+ throw new Error(
3916
+ `Swap ${pendingSwap.id}: destination address is required`
3917
+ );
3810
3918
  if (!pendingSwap.response.claimDetails.swapTree)
3811
- throw new Error("Missing swap tree in claim details");
3919
+ throw new Error(
3920
+ `Swap ${pendingSwap.id}: missing swap tree in claim details`
3921
+ );
3812
3922
  if (!pendingSwap.response.claimDetails.serverPublicKey)
3813
- throw new Error("Missing server public key in claim details");
3923
+ throw new Error(
3924
+ `Swap ${pendingSwap.id}: missing server public key in claim details`
3925
+ );
3814
3926
  const swapStatus = await this.getSwapStatus(pendingSwap.id);
3815
3927
  if (!swapStatus.transaction?.hex)
3816
- throw new Error("BTC transaction hex is required");
3928
+ throw new Error(
3929
+ `Swap ${pendingSwap.id}: BTC transaction hex is required`
3930
+ );
3817
3931
  const lockupTx = import_btc_signer5.Transaction.fromRaw(
3818
3932
  import_base9.hex.decode(swapStatus.transaction.hex)
3819
3933
  );
@@ -3868,7 +3982,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3868
3982
  }
3869
3983
  );
3870
3984
  if (!signedTxData.pubNonce || !signedTxData.partialSignature)
3871
- throw new Error("Invalid signature data from server");
3985
+ throw new Error(
3986
+ `Swap ${pendingSwap.id}: invalid signature data from server`
3987
+ );
3872
3988
  const musigSession = musigMessage.aggregateNonces([
3873
3989
  [
3874
3990
  import_base9.hex.decode(
@@ -3893,9 +4009,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
3893
4009
  */
3894
4010
  async refundArk(pendingSwap) {
3895
4011
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
3896
- throw new Error("Missing server public key in lockup details");
4012
+ throw new Error(
4013
+ `Swap ${pendingSwap.id}: missing server public key in lockup details`
4014
+ );
3897
4015
  if (!pendingSwap.response.lockupDetails.timeouts)
3898
- throw new Error("Missing timeouts in lockup details");
4016
+ throw new Error(
4017
+ `Swap ${pendingSwap.id}: missing timeouts in lockup details`
4018
+ );
3899
4019
  const arkInfo = await this.arkProvider.getInfo();
3900
4020
  const address = await this.wallet.getAddress();
3901
4021
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
@@ -3921,12 +4041,12 @@ var ArkadeSwaps = class _ArkadeSwaps {
3921
4041
  });
3922
4042
  if (vtxos.length === 0) {
3923
4043
  throw new Error(
3924
- `VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4044
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
3925
4045
  );
3926
4046
  }
3927
4047
  const vtxo = vtxos[0];
3928
4048
  if (vtxo.isSpent) {
3929
- throw new Error("VHTLC is already spent");
4049
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3930
4050
  }
3931
4051
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
3932
4052
  network: arkInfo.network,
@@ -3937,7 +4057,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3937
4057
  timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
3938
4058
  });
3939
4059
  if (!vhtlcScript.refundScript)
3940
- throw new Error("Failed to create VHTLC script for chain swap");
4060
+ throw new Error(
4061
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4062
+ );
3941
4063
  if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
3942
4064
  throw new SwapError({
3943
4065
  message: "Unable to claim: invalid VHTLC address"
@@ -4111,11 +4233,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
4111
4233
  */
4112
4234
  async claimArk(pendingSwap) {
4113
4235
  if (!pendingSwap.toAddress)
4114
- throw new Error("Destination address is required");
4236
+ throw new Error(
4237
+ `Swap ${pendingSwap.id}: destination address is required`
4238
+ );
4115
4239
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4116
- throw new Error("Missing server public key in claim details");
4240
+ throw new Error(
4241
+ `Swap ${pendingSwap.id}: missing server public key in claim details`
4242
+ );
4117
4243
  if (!pendingSwap.response.claimDetails.timeouts)
4118
- throw new Error("Missing timeouts in claim details");
4244
+ throw new Error(
4245
+ `Swap ${pendingSwap.id}: missing timeouts in claim details`
4246
+ );
4119
4247
  const arkInfo = await this.arkProvider.getInfo();
4120
4248
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
4121
4249
  const address = await this.wallet.getAddress();
@@ -4140,7 +4268,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4140
4268
  timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
4141
4269
  });
4142
4270
  if (!vhtlcScript.claimScript)
4143
- throw new Error("Failed to create VHTLC script for chain swap");
4271
+ throw new Error(
4272
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4273
+ );
4144
4274
  if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
4145
4275
  throw new SwapError({
4146
4276
  message: "Unable to claim: invalid VHTLC address"
@@ -4151,7 +4281,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4151
4281
  spendableOnly: true
4152
4282
  });
4153
4283
  if (spendableVtxos.vtxos.length === 0)
4154
- throw new Error("No spendable virtual coins found");
4284
+ throw new Error(
4285
+ `Swap ${pendingSwap.id}: no spendable virtual coins found`
4286
+ );
4155
4287
  const vtxo = spendableVtxos.vtxos[0];
4156
4288
  const input = {
4157
4289
  ...vtxo,
@@ -4191,16 +4323,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
4191
4323
  */
4192
4324
  async signCooperativeClaimForServer(pendingSwap) {
4193
4325
  if (!pendingSwap.response.lockupDetails.swapTree)
4194
- throw new Error("Missing swap tree in lockup details");
4326
+ throw new Error(
4327
+ `Swap ${pendingSwap.id}: missing swap tree in lockup details`
4328
+ );
4195
4329
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4196
- throw new Error("Missing server public key in lockup details");
4330
+ throw new Error(
4331
+ `Swap ${pendingSwap.id}: missing server public key in lockup details`
4332
+ );
4197
4333
  const claimDetails = await this.swapProvider.getChainClaimDetails(
4198
4334
  pendingSwap.id
4199
4335
  );
4200
4336
  const serverPubKey = pendingSwap.response.lockupDetails.serverPublicKey;
4201
4337
  if (claimDetails.publicKey !== serverPubKey) {
4202
4338
  throw new Error(
4203
- `Server public key mismatch: claim response has ${claimDetails.publicKey}, expected ${serverPubKey}`
4339
+ `Swap ${pendingSwap.id}: server public key mismatch \u2014 claim response has ${claimDetails.publicKey}, expected ${serverPubKey}`
4204
4340
  );
4205
4341
  }
4206
4342
  const musig = tweakMusig(
@@ -4320,15 +4456,23 @@ var ArkadeSwaps = class _ArkadeSwaps {
4320
4456
  const { to, from, swap, arkInfo } = args;
4321
4457
  if (from === "ARK") {
4322
4458
  if (!swap.response.lockupDetails.serverPublicKey)
4323
- throw new Error("Missing serverPublicKey in lockup details");
4459
+ throw new Error(
4460
+ `Swap ${swap.id}: missing serverPublicKey in lockup details`
4461
+ );
4324
4462
  if (!swap.response.lockupDetails.timeouts)
4325
- throw new Error("Missing timeouts in lockup details");
4463
+ throw new Error(
4464
+ `Swap ${swap.id}: missing timeouts in lockup details`
4465
+ );
4326
4466
  }
4327
4467
  if (to === "ARK") {
4328
4468
  if (!swap.response.claimDetails.serverPublicKey)
4329
- throw new Error("Missing serverPublicKey in claim details");
4469
+ throw new Error(
4470
+ `Swap ${swap.id}: missing serverPublicKey in claim details`
4471
+ );
4330
4472
  if (!swap.response.claimDetails.timeouts)
4331
- throw new Error("Missing timeouts in claim details");
4473
+ throw new Error(
4474
+ `Swap ${swap.id}: missing timeouts in claim details`
4475
+ );
4332
4476
  }
4333
4477
  const lockupAddress = to === "ARK" ? swap.response.claimDetails.lockupAddress : swap.response.lockupDetails.lockupAddress;
4334
4478
  const receiverPubkey = to === "ARK" ? swap.request.claimPublicKey : swap.response.lockupDetails.serverPublicKey;
@@ -4538,7 +4682,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4538
4682
  lockupAddress,
4539
4683
  preimageHash,
4540
4684
  serverPublicKey,
4541
- tree
4685
+ tree,
4686
+ timeoutBlockHeights
4542
4687
  } = swap.claimDetails;
4543
4688
  reverseSwaps.push({
4544
4689
  id,
@@ -4554,18 +4699,18 @@ var ArkadeSwaps = class _ArkadeSwaps {
4554
4699
  onchainAmount: amount,
4555
4700
  lockupAddress,
4556
4701
  refundPublicKey: serverPublicKey,
4557
- timeoutBlockHeights: {
4702
+ timeoutBlockHeights: timeoutBlockHeights ?? {
4558
4703
  refund: extractTimeLockFromLeafOutput(
4559
- tree.refundWithoutBoltzLeaf.output
4704
+ tree.refundWithoutBoltzLeaf?.output ?? ""
4560
4705
  ),
4561
4706
  unilateralClaim: extractTimeLockFromLeafOutput(
4562
- tree.unilateralClaimLeaf.output
4707
+ tree.unilateralClaimLeaf?.output ?? ""
4563
4708
  ),
4564
4709
  unilateralRefund: extractTimeLockFromLeafOutput(
4565
- tree.unilateralRefundLeaf.output
4710
+ tree.unilateralRefundLeaf?.output ?? ""
4566
4711
  ),
4567
4712
  unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4568
- tree.unilateralRefundWithoutBoltzLeaf.output
4713
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4569
4714
  )
4570
4715
  }
4571
4716
  },
@@ -4574,7 +4719,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4574
4719
  preimage: ""
4575
4720
  });
4576
4721
  } else if (isRestoredSubmarineSwap(swap)) {
4577
- const { amount, lockupAddress, serverPublicKey, tree } = swap.refundDetails;
4722
+ const {
4723
+ amount,
4724
+ lockupAddress,
4725
+ serverPublicKey,
4726
+ tree,
4727
+ timeoutBlockHeights
4728
+ } = swap.refundDetails;
4578
4729
  let preimage = "";
4579
4730
  if (!isSubmarineFinalStatus(status)) {
4580
4731
  try {
@@ -4605,29 +4756,31 @@ var ArkadeSwaps = class _ArkadeSwaps {
4605
4756
  address: lockupAddress,
4606
4757
  expectedAmount: amount,
4607
4758
  claimPublicKey: serverPublicKey,
4608
- timeoutBlockHeights: {
4759
+ timeoutBlockHeights: timeoutBlockHeights ?? {
4609
4760
  refund: extractTimeLockFromLeafOutput(
4610
- tree.refundWithoutBoltzLeaf.output
4761
+ tree.refundWithoutBoltzLeaf?.output ?? ""
4611
4762
  ),
4612
4763
  unilateralClaim: extractTimeLockFromLeafOutput(
4613
- tree.unilateralClaimLeaf.output
4764
+ tree.unilateralClaimLeaf?.output ?? ""
4614
4765
  ),
4615
4766
  unilateralRefund: extractTimeLockFromLeafOutput(
4616
- tree.unilateralRefundLeaf.output
4767
+ tree.unilateralRefundLeaf?.output ?? ""
4617
4768
  ),
4618
4769
  unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4619
- tree.unilateralRefundWithoutBoltzLeaf.output
4770
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4620
4771
  )
4621
4772
  }
4622
4773
  }
4623
4774
  });
4624
4775
  } else if (isRestoredChainSwap(swap)) {
4776
+ const refundDetails = swap.refundDetails;
4777
+ if (!refundDetails) continue;
4625
4778
  const {
4626
4779
  amount,
4627
4780
  lockupAddress,
4628
4781
  serverPublicKey,
4629
4782
  timeoutBlockHeight
4630
- } = swap.refundDetails;
4783
+ } = refundDetails;
4631
4784
  chainSwaps.push({
4632
4785
  id,
4633
4786
  type: "chain",
@@ -5981,6 +6134,7 @@ async function getContractCollection(storage, contractType) {
5981
6134
  ArkadeLightningMessageHandler,
5982
6135
  ArkadeSwaps,
5983
6136
  ArkadeSwapsMessageHandler,
6137
+ BoltzRefundError,
5984
6138
  BoltzSwapProvider,
5985
6139
  IndexedDbSwapRepository,
5986
6140
  InsufficientFundsError,