@arkade-os/boltz-swap 0.3.32 → 0.3.34

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.
Files changed (31) hide show
  1. package/README.md +22 -26
  2. package/dist/{arkade-swaps-CS8FZSVL.d.cts → arkade-swaps-BXAD1s8j.d.ts} +69 -8
  3. package/dist/{arkade-swaps-WiKCanCL.d.ts → arkade-swaps-CfMets16.d.cts} +69 -8
  4. package/dist/{chunk-NHBWNN6H.js → chunk-5K2FS2FE.js} +8 -27
  5. package/dist/chunk-SJQJQO7P.js +25 -0
  6. package/dist/{chunk-B3Q4TFWT.js → chunk-TDBUZE4N.js} +817 -866
  7. package/dist/expo/background.cjs +840 -892
  8. package/dist/expo/background.d.cts +3 -3
  9. package/dist/expo/background.d.ts +3 -3
  10. package/dist/expo/background.js +9 -20
  11. package/dist/expo/index.cjs +840 -870
  12. package/dist/expo/index.d.cts +8 -6
  13. package/dist/expo/index.d.ts +8 -6
  14. package/dist/expo/index.js +17 -20
  15. package/dist/index.cjs +1034 -1022
  16. package/dist/index.d.cts +95 -11
  17. package/dist/index.d.ts +95 -11
  18. package/dist/index.js +164 -119
  19. package/dist/repositories/realm/index.cjs +10 -22
  20. package/dist/repositories/realm/index.d.cts +7 -5
  21. package/dist/repositories/realm/index.d.ts +7 -5
  22. package/dist/repositories/realm/index.js +8 -22
  23. package/dist/repositories/sqlite/index.cjs +12 -23
  24. package/dist/repositories/sqlite/index.d.cts +1 -1
  25. package/dist/repositories/sqlite/index.d.ts +1 -1
  26. package/dist/repositories/sqlite/index.js +10 -23
  27. package/dist/{swapsPollProcessor-BF3uTFae.d.cts → swapsPollProcessor-BpAqG0V6.d.cts} +3 -3
  28. package/dist/{swapsPollProcessor-wYOMzldd.d.ts → swapsPollProcessor-DFVOAy_-.d.ts} +3 -3
  29. package/dist/{types-BBI7-KJ0.d.cts → types--axEWA8c.d.cts} +42 -4
  30. package/dist/{types-BBI7-KJ0.d.ts → types--axEWA8c.d.ts} +42 -4
  31. package/package.json +10 -25
@@ -41,6 +41,7 @@ var TaskManager = __toESM(require("expo-task-manager"), 1);
41
41
  var BackgroundTask = __toESM(require("expo-background-task"), 1);
42
42
  var import_expo = require("@arkade-os/sdk/worker/expo");
43
43
  var import_expo2 = require("@arkade-os/sdk/adapters/expo");
44
+ var import_sdk9 = require("@arkade-os/sdk");
44
45
 
45
46
  // src/boltz-swap-provider.ts
46
47
  var import_sdk = require("@arkade-os/sdk");
