@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.
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/errors.ts
34
- var SwapError, InvoiceExpiredError, InvoiceFailedToPayError, NetworkError, SchemaError, SwapExpiredError, TransactionFailedError, TransactionLockupFailedError, TransactionRefundedError;
34
+ var SwapError, InvoiceExpiredError, InvoiceFailedToPayError, NetworkError, SchemaError, SwapExpiredError, TransactionFailedError, TransactionLockupFailedError, TransactionRefundedError, BoltzRefundError;
35
35
  var init_errors = __esm({
36
36
  "src/errors.ts"() {
37
37
  "use strict";
@@ -107,6 +107,13 @@ var init_errors = __esm({
107
107
  this.name = "TransactionRefundedError";
108
108
  }
109
109
  };
110
+ BoltzRefundError = class extends Error {
111
+ constructor(message, cause) {
112
+ super(message);
113
+ this.cause = cause;
114
+ this.name = "BoltzRefundError";
115
+ }
116
+ };
110
117
  }
111
118
  });
112
119
 
@@ -185,7 +192,7 @@ var init_boltz_swap_provider = __esm({
185
192
  return data && typeof data === "object" && typeof data.id === "string" && typeof data.timeoutBlockHeight === "number";
186
193
  };
187
194
  isGetSwapStatusResponse = (data) => {
188
- 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"));
195
+ 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"));
189
196
  };
190
197
  isGetSubmarinePairsResponse = (data) => {
191
198
  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";
@@ -194,13 +201,13 @@ var init_boltz_swap_provider = __esm({
194
201
  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";
195
202
  };
196
203
  isCreateSubmarineSwapResponse = (data) => {
197
- 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);
204
+ 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));
198
205
  };
199
206
  isGetSwapPreimageResponse = (data) => {
200
207
  return data && typeof data === "object" && typeof data.preimage === "string";
201
208
  };
202
209
  isCreateReverseSwapResponse = (data) => {
203
- 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);
210
+ 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));
204
211
  };
205
212
  isRefundSubmarineSwapResponse = (data) => {
206
213
  return data && typeof data === "object" && typeof data.transaction === "string" && typeof data.checkpoint === "string";
@@ -239,19 +246,19 @@ var init_boltz_swap_provider = __esm({
239
246
  return data && typeof data === "object" && typeof data.version === "number" && typeof data.output === "string";
240
247
  };
241
248
  isTree = (data) => {
242
- return data && typeof data === "object" && isLeaf(data.claimLeaf) && isLeaf(data.refundLeaf) && isLeaf(data.refundWithoutBoltzLeaf) && isLeaf(data.unilateralClaimLeaf) && isLeaf(data.unilateralRefundLeaf) && isLeaf(data.unilateralRefundWithoutBoltzLeaf);
249
+ 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));
243
250
  };
244
251
  isDetails = (data) => {
245
- 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");
252
+ 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");
246
253
  };
247
254
  isRestoredChainSwap = (data) => {
248
- 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";
255
+ 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));
249
256
  };
250
257
  isRestoredSubmarineSwap = (data) => {
251
- 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");
258
+ 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");
252
259
  };
253
260
  isRestoredReverseSwap = (data) => {
254
- 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");
261
+ 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");
255
262
  };
