@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
@@ -45,7 +45,10 @@ var SwapError = class extends Error {
45
45
  /** The pending swap associated with this error, if available. */
46
46
  pendingSwap;
47
47
  constructor(options = {}) {
48
- super(options.message ?? "Error during swap.");
48
+ super(
49
+ options.message ?? "Error during swap.",
50
+ options.cause !== void 0 ? { cause: options.cause } : void 0
51
+ );
49
52
  this.name = "SwapError";
50
53
  this.isClaimable = options.isClaimable ?? false;
51
54
  this.isRefundable = options.isRefundable ?? false;
@@ -122,6 +125,100 @@ var TransactionRefundedError = class extends SwapError {
122
125
  this.name = "TransactionRefundedError";
123
126
  }
124
127
  };
128
+ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
129
+ reason;
130
+ quotedAmount;
131
+ floor;
132
+ constructor(options) {
133
+ super({
134
+ message: options.message ?? _QuoteRejectedError.defaultMessage(options),
135
+ ...options
136
+ });
137
+ this.name = "QuoteRejectedError";
138
+ this.reason = options.reason;
139
+ this.quotedAmount = "quotedAmount" in options ? options.quotedAmount : void 0;
140
+ this.floor = "floor" in options ? options.floor : void 0;
141
+ }
142
+ static defaultMessage(options) {
143
+ switch (options.reason) {
144
+ case "below_floor":
145
+ return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
146
+ case "non_positive":
147
+ return `Boltz quote ${options.quotedAmount} is not positive`;
148
+ case "non_safe_integer":
149
+ return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
150
+ case "no_baseline":
151
+ return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
152
+ }
153
+ }
154
+ /**
155
+ * Serialize into a plain `Error` whose `.message` carries the full
156
+ * rejection payload as JSON behind a marker prefix. Structured clone
157
+ * (used by `postMessage` between page and service worker) preserves
158
+ * `Error.message` reliably but strips custom `.name` and own properties,
159
+ * so we move the typed data into the message field for transport.
160
+ */
161
+ toTransportError() {
162
+ return new Error(
163
+ QUOTE_REJECTION_TRANSPORT_PREFIX + JSON.stringify({
164
+ reason: this.reason,
165
+ message: this.message,
166
+ quotedAmount: this.quotedAmount,
167
+ floor: this.floor
168
+ })
169
+ );
170
+ }
171
+ /**
172
+ * Inverse of `toTransportError`. Returns a real `QuoteRejectedError` if
173
+ * `error` carries the transport prefix, else `null`.
174
+ */
175
+ static fromTransportError(error) {
176
+ if (!(error instanceof Error) || !error.message.startsWith(QUOTE_REJECTION_TRANSPORT_PREFIX)) {
177
+ return null;
178
+ }
179
+ const payload = error.message.slice(QUOTE_REJECTION_TRANSPORT_PREFIX.length);
180
+ let data;
181
+ try {
182
+ data = JSON.parse(payload);
183
+ } catch {
184
+ return null;
185
+ }
186
+ if (typeof data.reason !== "string" || !QUOTE_REJECTION_REASONS.has(data.reason)) {
187
+ return null;
188
+ }
189
+ const message = typeof data.message === "string" ? data.message : void 0;
190
+ const reason = data.reason;
191
+ const quotedAmount = typeof data.quotedAmount === "number" ? data.quotedAmount : null;
192
+ const floor = typeof data.floor === "number" ? data.floor : null;
193
+ switch (reason) {
194
+ case "below_floor":
195
+ if (quotedAmount === null || floor === null) return null;
196
+ return new _QuoteRejectedError({
197
+ reason,
198
+ quotedAmount,
199
+ floor,
200
+ message
201
+ });
202
+ case "non_positive":
203
+ case "non_safe_integer":
204
+ if (quotedAmount === null) return null;
205
+ return new _QuoteRejectedError({
206
+ reason,
207
+ quotedAmount,
208
+ message
209
+ });
210
+ case "no_baseline":
211
+ return new _QuoteRejectedError({ reason, message });
212
+ }
213
+ }
214
+ };
215
+ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
216
+ var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
217
+ "below_floor",
218
+ "non_positive",
219
+ "non_safe_integer",
220
+ "no_baseline"
221
+ ]);
125
222
  var BoltzRefundError = class extends Error {
126
223
  constructor(message, cause) {
127
224
  super(message);
@@ -137,18 +234,10 @@ var import_sdk8 = require("@arkade-os/sdk");
137
234
  var import_sdk = require("@arkade-os/sdk");
138
235
  var import_base = require("@scure/base");
139
236
  var isSubmarineFinalStatus = (status) => {
140
- return [
141
- "invoice.failedToPay",
142
- "transaction.claimed",
143
- "swap.expired"
144
- ].includes(status);
237
+ return ["invoice.failedToPay", "transaction.claimed", "swap.expired"].includes(status);
145
238
  };
146
239
  var isSubmarineRefundableStatus = (status) => {
147
- return [
148
- "invoice.failedToPay",
149
- "transaction.lockupFailed",
150
- "swap.expired"
151
- ].includes(status);
240
+ return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status);
152
241
  };
153
242
  var isSubmarineSuccessStatus = (status) => {
154
243
  return status === "transaction.claimed";
@@ -167,10 +256,7 @@ var isReverseClaimableStatus = (status) => {
167
256
  return ["transaction.mempool", "transaction.confirmed"].includes(status);
168
257
  };
169
258
  var isChainClaimableStatus = (status) => {
170
- return [
171
- "transaction.server.mempool",
172
- "transaction.server.confirmed"
173
- ].includes(status);
259
+ return ["transaction.server.mempool", "transaction.server.confirmed"].includes(status);
174
260
  };
175
261
  var isChainFinalStatus = (status) => {
176
262
  return [
@@ -284,8 +370,7 @@ var BASE_URLS = {
284
370
  var isSwapNotFoundBody = (error) => {
285
371
  const needle = "could not find swap";
286
372
  const fromJson = error.errorData?.error;
287
- if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle))
288
- return true;
373
+ if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle)) return true;
289
374
  return error.message.toLowerCase().includes(needle);
290
375
  };
291
376
  var BoltzSwapProvider = class {
@@ -299,10 +384,7 @@ var BoltzSwapProvider = class {
299
384
  this.network = config.network;
300
385
  this.referralId = config.referralId ?? "arkade-ts-sdk";
301
386
  const apiUrl = config.apiUrl || BASE_URLS[config.network];
302
- if (!apiUrl)
303
- throw new Error(
304
- `API URL is required for network: ${config.network}`
305
- );
387
+ if (!apiUrl) throw new Error(`API URL is required for network: ${config.network}`);
306
388
  this.apiUrl = apiUrl;
307
389
  this.wsUrl = this.apiUrl.replace(/^http(s)?:\/\//, "ws$1://").replace("9069", "9004") + "/v2/ws";
308
390
  }
@@ -321,10 +403,7 @@ var BoltzSwapProvider = class {
321
403
  /** Returns current Lightning swap fees (submarine + reverse) from Boltz. */
322
404
  async getFees() {
323
405
  const [submarine, reverse] = await Promise.all([
324
- this.request(
325
- "/v2/swap/submarine",
326
- "GET"
327
- ),
406
+ this.request("/v2/swap/submarine", "GET"),
328
407
  this.request("/v2/swap/reverse", "GET")
329
408
  ]);
330
409
  if (!isGetSubmarinePairsResponse(submarine))
@@ -344,10 +423,7 @@ var BoltzSwapProvider = class {
344
423
  }
345
424
  /** Returns current Lightning swap min/max limits from Boltz. */
346
425
  async getLimits() {
347
- const response = await this.request(
348
- "/v2/swap/submarine",
349
- "GET"
350
- );
426
+ const response = await this.request("/v2/swap/submarine", "GET");
351
427
  if (!isGetSubmarinePairsResponse(response))
352
428
  throw new SchemaError({ message: "error fetching limits" });
353
429
  return {
@@ -357,10 +433,7 @@ var BoltzSwapProvider = class {
357
433
  }
358
434
  /** Returns the current BTC chain tip height from Boltz. */
359
435
  async getChainHeight() {
360
- const response = await this.request(
361
- "/v2/chain/heights",
362
- "GET"
363
- );
436
+ const response = await this.request("/v2/chain/heights", "GET");
364
437
  if (typeof response?.BTC !== "number")
365
438
  throw new SchemaError({
366
439
  message: "error fetching chain heights"
@@ -390,10 +463,7 @@ var BoltzSwapProvider = class {
390
463
  async getSwapStatus(id) {
391
464
  let response;
392
465
  try {
393
- response = await this.request(
394
- `/v2/swap/${id}`,
395
- "GET"
396
- );
466
+ response = await this.request(`/v2/swap/${id}`, "GET");
397
467
  } catch (error) {
398
468
  if (error instanceof NetworkError && error.statusCode === 404 && isSwapNotFoundBody(error)) {
399
469
  throw new SwapNotFoundError(id, error.errorData);
@@ -489,12 +559,10 @@ var BoltzSwapProvider = class {
489
559
  throw new SwapError({ message: "Invalid 'to' chain" });
490
560
  if (["BTC", "ARK"].indexOf(from) === -1)
491
561
  throw new SwapError({ message: "Invalid 'from' chain" });
492
- if (to === from)
493
- throw new SwapError({ message: "Invalid swap direction" });
562
+ if (to === from) throw new SwapError({ message: "Invalid swap direction" });
494
563
  if (!preimageHash || preimageHash.length != 64)
495
564
  throw new SwapError({ message: "Invalid preimageHash" });
496
- if (feeSatsPerByte <= 0)
497
- throw new SwapError({ message: "Invalid feeSatsPerByte" });
565
+ if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" });
498
566
  if (serverLockAmount !== void 0 && userLockAmount !== void 0 || serverLockAmount === void 0 && userLockAmount === void 0)
499
567
  throw new SwapError({
500
568
  message: "Either serverLockAmount or userLockAmount must be provided"
@@ -549,12 +617,8 @@ var BoltzSwapProvider = class {
549
617
  message: "Error refunding submarine swap"
550
618
  });
551
619
  return {
552
- transaction: import_sdk.Transaction.fromPSBT(
553
- import_base.base64.decode(response.transaction)
554
- ),
555
- checkpoint: import_sdk.Transaction.fromPSBT(
556
- import_base.base64.decode(response.checkpoint)
557
- )
620
+ transaction: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.transaction)),
621
+ checkpoint: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.checkpoint))
558
622
  };
559
623
  }
560
624
  /** Requests Boltz co-signature for a chain swap refund. Returns signed transaction + checkpoint. */
@@ -573,53 +637,53 @@ var BoltzSwapProvider = class {
573
637
  message: "Error refunding chain swap"
574
638
  });
575
639
  return {
576
- transaction: import_sdk.Transaction.fromPSBT(
577
- import_base.base64.decode(response.transaction)
578
- ),
579
- checkpoint: import_sdk.Transaction.fromPSBT(
580
- import_base.base64.decode(response.checkpoint)
581
- )
640
+ transaction: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.transaction)),
641
+ checkpoint: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.checkpoint))
582
642
  };
583
643
  }
584
- /** Monitors swap status updates via WebSocket. Calls update callback on each status change. Resolves when terminal. */
644
+ /**
645
+ * Monitors swap status updates and forwards them to the update callback.
646
+ * Prefers a WebSocket subscription; on connection error or premature close
647
+ * it falls back to REST polling so callers don't fail when the WS endpoint
648
+ * is flaky (observed in CI). Resolves when the swap reaches a terminal
649
+ * status.
650
+ */
585
651
  async monitorSwap(swapId, update) {
586
652
  return new Promise((resolve, reject) => {
587
- const webSocket = new globalThis.WebSocket(this.wsUrl);
588
- const connectionTimeout = setTimeout(() => {
589
- webSocket.close();
590
- reject(new NetworkError("WebSocket connection timeout"));
591
- }, 3e4);
592
- webSocket.onerror = (error) => {
593
- clearTimeout(connectionTimeout);
594
- reject(
595
- new NetworkError(
596
- `WebSocket error: ${error.message}`
597
- )
598
- );
599
- };
600
- webSocket.onopen = () => {
601
- clearTimeout(connectionTimeout);
602
- webSocket.send(
603
- JSON.stringify({
604
- op: "subscribe",
605
- channel: "swap.update",
606
- args: [swapId]
607
- })
608
- );
653
+ let settled = false;
654
+ let lastStatus = null;
655
+ let pollTimer = null;
656
+ let webSocket = null;
657
+ let connectionTimeout = null;
658
+ const cleanup = () => {
659
+ if (connectionTimeout) {
660
+ clearTimeout(connectionTimeout);
661
+ connectionTimeout = null;
662
+ }
663
+ if (pollTimer) {
664
+ clearInterval(pollTimer);
665
+ pollTimer = null;
666
+ }
667
+ if (webSocket) {
668
+ try {
669
+ webSocket.close();
670
+ } catch {
671
+ }
672
+ webSocket = null;
673
+ }
609
674
  };
610
- webSocket.onclose = () => {
611
- clearTimeout(connectionTimeout);
612
- resolve();
675
+ const finish = (err) => {
676
+ if (settled) return;
677
+ settled = true;
678
+ cleanup();
679
+ if (err) reject(err);
680
+ else resolve();
613
681
  };
614
- webSocket.onmessage = async (rawMsg) => {
615
- const msg = JSON.parse(rawMsg.data);
616
- if (msg.event !== "update" || msg.args[0].id !== swapId) return;
617
- if (msg.args[0].error) {
618
- webSocket.close();
619
- reject(new SwapError({ message: msg.args[0].error }));
620
- }
621
- const status = msg.args[0].status;
622
- const negotiable = status === "transaction.lockupFailed" && msg.args[0].failureDetails?.actual !== void 0 && msg.args[0].failureDetails?.expected !== void 0;
682
+ const handleStatus = (status, data) => {
683
+ if (settled) return;
684
+ if (status === lastStatus) return;
685
+ lastStatus = status;
686
+ const negotiable = status === "transaction.lockupFailed" && data?.failureDetails?.actual !== void 0 && data?.failureDetails?.expected !== void 0;
623
687
  switch (status) {
624
688
  case "invoice.settled":
625
689
  case "transaction.claimed":
@@ -628,13 +692,13 @@ var BoltzSwapProvider = class {
628
692
  case "invoice.failedToPay":
629
693
  case "transaction.failed":
630
694
  case "swap.expired":
631
- webSocket.close();
632
- update(status, msg.args[0]);
633
- break;
695
+ update(status, data);
696
+ finish();
697
+ return;
634
698
  case "transaction.lockupFailed":
635
- if (!negotiable) webSocket.close();
636
- update(status, msg.args[0]);
637
- break;
699
+ update(status, data);
700
+ if (!negotiable) finish();
701
+ return;
638
702
  case "invoice.paid":
639
703
  case "invoice.pending":
640
704
  case "invoice.set":
@@ -644,9 +708,77 @@ var BoltzSwapProvider = class {
644
708
  case "transaction.claim.pending":
645
709
  case "transaction.server.mempool":
646
710
  case "transaction.server.confirmed":
647
- update(status, msg.args[0]);
711
+ update(status, data);
712
+ return;
648
713
  }
649
714
  };
715
+ const startPolling = () => {
716
+ if (settled || pollTimer) return;
717
+ const poll = async () => {
718
+ if (settled) return;
719
+ try {
720
+ const result = await this.getSwapStatus(swapId);
721
+ handleStatus(result.status, { ...result, id: swapId });
722
+ } catch {
723
+ }
724
+ };
725
+ pollTimer = setInterval(poll, 1e3);
726
+ poll();
727
+ };
728
+ try {
729
+ webSocket = new globalThis.WebSocket(this.wsUrl);
730
+ connectionTimeout = setTimeout(() => {
731
+ try {
732
+ webSocket?.close();
733
+ } catch {
734
+ }
735
+ webSocket = null;
736
+ startPolling();
737
+ }, 5e3);
738
+ webSocket.onerror = () => {
739
+ if (connectionTimeout) {
740
+ clearTimeout(connectionTimeout);
741
+ connectionTimeout = null;
742
+ }
743
+ webSocket = null;
744
+ startPolling();
745
+ };
746
+ webSocket.onopen = () => {
747
+ if (connectionTimeout) {
748
+ clearTimeout(connectionTimeout);
749
+ connectionTimeout = null;
750
+ }
751
+ webSocket?.send(
752
+ JSON.stringify({
753
+ op: "subscribe",
754
+ channel: "swap.update",
755
+ args: [swapId]
756
+ })
757
+ );
758
+ };
759
+ webSocket.onclose = () => {
760
+ if (connectionTimeout) {
761
+ clearTimeout(connectionTimeout);
762
+ connectionTimeout = null;
763
+ }
764
+ if (!settled && !pollTimer) startPolling();
765
+ };
766
+ webSocket.onmessage = (rawMsg) => {
767
+ try {
768
+ const msg = JSON.parse(rawMsg.data);
769
+ if (msg.event !== "update" || msg.args?.[0]?.id !== swapId) return;
770
+ if (msg.args[0].error) {
771
+ finish(new SwapError({ message: msg.args[0].error }));
772
+ return;
773
+ }
774
+ handleStatus(msg.args[0].status, msg.args[0]);
775
+ } catch {
776
+ }
777
+ };
778
+ } catch {
779
+ webSocket = null;
780
+ startPolling();
781
+ }
650
782
  });
651
783
  }
652
784
  /** Returns current chain swap fees for a given pair (e.g. ARK→BTC). */
@@ -654,10 +786,7 @@ var BoltzSwapProvider = class {
654
786
  if (from === to) {
655
787
  throw new SwapError({ message: "Invalid chain pair" });
656
788
  }
657
- const response = await this.request(
658
- "/v2/swap/chain",
659
- "GET"
660
- );
789
+ const response = await this.request("/v2/swap/chain", "GET");
661
790
  if (!isGetChainPairsResponse(response))
662
791
  throw new SchemaError({ message: "error fetching fees" });
663
792
  if (!response[from]?.[to]) {
@@ -672,10 +801,7 @@ var BoltzSwapProvider = class {
672
801
  if (from === to) {
673
802
  throw new SwapError({ message: "Invalid chain pair" });
674
803
  }
675
- const response = await this.request(
676
- "/v2/swap/chain",
677
- "GET"
678
- );
804
+ const response = await this.request("/v2/swap/chain", "GET");
679
805
  if (!isGetChainPairsResponse(response))
680
806
  throw new SchemaError({ message: "error fetching limits" });
681
807
  if (!response[from]?.[to]) {
@@ -804,9 +930,7 @@ var BoltzSwapProvider = class {
804
930
  return await response.json();
805
931
  } catch (error) {
806
932
  if (error instanceof NetworkError) throw error;
807
- throw new NetworkError(
808
- `Request to ${url} failed: ${error.message}`
809
- );
933
+ throw new NetworkError(`Request to ${url} failed: ${error.message}`);
810
934
  }
811
935
  }
812
936
  };
@@ -828,8 +952,7 @@ var findKeyIndex = (keys, target) => keys.findIndex((k) => (0, import_utils.equa
828
952
  var assertPublicKeys = (keys) => {
829
953
  const seen = /* @__PURE__ */ new Set();
830
954
  for (const key of keys) {
831
- if (key.length !== 33)
832
- throw new Error(`public key must be 33 bytes, got ${key.length}`);
955
+ if (key.length !== 33) throw new Error(`public key must be 33 bytes, got ${key.length}`);
833
956
  const enc = import_base2.hex.encode(key);
834
957
  if (seen.has(enc)) throw new Error(`duplicate public key ${enc}`);
835
958
  seen.add(enc);
@@ -837,9 +960,7 @@ var assertPublicKeys = (keys) => {
837
960
  };
838
961
  var aggregateKeys = (publicKeys, tweak) => {
839
962
  assertPublicKeys([...publicKeys]);
840
- return (0, import_musig2.keyAggExport)(
841
- (0, import_musig2.keyAggregate)([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : [])
842
- );
963
+ return (0, import_musig2.keyAggExport)((0, import_musig2.keyAggregate)([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : []));
843
964
  };
844
965
  var MusigKeyAgg = class _MusigKeyAgg {
845
966
  constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, internalKey, _tweak) {
@@ -886,18 +1007,12 @@ var MusigWithMessage = class {
886
1007
  this.msg = msg;
887
1008
  }
888
1009
  generateNonce() {
889
- const nonce = (0, import_musig2.nonceGen)(
890
- this.myPublicKey,
891
- this.privateKey,
892
- this.aggPubkey,
893
- this.msg
894
- );
1010
+ const nonce = (0, import_musig2.nonceGen)(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
895
1011
  return new MusigWithNonce(
896
1012
  this.privateKey,
897
1013
  this.myPublicKey,
898
1014
  this.publicKeys,
899
1015
  this.myIndex,
900
- this.aggPubkey,
901
1016
  this.tweak,
902
1017
  this.msg,
903
1018
  nonce
@@ -905,12 +1020,11 @@ var MusigWithMessage = class {
905
1020
  }
906
1021
  };
907
1022
  var MusigWithNonce = class {
908
- constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, tweak, msg, nonce) {
1023
+ constructor(privateKey, myPublicKey, publicKeys, myIndex, tweak, msg, nonce) {
909
1024
  this.privateKey = privateKey;
910
1025
  this.myPublicKey = myPublicKey;
911
1026
  this.publicKeys = publicKeys;
912
1027
  this.myIndex = myIndex;
913
- this.aggPubkey = aggPubkey;
914
1028
  this.tweak = tweak;
915
1029
  this.msg = msg;
916
1030
  this.nonce = nonce;
@@ -942,10 +1056,8 @@ var MusigWithNonce = class {
942
1056
  const aggregatedNonce = (0, import_musig2.nonceAggregate)([...ordered]);
943
1057
  return new MusigNoncesAggregated(
944
1058
  this.privateKey,
945
- this.myPublicKey,
946
1059
  this.publicKeys,
947
1060
  this.myIndex,
948
- this.aggPubkey,
949
1061
  this.tweak,
950
1062
  this.msg,
951
1063
  this.nonce,
@@ -955,12 +1067,10 @@ var MusigWithNonce = class {
955
1067
  }
956
1068
  };
957
1069
  var MusigNoncesAggregated = class {
958
- constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, tweak, msg, nonce, pubNonces, aggregatedNonce) {
1070
+ constructor(privateKey, publicKeys, myIndex, tweak, msg, nonce, pubNonces, aggregatedNonce) {
959
1071
  this.privateKey = privateKey;
960
- this.myPublicKey = myPublicKey;
961
1072
  this.publicKeys = publicKeys;
962
1073
  this.myIndex = myIndex;
963
- this.aggPubkey = aggPubkey;
964
1074
  this.tweak = tweak;
965
1075
  this.msg = msg;
966
1076
  this.nonce = nonce;
@@ -1006,11 +1116,7 @@ var MusigSession = class {
1006
1116
  const index = typeof publicKeyOrIndex === "number" ? publicKeyOrIndex : findKeyIndex(this.publicKeys, publicKeyOrIndex);
1007
1117
  if (index < 0 || index >= this.publicKeys.length)
1008
1118
  throw new Error("public key not found or index out of range");
1009
- if (!this.session.partialSigVerify(
1010
- signature,
1011
- [...this.pubNonces],
1012
- index
1013
- )) {
1119
+ if (!this.session.partialSigVerify(signature, [...this.pubNonces], index)) {
1014
1120
  throw new Error("invalid partial signature");
1015
1121
  }
1016
1122
  this.partialSignatures[index] = signature;
@@ -1019,12 +1125,7 @@ var MusigSession = class {
1019
1125
  signPartial() {
1020
1126
  const sig = this.session.sign(this.nonce.secret, this.privateKey, true);
1021
1127
  this.partialSignatures[this.myIndex] = sig;
1022
- return new MusigSigned(
1023
- this.session,
1024
- [...this.partialSignatures],
1025
- sig,
1026
- this.nonce.public
1027
- );
1128
+ return new MusigSigned(this.session, [...this.partialSignatures], sig, this.nonce.public);
1028
1129
  }
1029
1130
  };
1030
1131
  var MusigSigned = class {
@@ -1038,14 +1139,11 @@ var MusigSigned = class {
1038
1139
  if (this.partialSignatures.some((s) => s === null)) {
1039
1140
  throw new Error("not all partial signatures are set");
1040
1141
  }
1041
- return this.session.partialSigAgg(
1042
- this.partialSignatures
1043
- );
1142
+ return this.session.partialSigAgg(this.partialSignatures);
1044
1143
  }
1045
1144
  };
1046
1145
  var create = (privateKey, publicKeys) => {
1047
- if (publicKeys.length < 2)
1048
- throw new Error("need at least 2 keys to aggregate");
1146
+ if (publicKeys.length < 2) throw new Error("need at least 2 keys to aggregate");
1049
1147
  const keys = [...publicKeys];
1050
1148
  assertPublicKeys(keys);
1051
1149
  Object.freeze(keys);
@@ -1053,14 +1151,7 @@ var create = (privateKey, publicKeys) => {
1053
1151
  const myIndex = findKeyIndex(keys, myPublicKey);
1054
1152
  if (myIndex === -1) throw new Error("our key is not in publicKeys");
1055
1153
  const aggPubkey = aggregateKeys(keys);
1056
- return new MusigKeyAgg(
1057
- privateKey,
1058
- myPublicKey,
1059
- keys,
1060
- myIndex,
1061
- aggPubkey,
1062
- aggPubkey
1063
- );
1154
+ return new MusigKeyAgg(privateKey, myPublicKey, keys, myIndex, aggPubkey, aggPubkey);
1064
1155
  };
1065
1156
 
1066
1157
  // src/utils/boltz-swap-tx.ts
@@ -1136,9 +1227,7 @@ var taprootHashTree = (tree) => {
1136
1227
  };
1137
1228
  var tweakMusig = (musig, tree) => {
1138
1229
  const tweak = taprootHashTree(tree).hash;
1139
- return musig.xonlyTweakAdd(
1140
- import_secp256k12.schnorr.utils.taggedHash("TapTweak", musig.aggPubkey, tweak)
1141
- );
1230
+ return musig.xonlyTweakAdd(import_secp256k12.schnorr.utils.taggedHash("TapTweak", musig.aggPubkey, tweak));
1142
1231
  };
1143
1232
  var toXOnly = (pubKey) => {
1144
1233
  if (pubKey.length === 32) return pubKey;
@@ -1150,9 +1239,7 @@ var toXOnly = (pubKey) => {
1150
1239
  }
1151
1240
  return pubKey.subarray(1, 33);
1152
1241
  }
1153
- throw new Error(
1154
- `Invalid public key length: expected 32 or 33 bytes, got ${pubKey.length}`
1155
- );
1242
+ throw new Error(`Invalid public key length: expected 32 or 33 bytes, got ${pubKey.length}`);
1156
1243
  };
1157
1244
  var p2trScript = (publicKey) => import_btc_signer.Script.encode(["OP_1", toXOnly(publicKey)]);
1158
1245
  var detectSwapOutput = (tweakedKey, transaction) => {
@@ -1167,8 +1254,7 @@ var detectSwapOutput = (tweakedKey, transaction) => {
1167
1254
  };
1168
1255
  var DUMMY_TAPROOT_SIGNATURE = new Uint8Array(64);
1169
1256
  var constructClaimTransaction = (utxo, destinationScript, fee) => {
1170
- if (fee < BigInt(0) || fee >= utxo.amount)
1171
- throw new Error("fee exceeds utxo amount");
1257
+ if (fee < BigInt(0) || fee >= utxo.amount) throw new Error("fee exceeds utxo amount");
1172
1258
  const tx = new import_btc_signer.Transaction({ version: 2 });
1173
1259
  tx.addOutput({
1174
1260
  amount: utxo.amount - fee,
@@ -1187,9 +1273,7 @@ var constructClaimTransaction = (utxo, destinationScript, fee) => {
1187
1273
  };
1188
1274
  var targetFee = (satPerVbyte, constructTx) => {
1189
1275
  const tx = constructTx(BigInt(1));
1190
- return constructTx(
1191
- BigInt(Math.ceil((tx.vsize + tx.inputsLength) * satPerVbyte))
1192
- );
1276
+ return constructTx(BigInt(Math.ceil((tx.vsize + tx.inputsLength) * satPerVbyte)));
1193
1277
  };
1194
1278
 
1195
1279
  // src/utils/decoding.ts
@@ -1197,9 +1281,7 @@ var import_light_bolt11_decoder = __toESM(require("light-bolt11-decoder"), 1);
1197
1281
  var import_sdk2 = require("@arkade-os/sdk");
1198
1282
  var decodeInvoice = (invoice) => {
1199
1283
  const decoded = import_light_bolt11_decoder.default.decode(invoice);
1200
- const millisats = Number(
1201
- decoded.sections.find((s) => s.name === "amount")?.value ?? "0"
1202
- );
1284
+ const millisats = Number(decoded.sections.find((s) => s.name === "amount")?.value ?? "0");
1203
1285
  return {
1204
1286
  expiry: decoded.expiry ?? 3600,
1205
1287
  amountSats: Math.floor(millisats / 1e3),
@@ -1262,10 +1344,7 @@ function extractTimeLockFromLeafOutput(scriptHex) {
1262
1344
  const data = opcodes[hasCSV - 1];
1263
1345
  if (data instanceof Uint8Array) {
1264
1346
  const dataBytes = new Uint8Array(data).reverse();
1265
- const {
1266
- blocks,
1267
- seconds
1268
- } = import_bip68.default.decode(
1347
+ const { blocks, seconds } = import_bip68.default.decode(
1269
1348
  parseInt(import_base5.hex.encode(dataBytes), 16)
1270
1349
  );
1271
1350
  return blocks ?? seconds ?? 0;
@@ -1298,6 +1377,13 @@ var SwapManager = class _SwapManager {
1298
1377
  * enough that a real "swap unknown to this provider" surfaces quickly.
1299
1378
  */
1300
1379
  static NOT_FOUND_THRESHOLD = 10;
1380
+ /**
1381
+ * Delay between re-attempts of a chain refund that left VTXOs deferred
1382
+ * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1383
+ * has elapsed). Boltz won't send another status update once the swap
1384
+ * is `swap.expired`, so the manager owns the local retry cadence.
1385
+ */
1386
+ static REFUND_RETRY_DELAY_MS = 6e4;
1301
1387
  swapProvider;
1302
1388
  config;
1303
1389
  // Event listeners storage (supports multiple listeners per event)
@@ -1316,6 +1402,11 @@ var SwapManager = class _SwapManager {
1316
1402
  reconnectTimer = null;
1317
1403
  initialPollTimer = null;
1318
1404
  pollRetryTimers = /* @__PURE__ */ new Map();
1405
+ // Per-swap retry timers for chain refunds that left work undone
1406
+ // (refundArk returned `skipped > 0`). The swap is held in
1407
+ // `monitoredSwaps` past its terminal Boltz status until the local
1408
+ // refund completes or the manager stops.
1409
+ refundRetryTimers = /* @__PURE__ */ new Map();
1319
1410
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1320
1411
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1321
1412
  // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
@@ -1369,9 +1460,7 @@ var SwapManager = class _SwapManager {
1369
1460
  this.wsConnectedListeners.add(config.events.onWebSocketConnected);
1370
1461
  }
1371
1462
  if (config.events?.onWebSocketDisconnected) {
1372
- this.wsDisconnectedListeners.add(
1373
- config.events.onWebSocketDisconnected
1374
- );
1463
+ this.wsDisconnectedListeners.add(config.events.onWebSocketDisconnected);
1375
1464
  }
1376
1465
  this.currentReconnectDelay = this.config.reconnectDelayMs;
1377
1466
  this.currentPollRetryDelay = this.config.pollRetryDelayMs;
@@ -1514,6 +1603,10 @@ var SwapManager = class _SwapManager {
1514
1603
  clearTimeout(timer);
1515
1604
  }
1516
1605
  this.pollRetryTimers.clear();
1606
+ for (const timer of this.refundRetryTimers.values()) {
1607
+ clearTimeout(timer);
1608
+ }
1609
+ this.refundRetryTimers.clear();
1517
1610
  this.notFoundCounts.clear();
1518
1611
  }
1519
1612
  /**
@@ -1522,9 +1615,7 @@ var SwapManager = class _SwapManager {
1522
1615
  */
1523
1616
  setPollInterval(ms) {
1524
1617
  if (ms <= 0) {
1525
- throw new RangeError(
1526
- `setPollInterval: ms must be a positive number, got ${ms}`
1527
- );
1618
+ throw new RangeError(`setPollInterval: ms must be a positive number, got ${ms}`);
1528
1619
  }
1529
1620
  const cappedInterval = Math.min(ms, this.config.maxPollIntervalMs);
1530
1621
  if (cappedInterval !== ms) {
@@ -1533,10 +1624,7 @@ var SwapManager = class _SwapManager {
1533
1624
  );
1534
1625
  }
1535
1626
  this.config.pollInterval = cappedInterval;
1536
- this.currentPollRetryDelay = Math.min(
1537
- cappedInterval,
1538
- this.config.pollRetryDelayMs
1539
- );
1627
+ this.currentPollRetryDelay = Math.min(cappedInterval, this.config.pollRetryDelayMs);
1540
1628
  if (this.isRunning) {
1541
1629
  if (this.usePollingFallback) {
1542
1630
  this.startPollingFallback();
@@ -1575,6 +1663,11 @@ var SwapManager = class _SwapManager {
1575
1663
  clearTimeout(retryTimer);
1576
1664
  this.pollRetryTimers.delete(swapId);
1577
1665
  }
1666
+ const refundRetryTimer = this.refundRetryTimers.get(swapId);
1667
+ if (refundRetryTimer) {
1668
+ clearTimeout(refundRetryTimer);
1669
+ this.refundRetryTimers.delete(swapId);
1670
+ }
1578
1671
  this.notFoundCounts.delete(swapId);
1579
1672
  logger.log(`Removed swap ${swapId} from monitoring`);
1580
1673
  }
@@ -1621,9 +1714,7 @@ var SwapManager = class _SwapManager {
1621
1714
  }
1622
1715
  if (this.isFinalStatus(swap)) {
1623
1716
  if (isPendingReverseSwap(swap)) {
1624
- const response = await this.swapProvider.getReverseSwapTxId(
1625
- swap.id
1626
- );
1717
+ const response = await this.swapProvider.getReverseSwapTxId(swap.id);
1627
1718
  return { txid: response.id };
1628
1719
  }
1629
1720
  if (isPendingSubmarineSwap(swap)) {
@@ -1640,31 +1731,19 @@ var SwapManager = class _SwapManager {
1640
1731
  if (updatedSwap.status === "invoice.settled") {
1641
1732
  this.swapProvider.getReverseSwapTxId(updatedSwap.id).then((response) => resolve({ txid: response.id })).catch((error) => reject(error));
1642
1733
  } else {
1643
- reject(
1644
- new Error(
1645
- `Swap failed with status: ${updatedSwap.status}`
1646
- )
1647
- );
1734
+ reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
1648
1735
  }
1649
1736
  } else if (isPendingSubmarineSwap(updatedSwap)) {
1650
1737
  if (updatedSwap.status === "transaction.claimed") {
1651
1738
  resolve({ txid: updatedSwap.id });
1652
1739
  } else {
1653
- reject(
1654
- new Error(
1655
- `Swap failed with status: ${updatedSwap.status}`
1656
- )
1657
- );
1740
+ reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
1658
1741
  }
1659
1742
  } else if (isPendingChainSwap(updatedSwap)) {
1660
1743
  if (updatedSwap.status === "transaction.claimed") {
1661
1744
  resolve({ txid: updatedSwap.id });
1662
1745
  } else {
1663
- reject(
1664
- new Error(
1665
- `Swap failed with status: ${updatedSwap.status}`
1666
- )
1667
- );
1746
+ reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
1668
1747
  }
1669
1748
  }
1670
1749
  };
@@ -1707,16 +1786,12 @@ var SwapManager = class _SwapManager {
1707
1786
  const connectionTimeout = setTimeout(() => {
1708
1787
  logger.error("WebSocket connection timeout");
1709
1788
  this.websocket?.close();
1710
- this.enterPollingFallback(
1711
- new NetworkError("WebSocket connection failed")
1712
- );
1789
+ this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
1713
1790
  }, 1e4);
1714
1791
  this.websocket.onerror = (error) => {
1715
1792
  clearTimeout(connectionTimeout);
1716
1793
  logger.error("WebSocket error:", error);
1717
- this.enterPollingFallback(
1718
- new NetworkError("WebSocket connection failed")
1719
- );
1794
+ this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
1720
1795
  };
1721
1796
  this.websocket.onopen = () => {
1722
1797
  clearTimeout(connectionTimeout);
@@ -1756,9 +1831,7 @@ var SwapManager = class _SwapManager {
1756
1831
  };
1757
1832
  } catch (error) {
1758
1833
  logger.error("Failed to create WebSocket:", error);
1759
- this.enterPollingFallback(
1760
- new NetworkError("WebSocket connection failed")
1761
- );
1834
+ this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
1762
1835
  }
1763
1836
  }
1764
1837
  /**
@@ -1793,11 +1866,8 @@ var SwapManager = class _SwapManager {
1793
1866
  * Schedule WebSocket reconnection with exponential backoff
1794
1867
  */
1795
1868
  scheduleReconnect() {
1796
- if (this.reconnectTimer || this.webSocketUnavailable || !this.hasWebSocketSupport())
1797
- return;
1798
- logger.log(
1799
- `Scheduling WebSocket reconnect in ${this.currentReconnectDelay}ms`
1800
- );
1869
+ if (this.reconnectTimer || this.webSocketUnavailable || !this.hasWebSocketSupport()) return;
1870
+ logger.log(`Scheduling WebSocket reconnect in ${this.currentReconnectDelay}ms`);
1801
1871
  this.reconnectTimer = setTimeout(() => {
1802
1872
  this.reconnectTimer = null;
1803
1873
  this.isReconnecting = false;
@@ -1812,8 +1882,7 @@ var SwapManager = class _SwapManager {
1812
1882
  * Subscribe to a specific swap ID on the WebSocket
1813
1883
  */
1814
1884
  subscribeToSwap(swapId) {
1815
- if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN)
1816
- return;
1885
+ if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) return;
1817
1886
  this.websocket.send(
1818
1887
  JSON.stringify({
1819
1888
  op: "subscribe",
@@ -1836,9 +1905,7 @@ var SwapManager = class _SwapManager {
1836
1905
  if (msg.args[0].error) {
1837
1906
  logger.error(`Swap ${swapId} error:`, msg.args[0].error);
1838
1907
  const error = new Error(msg.args[0].error);
1839
- this.swapFailedListeners.forEach(
1840
- (listener) => listener(swap, error)
1841
- );
1908
+ this.swapFailedListeners.forEach((listener) => listener(swap, error));
1842
1909
  return;
1843
1910
  }
1844
1911
  const newStatus = msg.args[0].status;
@@ -1856,19 +1923,14 @@ var SwapManager = class _SwapManager {
1856
1923
  const oldStatus = swap.status;
1857
1924
  if (oldStatus === newStatus) return;
1858
1925
  swap.status = newStatus;
1859
- this.swapUpdateListeners.forEach(
1860
- (listener) => listener(swap, oldStatus)
1861
- );
1926
+ this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
1862
1927
  const subscribers = this.swapSubscriptions.get(swap.id);
1863
1928
  if (subscribers) {
1864
1929
  subscribers.forEach((callback) => {
1865
1930
  try {
1866
1931
  callback(swap, oldStatus);
1867
1932
  } catch (error) {
1868
- logger.error(
1869
- `Error in swap subscription callback for ${swap.id}:`,
1870
- error
1871
- );
1933
+ logger.error(`Error in swap subscription callback for ${swap.id}:`, error);
1872
1934
  }
1873
1935
  });
1874
1936
  }
@@ -1877,15 +1939,57 @@ var SwapManager = class _SwapManager {
1877
1939
  await this.executeAutonomousAction(swap);
1878
1940
  }
1879
1941
  if (this.isFinalStatus(swap)) {
1880
- this.monitoredSwaps.delete(swap.id);
1881
- this.swapSubscriptions.delete(swap.id);
1882
- const retryTimer = this.pollRetryTimers.get(swap.id);
1883
- if (retryTimer) {
1884
- clearTimeout(retryTimer);
1885
- this.pollRetryTimers.delete(swap.id);
1942
+ if (this.refundRetryTimers.has(swap.id)) {
1943
+ return;
1886
1944
  }
1887
- this.swapCompletedListeners.forEach((listener) => listener(swap));
1945
+ this.finalizeMonitoredSwap(swap);
1946
+ }
1947
+ }
1948
+ /**
1949
+ * Drop a swap from monitoring and emit the terminal completion event.
1950
+ * Shared between the on-status-update finalization path and the
1951
+ * refund-retry finalization path (used when a previously-deferred
1952
+ * chain refund has finished its remaining work).
1953
+ */
1954
+ finalizeMonitoredSwap(swap) {
1955
+ if (!this.monitoredSwaps.has(swap.id)) return;
1956
+ this.monitoredSwaps.delete(swap.id);
1957
+ this.swapSubscriptions.delete(swap.id);
1958
+ const retryTimer = this.pollRetryTimers.get(swap.id);
1959
+ if (retryTimer) {
1960
+ clearTimeout(retryTimer);
1961
+ this.pollRetryTimers.delete(swap.id);
1962
+ }
1963
+ const refundRetry = this.refundRetryTimers.get(swap.id);
1964
+ if (refundRetry) {
1965
+ clearTimeout(refundRetry);
1966
+ this.refundRetryTimers.delete(swap.id);
1888
1967
  }
1968
+ this.swapCompletedListeners.forEach((listener) => listener(swap));
1969
+ }
1970
+ /**
1971
+ * Schedule another `executeAutonomousAction` run for a chain swap whose
1972
+ * refund left VTXOs deferred. After the retry completes, if no further
1973
+ * deferral was reported, finalize monitoring cleanup.
1974
+ */
1975
+ scheduleRefundRetry(swap, delayMs) {
1976
+ const existing = this.refundRetryTimers.get(swap.id);
1977
+ if (existing) clearTimeout(existing);
1978
+ this.refundRetryTimers.set(
1979
+ swap.id,
1980
+ setTimeout(async () => {
1981
+ this.refundRetryTimers.delete(swap.id);
1982
+ if (!this.isRunning) return;
1983
+ if (!this.monitoredSwaps.has(swap.id)) return;
1984
+ try {
1985
+ await this.executeAutonomousAction(swap);
1986
+ } finally {
1987
+ if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
1988
+ this.finalizeMonitoredSwap(swap);
1989
+ }
1990
+ }
1991
+ }, delayMs)
1992
+ );
1889
1993
  }
1890
1994
  /**
1891
1995
  * Execute autonomous action based on swap status
@@ -1893,9 +1997,7 @@ var SwapManager = class _SwapManager {
1893
1997
  */
1894
1998
  async executeAutonomousAction(swap) {
1895
1999
  if (this.swapsInProgress.has(swap.id)) {
1896
- logger.log(
1897
- `Swap ${swap.id} is already being processed, skipping autonomous action`
1898
- );
2000
+ logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
1899
2001
  return;
1900
2002
  }
1901
2003
  try {
@@ -1910,9 +2012,7 @@ var SwapManager = class _SwapManager {
1910
2012
  if (isReverseClaimableStatus(swap.status)) {
1911
2013
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
1912
2014
  await this.executeClaimAction(swap);
1913
- this.actionExecutedListeners.forEach(
1914
- (listener) => listener(swap, "claim")
1915
- );
2015
+ this.actionExecutedListeners.forEach((listener) => listener(swap, "claim"));
1916
2016
  }
1917
2017
  } else if (isPendingSubmarineSwap(swap)) {
1918
2018
  if (!swap.request?.invoice || swap.request.invoice.length === 0) {
@@ -1924,9 +2024,7 @@ var SwapManager = class _SwapManager {
1924
2024
  if (isSubmarineRefundableStatus(swap.status)) {
1925
2025
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
1926
2026
  await this.executeRefundAction(swap);
1927
- this.actionExecutedListeners.forEach(
1928
- (listener) => listener(swap, "refund")
1929
- );
2027
+ this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
1930
2028
  }
1931
2029
  } else if (isPendingChainSwap(swap)) {
1932
2030
  if (isChainClaimableStatus(swap.status)) {
@@ -1946,10 +2044,27 @@ var SwapManager = class _SwapManager {
1946
2044
  } else if (isChainRefundableStatus(swap.status)) {
1947
2045
  if (swap.request.from === "ARK") {
1948
2046
  logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
1949
- await this.executeRefundArkAction(swap);
1950
- this.actionExecutedListeners.forEach(
1951
- (listener) => listener(swap, "refundArk")
1952
- );
2047
+ try {
2048
+ const outcome = await this.executeRefundArkAction(swap);
2049
+ if (outcome && outcome.skipped > 0) {
2050
+ logger.log(
2051
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2052
+ );
2053
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2054
+ }
2055
+ this.actionExecutedListeners.forEach(
2056
+ (listener) => listener(swap, "refundArk")
2057
+ );
2058
+ } catch (error) {
2059
+ logger.error(
2060
+ `Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
2061
+ error
2062
+ );
2063
+ this.swapFailedListeners.forEach(
2064
+ (listener) => listener(swap, error)
2065
+ );
2066
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2067
+ }
1953
2068
  }
1954
2069
  if (swap.request.from === "BTC") {
1955
2070
  logger.warn(
@@ -1976,13 +2091,8 @@ var SwapManager = class _SwapManager {
1976
2091
  }
1977
2092
  }
1978
2093
  } catch (error) {
1979
- logger.error(
1980
- `Failed to execute autonomous action for swap ${swap.id}:`,
1981
- error
1982
- );
1983
- this.swapFailedListeners.forEach(
1984
- (listener) => listener(swap, error)
1985
- );
2094
+ logger.error(`Failed to execute autonomous action for swap ${swap.id}:`, error);
2095
+ this.swapFailedListeners.forEach((listener) => listener(swap, error));
1986
2096
  } finally {
1987
2097
  this.swapsInProgress.delete(swap.id);
1988
2098
  }
@@ -2035,7 +2145,7 @@ var SwapManager = class _SwapManager {
2035
2145
  logger.error("refundArk callback not set");
2036
2146
  return;
2037
2147
  }
2038
- await this.refundArkCallback(swap);
2148
+ return this.refundArkCallback(swap);
2039
2149
  }
2040
2150
  /**
2041
2151
  * Execute sign server claim action for chain swap.
@@ -2083,9 +2193,7 @@ var SwapManager = class _SwapManager {
2083
2193
  logger.log(`Resuming chain refund for swap ${swap.id}`);
2084
2194
  await this.executeAutonomousAction(swap);
2085
2195
  } else if (isPendingChainSwap(swap) && swap.request.to === "ARK" && isChainSignableStatus(swap.status)) {
2086
- logger.log(
2087
- `Resuming server claim signing for swap ${swap.id}`
2088
- );
2196
+ logger.log(`Resuming server claim signing for swap ${swap.id}`);
2089
2197
  await this.executeAutonomousAction(swap);
2090
2198
  }
2091
2199
  } catch (error) {
@@ -2137,16 +2245,12 @@ var SwapManager = class _SwapManager {
2137
2245
  */
2138
2246
  async pollAllSwaps() {
2139
2247
  if (this.monitoredSwaps.size === 0) return;
2140
- const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2141
- (swap) => this.pollSingleSwap(swap)
2142
- );
2248
+ const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
2143
2249
  await Promise.allSettled(pollPromises);
2144
2250
  }
2145
2251
  async pollSingleSwap(swap) {
2146
2252
  try {
2147
- const statusResponse = await this.swapProvider.getSwapStatus(
2148
- swap.id
2149
- );
2253
+ const statusResponse = await this.swapProvider.getSwapStatus(swap.id);
2150
2254
  this.notFoundCounts.delete(swap.id);
2151
2255
  if (statusResponse.status !== swap.status) {
2152
2256
  await this.handleSwapStatusUpdate(swap, statusResponse.status);
@@ -2157,9 +2261,7 @@ var SwapManager = class _SwapManager {
2157
2261
  return;
2158
2262
  }
2159
2263
  if (error instanceof NetworkError && error.statusCode === 429) {
2160
- logger.warn(
2161
- `Rate-limited polling swap ${swap.id}, retrying in 2s`
2162
- );
2264
+ logger.warn(`Rate-limited polling swap ${swap.id}, retrying in 2s`);
2163
2265
  const existing = this.pollRetryTimers.get(swap.id);
2164
2266
  if (existing) clearTimeout(existing);
2165
2267
  this.pollRetryTimers.set(
@@ -2167,25 +2269,17 @@ var SwapManager = class _SwapManager {
2167
2269
  setTimeout(async () => {
2168
2270
  this.pollRetryTimers.delete(swap.id);
2169
2271
  try {
2170
- const retry = await this.swapProvider.getSwapStatus(
2171
- swap.id
2172
- );
2272
+ const retry = await this.swapProvider.getSwapStatus(swap.id);
2173
2273
  this.notFoundCounts.delete(swap.id);
2174
2274
  if (retry.status !== swap.status) {
2175
- await this.handleSwapStatusUpdate(
2176
- swap,
2177
- retry.status
2178
- );
2275
+ await this.handleSwapStatusUpdate(swap, retry.status);
2179
2276
  }
2180
2277
  } catch (retryError) {
2181
2278
  if (retryError instanceof SwapNotFoundError) {
2182
2279
  await this.handleSwapNotFound(swap);
2183
2280
  return;
2184
2281
  }
2185
- logger.error(
2186
- `Retry poll for swap ${swap.id} also failed:`,
2187
- retryError
2188
- );
2282
+ logger.error(`Retry poll for swap ${swap.id} also failed:`, retryError);
2189
2283
  }
2190
2284
  }, 2e3)
2191
2285
  );
@@ -2204,6 +2298,7 @@ var SwapManager = class _SwapManager {
2204
2298
  * Boltz endpoint).
2205
2299
  */
2206
2300
  async handleSwapNotFound(swap) {
2301
+ if (this.refundRetryTimers.has(swap.id)) return;
2207
2302
  const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2208
2303
  this.notFoundCounts.set(swap.id, count);
2209
2304
  logger.warn(
@@ -2224,6 +2319,7 @@ var SwapManager = class _SwapManager {
2224
2319
  * 404s without recovering anything.
2225
2320
  */
2226
2321
  async markSwapAsUnknownToProvider(swap) {
2322
+ if (this.refundRetryTimers.has(swap.id)) return;
2227
2323
  if (!this.monitoredSwaps.has(swap.id)) {
2228
2324
  this.notFoundCounts.delete(swap.id);
2229
2325
  return;
@@ -2236,10 +2332,13 @@ var SwapManager = class _SwapManager {
2236
2332
  clearTimeout(retryTimer);
2237
2333
  this.pollRetryTimers.delete(swap.id);
2238
2334
  }
2335
+ const refundRetryTimer = this.refundRetryTimers.get(swap.id);
2336
+ if (refundRetryTimer) {
2337
+ clearTimeout(refundRetryTimer);
2338
+ this.refundRetryTimers.delete(swap.id);
2339
+ }
2239
2340
  this.notFoundCounts.delete(swap.id);
2240
- this.swapUpdateListeners.forEach(
2241
- (listener) => listener(swap, oldStatus)
2242
- );
2341
+ this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
2243
2342
  const subscribers = this.swapSubscriptions.get(swap.id);
2244
2343
  if (subscribers) {
2245
2344
  subscribers.forEach((callback) => {
@@ -2304,9 +2403,7 @@ async function saveSwap(swap, saver) {
2304
2403
  if (saver.saveSubmarineSwap) {
2305
2404
  await saver.saveSubmarineSwap(swap);
2306
2405
  } else {
2307
- console.warn(
2308
- "No saveSubmarineSwap handler provided, swap not saved"
2309
- );
2406
+ console.warn("No saveSubmarineSwap handler provided, swap not saved");
2310
2407
  }
2311
2408
  } else if (isPendingChainSwap(swap)) {
2312
2409
  if (saver.saveChainSwap) {
@@ -2363,6 +2460,26 @@ function enrichSubmarineSwapInvoice(swap, invoice) {
2363
2460
  return swap;
2364
2461
  }
2365
2462
 
2463
+ // src/repositories/swap-repository.ts
2464
+ function hasImpossibleSwapsFilter(filter) {
2465
+ if (!filter) return false;
2466
+ 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;
2467
+ }
2468
+ function matchesCriterion(value, criterion) {
2469
+ if (criterion === void 0) return true;
2470
+ return Array.isArray(criterion) ? criterion.includes(value) : value === criterion;
2471
+ }
2472
+ function applySwapsFilter(swaps, filter) {
2473
+ return swaps.filter(
2474
+ (swap) => !!swap && matchesCriterion(swap.id, filter.id) && matchesCriterion(swap.status, filter.status) && matchesCriterion(swap.type, filter.type)
2475
+ );
2476
+ }
2477
+ function applyCreatedAtOrder(swaps, filter) {
2478
+ if (filter?.orderBy !== "createdAt") return swaps;
2479
+ const direction = filter.orderDirection === "asc" ? 1 : -1;
2480
+ return swaps.slice().sort((a, b) => (a.createdAt - b.createdAt) * direction);
2481
+ }
2482
+
2366
2483
  // src/repositories/IndexedDb/swap-repository.ts
2367
2484
  var import_sdk4 = require("@arkade-os/sdk");
2368
2485
  var DEFAULT_DB_NAME = "arkade-boltz-swap";
@@ -2378,6 +2495,10 @@ function initDatabase(db) {
2378
2495
  swapStore.createIndex("createdAt", "createdAt", { unique: false });
2379
2496
  }
2380
2497
  }
2498
+ function asArray(v) {
2499
+ if (v === void 0) return void 0;
2500
+ return Array.isArray(v) ? v : [v];
2501
+ }
2381
2502
  var IndexedDbSwapRepository = class {
2382
2503
  constructor(dbName = DEFAULT_DB_NAME) {
2383
2504
  this.dbName = dbName;
@@ -2392,10 +2513,7 @@ var IndexedDbSwapRepository = class {
2392
2513
  async saveSwap(swap) {
2393
2514
  const db = await this.getDB();
2394
2515
  return new Promise((resolve, reject) => {
2395
- const transaction = db.transaction(
2396
- [STORE_SWAPS_STATE],
2397
- "readwrite"
2398
- );
2516
+ const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
2399
2517
  const store = transaction.objectStore(STORE_SWAPS_STATE);
2400
2518
  const request = store.put(swap);
2401
2519
  request.onsuccess = () => resolve();
@@ -2405,10 +2523,7 @@ var IndexedDbSwapRepository = class {
2405
2523
  async deleteSwap(id) {
2406
2524
  const db = await this.getDB();
2407
2525
  return new Promise((resolve, reject) => {
2408
- const transaction = db.transaction(
2409
- [STORE_SWAPS_STATE],
2410
- "readwrite"
2411
- );
2526
+ const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
2412
2527
  const store = transaction.objectStore(STORE_SWAPS_STATE);
2413
2528
  const request = store.delete(id);
2414
2529
  request.onsuccess = () => resolve();
@@ -2421,10 +2536,7 @@ var IndexedDbSwapRepository = class {
2421
2536
  async clear() {
2422
2537
  const db = await this.getDB();
2423
2538
  return new Promise((resolve, reject) => {
2424
- const transaction = db.transaction(
2425
- [STORE_SWAPS_STATE],
2426
- "readwrite"
2427
- );
2539
+ const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
2428
2540
  const store = transaction.objectStore(STORE_SWAPS_STATE);
2429
2541
  const request = store.clear();
2430
2542
  request.onsuccess = () => resolve();
@@ -2441,11 +2553,10 @@ var IndexedDbSwapRepository = class {
2441
2553
  request.onsuccess = () => resolve(request.result ?? []);
2442
2554
  })
2443
2555
  );
2444
- return Promise.all(requests).then(
2445
- (results) => results.flatMap((result) => result)
2446
- );
2556
+ return Promise.all(requests).then((results) => results.flatMap((result) => result));
2447
2557
  }
2448
2558
  async getAllSwapsFromStore(filter) {
2559
+ if (hasImpossibleSwapsFilter(filter)) return [];
2449
2560
  const db = await this.getDB();
2450
2561
  const store = db.transaction([STORE_SWAPS_STATE], "readonly").objectStore(STORE_SWAPS_STATE);
2451
2562
  if (!filter || Object.keys(filter).length === 0) {
@@ -2455,9 +2566,8 @@ var IndexedDbSwapRepository = class {
2455
2566
  request.onerror = () => reject(request.error);
2456
2567
  });
2457
2568
  }
2458
- const normalizedFilter = normalizeFilter(filter);
2459
- if (normalizedFilter.has("id")) {
2460
- const ids = normalizedFilter.get("id");
2569
+ const ids = asArray(filter.id);
2570
+ if (ids) {
2461
2571
  const swaps = await Promise.all(
2462
2572
  ids.map(
2463
2573
  (id) => new Promise((resolve, reject) => {
@@ -2467,34 +2577,17 @@ var IndexedDbSwapRepository = class {
2467
2577
  })
2468
2578
  )
2469
2579
  );
2470
- return this.sortIfNeeded(
2471
- this.applySwapsFilter(swaps, normalizedFilter),
2472
- filter
2473
- );
2580
+ return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
2474
2581
  }
2475
- if (normalizedFilter.has("type")) {
2476
- const types = normalizedFilter.get("type");
2477
- const swaps = await this.getSwapsByIndexValues(
2478
- store,
2479
- "type",
2480
- types
2481
- );
2482
- return this.sortIfNeeded(
2483
- this.applySwapsFilter(swaps, normalizedFilter),
2484
- filter
2485
- );
2582
+ const types = asArray(filter.type);
2583
+ if (types) {
2584
+ const swaps = await this.getSwapsByIndexValues(store, "type", types);
2585
+ return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
2486
2586
  }
2487
- if (normalizedFilter.has("status")) {
2488
- const ids = normalizedFilter.get("status");
2489
- const swaps = await this.getSwapsByIndexValues(
2490
- store,
2491
- "status",
2492
- ids
2493
- );
2494
- return this.sortIfNeeded(
2495
- this.applySwapsFilter(swaps, normalizedFilter),
2496
- filter
2497
- );
2587
+ const statuses = asArray(filter.status);
2588
+ if (statuses) {
2589
+ const swaps = await this.getSwapsByIndexValues(store, "status", statuses);
2590
+ return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
2498
2591
  }
2499
2592
  if (filter.orderBy === "createdAt") {
2500
2593
  return this.getAllSwapsByCreatedAt(store, filter.orderDirection);
@@ -2504,22 +2597,7 @@ var IndexedDbSwapRepository = class {
2504
2597
  request.onsuccess = () => resolve(request.result ?? []);
2505
2598
  request.onerror = () => reject(request.error);
2506
2599
  });
2507
- return this.sortIfNeeded(
2508
- this.applySwapsFilter(allSwaps, normalizedFilter),
2509
- filter
2510
- );
2511
- }
2512
- applySwapsFilter(swaps, filter) {
2513
- return swaps.filter((swap) => {
2514
- if (swap === void 0) return false;
2515
- if (filter.has("id") && !filter.get("id")?.includes(swap.id))
2516
- return false;
2517
- if (filter.has("status") && !filter.get("status")?.includes(swap.status))
2518
- return false;
2519
- if (filter.has("type") && !filter.get("type")?.includes(swap.type))
2520
- return false;
2521
- return true;
2522
- });
2600
+ return applyCreatedAtOrder(applySwapsFilter(allSwaps, filter), filter);
2523
2601
  }
2524
2602
  async getAllSwapsByCreatedAt(store, orderDirection) {
2525
2603
  const index = store.index("createdAt");
@@ -2539,30 +2617,12 @@ var IndexedDbSwapRepository = class {
2539
2617
  };
2540
2618
  });
2541
2619
  }
2542
- sortIfNeeded(swaps, filter) {
2543
- if (filter?.orderBy !== "createdAt") return swaps;
2544
- const direction = filter.orderDirection === "asc" ? 1 : -1;
2545
- return swaps.slice().sort((a, b) => (a.createdAt - b.createdAt) * direction);
2546
- }
2547
2620
  async [Symbol.asyncDispose]() {
2548
2621
  if (!this.db) return;
2549
2622
  await (0, import_sdk4.closeDatabase)(this.dbName);
2550
2623
  this.db = null;
2551
2624
  }
2552
2625
  };
2553
- var FILTER_FIELDS = ["id", "status", "type"];
2554
- function normalizeFilter(filter) {
2555
- const res = /* @__PURE__ */ new Map();
2556
- FILTER_FIELDS.forEach((current) => {
2557
- if (!filter?.[current]) return;
2558
- if (Array.isArray(filter[current])) {
2559
- res.set(current, filter[current]);
2560
- } else {
2561
- res.set(current, [filter[current]]);
2562
- }
2563
- });
2564
- return res;
2565
- }
2566
2626
 
2567
2627
  // src/utils/identity.ts
2568
2628
  var import_sdk5 = require("@arkade-os/sdk");
@@ -2574,13 +2634,8 @@ function claimVHTLCIdentity(identity, preimage) {
2574
2634
  let signedTx = await identity.sign(cpy, inputIndexes);
2575
2635
  signedTx = import_sdk5.Transaction.fromPSBT(signedTx.toPSBT());
2576
2636
  if (preimage) {
2577
- for (const inputIndex of inputIndexes || Array.from(
2578
- { length: signedTx.inputsLength },
2579
- (_, i) => i
2580
- )) {
2581
- (0, import_sdk5.setArkPsbtField)(signedTx, inputIndex, import_sdk5.ConditionWitness, [
2582
- preimage
2583
- ]);
2637
+ for (const inputIndex of inputIndexes || Array.from({ length: signedTx.inputsLength }, (_, i) => i)) {
2638
+ (0, import_sdk5.setArkPsbtField)(signedTx, inputIndex, import_sdk5.ConditionWitness, [preimage]);
2584
2639
  }
2585
2640
  }
2586
2641
  return signedTx;
@@ -2635,17 +2690,13 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2635
2690
  if (!sweepTapTreeRoot) {
2636
2691
  throw new Error("Sweep tap tree root not set");
2637
2692
  }
2638
- const xOnlyPublicKeys = event.cosignersPublicKeys.map(
2639
- (k) => k.slice(2)
2640
- );
2693
+ const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
2641
2694
  const signerPublicKey = await session.getPublicKey();
2642
2695
  const xonlySignerPublicKey = signerPublicKey.subarray(1);
2643
2696
  if (!xOnlyPublicKeys.includes(import_base7.hex.encode(xonlySignerPublicKey))) {
2644
2697
  return { skip: true };
2645
2698
  }
2646
- const commitmentTx = import_sdk6.Transaction.fromPSBT(
2647
- import_base7.base64.decode(event.unsignedCommitmentTx)
2648
- );
2699
+ const commitmentTx = import_sdk6.Transaction.fromPSBT(import_base7.base64.decode(event.unsignedCommitmentTx));
2649
2700
  (0, import_sdk6.validateVtxoTxGraph)(vtxoTree, commitmentTx, sweepTapTreeRoot);
2650
2701
  const sharedOutput = commitmentTx.getOutput(0);
2651
2702
  if (!sharedOutput?.amount) {
@@ -2661,18 +2712,11 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2661
2712
  if (!session) {
2662
2713
  return { fullySigned: true };
2663
2714
  }
2664
- const { hasAllNonces } = await session.aggregatedNonces(
2665
- event.txid,
2666
- event.nonces
2667
- );
2715
+ const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
2668
2716
  if (!hasAllNonces) return { fullySigned: false };
2669
2717
  const signatures = await session.sign();
2670
2718
  const pubkey = import_base7.hex.encode(await session.getPublicKey());
2671
- await arkProvider.submitTreeSignatures(
2672
- event.id,
2673
- pubkey,
2674
- signatures
2675
- );
2719
+ await arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
2676
2720
  return { fullySigned: true };
2677
2721
  },
2678
2722
  onBatchFinalization: async (event, _, connectorTree) => {
@@ -2680,9 +2724,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2680
2724
  return;
2681
2725
  }
2682
2726
  if (!connectorTree) {
2683
- throw new Error(
2684
- "BatchFinalizationEvent: expected connector tree to be defined"
2685
- );
2727
+ throw new Error("BatchFinalizationEvent: expected connector tree to be defined");
2686
2728
  }
2687
2729
  (0, import_sdk6.validateConnectorsTxGraph)(event.commitmentTx, connectorTree);
2688
2730
  const connectors = connectorTree.leaves();
@@ -2697,9 +2739,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
2697
2739
  connectors[connectorIndex]
2698
2740
  );
2699
2741
  const signedForfeitTx = await identity.sign(forfeitTx);
2700
- await arkProvider.submitSignedForfeitTxs([
2701
- import_base7.base64.encode(signedForfeitTx.toPSBT())
2702
- ]);
2742
+ await arkProvider.submitSignedForfeitTxs([import_base7.base64.encode(signedForfeitTx.toPSBT())]);
2703
2743
  }
2704
2744
  };
2705
2745
  }
@@ -2759,36 +2799,22 @@ var createVHTLCScript = (args) => {
2759
2799
  serverPubkey,
2760
2800
  timeoutBlockHeights: vhtlcTimeouts
2761
2801
  } = args;
2762
- const receiverXOnlyPublicKey = normalizeToXOnlyKey(
2763
- import_base8.hex.decode(receiverPubkey),
2764
- "receiver"
2765
- );
2766
- const senderXOnlyPublicKey = normalizeToXOnlyKey(
2767
- import_base8.hex.decode(senderPubkey),
2768
- "sender"
2769
- );
2770
- const serverXOnlyPublicKey = normalizeToXOnlyKey(
2771
- import_base8.hex.decode(serverPubkey),
2772
- "server"
2773
- );
2802
+ const receiverXOnlyPublicKey = normalizeToXOnlyKey(import_base8.hex.decode(receiverPubkey), "receiver");
2803
+ const senderXOnlyPublicKey = normalizeToXOnlyKey(import_base8.hex.decode(senderPubkey), "sender");
2804
+ const serverXOnlyPublicKey = normalizeToXOnlyKey(import_base8.hex.decode(serverPubkey), "server");
2774
2805
  const vhtlcScript = new import_sdk7.VHTLC.Script({
2775
2806
  preimageHash: (0, import_legacy.ripemd160)(preimageHash),
2776
2807
  sender: senderXOnlyPublicKey,
2777
2808
  receiver: receiverXOnlyPublicKey,
2778
2809
  server: serverXOnlyPublicKey,
2779
2810
  refundLocktime: BigInt(vhtlcTimeouts.refund),
2780
- unilateralClaimDelay: toBip68RelativeTimelock(
2781
- vhtlcTimeouts.unilateralClaim
2782
- ),
2783
- unilateralRefundDelay: toBip68RelativeTimelock(
2784
- vhtlcTimeouts.unilateralRefund
2785
- ),
2811
+ unilateralClaimDelay: toBip68RelativeTimelock(vhtlcTimeouts.unilateralClaim),
2812
+ unilateralRefundDelay: toBip68RelativeTimelock(vhtlcTimeouts.unilateralRefund),
2786
2813
  unilateralRefundWithoutReceiverDelay: toBip68RelativeTimelock(
2787
2814
  vhtlcTimeouts.unilateralRefundWithoutReceiver
2788
2815
  )
2789
2816
  });
2790
- if (!vhtlcScript.claimScript)
2791
- throw new Error("Failed to create VHTLC script");
2817
+ if (!vhtlcScript.claimScript) throw new Error("Failed to create VHTLC script");
2792
2818
  const hrp = network === "bitcoin" ? "ark" : "tark";
2793
2819
  const vhtlcAddress = vhtlcScript.address(hrp, serverXOnlyPublicKey).encode();
2794
2820
  return { vhtlcScript, vhtlcAddress };
@@ -2822,11 +2848,7 @@ var joinBatch = async (arkProvider, identity, input, output, {
2822
2848
  unknown: [import_sdk7.VtxoTaprootTree.encode(input.tapTree)],
2823
2849
  sequence: (0, import_sdk7.getSequence)(input.tapLeafScript)
2824
2850
  };
2825
- const registerIntent = import_sdk7.Intent.create(
2826
- intentMessage,
2827
- [intentInput],
2828
- [output]
2829
- );
2851
+ const registerIntent = import_sdk7.Intent.create(intentMessage, [intentInput], [output]);
2830
2852
  const deleteIntent = import_sdk7.Intent.create(deleteMessage, [intentInput]);
2831
2853
  const [signedRegisterIntent, signedDeleteIntent] = await Promise.all([
2832
2854
  identity.sign(registerIntent),
@@ -2850,14 +2872,8 @@ var joinBatch = async (arkProvider, identity, input, output, {
2850
2872
  normalizeToXOnlyKey(forfeitPubkey, "forfeit"),
2851
2873
  isRecoverable2 ? void 0 : import_btc_signer4.OutScript.encode(decodedAddress)
2852
2874
  );
2853
- const topics = [
2854
- import_base8.hex.encode(signerPublicKey),
2855
- `${input.txid}:${input.vout}`
2856
- ];
2857
- const eventStream = arkProvider.getEventStream(
2858
- abortController.signal,
2859
- topics
2860
- );
2875
+ const topics = [import_base8.hex.encode(signerPublicKey), `${input.txid}:${input.vout}`];
2876
+ const eventStream = arkProvider.getEventStream(abortController.signal, topics);
2861
2877
  const commitmentTxid = await import_sdk7.Batch.join(eventStream, handler, {
2862
2878
  abortController
2863
2879
  });
@@ -2878,14 +2894,8 @@ var joinBatch = async (arkProvider, identity, input, output, {
2878
2894
  };
2879
2895
  var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKey, input, output, arkInfo, arkProvider) => {
2880
2896
  const rawCheckpointTapscript = import_base8.hex.decode(arkInfo.checkpointTapscript);
2881
- const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(
2882
- rawCheckpointTapscript
2883
- );
2884
- const { arkTx, checkpoints } = (0, import_sdk7.buildOffchainTx)(
2885
- [input],
2886
- [output],
2887
- serverUnrollScript
2888
- );
2897
+ const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(rawCheckpointTapscript);
2898
+ const { arkTx, checkpoints } = (0, import_sdk7.buildOffchainTx)([input], [output], serverUnrollScript);
2889
2899
  const signedArkTx = await identity.sign(arkTx);
2890
2900
  const { arkTxid, finalArkTx, signedCheckpointTxs } = await arkProvider.submitTx(
2891
2901
  import_base8.base64.encode(signedArkTx.toPSBT()),
@@ -2893,9 +2903,7 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
2893
2903
  );
2894
2904
  const finalTx = import_sdk7.Transaction.fromPSBT(import_base8.base64.decode(finalArkTx));
2895
2905
  const serverPubkeyHex = import_base8.hex.encode(serverXOnlyPublicKey);
2896
- const claimLeafHash = (0, import_payment3.tapLeafHash)(
2897
- scriptFromTapLeafScript(vhtlcScript.claim())
2898
- );
2906
+ const claimLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(vhtlcScript.claim()));
2899
2907
  for (let i = 0; i < finalTx.inputsLength; i++) {
2900
2908
  if (!verifySignatures(finalTx, i, [serverPubkeyHex], claimLeafHash)) {
2901
2909
  throw new Error("Invalid final Ark transaction");
@@ -2905,13 +2913,9 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
2905
2913
  signedCheckpointTxs.map(async (c, idx) => {
2906
2914
  const tx = import_sdk7.Transaction.fromPSBT(import_base8.base64.decode(c));
2907
2915
  const checkpointLeaf = checkpoints[idx].getInput(0).tapLeafScript[0];
2908
- const cpLeafHash = (0, import_payment3.tapLeafHash)(
2909
- scriptFromTapLeafScript(checkpointLeaf)
2910
- );
2916
+ const cpLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(checkpointLeaf));
2911
2917
  if (!verifySignatures(tx, 0, [serverPubkeyHex], cpLeafHash)) {
2912
- throw new Error(
2913
- "Invalid server signature in checkpoint transaction"
2914
- );
2918
+ throw new Error("Invalid server signature in checkpoint transaction");
2915
2919
  }
2916
2920
  const signedCheckpoint = await identity.sign(tx, [0]);
2917
2921
  return import_base8.base64.encode(signedCheckpoint.toPSBT());
@@ -2921,61 +2925,37 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
2921
2925
  };
2922
2926
  var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnlyPublicKey, ourXOnlyPublicKey, serverXOnlyPublicKey, input, output, arkInfo, refundFunc) => {
2923
2927
  const rawCheckpointTapscript = import_base8.hex.decode(arkInfo.checkpointTapscript);
2924
- const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(
2925
- rawCheckpointTapscript
2928
+ const serverUnrollScript = import_sdk7.CSVMultisigTapscript.decode(rawCheckpointTapscript);
2929
+ const { arkTx: unsignedRefundTx, checkpoints: checkpointPtxs } = (0, import_sdk7.buildOffchainTx)(
2930
+ [input],
2931
+ [output],
2932
+ serverUnrollScript
2926
2933
  );
2927
- const { arkTx: unsignedRefundTx, checkpoints: checkpointPtxs } = (0, import_sdk7.buildOffchainTx)([input], [output], serverUnrollScript);
2928
2934
  if (checkpointPtxs.length !== 1)
2929
- throw new Error(
2930
- `Expected one checkpoint transaction, got ${checkpointPtxs.length}`
2931
- );
2935
+ throw new Error(`Expected one checkpoint transaction, got ${checkpointPtxs.length}`);
2932
2936
  const unsignedCheckpointTx = checkpointPtxs[0];
2933
2937
  let boltzSignedRefundTx;
2934
2938
  let boltzSignedCheckpointTx;
2935
2939
  try {
2936
- const result = await refundFunc(
2937
- swapId,
2938
- unsignedRefundTx,
2939
- unsignedCheckpointTx
2940
- );
2940
+ const result = await refundFunc(swapId, unsignedRefundTx, unsignedCheckpointTx);
2941
2941
  boltzSignedRefundTx = result.transaction;
2942
2942
  boltzSignedCheckpointTx = result.checkpoint;
2943
2943
  } catch (error) {
2944
- throw new BoltzRefundError(
2945
- `Boltz rejected refund for swap ${swapId}`,
2946
- error
2947
- );
2944
+ throw new BoltzRefundError(`Boltz rejected refund for swap ${swapId}`, error);
2948
2945
  }
2949
2946
  const boltzXOnlyPublicKeyHex = import_base8.hex.encode(boltzXOnlyPublicKey);
2950
- const refundLeafHash = (0, import_payment3.tapLeafHash)(
2951
- scriptFromTapLeafScript(input.tapLeafScript)
2952
- );
2953
- if (!verifySignatures(
2954
- boltzSignedRefundTx,
2955
- 0,
2956
- [boltzXOnlyPublicKeyHex],
2957
- refundLeafHash
2958
- )) {
2947
+ const refundLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(input.tapLeafScript));
2948
+ if (!verifySignatures(boltzSignedRefundTx, 0, [boltzXOnlyPublicKeyHex], refundLeafHash)) {
2959
2949
  throw new Error("Invalid Boltz signature in refund transaction");
2960
2950
  }
2961
2951
  const checkpointLeaf = unsignedCheckpointTx.getInput(0).tapLeafScript[0];
2962
- const checkpointLeafHash = (0, import_payment3.tapLeafHash)(
2963
- scriptFromTapLeafScript(checkpointLeaf)
2964
- );
2965
- if (!verifySignatures(
2966
- boltzSignedCheckpointTx,
2967
- 0,
2968
- [boltzXOnlyPublicKeyHex],
2969
- checkpointLeafHash
2970
- )) {
2952
+ const checkpointLeafHash = (0, import_payment3.tapLeafHash)(scriptFromTapLeafScript(checkpointLeaf));
2953
+ if (!verifySignatures(boltzSignedCheckpointTx, 0, [boltzXOnlyPublicKeyHex], checkpointLeafHash)) {
2971
2954
  throw new Error("Invalid Boltz signature in checkpoint transaction");
2972
2955
  }
2973
2956
  const signedRefundTx = await identity.sign(unsignedRefundTx);
2974
2957
  const signedCheckpointTx = await identity.sign(unsignedCheckpointTx);
2975
- const combinedSignedRefundTx = (0, import_sdk7.combineTapscriptSigs)(
2976
- boltzSignedRefundTx,
2977
- signedRefundTx
2978
- );
2958
+ const combinedSignedRefundTx = (0, import_sdk7.combineTapscriptSigs)(boltzSignedRefundTx, signedRefundTx);
2979
2959
  const combinedSignedCheckpointTx = (0, import_sdk7.combineTapscriptSigs)(
2980
2960
  boltzSignedCheckpointTx,
2981
2961
  signedCheckpointTx
@@ -2999,25 +2979,16 @@ var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnly
2999
2979
  `Expected one signed checkpoint transaction, got ${signedCheckpointTxs.length}`
3000
2980
  );
3001
2981
  }
3002
- const serverSignedCheckpointTx = import_sdk7.Transaction.fromPSBT(
3003
- import_base8.base64.decode(signedCheckpointTxs[0])
3004
- );
2982
+ const serverSignedCheckpointTx = import_sdk7.Transaction.fromPSBT(import_base8.base64.decode(signedCheckpointTxs[0]));
3005
2983
  const serverPubkeyHex = import_base8.hex.encode(serverXOnlyPublicKey);
3006
- if (!verifySignatures(
3007
- serverSignedCheckpointTx,
3008
- 0,
3009
- [serverPubkeyHex],
3010
- checkpointLeafHash
3011
- )) {
2984
+ if (!verifySignatures(serverSignedCheckpointTx, 0, [serverPubkeyHex], checkpointLeafHash)) {
3012
2985
  throw new Error("Invalid server signature in checkpoint transaction");
3013
2986
  }
3014
2987
  const finalCheckpointTx = (0, import_sdk7.combineTapscriptSigs)(
3015
2988
  combinedSignedCheckpointTx,
3016
2989
  serverSignedCheckpointTx
3017
2990
  );
3018
- await arkProvider.finalizeTx(arkTxid, [
3019
- import_base8.base64.encode(finalCheckpointTx.toPSBT())
3020
- ]);
2991
+ await arkProvider.finalizeTx(arkTxid, [import_base8.base64.encode(finalCheckpointTx.toPSBT())]);
3021
2992
  };
3022
2993
  function scriptFromTapLeafScript(leaf) {
3023
2994
  return leaf[1].subarray(0, leaf[1].length - 1);
@@ -3025,21 +2996,21 @@ function scriptFromTapLeafScript(leaf) {
3025
2996
 
3026
2997
  // src/arkade-swaps.ts
3027
2998
  var dedupeVtxos = (vtxos) => [
3028
- ...new Map(
3029
- vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])
3030
- ).values()
2999
+ ...new Map(vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])).values()
3031
3000
  ];
3032
3001
  var hasNonEmptyString = (value) => typeof value === "string" && value.length > 0;
3033
3002
  var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
3034
3003
  const hasRequiredSwapMetadata = hasNonEmptyString(swap.id) && hasNonEmptyString(swap.request.refundPublicKey) && hasNonEmptyString(swap.response.address) && hasNonEmptyString(swap.response.claimPublicKey) && !!swap.response.timeoutBlockHeights;
3035
3004
  if (!hasRequiredSwapMetadata) return false;
3036
- return refundableVtxos.some(
3037
- (vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo)
3038
- );
3005
+ return refundableVtxos.some((vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo));
3039
3006
  };
3040
3007
  var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
3041
3008
  var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
3042
3009
  var CLAIM_VTXO_RETRY_DELAY_MS = 500;
3010
+ var quoteOptionsForSwap = (swap) => {
3011
+ const amount = swap.response?.claimDetails?.amount;
3012
+ return typeof amount === "number" ? { minAcceptableAmount: amount } : void 0;
3013
+ };
3043
3014
  var ArkadeSwaps = class _ArkadeSwaps {
3044
3015
  /** The Arkade wallet instance used for signing and address generation. */
3045
3016
  wallet;
@@ -3075,10 +3046,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3075
3046
  return new _ArkadeSwaps(config);
3076
3047
  }
3077
3048
  const arkProvider = config.arkProvider ?? config.wallet.arkProvider;
3078
- if (!arkProvider)
3079
- throw new Error(
3080
- "Ark provider is required either in wallet or config."
3081
- );
3049
+ if (!arkProvider) throw new Error("Ark provider is required either in wallet or config.");
3082
3050
  const arkInfo = await arkProvider.getInfo();
3083
3051
  const network = arkInfo.network;
3084
3052
  const swapProvider = new BoltzSwapProvider({ network });
@@ -3089,16 +3057,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
3089
3057
  if (!config.swapProvider) throw new Error("Swap provider is required.");
3090
3058
  this.wallet = config.wallet;
3091
3059
  const arkProvider = config.arkProvider ?? config.wallet.arkProvider;
3092
- if (!arkProvider)
3093
- throw new Error(
3094
- "Ark provider is required either in wallet or config."
3095
- );
3060
+ if (!arkProvider) throw new Error("Ark provider is required either in wallet or config.");
3096
3061
  this.arkProvider = arkProvider;
3097
3062
  const indexerProvider = config.indexerProvider ?? config.wallet.indexerProvider;
3098
3063
  if (!indexerProvider)
3099
- throw new Error(
3100
- "Indexer provider is required either in wallet or config."
3101
- );
3064
+ throw new Error("Indexer provider is required either in wallet or config.");
3102
3065
  this.indexerProvider = indexerProvider;
3103
3066
  this.swapProvider = config.swapProvider;
3104
3067
  if (config.swapRepository) {
@@ -3109,10 +3072,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3109
3072
  if (config.swapManager !== false) {
3110
3073
  const swapManagerConfig = !config.swapManager || config.swapManager === true ? {} : config.swapManager;
3111
3074
  const shouldAutostart = swapManagerConfig.autoStart ?? true;
3112
- this.swapManager = new SwapManager(
3113
- this.swapProvider,
3114
- swapManagerConfig
3115
- );
3075
+ this.swapManager = new SwapManager(this.swapProvider, swapManagerConfig);
3116
3076
  this.swapManager.setCallbacks({
3117
3077
  claim: async (swap) => {
3118
3078
  await this.claimVHTLC(swap);
@@ -3127,7 +3087,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3127
3087
  await this.claimBtc(swap);
3128
3088
  },
3129
3089
  refundArk: async (swap) => {
3130
- await this.refundArk(swap);
3090
+ return this.refundArk(swap);
3131
3091
  },
3132
3092
  signServerClaim: async (swap) => {
3133
3093
  await this.signCooperativeClaimForServer(swap);
@@ -3260,19 +3220,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
3260
3220
  * @throws {SwapError} If amount is <= 0 or key retrieval fails.
3261
3221
  */
3262
3222
  async createReverseSwap(args) {
3263
- if (args.amount <= 0)
3264
- throw new SwapError({ message: "Amount must be greater than 0" });
3265
- const claimPublicKey = import_base9.hex.encode(
3266
- await this.wallet.identity.compressedPublicKey()
3267
- );
3223
+ if (args.amount <= 0) throw new SwapError({ message: "Amount must be greater than 0" });
3224
+ const claimPublicKey = import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
3268
3225
  if (!claimPublicKey)
3269
3226
  throw new SwapError({
3270
3227
  message: "Failed to get claim public key from wallet"
3271
3228
  });
3272
3229
  const preimage = (0, import_utils3.randomBytes)(32);
3273
3230
  const preimageHash = import_base9.hex.encode((0, import_sha23.sha256)(preimage));
3274
- if (!preimageHash)
3275
- throw new SwapError({ message: "Failed to get preimage hash" });
3231
+ if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" });
3276
3232
  const swapRequest = {
3277
3233
  invoiceAmount: args.amount,
3278
3234
  claimPublicKey,
@@ -3302,18 +3258,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
3302
3258
  */
3303
3259
  async claimVHTLC(pendingSwap) {
3304
3260
  if (!pendingSwap.preimage)
3305
- throw new Error(
3306
- `Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
3307
- );
3261
+ throw new Error(`Swap ${pendingSwap.id}: preimage is required to claim VHTLC`);
3308
3262
  const {
3309
3263
  refundPublicKey,
3310
3264
  lockupAddress,
3311
3265
  timeoutBlockHeights: vhtlcTimeouts
3312
3266
  } = pendingSwap.response;
3313
3267
  if (!refundPublicKey || !lockupAddress || !vhtlcTimeouts)
3314
- throw new Error(
3315
- `Swap ${pendingSwap.id}: incomplete reverse swap response`
3316
- );
3268
+ throw new Error(`Swap ${pendingSwap.id}: incomplete reverse swap response`);
3317
3269
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
3318
3270
  const arkInfo = await this.arkProvider.getInfo();
3319
3271
  const address = await this.wallet.getAddress();
@@ -3348,58 +3300,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
3348
3300
  throw new Error(
3349
3301
  `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3350
3302
  );
3351
- let vtxo;
3303
+ let unspentVtxos = [];
3304
+ let rawVtxos = [];
3352
3305
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
3353
- const { vtxos } = await this.indexerProvider.getVtxos({
3306
+ const result = await this.indexerProvider.getVtxos({
3354
3307
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3355
3308
  });
3356
- if (vtxos.length > 0) {
3357
- vtxo = vtxos[0];
3309
+ rawVtxos = result.vtxos;
3310
+ unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
3311
+ if (unspentVtxos.length > 0) {
3358
3312
  break;
3359
3313
  }
3360
3314
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
3361
- await new Promise(
3362
- (resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS)
3363
- );
3315
+ await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
3364
3316
  }
3365
3317
  }
3366
- if (!vtxo) {
3367
- throw new Error(
3368
- `Swap ${pendingSwap.id}: no spendable virtual coins found`
3369
- );
3370
- }
3371
- if (vtxo.isSpent) {
3318
+ if (unspentVtxos.length === 0) {
3319
+ if (rawVtxos.length === 0) {
3320
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3321
+ }
3372
3322
  throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3373
3323
  }
3374
- const input = {
3375
- ...vtxo,
3376
- tapLeafScript: vhtlcScript.claim(),
3377
- tapTree: vhtlcScript.encode()
3378
- };
3379
- const output = {
3380
- amount: BigInt(vtxo.value),
3381
- script: import_sdk8.ArkAddress.decode(address).pkScript
3382
- };
3383
- const vhtlcIdentity = claimVHTLCIdentity(
3384
- this.wallet.identity,
3385
- preimage
3386
- );
3387
- let finalStatus;
3388
- if ((0, import_sdk8.isRecoverable)(vtxo)) {
3389
- await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3390
- finalStatus = "transaction.claimed";
3391
- } else {
3392
- await claimVHTLCwithOffchainTx(
3393
- vhtlcIdentity,
3394
- vhtlcScript,
3395
- serverXOnly,
3396
- input,
3397
- output,
3398
- arkInfo,
3399
- this.arkProvider
3324
+ const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
3325
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3326
+ const claimErrors = [];
3327
+ let usedOffchainClaim = false;
3328
+ for (const vtxo of unspentVtxos) {
3329
+ const input = {
3330
+ ...vtxo,
3331
+ tapLeafScript: vhtlcScript.claim(),
3332
+ tapTree: vhtlcScript.encode()
3333
+ };
3334
+ const output = {
3335
+ amount: BigInt(vtxo.value),
3336
+ script: outputScript
3337
+ };
3338
+ try {
3339
+ if ((0, import_sdk8.isRecoverable)(vtxo)) {
3340
+ await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3341
+ } else {
3342
+ await claimVHTLCwithOffchainTx(
3343
+ vhtlcIdentity,
3344
+ vhtlcScript,
3345
+ serverXOnly,
3346
+ input,
3347
+ output,
3348
+ arkInfo,
3349
+ this.arkProvider
3350
+ );
3351
+ usedOffchainClaim = true;
3352
+ }
3353
+ } catch (error) {
3354
+ claimErrors.push({ vtxo, error });
3355
+ }
3356
+ }
3357
+ if (claimErrors.length > 0) {
3358
+ const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
3359
+ throw new Error(
3360
+ `Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
3400
3361
  );
3401
- finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
3402
3362
  }
3363
+ const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
3403
3364
  await updateReverseSwapStatus(
3404
3365
  pendingSwap,
3405
3366
  finalStatus,
@@ -3502,9 +3463,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3502
3463
  async sendLightningPayment(args) {
3503
3464
  const pendingSwap = await this.createSubmarineSwap(args);
3504
3465
  if (!pendingSwap.response.address)
3505
- throw new Error(
3506
- `Swap ${pendingSwap.id}: missing address in submarine swap response`
3507
- );
3466
+ throw new Error(`Swap ${pendingSwap.id}: missing address in submarine swap response`);
3508
3467
  await this.savePendingSubmarineSwap(pendingSwap);
3509
3468
  const txid = await this.wallet.send({
3510
3469
  address: pendingSwap.response.address,
@@ -3537,9 +3496,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3537
3496
  * @throws {SwapError} If invoice is missing or key retrieval fails.
3538
3497
  */
3539
3498
  async createSubmarineSwap(args) {
3540
- const refundPublicKey = import_base9.hex.encode(
3541
- await this.wallet.identity.compressedPublicKey()
3542
- );
3499
+ const refundPublicKey = import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
3543
3500
  if (!refundPublicKey)
3544
3501
  throw new SwapError({
3545
3502
  message: "Failed to get refund public key from wallet"
@@ -3577,9 +3534,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3577
3534
  async buildSubmarineVHTLCContext(swap, arkInfo) {
3578
3535
  const preimageHash = swap.request.invoice ? getInvoicePaymentHash(swap.request.invoice) : swap.preimageHash;
3579
3536
  if (!preimageHash)
3580
- throw new Error(
3581
- `Swap ${swap.id}: preimage hash is required to refund VHTLC`
3582
- );
3537
+ throw new Error(`Swap ${swap.id}: preimage hash is required to refund VHTLC`);
3583
3538
  const resolvedArkInfo = arkInfo ?? await this.arkProvider.getInfo();
3584
3539
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
3585
3540
  await this.wallet.identity.xOnlyPublicKey(),
@@ -3593,9 +3548,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3593
3548
  );
3594
3549
  const { claimPublicKey, timeoutBlockHeights: vhtlcTimeouts } = swap.response;
3595
3550
  if (!claimPublicKey || !vhtlcTimeouts)
3596
- throw new Error(
3597
- `Swap ${swap.id}: incomplete submarine swap response`
3598
- );
3551
+ throw new Error(`Swap ${swap.id}: incomplete submarine swap response`);
3599
3552
  const boltzXOnlyPublicKey = normalizeToXOnlyKey(
3600
3553
  import_base9.hex.decode(claimPublicKey),
3601
3554
  "boltz",
@@ -3610,9 +3563,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3610
3563
  timeoutBlockHeights: vhtlcTimeouts
3611
3564
  });
3612
3565
  if (!vhtlcScript.claimScript)
3613
- throw new Error(
3614
- `Swap ${swap.id}: failed to create VHTLC script for submarine swap`
3615
- );
3566
+ throw new Error(`Swap ${swap.id}: failed to create VHTLC script for submarine swap`);
3616
3567
  if (vhtlcAddress !== swap.response.address)
3617
3568
  throw new Error(
3618
3569
  `VHTLC address mismatch for swap ${swap.id}: expected ${swap.response.address}, got ${vhtlcAddress}`
@@ -3651,10 +3602,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3651
3602
  recoverableOnly: true
3652
3603
  })
3653
3604
  ]);
3654
- const refundableVtxos = dedupeVtxos([
3655
- ...spendableResult.vtxos,
3656
- ...recoverableResult.vtxos
3657
- ]);
3605
+ const refundableVtxos = dedupeVtxos([...spendableResult.vtxos, ...recoverableResult.vtxos]);
3658
3606
  let diagnostic;
3659
3607
  if (refundableVtxos.length === 0) {
3660
3608
  const { vtxos: allVtxos } = await this.indexerProvider.getVtxos({
@@ -3674,13 +3622,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
3674
3622
  submarineRecoveryInfoFromLookup(swap, lookup) {
3675
3623
  const { refundableVtxos, diagnostic, vhtlcTimeouts } = lookup;
3676
3624
  if (refundableVtxos.length > 0) {
3677
- const cltvSatisfied = isSubmarineRefundLocktimeReached(
3678
- vhtlcTimeouts.refund
3679
- );
3680
- const amountSats = refundableVtxos.reduce(
3681
- (sum, vtxo) => sum + Number(vtxo.value),
3682
- 0
3683
- );
3625
+ const cltvSatisfied = isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund);
3626
+ const amountSats = refundableVtxos.reduce((sum, vtxo) => sum + Number(vtxo.value), 0);
3684
3627
  const isRecoverable2 = cltvSatisfied || canRecoverViaBoltz3of3(refundableVtxos, swap);
3685
3628
  return {
3686
3629
  swap,
@@ -3744,19 +3687,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
3744
3687
  );
3745
3688
  }
3746
3689
  if (diagnostic.allSpent) {
3747
- throw new Error(
3748
- `Swap ${pendingSwap.id}: VHTLC is already spent`
3749
- );
3690
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3750
3691
  }
3751
- throw new Error(
3752
- `Swap ${pendingSwap.id}: VHTLC has no refundable VTXOs yet`
3753
- );
3692
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC has no refundable VTXOs yet`);
3754
3693
  }
3755
3694
  const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3756
3695
  const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
3757
- const cltvSatisfied = isSubmarineRefundLocktimeReached(
3758
- vhtlcTimeouts.refund
3759
- );
3696
+ const cltvSatisfied = isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund);
3760
3697
  let boltzCallCount = 0;
3761
3698
  let sweptCount = 0;
3762
3699
  let skippedCount = 0;
@@ -3798,6 +3735,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3798
3735
  if (boltzCallCount > 0) {
3799
3736
  await new Promise((r) => setTimeout(r, 2e3));
3800
3737
  }
3738
+ boltzCallCount++;
3801
3739
  await refundVHTLCwithOffchainTx(
3802
3740
  pendingSwap.id,
3803
3741
  this.wallet.identity,
@@ -3808,11 +3746,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
3808
3746
  input,
3809
3747
  output,
3810
3748
  arkInfo,
3811
- this.swapProvider.refundSubmarineSwap.bind(
3812
- this.swapProvider
3813
- )
3749
+ this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3814
3750
  );
3815
- boltzCallCount++;
3816
3751
  sweptCount++;
3817
3752
  } catch (error) {
3818
3753
  if (!(error instanceof BoltzRefundError)) {
@@ -3833,13 +3768,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3833
3768
  tapLeafScript: refundWithoutReceiverLeaf,
3834
3769
  tapTree: vhtlcScript.encode()
3835
3770
  };
3836
- await this.joinBatch(
3837
- this.wallet.identity,
3838
- fallbackInput,
3839
- output,
3840
- arkInfo,
3841
- false
3842
- );
3771
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
3843
3772
  sweptCount++;
3844
3773
  }
3845
3774
  }
@@ -3935,10 +3864,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3935
3864
  try {
3936
3865
  return {
3937
3866
  swap,
3938
- context: await this.buildSubmarineVHTLCContext(
3939
- swap,
3940
- arkInfo
3941
- )
3867
+ context: await this.buildSubmarineVHTLCContext(swap, arkInfo)
3942
3868
  };
3943
3869
  } catch (err) {
3944
3870
  return {
@@ -3951,9 +3877,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3951
3877
  const valid = prepared.filter(
3952
3878
  (item) => "context" in item
3953
3879
  );
3954
- const scripts = [
3955
- ...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))
3956
- ];
3880
+ const scripts = [...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))];
3957
3881
  const refundableByScript = /* @__PURE__ */ new Map();
3958
3882
  if (scripts.length > 0) {
3959
3883
  const [spendableResult, recoverableResult] = await Promise.all([
@@ -3988,9 +3912,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3988
3912
  error: item.error
3989
3913
  };
3990
3914
  }
3991
- const refundableVtxos = refundableByScript.get(
3992
- item.context.vhtlcPkScriptHex.toLowerCase()
3993
- ) ?? [];
3915
+ const refundableVtxos = refundableByScript.get(item.context.vhtlcPkScriptHex.toLowerCase()) ?? [];
3994
3916
  return this.submarineRecoveryInfoFromLookup(item.swap, {
3995
3917
  ...item.context,
3996
3918
  refundableVtxos
@@ -4169,9 +4091,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4169
4091
  */
4170
4092
  async waitAndClaimBtc(pendingSwap) {
4171
4093
  if (this.swapManager && await this.swapManager.hasSwap(pendingSwap.id)) {
4172
- const { txid } = await this.swapManager.waitForSwapCompletion(
4173
- pendingSwap.id
4174
- );
4094
+ const { txid } = await this.swapManager.waitForSwapCompletion(pendingSwap.id);
4175
4095
  return { txid };
4176
4096
  }
4177
4097
  return new Promise((resolve, reject) => {
@@ -4198,24 +4118,25 @@ var ArkadeSwaps = class _ArkadeSwaps {
4198
4118
  }
4199
4119
  case "transaction.claimed":
4200
4120
  await updateSwapStatus();
4201
- const claimedStatus = await this.getSwapStatus(
4202
- pendingSwap.id
4203
- );
4121
+ const claimedStatus = await this.getSwapStatus(pendingSwap.id);
4204
4122
  resolve({
4205
4123
  txid: claimedStatus.transaction?.id ?? ""
4206
4124
  });
4207
4125
  break;
4208
4126
  case "transaction.lockupFailed":
4209
4127
  await updateSwapStatus();
4210
- await this.quoteSwap(swap.response.id).catch((err) => {
4211
- reject(
4212
- new SwapError({
4213
- message: `Failed to renegotiate quote: ${err.message}`,
4214
- isRefundable: true,
4215
- pendingSwap: swap
4216
- })
4217
- );
4218
- });
4128
+ await this.quoteSwap(swap.response.id, quoteOptionsForSwap(swap)).catch(
4129
+ (err) => {
4130
+ reject(
4131
+ new SwapError({
4132
+ message: `Failed to renegotiate quote: ${err.message}`,
4133
+ isRefundable: true,
4134
+ pendingSwap: swap,
4135
+ cause: err
4136
+ })
4137
+ );
4138
+ }
4139
+ );
4219
4140
  break;
4220
4141
  case "swap.expired":
4221
4142
  await updateSwapStatus();
@@ -4253,30 +4174,18 @@ var ArkadeSwaps = class _ArkadeSwaps {
4253
4174
  */
4254
4175
  async claimBtc(pendingSwap) {
4255
4176
  if (!pendingSwap.toAddress)
4256
- throw new Error(
4257
- `Swap ${pendingSwap.id}: destination address is required`
4258
- );
4177
+ throw new Error(`Swap ${pendingSwap.id}: destination address is required`);
4259
4178
  if (!pendingSwap.response.claimDetails.swapTree)
4260
- throw new Error(
4261
- `Swap ${pendingSwap.id}: missing swap tree in claim details`
4262
- );
4179
+ throw new Error(`Swap ${pendingSwap.id}: missing swap tree in claim details`);
4263
4180
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4264
- throw new Error(
4265
- `Swap ${pendingSwap.id}: missing server public key in claim details`
4266
- );
4181
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in claim details`);
4267
4182
  const swapStatus = await this.getSwapStatus(pendingSwap.id);
4268
4183
  if (!swapStatus.transaction?.hex)
4269
- throw new Error(
4270
- `Swap ${pendingSwap.id}: BTC transaction hex is required`
4271
- );
4272
- const lockupTx = import_btc_signer5.Transaction.fromRaw(
4273
- import_base9.hex.decode(swapStatus.transaction.hex)
4274
- );
4184
+ throw new Error(`Swap ${pendingSwap.id}: BTC transaction hex is required`);
4185
+ const lockupTx = import_btc_signer5.Transaction.fromRaw(import_base9.hex.decode(swapStatus.transaction.hex));
4275
4186
  const arkInfo = await this.arkProvider.getInfo();
4276
4187
  const network = arkInfo.network === "bitcoin" ? import_utils4.NETWORK : arkInfo.network === "mutinynet" ? MUTINYNET_NETWORK : REGTEST_NETWORK;
4277
- const swapTree = deserializeSwapTree(
4278
- pendingSwap.response.claimDetails.swapTree
4279
- );
4188
+ const swapTree = deserializeSwapTree(pendingSwap.response.claimDetails.swapTree);
4280
4189
  const musig = tweakMusig(
4281
4190
  create(import_base9.hex.decode(pendingSwap.ephemeralKey), [
4282
4191
  import_base9.hex.decode(pendingSwap.response.claimDetails.serverPublicKey),
@@ -4297,19 +4206,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
4297
4206
  vout: swapOutput.vout,
4298
4207
  transactionId: lockupTx.id
4299
4208
  },
4300
- import_btc_signer5.OutScript.encode(
4301
- (0, import_btc_signer5.Address)(network).decode(pendingSwap.toAddress)
4302
- ),
4209
+ import_btc_signer5.OutScript.encode((0, import_btc_signer5.Address)(network).decode(pendingSwap.toAddress)),
4303
4210
  feeToDeliverExactAmount > fee ? feeToDeliverExactAmount : fee
4304
4211
  )
4305
4212
  );
4306
4213
  const musigMessage = musig.message(
4307
- claimTx.preimageWitnessV1(
4308
- 0,
4309
- [swapOutput.script],
4310
- import_btc_signer5.SigHash.DEFAULT,
4311
- [swapOutput.amount]
4312
- )
4214
+ claimTx.preimageWitnessV1(0, [swapOutput.script], import_btc_signer5.SigHash.DEFAULT, [
4215
+ swapOutput.amount
4216
+ ])
4313
4217
  ).generateNonce();
4314
4218
  const signedTxData = await this.swapProvider.postChainClaimDetails(
4315
4219
  pendingSwap.response.id,
@@ -4323,14 +4227,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
4323
4227
  }
4324
4228
  );
4325
4229
  if (!signedTxData.pubNonce || !signedTxData.partialSignature)
4326
- throw new Error(
4327
- `Swap ${pendingSwap.id}: invalid signature data from server`
4328
- );
4230
+ throw new Error(`Swap ${pendingSwap.id}: invalid signature data from server`);
4329
4231
  const musigSession = musigMessage.aggregateNonces([
4330
4232
  [
4331
- import_base9.hex.decode(
4332
- pendingSwap.response.claimDetails.serverPublicKey
4333
- ),
4233
+ import_base9.hex.decode(pendingSwap.response.claimDetails.serverPublicKey),
4334
4234
  import_base9.hex.decode(signedTxData.pubNonce)
4335
4235
  ]
4336
4236
  ]).initializeSession();
@@ -4345,18 +4245,23 @@ var ArkadeSwaps = class _ArkadeSwaps {
4345
4245
  await this.swapProvider.postBtcTransaction(claimTx.hex);
4346
4246
  }
4347
4247
  /**
4348
- * When an ARK to BTC swap fails, refund sats on ARK chain by claiming the VHTLC.
4248
+ * When an ARK to BTC swap fails, refund every unspent VTXO at the chain
4249
+ * swap's ARK lockup address.
4250
+ *
4251
+ * Path selection per VTXO:
4252
+ * - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
4253
+ * - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
4254
+ * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
4255
+ *
4349
4256
  * @param pendingSwap - The pending chain swap to refund.
4257
+ * @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
4258
+ * the call was a no-op — callers should retry after CLTV.
4350
4259
  */
4351
4260
  async refundArk(pendingSwap) {
4352
4261
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4353
- throw new Error(
4354
- `Swap ${pendingSwap.id}: missing server public key in lockup details`
4355
- );
4262
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in lockup details`);
4356
4263
  if (!pendingSwap.response.lockupDetails.timeouts)
4357
- throw new Error(
4358
- `Swap ${pendingSwap.id}: missing timeouts in lockup details`
4359
- );
4264
+ throw new Error(`Swap ${pendingSwap.id}: missing timeouts in lockup details`);
4360
4265
  const arkInfo = await this.arkProvider.getInfo();
4361
4266
  const address = await this.wallet.getAddress();
4362
4267
  const ourXOnlyPublicKey = normalizeToXOnlyKey(
@@ -4374,21 +4279,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
4374
4279
  "boltz",
4375
4280
  pendingSwap.id
4376
4281
  );
4377
- const vhtlcPkScript = import_sdk8.ArkAddress.decode(
4378
- pendingSwap.response.lockupDetails.lockupAddress
4379
- ).pkScript;
4380
- const { vtxos } = await this.indexerProvider.getVtxos({
4381
- scripts: [import_base9.hex.encode(vhtlcPkScript)]
4382
- });
4383
- if (vtxos.length === 0) {
4384
- throw new Error(
4385
- `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4386
- );
4387
- }
4388
- const vtxo = vtxos[0];
4389
- if (vtxo.isSpent) {
4390
- throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4391
- }
4392
4282
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4393
4283
  network: arkInfo.network,
4394
4284
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4398,45 +4288,111 @@ var ArkadeSwaps = class _ArkadeSwaps {
4398
4288
  timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
4399
4289
  });
4400
4290
  if (!vhtlcScript.refundScript)
4401
- throw new Error(
4402
- `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4403
- );
4291
+ throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
4404
4292
  if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
4405
4293
  throw new SwapError({
4406
4294
  message: "Unable to claim: invalid VHTLC address"
4407
4295
  });
4408
4296
  }
4409
- const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4410
- const input = {
4411
- ...vtxo,
4412
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
4413
- tapTree: vhtlcScript.encode()
4414
- };
4415
- const output = {
4416
- amount: BigInt(vtxo.value),
4417
- script: import_sdk8.ArkAddress.decode(address).pkScript
4418
- };
4419
- if (isRecoverableVtxo) {
4420
- await this.joinBatch(this.wallet.identity, input, output, arkInfo);
4421
- } else {
4422
- await refundVHTLCwithOffchainTx(
4423
- pendingSwap.id,
4424
- this.wallet.identity,
4425
- this.arkProvider,
4426
- boltzXOnlyPublicKey,
4427
- ourXOnlyPublicKey,
4428
- serverXOnlyPublicKey,
4429
- input,
4430
- output,
4431
- arkInfo,
4432
- this.swapProvider.refundChainSwap.bind(this.swapProvider)
4297
+ const { vtxos } = await this.indexerProvider.getVtxos({
4298
+ scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
4299
+ });
4300
+ if (vtxos.length === 0) {
4301
+ throw new Error(
4302
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4433
4303
  );
4434
4304
  }
4305
+ const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
4306
+ if (unspentVtxos.length === 0) {
4307
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4308
+ }
4309
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
4310
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
4311
+ const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
4312
+ let boltzCallCount = 0;
4313
+ let sweptCount = 0;
4314
+ let skippedCount = 0;
4315
+ for (const vtxo of unspentVtxos) {
4316
+ const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4317
+ const output = {
4318
+ amount: BigInt(vtxo.value),
4319
+ script: outputScript
4320
+ };
4321
+ if (isSubmarineRefundLocktimeReached(refundLocktime)) {
4322
+ const input2 = {
4323
+ ...vtxo,
4324
+ tapLeafScript: refundWithoutReceiverLeaf,
4325
+ tapTree: vhtlcScript.encode()
4326
+ };
4327
+ await this.joinBatch(
4328
+ this.wallet.identity,
4329
+ input2,
4330
+ output,
4331
+ arkInfo,
4332
+ isRecoverableVtxo
4333
+ );
4334
+ sweptCount++;
4335
+ continue;
4336
+ }
4337
+ if (isRecoverableVtxo) {
4338
+ logger.error(
4339
+ `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.`
4340
+ );
4341
+ skippedCount++;
4342
+ continue;
4343
+ }
4344
+ const input = {
4345
+ ...vtxo,
4346
+ tapLeafScript: vhtlcScript.refund(),
4347
+ tapTree: vhtlcScript.encode()
4348
+ };
4349
+ try {
4350
+ if (boltzCallCount > 0) {
4351
+ await new Promise((r) => setTimeout(r, 2e3));
4352
+ }
4353
+ boltzCallCount++;
4354
+ await refundVHTLCwithOffchainTx(
4355
+ pendingSwap.id,
4356
+ this.wallet.identity,
4357
+ this.arkProvider,
4358
+ boltzXOnlyPublicKey,
4359
+ ourXOnlyPublicKey,
4360
+ serverXOnlyPublicKey,
4361
+ input,
4362
+ output,
4363
+ arkInfo,
4364
+ this.swapProvider.refundChainSwap.bind(this.swapProvider)
4365
+ );
4366
+ sweptCount++;
4367
+ } catch (error) {
4368
+ if (!(error instanceof BoltzRefundError)) {
4369
+ throw error;
4370
+ }
4371
+ if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
4372
+ logger.error(
4373
+ `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.`
4374
+ );
4375
+ skippedCount++;
4376
+ continue;
4377
+ }
4378
+ logger.warn(
4379
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
4380
+ );
4381
+ const fallbackInput = {
4382
+ ...vtxo,
4383
+ tapLeafScript: refundWithoutReceiverLeaf,
4384
+ tapTree: vhtlcScript.encode()
4385
+ };
4386
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
4387
+ sweptCount++;
4388
+ }
4389
+ }
4435
4390
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4436
4391
  await this.savePendingChainSwap({
4437
4392
  ...pendingSwap,
4438
4393
  status: finalStatus.status
4439
4394
  });
4395
+ return { swept: sweptCount, skipped: skippedCount };
4440
4396
  }
4441
4397
  // =========================================================================
4442
4398
  // Chain swaps: BTC -> ARK
@@ -4481,9 +4437,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4481
4437
  */
4482
4438
  async waitAndClaimArk(pendingSwap) {
4483
4439
  if (this.swapManager && await this.swapManager.hasSwap(pendingSwap.id)) {
4484
- const { txid } = await this.swapManager.waitForSwapCompletion(
4485
- pendingSwap.id
4486
- );
4440
+ const { txid } = await this.swapManager.waitForSwapCompletion(pendingSwap.id);
4487
4441
  return { txid };
4488
4442
  }
4489
4443
  return new Promise((resolve, reject) => {
@@ -4504,37 +4458,33 @@ var ArkadeSwaps = class _ArkadeSwaps {
4504
4458
  break;
4505
4459
  case "transaction.claimed":
4506
4460
  await updateSwapStatus();
4507
- const claimedStatus = await this.getSwapStatus(
4508
- pendingSwap.id
4509
- );
4461
+ const claimedStatus = await this.getSwapStatus(pendingSwap.id);
4510
4462
  resolve({
4511
4463
  txid: claimedStatus.transaction?.id ?? ""
4512
4464
  });
4513
4465
  break;
4514
4466
  case "transaction.claim.pending":
4515
4467
  await updateSwapStatus();
4516
- await this.signCooperativeClaimForServer(swap).catch(
4468
+ await this.signCooperativeClaimForServer(swap).catch((err) => {
4469
+ logger.error(`Failed to sign cooperative claim for ${swap.id}:`, err);
4470
+ });
4471
+ break;
4472
+ case "transaction.lockupFailed":
4473
+ await updateSwapStatus();
4474
+ await this.quoteSwap(swap.response.id, quoteOptionsForSwap(swap)).catch(
4517
4475
  (err) => {
4518
- logger.error(
4519
- `Failed to sign cooperative claim for ${swap.id}:`,
4520
- err
4476
+ reject(
4477
+ new SwapError({
4478
+ message: `Failed to renegotiate quote: ${err.message}`,
4479
+ isRefundable: false,
4480
+ // TODO btc refund not implemented yet
4481
+ pendingSwap: swap,
4482
+ cause: err
4483
+ })
4521
4484
  );
4522
4485
  }
4523
4486
  );
4524
4487
  break;
4525
- case "transaction.lockupFailed":
4526
- await updateSwapStatus();
4527
- await this.quoteSwap(swap.response.id).catch((err) => {
4528
- reject(
4529
- new SwapError({
4530
- message: `Failed to renegotiate quote: ${err.message}`,
4531
- isRefundable: false,
4532
- // TODO btc refund not implemented yet
4533
- pendingSwap: swap
4534
- })
4535
- );
4536
- });
4537
- break;
4538
4488
  case "swap.expired":
4539
4489
  await updateSwapStatus();
4540
4490
  reject(
@@ -4574,17 +4524,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
4574
4524
  */
4575
4525
  async claimArk(pendingSwap) {
4576
4526
  if (!pendingSwap.toAddress)
4577
- throw new Error(
4578
- `Swap ${pendingSwap.id}: destination address is required`
4579
- );
4527
+ throw new Error(`Swap ${pendingSwap.id}: destination address is required`);
4580
4528
  if (!pendingSwap.response.claimDetails.serverPublicKey)
4581
- throw new Error(
4582
- `Swap ${pendingSwap.id}: missing server public key in claim details`
4583
- );
4529
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in claim details`);
4584
4530
  if (!pendingSwap.response.claimDetails.timeouts)
4585
- throw new Error(
4586
- `Swap ${pendingSwap.id}: missing timeouts in claim details`
4587
- );
4531
+ throw new Error(`Swap ${pendingSwap.id}: missing timeouts in claim details`);
4588
4532
  const arkInfo = await this.arkProvider.getInfo();
4589
4533
  const preimage = import_base9.hex.decode(pendingSwap.preimage);
4590
4534
  const address = await this.wallet.getAddress();
@@ -4596,10 +4540,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4596
4540
  pendingSwap.response.claimDetails.serverPublicKey,
4597
4541
  "sender"
4598
4542
  );
4599
- const serverXOnlyPublicKey = normalizeToXOnlyKey(
4600
- arkInfo.signerPubkey,
4601
- "server"
4602
- );
4543
+ const serverXOnlyPublicKey = normalizeToXOnlyKey(arkInfo.signerPubkey, "server");
4603
4544
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4604
4545
  network: arkInfo.network,
4605
4546
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4609,9 +4550,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4609
4550
  timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
4610
4551
  });
4611
4552
  if (!vhtlcScript.claimScript)
4612
- throw new Error(
4613
- `Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
4614
- );
4553
+ throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
4615
4554
  if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
4616
4555
  throw new SwapError({
4617
4556
  message: "Unable to claim: invalid VHTLC address"
@@ -4628,15 +4567,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
4628
4567
  break;
4629
4568
  }
4630
4569
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
4631
- await new Promise(
4632
- (resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS)
4633
- );
4570
+ await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
4634
4571
  }
4635
4572
  }
4636
4573
  if (!vtxo) {
4637
- throw new Error(
4638
- `Swap ${pendingSwap.id}: no spendable virtual coins found`
4639
- );
4574
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
4640
4575
  }
4641
4576
  const input = {
4642
4577
  ...vtxo,
@@ -4647,10 +4582,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4647
4582
  amount: BigInt(vtxo.value),
4648
4583
  script: import_sdk8.ArkAddress.decode(address).pkScript
4649
4584
  };
4650
- const vhtlcIdentity = claimVHTLCIdentity(
4651
- this.wallet.identity,
4652
- preimage
4653
- );
4585
+ const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
4654
4586
  if ((0, import_sdk8.isRecoverable)(vtxo)) {
4655
4587
  await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
4656
4588
  } else {
@@ -4676,16 +4608,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
4676
4608
  */
4677
4609
  async signCooperativeClaimForServer(pendingSwap) {
4678
4610
  if (!pendingSwap.response.lockupDetails.swapTree)
4679
- throw new Error(
4680
- `Swap ${pendingSwap.id}: missing swap tree in lockup details`
4681
- );
4611
+ throw new Error(`Swap ${pendingSwap.id}: missing swap tree in lockup details`);
4682
4612
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
4683
- throw new Error(
4684
- `Swap ${pendingSwap.id}: missing server public key in lockup details`
4685
- );
4686
- const claimDetails = await this.swapProvider.getChainClaimDetails(
4687
- pendingSwap.id
4688
- );
4613
+ throw new Error(`Swap ${pendingSwap.id}: missing server public key in lockup details`);
4614
+ const claimDetails = await this.swapProvider.getChainClaimDetails(pendingSwap.id);
4689
4615
  const serverPubKey = pendingSwap.response.lockupDetails.serverPublicKey;
4690
4616
  if (claimDetails.publicKey !== serverPubKey) {
4691
4617
  throw new Error(
@@ -4701,9 +4627,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4701
4627
  );
4702
4628
  const musigNonces = musig.message(import_base9.hex.decode(claimDetails.transactionHash)).generateNonce().aggregateNonces([
4703
4629
  [
4704
- import_base9.hex.decode(
4705
- pendingSwap.response.lockupDetails.serverPublicKey
4706
- ),
4630
+ import_base9.hex.decode(pendingSwap.response.lockupDetails.serverPublicKey),
4707
4631
  import_base9.hex.decode(claimDetails.pubNonce)
4708
4632
  ]
4709
4633
  ]).initializeSession();
@@ -4722,10 +4646,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4722
4646
  * @returns The transaction ID of the claim.
4723
4647
  */
4724
4648
  async waitAndClaimChain(pendingSwap) {
4725
- if (pendingSwap.request.to === "ARK")
4726
- return this.waitAndClaimArk(pendingSwap);
4727
- if (pendingSwap.request.to === "BTC")
4728
- return this.waitAndClaimBtc(pendingSwap);
4649
+ if (pendingSwap.request.to === "ARK") return this.waitAndClaimArk(pendingSwap);
4650
+ if (pendingSwap.request.to === "BTC") return this.waitAndClaimBtc(pendingSwap);
4729
4651
  throw new SwapError({
4730
4652
  message: `Unsupported swap destination: ${pendingSwap.request.to}`
4731
4653
  });
@@ -4740,11 +4662,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4740
4662
  */
4741
4663
  async createChainSwap(args) {
4742
4664
  const { to, from, receiverLockAmount, senderLockAmount, toAddress } = args;
4743
- if (!toAddress)
4744
- throw new SwapError({ message: "Destination address is required" });
4665
+ if (!toAddress) throw new SwapError({ message: "Destination address is required" });
4745
4666
  const feeSatsPerByte = args.feeSatsPerByte ?? 1;
4746
- if (feeSatsPerByte <= 0)
4747
- throw new SwapError({ message: "Invalid feeSatsPerByte" });
4667
+ if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" });
4748
4668
  let amount, serverLockAmount, userLockAmount;
4749
4669
  if (receiverLockAmount) {
4750
4670
  amount = receiverLockAmount;
@@ -4759,8 +4679,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4759
4679
  }
4760
4680
  const preimage = (0, import_utils3.randomBytes)(32);
4761
4681
  const preimageHash = import_base9.hex.encode((0, import_sha23.sha256)(preimage));
4762
- if (!preimageHash)
4763
- throw new SwapError({ message: "Failed to get preimage hash" });
4682
+ if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" });
4764
4683
  const ephemeralKey = import_secp256k13.secp256k1.utils.randomSecretKey();
4765
4684
  const refundPublicKey = to === "ARK" ? import_base9.hex.encode(import_secp256k13.secp256k1.getPublicKey(ephemeralKey)) : import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
4766
4685
  if (!refundPublicKey)
@@ -4809,30 +4728,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
4809
4728
  const { to, from, swap, arkInfo } = args;
4810
4729
  if (from === "ARK") {
4811
4730
  if (!swap.response.lockupDetails.serverPublicKey)
4812
- throw new Error(
4813
- `Swap ${swap.id}: missing serverPublicKey in lockup details`
4814
- );
4731
+ throw new Error(`Swap ${swap.id}: missing serverPublicKey in lockup details`);
4815
4732
  if (!swap.response.lockupDetails.timeouts)
4816
- throw new Error(
4817
- `Swap ${swap.id}: missing timeouts in lockup details`
4818
- );
4733
+ throw new Error(`Swap ${swap.id}: missing timeouts in lockup details`);
4819
4734
  }
4820
4735
  if (to === "ARK") {
4821
4736
  if (!swap.response.claimDetails.serverPublicKey)
4822
- throw new Error(
4823
- `Swap ${swap.id}: missing serverPublicKey in claim details`
4824
- );
4737
+ throw new Error(`Swap ${swap.id}: missing serverPublicKey in claim details`);
4825
4738
  if (!swap.response.claimDetails.timeouts)
4826
- throw new Error(
4827
- `Swap ${swap.id}: missing timeouts in claim details`
4828
- );
4739
+ throw new Error(`Swap ${swap.id}: missing timeouts in claim details`);
4829
4740
  }
4830
4741
  const lockupAddress = to === "ARK" ? swap.response.claimDetails.lockupAddress : swap.response.lockupDetails.lockupAddress;
4831
4742
  const receiverPubkey = to === "ARK" ? swap.request.claimPublicKey : swap.response.lockupDetails.serverPublicKey;
4832
4743
  const senderPubkey = to === "ARK" ? swap.response.claimDetails.serverPublicKey : swap.request.refundPublicKey;
4833
- const serverPubkey = import_base9.hex.encode(
4834
- normalizeToXOnlyKey(arkInfo.signerPubkey, "server")
4835
- );
4744
+ const serverPubkey = import_base9.hex.encode(normalizeToXOnlyKey(arkInfo.signerPubkey, "server"));
4836
4745
  const vhtlcTimeouts = to === "ARK" ? swap.response.claimDetails.timeouts : swap.response.lockupDetails.timeouts;
4837
4746
  const { vhtlcAddress } = this.createVHTLCScript({
4838
4747
  network: arkInfo.network,
@@ -4850,15 +4759,122 @@ var ArkadeSwaps = class _ArkadeSwaps {
4850
4759
  return true;
4851
4760
  }
4852
4761
  /**
4853
- * Renegotiates the quote for an existing swap.
4762
+ * Renegotiates the quote for an existing chain swap. Convenience wrapper
4763
+ * over `getSwapQuote` + `acceptSwapQuote` with a safety floor.
4764
+ *
4765
+ * The floor is resolved in order:
4766
+ * 1. `options.minAcceptableAmount` if provided.
4767
+ * 2. The original `response.claimDetails.amount` of the stored
4768
+ * pending swap (Boltz-confirmed server-lock amount at creation).
4769
+ * 3. Otherwise throws `QuoteRejectedError({ reason: "no_baseline" })`.
4770
+ *
4771
+ * `options.maxSlippageBps` (default 0) relaxes the floor by basis points.
4772
+ * Quotes ≤ 0 are always rejected. On rejection the acceptance is NOT
4773
+ * posted to Boltz.
4774
+ *
4775
+ * Prefer `getSwapQuote` / `acceptSwapQuote` for callers that want to
4776
+ * inspect the quote before committing.
4777
+ *
4854
4778
  * @param swapId - The ID of the swap.
4779
+ * @param options - Optional floor and slippage configuration.
4855
4780
  * @returns The accepted quote amount.
4781
+ * @throws QuoteRejectedError if the quote is non-positive, below the
4782
+ * effective floor, or no baseline is available.
4783
+ */
4784
+ async quoteSwap(swapId, options) {
4785
+ const effectiveFloor = await this.resolveEffectiveFloor(swapId, options);
4786
+ const amount = await this.getSwapQuote(swapId);
4787
+ this.validateQuote(amount, effectiveFloor);
4788
+ await this.swapProvider.postChainQuote(swapId, { amount });
4789
+ return amount;
4790
+ }
4791
+ /**
4792
+ * Fetches a renegotiated quote from Boltz without accepting it.
4793
+ * Pair with `acceptSwapQuote` to commit a specific value.
4856
4794
  */
4857
- async quoteSwap(swapId) {
4795
+ async getSwapQuote(swapId) {
4858
4796
  const { amount } = await this.swapProvider.getChainQuote(swapId);
4797
+ return amount;
4798
+ }
4799
+ /**
4800
+ * Accepts a quote amount for an existing chain swap, after validating it
4801
+ * against the configured floor. See `quoteSwap` for floor-resolution rules.
4802
+ *
4803
+ * @throws QuoteRejectedError if `amount` ≤ 0, below the effective floor,
4804
+ * or no baseline is available.
4805
+ */
4806
+ async acceptSwapQuote(swapId, amount, options) {
4807
+ const effectiveFloor = await this.resolveEffectiveFloor(swapId, options);
4808
+ this.validateQuote(amount, effectiveFloor);
4859
4809
  await this.swapProvider.postChainQuote(swapId, { amount });
4860
4810
  return amount;
4861
4811
  }
4812
+ async resolveEffectiveFloor(swapId, options) {
4813
+ this.validateQuoteOptions(options);
4814
+ const floor = await this.resolveQuoteFloor(swapId, options);
4815
+ const slippageBps = options?.maxSlippageBps ?? 0;
4816
+ const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
4817
+ if (effectiveFloor < 1) {
4818
+ throw new TypeError(
4819
+ `Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
4820
+ );
4821
+ }
4822
+ return effectiveFloor;
4823
+ }
4824
+ async resolveQuoteFloor(swapId, options) {
4825
+ if (options?.minAcceptableAmount !== void 0) {
4826
+ return options.minAcceptableAmount;
4827
+ }
4828
+ const swaps = await this.swapRepository.getAllSwaps({
4829
+ id: swapId,
4830
+ type: "chain"
4831
+ });
4832
+ const stored = swaps[0];
4833
+ const amount = stored?.response?.claimDetails?.amount;
4834
+ if (typeof amount !== "number") {
4835
+ throw new QuoteRejectedError({ reason: "no_baseline" });
4836
+ }
4837
+ return amount;
4838
+ }
4839
+ validateQuoteOptions(options) {
4840
+ if (options?.minAcceptableAmount !== void 0) {
4841
+ const v = options.minAcceptableAmount;
4842
+ if (!Number.isInteger(v) || v <= 0) {
4843
+ throw new TypeError(
4844
+ `Invalid minAcceptableAmount: ${v} \u2014 must be a positive integer`
4845
+ );
4846
+ }
4847
+ }
4848
+ if (options?.maxSlippageBps !== void 0) {
4849
+ const v = options.maxSlippageBps;
4850
+ if (!Number.isInteger(v) || v < 0 || v > 1e4) {
4851
+ throw new TypeError(
4852
+ `Invalid maxSlippageBps: ${v} \u2014 must be an integer in [0, 10000]`
4853
+ );
4854
+ }
4855
+ }
4856
+ }
4857
+ validateQuote(amount, effectiveFloor) {
4858
+ if (!Number.isSafeInteger(amount)) {
4859
+ throw new QuoteRejectedError({
4860
+ reason: "non_safe_integer",
4861
+ quotedAmount: amount
4862
+ });
4863
+ }
4864
+ if (amount <= 0) {
4865
+ throw new QuoteRejectedError({
4866
+ reason: "non_positive",
4867
+ quotedAmount: amount
4868
+ });
4869
+ }
4870
+ if (amount < effectiveFloor) {
4871
+ throw new QuoteRejectedError({
4872
+ reason: "below_floor",
4873
+ quotedAmount: amount,
4874
+ floor: effectiveFloor
4875
+ });
4876
+ }
4877
+ }
4862
4878
  // =========================================================================
4863
4879
  // Shared utilities
4864
4880
  // =========================================================================
@@ -4872,14 +4888,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4872
4888
  * @returns The commitment transaction ID.
4873
4889
  */
4874
4890
  async joinBatch(identity, input, output, arkInfo, isRecoverable2 = true) {
4875
- return joinBatch(
4876
- this.arkProvider,
4877
- identity,
4878
- input,
4879
- output,
4880
- arkInfo,
4881
- isRecoverable2
4882
- );
4891
+ return joinBatch(this.arkProvider, identity, input, output, arkInfo, isRecoverable2);
4883
4892
  }
4884
4893
  /**
4885
4894
  * Creates a VHTLC script for the swap.
@@ -4917,9 +4926,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4917
4926
  async getPendingSubmarineSwaps() {
4918
4927
  const swaps = await this.getPendingSubmarineSwapsFromStorage();
4919
4928
  if (!swaps) return [];
4920
- return swaps.filter(
4921
- (swap) => swap.status === "invoice.set"
4922
- );
4929
+ return swaps.filter((swap) => swap.status === "invoice.set");
4923
4930
  }
4924
4931
  /**
4925
4932
  * Returns pending reverse swaps (those with status `swap.created`).
@@ -4927,9 +4934,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4927
4934
  async getPendingReverseSwaps() {
4928
4935
  const swaps = await this.getPendingReverseSwapsFromStorage();
4929
4936
  if (!swaps) return [];
4930
- return swaps.filter(
4931
- (swap) => swap.status === "swap.created"
4932
- );
4937
+ return swaps.filter((swap) => swap.status === "swap.created");
4933
4938
  }
4934
4939
  /**
4935
4940
  * Returns pending chain swaps (those with status `swap.created`).
@@ -4968,10 +4973,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4968
4973
  this.savePendingReverseSwap.bind(this)
4969
4974
  )
4970
4975
  ).catch((error) => {
4971
- logger.error(
4972
- `Failed to refresh swap status for ${swap.id}:`,
4973
- error
4974
- );
4976
+ logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
4975
4977
  })
4976
4978
  );
4977
4979
  }
@@ -4985,23 +4987,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
4985
4987
  this.savePendingSubmarineSwap.bind(this)
4986
4988
  )
4987
4989
  ).catch((error) => {
4988
- logger.error(
4989
- `Failed to refresh swap status for ${swap.id}:`,
4990
- error
4991
- );
4990
+ logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
4992
4991
  })
4993
4992
  );
4994
4993
  }
4995
4994
  for (const swap of await this.getPendingChainSwapsFromStorage()) {
4996
4995
  if (isChainFinalStatus(swap.status)) continue;
4997
4996
  promises.push(
4998
- this.getSwapStatus(swap.id).then(
4999
- ({ status }) => this.savePendingChainSwap({ ...swap, status })
5000
- ).catch((error) => {
5001
- logger.error(
5002
- `Failed to refresh swap status for ${swap.id}:`,
5003
- error
5004
- );
4997
+ this.getSwapStatus(swap.id).then(({ status }) => this.savePendingChainSwap({ ...swap, status })).catch((error) => {
4998
+ logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
5005
4999
  })
5006
5000
  );
5007
5001
  }
@@ -5018,9 +5012,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
5018
5012
  * display/monitoring and are not automatically wired into the SwapManager.
5019
5013
  */
5020
5014
  async restoreSwaps(boltzFees) {
5021
- const publicKey = import_base9.hex.encode(
5022
- await this.wallet.identity.compressedPublicKey()
5023
- );
5015
+ const publicKey = import_base9.hex.encode(await this.wallet.identity.compressedPublicKey());
5024
5016
  if (!publicKey) throw new Error("Failed to get public key from wallet");
5025
5017
  const fees = boltzFees ?? await this.swapProvider.getFees();
5026
5018
  const chainSwaps = [];
@@ -5072,25 +5064,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
5072
5064
  preimage: ""
5073
5065
  });
5074
5066
  } else if (isRestoredSubmarineSwap(swap)) {
5075
- const {
5076
- amount,
5077
- lockupAddress,
5078
- serverPublicKey,
5079
- tree,
5080
- timeoutBlockHeights
5081
- } = swap.refundDetails;
5067
+ const { amount, lockupAddress, serverPublicKey, tree, timeoutBlockHeights } = swap.refundDetails;
5082
5068
  let preimage = "";
5083
5069
  if (!isSubmarineFinalStatus(status)) {
5084
5070
  try {
5085
- const data = await this.swapProvider.getSwapPreimage(
5086
- swap.id
5087
- );
5071
+ const data = await this.swapProvider.getSwapPreimage(swap.id);
5088
5072
  preimage = data.preimage;
5089
5073
  } catch (error) {
5090
- logger.warn(
5091
- `Failed to restore preimage for submarine swap ${id}`,
5092
- error
5093
- );
5074
+ logger.warn(`Failed to restore preimage for submarine swap ${id}`, error);
5094
5075
  }
5095
5076
  }
5096
5077
  submarineSwaps.push({
@@ -5128,12 +5109,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
5128
5109
  } else if (isRestoredChainSwap(swap)) {
5129
5110
  const refundDetails = swap.refundDetails;
5130
5111
  if (!refundDetails) continue;
5131
- const {
5132
- amount,
5133
- lockupAddress,
5134
- serverPublicKey,
5135
- timeoutBlockHeight
5136
- } = refundDetails;
5112
+ const { amount, lockupAddress, serverPublicKey, timeoutBlockHeight } = refundDetails;
5137
5113
  chainSwaps.push({
5138
5114
  id,
5139
5115
  type: "chain",
@@ -5185,9 +5161,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
5185
5161
  var SWAP_POLL_TASK_TYPE = "swap-poll";
5186
5162
 
5187
5163
  // src/expo/arkade-lightning.ts
5188
- function getRandomId() {
5189
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
5190
- }
5164
+ var import_sdk9 = require("@arkade-os/sdk");
5191
5165
  function warnOnRemovedBackgroundFields(bg) {
5192
5166
  if (!bg || typeof bg !== "object") return;
5193
5167
  const removed = [];
@@ -5243,9 +5217,7 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
5243
5217
  const instance = new _ExpoArkadeSwaps(inner, config);
5244
5218
  await instance.seedSwapPollTask();
5245
5219
  if (config.background.foregroundIntervalMs && config.background.foregroundIntervalMs > 0) {
5246
- instance.startForegroundPolling(
5247
- config.background.foregroundIntervalMs
5248
- );
5220
+ instance.startForegroundPolling(config.background.foregroundIntervalMs);
5249
5221
  }
5250
5222
  return instance;
5251
5223
  }
@@ -5259,9 +5231,7 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
5259
5231
  const { taskQueue } = this.config.background;
5260
5232
  const results = await taskQueue.getResults();
5261
5233
  if (results.length > 0) {
5262
- await taskQueue.acknowledgeResults(
5263
- results.map((r) => r.id)
5264
- );
5234
+ await taskQueue.acknowledgeResults(results.map((r) => r.id));
5265
5235
  }
5266
5236
  await this.seedSwapPollTask();
5267
5237
  }
@@ -5270,7 +5240,7 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
5270
5240
  const existing = await taskQueue.getTasks(SWAP_POLL_TASK_TYPE);
5271
5241
  if (existing.length > 0) return;
5272
5242
  const task = {
5273
- id: getRandomId(),
5243
+ id: (0, import_sdk9.getRandomId)(),
5274
5244
  type: SWAP_POLL_TASK_TYPE,
5275
5245
  data: {},
5276
5246
  createdAt: Date.now()
@@ -5387,17 +5357,17 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
5387
5357
  verifyChainSwap(args) {
5388
5358
  return this.inner.verifyChainSwap(args);
5389
5359
  }
5390
- quoteSwap(swapId) {
5391
- return this.inner.quoteSwap(swapId);
5360
+ quoteSwap(swapId, options) {
5361
+ return this.inner.quoteSwap(swapId, options);
5362
+ }
5363
+ getSwapQuote(swapId) {
5364
+ return this.inner.getSwapQuote(swapId);
5365
+ }
5366
+ acceptSwapQuote(swapId, amount, options) {
5367
+ return this.inner.acceptSwapQuote(swapId, amount, options);
5392
5368
  }
5393
5369
  joinBatch(identity, input, output, arkInfo, isRecoverable2) {
5394
- return this.inner.joinBatch(
5395
- identity,
5396
- input,
5397
- output,
5398
- arkInfo,
5399
- isRecoverable2
5400
- );
5370
+ return this.inner.joinBatch(identity, input, output, arkInfo, isRecoverable2);
5401
5371
  }
5402
5372
  createVHTLCScript(params) {
5403
5373
  return this.inner.createVHTLCScript(params);