@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.
@@ -1,5 +1,5 @@
1
1
  import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
2
- import { m as BoltzSwapProvider, V as SwapManager, j as SwapRepository, X as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, h as ArkToBtcResponse, a as BoltzChainSwap, i as BtcToArkResponse, d as Chain, F as FeesResponse, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-x542EUL6.cjs';
2
+ import { m as BoltzSwapProvider, V as SwapManager, j as SwapRepository, X as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, h as ArkToBtcResponse, a as BoltzChainSwap, i as BtcToArkResponse, d as Chain, F as FeesResponse, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-CKxFfdEH.cjs';
3
3
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
2
- import { m as BoltzSwapProvider, V as SwapManager, j as SwapRepository, X as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, h as ArkToBtcResponse, a as BoltzChainSwap, i as BtcToArkResponse, d as Chain, F as FeesResponse, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-x542EUL6.js';
2
+ import { m as BoltzSwapProvider, V as SwapManager, j as SwapRepository, X as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, h as ArkToBtcResponse, a as BoltzChainSwap, i as BtcToArkResponse, d as Chain, F as FeesResponse, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-CKxFfdEH.js';
3
3
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
4
4
 
5
5
  /**
@@ -2,8 +2,8 @@ import {
2
2
  defineExpoSwapBackgroundTask,
3
3
  registerExpoSwapBackgroundTask,
4
4
  unregisterExpoSwapBackgroundTask
5
- } from "./chunk-2TWYSAFO.js";
6
- import "./chunk-AIVWXKNG.js";
5
+ } from "./chunk-CNH5DDKV.js";
6
+ import "./chunk-RL4PDED5.js";
7
7
  import "./chunk-3RG5ZIWI.js";
8
8
  export {
9
9
  defineExpoSwapBackgroundTask,
@@ -8,7 +8,7 @@ import {
8
8
  isSubmarineFinalStatus,
9
9
  isSubmarineSwapRefundable,
10
10
  logger
11
- } from "./chunk-AIVWXKNG.js";
11
+ } from "./chunk-RL4PDED5.js";
12
12
  import {
13
13
  __require
14
14
  } from "./chunk-3RG5ZIWI.js";
@@ -86,6 +86,13 @@ var TransactionRefundedError = class extends SwapError {
86
86
  this.name = "TransactionRefundedError";
87
87
  }
88
88
  };
89
+ var BoltzRefundError = class extends Error {
90
+ constructor(message, cause) {
91
+ super(message);
92
+ this.cause = cause;
93
+ this.name = "BoltzRefundError";
94
+ }
95
+ };
89
96
 
90
97
  // src/boltz-swap-provider.ts
91
98
  import { Transaction } from "@arkade-os/sdk";
@@ -221,7 +228,7 @@ var isGetReverseSwapTxIdResponse = (data) => {
221
228
  return data && typeof data === "object" && typeof data.id === "string" && typeof data.timeoutBlockHeight === "number";
222
229
  };
223
230
  var isGetSwapStatusResponse = (data) => {
224
- 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"));
231
+ 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"));
225
232
  };
226
233
  var isGetSubmarinePairsResponse = (data) => {
227
234
  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";
@@ -230,13 +237,13 @@ var isGetReversePairsResponse = (data) => {
230
237
  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";
231
238
  };
232
239
  var isCreateSubmarineSwapResponse = (data) => {
233
- 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);
240
+ 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));
234
241
  };
235
242
  var isGetSwapPreimageResponse = (data) => {
236
243
  return data && typeof data === "object" && typeof data.preimage === "string";
237
244
  };
238
245
  var isCreateReverseSwapResponse = (data) => {
239
- 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);
246
+ 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));
240
247
  };
241
248
  var isRefundSubmarineSwapResponse = (data) => {
242
249
  return data && typeof data === "object" && typeof data.transaction === "string" && typeof data.checkpoint === "string";
@@ -275,19 +282,19 @@ var isLeaf = (data) => {
275
282
  return data && typeof data === "object" && typeof data.version === "number" && typeof data.output === "string";
276
283
  };
277
284
  var isTree = (data) => {
278
- return data && typeof data === "object" && isLeaf(data.claimLeaf) && isLeaf(data.refundLeaf) && isLeaf(data.refundWithoutBoltzLeaf) && isLeaf(data.unilateralClaimLeaf) && isLeaf(data.unilateralRefundLeaf) && isLeaf(data.unilateralRefundWithoutBoltzLeaf);
285
+ 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));
279
286
  };
280
287
  var isDetails = (data) => {
281
- 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");
288
+ 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");
282
289
  };
283
290
  var isRestoredChainSwap = (data) => {
284
- 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";
291
+ 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));
285
292
  };
286
293
  var isRestoredSubmarineSwap = (data) => {
287
- 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");
294
+ 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");
288
295
  };
289
296
  var isRestoredReverseSwap = (data) => {
290
- 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");
297
+ 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");
291
298
  };
292
299
  var isCreateSwapsRestoreResponse = (data) => {
293
300
  return Array.isArray(data) && data.every(
@@ -366,6 +373,18 @@ var BoltzSwapProvider = class {
366
373
  max: response.ARK.BTC.limits.maximal
367
374
  };
368
375
  }
376
+ /** Returns the current BTC chain tip height from Boltz. */
377
+ async getChainHeight() {
378
+ const response = await this.request(
379
+ "/v2/chain/heights",
380
+ "GET"
381
+ );
382
+ if (typeof response?.BTC !== "number")
383
+ throw new SchemaError({
384
+ message: "error fetching chain heights"
385
+ });
386
+ return response.BTC;
387
+ }
369
388
  /** Gets the lockup transaction ID for a reverse swap. */
370
389
  async getReverseSwapTxId(id) {
371
390
  const res = await this.request(
@@ -2869,10 +2888,22 @@ var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnly
2869
2888
  `Expected one checkpoint transaction, got ${checkpointPtxs.length}`
2870
2889
  );
2871
2890
  const unsignedCheckpointTx = checkpointPtxs[0];
2872
- const {
2873
- transaction: boltzSignedRefundTx,
2874
- checkpoint: boltzSignedCheckpointTx
2875
- } = await refundFunc(swapId, unsignedRefundTx, unsignedCheckpointTx);
2891
+ let boltzSignedRefundTx;
2892
+ let boltzSignedCheckpointTx;
2893
+ try {
2894
+ const result = await refundFunc(
2895
+ swapId,
2896
+ unsignedRefundTx,
2897
+ unsignedCheckpointTx
2898
+ );
2899
+ boltzSignedRefundTx = result.transaction;
2900
+ boltzSignedCheckpointTx = result.checkpoint;
2901
+ } catch (error) {
2902
+ throw new BoltzRefundError(
2903
+ `Boltz rejected refund for swap ${swapId}`,
2904
+ error
2905
+ );
2906
+ }
2876
2907
  const boltzXOnlyPublicKeyHex = hex7.encode(boltzXOnlyPublicKey);
2877
2908
  const refundLeafHash = tapLeafHash3(
2878
2909
  scriptFromTapLeafScript(input.tapLeafScript)
@@ -3213,7 +3244,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
3213
3244
  */
3214
3245
  async claimVHTLC(pendingSwap) {
3215
3246
  if (!pendingSwap.preimage)
3216
- throw new Error("Preimage is required to claim VHTLC");
3247
+ throw new Error(
3248
+ `Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
3249
+ );
3250
+ const { refundPublicKey, lockupAddress, timeoutBlockHeights } = pendingSwap.response;
3251
+ if (!refundPublicKey || !lockupAddress || !timeoutBlockHeights)
3252
+ throw new Error(
3253
+ `Swap ${pendingSwap.id}: incomplete reverse swap response`
3254
+ );
3217
3255
  const preimage = hex8.decode(pendingSwap.preimage);
3218
3256
  const arkInfo = await this.arkProvider.getInfo();
3219
3257
  const address = await this.wallet.getAddress();
@@ -3223,7 +3261,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3223
3261
  pendingSwap.id
3224
3262
  );
3225
3263
  const senderXOnly = normalizeToXOnlyKey(
3226
- hex8.decode(pendingSwap.response.refundPublicKey),
3264
+ hex8.decode(refundPublicKey),
3227
3265
  "boltz",
3228
3266
  pendingSwap.id
3229
3267
  );
@@ -3238,20 +3276,26 @@ var ArkadeSwaps = class _ArkadeSwaps {
3238
3276
  receiverPubkey: hex8.encode(receiverXOnly),
3239
3277
  senderPubkey: hex8.encode(senderXOnly),
3240
3278
  serverPubkey: hex8.encode(serverXOnly),
3241
- timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
3279
+ timeoutBlockHeights
3242
3280
  });
3243
3281
  if (!vhtlcScript.claimScript)
3244
- throw new Error("Failed to create VHTLC script for reverse swap");
3245
- if (vhtlcAddress !== pendingSwap.response.lockupAddress)
3246
- throw new Error("Boltz is trying to scam us");
3282
+ throw new Error(
3283
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for reverse swap`
3284
+ );
3285
+ if (vhtlcAddress !== lockupAddress)
3286
+ throw new Error(
3287
+ `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3288
+ );
3247
3289
  const { vtxos } = await this.indexerProvider.getVtxos({
3248
3290
  scripts: [hex8.encode(vhtlcScript.pkScript)]
3249
3291
  });
3250
3292
  if (vtxos.length === 0)
3251
- throw new Error("No spendable virtual coins found");
3293
+ throw new Error(
3294
+ `Swap ${pendingSwap.id}: no spendable virtual coins found`
3295
+ );
3252
3296
  const vtxo = vtxos[0];
3253
3297
  if (vtxo.isSpent) {
3254
- throw new Error("VHTLC is already spent");
3298
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3255
3299
  }
3256
3300
  const input = {
3257
3301
  ...vtxo,
@@ -3383,6 +3427,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
3383
3427
  */
3384
3428
  async sendLightningPayment(args) {
3385
3429
  const pendingSwap = await this.createSubmarineSwap(args);
3430
+ if (!pendingSwap.response.address)
3431
+ throw new Error(
3432
+ `Swap ${pendingSwap.id}: missing address in submarine swap response`
3433
+ );
3386
3434
  await this.savePendingSubmarineSwap(pendingSwap);
3387
3435
  const txid = await this.wallet.send({
3388
3436
  address: pendingSwap.response.address,
@@ -3452,7 +3500,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3452
3500
  async refundVHTLC(pendingSwap) {
3453
3501
  const preimageHash = pendingSwap.request.invoice ? getInvoicePaymentHash(pendingSwap.request.invoice) : pendingSwap.preimageHash;
3454
3502
  if (!preimageHash)
3455
- throw new Error("Preimage hash is required to refund VHTLC");
3503
+ throw new Error(
3504
+ `Swap ${pendingSwap.id}: preimage hash is required to refund VHTLC`
3505
+ );
3456
3506
  const arkInfo = await this.arkProvider.getInfo();
3457
3507
  const address = await this.wallet.getAddress();
3458
3508
  if (!address) throw new Error("Failed to get ark address from wallet");
@@ -3466,8 +3516,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
3466
3516
  "server",
3467
3517
  pendingSwap.id
3468
3518
  );
3519
+ const { claimPublicKey, timeoutBlockHeights } = pendingSwap.response;
3520
+ if (!claimPublicKey || !timeoutBlockHeights)
3521
+ throw new Error(
3522
+ `Swap ${pendingSwap.id}: incomplete submarine swap response`
3523
+ );
3469
3524
  const boltzXOnlyPublicKey = normalizeToXOnlyKey(
3470
- hex8.decode(pendingSwap.response.claimPublicKey),
3525
+ hex8.decode(claimPublicKey),
3471
3526
  "boltz",
3472
3527
  pendingSwap.id
3473
3528
  );
@@ -3477,10 +3532,12 @@ var ArkadeSwaps = class _ArkadeSwaps {
3477
3532
  receiverPubkey: hex8.encode(boltzXOnlyPublicKey),
3478
3533
  senderPubkey: hex8.encode(ourXOnlyPublicKey),
3479
3534
  serverPubkey: hex8.encode(serverXOnlyPublicKey),
3480
- timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
3535
+ timeoutBlockHeights
3481
3536
  });
3482
3537
  if (!vhtlcScript.claimScript)
3483
- throw new Error("Failed to create VHTLC script for submarine swap");
3538
+ throw new Error(
3539
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for submarine swap`
3540
+ );
3484
3541
  if (vhtlcAddress !== pendingSwap.response.address)
3485
3542
  throw new Error(
3486
3543
  `VHTLC address mismatch for swap ${pendingSwap.id}: expected ${pendingSwap.response.address}, got ${vhtlcAddress}`
@@ -3495,30 +3552,50 @@ var ArkadeSwaps = class _ArkadeSwaps {
3495
3552
  scripts: [vhtlcPkScriptHex]
3496
3553
  });
3497
3554
  throw new Error(
3498
- allVtxos.length > 0 ? "VHTLC is already spent" : `VHTLC not found for address ${pendingSwap.response.address}`
3555
+ allVtxos.length > 0 ? `Swap ${pendingSwap.id}: VHTLC is already spent` : `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.address}`
3499
3556
  );
3500
3557
  }
3501
3558
  const outputScript = ArkAddress2.decode(address).pkScript;
3559
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
3560
+ const refundLocktime = BigInt(timeoutBlockHeights.refund);
3561
+ const currentBlockHeight = await this.swapProvider.getChainHeight();
3562
+ const cltvSatisfied = BigInt(currentBlockHeight) >= refundLocktime;
3502
3563
  let boltzCallCount = 0;
3564
+ let skippedCount = 0;
3503
3565
  for (const vtxo of spendableVtxos) {
3504
3566
  const isRecoverableVtxo = isRecoverable(vtxo);
3505
- const input = {
3506
- ...vtxo,
3507
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
3508
- tapTree: vhtlcScript.encode()
3509
- };
3510
3567
  const output = {
3511
3568
  amount: BigInt(vtxo.value),
3512
3569
  script: outputScript
3513
3570
  };
3514
- if (isRecoverableVtxo) {
3571
+ if (cltvSatisfied) {
3572
+ const input2 = {
3573
+ ...vtxo,
3574
+ tapLeafScript: refundWithoutReceiverLeaf,
3575
+ tapTree: vhtlcScript.encode()
3576
+ };
3515
3577
  await this.joinBatch(
3516
3578
  this.wallet.identity,
3517
- input,
3579
+ input2,
3518
3580
  output,
3519
- arkInfo
3581
+ arkInfo,
3582
+ isRecoverableVtxo
3520
3583
  );
3521
- } else {
3584
+ continue;
3585
+ }
3586
+ if (isRecoverableVtxo) {
3587
+ logger.error(
3588
+ `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.`
3589
+ );
3590
+ skippedCount++;
3591
+ continue;
3592
+ }
3593
+ const input = {
3594
+ ...vtxo,
3595
+ tapLeafScript: vhtlcScript.refund(),
3596
+ tapTree: vhtlcScript.encode()
3597
+ };
3598
+ try {
3522
3599
  if (boltzCallCount > 0) {
3523
3600
  await new Promise((r) => setTimeout(r, 2e3));
3524
3601
  }
@@ -3537,14 +3614,42 @@ var ArkadeSwaps = class _ArkadeSwaps {
3537
3614
  )
3538
3615
  );
3539
3616
  boltzCallCount++;
3617
+ } catch (error) {
3618
+ if (!(error instanceof BoltzRefundError)) {
3619
+ throw error;
3620
+ }
3621
+ const tipNow = await this.swapProvider.getChainHeight();
3622
+ if (BigInt(tipNow) < refundLocktime) {
3623
+ logger.error(
3624
+ `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.`
3625
+ );
3626
+ skippedCount++;
3627
+ continue;
3628
+ }
3629
+ logger.warn(
3630
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
3631
+ );
3632
+ const fallbackInput = {
3633
+ ...vtxo,
3634
+ tapLeafScript: refundWithoutReceiverLeaf,
3635
+ tapTree: vhtlcScript.encode()
3636
+ };
3637
+ await this.joinBatch(
3638
+ this.wallet.identity,
3639
+ fallbackInput,
3640
+ output,
3641
+ arkInfo,
3642
+ false
3643
+ );
3540
3644
  }
3541
3645
  }
3646
+ const fullyRefunded = skippedCount === 0;
3542
3647
  await updateSubmarineSwapStatus(
3543
3648
  pendingSwap,
3544
3649
  pendingSwap.status,
3545
3650
  // Keep current status
3546
3651
  this.savePendingSubmarineSwap.bind(this),
3547
- { refundable: true, refunded: true }
3652
+ { refundable: true, refunded: fullyRefunded }
3548
3653
  );
3549
3654
  }
3550
3655
  /**
@@ -3747,14 +3852,22 @@ var ArkadeSwaps = class _ArkadeSwaps {
3747
3852
  */
3748
3853
  async claimBtc(pendingSwap) {
3749
3854
  if (!pendingSwap.toAddress)
3750
- throw new Error("Destination address is required");
3855
+ throw new Error(
3856
+ `Swap ${pendingSwap.id}: destination address is required`
3857
+ );
3751
3858
  if (!pendingSwap.response.claimDetails.swapTree)
3752
- throw new Error("Missing swap tree in claim details");
3859
+ throw new Error(
3860
+ `Swap ${pendingSwap.id}: missing swap tree in claim details`
3861
+ );
3753
3862
  if (!pendingSwap.response.claimDetails.serverPublicKey)
3754
- throw new Error("Missing server public key in claim details");
3863
+ throw new Error(
3864
+ `Swap ${pendingSwap.id}: missing server public key in claim details`
3865
+ );
3755
3866
  const swapStatus = await this.getSwapStatus(pendingSwap.id);
3756
3867
  if (!swapStatus.transaction?.hex)
3757
- throw new Error("BTC transaction hex is required");
3868
+ throw new Error(
3869
+ `Swap ${pendingSwap.id}: BTC transaction hex is required`
3870
+ );
3758
3871
  const lockupTx = Transaction6.fromRaw(
3759
3872
  hex8.decode(swapStatus.transaction.hex)
3760
3873
  );
@@ -3809,7 +3922,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3809
3922
  }
3810
3923
  );
3811
3924
  if (!signedTxData.pubNonce || !signedTxData.partialSignature)
3812
- throw new Error("Invalid signature data from server");
3925
+ throw new Error(
3926
+ `Swap ${pendingSwap.id}: invalid signature data from server`
3927
+ );
3813
3928
  const musigSession = musigMessage.aggregateNonces([
3814
3929
  [
3815
3930
  hex8.decode(
@@ -3834,9 +3949,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
3834
3949
  */
3835
3950
  async refundArk(pendingSwap) {
3836
3951
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
3837
- throw new Error("Missing server public key in lockup details");
3952
+ throw new Error(
3953
+ `Swap ${pendingSwap.id}: missing server public key in lockup details`
3954
+ );
3838
3955
  if (!pendingSwap.response.lockupDetails.timeouts)
3839
- throw new Error("Missing timeouts in lockup details");
3956
+ throw new Error(
3957
+ `Swap ${pendingSwap.id}: missing timeouts in lockup details`
3958
+ );
3840
3959
  const arkInfo = await this.arkProvider.getInfo();
3841
3960
  const address = await this.wallet.getAddress();
3842
3961
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
@@ -3862,12 +3981,12 @@ var ArkadeSwaps = class _ArkadeSwaps {
3862
3981
  });
3863
3982
  if (vtxos.length === 0) {
3864
3983
  throw new Error(
3865
- `VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
3984
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
3866
3985
  );
3867
3986
  }
3868
3987
  const vtxo = vtxos[0];
3869
3988
  if (vtxo.isSpent) {
3870
- throw new Error("VHTLC is already spent");
3989
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3871
3990
  }
3872
3991
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
3873
3992
  network: arkInfo.network,
@@ -3878,7 +3997,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3878
3997
  timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
3879
3998
  });
3880
3999
  if (!vhtlcScript.refundScript)
3881
- throw new Error("Failed to create VHTLC script for chain swap");
4000
+ throw new Error(
4001
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4002
+ );
3882
4003
  if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
3883
4004
  throw new SwapError({
3884
4005
  message: "Unable to claim: invalid VHTLC address"
@@ -4052,11 +4173,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
4052
4173
  */
4053
4174
  async claimArk(pendingSwap) {
4054
4175
  if (!pendingSwap.toAddress)
4055
- throw new Error("Destination address is required");
4176
+ throw new Error(
4177
+ `Swap ${pendingSwap.id}: destination address is required`
4178
+ );
4056
4179
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4057
- throw new Error("Missing server public key in claim details");
4180
+ throw new Error(
4181
+ `Swap ${pendingSwap.id}: missing server public key in claim details`
4182
+ );
4058
4183
  if (!pendingSwap.response.claimDetails.timeouts)
4059
- throw new Error("Missing timeouts in claim details");
4184
+ throw new Error(
4185
+ `Swap ${pendingSwap.id}: missing timeouts in claim details`
4186
+ );
4060
4187
  const arkInfo = await this.arkProvider.getInfo();
4061
4188
  const preimage = hex8.decode(pendingSwap.preimage);
4062
4189
  const address = await this.wallet.getAddress();
@@ -4081,7 +4208,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4081
4208
  timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
4082
4209
  });
4083
4210
  if (!vhtlcScript.claimScript)
4084
- throw new Error("Failed to create VHTLC script for chain swap");
4211
+ throw new Error(
4212
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4213
+ );
4085
4214
  if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
4086
4215
  throw new SwapError({
4087
4216
  message: "Unable to claim: invalid VHTLC address"
@@ -4092,7 +4221,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4092
4221
  spendableOnly: true
4093
4222
  });
4094
4223
  if (spendableVtxos.vtxos.length === 0)
4095
- throw new Error("No spendable virtual coins found");
4224
+ throw new Error(
4225
+ `Swap ${pendingSwap.id}: no spendable virtual coins found`
4226
+ );
4096
4227
  const vtxo = spendableVtxos.vtxos[0];
4097
4228
  const input = {
4098
4229
  ...vtxo,
@@ -4132,16 +4263,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
4132
4263
  */
4133
4264
  async signCooperativeClaimForServer(pendingSwap) {
4134
4265
  if (!pendingSwap.response.lockupDetails.swapTree)
4135
- throw new Error("Missing swap tree in lockup details");
4266
+ throw new Error(
4267
+ `Swap ${pendingSwap.id}: missing swap tree in lockup details`
4268
+ );
4136
4269
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4137
- throw new Error("Missing server public key in lockup details");
4270
+ throw new Error(
4271
+ `Swap ${pendingSwap.id}: missing server public key in lockup details`
4272
+ );
4138
4273
  const claimDetails = await this.swapProvider.getChainClaimDetails(
4139
4274
  pendingSwap.id
4140
4275
  );
4141
4276
  const serverPubKey = pendingSwap.response.lockupDetails.serverPublicKey;
4142
4277
  if (claimDetails.publicKey !== serverPubKey) {
4143
4278
  throw new Error(
4144
- `Server public key mismatch: claim response has ${claimDetails.publicKey}, expected ${serverPubKey}`
4279
+ `Swap ${pendingSwap.id}: server public key mismatch \u2014 claim response has ${claimDetails.publicKey}, expected ${serverPubKey}`
4145
4280
  );
4146
4281
  }
4147
4282
  const musig = tweakMusig(
@@ -4261,15 +4396,23 @@ var ArkadeSwaps = class _ArkadeSwaps {
4261
4396
  const { to, from, swap, arkInfo } = args;
4262
4397
  if (from === "ARK") {
4263
4398
  if (!swap.response.lockupDetails.serverPublicKey)
4264
- throw new Error("Missing serverPublicKey in lockup details");
4399
+ throw new Error(
4400
+ `Swap ${swap.id}: missing serverPublicKey in lockup details`
4401
+ );
4265
4402
  if (!swap.response.lockupDetails.timeouts)
4266
- throw new Error("Missing timeouts in lockup details");
4403
+ throw new Error(
4404
+ `Swap ${swap.id}: missing timeouts in lockup details`
4405
+ );
4267
4406
  }
4268
4407
  if (to === "ARK") {
4269
4408
  if (!swap.response.claimDetails.serverPublicKey)
4270
- throw new Error("Missing serverPublicKey in claim details");
4409
+ throw new Error(
4410
+ `Swap ${swap.id}: missing serverPublicKey in claim details`
4411
+ );
4271
4412
  if (!swap.response.claimDetails.timeouts)
4272
- throw new Error("Missing timeouts in claim details");
4413
+ throw new Error(
4414
+ `Swap ${swap.id}: missing timeouts in claim details`
4415
+ );
4273
4416
  }
4274
4417
  const lockupAddress = to === "ARK" ? swap.response.claimDetails.lockupAddress : swap.response.lockupDetails.lockupAddress;
4275
4418
  const receiverPubkey = to === "ARK" ? swap.request.claimPublicKey : swap.response.lockupDetails.serverPublicKey;
@@ -4479,7 +4622,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4479
4622
  lockupAddress,
4480
4623
  preimageHash,
4481
4624
  serverPublicKey,
4482
- tree
4625
+ tree,
4626
+ timeoutBlockHeights
4483
4627
  } = swap.claimDetails;
4484
4628
  reverseSwaps.push({
4485
4629
  id,
@@ -4495,18 +4639,18 @@ var ArkadeSwaps = class _ArkadeSwaps {
4495
4639
  onchainAmount: amount,
4496
4640
  lockupAddress,
4497
4641
  refundPublicKey: serverPublicKey,
4498
- timeoutBlockHeights: {
4642
+ timeoutBlockHeights: timeoutBlockHeights ?? {
4499
4643
  refund: extractTimeLockFromLeafOutput(
4500
- tree.refundWithoutBoltzLeaf.output
4644
+ tree.refundWithoutBoltzLeaf?.output ?? ""
4501
4645
  ),
4502
4646
  unilateralClaim: extractTimeLockFromLeafOutput(
4503
- tree.unilateralClaimLeaf.output
4647
+ tree.unilateralClaimLeaf?.output ?? ""
4504
4648
  ),
4505
4649
  unilateralRefund: extractTimeLockFromLeafOutput(
4506
- tree.unilateralRefundLeaf.output
4650
+ tree.unilateralRefundLeaf?.output ?? ""
4507
4651
  ),
4508
4652
  unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4509
- tree.unilateralRefundWithoutBoltzLeaf.output
4653
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4510
4654
  )
4511
4655
  }
4512
4656
  },
@@ -4515,7 +4659,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4515
4659
  preimage: ""
4516
4660
  });
4517
4661
  } else if (isRestoredSubmarineSwap(swap)) {
4518
- const { amount, lockupAddress, serverPublicKey, tree } = swap.refundDetails;
4662
+ const {
4663
+ amount,
4664
+ lockupAddress,
4665
+ serverPublicKey,
4666
+ tree,
4667
+ timeoutBlockHeights
4668
+ } = swap.refundDetails;
4519
4669
  let preimage = "";
4520
4670
  if (!isSubmarineFinalStatus(status)) {
4521
4671
  try {
@@ -4546,29 +4696,31 @@ var ArkadeSwaps = class _ArkadeSwaps {
4546
4696
  address: lockupAddress,
4547
4697
  expectedAmount: amount,
4548
4698
  claimPublicKey: serverPublicKey,
4549
- timeoutBlockHeights: {
4699
+ timeoutBlockHeights: timeoutBlockHeights ?? {
4550
4700
  refund: extractTimeLockFromLeafOutput(
4551
- tree.refundWithoutBoltzLeaf.output
4701
+ tree.refundWithoutBoltzLeaf?.output ?? ""
4552
4702
  ),
4553
4703
  unilateralClaim: extractTimeLockFromLeafOutput(
4554
- tree.unilateralClaimLeaf.output
4704
+ tree.unilateralClaimLeaf?.output ?? ""
4555
4705
  ),
4556
4706
  unilateralRefund: extractTimeLockFromLeafOutput(
4557
- tree.unilateralRefundLeaf.output
4707
+ tree.unilateralRefundLeaf?.output ?? ""
4558
4708
  ),
4559
4709
  unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4560
- tree.unilateralRefundWithoutBoltzLeaf.output
4710
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4561
4711
  )
4562
4712
  }
4563
4713
  }
4564
4714
  });
4565
4715
  } else if (isRestoredChainSwap(swap)) {
4716
+ const refundDetails = swap.refundDetails;
4717
+ if (!refundDetails) continue;
4566
4718
  const {
4567
4719
  amount,
4568
4720
  lockupAddress,
4569
4721
  serverPublicKey,
4570
4722
  timeoutBlockHeight
4571
- } = swap.refundDetails;
4723
+ } = refundDetails;
4572
4724
  chainSwaps.push({
4573
4725
  id,
4574
4726
  type: "chain",
@@ -4626,6 +4778,7 @@ export {
4626
4778
  SwapExpiredError,
4627
4779
  TransactionFailedError,
4628
4780
  PreimageFetchError,
4781
+ BoltzRefundError,
4629
4782
  isSubmarineFailedStatus,
4630
4783
  isSubmarineFinalStatus,
4631
4784
  isSubmarinePendingStatus,