@@ -54,7 +55,10 @@ var SwapError = class extends Error {
54
55
  /** The pending swap associated with this error, if available. */
55
56
  pendingSwap;
56
57
  constructor(options = {}) {
57
- super(options.message ?? "Error during swap.");
58
+ super(
59
+ options.message ?? "Error during swap.",
60
+ options.cause !== void 0 ? { cause: options.cause } : void 0
61
+ );
58
62
  this.name = "SwapError";
59
63
  this.isClaimable = options.isClaimable ?? false;
60
64
  this.isRefundable = options.isRefundable ?? false;
@@ -131,6 +135,100 @@ var TransactionRefundedError = class extends SwapError {
131
135
  this.name = "TransactionRefundedError";
132
136
  }
133
137
  };
138
+ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
139
+ reason;
140
+ quotedAmount;
141
+ floor;
142
+ constructor(options) {
143
+ super({
144
+ message: options.message ?? _QuoteRejectedError.defaultMessage(options),
145
+ ...options
146
+ });
147
+ this.name = "QuoteRejectedError";
148
+ this.reason = options.reason;
149
+ this.quotedAmount = "quotedAmount" in options ? options.quotedAmount : void 0;
150
+ this.floor = "floor" in options ? options.floor : void 0;
151
+ }
152
+ static defaultMessage(options) {
153
+ switch (options.reason) {
154
+ case "below_floor":
155
+ return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
156
+ case "non_positive":
157
+ return `Boltz quote ${options.quotedAmount} is not positive`;
158
+ case "non_safe_integer":
159
+ return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
160
+ case "no_baseline":
161
+ return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
162
+ }
163
+ }
164
+ /**
165
+ * Serialize into a plain `Error` whose `.message` carries the full
166
+ * rejection payload as JSON behind a marker prefix. Structured clone
167
+ * (used by `postMessage` between page and service worker) preserves
168
+ * `Error.message` reliably but strips custom `.name` and own properties,
169
+ * so we move the typed data into the message field for transport.
170
+ */
171
+ toTransportError() {
172
+ return new Error(
173
+ QUOTE_REJECTION_TRANSPORT_PREFIX + JSON.stringify({
174
+ reason: this.reason,
175
+ message: this.message,
176
+ quotedAmount: this.quotedAmount,
177
+ floor: this.floor
178
+ })
179
+ );
180
+ }
181
+ /**
182
+ * Inverse of `toTransportError`. Returns a real `QuoteRejectedError` if
183
+ * `error` carries the transport prefix, else `null`.
184
+ */
185
+ static fromTransportError(error) {
186
+ if (!(error instanceof Error) || !error.message.startsWith(QUOTE_REJECTION_TRANSPORT_PREFIX)) {
187
+ return null;
188
+ }
189
+ const payload = error.message.slice(QUOTE_REJECTION_TRANSPORT_PREFIX.length);
190
+ let data;
191
+ try {
192
+ data = JSON.parse(payload);
193
+ } catch {
194
+ return null;
195
+ }
196
+ if (typeof data.reason !== "string" || !QUOTE_REJECTION_REASONS.has(data.reason)) {
197
+ return null;
198
+ }
199
+ const message = typeof data.message === "string" ? data.message : void 0;
200
+ const reason = data.reason;
201
+ const quotedAmount = typeof data.quotedAmount === "number" ? data.quotedAmount : null;
202
+ const floor = typeof data.floor === "number" ? data.floor : null;
203
+ switch (reason) {
204
+ case "below_floor":
205
+ if (quotedAmount === null || floor === null) return null;
206
+ return new _QuoteRejectedError({
207
+ reason,
208
+ quotedAmount,
209
+ floor,
210
+ message
211
+ });
212
+ case "non_positive":
213
+ case "non_safe_integer":
214
+ if (quotedAmount === null) return null;
215
+ return new _QuoteRejectedError({
216
+ reason,
217
+ quotedAmount,
218
+ message
219
+ });
220
+ case "no_baseline":
221
+ return new _QuoteRejectedError({ reason, message });
222
+ }
223
+ }
224
+ };
225
+ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
226
+ var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
227
+ "below_floor",
228
+ "non_positive",
229
+ "non_safe_integer",
230
+ "no_baseline"
231
+ ]);
134
232
  var BoltzRefundError = class extends Error {
135
233
  constructor(message, cause) {
136
234
  super(message);
@@ -142,18 +240,10 @@ var BoltzRefundError = class extends Error {
142
240
  // src/boltz-swap-provider.ts
143
241
  var import_base = require("@scure/base");
144
242
  var isSubmarineFinalStatus = (status) => {
145
- return [
146
- "invoice.failedToPay",
147
- "transaction.claimed",
148
- "swap.expired"
149
- ].includes(status);
243
+ return ["invoice.failedToPay", "transaction.claimed", "swap.expired"].includes(status);
150
244
  };
151
245
  var isSubmarineRefundableStatus = (status) => {
152
- return [
153
- "invoice.failedToPay",
154
- "transaction.lockupFailed",
155
- "swap.expired"
156
- ].includes(status);
246
+ return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status);
157
247
  };
158
248
  var isSubmarineSuccessStatus = (status) => {
159
249
  return status === "transaction.claimed";
@@ -172,10 +262,7 @@ var isReverseClaimableStatus = (status) => {
172
262
  return ["transaction.mempool", "transaction.confirmed"].includes(status);
173
263
  };
174
264
  var isChainClaimableStatus = (status) => {
175
- return [
176
- "transaction.server.mempool",
177
- "transaction.server.confirmed"
178
- ].includes(status);
265
+ return ["transaction.server.mempool", "transaction.server.confirmed"].includes(status);
179
266
  };
180
267
  var isChainFinalStatus = (status) => {
181
268
  return [
@@ -292,8 +379,7 @@ var BASE_URLS = {
292
379
  var isSwapNotFoundBody = (error) => {
293
380
  const needle = "could not find swap";
294
381
  const fromJson = error.errorData?.error;
295
- if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle))
296
- return true;
382
+ if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle)) return true;
297
383
  return error.message.toLowerCase().includes(needle);
298
384
  };
299
385
  var BoltzSwapProvider = class {
@@ -307,10 +393,7 @@ var BoltzSwapProvider = class {
307
393
  this.network = config.network;
308
394
  this.referralId = config.referralId ?? "arkade-ts-sdk";
309
395
  const apiUrl = config.apiUrl || BASE_URLS[config.network];
310
- if (!apiUrl)
311
- throw new Error(
312
- `API URL is required for network: ${config.network}`
313
- );
396
+ if (!apiUrl) throw new Error(`API URL is required for network: ${config.network}`);
314
397
  this.apiUrl = apiUrl;
315
398
  this.wsUrl = this.apiUrl.replace(/^http(s)?:\/\//, "ws$1://").replace("9069", "9004") + "/v2/ws";
316
399
  }
@@ -329,10 +412,7 @@ var BoltzSwapProvider = class {
329
412
  /** Returns current Lightning swap fees (submarine + reverse) from Boltz. */
330
413
  async getFees() {
331
414
  const [submarine, reverse] = await Promise.all([
332
- this.request(
333
- "/v2/swap/submarine",
334
- "GET"
335
- ),
415
+ this.request("/v2/swap/submarine", "GET"),
336
416
  this.request("/v2/swap/reverse", "GET")
337
417
  ]);
338
418
  if (!isGetSubmarinePairsResponse(submarine))
@@ -352,10 +432,7 @@ var BoltzSwapProvider = class {
352
432
  }
353
433
  /** Returns current Lightning swap min/max limits from Boltz. */
354
434
  async getLimits() {
355
- const response = await this.request(
356
- "/v2/swap/submarine",
357
- "GET"
358
- );
435
+ const response = await this.request("/v2/swap/submarine", "GET");
359
436
  if (!isGetSubmarinePairsResponse(response))
360
437
  throw new SchemaError({ message: "error fetching limits" });
361
438
  return {
@@ -365,10 +442,7 @@ var BoltzSwapProvider = class {
365
442
  }
366
443
  /** Returns the current BTC chain tip height from Boltz. */
367
444
  async getChainHeight() {
368
- const response = await this.request(
369
- "/v2/chain/heights",
370
- "GET"
371
- );
445
+ const response = await this.request("/v2/chain/heights", "GET");
372
446
  if (typeof response?.BTC !== "number")
373
447
  throw new SchemaError({
374
448
  message: "error fetching chain heights"
@@ -398,10 +472,7 @@ var BoltzSwapProvider = class {
398
472
  async getSwapStatus(id) {
399
473
  let response;
400
474
  try {
401
- response = await this.request(
402
- `/v2/swap/${id}`,
403
- "GET"
404
- );
475
+ response = await this.request(`/v2/swap/${id}`, "GET");
405
476
  } catch (error) {
406
477
  if (error instanceof NetworkError && error.statusCode === 404 && isSwapNotFoundBody(error)) {
407
478
  throw new SwapNotFoundError(id, error.errorData);
@@ -497,12 +568,10 @@ var BoltzSwapProvider = class {
497
568
  throw new SwapError({ message: "Invalid 'to' chain" });
498
569
  if (["BTC", "ARK"].indexOf(from) === -1)
499
570
  throw new SwapError({ message: "Invalid 'from' chain" });
500
- if (to === from)
501
- throw new SwapError({ message: "Invalid swap direction" });
571
+ if (to === from) throw new SwapError({ message: "Invalid swap direction" });
502
572
  if (!preimageHash || preimageHash.length != 64)
503
573
  throw new SwapError({ message: "Invalid preimageHash" });
504
- if (feeSatsPerByte <= 0)
505
- throw new SwapError({ message: "Invalid feeSatsPerByte" });
574
+ if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" });
506
575
  if (serverLockAmount !== void 0 && userLockAmount !== void 0 || serverLockAmount === void 0 && userLockAmount === void 0)
507
576
  throw new SwapError({
508
577
  message: "Either serverLockAmount or userLockAmount must be provided"
@@ -557,12 +626,8 @@ var BoltzSwapProvider = class {
557
626
  message: "Error refunding submarine swap"
558
627
  });
559
628
  return {
560
- transaction: import_sdk.Transaction.fromPSBT(
561
- import_base.base64.decode(response.transaction)
562
- ),
563
- checkpoint: import_sdk.Transaction.fromPSBT(
564
- import_base.base64.decode(response.checkpoint)
565
- )
629
+ transaction: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.transaction)),
630
+ checkpoint: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.checkpoint))
566
631
  };
567
632
  }
568
633
  /** Requests Boltz co-signature for a chain swap refund. Returns signed transaction + checkpoint. */
@@ -581,53 +646,53 @@ var BoltzSwapProvider = class {
581
646
  message: "Error refunding chain swap"
582
647
  });
583
648
  return {
584
- transaction: import_sdk.Transaction.fromPSBT(
585
- import_base.base64.decode(response.transaction)
586
- ),
587
- checkpoint: import_sdk.Transaction.fromPSBT(
588
- import_base.base64.decode(response.checkpoint)
589
- )
649
+ transaction: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.transaction)),
650
+ checkpoint: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.checkpoint))
590
651
  };
591
652
  }
592
- /** Monitors swap status updates via WebSocket. Calls update callback on each status change. Resolves when terminal. */
653
+ /**
654
+ * Monitors swap status updates and forwards them to the update callback.
655
+ * Prefers a WebSocket subscription; on connection error or premature close
656
+ * it falls back to REST polling so callers don't fail when the WS endpoint
657
+ * is flaky (observed in CI). Resolves when the swap reaches a terminal
658
+ * status.
659
+ */
593
660
  async monitorSwap(swapId, update) {
594
661
  return new Promise((resolve, reject) => {
595
- const webSocket = new globalThis.WebSocket(this.wsUrl);
596
- const connectionTimeout = setTimeout(() => {
597
- webSocket.close();
598
- reject(new NetworkError("WebSocket connection timeout"));
599
- }, 3e4);
600
- webSocket.onerror = (error) => {
601
- clearTimeout(connectionTimeout);
602
- reject(
603
- new NetworkError(
604
- `WebSocket error: ${error.message}`
605
- )
606
- );
607
- };
608
- webSocket.onopen = () => {
609
- clearTimeout(connectionTimeout);
610
- webSocket.send(
611
- JSON.stringify({
612
- op: "subscribe",
613
- channel: "swap.update",
614
- args: [swapId]
615
- })
616
- );
662
+ let settled = false;
663
+ let lastStatus = null;
664
+ let pollTimer = null;
665
+ let webSocket = null;
666
+ let connectionTimeout = null;
667
+ const cleanup = () => {
668
+ if (connectionTimeout) {
669
+ clearTimeout(connectionTimeout);
670
+ connectionTimeout = null;
671
+ }
672
+ if (pollTimer) {
673
+ clearInterval(pollTimer);
674
+ pollTimer = null;
675
+ }
676
+ if (webSocket) {
677
+ try {
678
+ webSocket.close();
679
+ } catch {
680
+ }
681
+ webSocket = null;
682
+ }
617
683
  };
618
- webSocket.onclose = () => {
619
- clearTimeout(connectionTimeout);
620
- resolve();
684
+ const finish = (err) => {
685
+ if (settled) return;
686
+ settled = true;
687
+ cleanup();
688
+ if (err) reject(err);
689
+ else resolve();
621
690
  };
622
- webSocket.onmessage = async (rawMsg) => {
623
- const msg = JSON.parse(rawMsg.data);
624
- if (msg.event !== "update" || msg.args[0].id !== swapId) return;
625
- if (msg.args[0].error) {
626
- webSocket.close();
627
- reject(new SwapError({ message: msg.args[0].error }));
628
- }
629
- const status = msg.args[0].status;
630
- const negotiable = status === "transaction.lockupFailed" && msg.args[0].failureDetails?.actual !== void 0 && msg.args[0].failureDetails?.expected !== void 0;
691
+ const handleStatus = (status, data) => {
692
+ if (settled) return;
693
+ if (status === lastStatus) return;
694
+ lastStatus = status;
695
+ const negotiable = status === "transaction.lockupFailed" && data?.failureDetails?.actual !== void 0 && data?.failureDetails?.expected !== void 0;
631
696
  switch (status) {
632
697
  case "invoice.settled":
633
698
  case "transaction.claimed":
@@ -636,13 +701,13 @@ var BoltzSwapProvider = class {
636
701
  case "invoice.failedToPay":
637
702
  case "transaction.failed":
638
703
  case "swap.expired":
639
- webSocket.close();
640
- update(status, msg.args[0]);
641
- break;
704
+ update(status, data);
705
+ finish();
706
+ return;
642
707
  case "transaction.lockupFailed":
643
- if (!negotiable) webSocket.close();
644
- update(status, msg.args[0]);
645
- break;
708
+ update(status, data);
709
+ if (!negotiable) finish();
710
+ return;
646
711
  case "invoice.paid":
647
712
  case "invoice.pending":
648
713
  case "invoice.set":
@@ -652,9 +717,77 @@ var BoltzSwapProvider = class {
652
717
  case "transaction.claim.pending":
653
718
  case "transaction.server.mempool":
654
719
  case "transaction.server.confirmed":
655
- update(status, msg.args[0]);
720
+ update(status, data);
721
+ return;
656
722
  }
657
723
  };
724
+ const startPolling = () => {
725
+ if (settled || pollTimer) return;
726
+ const poll = async () => {
727
+ if (settled) return;
728
+ try {
729
+ const result = await this.getSwapStatus(swapId);
730
+ handleStatus(result.status, { ...result, id: swapId });
731
+ } catch {
732
+ }
733
+ };
734
+ pollTimer = setInterval(poll, 1e3);
735
+ poll();
736
+ };
737
+ try {
738
+ webSocket = new globalThis.WebSocket(this.wsUrl);
739
+ connectionTimeout = setTimeout(() => {
740
+ try {
741
+ webSocket?.close();
742
+ } catch {
743
+ }
744
+ webSocket = null;
745
+ startPolling();
746
+ }, 5e3);
747
+ webSocket.onerror = () => {
748
+ if (connectionTimeout) {
749
+ clearTimeout(connectionTimeout);
750
+ connectionTimeout = null;
751
+ }
752
+ webSocket = null;
753
+ startPolling();
754
+ };
755
+ webSocket.onopen = () => {
756
+ if (connectionTimeout) {
757
+ clearTimeout(connectionTimeout);
758
+ connectionTimeout = null;
759
+ }
760
+ webSocket?.send(
761
+ JSON.stringify({
762
+ op: "subscribe",
763
+ channel: "swap.update",
764
+ args: [swapId]
765
+ })
766
+ );
767
+ };
768
+ webSocket.onclose = () => {
769
+ if (connectionTimeout) {
770
+ clearTimeout(connectionTimeout);
771
+ connectionTimeout = null;
772
+ }
773
+ if (!settled && !pollTimer) startPolling();
774
+ };
775
+ webSocket.onmessage = (rawMsg) => {
776
+ try {
777
+ const msg = JSON.parse(rawMsg.data);
778
+ if (msg.event !== "update" || msg.args?.[0]?.id !== swapId) return;
779
+ if (msg.args[0].error) {
780
+ finish(new SwapError({ message: msg.args[0].error }));
781
+ return;
782
+ }
783
+ handleStatus(msg.args[0].status, msg.args[0]);
784
+ } catch {
785
+ }
786
+ };
787
+ } catch {
788
+ webSocket = null;
789
+ startPolling();
790
+ }
658
791
  });
659
792
  }
660
793
  /** Returns current chain swap fees for a given pair (e.g. ARK→BTC). */
@@ -662,10 +795,7 @@ var BoltzSwapProvider = class {
662
795
  if (from === to) {
663
796
  throw new SwapError({ message: "Invalid chain pair" });
664
797
  }
665
- const response = await this.request(
666
- "/v2/swap/chain",
667
- "GET"
668
- );
798
+ const response = await this.request("/v2/swap/chain", "GET");
669
799
  if (!isGetChainPairsResponse(response))
670
800
  throw new SchemaError({ message: "error fetching fees" });
671
801
  if (!response[from]?.[to]) {
@@ -680,10 +810,7 @@ var BoltzSwapProvider = class {
680
810
  if (from === to) {
681
811
  throw new SwapError({ message: "Invalid chain pair" });
682
812
  }
683
- const response = await this.request(
684
- "/v2/swap/chain",
685
- "GET"
686
- );
813
+ const response = await this.request("/v2/swap/chain", "GET");
687
814
  if (!isGetChainPairsResponse(response))
688
815
  throw new SchemaError({ message: "error fetching limits" });
689
816
  if (!response[from]?.[to]) {
@@ -812,9 +939,7 @@ var BoltzSwapProvider = class {
812
939
  return await response.json();
813
940
  } catch (error) {
814
941
  if (error instanceof NetworkError) throw error;
815
- throw new NetworkError(
816
- `Request to ${url} failed: ${error.message}`
817
- );
942
+ throw new NetworkError(`Request to ${url} failed: ${error.message}`);
818
943
  }
819
944
  }
820
945
  };
@@ -837,8 +962,7 @@ var findKeyIndex = (keys, target) => keys.findIndex((k) => (0, import_utils.equa
837
962
  var assertPublicKeys = (keys) => {
838
963
  const seen = /* @__PURE__ */ new Set();
839
964
  for (const key of keys) {
840
- if (key.length !== 33)
841
- throw new Error(`public key must be 33 bytes, got ${key.length}`);
965
+ if (key.length !== 33) throw new Error(`public key must be 33 bytes, got ${key.length}`);
842
966
  const enc = import_base2.hex.encode(key);
843
967
  if (seen.has(enc)) throw new Error(`duplicate public key ${enc}`);
844
968
  seen.add(enc);
@@ -846,9 +970,7 @@ var assertPublicKeys = (keys) => {
846
970
  };
847
971
  var aggregateKeys = (publicKeys, tweak) => {
848
972
  assertPublicKeys([...publicKeys]);
849
- return (0, import_musig2.keyAggExport)(
850
- (0, import_musig2.keyAggregate)([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : [])
851
- );
973
+ return (0, import_musig2.keyAggExport)((0, import_musig2.keyAggregate)([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : []));
852
974
  };
853
975
  var MusigKeyAgg = class _MusigKeyAgg {
854
976
  constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, internalKey, _tweak) {
@@ -895,18 +1017,12 @@ var MusigWithMessage = class {
895
1017
  this.msg = msg;
896
1018
  }
897
1019
  generateNonce() {
898
- const nonce = (0, import_musig2.nonceGen)(
899
- this.myPublicKey,
900
- this.privateKey,
901
- this.aggPubkey,
902
- this.msg
903
- );
1020
+ const nonce = (0, import_musig2.nonceGen)(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
904
1021
  return new MusigWithNonce(
905
1022
  this.privateKey,
906
1023
  this.myPublicKey,
907
1024
  this.publicKeys,
908
1025
  this.myIndex,
909
- this.aggPubkey,
910
1026
  this.tweak,
911
1027
  this.msg,
912
1028
  nonce
@@ -914,12 +1030,11 @@ var MusigWithMessage = class {
914
1030
  }
915
1031
  };
916
1032
  var MusigWithNonce = class {
917
- constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, tweak, msg, nonce) {
1033
+ constructor(privateKey, myPublicKey, publicKeys, myIndex, tweak, msg, nonce) {
918
1034
  this.privateKey = privateKey;
919
1035
  this.myPublicKey = myPublicKey;
920
1036
  this.publicKeys = publicKeys;
921
1037
  this.myIndex = myIndex;
922
- this.aggPubkey = aggPubkey;
923
1038
  this.tweak = tweak;
924
1039
  this.msg = msg;
925
1040
  this.nonce = nonce;
@@ -951,10 +1066,8 @@ var MusigWithNonce = class {
951
1066
  const aggregatedNonce = (0, import_musig2.nonceAggregate)([...ordered]);
952
1067
  return new MusigNoncesAggregated(
953
1068
  this.privateKey,
954
- this.myPublicKey,
955
1069
  this.publicKeys,
956
1070
  this.myIndex,
957
- this.aggPubkey,
958
1071
  this.tweak,
959
1072
  this.msg,
960
1073
  this.nonce,
@@ -964,12 +1077,10 @@ var MusigWithNonce = class {
964
1077
  }
965
1078
  };
966
1079
  var MusigNoncesAggregated = class {
967
- constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, tweak, msg, nonce, pubNonces, aggregatedNonce) {
1080
+ constructor(privateKey, publicKeys, myIndex, tweak, msg, nonce, pubNonces, aggregatedNonce) {
968
1081
  this.privateKey = privateKey;
969
- this.myPublicKey = myPublicKey;
970
1082
  this.publicKeys = publicKeys;
971
1083
  this.myIndex = myIndex;
972
- this.aggPubkey = aggPubkey;
973
1084
  this.tweak = tweak;
974
1085
  this.msg = msg;
975
1086
  this.nonce = nonce;
@@ -1015,11 +1126,7 @@ var MusigSession = class {
1015
1126
  const index = typeof publicKeyOrIndex === "number" ? publicKeyOrIndex : findKeyIndex(this.publicKeys, publicKeyOrIndex);
1016
1127
  if (index < 0 || index >= this.publicKeys.length)
1017
1128
  throw new Error("public key not found or index out of range");
1018
- if (!this.session.partialSigVerify(
1019
- signature,
1020
- [...this.pubNonces],
1021
- index
1022
- )) {
1129
+ if (!this.session.partialSigVerify(signature, [...this.pubNonces], index)) {
1023
1130
  throw new Error("invalid partial signature");
1024
1131
  }
1025
1132
  this.partialSignatures[index] = signature;
@@ -1028,12 +1135,7 @@ var MusigSession = class {
1028
1135
  signPartial() {
1029
1136
  const sig = this.session.sign(this.nonce.secret, this.privateKey, true);
1030
1137
  this.partialSignatures[this.myIndex] = sig;
1031
- return new MusigSigned(
1032
- this.session,
1033
- [...this.partialSignatures],
1034
- sig,
1035
- this.nonce.public
1036
- );
1138
+ return new MusigSigned(this.session, [...this.partialSignatures], sig, this.nonce.public);
1037
1139
  }
1038
1140
  };
1039
1141
  var MusigSigned = class {
@@ -1047,14 +1149,11 @@ var MusigSigned = class {
1047
1149
  if (this.partialSignatures.some((s) => s === null)) {
1048
1150
  throw new Error("not all partial signatures are set");
1049
1151
  }
1050
- return this.session.partialSigAgg(
1051
- this.partialSignatures
1052
- );
1152
+ return this.session.partialSigAgg(this.partialSignatures);
1053
1153
  }
1054
1154
  };
1055
1155
  var create = (privateKey, publicKeys) => {
1056
- if (publicKeys.length < 2)
1057
- throw new Error("need at least 2 keys to aggregate");
1156
+ if (publicKeys.length < 2) throw new Error("need at least 2 keys to aggregate");
1058
1157
  const keys = [...publicKeys];
1059
1158
  assertPublicKeys(keys);
1060
1159
  Object.freeze(keys);
@@ -1062,14 +1161,7 @@ var create = (privateKey, publicKeys) => {
1062
1161
  const myIndex = findKeyIndex(keys, myPublicKey);
1063
1162
  if (myIndex === -1) throw new Error("our key is not in publicKeys");
1064
1163
  const aggPubkey = aggregateKeys(keys);
1065
- return new MusigKeyAgg(
1066
- privateKey,
1067
- myPublicKey,
1068
- keys,
1069
- myIndex,
1070
- aggPubkey,
1071
- aggPubkey
1072
- );
1164
+ return new MusigKeyAgg(privateKey, myPublicKey, keys, myIndex, aggPubkey, aggPubkey);
1073
1165
  };
1074
1166
 
1075
1167
  // src/utils/boltz-swap-tx.ts
@@ -1145,9 +1237,7 @@ var taprootHashTree = (tree) => {
1145
1237
  };
1146
1238
  var tweakMusig = (musig, tree) => {
1147
1239
  const tweak = taprootHashTree(tree).hash;
1148
- return musig.xonlyTweakAdd(
1149
- import_secp256k12.schnorr.utils.taggedHash("TapTweak", musig.aggPubkey, tweak)
1150
- );
1240
+ return musig.xonlyTweakAdd(import_secp256k12.schnorr.utils.taggedHash("TapTweak", musig.aggPubkey, tweak));
1151
1241
  };
1152
1242
  var toXOnly = (pubKey) => {
1153
1243
  if (pubKey.length === 32) return pubKey;
@@ -1159,9 +1249,7 @@ var toXOnly = (pubKey) => {
1159
1249
  }
1160
1250
  return pubKey.subarray(1, 33);
1161
1251
  }
1162
- throw new Error(
1163
- `Invalid public key length: expected 32 or 33 bytes, got ${pubKey.length}`
1164
- );
1252
+ throw new Error(`Invalid public key length: expected 32 or 33 bytes, got ${pubKey.length}`);
1165
1253
  };
1166
1254
  var p2trScript = (publicKey) => import_btc_signer.Script.encode(["OP_1", toXOnly(publicKey)]);
1167
1255
  var detectSwapOutput = (tweakedKey, transaction) => {
@@ -1176,8 +1264,7 @@ var detectSwapOutput = (tweakedKey, transaction) => {
1176
1264
  };
1177
1265
  var DUMMY_TAPROOT_SIGNATURE = new Uint8Array(64);
1178
1266
  var constructClaimTransaction = (utxo, destinationScript, fee) => {
1179
- if (fee < BigInt(0) || fee >= utxo.amount)
1180
- throw new Error("fee exceeds utxo amount");
1267
+ if (fee < BigInt(0) || fee >= utxo.amount) throw new Error("fee exceeds utxo amount");
1181
1268
  const tx = new import_btc_signer.Transaction({ version: 2 });
1182
1269
  tx.addOutput({
1183
1270
  amount: utxo.amount - fee,
@@ -1196,9 +1283,7 @@ var constructClaimTransaction = (utxo, destinationScript, fee) => {
1196
1283
  };
1197
1284
  var targetFee = (satPerVbyte, constructTx) => {
1198
1285
  const tx = constructTx(BigInt(1));
1199
- return constructTx(
1200
- BigInt(Math.ceil((tx.vsize + tx.inputsLength) * satPerVbyte))
1201
- );
1286
+ return constructTx(BigInt(Math.ceil((tx.vsize + tx.inputsLength) * satPerVbyte)));
1202
1287
  };
1203
1288
 
1204
1289
  // src/utils/decoding.ts
@@ -1206,9 +1291,7 @@ var import_light_bolt11_decoder = __toESM(require("light-bolt11-decoder"), 1);
1206
1291
  var import_sdk2 = require("@arkade-os/sdk");
1207
1292
  var decodeInvoice = (invoice) => {
1208
1293
  const decoded = import_light_bolt11_decoder.default.decode(invoice);
1209
- const millisats = Number(
1210
- decoded.sections.find((s) => s.name === "amount")?.value ?? "0"
1211
- );
1294
+ const millisats = Number(decoded.sections.find((s) => s.name === "amount")?.value ?? "0");
1212
1295
  return {
1213
1296
  expiry: decoded.expiry ?? 3600,
1214
1297
  amountSats: Math.floor(millisats / 1e3),
@@ -1271,10 +1354,7 @@ function extractTimeLockFromLeafOutput(scriptHex) {
1271
1354
  const data = opcodes[hasCSV - 1];
1272
1355
  if (data instanceof Uint8Array) {
1273
1356
  const dataBytes = new Uint8Array(data).reverse();
1274
- const {
1275
- blocks,
1276
- seconds
1277
- } = import_bip68.default.decode(
1357
+ const { blocks, seconds } = import_bip68.default.decode(
1278
1358
  parseInt(import_base5.hex.encode(dataBytes), 16)
1279
1359
  );
1280
1360
  return blocks ?? seconds ?? 0;
@@ -1307,6 +1387,13 @@ var SwapManager = class _SwapManager {
1307
1387
  * enough that a real "swap unknown to this provider" surfaces quickly.
1308
1388
  */
1309
1389
  static NOT_FOUND_THRESHOLD = 10;
1390
+ /**
1391
+ * Delay between re-attempts of a chain refund that left VTXOs deferred
1392
+ * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1393
+ * has elapsed). Boltz won't send another status update once the swap
1394
+ * is `swap.expired`, so the manager owns the local retry cadence.
1395
+ */
1396
+ static REFUND_RETRY_DELAY_MS = 6e4;
1310
1397
  swapProvider;
1311
1398
  config;
1312
1399
  // Event listeners storage (supports multiple listeners per event)
@@ -1325,6 +1412,11 @@ var SwapManager = class _SwapManager {
1325
1412
  reconnectTimer = null;
1326
1413
  initialPollTimer = null;
1327
1414
  pollRetryTimers = /* @__PURE__ */ new Map();
1415
+ // Per-swap retry timers for chain refunds that left work undone
1416
+ // (refundArk returned `skipped > 0`). The swap is held in
1417
+ // `monitoredSwaps` past its terminal Boltz status until the local
1418
+ // refund completes or the manager stops.
1419
+ refundRetryTimers = /* @__PURE__ */ new Map();
1328
1420
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1329
1421
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1330
1422
  // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
@@ -1378,9 +1470,7 @@ var SwapManager = class _SwapManager {
1378
1470
  this.wsConnectedListeners.add(config.events.onWebSocketConnected);
1379
1471
  }
1380
1472
  if (config.events?.onWebSocketDisconnected) {
1381
- this.wsDisconnectedListeners.add(
1382
- config.events.onWebSocketDisconnected
1383
- );
1473
+ this.wsDisconnectedListeners.add(config.events.onWebSocketDisconnected);
1384
1474
  }
1385
1475
  this.currentReconnectDelay = this.config.reconnectDelayMs;
1386
1476
  this.currentPollRetryDelay = this.config.pollRetryDelayMs;
@@ -1523,6 +1613,10 @@ var SwapManager = class _SwapManager {
1523
1613
  clearTimeout(timer);
1524
1614
  }
1525
1615
  this.pollRetryTimers.clear();
1616
+ for (const timer of this.refundRetryTimers.values()) {
1617
+ clearTimeout(timer);
1618
+ }
1619
+ this.refundRetryTimers.clear();
1526
1620
  this.notFoundCounts.clear();
1527
1621
  }
1528
1622
  /**
@@ -1531,9 +1625,7 @@ var SwapManager = class _SwapManager {
1531
1625
  */
1532
1626
  setPollInterval(ms) {
1533
1627
  if (ms <= 0) {
1534
- throw new RangeError(
1535
- `setPollInterval: ms must be a positive number, got ${ms}`
1536
- );
1628
+ throw new RangeError(`setPollInterval: ms must be a positive number, got ${ms}`);
1537
1629
  }
1538
1630
  const cappedInterval = Math.min(ms, this.config.maxPollIntervalMs);
1539
1631
  if (cappedInterval !== ms) {
@@ -1542,10 +1634,7 @@ var SwapManager = class _SwapManager {
1542
1634
  );
1543
1635
  }
1544
1636
  this.config.pollInterval = cappedInterval;
1545
- this.currentPollRetryDelay = Math.min(
1546
- cappedInterval,
1547
- this.config.pollRetryDelayMs
1548
- );
1637
+ this.currentPollRetryDelay = Math.min(cappedInterval, this.config.pollRetryDelayMs);
1549
1638
  if (this.isRunning) {
1550
1639
  if (this.usePollingFallback) {
1551
1640
  this.startPollingFallback();
@@ -1584,6 +1673,11 @@ var SwapManager = class _SwapManager {
1584
1673
  clearTimeout(retryTimer);
1585
1674
  this.pollRetryTimers.delete(swapId);
1586
1675
  }
1676
+ const refundRetryTimer = this.refundRetryTimers.get(swapId);
1677
+ if (refundRetryTimer) {
1678
+ clearTimeout(refundRetryTimer);
1679
+ this.refundRetryTimers.delete(swapId);
1680
+ }
1587
1681
  this.notFoundCounts.delete(swapId);
1588
1682
  logger.log(`Removed swap ${swapId} from monitoring`);
1589
1683
  }
@@ -1630,9 +1724,7 @@ var SwapManager = class _SwapManager {
1630
1724
  }
1631
1725
  if (this.isFinalStatus(swap)) {
1632
1726
  if (isPendingReverseSwap(swap)) {
1633
- const response = await this.swapProvider.getReverseSwapTxId(
1634
- swap.id
1635
- );
1727
+ const response = await this.swapProvider.getReverseSwapTxId(swap.id);
1636
1728
  return { txid: response.id };
1637
1729
  }
1638
1730
  if (isPendingSubmarineSwap(swap)) {
@@ -1649,31 +1741,19 @@ var SwapManager = class _SwapManager {
1649
1741
  if (updatedSwap.status === "invoice.settled") {
1650
1742
  this.swapProvider.getReverseSwapTxId(updatedSwap.id).then((response) => resolve({ txid: response.id })).catch((error) => reject(error));
1651
1743
  } else {
1652
- reject(
1653
- new Error(
1654
- `Swap failed with status: ${updatedSwap.status}`
1655
- )
1656
- );
1744
+ reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
1657
1745
  }
1658
1746
  } else if (isPendingSubmarineSwap(updatedSwap)) {
1659
1747
  if (updatedSwap.status === "transaction.claimed") {
1660
1748
  resolve({ txid: updatedSwap.id });
1661
1749
  } else {
1662
- reject(
1663
- new Error(
1664
- `Swap failed with status: ${updatedSwap.status}`
1665
- )
1666
- );
1750
+ reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
1667
1751
  }
1668
1752
  } else if (isPendingChainSwap(updatedSwap)) {
1669
1753
  if (updatedSwap.status === "transaction.claimed") {
1670
1754
  resolve({ txid: updatedSwap.id });
1671
1755
  } else {
1672
- reject(
1673
- new Error(
1674
- `Swap failed with status: ${updatedSwap.status}`
1675
- )
1676
- );
1756
+ reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
1677
1757
  }
1678
1758
  }
1679
1759
  };
@@ -1716,16 +1796,12 @@ var SwapManager = class _SwapManager {
1716
1796
  const connectionTimeout = setTimeout(() => {
1717
1797
  logger.error("WebSocket connection timeout");
1718
1798
  this.websocket?.close();
1719
- this.enterPollingFallback(
1720
- new NetworkError("WebSocket connection failed")
1721
- );
1799
+ this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
1722
1800
  }, 1e4);
1723
1801
  this.websocket.onerror = (error) => {
1724
1802
  clearTimeout(connectionTimeout);
1725
1803
  logger.error("WebSocket error:", error);
1726
- this.enterPollingFallback(
1727
- new NetworkError("WebSocket connection failed")
1728
- );
1804
+ this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
1729
1805
  };
1730
1806
  this.websocket.onopen = () => {
1731
1807
  clearTimeout(connectionTimeout);
@@ -1765,9 +1841,7 @@ var SwapManager = class _SwapManager {
1765
1841
  };
1766
1842
  } catch (error) {
1767
1843
  logger.error("Failed to create WebSocket:", error);
1768
- this.enterPollingFallback(
1769
- new NetworkError("WebSocket connection failed")
1770
- );
1844
+ this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
1771
1845
  }
1772
1846
  }
1773
1847
  /**
@@ -1802,11 +1876,8 @@ var SwapManager = class _SwapManager {
1802
1876
  * Schedule WebSocket reconnection with exponential backoff
1803
1877
  */
1804
1878
  scheduleReconnect() {
1805
- if (this.reconnectTimer || this.webSocketUnavailable || !this.hasWebSocketSupport())
1806
- return;
1807
- logger.log(
1808
- `Scheduling WebSocket reconnect in ${this.currentReconnectDelay}ms`
1809
- );
1879
+ if (this.reconnectTimer || this.webSocketUnavailable || !this.hasWebSocketSupport()) return;
1880
+ logger.log(`Scheduling WebSocket reconnect in ${this.currentReconnectDelay}ms`);
1810
1881
  this.reconnectTimer = setTimeout(() => {
1811
1882
  this.reconnectTimer = null;
1812
1883
  this.isReconnecting = false;
@@ -1821,8 +1892,7 @@ var SwapManager = class _SwapManager {
1821
1892
  * Subscribe to a specific swap ID on the WebSocket
1822
1893
  */
1823
1894
  subscribeToSwap(swapId) {
1824
- if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN)
1825
- return;
1895
+ if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) return;
1826
1896
  this.websocket.send(
1827
1897
  JSON.stringify({
1828
1898
  op: "subscribe",
@@ -1845,9 +1915,7 @@ var SwapManager = class _SwapManager {
1845
1915
  if (msg.args[0].error) {
1846
1916
  logger.error(`Swap ${swapId} error:`, msg.args[0].error);
1847
1917
  const error = new Error(msg.args[0].error);
1848
- this.swapFailedListeners.forEach(
1849
- (listener) => listener(swap, error)
1850
- );
1918
+ this.swapFailedListeners.forEach((listener) => listener(swap, error));
1851
1919
  return;
1852
1920
  }
1853
1921
  const newStatus = msg.args[0].status;
@@ -1865,19 +1933,14 @@ var SwapManager = class _SwapManager {
1865
1933
  const oldStatus = swap.status;
1866
1934
  if (oldStatus === newStatus) return;
1867
1935
  swap.status = newStatus;
1868
- this.swapUpdateListeners.forEach(
1869
- (listener) => listener(swap, oldStatus)
1870
- );
1936
+ this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
1871
1937
  const subscribers = this.swapSubscriptions.get(swap.id);
1872
1938
  if (subscribers) {
1873
1939
  subscribers.forEach((callback) => {
1874
1940
  try {
1875
1941
  callback(swap, oldStatus);
1876
1942
  } catch (error) {
1877
- logger.error(
1878
- `Error in swap subscription callback for ${swap.id}:`,
1879
- error
1880
- );
1943
+ logger.error(`Error in swap subscription callback for ${swap.id}:`, error);
1881
1944
  }
1882
1945
  });
1883
1946
  }
@@ -1886,15 +1949,57 @@ var SwapManager = class _SwapManager {
1886
1949
  await this.executeAutonomousAction(swap);
1887
1950
  }
1888
1951
  if (this.isFinalStatus(swap)) {
1889
- this.monitoredSwaps.delete(swap.id);
1890
- this.swapSubscriptions.delete(swap.id);
1891
- const retryTimer = this.pollRetryTimers.get(swap.id);
1892
- if (retryTimer) {
1893
- clearTimeout(retryTimer);
1894
- this.pollRetryTimers.delete(swap.id);
1952
+ if (this.refundRetryTimers.has(swap.id)) {
1953
+ return;
1895
1954
  }
1896
- this.swapCompletedListeners.forEach((listener) => listener(swap));
1955
+ this.finalizeMonitoredSwap(swap);
1956
+ }
1957
+ }
1958
+ /**
1959
+ * Drop a swap from monitoring and emit the terminal completion event.
1960
+ * Shared between the on-status-update finalization path and the
1961
+ * refund-retry finalization path (used when a previously-deferred
1962
+ * chain refund has finished its remaining work).
1963
+ */
1964
+ finalizeMonitoredSwap(swap) {
1965
+ if (!this.monitoredSwaps.has(swap.id)) return;
1966
+ this.monitoredSwaps.delete(swap.id);
1967
+ this.swapSubscriptions.delete(swap.id);
1968
+ const retryTimer = this.pollRetryTimers.get(swap.id);
1969
+ if (retryTimer) {
1970
+ clearTimeout(retryTimer);
1971
+ this.pollRetryTimers.delete(swap.id);
1972
+ }
1973
+ const refundRetry = this.refundRetryTimers.get(swap.id);
1974
+ if (refundRetry) {
1975
+ clearTimeout(refundRetry);
1976
+ this.refundRetryTimers.delete(swap.id);
1897
1977
  }
1978
+ this.swapCompletedListeners.forEach((listener) => listener(swap));
1979
+ }
1980
+ /**
1981
+ * Schedule another `executeAutonomousAction` run for a chain swap whose
1982
+ * refund left VTXOs deferred. After the retry completes, if no further
1983
+ * deferral was reported, finalize monitoring cleanup.
1984
+ */
1985
+ scheduleRefundRetry(swap, delayMs) {
1986
+ const existing = this.refundRetryTimers.get(swap.id);
1987
+ if (existing) clearTimeout(existing);
1988
+ this.refundRetryTimers.set(
1989
+ swap.id,
1990
+ setTimeout(async () => {
1991
+ this.refundRetryTimers.delete(swap.id);
1992
+ if (!this.isRunning) return;
1993
+ if (!this.monitoredSwaps.has(swap.id)) return;
1994
+ try {
1995
+ await this.executeAutonomousAction(swap);
1996
+ } finally {
1997
+ if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
1998
+ this.finalizeMonitoredSwap(swap);
1999
+ }
2000
+ }
2001
+ }, delayMs)
2002
+ );
1898
2003
  }
1899
2004
  /**
1900
2005
  * Execute autonomous action based on swap status
@@ -1902,9 +2007,7 @@ var SwapManager = class _SwapManager {
1902
2007
  */
1903
2008
  async executeAutonomousAction(swap) {
1904
2009
  if (this.swapsInProgress.has(swap.id)) {
1905
- logger.log(
1906
- `Swap ${swap.id} is already being processed, skipping autonomous action`
1907
- );
2010
+ logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
1908
2011
  return;
1909
2012
  }
1910
2013
  try {
@@ -1919,9 +2022,7 @@ var SwapManager = class _SwapManager {
1919
2022
  if (isReverseClaimableStatus(swap.status)) {
1920
2023
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
1921
2024
  await this.executeClaimAction(swap);
1922
- this.actionExecutedListeners.forEach(
1923
- (listener) => listener(swap, "claim")
1924
- );
2025
+ this.actionExecutedListeners.forEach((listener) => listener(swap, "claim"));
1925
2026
  }
1926
2027
  } else if (isPendingSubmarineSwap(swap)) {
1927
2028
  if (!swap.request?.invoice || swap.request.invoice.length === 0) {
@@ -1933,9 +2034,7 @@ var SwapManager = class _SwapManager {
1933
2034
  if (isSubmarineRefundableStatus(swap.status)) {
1934
2035
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
1935
2036
  await this.executeRefundAction(swap);
1936
- this.actionExecutedListeners.forEach(
1937
- (listener) => listener(swap, "refund")
1938
- );
2037
+ this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
1939
2038
  }
1940
2039
  } else if (isPendingChainSwap(swap)) {
1941
2040
  if (isChainClaimableStatus(swap.status)) {
@@ -1955,10 +2054,27 @@ var SwapManager = class _SwapManager {
1955
2054
  } else if (isChainRefundableStatus(swap.status)) {
1956
2055
  if (swap.request.from === "ARK") {
1957
2056
  logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
1958
- await this.executeRefundArkAction(swap);
1959
- this.actionExecutedListeners.forEach(
1960
- (listener) => listener(swap, "refundArk")
1961
- );
2057
+ try {
2058
+ const outcome = await this.executeRefundArkAction(swap);
2059
+ if (outcome && outcome.skipped > 0) {
2060
+ logger.log(
2061
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2062
+ );
2063
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2064
+ }
2065
+ this.actionExecutedListeners.forEach(
2066
+ (listener) => listener(swap, "refundArk")
2067
+ );
2068
+ } catch (error) {
2069
+ logger.error(
2070
+ `Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
2071
+ error
2072
+ );
2073
+ this.swapFailedListeners.forEach(
2074
+ (listener) => listener(swap, error)
2075
+ );
2076
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2077
+ }
1962
2078
  }
1963
2079
  if (swap.request.from === "BTC") {
1964
2080
  logger.warn(
@@ -1985,13 +2101,8 @@ var SwapManager = class _SwapManager {
1985
2101
  }
1986
2102
  }
1987
2103
  } catch (error) {
1988
- logger.error(
1989
- `Failed to execute autonomous action for swap ${swap.id}:`,
1990
- error
1991
- );
1992
- this.swapFailedListeners.forEach(
1993
- (listener) => listener(swap, error)
1994
- );
2104
+ logger.error(`Failed to execute autonomous action for swap ${swap.id}:`, error);
2105
+ this.swapFailedListeners.forEach((listener) => listener(swap, error));
1995
2106
  } finally {
1996
2107
  this.swapsInProgress.delete(swap.id);
1997
2108
  }
@@ -2044,7 +2155,7 @@ var SwapManager = class _SwapManager {
2044
2155
  logger.error("refundArk callback not set");
2045
2156
  return;
2046
2157
  }
2047
- await this.refundArkCallback(swap);
2158
+ return this.refundArkCallback(swap);
2048
2159
  }
2049
2160
  /**
2050
2161
  * Execute sign server claim action for chain swap.
@@ -2092,9 +2203,7 @@ var SwapManager = class _SwapManager {
2092
2203
  logger.log(`Resuming chain refund for swap ${swap.id}`);
2093
2204
  await this.executeAutonomousAction(swap);
2094
2205
  } else if (isPendingChainSwap(swap) && swap.request.to === "ARK" && isChainSignableStatus(swap.status)) {
2095
- logger.log(
2096
- `Resuming server claim signing for swap ${swap.id}`
2097
- );
2206
+ logger.log(`Resuming server claim signing for swap ${swap.id}`);
2098
2207
  await this.executeAutonomousAction(swap);
2099
2208
  }
2100
2209
  } catch (error) {
@@ -2146,16 +2255,12 @@ var SwapManager = class _SwapManager {
2146
2255
  */
2147
2256
  async pollAllSwaps() {
2148
2257
  if (this.monitoredSwaps.size === 0) return;
2149
- const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2150
- (swap) => this.pollSingleSwap(swap)
2151
- );
2258
+ const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
2152
2259
  await Promise.allSettled(pollPromises);
2153
2260
  }
2154
2261
  async pollSingleSwap(swap) {
2155
2262
  try {
2156
- const statusResponse = await this.swapProvider.getSwapStatus(
2157
- swap.id
2158
- );
2263
+ const statusResponse = await this.swapProvider.getSwapStatus(swap.id);
2159
2264
  this.notFoundCounts.delete(swap.id);
2160
2265
  if (statusResponse.status !== swap.status) {
2161
2266
  await this.handleSwapStatusUpdate(swap, statusResponse.status);
@@ -2166,9 +2271,7 @@ var SwapManager = class _SwapManager {
2166
2271
  return;
2167
2272
  }
2168
2273
  if (error instanceof NetworkError && error.statusCode === 429) {
2169
- logger.warn(
2170
- `Rate-limited polling swap ${swap.id}, retrying in 2s`
2171
- );
2274
+ logger.warn(`Rate-limited polling swap ${swap.id}, retrying in 2s`);
2172
2275
  const existing = this.pollRetryTimers.get(swap.id);
2173
2276
  if (existing) clearTimeout(existing);
2174
2277
  this.pollRetryTimers.set(
@@ -2176,25 +2279,17 @@ var SwapManager = class _SwapManager {
2176
2279
  setTimeout(async () => {
2177
2280
  this.pollRetryTimers.delete(swap.id);
2178
2281
  try {
2179
- const retry = await this.swapProvider.getSwapStatus(
2180
- swap.id
2181
- );
2282
+ const retry = await this.swapProvider.getSwapStatus(swap.id);
2182
2283
  this.notFoundCounts.delete(swap.id);
2183
2284
  if (retry.status !== swap.status) {
2184
- await this.handleSwapStatusUpdate(
2185
- swap,
2186
- retry.status
2187
- );
2285
+ await this.handleSwapStatusUpdate(swap, retry.status);
2188
2286
  }
2189
2287
  } catch (retryError) {
2190
2288
  if (retryError instanceof SwapNotFoundError) {
2191
2289
  await this.handleSwapNotFound(swap);
2192
2290
  return;
2193
2291
  }
2194
- logger.error(
2195
- `Retry poll for swap ${swap.id} also failed:`,
2196
- retryError
2197
- );
2292
+ logger.error(`Retry poll for swap ${swap.id} also failed:`, retryError);
2198
2293
  }
2199
2294
  }, 2e3)
2200
2295
  );
@@ -2213,6 +2308,7 @@ var SwapManager = class _SwapManager {
2213
2308
  * Boltz endpoint).
2214
2309
  */
2215
2310
  async handleSwapNotFound(swap) {
2311
+ if (this.refundRetryTimers.has(swap.id)) return;
2216
2312
  const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2217
2313
  this.notFoundCounts.set(swap.id, count);
2218
2314
  logger.warn(
@@ -2233,6 +2329,7 @@ var SwapManager = class _SwapManager {
2233
2329
  * 404s without recovering anything.
2234
2330
  */
2235
2331
  async markSwapAsUnknownToProvider(swap) {
2332
+ if (this.refundRetryTimers.has(swap.id)) return;
2236
2333
  if (!this.monitoredSwaps.has(swap.id)) {
2237
2334
  this.notFoundCounts.delete(swap.id);
2238
2335
  return;
@@ -2245,10 +2342,13 @@ var SwapManager = class _SwapManager {
2245
2342
  clearTimeout(retryTimer);
2246
2343
  this.pollRetryTimers.delete(swap.id);
2247
2344
  }
2345
+ const refundRetryTimer = this.refundRetryTimers.get(swap.id);
2346
+ if (refundRetryTimer) {
2347
+ clearTimeout(refundRetryTimer);
2348
+ this.refundRetryTimers.delete(swap.id);
2349
+ }
2248
2350
  this.notFoundCounts.delete(swap.id);
2249
- this.swapUpdateListeners.forEach(
2250
- (listener) => listener(swap, oldStatus)
2251
- );
2351
+ this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
2252
2352
  const subscribers = this.swapSubscriptions.get(swap.id);
2253
2353
  if (subscribers) {
2254
2354
  subscribers.forEach((callback) => {
@@ -2313,9 +2413,7 @@ async function saveSwap(swap, saver) {
2313
2413
  if (saver.saveSubmarineSwap) {
2314
2414
  await saver.saveSubmarineSwap(swap);
2315
2415
  } else {
2316
- console.warn(
2317
- "No saveSubmarineSwap handler provided, swap not saved"
2318
- );
2416
+ console.warn("No saveSubmarineSwap handler provided, swap not saved");
2319
2417
  }
2320
2418
  } else if (isPendingChainSwap(swap)) {
2321
2419
  if (saver.saveChainSwap) {
@@ -2372,6 +2470,26 @@ function enrichSubmarineSwapInvoice(swap, invoice) {
2372
2470
  return swap;
2373
2471
  }
2374
2472
 
2473
+ // src/repositories/swap-repository.ts
2474
+ function hasImpossibleSwapsFilter(filter) {
2475
+ if (!filter) return false;
2476
+ return Array.isArray(filter.id) && filter.id.length === 0 || Array.isArray(filter.status) && filter.status.length === 0 || Array.isArray(filter.type) && filter.type.length === 0;
2477
+ }
2478
+ function matchesCriterion(value, criterion) {
2479
+ if (criterion === void 0) return true;
2480
+ return Array.isArray(criterion) ? criterion.includes(value) : value === criterion;
2481
+ }
2482
+ function applySwapsFilter(swaps, filter) {
2483
+ return swaps.filter(
2484
+ (swap) => !!swap && matchesCriterion(swap.id, filter.id) && matchesCriterion(swap.status, filter.status) && matchesCriterion(swap.type, filter.type)
2485
+ );
2486
+ }
2487
+ function applyCreatedAtOrder(swaps, filter) {
2488
+ if (filter?.orderBy !== "createdAt") return swaps;
2489
+ const direction = filter.orderDirection === "asc" ? 1 : -1;
2490
+ return swaps.slice().sort((a, b) => (a.createdAt - b.createdAt) * direction);
2491
+ }
2492
+
2375
2493
  // src/repositories/IndexedDb/swap-repository.ts
2376
2494
  var import_sdk4 = require("@arkade-os/sdk");
2377
2495
  var DEFAULT_DB_NAME = "arkade-boltz-swap";
@@ -2387,6 +2505,10 @@ function initDatabase(db) {
2387
2505
  swapStore.createIndex("createdAt", "createdAt", { unique: false });
2388
2506
  }
2389
2507
  }
2508
+ function asArray(v) {
2509
+ if (v === void 0) return void 0;
2510
+ return Array.isArray(v) ? v : [v];
2511
+ }
2390
2512
  var IndexedDbSwapRepository = class {
2391
2513
  constructor(dbName = DEFAULT_DB_NAME) {
2392
2514
  this.dbName = dbName;
@@ -2401,10 +2523,7 @@ var IndexedDbSwapRepository = class {
2401
2523
  async saveSwap(swap) {
2402
2524
  const db = await this.getDB();
2403
2525
  return new Promise((resolve, reject) => {
2404
- const transaction = db.transaction(
2405
- [STORE_SWAPS_STATE],
2406
- "readwrite"
2407
- );
2526
+ const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
2408
2527
  const store = transaction.objectStore(STORE_SWAPS_STATE);
2409
2528
  const request = store.put(swap);
2410
2529
  request.onsuccess = () => resolve();
@@ -2414,10 +2533,7 @@ var IndexedDbSwapRepository = class {
2414
2533
  async deleteSwap(id) {
2415
2534
  const db = await this.getDB();
2416
2535
  return new Promise((resolve, reject) => {
2417
- const transaction = db.transaction(
2418
- [STORE_SWAPS_STATE],
2419
- "readwrite"
2420
- );
2536
+ const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
2421
2537
  const store = transaction.objectStore(STORE_SWAPS_STATE);
2422
2538
  const request = store.delete(id);
2423
2539
  request.onsuccess = () => resolve();
@@ -2430,10 +2546,7 @@ var IndexedDbSwapRepository = class {
2430
2546
  async clear() {
2431
2547
  const db = await this.getDB();
2432
2548
  return new Promise((resolve, reject) => {
2433
- const transaction = db.transaction(
2434
- [STORE_SWAPS_STATE],
2435
- "readwrite"
2436
- );
2549
+ const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
2437
2550
  const store = transaction.objectStore(STORE_SWAPS_STATE);
2438
2551
  const request = store.clear();
2439
2552
  request.onsuccess = () => resolve();
@@ -2450,11 +2563,10 @@ var IndexedDbSwapRepository = class {
2450
2563
  request.onsuccess = () => resolve(request.result ?? []);
2451
2564
  })
2452
2565
  );
2453
- return Promise.all(requests).then(
2454
- (results) => results.flatMap((result) => result)
2455
- );
2566
+ return Promise.all(requests).then((results) => results.flatMap((result) => result));
2456
2567
  }
2457
2568
  async getAllSwapsFromStore(filter) {
2569
+ if (hasImpossibleSwapsFilter(filter)) return [];
2458
2570
  const db = await this.getDB();
2459
2571
  const store = db.transaction([STORE_SWAPS_STATE], "readonly").objectStore(STORE_SWAPS_STATE);
2460
2572
  if (!filter || Object.keys(filter).length === 0) {
@@ -2464,9 +2576,8 @@ var IndexedDbSwapRepository = class {
2464
2576
  request.onerror = () => reject(request.error);
2465
2577
  });
2466
2578
  }
2467
- const normalizedFilter = normalizeFilter(filter);
2468
- if (normalizedFilter.has("id")) {
2469
- const ids = normalizedFilter.get("id");
2579
+ const ids = asArray(filter.id);
2580
+ if (ids) {
2470
2581
  const swaps = await Promise.all(
2471
2582
  ids.map(
2472
2583
  (id) => new Promise((resolve, reject) => {
@@ -2476,34 +2587,17 @@ var IndexedDbSwapRepository = class {
2476
2587
  })
2477
2588
  )
2478
2589
  );
2479
- return this.sortIfNeeded(
2480
- this.applySwapsFilter(swaps, normalizedFilter),
2481
- filter
2482
- );
2590
+ return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
2483
2591
  }
2484
- if (normalizedFilter.has("type")) {
2485
- const types = normalizedFilter.get("type");
2486
- const swaps = await this.getSwapsByIndexValues(
2487
- store,
2488
- "type",
2489
- types
2490
- );
2491
- return this.sortIfNeeded(
2492
- this.applySwapsFilter(swaps, normalizedFilter),
2493
- filter
2494
- );
2592
+ const types = asArray(filter.type);
2593
+ if (types) {
2594
+ const swaps = await this.getSwapsByIndexValues(store, "type", types);
2595
+ return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
2495
2596
  }
2496
- if (normalizedFilter.has("status")) {
2497
- const ids = normalizedFilter.get("status");
2498
- const swaps = await this.getSwapsByIndexValues(
2499
- store,
2500
- "status",
2501
- ids
2502
- );
2503
- return this.sortIfNeeded(
2504
- this.applySwapsFilter(swaps, normalizedFilter),
2505
- filter
2506
- );
2597
+ const statuses = asArray(filter.status);
2598
+ if (statuses) {
2599
+ const swaps = await this.getSwapsByIndexValues(store, "status", statuses);
2600
+ return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
2507
2601
  }
2508
2602
  if (filter.orderBy === "createdAt") {
2509
2603
  return this.getAllSwapsByCreatedAt(store, filter.orderDirection);
@@ -2513,22 +2607,7 @@ var IndexedDbSwapRepository = class {
2513
2607
  request.onsuccess = () => resolve(request.result ?? []);
2514
2608
  request.onerror = () => reject(request.error);
2515
2609
  });
2516
- return this.sortIfNeeded(
2517
- this.applySwapsFilter(allSwaps, normalizedFilter),
2518
- filter
2519
- );
2520
- }
2521
- applySwapsFilter(swaps, filter) {
2522
- return swaps.filter((swap) => {
2523
- if (swap === void 0) return false;
2524
- if (filter.has("id") && !filter.get("id")?.includes(swap.id))
2525
- return false;
2526
- if (filter.has("status") && !filter.get("status")?.includes(swap.status))
2527
- return false;
2528
- if (filter.has("type") && !filter.get("type")?.includes(swap.type))
2529
- return false;
2530
- return true;
2531
- });
2610
+ return applyCreatedAtOrder(applySwapsFilter(allSwaps, filter), filter);
2532
2611
  }
2533
2612
  async getAllSwapsByCreatedAt(store, orderDirection) {
2534
2613
  const index = store.index("createdAt");
@@ -2548,30 +2627,12 @@ var IndexedDbSwapRepository = class {
2548
2627
  };
2549
2628
  });
2550
2629
  }
2551
- sortIfNeeded(swaps, filter) {
2552
- if (filter?.orderBy !== "createdAt") return swaps;
2553
- const direction = filter.orderDirection === "asc" ? 1 : -1;
2554
- return swaps.slice().sort((a, b) => (a.createdAt - b.createdAt) * direction);
2555
- }
2556
2630
  async [Symbol.asyncDispose]() {
2557
2631
  if (!this.db) return;
2558
2632
  await (0, import_sdk4.closeDatabase)(this.dbName);
2559
2633
  this.db = null;
2560
2634
  }
2561
2635
  };
2562
- var FILTER_FIELDS = ["id", "status", "type"];
2563
- function normalizeFilter(filter) {
2564
- const res = /* @__PURE__ */ new Map();
2565
- FILTER_FIELDS.forEach((current) => {
2566
- if (!filter?.[current]) return;
2567
- if (Array.isArray(filter[current])) {
2568
- res.set(current, filter[current]);
2569
- } else {
2570
- res.set(current, [filter[current]]);
2571
- }
2572
- });
2573
- return res;
2574
- }
2575
2636
 
2576
2637
  // src/utils/identity.ts
2577
2638
  var import_sdk5 = require("@arkade-os/sdk");
@@ -2583,13 +2644,8 @@ function claimVHTLCIdentity(identity, preimage) {
2583
2644
  let signedTx = await identity.sign(cpy, inputIndexes);
2584
2645
  signedTx = import_sdk5.Transaction.fromPSBT(signedTx.toPSBT());
2585
2646
  if (preimage) {
2586
- for (const inputIndex of inputIndexes || Array.from(
2587
- { length: signedTx.inputsLength },
2588
- (_, i) => i
2589
- )) {
2590
- (0, import_sdk5.setArkPsbtField)(signedTx, inputIndex, import_sdk5.ConditionWitness, [
2591
- preimage
2592
- ]);
2647
+ for (const inputIndex of inputIndexes || Array.from({ length: signedTx.inputsLength }, (_, i) => i)) {
2648
+ (0, import_sdk5.setArkPsbtField)(signedTx, inputIndex, import_sdk5.ConditionWitness, [preimage]);
2593
2649
  }
2594
2650
  }
2595
2651
  return signedTx;
@@ -2644,17 +2700,13 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2644
2700
  if (!sweepTapTreeRoot) {
2645
2701
  throw new Error("Sweep tap tree root not set");
2646
2702
  }
2647
- const xOnlyPublicKeys = event.cosignersPublicKeys.map(
2648
- (k) => k.slice(2)
2649
- );
2703
+ const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
2650
2704
  const signerPublicKey = await session.getPublicKey();
2651
2705
  const xonlySignerPublicKey = signerPublicKey.subarray(1);
2652
2706
  if (!xOnlyPublicKeys.includes(import_base7.hex.encode(xonlySignerPublicKey))) {
2653
2707
  return { skip: true };
2654
2708
  }
2655
- const commitmentTx = import_sdk6.Transaction.fromPSBT(
2656
- import_base7.base64.decode(event.unsignedCommitmentTx)
2657
- );
2709
+ const commitmentTx = import_sdk6.Transaction.fromPSBT(import_base7.base64.decode(event.unsignedCommitmentTx));
2658
2710
  (0, import_sdk6.validateVtxoTxGraph)(vtxoTree, commitmentTx, sweepTapTreeRoot);
2659
2711
  const sharedOutput = commitmentTx.getOutput(0);
2660
2712
  if (!sharedOutput?.amount) {
@@ -2670,18 +2722,11 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2670
2722
  if (!session) {
2671
2723
  return { fullySigned: true };
2672
2724
  }
2673
- const { hasAllNonces } = await session.aggregatedNonces(
2674
- event.txid,
2675
- event.nonces
2676
- );
2725
+ const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
2677
2726
  if (!hasAllNonces) return { fullySigned: false };
2678
2727
  const signatures = await session.sign();
2679
2728
  const pubkey = import_base7.hex.encode(await session.getPublicKey());
2680
- await arkProvider.submitTreeSignatures(
2681
- event.id,
2682
- pubkey,
2683
- signatures
2684
- );
2729
+ await arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
2685
2730
  return { fullySigned: true };
2686
2731
  },
2687
2732
  onBatchFinalization: async (event, _, connectorTree) => {
@@ -2689,9 +2734,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2689
2734
  return;
2690
2735
  }
2691
2736
  if (!connectorTree) {
2692
- throw new Error(
2693
- "BatchFinalizationEvent: expected connector tree to be defined"
2694
- );
2737
+ throw new Error("BatchFinalizationEvent: expected connector tree to be defined");
2695
2738
  }
2696
2739
  (0, import_sdk6.validateConnectorsTxGraph)(event.commitmentTx, connectorTree);
2697
2740
  const connectors = connectorTree.leaves();
@@ -2706,9 +2749,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2706
2749
  connectors[connectorIndex]
2707
2750
  );
2708
2751
  const signedForfeitTx = await identity.sign(forfeitTx);
2709
- await arkProvider.submitSignedForfeitTxs([
2710
- import_base7.base64.encode(signedForfeitTx.toPSBT())
2711
- ]);
2752
+ await arkProvider.submitSignedForfeitTxs([import_base7.base64.encode(signedForfeitTx.toPSBT())]);
2712
2753
  }
2713
2754
  };
2714
2755
  }
@@ -2768,36 +2809,22 @@ var createVHTLCScript = (args) => {
2768
2809
  serverPubkey,
2769
2810
  timeoutBlockHeights: vhtlcTimeouts
2770
2811
  } = args;
2771
- const receiverXOnlyPublicKey = normalizeToXOnlyKey(
2772
- import_base8.hex.decode(receiverPubkey),
2773
- "receiver"
2774
- );
2775
- const senderXOnlyPublicKey = normalizeToXOnlyKey(
2776
- import_base8.hex.decode(senderPubkey),
2777
- "sender"
2778
- );
2779
- const serverXOnlyPublicKey = normalizeToXOnlyKey(
2780
- import_base8.hex.decode(serverPubkey),
2781
- "server"
2782
- );
2812
+ const receiverXOnlyPublicKey = normalizeToXOnlyKey(import_base8.hex.decode(receiverPubkey), "receiver");
2813
+ const senderXOnlyPublicKey = normalizeToXOnlyKey(import_base8.hex.decode(senderPubkey), "sender");
2814
+ const serverXOnlyPublicKey = normalizeToXOnlyKey(import_base8.hex.decode(serverPubkey), "server");
2783
2815
  const vhtlcScript = new import_sdk7.VHTLC.Script({
2784
2816
  preimageHash: (0, import_legacy.ripemd160)(preimageHash),
2785
2817
  sender: senderXOnlyPublicKey,
2786
2818
  receiver: receiverXOnlyPublicKey,
2787
2819
  server: serverXOnlyPublicKey,
2788
2820
  refundLocktime: BigInt(vhtlcTimeouts.refund),
2789
- unilateralClaimDelay: toBip68RelativeTimelock(
2790
- vhtlcTimeouts.unilateralClaim
2791
- ),
2792
- unilateralRefundDelay: toBip68RelativeTimelock(
2793
- vhtlcTimeouts.unilateralRefund
2794
- ),
2821
+ unilateralClaimDelay: toBip68RelativeTimelock(vhtlcTimeouts.unilateralClaim),
2822
+ unilateralRefundDelay: toBip68RelativeTimelock(vhtlcTimeouts.unilateralRefund),
2795
2823
  unilateralRefundWithoutReceiverDelay: toBip68RelativeTimelock(
2796
2824
  vhtlcTimeouts.unilateralRefundWithoutReceiver
2797
2825
  )
2798
2826
  });
2799
- if (!vhtlcScript.claimScript)
2800
- throw new Error("Failed to create VHTLC script");
2827
+ if (!vhtlcScript.claimScript) throw new Error("Failed to create VHTLC script");
2801
2828
  const hrp = network === "bitcoin" ? "ark" : "tark";
2802
2829
  const vhtlcAddress = vhtlcScript.address(hrp, serverXOnlyPublicKey).encode();
2803
2830
  return { vhtlcScript, vhtlcAddress };
@@ -2831,11 +2858,7 @@ var joinBatch = async (arkProvider, identity, input, output, {
2831
2858
  unknown: [import_sdk7.VtxoTaprootTree.encode(input.tapTree)],
2832
2859
  sequence: (0, import_sdk7.getSequence)(input.tapLeafScript)
2833
2860
  };
2834
- const registerIntent = import_sdk7.Intent.create(
2835
- intentMessage,
2836
- [intentInput],
2837
- [output]
2838
- );
2861
+ const registerIntent = import_sdk7.Intent.create(intentMessage, [intentInput], [output]);
2839
2862
  const deleteIntent = import_sdk7.Intent.create(deleteMessage, [intentInput]);
2840
2863
  const [signedRegisterIntent, signedDeleteIntent] = await Promise.all([
2841
2864
  identity.sign(registerIntent),
@@ -2859,14 +2882,8 @@ var joinBatch = async (arkProvider, identity, input, output, {
2859
2882
  normalizeToXOnlyKey(forfeitPubkey, "forfeit"),
2860
2883
  isRecoverable2 ? void 0 : import_btc_signer4.OutScript.encode(decodedAddress)
2861
2884
  );
2862
- const topics = [
2863
- import_base8.hex.encode(signerPublicKey),
2864
- `${input.txid}:${input.vout}`
2865
- ];
2866
- const eventStream = arkProvider.getEventStream(
2867
- abortController.signal,
2868
- topics
2869
- );
2885
+ const topics = [import_base8.hex.encode(signerPublicKey), `${input.txid}:${input.vout}`];
2886
+ const eventStream = arkProvider.getEventStream(abortController.signal, topics);
2870
2887
  const commitmentTxid = await import_sdk7.Batch.join(eventStream, handler, {
2871
2888
  abortController
2872
2889
  });
@@ -2887,14 +2904,8 @@ var joinBatch = async (arkProvider, identity, input, output, {
2887
2904
  };
2888
2905
  var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKey, input, output, arkInfo, arkProvider) => {
2889
2906
  const rawCheckpointTapscript = import_base8.hex.decode(arkInfo.checkpointTapscript);
2890
- const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(
2891
- rawCheckpointTapscript
2892
- );
2893
- const { arkTx, checkpoints } = (0, import_sdk7.buildOffchainTx)(
2894
- [input],
2895
- [output],
2896
- serverUnrollScript
2897
- );
2907
+ const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(rawCheckpointTapscript);
2908
+ const { arkTx, checkpoints } = (0, import_sdk7.buildOffchainTx)([input], [output], serverUnrollScript);
2898
2909
  const signedArkTx = await identity.sign(arkTx);
2899
2910
  const { arkTxid, finalArkTx, signedCheckpointTxs } = await arkProvider.submitTx(
2900
2911
  import_base8.base64.encode(signedArkTx.toPSBT()),
@@ -2902,9 +2913,7 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
2902
2913
  );
2903
2914
  const finalTx = import_sdk7.Transaction.fromPSBT(import_base8.base64.decode(finalArkTx));
2904
2915
  const serverPubkeyHex = import_base8.hex.encode(serverXOnlyPublicKey);
2905
- const claimLeafHash = (0, import_payment3.tapLeafHash)(
2906
- scriptFromTapLeafScript(vhtlcScript.claim())
2907
- );
2916
+ const claimLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(vhtlcScript.claim()));
2908
2917
  for (let i = 0; i < finalTx.inputsLength; i++) {
2909
2918
  if (!verifySignatures(finalTx, i, [serverPubkeyHex], claimLeafHash)) {
2910
2919
  throw new Error("Invalid final Ark transaction");
@@ -2914,13 +2923,9 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
2914
2923
  signedCheckpointTxs.map(async (c, idx) => {
2915
2924
  const tx = import_sdk7.Transaction.fromPSBT(import_base8.base64.decode(c));
2916
2925
  const checkpointLeaf = checkpoints[idx].getInput(0).tapLeafScript[0];
2917
- const cpLeafHash = (0, import_payment3.tapLeafHash)(
2918
- scriptFromTapLeafScript(checkpointLeaf)
2919
- );
2926
+ const cpLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(checkpointLeaf));
2920
2927
  if (!verifySignatures(tx, 0, [serverPubkeyHex], cpLeafHash)) {
2921
- throw new Error(
2922
- "Invalid server signature in checkpoint transaction"
2923
- );
2928
+ throw new Error("Invalid server signature in checkpoint transaction");
2924
2929
  }
2925
2930
  const signedCheckpoint = await identity.sign(tx, [0]);
2926
2931
  return import_base8.base64.encode(signedCheckpoint.toPSBT());
@@ -2930,61 +2935,37 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
2930
2935
  };
2931
2936
  var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnlyPublicKey, ourXOnlyPublicKey, serverXOnlyPublicKey, input, output, arkInfo, refundFunc) => {
2932
2937
  const rawCheckpointTapscript = import_base8.hex.decode(arkInfo.checkpointTapscript);
2933
- const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(
2934
- rawCheckpointTapscript
2938
+ const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(rawCheckpointTapscript);
2939
+ const { arkTx: unsignedRefundTx, checkpoints: checkpointPtxs } = (0, import_sdk7.buildOffchainTx)(
2940
+ [input],
2941
+ [output],
2942
+ serverUnrollScript
2935
2943
  );
2936
- const { arkTx: unsignedRefundTx, checkpoints: checkpointPtxs } = (0, import_sdk7.buildOffchainTx)([input], [output], serverUnrollScript);
2937
2944
  if (checkpointPtxs.length !== 1)
2938
- throw new Error(
2939
- `Expected one checkpoint transaction, got ${checkpointPtxs.length}`
2940
- );
2945
+ throw new Error(`Expected one checkpoint transaction, got ${checkpointPtxs.length}`);
2941
2946
  const unsignedCheckpointTx = checkpointPtxs[0];
2942
2947
  let boltzSignedRefundTx;
2943
2948
  let boltzSignedCheckpointTx;
2944
2949
  try {
2945
- const result = await refundFunc(
2946
- swapId,
2947
- unsignedRefundTx,
2948
- unsignedCheckpointTx
2949
- );
2950
+ const result = await refundFunc(swapId, unsignedRefundTx, unsignedCheckpointTx);
2950
2951
  boltzSignedRefundTx = result.transaction;
2951
2952
  boltzSignedCheckpointTx = result.checkpoint;
2952
2953
  } catch (error) {
2953
- throw new BoltzRefundError(
2954
- `Boltz rejected refund for swap ${swapId}`,
2955
- error
2956
- );
2954
+ throw new BoltzRefundError(`Boltz rejected refund for swap ${swapId}`, error);
2957
2955
  }
2958
2956
  const boltzXOnlyPublicKeyHex = import_base8.hex.encode(boltzXOnlyPublicKey);
2959
- const refundLeafHash = (0, import_payment3.tapLeafHash)(
2960
- scriptFromTapLeafScript(input.tapLeafScript)
2961
- );
2962
- if (!verifySignatures(
2963
- boltzSignedRefundTx,
2964
- 0,
2965
- [boltzXOnlyPublicKeyHex],
2966
- refundLeafHash
2967
- )) {
2957
+ const refundLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(input.tapLeafScript));
2958
+ if (!verifySignatures(boltzSignedRefundTx, 0, [boltzXOnlyPublicKeyHex], refundLeafHash)) {
2968
2959
  throw new Error("Invalid Boltz signature in refund transaction");
2969
2960
  }
2970
2961
  const checkpointLeaf = unsignedCheckpointTx.getInput(0).tapLeafScript[0];
2971
- const checkpointLeafHash = (0, import_payment3.tapLeafHash)(
2972
- scriptFromTapLeafScript(checkpointLeaf)
2973
- );
2974
- if (!verifySignatures(
2975
- boltzSignedCheckpointTx,
2976
- 0,
2977
- [boltzXOnlyPublicKeyHex],
2978
- checkpointLeafHash
2979
- )) {
2962
+ const checkpointLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(checkpointLeaf));
2963
+ if (!verifySignatures(boltzSignedCheckpointTx, 0, [boltzXOnlyPublicKeyHex], checkpointLeafHash)) {
2980
2964
  throw new Error("Invalid Boltz signature in checkpoint transaction");
2981
2965
  }
2982
2966
  const signedRefundTx = await identity.sign(unsignedRefundTx);
2983
2967
  const signedCheckpointTx = await identity.sign(unsignedCheckpointTx);
2984
- const combinedSignedRefundTx = (0, import_sdk7.combineTapscriptSigs)(
2985
- boltzSignedRefundTx,
2986
- signedRefundTx
2987
- );
2968
+ const combinedSignedRefundTx = (0, import_sdk7.combineTapscriptSigs)(boltzSignedRefundTx, signedRefundTx);
2988
2969
  const combinedSignedCheckpointTx = (0, import_sdk7.combineTapscriptSigs)(
2989
2970
  boltzSignedCheckpointTx,
2990
2971
  signedCheckpointTx
@@ -3008,25 +2989,16 @@ var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnly
3008
2989
  `Expected one signed checkpoint transaction, got ${signedCheckpointTxs.length}`
3009
2990
  );
3010
2991
  }
3011
- const serverSignedCheckpointTx = import_sdk7.Transaction.fromPSBT(
3012
- import_base8.base64.decode(signedCheckpointTxs[0])
3013
- );
2992
+ const serverSignedCheckpointTx = import_sdk7.Transaction.fromPSBT(import_base8.base64.decode(signedCheckpointTxs[0]));
3014
2993
  const serverPubkeyHex = import_base8.hex.encode(serverXOnlyPublicKey);
3015
- if (!verifySignatures(
3016
- serverSignedCheckpointTx,
3017
- 0,
3018
- [serverPubkeyHex],
3019
- checkpointLeafHash
3020
- )) {
2994
+ if (!verifySignatures(serverSignedCheckpointTx, 0, [serverPubkeyHex], checkpointLeafHash)) {
3021
2995
  throw new Error("Invalid server signature in checkpoint transaction");
3022
2996
  }
3023
2997
  const finalCheckpointTx = (0, import_sdk7.combineTapscriptSigs)(
3024
2998
  combinedSignedCheckpointTx,
3025
2999
  serverSignedCheckpointTx
3026
3000
  );
3027
- await arkProvider.finalizeTx(arkTxid, [
3028
- import_base8.base64.encode(finalCheckpointTx.toPSBT())
3029
- ]);
3001
+ await arkProvider.finalizeTx(arkTxid, [import_base8.base64.encode(finalCheckpointTx.toPSBT())]);
3030
3002
  };
3031
3003
  function scriptFromTapLeafScript(leaf) {
3032
3004
  return leaf[1].subarray(0, leaf[1].length - 1);
@@ -3034,21 +3006,21 @@ function scriptFromTapLeafScript(leaf) {
3034
3006
 
3035
3007
  // src/arkade-swaps.ts
3036
3008
  var dedupeVtxos = (vtxos) => [
3037
- ...new Map(
3038
- vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])
3039
- ).values()
3009
+ ...new Map(vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])).values()
3040
3010
  ];
3041
3011
  var hasNonEmptyString = (value) => typeof value === "string" && value.length > 0;
3042
3012
  var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
3043
3013
  const hasRequiredSwapMetadata = hasNonEmptyString(swap.id) && hasNonEmptyString(swap.request.refundPublicKey) && hasNonEmptyString(swap.response.address) && hasNonEmptyString(swap.response.claimPublicKey) && !!swap.response.timeoutBlockHeights;
3044
3014
  if (!hasRequiredSwapMetadata) return false;
3045
- return refundableVtxos.some(
3046
- (vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo)
3047
- );
3015
+ return refundableVtxos.some((vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo));
3048
3016
  };
3049
3017
  var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
3050
3018
  var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
3051
3019
  var CLAIM_VTXO_RETRY_DELAY_MS = 500;
3020
+ var quoteOptionsForSwap = (swap) => {
3021
+ const amount = swap.response?.claimDetails?.amount;
3022
+ return typeof amount === "number" ? { minAcceptableAmount: amount } : void 0;
3023
+ };
3052
3024
  var ArkadeSwaps = class _ArkadeSwaps {
3053
3025
  /** The Arkade wallet instance used for signing and address generation. */
3054
3026
  wallet;
@@ -3084,10 +3056,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3084
3056
  return new _ArkadeSwaps(config);
3085
3057
  }
3086
3058
  const arkProvider = config.arkProvider ?? config.wallet.arkProvider;
3087
- if (!arkProvider)
3088
- throw new Error(
3089
- "Ark provider is required either in wallet or config."
3090
- );
3059
+ if (!arkProvider) throw new Error("Ark provider is required either in wallet or config.");
3091
3060
  const arkInfo = await arkProvider.getInfo();
3092
3061
  const network = arkInfo.network;
3093
3062
  const swapProvider = new BoltzSwapProvider({ network });
@@ -3098,16 +3067,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
3098
3067
  if (!config.swapProvider) throw new Error("Swap provider is required.");
3099
3068
  this.wallet = config.wallet;
3100
3069
  const arkProvider = config.arkProvider ?? config.wallet.arkProvider;
3101
- if (!arkProvider)
3102
- throw new Error(
3103
- "Ark provider is required either in wallet or config."
3104
- );
3070
+ if (!arkProvider) throw new Error("Ark provider is required either in wallet or config.");
3105
3071
  this.arkProvider = arkProvider;
3106
3072
  const indexerProvider = config.indexerProvider ?? config.wallet.indexerProvider;
3107
3073
  if (!indexerProvider)
3108
- throw new Error(
3109
- "Indexer provider is required either in wallet or config."
3110
- );
3074
+ throw new Error("Indexer provider is required either in wallet or config.");
3111
3075
  this.indexerProvider = indexerProvider;
3112
3076
  this.swapProvider = config.swapProvider;
3113
3077
  if (config.swapRepository) {
@@ -3118,10 +3082,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3118
3082
  if (config.swapManager !== false) {
3119
3083
  const swapManagerConfig = !config.swapManager || config.swapManager === true ? {} : config.swapManager;
3120
3084
  const shouldAutostart = swapManagerConfig.autoStart ?? true;
3121
- this.swapManager = new SwapManager(
3122
- this.swapProvider,
3123
- swapManagerConfig
3124
- );
3085
+ this.swapManager = new SwapManager(this.swapProvider, swapManagerConfig);
3125
3086
  this.swapManager.setCallbacks({
3126
3087
  claim: async (swap) => {
3127
3088
  await this.claimVHTLC(swap);
@@ -3136,7 +3097,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3136
3097
  await this.claimBtc(swap);
3137
3098
  },
3138
3099
  refundArk: async (swap) => {
3139
- await this.refundArk(swap);
3100
+ return this.refundArk(swap);
3140
3101
  },
3141
3102
  signServerClaim: async (swap) => {
3142
3103
  await this.signCooperativeClaimForServer(swap);
@@ -3269,19 +3230,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
3269
3230
  * @throws {SwapError} If amount is <= 0 or key retrieval fails.
3270
3231
  */
3271
3232
  async createReverseSwap(args) {
3272
- if (args.amount <= 0)
3273
- throw new SwapError({ message: "Amount must be greater than 0" });
3274
- const claimPublicKey = import_base9.hex.encode(
3275
- await this.wallet.identity.compressedPublicKey()
3276
- );
3233
+ if (args.amount <= 0) throw new SwapError({ message: "Amount must be greater than 0" });
3234
+ const claimPublicKey = import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
3277
3235
  if (!claimPublicKey)
3278
3236
  throw new SwapError({
3279
3237
  message: "Failed to get claim public key from wallet"
3280
3238
  });
3281
3239
  const preimage = (0, import_utils3.randomBytes)(32);
3282
3240
  const preimageHash = import_base9.hex.encode((0, import_sha23.sha256)(preimage));
3283
- if (!preimageHash)
3284
- throw new SwapError({ message: "Failed to get preimage hash" });
3241
+ if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" });
3285
3242
  const swapRequest = {
3286
3243
  invoiceAmount: args.amount,
3287
3244
  claimPublicKey,
@@ -3311,18 +3268,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
3311
3268
  */
3312
3269
  async claimVHTLC(pendingSwap) {
3313
3270
  if (!pendingSwap.preimage)
3314
- throw new Error(
3315
- `Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
3316
- );
3271
+ throw new Error(`Swap ${pendingSwap.id}: preimage is required to claim VHTLC`);
3317
3272
  const {
3318
3273
  refundPublicKey,
3319
3274
  lockupAddress,
3320
3275
  timeoutBlockHeights: vhtlcTimeouts
3321
3276
  } = pendingSwap.response;
3322
3277
  if (!refundPublicKey || !lockupAddress || !vhtlcTimeouts)
3323
- throw new Error(
3324
- `Swap ${pendingSwap.id}: incomplete reverse swap response`
3325
- );
3278
+ throw new Error(`Swap ${pendingSwap.id}: incomplete reverse swap response`);
3326
3279
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
3327
3280
  const arkInfo = await this.arkProvider.getInfo();
3328
3281
  const address = await this.wallet.getAddress();
@@ -3357,58 +3310,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
3357
3310
  throw new Error(
3358
3311
  `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3359
3312
  );
3360
- let vtxo;
3313
+ let unspentVtxos = [];
3314
+ let rawVtxos = [];
3361
3315
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
3362
- const { vtxos } = await this.indexerProvider.getVtxos({
3316
+ const result = await this.indexerProvider.getVtxos({
3363
3317
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3364
3318
  });
3365
- if (vtxos.length > 0) {
3366
- vtxo = vtxos[0];
3319
+ rawVtxos = result.vtxos;
3320
+ unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
3321
+ if (unspentVtxos.length > 0) {
3367
3322
  break;
3368
3323
  }
3369
3324
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
3370
- await new Promise(
3371
- (resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS)
3372
- );
3325
+ await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
3373
3326
  }
3374
3327
  }
3375
- if (!vtxo) {
3376
- throw new Error(
3377
- `Swap ${pendingSwap.id}: no spendable virtual coins found`
3378
- );
3379
- }
3380
- if (vtxo.isSpent) {
3328
+ if (unspentVtxos.length === 0) {
3329
+ if (rawVtxos.length === 0) {
3330
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3331
+ }
3381
3332
  throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3382
3333
  }
3383
- const input = {
3384
- ...vtxo,
3385
- tapLeafScript: vhtlcScript.claim(),
3386
- tapTree: vhtlcScript.encode()
3387
- };
3388
- const output = {
3389
- amount: BigInt(vtxo.value),
3390
- script: import_sdk8.ArkAddress.decode(address).pkScript
3391
- };
3392
- const vhtlcIdentity = claimVHTLCIdentity(
3393
- this.wallet.identity,
3394
- preimage
3395
- );
3396
- let finalStatus;
3397
- if ((0, import_sdk8.isRecoverable)(vtxo)) {
3398
- await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3399
- finalStatus = "transaction.claimed";
3400
- } else {
3401
- await claimVHTLCwithOffchainTx(
3402
- vhtlcIdentity,
3403
- vhtlcScript,
3404
- serverXOnly,
3405
- input,
3406
- output,
3407
- arkInfo,
3408
- this.arkProvider
3334
+ const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
3335
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3336
+ const claimErrors = [];
3337
+ let usedOffchainClaim = false;
3338
+ for (const vtxo of unspentVtxos) {
3339
+ const input = {
3340
+ ...vtxo,
3341
+ tapLeafScript: vhtlcScript.claim(),
3342
+ tapTree: vhtlcScript.encode()
3343
+ };
3344
+ const output = {
3345
+ amount: BigInt(vtxo.value),
3346
+ script: outputScript
3347
+ };
3348
+ try {
3349
+ if ((0, import_sdk8.isRecoverable)(vtxo)) {
3350
+ await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3351
+ } else {
3352
+ await claimVHTLCwithOffchainTx(
3353
+ vhtlcIdentity,
3354
+ vhtlcScript,
3355
+ serverXOnly,
3356
+ input,
3357
+ output,
3358
+ arkInfo,
3359
+ this.arkProvider
3360
+ );
3361
+ usedOffchainClaim = true;
3362
+ }
3363
+ } catch (error) {
3364
+ claimErrors.push({ vtxo, error });
3365
+ }
3366
+ }
3367
+ if (claimErrors.length > 0) {
3368
+ const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
3369
+ throw new Error(
3370
+ `Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
3409
3371
  );
3410
- finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
3411
3372
  }
3373
+ const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
3412
3374
  await updateReverseSwapStatus(
3413
3375
  pendingSwap,
3414
3376
  finalStatus,
@@ -3511,9 +3473,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3511
3473
  async sendLightningPayment(args) {
3512
3474
  const pendingSwap = await this.createSubmarineSwap(args);
3513
3475
  if (!pendingSwap.response.address)
3514
- throw new Error(
3515
- `Swap ${pendingSwap.id}: missing address in submarine swap response`
3516
- );
3476
+ throw new Error(`Swap ${pendingSwap.id}: missing address in submarine swap response`);
3517
3477
  await this.savePendingSubmarineSwap(pendingSwap);
3518
3478
  const txid = await this.wallet.send({
3519
3479
  address: pendingSwap.response.address,
@@ -3546,9 +3506,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3546
3506
  * @throws {SwapError} If invoice is missing or key retrieval fails.
3547
3507
  */
3548
3508
  async createSubmarineSwap(args) {
3549
- const refundPublicKey = import_base9.hex.encode(
3550
- await this.wallet.identity.compressedPublicKey()
3551
- );
3509
+ const refundPublicKey = import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
3552
3510
  if (!refundPublicKey)
3553
3511
  throw new SwapError({
3554
3512
  message: "Failed to get refund public key from wallet"
@@ -3586,9 +3544,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3586
3544
  async buildSubmarineVHTLCContext(swap, arkInfo) {
3587
3545
  const preimageHash = swap.request.invoice ? getInvoicePaymentHash(swap.request.invoice) : swap.preimageHash;
3588
3546
  if (!preimageHash)
3589
- throw new Error(
3590
- `Swap ${swap.id}: preimage hash is required to refund VHTLC`
3591
- );
3547
+ throw new Error(`Swap ${swap.id}: preimage hash is required to refund VHTLC`);
3592
3548
  const resolvedArkInfo = arkInfo ?? await this.arkProvider.getInfo();
3593
3549
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
3594
3550
  await this.wallet.identity.xOnlyPublicKey(),
@@ -3602,9 +3558,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3602
3558
  );
3603
3559
  const { claimPublicKey, timeoutBlockHeights: vhtlcTimeouts } = swap.response;
3604
3560
  if (!claimPublicKey || !vhtlcTimeouts)
3605
- throw new Error(
3606
- `Swap ${swap.id}: incomplete submarine swap response`
3607
- );
3561
+ throw new Error(`Swap ${swap.id}: incomplete submarine swap response`);
3608
3562
  const boltzXOnlyPublicKey = normalizeToXOnlyKey(
3609
3563
  import_base9.hex.decode(claimPublicKey),
3610
3564
  "boltz",
@@ -3619,9 +3573,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3619
3573
  timeoutBlockHeights: vhtlcTimeouts
3620
3574
  });
3621
3575
  if (!vhtlcScript.claimScript)
3622
- throw new Error(
3623
- `Swap ${swap.id}: failed to create VHTLC script for submarine swap`
3624
- );
3576
+ throw new Error(`Swap ${swap.id}: failed to create VHTLC script for submarine swap`);
3625
3577
  if (vhtlcAddress !== swap.response.address)
3626
3578
  throw new Error(
3627
3579
  `VHTLC address mismatch for swap ${swap.id}: expected ${swap.response.address}, got ${vhtlcAddress}`
@@ -3660,10 +3612,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3660
3612
  recoverableOnly: true
3661
3613
  })
3662
3614
  ]);
3663
- const refundableVtxos = dedupeVtxos([
3664
- ...spendableResult.vtxos,
3665
- ...recoverableResult.vtxos
3666
- ]);
3615
+ const refundableVtxos = dedupeVtxos([...spendableResult.vtxos, ...recoverableResult.vtxos]);
3667
3616
  let diagnostic;
3668
3617
  if (refundableVtxos.length === 0) {
3669
3618
  const { vtxos: allVtxos } = await this.indexerProvider.getVtxos({
@@ -3683,13 +3632,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
3683
3632
  submarineRecoveryInfoFromLookup(swap, lookup) {
3684
3633
  const { refundableVtxos, diagnostic, vhtlcTimeouts } = lookup;
3685
3634
  if (refundableVtxos.length > 0) {
3686
- const cltvSatisfied = isSubmarineRefundLocktimeReached(
3687
- vhtlcTimeouts.refund
3688
- );
3689
- const amountSats = refundableVtxos.reduce(
3690
- (sum, vtxo) => sum + Number(vtxo.value),
3691
- 0
3692
- );
3635
+ const cltvSatisfied = isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund);
3636
+ const amountSats = refundableVtxos.reduce((sum, vtxo) => sum + Number(vtxo.value), 0);
3693
3637
  const isRecoverable2 = cltvSatisfied || canRecoverViaBoltz3of3(refundableVtxos, swap);
3694
3638
  return {
3695
3639
  swap,
@@ -3753,19 +3697,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
3753
3697
  );
3754
3698
  }
3755
3699
  if (diagnostic.allSpent) {
3756
- throw new Error(
3757
- `Swap ${pendingSwap.id}: VHTLC is already spent`
3758
- );
3700
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3759
3701
  }
3760
- throw new Error(
3761
- `Swap ${pendingSwap.id}: VHTLC has no refundable VTXOs yet`
3762
- );
3702
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC has no refundable VTXOs yet`);
3763
3703
  }
3764
3704
  const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3765
3705
  const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
3766
- const cltvSatisfied = isSubmarineRefundLocktimeReached(
3767
- vhtlcTimeouts.refund
3768
- );
3706
+ const cltvSatisfied = isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund);
3769
3707
  let boltzCallCount = 0;
3770
3708
  let sweptCount = 0;
3771
3709
  let skippedCount = 0;
@@ -3807,6 +3745,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3807
3745
  if (boltzCallCount > 0) {
3808
3746
  await new Promise((r) => setTimeout(r, 2e3));
3809
3747
  }
3748
+ boltzCallCount++;
3810
3749
  await refundVHTLCwithOffchainTx(
3811
3750
  pendingSwap.id,
3812
3751
  this.wallet.identity,
@@ -3817,11 +3756,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
3817
3756
  input,
3818
3757
  output,
3819
3758
  arkInfo,
3820
- this.swapProvider.refundSubmarineSwap.bind(
3821
- this.swapProvider
3822
- )
3759
+ this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3823
3760
  );
3824
- boltzCallCount++;
3825
3761
  sweptCount++;
3826
3762
  } catch (error) {
3827
3763
  if (!(error instanceof BoltzRefundError)) {
@@ -3842,13 +3778,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3842
3778
  tapLeafScript: refundWithoutReceiverLeaf,
3843
3779
  tapTree: vhtlcScript.encode()
3844
3780
  };
3845
- await this.joinBatch(
3846
- this.wallet.identity,
3847
- fallbackInput,
3848
- output,
3849
- arkInfo,
3850
- false
3851
- );
3781
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
3852
3782
  sweptCount++;
3853
3783
  }
3854
3784
  }
@@ -3944,10 +3874,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3944
3874
  try {
3945
3875
  return {
3946
3876
  swap,
3947
- context: await this.buildSubmarineVHTLCContext(
3948
- swap,
3949
- arkInfo
3950
- )
3877
+ context: await this.buildSubmarineVHTLCContext(swap, arkInfo)
3951
3878
  };
3952
3879
  } catch (err) {
3953
3880
  return {
@@ -3960,9 +3887,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3960
3887
  const valid = prepared.filter(
3961
3888
  (item) => "context" in item
3962
3889
  );
3963
- const scripts = [
3964
- ...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))
3965
- ];
3890
+ const scripts = [...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))];
3966
3891
  const refundableByScript = /* @__PURE__ */ new Map();
3967
3892
  if (scripts.length > 0) {
3968
3893
  const [spendableResult, recoverableResult] = await Promise.all([
@@ -3997,9 +3922,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3997
3922
  error: item.error
3998
3923
  };
3999
3924
  }
4000
- const refundableVtxos = refundableByScript.get(
4001
- item.context.vhtlcPkScriptHex.toLowerCase()
4002
- ) ?? [];
3925
+ const refundableVtxos = refundableByScript.get(item.context.vhtlcPkScriptHex.toLowerCase()) ?? [];
4003
3926
  return this.submarineRecoveryInfoFromLookup(item.swap, {
4004
3927
  ...item.context,
4005
3928
  refundableVtxos
@@ -4178,9 +4101,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4178
4101
  */
4179
4102
  async waitAndClaimBtc(pendingSwap) {
4180
4103
  if (this.swapManager && await this.swapManager.hasSwap(pendingSwap.id)) {
4181
- const { txid } = await this.swapManager.waitForSwapCompletion(
4182
- pendingSwap.id
4183
- );
4104
+ const { txid } = await this.swapManager.waitForSwapCompletion(pendingSwap.id);
4184
4105
  return { txid };
4185
4106
  }
4186
4107
  return new Promise((resolve, reject) => {
@@ -4207,24 +4128,25 @@ var ArkadeSwaps = class _ArkadeSwaps {
4207
4128
  }
4208
4129
  case "transaction.claimed":
4209
4130
  await updateSwapStatus();
4210
- const claimedStatus = await this.getSwapStatus(
4211
- pendingSwap.id
4212
- );
4131
+ const claimedStatus = await this.getSwapStatus(pendingSwap.id);
4213
4132
  resolve({
4214
4133
  txid: claimedStatus.transaction?.id ?? ""
4215
4134
  });
4216
4135
  break;
4217
4136
  case "transaction.lockupFailed":
4218
4137
  await updateSwapStatus();
4219
- await this.quoteSwap(swap.response.id).catch((err) => {
4220
- reject(
4221
- new SwapError({
4222
- message: `Failed to renegotiate quote: ${err.message}`,
4223
- isRefundable: true,
4224
- pendingSwap: swap
4225
- })
4226
- );
4227
- });
4138
+ await this.quoteSwap(swap.response.id, quoteOptionsForSwap(swap)).catch(
4139
+ (err) => {
4140
+ reject(
4141
+ new SwapError({
4142
+ message: `Failed to renegotiate quote: ${err.message}`,
4143
+ isRefundable: true,
4144
+ pendingSwap: swap,
4145
+ cause: err
4146
+ })
4147
+ );
4148
+ }
4149
+ );
4228
4150
  break;
4229
4151
  case "swap.expired":
4230
4152
  await updateSwapStatus();
@@ -4262,30 +4184,18 @@ var ArkadeSwaps = class _ArkadeSwaps {
4262
4184
  */
4263
4185
  async claimBtc(pendingSwap) {
4264
4186
  if (!pendingSwap.toAddress)
4265
- throw new Error(
4266
- `Swap ${pendingSwap.id}: destination address is required`
4267
- );
4187
+ throw new Error(`Swap ${pendingSwap.id}: destination address is required`);
4268
4188
  if (!pendingSwap.response.claimDetails.swapTree)
4269
- throw new Error(
4270
- `Swap ${pendingSwap.id}: missing swap tree in claim details`
4271
- );
4189
+ throw new Error(`Swap ${pendingSwap.id}: missing swap tree in claim details`);
4272
4190
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4273
- throw new Error(
4274
- `Swap ${pendingSwap.id}: missing server public key in claim details`
4275
- );
4191
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in claim details`);
4276
4192
  const swapStatus = await this.getSwapStatus(pendingSwap.id);
4277
4193
  if (!swapStatus.transaction?.hex)
4278
- throw new Error(
4279
- `Swap ${pendingSwap.id}: BTC transaction hex is required`
4280
- );
4281
- const lockupTx = import_btc_signer5.Transaction.fromRaw(
4282
- import_base9.hex.decode(swapStatus.transaction.hex)
4283
- );
4194
+ throw new Error(`Swap ${pendingSwap.id}: BTC transaction hex is required`);
4195
+ const lockupTx = import_btc_signer5.Transaction.fromRaw(import_base9.hex.decode(swapStatus.transaction.hex));
4284
4196
  const arkInfo = await this.arkProvider.getInfo();
4285
4197
  const network = arkInfo.network === "bitcoin" ? import_utils4.NETWORK : arkInfo.network === "mutinynet" ? MUTINYNET_NETWORK : REGTEST_NETWORK;
4286
- const swapTree = deserializeSwapTree(
4287
- pendingSwap.response.claimDetails.swapTree
4288
- );
4198
+ const swapTree = deserializeSwapTree(pendingSwap.response.claimDetails.swapTree);
4289
4199
  const musig = tweakMusig(
4290
4200
  create(import_base9.hex.decode(pendingSwap.ephemeralKey), [
4291
4201
  import_base9.hex.decode(pendingSwap.response.claimDetails.serverPublicKey),
@@ -4306,19 +4216,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
4306
4216
  vout: swapOutput.vout,
4307
4217
  transactionId: lockupTx.id
4308
4218
  },
4309
- import_btc_signer5.OutScript.encode(
4310
- (0, import_btc_signer5.Address)(network).decode(pendingSwap.toAddress)
4311
- ),
4219
+ import_btc_signer5.OutScript.encode((0, import_btc_signer5.Address)(network).decode(pendingSwap.toAddress)),
4312
4220
  feeToDeliverExactAmount > fee ? feeToDeliverExactAmount : fee
4313
4221
  )
4314
4222
  );
4315
4223
  const musigMessage = musig.message(
4316
- claimTx.preimageWitnessV1(
4317
- 0,
4318
- [swapOutput.script],
4319
- import_btc_signer5.SigHash.DEFAULT,
4320
- [swapOutput.amount]
4321
- )
4224
+ claimTx.preimageWitnessV1(0, [swapOutput.script], import_btc_signer5.SigHash.DEFAULT, [
4225
+ swapOutput.amount
4226
+ ])
4322
4227
  ).generateNonce();
4323
4228
  const signedTxData = await this.swapProvider.postChainClaimDetails(
4324
4229
  pendingSwap.response.id,
@@ -4332,14 +4237,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
4332
4237
  }
4333
4238
  );
4334
4239
  if (!signedTxData.pubNonce || !signedTxData.partialSignature)
4335
- throw new Error(
4336
- `Swap ${pendingSwap.id}: invalid signature data from server`
4337
- );
4240
+ throw new Error(`Swap ${pendingSwap.id}: invalid signature data from server`);
4338
4241
  const musigSession = musigMessage.aggregateNonces([
4339
4242
  [
4340
- import_base9.hex.decode(
4341
- pendingSwap.response.claimDetails.serverPublicKey
4342
- ),
4243
+ import_base9.hex.decode(pendingSwap.response.claimDetails.serverPublicKey),
4343
4244
  import_base9.hex.decode(signedTxData.pubNonce)
4344
4245
  ]
4345
4246
  ]).initializeSession();
@@ -4354,18 +4255,23 @@ var ArkadeSwaps = class _ArkadeSwaps {
4354
4255
  await this.swapProvider.postBtcTransaction(claimTx.hex);
4355
4256
  }
4356
4257
  /**
4357
- * When an ARK to BTC swap fails, refund sats on ARK chain by claiming the VHTLC.
4258
+ * When an ARK to BTC swap fails, refund every unspent VTXO at the chain
4259
+ * swap's ARK lockup address.
4260
+ *
4261
+ * Path selection per VTXO:
4262
+ * - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
4263
+ * - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
4264
+ * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
4265
+ *
4358
4266
  * @param pendingSwap - The pending chain swap to refund.
4267
+ * @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
4268
+ * the call was a no-op — callers should retry after CLTV.
4359
4269
  */
4360
4270
  async refundArk(pendingSwap) {
4361
4271
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4362
- throw new Error(
4363
- `Swap ${pendingSwap.id}: missing server public key in lockup details`
4364
- );
4272
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in lockup details`);
4365
4273
  if (!pendingSwap.response.lockupDetails.timeouts)
4366
- throw new Error(
4367
- `Swap ${pendingSwap.id}: missing timeouts in lockup details`
4368
- );
4274
+ throw new Error(`Swap ${pendingSwap.id}: missing timeouts in lockup details`);
4369
4275
  const arkInfo = await this.arkProvider.getInfo();
4370
4276
  const address = await this.wallet.getAddress();
4371
4277
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
@@ -4383,21 +4289,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
4383
4289
  "boltz",
4384
4290
  pendingSwap.id
4385
4291
  );
4386
- const vhtlcPkScript = import_sdk8.ArkAddress.decode(
4387
- pendingSwap.response.lockupDetails.lockupAddress
4388
- ).pkScript;
4389
- const { vtxos } = await this.indexerProvider.getVtxos({
4390
- scripts: [import_base9.hex.encode(vhtlcPkScript)]
4391
- });
4392
- if (vtxos.length === 0) {
4393
- throw new Error(
4394
- `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4395
- );
4396
- }
4397
- const vtxo = vtxos[0];
4398
- if (vtxo.isSpent) {
4399
- throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4400
- }
4401
4292
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4402
4293
  network: arkInfo.network,
4403
4294
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4407,45 +4298,111 @@ var ArkadeSwaps = class _ArkadeSwaps {
4407
4298
  timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
4408
4299
  });
4409
4300
  if (!vhtlcScript.refundScript)
4410
- throw new Error(
4411
- `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4412
- );
4301
+ throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
4413
4302
  if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
4414
4303
  throw new SwapError({
4415
4304
  message: "Unable to claim: invalid VHTLC address"
4416
4305
  });
4417
4306
  }
4418
- const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4419
- const input = {
4420
- ...vtxo,
4421
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
4422
- tapTree: vhtlcScript.encode()
4423
- };
4424
- const output = {
4425
- amount: BigInt(vtxo.value),
4426
- script: import_sdk8.ArkAddress.decode(address).pkScript
4427
- };
4428
- if (isRecoverableVtxo) {
4429
- await this.joinBatch(this.wallet.identity, input, output, arkInfo);
4430
- } else {
4431
- await refundVHTLCwithOffchainTx(
4432
- pendingSwap.id,
4433
- this.wallet.identity,
4434
- this.arkProvider,
4435
- boltzXOnlyPublicKey,
4436
- ourXOnlyPublicKey,
4437
- serverXOnlyPublicKey,
4438
- input,
4439
- output,
4440
- arkInfo,
4441
- this.swapProvider.refundChainSwap.bind(this.swapProvider)
4307
+ const { vtxos } = await this.indexerProvider.getVtxos({
4308
+ scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
4309
+ });
4310
+ if (vtxos.length === 0) {
4311
+ throw new Error(
4312
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4442
4313
  );
4443
4314
  }
4315
+ const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
4316
+ if (unspentVtxos.length === 0) {
4317
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4318
+ }
4319
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
4320
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
4321
+ const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
4322
+ let boltzCallCount = 0;
4323
+ let sweptCount = 0;
4324
+ let skippedCount = 0;
4325
+ for (const vtxo of unspentVtxos) {
4326
+ const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4327
+ const output = {
4328
+ amount: BigInt(vtxo.value),
4329
+ script: outputScript
4330
+ };
4331
+ if (isSubmarineRefundLocktimeReached(refundLocktime)) {
4332
+ const input2 = {
4333
+ ...vtxo,
4334
+ tapLeafScript: refundWithoutReceiverLeaf,
4335
+ tapTree: vhtlcScript.encode()
4336
+ };
4337
+ await this.joinBatch(
4338
+ this.wallet.identity,
4339
+ input2,
4340
+ output,
4341
+ arkInfo,
4342
+ isRecoverableVtxo
4343
+ );
4344
+ sweptCount++;
4345
+ continue;
4346
+ }
4347
+ if (isRecoverableVtxo) {
4348
+ logger.error(
4349
+ `Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${refundLocktime}, currentTimestamp=${Math.floor(Date.now() / 1e3)}). Refund will be retried after locktime.`
4350
+ );
4351
+ skippedCount++;
4352
+ continue;
4353
+ }
4354
+ const input = {
4355
+ ...vtxo,
4356
+ tapLeafScript: vhtlcScript.refund(),
4357
+ tapTree: vhtlcScript.encode()
4358
+ };
4359
+ try {
4360
+ if (boltzCallCount > 0) {
4361
+ await new Promise((r) => setTimeout(r, 2e3));
4362
+ }
4363
+ boltzCallCount++;
4364
+ await refundVHTLCwithOffchainTx(
4365
+ pendingSwap.id,
4366
+ this.wallet.identity,
4367
+ this.arkProvider,
4368
+ boltzXOnlyPublicKey,
4369
+ ourXOnlyPublicKey,
4370
+ serverXOnlyPublicKey,
4371
+ input,
4372
+ output,
4373
+ arkInfo,
4374
+ this.swapProvider.refundChainSwap.bind(this.swapProvider)
4375
+ );
4376
+ sweptCount++;
4377
+ } catch (error) {
4378
+ if (!(error instanceof BoltzRefundError)) {
4379
+ throw error;
4380
+ }
4381
+ if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
4382
+ logger.error(
4383
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (currentTimestamp=${Math.floor(Date.now() / 1e3)}, locktime=${refundLocktime}). Refund will be retried after locktime.`
4384
+ );
4385
+ skippedCount++;
4386
+ continue;
4387
+ }
4388
+ logger.warn(
4389
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
4390
+ );
4391
+ const fallbackInput = {
4392
+ ...vtxo,
4393
+ tapLeafScript: refundWithoutReceiverLeaf,
4394
+ tapTree: vhtlcScript.encode()
4395
+ };
4396
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
4397
+ sweptCount++;
4398
+ }
4399
+ }
4444
4400
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4445
4401
  await this.savePendingChainSwap({
4446
4402
  ...pendingSwap,
4447
4403
  status: finalStatus.status
4448
4404
  });
4405
+ return { swept: sweptCount, skipped: skippedCount };
4449
4406
  }
4450
4407
  // =========================================================================
4451
4408
  // Chain swaps: BTC -> ARK
@@ -4490,9 +4447,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4490
4447
  */
4491
4448
  async waitAndClaimArk(pendingSwap) {
4492
4449
  if (this.swapManager && await this.swapManager.hasSwap(pendingSwap.id)) {
4493
- const { txid } = await this.swapManager.waitForSwapCompletion(
4494
- pendingSwap.id
4495
- );
4450
+ const { txid } = await this.swapManager.waitForSwapCompletion(pendingSwap.id);
4496
4451
  return { txid };
4497
4452
  }
4498
4453
  return new Promise((resolve, reject) => {
@@ -4513,37 +4468,33 @@ var ArkadeSwaps = class _ArkadeSwaps {
4513
4468
  break;
4514
4469
  case "transaction.claimed":
4515
4470
  await updateSwapStatus();
4516
- const claimedStatus = await this.getSwapStatus(
4517
- pendingSwap.id
4518
- );
4471
+ const claimedStatus = await this.getSwapStatus(pendingSwap.id);
4519
4472
  resolve({
4520
4473
  txid: claimedStatus.transaction?.id ?? ""
4521
4474
  });
4522
4475
  break;
4523
4476
  case "transaction.claim.pending":
4524
4477
  await updateSwapStatus();
4525
- await this.signCooperativeClaimForServer(swap).catch(
4478
+ await this.signCooperativeClaimForServer(swap).catch((err) => {
4479
+ logger.error(`Failed to sign cooperative claim for ${swap.id}:`, err);
4480
+ });
4481
+ break;
4482
+ case "transaction.lockupFailed":
4483
+ await updateSwapStatus();
4484
+ await this.quoteSwap(swap.response.id, quoteOptionsForSwap(swap)).catch(
4526
4485
  (err) => {
4527
- logger.error(
4528
- `Failed to sign cooperative claim for ${swap.id}:`,
4529
- err
4486
+ reject(
4487
+ new SwapError({
4488
+ message: `Failed to renegotiate quote: ${err.message}`,
4489
+ isRefundable: false,
4490
+ // TODO btc refund not implemented yet
4491
+ pendingSwap: swap,
4492
+ cause: err
4493
+ })
4530
4494
  );
4531
4495
  }
4532
4496
  );
4533
4497
  break;
4534
- case "transaction.lockupFailed":
4535
- await updateSwapStatus();
4536
- await this.quoteSwap(swap.response.id).catch((err) => {
4537
- reject(
4538
- new SwapError({
4539
- message: `Failed to renegotiate quote: ${err.message}`,
4540
- isRefundable: false,
4541
- // TODO btc refund not implemented yet
4542
- pendingSwap: swap
4543
- })
4544
- );
4545
- });
4546
- break;
4547
4498
  case "swap.expired":
4548
4499
  await updateSwapStatus();
4549
4500
  reject(
@@ -4583,17 +4534,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
4583
4534
  */
4584
4535
  async claimArk(pendingSwap) {
4585
4536
  if (!pendingSwap.toAddress)
4586
- throw new Error(
4587
- `Swap ${pendingSwap.id}: destination address is required`
4588
- );
4537
+ throw new Error(`Swap ${pendingSwap.id}: destination address is required`);
4589
4538
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4590
- throw new Error(
4591
- `Swap ${pendingSwap.id}: missing server public key in claim details`
4592
- );
4539
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in claim details`);
4593
4540
  if (!pendingSwap.response.claimDetails.timeouts)
4594
- throw new Error(
4595
- `Swap ${pendingSwap.id}: missing timeouts in claim details`
4596
- );
4541
+ throw new Error(`Swap ${pendingSwap.id}: missing timeouts in claim details`);
4597
4542
  const arkInfo = await this.arkProvider.getInfo();
4598
4543
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
4599
4544
  const address = await this.wallet.getAddress();
@@ -4605,10 +4550,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4605
4550
  pendingSwap.response.claimDetails.serverPublicKey,
4606
4551
  "sender"
4607
4552
  );
4608
- const serverXOnlyPublicKey = normalizeToXOnlyKey(
4609
- arkInfo.signerPubkey,
4610
- "server"
4611
- );
4553
+ const serverXOnlyPublicKey = normalizeToXOnlyKey(arkInfo.signerPubkey, "server");
4612
4554
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4613
4555
  network: arkInfo.network,
4614
4556
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4618,9 +4560,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4618
4560
  timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
4619
4561
  });
4620
4562
  if (!vhtlcScript.claimScript)
4621
- throw new Error(
4622
- `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4623
- );
4563
+ throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
4624
4564
  if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
4625
4565
  throw new SwapError({
4626
4566
  message: "Unable to claim: invalid VHTLC address"
@@ -4637,15 +4577,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
4637
4577
  break;
4638
4578
  }
4639
4579
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
4640
- await new Promise(
4641
- (resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS)
4642
- );
4580
+ await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
4643
4581
  }
4644
4582
  }
4645
4583
  if (!vtxo) {
4646
- throw new Error(
4647
- `Swap ${pendingSwap.id}: no spendable virtual coins found`
4648
- );
4584
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
4649
4585
  }
4650
4586
  const input = {
4651
4587
  ...vtxo,
@@ -4656,10 +4592,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4656
4592
  amount: BigInt(vtxo.value),
4657
4593
  script: import_sdk8.ArkAddress.decode(address).pkScript
4658
4594
  };
4659
- const vhtlcIdentity = claimVHTLCIdentity(
4660
- this.wallet.identity,
4661
- preimage
4662
- );
4595
+ const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
4663
4596
  if ((0, import_sdk8.isRecoverable)(vtxo)) {
4664
4597
  await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
4665
4598
  } else {
@@ -4685,16 +4618,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
4685
4618
  */
4686
4619
  async signCooperativeClaimForServer(pendingSwap) {
4687
4620
  if (!pendingSwap.response.lockupDetails.swapTree)
4688
- throw new Error(
4689
- `Swap ${pendingSwap.id}: missing swap tree in lockup details`
4690
- );
4621
+ throw new Error(`Swap ${pendingSwap.id}: missing swap tree in lockup details`);
4691
4622
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4692
- throw new Error(
4693
- `Swap ${pendingSwap.id}: missing server public key in lockup details`
4694
- );
4695
- const claimDetails = await this.swapProvider.getChainClaimDetails(
4696
- pendingSwap.id
4697
- );
4623
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in lockup details`);
4624
+ const claimDetails = await this.swapProvider.getChainClaimDetails(pendingSwap.id);
4698
4625
  const serverPubKey = pendingSwap.response.lockupDetails.serverPublicKey;
4699
4626
  if (claimDetails.publicKey !== serverPubKey) {
4700
4627
  throw new Error(
@@ -4710,9 +4637,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4710
4637
  );
4711
4638
  const musigNonces = musig.message(import_base9.hex.decode(claimDetails.transactionHash)).generateNonce().aggregateNonces([
4712
4639
  [
4713
- import_base9.hex.decode(
4714
- pendingSwap.response.lockupDetails.serverPublicKey
4715
- ),
4640
+ import_base9.hex.decode(pendingSwap.response.lockupDetails.serverPublicKey),
4716
4641
  import_base9.hex.decode(claimDetails.pubNonce)
4717
4642
  ]
4718
4643
  ]).initializeSession();
@@ -4731,10 +4656,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4731
4656
  * @returns The transaction ID of the claim.
4732
4657
  */
4733
4658
  async waitAndClaimChain(pendingSwap) {
4734
- if (pendingSwap.request.to === "ARK")
4735
- return this.waitAndClaimArk(pendingSwap);
4736
- if (pendingSwap.request.to === "BTC")
4737
- return this.waitAndClaimBtc(pendingSwap);
4659
+ if (pendingSwap.request.to === "ARK") return this.waitAndClaimArk(pendingSwap);
4660
+ if (pendingSwap.request.to === "BTC") return this.waitAndClaimBtc(pendingSwap);
4738
4661
  throw new SwapError({
4739
4662
  message: `Unsupported swap destination: ${pendingSwap.request.to}`
4740
4663
  });
@@ -4749,11 +4672,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4749
4672
  */
4750
4673
  async createChainSwap(args) {
4751
4674
  const { to, from, receiverLockAmount, senderLockAmount, toAddress } = args;
4752
- if (!toAddress)
4753
- throw new SwapError({ message: "Destination address is required" });
4675
+ if (!toAddress) throw new SwapError({ message: "Destination address is required" });
4754
4676
  const feeSatsPerByte = args.feeSatsPerByte ?? 1;
4755
- if (feeSatsPerByte <= 0)
4756
- throw new SwapError({ message: "Invalid feeSatsPerByte" });
4677
+ if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" });
4757
4678
  let amount, serverLockAmount, userLockAmount;
4758
4679
  if (receiverLockAmount) {
4759
4680
  amount = receiverLockAmount;
@@ -4768,8 +4689,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4768
4689
  }
4769
4690
  const preimage = (0, import_utils3.randomBytes)(32);
4770
4691
  const preimageHash = import_base9.hex.encode((0, import_sha23.sha256)(preimage));
4771
- if (!preimageHash)
4772
- throw new SwapError({ message: "Failed to get preimage hash" });
4692
+ if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" });
4773
4693
  const ephemeralKey = import_secp256k13.secp256k1.utils.randomSecretKey();
4774
4694
  const refundPublicKey = to === "ARK" ? import_base9.hex.encode(import_secp256k13.secp256k1.getPublicKey(ephemeralKey)) : import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
4775
4695
  if (!refundPublicKey)
@@ -4818,30 +4738,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
4818
4738
  const { to, from, swap, arkInfo } = args;
4819
4739
  if (from === "ARK") {
4820
4740
  if (!swap.response.lockupDetails.serverPublicKey)
4821
- throw new Error(
4822
- `Swap ${swap.id}: missing serverPublicKey in lockup details`
4823
- );
4741
+ throw new Error(`Swap ${swap.id}: missing serverPublicKey in lockup details`);
4824
4742
  if (!swap.response.lockupDetails.timeouts)
4825
- throw new Error(
4826
- `Swap ${swap.id}: missing timeouts in lockup details`
4827
- );
4743
+ throw new Error(`Swap ${swap.id}: missing timeouts in lockup details`);
4828
4744
  }
4829
4745
  if (to === "ARK") {
4830
4746
  if (!swap.response.claimDetails.serverPublicKey)
4831
- throw new Error(
4832
- `Swap ${swap.id}: missing serverPublicKey in claim details`
4833
- );
4747
+ throw new Error(`Swap ${swap.id}: missing serverPublicKey in claim details`);
4834
4748
  if (!swap.response.claimDetails.timeouts)
4835
- throw new Error(
4836
- `Swap ${swap.id}: missing timeouts in claim details`
4837
- );
4749
+ throw new Error(`Swap ${swap.id}: missing timeouts in claim details`);
4838
4750
  }
4839
4751
  const lockupAddress = to === "ARK" ? swap.response.claimDetails.lockupAddress : swap.response.lockupDetails.lockupAddress;
4840
4752
  const receiverPubkey = to === "ARK" ? swap.request.claimPublicKey : swap.response.lockupDetails.serverPublicKey;
4841
4753
  const senderPubkey = to === "ARK" ? swap.response.claimDetails.serverPublicKey : swap.request.refundPublicKey;
4842
- const serverPubkey = import_base9.hex.encode(
4843
- normalizeToXOnlyKey(arkInfo.signerPubkey, "server")
4844
- );
4754
+ const serverPubkey = import_base9.hex.encode(normalizeToXOnlyKey(arkInfo.signerPubkey, "server"));
4845
4755
  const vhtlcTimeouts = to === "ARK" ? swap.response.claimDetails.timeouts : swap.response.lockupDetails.timeouts;
4846
4756
  const { vhtlcAddress } = this.createVHTLCScript({
4847
4757
  network: arkInfo.network,
@@ -4859,15 +4769,122 @@ var ArkadeSwaps = class _ArkadeSwaps {
4859
4769
  return true;
4860
4770
  }
4861
4771
  /**
4862
- * Renegotiates the quote for an existing swap.
4772
+ * Renegotiates the quote for an existing chain swap. Convenience wrapper
4773
+ * over `getSwapQuote` + `acceptSwapQuote` with a safety floor.
4774
+ *
4775
+ * The floor is resolved in order:
4776
+ * 1. `options.minAcceptableAmount` if provided.
4777
+ * 2. The original `response.claimDetails.amount` of the stored
4778
+ * pending swap (Boltz-confirmed server-lock amount at creation).
4779
+ * 3. Otherwise throws `QuoteRejectedError({ reason: "no_baseline" })`.
4780
+ *
4781
+ * `options.maxSlippageBps` (default 0) relaxes the floor by basis points.
4782
+ * Quotes ≤ 0 are always rejected. On rejection the acceptance is NOT
4783
+ * posted to Boltz.
4784
+ *
4785
+ * Prefer `getSwapQuote` / `acceptSwapQuote` for callers that want to
4786
+ * inspect the quote before committing.
4787
+ *
4863
4788
  * @param swapId - The ID of the swap.
4789
+ * @param options - Optional floor and slippage configuration.
4864
4790
  * @returns The accepted quote amount.
4791
+ * @throws QuoteRejectedError if the quote is non-positive, below the
4792
+ * effective floor, or no baseline is available.
4793
+ */
4794
+ async quoteSwap(swapId, options) {
4795
+ const effectiveFloor = await this.resolveEffectiveFloor(swapId, options);
4796
+ const amount = await this.getSwapQuote(swapId);
4797
+ this.validateQuote(amount, effectiveFloor);
4798
+ await this.swapProvider.postChainQuote(swapId, { amount });
4799
+ return amount;
4800
+ }
4801
+ /**
4802
+ * Fetches a renegotiated quote from Boltz without accepting it.
4803
+ * Pair with `acceptSwapQuote` to commit a specific value.
4865
4804
  */
4866
- async quoteSwap(swapId) {
4805
+ async getSwapQuote(swapId) {
4867
4806
  const { amount } = await this.swapProvider.getChainQuote(swapId);
4807
+ return amount;
4808
+ }
4809
+ /**
4810
+ * Accepts a quote amount for an existing chain swap, after validating it
4811
+ * against the configured floor. See `quoteSwap` for floor-resolution rules.
4812
+ *
4813
+ * @throws QuoteRejectedError if `amount` ≤ 0, below the effective floor,
4814
+ * or no baseline is available.
4815
+ */
4816
+ async acceptSwapQuote(swapId, amount, options) {
4817
+ const effectiveFloor = await this.resolveEffectiveFloor(swapId, options);
4818
+ this.validateQuote(amount, effectiveFloor);
4868
4819
  await this.swapProvider.postChainQuote(swapId, { amount });
4869
4820
  return amount;
4870
4821
  }
4822
+ async resolveEffectiveFloor(swapId, options) {
4823
+ this.validateQuoteOptions(options);
4824
+ const floor = await this.resolveQuoteFloor(swapId, options);
4825
+ const slippageBps = options?.maxSlippageBps ?? 0;
4826
+ const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
4827
+ if (effectiveFloor < 1) {
4828
+ throw new TypeError(
4829
+ `Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
4830
+ );
4831
+ }
4832
+ return effectiveFloor;
4833
+ }
4834
+ async resolveQuoteFloor(swapId, options) {
4835
+ if (options?.minAcceptableAmount !== void 0) {
4836
+ return options.minAcceptableAmount;
4837
+ }
4838
+ const swaps = await this.swapRepository.getAllSwaps({
4839
+ id: swapId,
4840
+ type: "chain"
4841
+ });
4842
+ const stored = swaps[0];
4843
+ const amount = stored?.response?.claimDetails?.amount;
4844
+ if (typeof amount !== "number") {
4845
+ throw new QuoteRejectedError({ reason: "no_baseline" });
4846
+ }
4847
+ return amount;
4848
+ }
4849
+ validateQuoteOptions(options) {
4850
+ if (options?.minAcceptableAmount !== void 0) {
4851
+ const v = options.minAcceptableAmount;
4852
+ if (!Number.isInteger(v) || v <= 0) {
4853
+ throw new TypeError(
4854
+ `Invalid minAcceptableAmount: ${v} \u2014 must be a positive integer`
4855
+ );
4856
+ }
4857
+ }
4858
+ if (options?.maxSlippageBps !== void 0) {
4859
+ const v = options.maxSlippageBps;
4860
+ if (!Number.isInteger(v) || v < 0 || v > 1e4) {
4861
+ throw new TypeError(
4862
+ `Invalid maxSlippageBps: ${v} \u2014 must be an integer in [0, 10000]`
4863
+ );
4864
+ }
4865
+ }
4866
+ }
4867
+ validateQuote(amount, effectiveFloor) {
4868
+ if (!Number.isSafeInteger(amount)) {
4869
+ throw new QuoteRejectedError({
4870
+ reason: "non_safe_integer",
4871
+ quotedAmount: amount
4872
+ });
4873
+ }
4874
+ if (amount <= 0) {
4875
+ throw new QuoteRejectedError({
4876
+ reason: "non_positive",
4877
+ quotedAmount: amount
4878
+ });
4879
+ }
4880
+ if (amount < effectiveFloor) {
4881
+ throw new QuoteRejectedError({
4882
+ reason: "below_floor",
4883
+ quotedAmount: amount,
4884
+ floor: effectiveFloor
4885
+ });
4886
+ }
4887
+ }
4871
4888
  // =========================================================================
4872
4889
  // Shared utilities
4873
4890
  // =========================================================================
@@ -4881,14 +4898,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4881
4898
  * @returns The commitment transaction ID.
4882
4899
  */
4883
4900
  async joinBatch(identity, input, output, arkInfo, isRecoverable2 = true) {
4884
- return joinBatch(
4885
- this.arkProvider,
4886
- identity,
4887
- input,
4888
- output,
4889
- arkInfo,
4890
- isRecoverable2
4891
- );
4901
+ return joinBatch(this.arkProvider, identity, input, output, arkInfo, isRecoverable2);
4892
4902
  }
4893
4903
  /**
4894
4904
  * Creates a VHTLC script for the swap.
@@ -4926,9 +4936,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4926
4936
  async getPendingSubmarineSwaps() {
4927
4937
  const swaps = await this.getPendingSubmarineSwapsFromStorage();
4928
4938
  if (!swaps) return [];
4929
- return swaps.filter(
4930
- (swap) => swap.status === "invoice.set"
4931
- );
4939
+ return swaps.filter((swap) => swap.status === "invoice.set");
4932
4940
  }
4933
4941
  /**
4934
4942
  * Returns pending reverse swaps (those with status `swap.created`).
@@ -4936,9 +4944,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4936
4944
  async getPendingReverseSwaps() {
4937
4945
  const swaps = await this.getPendingReverseSwapsFromStorage();
4938
4946
  if (!swaps) return [];
4939
- return swaps.filter(
4940
- (swap) => swap.status === "swap.created"
4941
- );
4947
+ return swaps.filter((swap) => swap.status === "swap.created");
4942
4948
  }
4943
4949
  /**
4944
4950
  * Returns pending chain swaps (those with status `swap.created`).
@@ -4977,10 +4983,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4977
4983
  this.savePendingReverseSwap.bind(this)
4978
4984
  )
4979
4985
  ).catch((error) => {
4980
- logger.error(
4981
- `Failed to refresh swap status for ${swap.id}:`,
4982
- error
4983
- );
4986
+ logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
4984
4987
  })
4985
4988
  );
4986
4989
  }
@@ -4994,23 +4997,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
4994
4997
  this.savePendingSubmarineSwap.bind(this)
4995
4998
  )
4996
4999
  ).catch((error) => {
4997
- logger.error(
4998
- `Failed to refresh swap status for ${swap.id}:`,
4999
- error
5000
- );
5000
+ logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
5001
5001
  })
5002
5002
  );
5003
5003
  }
5004
5004
  for (const swap of await this.getPendingChainSwapsFromStorage()) {
5005
5005
  if (isChainFinalStatus(swap.status)) continue;
5006
5006
  promises.push(
5007
- this.getSwapStatus(swap.id).then(
5008
- ({ status }) => this.savePendingChainSwap({ ...swap, status })
5009
- ).catch((error) => {
5010
- logger.error(
5011
- `Failed to refresh swap status for ${swap.id}:`,
5012
- error
5013
- );
5007
+ this.getSwapStatus(swap.id).then(({ status }) => this.savePendingChainSwap({ ...swap, status })).catch((error) => {
5008
+ logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
5014
5009
  })
5015
5010
  );
5016
5011
  }
@@ -5027,9 +5022,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
5027
5022
  * display/monitoring and are not automatically wired into the SwapManager.
5028
5023
  */
5029
5024
  async restoreSwaps(boltzFees) {
5030
- const publicKey = import_base9.hex.encode(
5031
- await this.wallet.identity.compressedPublicKey()
5032
- );
5025
+ const publicKey = import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
5033
5026
  if (!publicKey) throw new Error("Failed to get public key from wallet");
5034
5027
  const fees = boltzFees ?? await this.swapProvider.getFees();
5035
5028
  const chainSwaps = [];
@@ -5081,25 +5074,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
5081
5074
  preimage: ""
5082
5075
  });
5083
5076
  } else if (isRestoredSubmarineSwap(swap)) {
5084
- const {
5085
- amount,
5086
- lockupAddress,
5087
- serverPublicKey,
5088
- tree,
5089
- timeoutBlockHeights
5090
- } = swap.refundDetails;
5077
+ const { amount, lockupAddress, serverPublicKey, tree, timeoutBlockHeights } = swap.refundDetails;
5091
5078
  let preimage = "";
5092
5079
  if (!isSubmarineFinalStatus(status)) {
5093
5080
  try {
5094
- const data = await this.swapProvider.getSwapPreimage(
5095
- swap.id
5096
- );
5081
+ const data = await this.swapProvider.getSwapPreimage(swap.id);
5097
5082
  preimage = data.preimage;
5098
5083
  } catch (error) {
5099
- logger.warn(
5100
- `Failed to restore preimage for submarine swap ${id}`,
5101
- error
5102
- );
5084
+ logger.warn(`Failed to restore preimage for submarine swap ${id}`, error);
5103
5085
  }
5104
5086
  }
5105
5087
  submarineSwaps.push({
@@ -5137,12 +5119,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
5137
5119
  } else if (isRestoredChainSwap(swap)) {
5138
5120
  const refundDetails = swap.refundDetails;
5139
5121
  if (!refundDetails) continue;
5140
- const {
5141
- amount,
5142
- lockupAddress,
5143
- serverPublicKey,
5144
- timeoutBlockHeight
5145
- } = refundDetails;
5122
+ const { amount, lockupAddress, serverPublicKey, timeoutBlockHeight } = refundDetails;
5146
5123
  chainSwaps.push({
5147
5124
  id,
5148
5125
  type: "chain",
@@ -5195,19 +5172,11 @@ var SWAP_POLL_TASK_TYPE = "swap-poll";
5195
5172
  var swapsPollProcessor = {
5196
5173
  taskType: SWAP_POLL_TASK_TYPE,
5197
5174
  async execute(item, deps) {
5198
- const {
5199
- swapRepository,
5200
- swapProvider,
5201
- wallet,
5202
- arkProvider,
5203
- indexerProvider
5204
- } = deps;
5175
+ const { swapRepository, swapProvider, wallet, arkProvider, indexerProvider } = deps;
5205
5176
  const allSwaps = await swapRepository.getAllSwaps();
5206
5177
  const pendingSwaps = allSwaps.filter((swap) => {
5207
- if (isPendingReverseSwap(swap))
5208
- return !isReverseFinalStatus(swap.status);
5209
- if (isPendingSubmarineSwap(swap))
5210
- return !isSubmarineFinalStatus(swap.status);
5178
+ if (isPendingReverseSwap(swap)) return !isReverseFinalStatus(swap.status);
5179
+ if (isPendingSubmarineSwap(swap)) return !isSubmarineFinalStatus(swap.status);
5211
5180
  return false;
5212
5181
  });
5213
5182
  let polled = 0;
@@ -5237,19 +5206,14 @@ var swapsPollProcessor = {
5237
5206
  }
5238
5207
  if (isPendingReverseSwap(swap) && isReverseClaimableStatus(currentStatus)) {
5239
5208
  if (!swap.preimage) {
5240
- logger.warn(
5241
- `[swap-poll] Skipping claim for ${swap.id}: no preimage`
5242
- );
5209
+ logger.warn(`[swap-poll] Skipping claim for ${swap.id}: no preimage`);
5243
5210
  continue;
5244
5211
  }
5245
5212
  try {
5246
5213
  await tempSwaps.claimVHTLC(swap);
5247
5214
  claimed++;
5248
5215
  } catch (claimError) {
5249
- logger.error(
5250
- `[swap-poll] Claim failed for ${swap.id}:`,
5251
- claimError
5252
- );
5216
+ logger.error(`[swap-poll] Claim failed for ${swap.id}:`, claimError);
5253
5217
  errors++;
5254
5218
  }
5255
5219
  }
@@ -5265,18 +5229,12 @@ var swapsPollProcessor = {
5265
5229
  await tempSwaps.refundVHTLC(swapWithStatus);
5266
5230
  refunded++;
5267
5231
  } catch (refundError) {
5268
- logger.error(
5269
- `[swap-poll] Refund failed for ${swap.id}:`,
5270
- refundError
5271
- );
5232
+ logger.error(`[swap-poll] Refund failed for ${swap.id}:`, refundError);
5272
5233
  errors++;
5273
5234
  }
5274
5235
  }
5275
5236
  } catch (swapError) {
5276
- logger.error(
5277
- `[swap-poll] Error processing swap ${swap.id}:`,
5278
- swapError
5279
- );
5237
+ logger.error(`[swap-poll] Error processing swap ${swap.id}:`, swapError);
5280
5238
  errors++;
5281
5239
  }
5282
5240
  }
@@ -5293,9 +5251,6 @@ var swapsPollProcessor = {
5293
5251
  };
5294
5252
 
5295
5253
  // src/expo/background.ts
5296
- function getRandomId() {
5297
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
5298
- }
5299
5254
  function createBackgroundWalletShim(args) {
5300
5255
  const notImplemented = (method) => {
5301
5256
  throw new Error(
@@ -5311,6 +5266,7 @@ function createBackgroundWalletShim(args) {
5311
5266
  getBoardingUtxos: async () => notImplemented("getBoardingUtxos"),
5312
5267
  getTransactionHistory: async () => notImplemented("getTransactionHistory"),
5313
5268
  getContractManager: async () => notImplemented("getContractManager"),
5269
+ getDelegateManager: async () => notImplemented("getDelegateManager"),
5314
5270
  getDelegatorManager: async () => notImplemented("getDelegatorManager"),
5315
5271
  sendBitcoin: async () => notImplemented("sendBitcoin"),
5316
5272
  send: async () => notImplemented("send"),
@@ -5330,9 +5286,7 @@ function defineExpoSwapBackgroundTask(taskName, options) {
5330
5286
  }
5331
5287
  const identity = await identityFactory();
5332
5288
  const arkProvider = new import_expo2.ExpoArkProvider(config.arkServerUrl);
5333
- const indexerProvider = new import_expo2.ExpoIndexerProvider(
5334
- config.arkServerUrl
5335
- );
5289
+ const indexerProvider = new import_expo2.ExpoIndexerProvider(config.arkServerUrl);
5336
5290
  const swapProvider = new BoltzSwapProvider({
5337
5291
  network: config.network,
5338
5292
  apiUrl: config.boltzApiUrl
@@ -5347,11 +5301,7 @@ function defineExpoSwapBackgroundTask(taskName, options) {
5347
5301
  const serverPubKey = hex9.decode(info.signerPubkey);
5348
5302
  const xOnlyServerPubKey = serverPubKey.length === 33 ? serverPubKey.slice(1) : serverPubKey;
5349
5303
  const hrp = info.network === "bitcoin" ? "ark" : "tark";
5350
- return new ArkAddress3(
5351
- xOnlyServerPubKey,
5352
- pubkey,
5353
- hrp
5354
- ).encode();
5304
+ return new ArkAddress3(xOnlyServerPubKey, pubkey, hrp).encode();
5355
5305
  }
5356
5306
  });
5357
5307
  const deps = {
@@ -5365,14 +5315,12 @@ function defineExpoSwapBackgroundTask(taskName, options) {
5365
5315
  await (0, import_expo.runTasks)(taskQueue, [swapsPollProcessor], deps);
5366
5316
  const results = await taskQueue.getResults();
5367
5317
  if (results.length > 0) {
5368
- await taskQueue.acknowledgeResults(
5369
- results.map((r) => r.id)
5370
- );
5318
+ await taskQueue.acknowledgeResults(results.map((r) => r.id));
5371
5319
  }
5372
5320
  const existing = await taskQueue.getTasks(SWAP_POLL_TASK_TYPE);
5373
5321
  if (existing.length === 0) {
5374
5322
  const task = {
5375
- id: getRandomId(),
5323
+ id: (0, import_sdk9.getRandomId)(),
5376
5324
  type: SWAP_POLL_TASK_TYPE,
5377
5325
  data: {},
5378
5326
  createdAt: Date.now()