256
263
  isCreateSwapsRestoreResponse = (data) => {
257
264
  return Array.isArray(data) && data.every(
@@ -330,6 +337,18 @@ var init_boltz_swap_provider = __esm({
330
337
  max: response.ARK.BTC.limits.maximal
331
338
  };
332
339
  }
340
+ /** Returns the current BTC chain tip height from Boltz. */
341
+ async getChainHeight() {
342
+ const response = await this.request(
343
+ "/v2/chain/heights",
344
+ "GET"
345
+ );
346
+ if (typeof response?.BTC !== "number")
347
+ throw new SchemaError({
348
+ message: "error fetching chain heights"
349
+ });
350
+ return response.BTC;
351
+ }
333
352
  /** Gets the lockup transaction ID for a reverse swap. */
334
353
  async getReverseSwapTxId(id) {
335
354
  const res = await this.request(
@@ -2659,6 +2678,7 @@ var init_vhtlc = __esm({
2659
2678
  "use strict";
2660
2679
  import_sdk7 = require("@arkade-os/sdk");
2661
2680
  init_logger();
2681
+ init_errors();
2662
2682
  import_base8 = require("@scure/base");
2663
2683
  init_batch();
2664
2684
  import_legacy = require("@noble/hashes/legacy.js");
@@ -2851,10 +2871,22 @@ var init_vhtlc = __esm({
2851
2871
  `Expected one checkpoint transaction, got ${checkpointPtxs.length}`
2852
2872
  );
2853
2873
  const unsignedCheckpointTx = checkpointPtxs[0];
2854
- const {
2855
- transaction: boltzSignedRefundTx,
2856
- checkpoint: boltzSignedCheckpointTx
2857
- } = await refundFunc(swapId, unsignedRefundTx, unsignedCheckpointTx);
2874
+ let boltzSignedRefundTx;
2875
+ let boltzSignedCheckpointTx;
2876
+ try {
2877
+ const result = await refundFunc(
2878
+ swapId,
2879
+ unsignedRefundTx,
2880
+ unsignedCheckpointTx
2881
+ );
2882
+ boltzSignedRefundTx = result.transaction;
2883
+ boltzSignedCheckpointTx = result.checkpoint;
2884
+ } catch (error) {
2885
+ throw new BoltzRefundError(
2886
+ `Boltz rejected refund for swap ${swapId}`,
2887
+ error
2888
+ );
2889
+ }
2858
2890
  const boltzXOnlyPublicKeyHex = import_base8.hex.encode(boltzXOnlyPublicKey);
2859
2891
  const refundLeafHash = (0, import_payment3.tapLeafHash)(
2860
2892
  scriptFromTapLeafScript(input.tapLeafScript)
@@ -3218,7 +3250,14 @@ var init_arkade_swaps = __esm({
3218
3250
  */
3219
3251
  async claimVHTLC(pendingSwap) {
3220
3252
  if (!pendingSwap.preimage)
3221
- throw new Error("Preimage is required to claim VHTLC");
3253
+ throw new Error(
3254
+ `Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
3255
+ );
3256
+ const { refundPublicKey, lockupAddress, timeoutBlockHeights } = pendingSwap.response;
3257
+ if (!refundPublicKey || !lockupAddress || !timeoutBlockHeights)
3258
+ throw new Error(
3259
+ `Swap ${pendingSwap.id}: incomplete reverse swap response`
3260
+ );
3222
3261
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
3223
3262
  const arkInfo = await this.arkProvider.getInfo();
3224
3263
  const address = await this.wallet.getAddress();
@@ -3228,7 +3267,7 @@ var init_arkade_swaps = __esm({
3228
3267
  pendingSwap.id
3229
3268
  );
3230
3269
  const senderXOnly = normalizeToXOnlyKey(
3231
- import_base9.hex.decode(pendingSwap.response.refundPublicKey),
3270
+ import_base9.hex.decode(refundPublicKey),
3232
3271
  "boltz",
3233
3272
  pendingSwap.id
3234
3273
  );
@@ -3243,20 +3282,26 @@ var init_arkade_swaps = __esm({
3243
3282
  receiverPubkey: import_base9.hex.encode(receiverXOnly),
3244
3283
  senderPubkey: import_base9.hex.encode(senderXOnly),
3245
3284
  serverPubkey: import_base9.hex.encode(serverXOnly),
3246
- timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
3285
+ timeoutBlockHeights
3247
3286
  });
3248
3287
  if (!vhtlcScript.claimScript)
3249
- throw new Error("Failed to create VHTLC script for reverse swap");
3250
- if (vhtlcAddress !== pendingSwap.response.lockupAddress)
3251
- throw new Error("Boltz is trying to scam us");
3288
+ throw new Error(
3289
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for reverse swap`
3290
+ );
3291
+ if (vhtlcAddress !== lockupAddress)
3292
+ throw new Error(
3293
+ `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3294
+ );
3252
3295
  const { vtxos } = await this.indexerProvider.getVtxos({
3253
3296
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3254
3297
  });
3255
3298
  if (vtxos.length === 0)
3256
- throw new Error("No spendable virtual coins found");
3299
+ throw new Error(
3300
+ `Swap ${pendingSwap.id}: no spendable virtual coins found`
3301
+ );
3257
3302
  const vtxo = vtxos[0];
3258
3303
  if (vtxo.isSpent) {
3259
- throw new Error("VHTLC is already spent");
3304
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3260
3305
  }
3261
3306
  const input = {
3262
3307
  ...vtxo,
@@ -3388,6 +3433,10 @@ var init_arkade_swaps = __esm({
3388
3433
  */
3389
3434
  async sendLightningPayment(args) {
3390
3435
  const pendingSwap = await this.createSubmarineSwap(args);
3436
+ if (!pendingSwap.response.address)
3437
+ throw new Error(
3438
+ `Swap ${pendingSwap.id}: missing address in submarine swap response`
3439
+ );
3391
3440
  await this.savePendingSubmarineSwap(pendingSwap);
3392
3441
  const txid = await this.wallet.send({
3393
3442
  address: pendingSwap.response.address,
@@ -3457,7 +3506,9 @@ var init_arkade_swaps = __esm({
3457
3506
  async refundVHTLC(pendingSwap) {
3458
3507
  const preimageHash = pendingSwap.request.invoice ? getInvoicePaymentHash(pendingSwap.request.invoice) : pendingSwap.preimageHash;
3459
3508
  if (!preimageHash)
3460
- throw new Error("Preimage hash is required to refund VHTLC");
3509
+ throw new Error(
3510
+ `Swap ${pendingSwap.id}: preimage hash is required to refund VHTLC`
3511
+ );
3461
3512
  const arkInfo = await this.arkProvider.getInfo();
3462
3513
  const address = await this.wallet.getAddress();
3463
3514
  if (!address) throw new Error("Failed to get ark address from wallet");
@@ -3471,8 +3522,13 @@ var init_arkade_swaps = __esm({
3471
3522
  "server",
3472
3523
  pendingSwap.id
3473
3524
  );
3525
+ const { claimPublicKey, timeoutBlockHeights } = pendingSwap.response;
3526
+ if (!claimPublicKey || !timeoutBlockHeights)
3527
+ throw new Error(
3528
+ `Swap ${pendingSwap.id}: incomplete submarine swap response`
3529
+ );
3474
3530
  const boltzXOnlyPublicKey = normalizeToXOnlyKey(
3475
- import_base9.hex.decode(pendingSwap.response.claimPublicKey),
3531
+ import_base9.hex.decode(claimPublicKey),
3476
3532
  "boltz",
3477
3533
  pendingSwap.id
3478
3534
  );
@@ -3482,10 +3538,12 @@ var init_arkade_swaps = __esm({
3482
3538
  receiverPubkey: import_base9.hex.encode(boltzXOnlyPublicKey),
3483
3539
  senderPubkey: import_base9.hex.encode(ourXOnlyPublicKey),
3484
3540
  serverPubkey: import_base9.hex.encode(serverXOnlyPublicKey),
3485
- timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
3541
+ timeoutBlockHeights
3486
3542
  });
3487
3543
  if (!vhtlcScript.claimScript)
3488
- throw new Error("Failed to create VHTLC script for submarine swap");
3544
+ throw new Error(
3545
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for submarine swap`
3546
+ );
3489
3547
  if (vhtlcAddress !== pendingSwap.response.address)
3490
3548
  throw new Error(
3491
3549
  `VHTLC address mismatch for swap ${pendingSwap.id}: expected ${pendingSwap.response.address}, got ${vhtlcAddress}`
@@ -3500,30 +3558,50 @@ var init_arkade_swaps = __esm({
3500
3558
  scripts: [vhtlcPkScriptHex]
3501
3559
  });
3502
3560
  throw new Error(
3503
- allVtxos.length > 0 ? "VHTLC is already spent" : `VHTLC not found for address ${pendingSwap.response.address}`
3561
+ allVtxos.length > 0 ? `Swap ${pendingSwap.id}: VHTLC is already spent` : `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.address}`
3504
3562
  );
3505
3563
  }
3506
3564
  const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3565
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
3566
+ const refundLocktime = BigInt(timeoutBlockHeights.refund);
3567
+ const currentBlockHeight = await this.swapProvider.getChainHeight();
3568
+ const cltvSatisfied = BigInt(currentBlockHeight) >= refundLocktime;
3507
3569
  let boltzCallCount = 0;
3570
+ let skippedCount = 0;
3508
3571
  for (const vtxo of spendableVtxos) {
3509
3572
  const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
3510
- const input = {
3511
- ...vtxo,
3512
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
3513
- tapTree: vhtlcScript.encode()
3514
- };
3515
3573
  const output = {
3516
3574
  amount: BigInt(vtxo.value),
3517
3575
  script: outputScript
3518
3576
  };
3519
- if (isRecoverableVtxo) {
3577
+ if (cltvSatisfied) {
3578
+ const input2 = {
3579
+ ...vtxo,
3580
+ tapLeafScript: refundWithoutReceiverLeaf,
3581
+ tapTree: vhtlcScript.encode()
3582
+ };
3520
3583
  await this.joinBatch(
3521
3584
  this.wallet.identity,
3522
- input,
3585
+ input2,
3523
3586
  output,
3524
- arkInfo
3587
+ arkInfo,
3588
+ isRecoverableVtxo
3525
3589
  );
3526
- } else {
3590
+ continue;
3591
+ }
3592
+ if (isRecoverableVtxo) {
3593
+ logger.error(
3594
+ `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.`
3595
+ );
3596
+ skippedCount++;
3597
+ continue;
3598
+ }
3599
+ const input = {
3600
+ ...vtxo,
3601
+ tapLeafScript: vhtlcScript.refund(),
3602
+ tapTree: vhtlcScript.encode()
3603
+ };
3604
+ try {
3527
3605
  if (boltzCallCount > 0) {
3528
3606
  await new Promise((r) => setTimeout(r, 2e3));
3529
3607
  }
@@ -3542,14 +3620,42 @@ var init_arkade_swaps = __esm({
3542
3620
  )
3543
3621
  );
3544
3622
  boltzCallCount++;
3623
+ } catch (error) {
3624
+ if (!(error instanceof BoltzRefundError)) {
3625
+ throw error;
3626
+ }
3627
+ const tipNow = await this.swapProvider.getChainHeight();
3628
+ if (BigInt(tipNow) < refundLocktime) {
3629
+ logger.error(
3630
+ `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.`
3631
+ );
3632
+ skippedCount++;
3633
+ continue;
3634
+ }
3635
+ logger.warn(
3636
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
3637
+ );
3638
+ const fallbackInput = {
3639
+ ...vtxo,
3640
+ tapLeafScript: refundWithoutReceiverLeaf,
3641
+ tapTree: vhtlcScript.encode()
3642
+ };
3643
+ await this.joinBatch(
3644
+ this.wallet.identity,
3645
+ fallbackInput,
3646
+ output,
3647
+ arkInfo,
3648
+ false
3649
+ );
3545
3650
  }
3546
3651
  }
3652
+ const fullyRefunded = skippedCount === 0;
3547
3653
  await updateSubmarineSwapStatus(
3548
3654
  pendingSwap,
3549
3655
  pendingSwap.status,
3550
3656
  // Keep current status
3551
3657
  this.savePendingSubmarineSwap.bind(this),
3552
- { refundable: true, refunded: true }
3658
+ { refundable: true, refunded: fullyRefunded }
3553
3659
  );
3554
3660
  }
3555
3661
  /**
@@ -3752,14 +3858,22 @@ var init_arkade_swaps = __esm({
3752
3858
  */
3753
3859
  async claimBtc(pendingSwap) {
3754
3860
  if (!pendingSwap.toAddress)
3755
- throw new Error("Destination address is required");
3861
+ throw new Error(
3862
+ `Swap ${pendingSwap.id}: destination address is required`
3863
+ );
3756
3864
  if (!pendingSwap.response.claimDetails.swapTree)
3757
- throw new Error("Missing swap tree in claim details");
3865
+ throw new Error(
3866
+ `Swap ${pendingSwap.id}: missing swap tree in claim details`
3867
+ );
3758
3868
  if (!pendingSwap.response.claimDetails.serverPublicKey)
3759
- throw new Error("Missing server public key in claim details");
3869
+ throw new Error(
3870
+ `Swap ${pendingSwap.id}: missing server public key in claim details`
3871
+ );
3760
3872
  const swapStatus = await this.getSwapStatus(pendingSwap.id);
3761
3873
  if (!swapStatus.transaction?.hex)
3762
- throw new Error("BTC transaction hex is required");
3874
+ throw new Error(
3875
+ `Swap ${pendingSwap.id}: BTC transaction hex is required`
3876
+ );
3763
3877
  const lockupTx = import_btc_signer5.Transaction.fromRaw(
3764
3878
  import_base9.hex.decode(swapStatus.transaction.hex)
3765
3879
  );
@@ -3814,7 +3928,9 @@ var init_arkade_swaps = __esm({
3814
3928
  }
3815
3929
  );
3816
3930
  if (!signedTxData.pubNonce || !signedTxData.partialSignature)
3817
- throw new Error("Invalid signature data from server");
3931
+ throw new Error(
3932
+ `Swap ${pendingSwap.id}: invalid signature data from server`
3933
+ );
3818
3934
  const musigSession = musigMessage.aggregateNonces([
3819
3935
  [
3820
3936
  import_base9.hex.decode(
@@ -3839,9 +3955,13 @@ var init_arkade_swaps = __esm({
3839
3955
  */
3840
3956
  async refundArk(pendingSwap) {
3841
3957
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
3842
- throw new Error("Missing server public key in lockup details");
3958
+ throw new Error(
3959
+ `Swap ${pendingSwap.id}: missing server public key in lockup details`
3960
+ );
3843
3961
  if (!pendingSwap.response.lockupDetails.timeouts)
3844
- throw new Error("Missing timeouts in lockup details");
3962
+ throw new Error(
3963
+ `Swap ${pendingSwap.id}: missing timeouts in lockup details`
3964
+ );
3845
3965
  const arkInfo = await this.arkProvider.getInfo();
3846
3966
  const address = await this.wallet.getAddress();
3847
3967
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
@@ -3867,12 +3987,12 @@ var init_arkade_swaps = __esm({
3867
3987
  });
3868
3988
  if (vtxos.length === 0) {
3869
3989
  throw new Error(
3870
- `VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
3990
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
3871
3991
  );
3872
3992
  }
3873
3993
  const vtxo = vtxos[0];
3874
3994
  if (vtxo.isSpent) {
3875
- throw new Error("VHTLC is already spent");
3995
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3876
3996
  }
3877
3997
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
3878
3998
  network: arkInfo.network,
@@ -3883,7 +4003,9 @@ var init_arkade_swaps = __esm({
3883
4003
  timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
3884
4004
  });
3885
4005
  if (!vhtlcScript.refundScript)
3886
- throw new Error("Failed to create VHTLC script for chain swap");
4006
+ throw new Error(
4007
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4008
+ );
3887
4009
  if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
3888
4010
  throw new SwapError({
3889
4011
  message: "Unable to claim: invalid VHTLC address"
@@ -4057,11 +4179,17 @@ var init_arkade_swaps = __esm({
4057
4179
  */
4058
4180
  async claimArk(pendingSwap) {
4059
4181
  if (!pendingSwap.toAddress)
4060
- throw new Error("Destination address is required");
4182
+ throw new Error(
4183
+ `Swap ${pendingSwap.id}: destination address is required`
4184
+ );
4061
4185
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4062
- throw new Error("Missing server public key in claim details");
4186
+ throw new Error(
4187
+ `Swap ${pendingSwap.id}: missing server public key in claim details`
4188
+ );
4063
4189
  if (!pendingSwap.response.claimDetails.timeouts)
4064
- throw new Error("Missing timeouts in claim details");
4190
+ throw new Error(
4191
+ `Swap ${pendingSwap.id}: missing timeouts in claim details`
4192
+ );
4065
4193
  const arkInfo = await this.arkProvider.getInfo();
4066
4194
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
4067
4195
  const address = await this.wallet.getAddress();
@@ -4086,7 +4214,9 @@ var init_arkade_swaps = __esm({
4086
4214
  timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
4087
4215
  });
4088
4216
  if (!vhtlcScript.claimScript)
4089
- throw new Error("Failed to create VHTLC script for chain swap");
4217
+ throw new Error(
4218
+ `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4219
+ );
4090
4220
  if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
4091
4221
  throw new SwapError({
4092
4222
  message: "Unable to claim: invalid VHTLC address"
@@ -4097,7 +4227,9 @@ var init_arkade_swaps = __esm({
4097
4227
  spendableOnly: true
4098
4228
  });
4099
4229
  if (spendableVtxos.vtxos.length === 0)
4100
- throw new Error("No spendable virtual coins found");
4230
+ throw new Error(
4231
+ `Swap ${pendingSwap.id}: no spendable virtual coins found`
4232
+ );
4101
4233
  const vtxo = spendableVtxos.vtxos[0];
4102
4234
  const input = {
4103
4235
  ...vtxo,
@@ -4137,16 +4269,20 @@ var init_arkade_swaps = __esm({
4137
4269
  */
4138
4270
  async signCooperativeClaimForServer(pendingSwap) {
4139
4271
  if (!pendingSwap.response.lockupDetails.swapTree)
4140
- throw new Error("Missing swap tree in lockup details");
4272
+ throw new Error(
4273
+ `Swap ${pendingSwap.id}: missing swap tree in lockup details`
4274
+ );
4141
4275
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4142
- throw new Error("Missing server public key in lockup details");
4276
+ throw new Error(
4277
+ `Swap ${pendingSwap.id}: missing server public key in lockup details`
4278
+ );
4143
4279
  const claimDetails = await this.swapProvider.getChainClaimDetails(
4144
4280
  pendingSwap.id
4145
4281
  );
4146
4282
  const serverPubKey = pendingSwap.response.lockupDetails.serverPublicKey;
4147
4283
  if (claimDetails.publicKey !== serverPubKey) {
4148
4284
  throw new Error(
4149
- `Server public key mismatch: claim response has ${claimDetails.publicKey}, expected ${serverPubKey}`
4285
+ `Swap ${pendingSwap.id}: server public key mismatch \u2014 claim response has ${claimDetails.publicKey}, expected ${serverPubKey}`
4150
4286
  );
4151
4287
  }
4152
4288
  const musig = tweakMusig(
@@ -4266,15 +4402,23 @@ var init_arkade_swaps = __esm({
4266
4402
  const { to, from, swap, arkInfo } = args;
4267
4403
  if (from === "ARK") {
4268
4404
  if (!swap.response.lockupDetails.serverPublicKey)
4269
- throw new Error("Missing serverPublicKey in lockup details");
4405
+ throw new Error(
4406
+ `Swap ${swap.id}: missing serverPublicKey in lockup details`
4407
+ );
4270
4408
  if (!swap.response.lockupDetails.timeouts)
4271
- throw new Error("Missing timeouts in lockup details");
4409
+ throw new Error(
4410
+ `Swap ${swap.id}: missing timeouts in lockup details`
4411
+ );
4272
4412
  }
4273
4413
  if (to === "ARK") {
4274
4414
  if (!swap.response.claimDetails.serverPublicKey)
4275
- throw new Error("Missing serverPublicKey in claim details");
4415
+ throw new Error(
4416
+ `Swap ${swap.id}: missing serverPublicKey in claim details`
4417
+ );
4276
4418
  if (!swap.response.claimDetails.timeouts)
4277
- throw new Error("Missing timeouts in claim details");
4419
+ throw new Error(
4420
+ `Swap ${swap.id}: missing timeouts in claim details`
4421
+ );
4278
4422
  }
4279
4423
  const lockupAddress = to === "ARK" ? swap.response.claimDetails.lockupAddress : swap.response.lockupDetails.lockupAddress;
4280
4424
  const receiverPubkey = to === "ARK" ? swap.request.claimPublicKey : swap.response.lockupDetails.serverPublicKey;
@@ -4484,7 +4628,8 @@ var init_arkade_swaps = __esm({
4484
4628
  lockupAddress,
4485
4629
  preimageHash,
4486
4630
  serverPublicKey,
4487
- tree
4631
+ tree,
4632
+ timeoutBlockHeights
4488
4633
  } = swap.claimDetails;
4489
4634
  reverseSwaps.push({
4490
4635
  id,
@@ -4500,18 +4645,18 @@ var init_arkade_swaps = __esm({
4500
4645
  onchainAmount: amount,
4501
4646
  lockupAddress,
4502
4647
  refundPublicKey: serverPublicKey,
4503
- timeoutBlockHeights: {
4648
+ timeoutBlockHeights: timeoutBlockHeights ?? {
4504
4649
  refund: extractTimeLockFromLeafOutput(
4505
- tree.refundWithoutBoltzLeaf.output
4650
+ tree.refundWithoutBoltzLeaf?.output ?? ""
4506
4651
  ),
4507
4652
  unilateralClaim: extractTimeLockFromLeafOutput(
4508
- tree.unilateralClaimLeaf.output
4653
+ tree.unilateralClaimLeaf?.output ?? ""
4509
4654
  ),
4510
4655
  unilateralRefund: extractTimeLockFromLeafOutput(
4511
- tree.unilateralRefundLeaf.output
4656
+ tree.unilateralRefundLeaf?.output ?? ""
4512
4657
  ),
4513
4658
  unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4514
- tree.unilateralRefundWithoutBoltzLeaf.output
4659
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4515
4660
  )
4516
4661
  }
4517
4662
  },
@@ -4520,7 +4665,13 @@ var init_arkade_swaps = __esm({
4520
4665
  preimage: ""
4521
4666
  });
4522
4667
  } else if (isRestoredSubmarineSwap(swap)) {
4523
- const { amount, lockupAddress, serverPublicKey, tree } = swap.refundDetails;
4668
+ const {
4669
+ amount,
4670
+ lockupAddress,
4671
+ serverPublicKey,
4672
+ tree,
4673
+ timeoutBlockHeights
4674
+ } = swap.refundDetails;
4524
4675
  let preimage = "";
4525
4676
  if (!isSubmarineFinalStatus(status)) {
4526
4677
  try {
@@ -4551,29 +4702,31 @@ var init_arkade_swaps = __esm({
4551
4702
  address: lockupAddress,
4552
4703
  expectedAmount: amount,
4553
4704
  claimPublicKey: serverPublicKey,
4554
- timeoutBlockHeights: {
4705
+ timeoutBlockHeights: timeoutBlockHeights ?? {
4555
4706
  refund: extractTimeLockFromLeafOutput(
4556
- tree.refundWithoutBoltzLeaf.output
4707
+ tree.refundWithoutBoltzLeaf?.output ?? ""
4557
4708
  ),
4558
4709
  unilateralClaim: extractTimeLockFromLeafOutput(
4559
- tree.unilateralClaimLeaf.output
4710
+ tree.unilateralClaimLeaf?.output ?? ""
4560
4711
  ),
4561
4712
  unilateralRefund: extractTimeLockFromLeafOutput(
4562
- tree.unilateralRefundLeaf.output
4713
+ tree.unilateralRefundLeaf?.output ?? ""
4563
4714
  ),
4564
4715
  unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4565
- tree.unilateralRefundWithoutBoltzLeaf.output
4716
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4566
4717
  )
4567
4718
  }
4568
4719
  }
4569
4720
  });
4570
4721
  } else if (isRestoredChainSwap(swap)) {
4722
+ const refundDetails = swap.refundDetails;
4723
+ if (!refundDetails) continue;
4571
4724
  const {
4572
4725
  amount,
4573
4726
  lockupAddress,
4574
4727
  serverPublicKey,
4575
4728
  timeoutBlockHeight
4576
- } = swap.refundDetails;
4729
+ } = refundDetails;
4577
4730
  chainSwaps.push({
4578
4731
  id,
4579
4732
  type: "chain",
@@ -1,5 +1,5 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps } from '../arkade-swaps-CwQbGdU9.cjs';
2
- import { A as ArkadeSwapsConfig, j as SwapRepository, m as BoltzSwapProvider, N as Network, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, F as FeesResponse, a as BoltzChainSwap, h as ArkToBtcResponse, i as BtcToArkResponse, d as Chain, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-x542EUL6.cjs';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps } from '../arkade-swaps-BlK0ASlg.cjs';
2
+ import { A as ArkadeSwapsConfig, j as SwapRepository, m as BoltzSwapProvider, N as Network, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, F as FeesResponse, a as BoltzChainSwap, h as ArkToBtcResponse, i as BtcToArkResponse, d as Chain, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-CKxFfdEH.cjs';
3
3
  import { Identity, ArkProvider, IndexerProvider, IWallet, ArkInfo, ArkTxInput, VHTLC } from '@arkade-os/sdk';
4
4
  import { AsyncStorageTaskQueue, TaskProcessor } from '@arkade-os/sdk/worker/expo';
5
5
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
@@ -1,5 +1,5 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps } from '../arkade-swaps-BfHIKMq0.js';
2
- import { A as ArkadeSwapsConfig, j as SwapRepository, m as BoltzSwapProvider, N as Network, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, F as FeesResponse, a as BoltzChainSwap, h as ArkToBtcResponse, i as BtcToArkResponse, d as Chain, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-x542EUL6.js';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps } from '../arkade-swaps-CM-UQ9-5.js';
2
+ import { A as ArkadeSwapsConfig, j as SwapRepository, m as BoltzSwapProvider, N as Network, k as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, F as FeesResponse, a as BoltzChainSwap, h as ArkToBtcResponse, i as BtcToArkResponse, d as Chain, g as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-CKxFfdEH.js';
3
3
  import { Identity, ArkProvider, IndexerProvider, IWallet, ArkInfo, ArkTxInput, VHTLC } from '@arkade-os/sdk';
4
4
  import { AsyncStorageTaskQueue, TaskProcessor } from '@arkade-os/sdk/worker/expo';
5
5
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';