@satora/swap 0.2.0 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -2,12 +2,13 @@ import {
2
2
  DEFAULT_ESPLORA_URLS,
3
3
  esploraReader,
4
4
  htlcFactsFromEsploraTxs
5
- } from "./chunk-J35RZBDW.js";
5
+ } from "./chunk-EIWELJJK.js";
6
6
 
7
7
  // src/index.ts
8
8
  export * from "@lendasat/lendaswap-sdk-pure";
9
9
 
10
10
  // src/actions/derive.ts
11
+ var FUNDING_REAP_GRACE_MS = 60 * 60 * 1e3;
11
12
  function deriveSwapActions(input) {
12
13
  const {
13
14
  status,
@@ -22,12 +23,23 @@ function deriveSwapActions(input) {
22
23
  if (!clientFunds) {
23
24
  const wait = {
24
25
  id: "wait",
26
+ waitingOn: "client_payment",
25
27
  recommended: true,
26
28
  automation: "auto",
27
29
  reason: "Waiting for the Lightning payment to be locked in."
28
30
  };
29
31
  return { recommended: "wait", actions: [wait] };
30
32
  }
33
+ if (clientChainNow >= clientRefundLocktime + FUNDING_REAP_GRACE_MS) {
34
+ const done = {
35
+ id: "none",
36
+ outcome: "expired",
37
+ recommended: true,
38
+ automation: "auto",
39
+ reason: "The funding window closed."
40
+ };
41
+ return { recommended: "none", actions: [done] };
42
+ }
31
43
  if (clientChainNow >= clientRefundLocktime) return { actions: [] };
32
44
  const fund = {
33
45
  id: "fund",
@@ -41,6 +53,7 @@ function deriveSwapActions(input) {
41
53
  case "clientfundingseen": {
42
54
  const wait = {
43
55
  id: "wait",
56
+ waitingOn: "client_funding_confirmation",
44
57
  recommended: true,
45
58
  automation: "auto",
46
59
  reason: "Waiting for your funding transaction to confirm."
@@ -60,6 +73,7 @@ function deriveSwapActions(input) {
60
73
  }
61
74
  const wait = {
62
75
  id: "wait",
76
+ waitingOn: "server_funding",
63
77
  recommended: true,
64
78
  automation: "auto",
65
79
  reason: "Waiting for the server to fund the swap."
@@ -90,6 +104,7 @@ function deriveSwapActions(input) {
90
104
  if (!clientFunds) {
91
105
  const done = {
92
106
  id: "none",
107
+ outcome: "refunded",
93
108
  recommended: true,
94
109
  automation: "auto",
95
110
  reason: "The claim window passed \u2014 your Lightning payment will be refunded automatically."
@@ -108,6 +123,7 @@ function deriveSwapActions(input) {
108
123
  }
109
124
  const wait = {
110
125
  id: "wait",
126
+ waitingOn: "refund_timelock",
111
127
  recommended: true,
112
128
  automation: "auto",
113
129
  reason: "Wait for your refund timelock \u2014 do not claim, as revealing the preimage would let the server take your deposit."
@@ -127,6 +143,7 @@ function deriveSwapActions(input) {
127
143
  case "clientredeeming": {
128
144
  const wait = {
129
145
  id: "wait",
146
+ waitingOn: "claim_confirmation",
130
147
  recommended: true,
131
148
  automation: "auto",
132
149
  reason: "Redeeming your funds \u2014 waiting for the claim to confirm."
@@ -137,6 +154,7 @@ function deriveSwapActions(input) {
137
154
  case "serverredeemed": {
138
155
  const done = {
139
156
  id: "none",
157
+ outcome: "completed",
140
158
  recommended: true,
141
159
  automation: "auto",
142
160
  reason: "Swap complete \u2014 you've received your funds."
@@ -146,6 +164,7 @@ function deriveSwapActions(input) {
146
164
  case "expired": {
147
165
  const done = {
148
166
  id: "none",
167
+ outcome: "expired",
149
168
  recommended: true,
150
169
  automation: "auto",
151
170
  reason: "The swap expired without funding \u2014 nothing was locked."
@@ -157,6 +176,7 @@ function deriveSwapActions(input) {
157
176
  case "clientrefundedserverfunded": {
158
177
  const done = {
159
178
  id: "none",
179
+ outcome: "refunded",
160
180
  recommended: true,
161
181
  automation: "auto",
162
182
  reason: "Refunded \u2014 your deposit was returned."
@@ -166,6 +186,8 @@ function deriveSwapActions(input) {
166
186
  case "clientredeemedandclientrefunded": {
167
187
  const done = {
168
188
  id: "none",
189
+ // The client holds both sides' funds — "completed" from its viewpoint.
190
+ outcome: "completed",
169
191
  recommended: true,
170
192
  automation: "auto",
171
193
  reason: "This swap ended in an anomalous state (redeemed and refunded); no action needed."
@@ -188,6 +210,7 @@ function deriveSwapActions(input) {
188
210
  }
189
211
  const wait = {
190
212
  id: "wait",
213
+ waitingOn: "refund_timelock",
191
214
  recommended: true,
192
215
  automation: "auto",
193
216
  reason: "The swap did not complete; waiting for the refund timelock to reclaim your deposit."
@@ -358,6 +381,7 @@ var ArkadeContractManager = class _ArkadeContractManager {
358
381
  /** pkScript → the verified preimage recovered from a claim spend. */
359
382
  #preimages = /* @__PURE__ */ new Map();
360
383
  #listeners = /* @__PURE__ */ new Set();
384
+ /** Last MTP reading (ms) + when it was fetched, for extrapolation. */
361
385
  #now;
362
386
  constructor(deps) {
363
387
  this.#indexer = deps.indexer;
@@ -383,7 +407,6 @@ var ArkadeContractManager = class _ArkadeContractManager {
383
407
  `ArkadeContractManager can't track a '${ref.ledger}' HTLC`
384
408
  );
385
409
  this.#refs.set(ref.script, ref);
386
- await this.#reconcileRef(ref);
387
410
  }
388
411
  async unregister(ref) {
389
412
  if (ref.ledger !== "arkade") return;
@@ -395,23 +418,27 @@ var ArkadeContractManager = class _ArkadeContractManager {
395
418
  return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
396
419
  }
397
420
  chainNow(_ref) {
398
- return this.#now;
421
+ if (!this.#now) return void 0;
422
+ return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
399
423
  }
400
424
  onEvent(cb) {
401
425
  this.#listeners.add(cb);
402
426
  return () => this.#listeners.delete(cb);
403
427
  }
428
+ /**
429
+ * Full reconcile of every tracked ref (clock + vtxos). Called on demand
430
+ * (tracker start / newly tracked swap), NOT periodically: onward, hints and
431
+ * the tracker's at-risk safety net drive targeted `reconcile`s.
432
+ */
404
433
  async refresh() {
405
- if (this.#chainTime) this.#now = await this.#chainTime();
406
- await Promise.all(
407
- [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
408
- );
434
+ await this.#scanAll();
409
435
  }
436
+ /** Targeted verify (hint / pre-action path) — never gated. */
410
437
  async reconcile(ref) {
411
438
  if (ref.ledger !== "arkade") return;
412
439
  const tracked = this.#refs.get(ref.script);
413
440
  if (!tracked) return;
414
- if (this.#chainTime) this.#now = await this.#chainTime();
441
+ await this.#readClock();
415
442
  await this.#reconcileRef(tracked);
416
443
  }
417
444
  dispose() {
@@ -421,6 +448,18 @@ var ArkadeContractManager = class _ArkadeContractManager {
421
448
  getPreimage(ref) {
422
449
  return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
423
450
  }
451
+ /** Read and store the MTP clock (with fetch time, for extrapolation). */
452
+ async #readClock() {
453
+ if (!this.#chainTime) return;
454
+ this.#now = { mtpMs: await this.#chainTime(), fetchedAtMs: Date.now() };
455
+ }
456
+ /** Full scan of every tracked ref (clock + vtxos). */
457
+ async #scanAll() {
458
+ await this.#readClock();
459
+ await Promise.all(
460
+ [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
461
+ );
462
+ }
424
463
  /** Read the VHTLC's vtxos and map them to an observation. */
425
464
  async #reconcileRef(ref) {
426
465
  const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
@@ -513,6 +552,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
513
552
  /** address → the preimage a claim revealed. */
514
553
  #preimages = /* @__PURE__ */ new Map();
515
554
  #listeners = /* @__PURE__ */ new Set();
555
+ /** Last MTP reading (ms) + when it was fetched, for extrapolation. */
516
556
  #now;
517
557
  constructor(deps) {
518
558
  this.#reader = deps.reader;
@@ -522,9 +562,11 @@ var BitcoinContractManager = class _BitcoinContractManager {
522
562
  return new _BitcoinContractManager(deps);
523
563
  }
524
564
  static async create(config) {
525
- const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-GRUP7RAG.js");
565
+ const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-HV6CANAA.js");
526
566
  return _BitcoinContractManager.fromDeps({
527
- reader: esploraReader2(config.esploraUrl),
567
+ reader: esploraReader2(config.esploraUrl, void 0, {
568
+ minConfirmations: config.minConfirmations
569
+ }),
528
570
  chainTime: config.chainTime
529
571
  });
530
572
  }
@@ -549,14 +591,15 @@ var BitcoinContractManager = class _BitcoinContractManager {
549
591
  return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
550
592
  }
551
593
  chainNow(_ref) {
552
- return this.#now;
594
+ if (!this.#now) return void 0;
595
+ return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
553
596
  }
554
597
  onEvent(cb) {
555
598
  this.#listeners.add(cb);
556
599
  return () => this.#listeners.delete(cb);
557
600
  }
558
601
  async refresh() {
559
- if (this.#chainTime) this.#now = await this.#chainTime();
602
+ await this.#readClock();
560
603
  await Promise.all(
561
604
  [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
562
605
  );
@@ -565,9 +608,14 @@ var BitcoinContractManager = class _BitcoinContractManager {
565
608
  if (ref.ledger !== "bitcoin") return;
566
609
  const tracked = this.#refs.get(ref.address);
567
610
  if (!tracked) return;
568
- if (this.#chainTime) this.#now = await this.#chainTime();
611
+ await this.#readClock();
569
612
  await this.#reconcileRef(tracked);
570
613
  }
614
+ /** Read and store the MTP clock (with fetch time, for extrapolation). */
615
+ async #readClock() {
616
+ if (!this.#chainTime) return;
617
+ this.#now = { mtpMs: await this.#chainTime(), fetchedAtMs: Date.now() };
618
+ }
571
619
  dispose() {
572
620
  this.#listeners.clear();
573
621
  }
@@ -576,7 +624,10 @@ var BitcoinContractManager = class _BitcoinContractManager {
576
624
  return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
577
625
  }
578
626
  async #reconcileRef(ref) {
579
- const facts = await this.#reader.getHtlcFacts(ref.address);
627
+ const facts = await this.#reader.getHtlcFacts(
628
+ ref.address,
629
+ ref.minConfirmations
630
+ );
580
631
  const { observation, preimage } = bitcoinObservation(
581
632
  facts,
582
633
  hex4.decode(ref.preimageHash),
@@ -622,14 +673,59 @@ function htlcKey(ref) {
622
673
  return `arkade:${ref.script}`;
623
674
  case "bitcoin":
624
675
  return `bitcoin:${ref.address}`;
625
- case "evm":
626
- return `evm:${ref.chainId}:${ref.preimageHash}`;
676
+ case "evm": {
677
+ const base = `evm:${ref.chainId}:${ref.htlc.toLowerCase()}:${ref.preimageHash.toLowerCase()}`;
678
+ if (!ref.expectedToken || !ref.sender || ref.timelockSec === void 0) {
679
+ return base;
680
+ }
681
+ return [
682
+ base,
683
+ ref.expectedAmount,
684
+ ref.expectedToken.toLowerCase(),
685
+ ref.sender.toLowerCase(),
686
+ ref.claimAddress.toLowerCase(),
687
+ ref.timelockSec
688
+ ].join(":");
689
+ }
627
690
  case "lightning":
628
691
  return `lightning:${ref.paymentHash}`;
629
692
  }
630
693
  }
631
694
 
632
695
  // src/contracts/evm-manager.ts
696
+ function htlcQueryKey(q) {
697
+ const base = `${q.htlc.toLowerCase()}:${q.preimageHash.toLowerCase()}`;
698
+ if (!q.terms || !q.claimAddress) return base;
699
+ const { amount, token, sender, timelockSec } = q.terms;
700
+ return [
701
+ base,
702
+ amount,
703
+ token.toLowerCase(),
704
+ sender.toLowerCase(),
705
+ q.claimAddress.toLowerCase(),
706
+ timelockSec
707
+ ].join(":");
708
+ }
709
+ function refQueryKey(ref) {
710
+ const full = activeQuery(ref);
711
+ return htlcQueryKey({
712
+ htlc: ref.htlc,
713
+ preimageHash: ref.preimageHash,
714
+ claimAddress: ref.claimAddress,
715
+ terms: full && {
716
+ amount: full.amount,
717
+ token: full.token,
718
+ sender: full.sender,
719
+ timelockSec: full.timelockSec
720
+ }
721
+ });
722
+ }
723
+ var AVG_BLOCK_MS = {
724
+ 1: 12e3,
725
+ 137: 2100,
726
+ 42161: 250
727
+ };
728
+ var SCAN_MARGIN_BLOCKS = 1000n;
633
729
  var EvmContractManager = class _EvmContractManager {
634
730
  ledger = "evm";
635
731
  #readers;
@@ -639,14 +735,8 @@ var EvmContractManager = class _EvmContractManager {
639
735
  #obs = /* @__PURE__ */ new Map();
640
736
  /** htlcKey → the preimage a claim revealed. */
641
737
  #preimages = /* @__PURE__ */ new Map();
642
- /** chainId → its latest block time (ms). */
738
+ /** chainId → its last clock reading (extrapolated in {@link chainNow}). */
643
739
  #now = /* @__PURE__ */ new Map();
644
- /** chainId → unsubscribe for its (single, shared) watch. */
645
- #watchUnsubs = /* @__PURE__ */ new Map();
646
- /** chainId → a block-event reconcile is currently running (coalescing). */
647
- #reconciling = /* @__PURE__ */ new Set();
648
- /** chainId → a block event arrived mid-reconcile; run exactly one more. */
649
- #reconcileAgain = /* @__PURE__ */ new Set();
650
740
  #listeners = /* @__PURE__ */ new Set();
651
741
  constructor(deps) {
652
742
  this.#readers = deps.readers;
@@ -665,8 +755,6 @@ var EvmContractManager = class _EvmContractManager {
665
755
  `no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
666
756
  );
667
757
  this.#refs.set(htlcKey(ref), ref);
668
- this.#ensureWatch(ref.chainId);
669
- await this.#reconcileChain(ref.chainId);
670
758
  }
671
759
  async unregister(ref) {
672
760
  if (ref.ledger !== "evm") return;
@@ -674,100 +762,176 @@ var EvmContractManager = class _EvmContractManager {
674
762
  this.#refs.delete(key);
675
763
  this.#obs.delete(key);
676
764
  this.#preimages.delete(key);
677
- if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId)) {
678
- this.#watchUnsubs.get(ref.chainId)?.();
679
- this.#watchUnsubs.delete(ref.chainId);
765
+ if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
680
766
  this.#now.delete(ref.chainId);
681
- this.#reconcileAgain.delete(ref.chainId);
682
- }
683
767
  }
684
768
  getState(ref) {
685
769
  return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
686
770
  }
687
771
  chainNow(ref) {
688
- return ref.ledger === "evm" ? this.#now.get(ref.chainId) : void 0;
772
+ if (ref.ledger !== "evm") return void 0;
773
+ const clock = this.#now.get(ref.chainId);
774
+ if (!clock) return void 0;
775
+ return clock.blockTimeMs + Math.max(0, Date.now() - clock.fetchedAtMs);
689
776
  }
690
777
  onEvent(cb) {
691
778
  this.#listeners.add(cb);
692
779
  return () => this.#listeners.delete(cb);
693
780
  }
781
+ /**
782
+ * Full reconcile of every tracked chain — seeds clocks + observations. Called
783
+ * on demand (tracker start / newly tracked swap), NOT periodically: onward,
784
+ * hints drive targeted `reconcile`s and the tracker's at-risk safety net
785
+ * re-reads only swaps with client funds on the line.
786
+ */
694
787
  async refresh() {
695
- const chainIds = new Set([...this.#refs.values()].map((r) => r.chainId));
696
- await Promise.all([...chainIds].map((c) => this.#reconcileChain(c)));
788
+ const chainIds = [
789
+ ...new Set([...this.#refs.values()].map((r) => r.chainId))
790
+ ];
791
+ const results = await Promise.allSettled(
792
+ chainIds.map((c) => this.#reconcileChain(c))
793
+ );
794
+ results.forEach((result, index) => {
795
+ if (result.status === "rejected")
796
+ console.warn(
797
+ `EVM reconcile failed for chain ${chainIds[index]}:`,
798
+ result.reason
799
+ );
800
+ });
697
801
  }
802
+ /** Targeted verify (hint / pre-action path) — never gated. */
698
803
  async reconcile(ref) {
699
804
  if (ref.ledger !== "evm") return;
700
805
  const tracked = this.#refs.get(htlcKey(ref));
701
806
  if (!tracked) return;
702
807
  const reader = this.#readers.get(tracked.chainId);
703
808
  if (!reader) return;
704
- this.#now.set(tracked.chainId, await reader.getBlockTimeMs());
705
- await this.#reconcileRef(reader, tracked);
809
+ await this.#readClock(reader, tracked.chainId);
810
+ await this.#reconcileRefs(reader, tracked.chainId, [tracked]);
706
811
  }
707
812
  dispose() {
708
- for (const unsub of this.#watchUnsubs.values()) unsub();
709
- this.#watchUnsubs.clear();
710
- this.#reconciling.clear();
711
- this.#reconcileAgain.clear();
712
813
  this.#listeners.clear();
713
814
  }
714
815
  /** The preimage a claim revealed on this HTLC, if one was seen. */
715
816
  getPreimage(ref) {
716
817
  return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
717
818
  }
718
- #ensureWatch(chainId) {
719
- if (this.#watchUnsubs.has(chainId)) return;
720
- const reader = this.#readers.get(chainId);
721
- if (!reader) return;
722
- this.#watchUnsubs.set(
723
- chainId,
724
- reader.watch(() => this.#scheduleReconcile(chainId))
725
- );
726
- }
727
- /**
728
- * Reconcile a chain in response to a block/log event, coalesced: at most one
729
- * reconcile runs per chain at a time, with at most one queued rerun to pick up
730
- * changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
731
- * redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
732
- * transient RPC failure in a background reconcile from becoming an unhandled
733
- * rejection. `register`/`refresh` still await `#reconcileChain` directly.
734
- */
735
- #scheduleReconcile(chainId) {
736
- if (this.#reconciling.has(chainId)) {
737
- this.#reconcileAgain.add(chainId);
738
- return;
739
- }
740
- this.#reconciling.add(chainId);
741
- void this.#reconcileChain(chainId).catch((error) => {
742
- console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
743
- }).finally(() => {
744
- this.#reconciling.delete(chainId);
745
- if (this.#reconcileAgain.delete(chainId))
746
- this.#scheduleReconcile(chainId);
819
+ /** Read and store a chain's clock (with its fetch time, for extrapolation). */
820
+ async #readClock(reader, chainId) {
821
+ const block = await reader.getLatestBlock();
822
+ this.#now.set(chainId, {
823
+ blockTimeMs: block.timeMs,
824
+ blockNumber: block.number,
825
+ fetchedAtMs: Date.now()
747
826
  });
748
827
  }
749
828
  /** Refresh one chain's clock and re-observe every HTLC tracked on it. */
750
829
  async #reconcileChain(chainId) {
751
830
  const reader = this.#readers.get(chainId);
752
831
  if (!reader) return;
753
- this.#now.set(chainId, await reader.getBlockTimeMs());
832
+ await this.#readClock(reader, chainId);
833
+ const refs = [...this.#refs.values()].filter((r) => r.chainId === chainId);
834
+ await this.#reconcileRefs(reader, chainId, refs);
835
+ }
836
+ /**
837
+ * Re-observe the given refs. Two tiers:
838
+ *
839
+ * 1. Refs with the full contract tuple get the cheap `isActive` check —
840
+ * `true` proves open-and-on-expected-terms (the swap key hashes them) →
841
+ * `confirmed`, no log scan at all. This is the common case.
842
+ * 2. Everything else — an incomplete tuple, an inactive-and-unclassified
843
+ * HTLC (never funded vs claimed vs refunded), or an `isActive` transport
844
+ * failure (e.g. no Multicall3 on a dev chain) — is classified from one
845
+ * batched log read, lower-bounded by the swaps' creation-block estimate.
846
+ *
847
+ * A ref already latched on a spend is skipped entirely: terminal per leg,
848
+ * nothing left to learn.
849
+ */
850
+ async #reconcileRefs(reader, chainId, refs) {
851
+ const open = refs.filter((r) => !this.#isSpent(r));
852
+ if (open.length === 0) return;
853
+ const fast = open.filter((r) => activeQuery(r) !== void 0);
854
+ let needLogs = open.filter((r) => activeQuery(r) === void 0);
855
+ if (fast.length > 0) {
856
+ try {
857
+ const active = await reader.isActiveBatch(
858
+ fast.map((r) => activeQuery(r))
859
+ );
860
+ for (const ref of fast) {
861
+ if (active.get(refQueryKey(ref)) === true)
862
+ this.#set(htlcKey(ref), "confirmed");
863
+ else needLogs.push(ref);
864
+ }
865
+ } catch (error) {
866
+ console.warn(
867
+ `EVM isActive check failed for chain ${chainId} \u2014 falling back to logs:`,
868
+ error
869
+ );
870
+ needLogs = open;
871
+ }
872
+ }
873
+ if (needLogs.length === 0) return;
874
+ const known = needLogs.filter((r) => r.createdAtMs !== void 0);
875
+ const unknown = needLogs.filter((r) => r.createdAtMs === void 0);
754
876
  await Promise.all(
755
- [...this.#refs.values()].filter((r) => r.chainId === chainId).map((r) => this.#reconcileRef(reader, r))
877
+ [
878
+ [known, this.#estimateFromBlock(chainId, known)],
879
+ [unknown, 0n]
880
+ ].filter(([refs2]) => refs2.length > 0).map(
881
+ ([refs2, fromBlock]) => this.#classifyFromLogs(reader, refs2, fromBlock)
882
+ )
756
883
  );
757
884
  }
758
- async #reconcileRef(reader, ref) {
759
- const events = await reader.getHtlcEvents(
760
- ref.htlc,
761
- ref.preimageHash,
762
- ref.claimAddress
885
+ /** Read the refs' lifecycle logs (from `fromBlock`) and set observations. */
886
+ async #classifyFromLogs(reader, refs, fromBlock) {
887
+ const events = await reader.getHtlcEventsBatch(
888
+ refs.map((r) => {
889
+ const full = activeQuery(r);
890
+ return {
891
+ htlc: r.htlc,
892
+ preimageHash: r.preimageHash,
893
+ claimAddress: r.claimAddress,
894
+ terms: full && {
895
+ amount: full.amount,
896
+ token: full.token,
897
+ sender: full.sender,
898
+ timelockSec: full.timelockSec
899
+ }
900
+ };
901
+ }),
902
+ fromBlock
763
903
  );
764
- const { observation, preimage } = evmObservation(events, {
765
- amount: ref.expectedAmount,
766
- token: ref.expectedToken
767
- });
768
- const key = htlcKey(ref);
769
- if (preimage) this.#preimages.set(key, preimage);
770
- this.#set(key, observation);
904
+ for (const ref of refs) {
905
+ const { observation, preimage } = evmObservation(
906
+ events.get(refQueryKey(ref)) ?? [],
907
+ { amount: ref.expectedAmount, token: ref.expectedToken }
908
+ );
909
+ const key = htlcKey(ref);
910
+ if (preimage) this.#preimages.set(key, preimage);
911
+ this.#set(key, observation);
912
+ }
913
+ }
914
+ #isSpent(ref) {
915
+ const obs = this.#obs.get(htlcKey(ref));
916
+ return obs === "spent_claim" || obs === "spent_refund";
917
+ }
918
+ /**
919
+ * The block to scan logs from: roughly where the OLDEST of the given swaps
920
+ * was created (an HTLC has no events before its swap existed). Only a lower
921
+ * bound, so estimation errs early — ×1.5 on elapsed time plus a fixed block
922
+ * margin. Falls back to genesis when the creation time, clock, or the
923
+ * chain's block time is unknown.
924
+ */
925
+ #estimateFromBlock(chainId, refs) {
926
+ const clock = this.#now.get(chainId);
927
+ const avgBlockMs = AVG_BLOCK_MS[chainId];
928
+ const createdAts = refs.map((r) => r.createdAtMs ?? 0);
929
+ const oldestMs = Math.min(...createdAts);
930
+ if (!clock || !avgBlockMs || oldestMs <= 0) return 0n;
931
+ const elapsedMs = Math.max(0, clock.blockTimeMs - oldestMs);
932
+ const blocksBehind = BigInt(Math.ceil(elapsedMs / avgBlockMs * 1.5)) + SCAN_MARGIN_BLOCKS;
933
+ const from = clock.blockNumber - blocksBehind;
934
+ return from > 0n ? from : 0n;
771
935
  }
772
936
  /** Update an observation and notify on change; never downgrade a resolved spend. */
773
937
  #set(key, observation) {
@@ -782,13 +946,31 @@ var EvmContractManager = class _EvmContractManager {
782
946
  for (const listener of this.#listeners) listener(ref, observation);
783
947
  }
784
948
  };
949
+ function activeQuery(ref) {
950
+ if (ref.sender === void 0 || ref.expectedToken === void 0 || ref.timelockSec === void 0)
951
+ return void 0;
952
+ return {
953
+ htlc: ref.htlc,
954
+ preimageHash: ref.preimageHash,
955
+ amount: ref.expectedAmount,
956
+ token: ref.expectedToken,
957
+ sender: ref.sender,
958
+ claimAddress: ref.claimAddress,
959
+ timelockSec: ref.timelockSec
960
+ };
961
+ }
785
962
 
786
963
  // src/contracts/evm-reader-viem.ts
787
964
  import {
788
965
  createPublicClient,
966
+ decodeEventLog,
967
+ decodeFunctionResult,
968
+ encodeFunctionData,
789
969
  fallback,
790
970
  http,
791
- parseAbiItem
971
+ numberToHex,
972
+ parseAbiItem,
973
+ toEventSelector
792
974
  } from "viem";
793
975
  var DEFAULT_EVM_RPCS = {
794
976
  // Polygon — viem's default (polygon.drpc.org) misbehaves for some calls, so
@@ -798,82 +980,185 @@ var DEFAULT_EVM_RPCS = {
798
980
  "https://tenderly.rpc.polygon.community",
799
981
  "https://polygon-bor-rpc.publicnode.com"
800
982
  ],
801
- 1: [
802
- "https://ethereum-rpc.publicnode.com",
803
- "https://rpc.ankr.com/eth",
804
- "https://eth.drpc.org"
805
- ],
983
+ // No publicnode: it rate-limits by IP across its whole fleet (403s), and as
984
+ // a PRIMARY that costs a full retry cycle per read before failing over.
985
+ // Both entries verified for eth_getLogs + eth_call from a browser origin.
986
+ 1: ["https://eth.drpc.org", "https://rpc.mevblocker.io"],
987
+ // The official gateway first; publicnode last (see above).
806
988
  42161: [
807
- "https://arbitrum-one-rpc.publicnode.com",
808
- "https://rpc.ankr.com/arbitrum",
809
- "https://arb1.arbitrum.io/rpc"
989
+ "https://arb1.arbitrum.io/rpc",
990
+ "https://arbitrum.drpc.org",
991
+ "https://arbitrum-one-rpc.publicnode.com"
810
992
  ]
811
993
  };
812
994
  var SWAP_CREATED = parseAbiItem(
813
- "event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock)"
995
+ "event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock, bytes32 key)"
814
996
  );
815
997
  var SWAP_REDEEMED = parseAbiItem(
816
- "event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
998
+ "event SwapRedeemed(bytes32 indexed preimageHash, bytes32 indexed key, bytes32 preimage)"
817
999
  );
818
1000
  var SWAP_REFUNDED = parseAbiItem(
819
- "event SwapRefunded(bytes32 indexed preimageHash)"
1001
+ "event SwapRefunded(bytes32 indexed preimageHash, bytes32 indexed key)"
1002
+ );
1003
+ var HTLC_EVENTS_ABI = [SWAP_CREATED, SWAP_REDEEMED, SWAP_REFUNDED];
1004
+ var HTLC_EVENT_TOPICS = HTLC_EVENTS_ABI.map(toEventSelector);
1005
+ var IS_ACTIVE = parseAbiItem(
1006
+ "function isActive(bytes32 preimageHash, uint256 amount, address token, address sender, address claimAddress, uint256 timelock) view returns (bool)"
820
1007
  );
1008
+ var MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
1009
+ var AGGREGATE3 = parseAbiItem(
1010
+ "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)"
1011
+ );
1012
+ function activeKey(q) {
1013
+ return htlcQueryKey({
1014
+ htlc: q.htlc,
1015
+ preimageHash: q.preimageHash,
1016
+ claimAddress: q.claimAddress,
1017
+ terms: {
1018
+ amount: q.amount,
1019
+ token: q.token,
1020
+ sender: q.sender,
1021
+ timelockSec: q.timelockSec
1022
+ }
1023
+ });
1024
+ }
821
1025
  function evmReaderFromClient(client) {
822
1026
  return {
823
- async getHtlcEvents(htlc, preimageHash, claimAddress) {
824
- const [created, redeemed, refunded] = await Promise.all([
825
- // Filter by claimAddress too (also indexed), so only the HTLC actually
826
- // claimable on the swap's terms is seen.
827
- client.getLogs({
828
- address: htlc,
829
- event: SWAP_CREATED,
830
- args: { preimageHash, claimAddress },
831
- fromBlock: "earliest"
832
- }),
833
- client.getLogs({
834
- address: htlc,
835
- event: SWAP_REDEEMED,
836
- args: { preimageHash },
837
- fromBlock: "earliest"
838
- }),
839
- client.getLogs({
840
- address: htlc,
841
- event: SWAP_REFUNDED,
842
- args: { preimageHash },
843
- fromBlock: "earliest"
1027
+ async getHtlcEventsBatch(queries, fromBlock = 0n) {
1028
+ const results = /* @__PURE__ */ new Map();
1029
+ if (queries.length === 0) return results;
1030
+ for (const q of queries) results.set(htlcQueryKey(q), []);
1031
+ const logs = await client.request({
1032
+ method: "eth_getLogs",
1033
+ params: [
1034
+ {
1035
+ address: unique(queries.map((q) => q.htlc)),
1036
+ topics: [
1037
+ HTLC_EVENT_TOPICS,
1038
+ unique(queries.map((q) => q.preimageHash))
1039
+ ],
1040
+ fromBlock: numberToHex(fromBlock)
1041
+ }
1042
+ ]
1043
+ });
1044
+ const grouped = /* @__PURE__ */ new Map();
1045
+ for (const log of logs) {
1046
+ const decoded = tryDecode(log);
1047
+ if (!decoded) continue;
1048
+ const group = `${log.address.toLowerCase()}:${decoded.preimageHash.toLowerCase()}`;
1049
+ const existing = grouped.get(group);
1050
+ if (existing) existing.push(decoded);
1051
+ else grouped.set(group, [decoded]);
1052
+ }
1053
+ for (const query of queries) {
1054
+ const group = `${query.htlc.toLowerCase()}:${query.preimageHash.toLowerCase()}`;
1055
+ const events = results.get(htlcQueryKey(query));
1056
+ if (!events) continue;
1057
+ for (const decoded of grouped.get(group) ?? []) {
1058
+ const event = toHtlcEvent(decoded, query);
1059
+ if (event) events.push(event);
1060
+ }
1061
+ }
1062
+ return results;
1063
+ },
1064
+ async isActiveBatch(queries) {
1065
+ const results = /* @__PURE__ */ new Map();
1066
+ if (queries.length === 0) return results;
1067
+ const calldatas = queries.map(
1068
+ (q) => encodeFunctionData({
1069
+ abi: [IS_ACTIVE],
1070
+ args: [
1071
+ q.preimageHash,
1072
+ q.amount,
1073
+ q.token,
1074
+ q.sender,
1075
+ q.claimAddress,
1076
+ BigInt(q.timelockSec)
1077
+ ]
844
1078
  })
845
- ]);
846
- const events = [];
847
- for (const log of created) {
848
- const args = log.args;
849
- events.push({
850
- kind: "created",
851
- amount: args?.amount ?? 0n,
852
- token: args?.token ?? "0x"
1079
+ );
1080
+ const callOne = async (i) => {
1081
+ const { data } = await client.call({
1082
+ to: queries[i].htlc,
1083
+ data: calldatas[i]
853
1084
  });
1085
+ results.set(activeKey(queries[i]), decodeIsActive(data));
1086
+ };
1087
+ if (queries.length === 1) {
1088
+ await callOne(0);
1089
+ return results;
854
1090
  }
855
- for (const log of redeemed) {
856
- const preimage = log.args?.preimage;
857
- if (preimage) events.push({ kind: "redeemed", preimage });
1091
+ try {
1092
+ const { data } = await client.call({
1093
+ to: MULTICALL3_ADDRESS,
1094
+ data: encodeFunctionData({
1095
+ abi: [AGGREGATE3],
1096
+ args: [
1097
+ queries.map((q, i) => ({
1098
+ target: q.htlc,
1099
+ allowFailure: true,
1100
+ callData: calldatas[i]
1101
+ }))
1102
+ ]
1103
+ })
1104
+ });
1105
+ if (data === void 0 || data === "0x")
1106
+ throw new Error("empty multicall result");
1107
+ const decoded = decodeFunctionResult({
1108
+ abi: [AGGREGATE3],
1109
+ data
1110
+ });
1111
+ queries.forEach((q, i) => {
1112
+ const r = decoded[i];
1113
+ results.set(
1114
+ activeKey(q),
1115
+ r?.success === true && decodeIsActive(r.returnData)
1116
+ );
1117
+ });
1118
+ } catch {
1119
+ await Promise.all(queries.map((_, i) => callOne(i)));
858
1120
  }
859
- if (refunded.length > 0) events.push({ kind: "refunded" });
860
- return events;
1121
+ return results;
861
1122
  },
862
- async getBlockTimeMs() {
1123
+ async getLatestBlock() {
863
1124
  const block = await client.getBlock();
864
- return Number(block.timestamp) * 1e3;
865
- },
866
- watch(cb) {
867
- return client.watchBlocks({ onBlock: () => cb() });
1125
+ return {
1126
+ timeMs: Number(block.timestamp) * 1e3,
1127
+ number: block.number ?? 0n
1128
+ };
868
1129
  }
869
1130
  };
870
1131
  }
1132
+ function decodeIsActive(data) {
1133
+ if (data === void 0 || data === "0x") return false;
1134
+ return decodeFunctionResult({ abi: [IS_ACTIVE], data }) === true;
1135
+ }
1136
+ function tryDecode(log) {
1137
+ try {
1138
+ const { eventName, args } = decodeEventLog({
1139
+ abi: HTLC_EVENTS_ABI,
1140
+ data: log.data,
1141
+ topics: log.topics
1142
+ });
1143
+ return { eventName, ...args };
1144
+ } catch {
1145
+ return void 0;
1146
+ }
1147
+ }
1148
+ function toHtlcEvent(decoded, query) {
1149
+ if (decoded.eventName === "SwapCreated") {
1150
+ if (decoded.claimAddress.toLowerCase() !== query.claimAddress.toLowerCase())
1151
+ return void 0;
1152
+ return { kind: "created", amount: decoded.amount, token: decoded.token };
1153
+ }
1154
+ return decoded.eventName === "SwapRedeemed" ? { kind: "redeemed", preimage: decoded.preimage } : { kind: "refunded" };
1155
+ }
1156
+ function unique(values) {
1157
+ return [...new Set(values)];
1158
+ }
871
1159
  function createEvmRpcReader(rpcUrls) {
872
1160
  const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
873
- const transport = urls.length > 1 ? fallback(
874
- urls.map((url) => http(url)),
875
- { rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
876
- ) : http(urls[0]);
1161
+ const transport = urls.length > 1 ? fallback(urls.map((url) => http(url))) : http(urls[0]);
877
1162
  const client = createPublicClient({ transport });
878
1163
  return evmReaderFromClient(client);
879
1164
  }
@@ -931,6 +1216,10 @@ var SwapWorker = class {
931
1216
  });
932
1217
  })
933
1218
  );
1219
+ if (this.#hintSource.onReconnect)
1220
+ this.#unsubs.push(
1221
+ this.#hintSource.onReconnect(() => this.#reverifyAllTracked())
1222
+ );
934
1223
  this.#hintSource.start();
935
1224
  this.#unsubs.push(
936
1225
  this.#tracker.subscribeToActions(
@@ -948,6 +1237,17 @@ var SwapWorker = class {
948
1237
  this.#retries.clear();
949
1238
  this.#executing.clear();
950
1239
  }
1240
+ /** Chain-reconcile every tracked swap (the post-outage catch-up sweep). */
1241
+ #reverifyAllTracked() {
1242
+ for (const swapId of this.#tracker.trackedSwapIds()) {
1243
+ void this.#tracker.applyHint(swapId).catch((error) => {
1244
+ console.warn(
1245
+ `SwapWorker: post-reconnect re-verify of ${swapId} failed:`,
1246
+ error
1247
+ );
1248
+ });
1249
+ }
1250
+ }
951
1251
  #onActions(swapId, actions) {
952
1252
  const recommended = actions.recommended;
953
1253
  if (recommended === "none") {
@@ -956,7 +1256,7 @@ var SwapWorker = class {
956
1256
  return;
957
1257
  }
958
1258
  this.#hintSource.subscribe([swapId]);
959
- if (recommended && AUTO_EXECUTABLE.has(recommended)) {
1259
+ if (recommended && AUTO_EXECUTABLE.has(recommended) && this.#execute) {
960
1260
  this.#autoExecute(swapId, actions);
961
1261
  return;
962
1262
  }
@@ -969,11 +1269,13 @@ var SwapWorker = class {
969
1269
  this.#onActionRequired?.(swapId, actions);
970
1270
  }
971
1271
  #autoExecute(swapId, actions) {
1272
+ const execute = this.#execute;
1273
+ if (!execute) return;
972
1274
  if (this.#executing.has(swapId)) return;
973
1275
  const actionId = actions.recommended;
974
1276
  if (!actionId) return;
975
1277
  this.#executing.add(swapId);
976
- void this.#execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
1278
+ void execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
977
1279
  console.warn(`SwapWorker: ${actionId}(${swapId}) failed:`, error);
978
1280
  this.#scheduleRetry(swapId, actions);
979
1281
  }).finally(() => this.#executing.delete(swapId));
@@ -1038,14 +1340,17 @@ var WsStatusSource = class {
1038
1340
  /**
1039
1341
  * Ids we want but have no slot for. Promoted FIFO as slots free (the worker
1040
1342
  * unsubscribes each swap when it reaches a terminal action, so they do free).
1041
- * A pending swap is not broken, only slower: it gets no hints, so it advances
1042
- * on the tracker's chain poll instead.
1343
+ * A queued swap is not broken, only slower: it gets no hints, so it advances
1344
+ * on the tracker's at-risk reconciles and the worker's reconnect sweep.
1043
1345
  */
1044
1346
  #pending = /* @__PURE__ */ new Set();
1045
1347
  #listeners = /* @__PURE__ */ new Set();
1348
+ #reconnectListeners = /* @__PURE__ */ new Set();
1046
1349
  #ws;
1047
1350
  #open = false;
1048
1351
  #stopped = false;
1352
+ /** A connect failed or the socket dropped — the next open is a RE-connect. */
1353
+ #hadDisruption = false;
1049
1354
  #backoffMs;
1050
1355
  #reconnectTimer;
1051
1356
  constructor(options) {
@@ -1075,6 +1380,18 @@ var WsStatusSource = class {
1075
1380
  this.#listeners.add(cb);
1076
1381
  return () => this.#listeners.delete(cb);
1077
1382
  }
1383
+ /**
1384
+ * Emit whenever the socket comes (back) up after a disruption — a drop, or a
1385
+ * first connect that had to be retried. NOT emitted on a pristine first open:
1386
+ * hints were never live before it, so there is no gap to recover from. While
1387
+ * the socket was down, pushed transitions were lost for good (the server only
1388
+ * snapshots on subscribe), so a consumer should re-verify its world against
1389
+ * the chain on this signal.
1390
+ */
1391
+ onReconnect(cb) {
1392
+ this.#reconnectListeners.add(cb);
1393
+ return () => this.#reconnectListeners.delete(cb);
1394
+ }
1078
1395
  /**
1079
1396
  * Track these swap ids — subscribe on the socket now if open, else on next open.
1080
1397
  *
@@ -1138,6 +1455,10 @@ var WsStatusSource = class {
1138
1455
  this.#open = true;
1139
1456
  if (this.#active.size > 0)
1140
1457
  this.#sendFrame("subscribe", [...this.#active]);
1458
+ if (this.#hadDisruption) {
1459
+ this.#hadDisruption = false;
1460
+ for (const cb of this.#reconnectListeners) cb();
1461
+ }
1141
1462
  };
1142
1463
  ws.onmessage = (event) => this.#handleMessage(event.data);
1143
1464
  ws.onclose = () => this.#onDisconnect();
@@ -1146,6 +1467,7 @@ var WsStatusSource = class {
1146
1467
  #onDisconnect() {
1147
1468
  this.#open = false;
1148
1469
  this.#ws = void 0;
1470
+ this.#hadDisruption = true;
1149
1471
  if (this.#stopped || this.#reconnectTimer) return;
1150
1472
  const delay = this.#backoffMs;
1151
1473
  this.#backoffMs = Math.min(this.#backoffMs * 2, this.#reconnectMaxMs);
@@ -1198,6 +1520,11 @@ function defaultWebSocketFactory(url) {
1198
1520
 
1199
1521
  // src/contracts/arkade-vhtlc.ts
1200
1522
  import { VHTLC, VHTLCContractHandler } from "@arkade-os/sdk";
1523
+ import {
1524
+ ARKADE_HTLC_SCRIPT_VERSION_LEGACY,
1525
+ ARKADE_HTLC_SCRIPT_VERSION_STRICT,
1526
+ StrictVhtlcScript
1527
+ } from "@lendasat/lendaswap-sdk-pure";
1201
1528
  import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
1202
1529
  import { hex as hex5 } from "@scure/base";
1203
1530
  function strip0x(value) {
@@ -1224,7 +1551,18 @@ function buildArkadeVhtlcRef(input) {
1224
1551
  input.unilateralRefundWithoutReceiverDelay
1225
1552
  )
1226
1553
  };
1227
- const vhtlc = new VHTLC.Script(params);
1554
+ const vhtlc = (() => {
1555
+ switch (input.arkadeHtlcScriptVersion ?? ARKADE_HTLC_SCRIPT_VERSION_LEGACY) {
1556
+ case ARKADE_HTLC_SCRIPT_VERSION_LEGACY:
1557
+ return new VHTLC.Script(params);
1558
+ case ARKADE_HTLC_SCRIPT_VERSION_STRICT:
1559
+ return new StrictVhtlcScript(params);
1560
+ default:
1561
+ throw new Error(
1562
+ `Unsupported Arkade HTLC script version: ${input.arkadeHtlcScriptVersion}`
1563
+ );
1564
+ }
1565
+ })();
1228
1566
  return {
1229
1567
  ledger: "arkade",
1230
1568
  script: hex5.encode(vhtlc.pkScript),
@@ -1237,6 +1575,18 @@ function buildArkadeVhtlcRef(input) {
1237
1575
 
1238
1576
  // src/tracker/from-swap.ts
1239
1577
  var ms = (seconds2) => seconds2 * 1e3;
1578
+ var MAINNET_LOCKED_TOKEN = {
1579
+ 1: "0x18084fba666a33d37592fa2633fd49a74dd93a88",
1580
+ // tBTC
1581
+ 42161: "0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40",
1582
+ // tBTC
1583
+ 137: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6"
1584
+ // WBTC
1585
+ };
1586
+ function lockedTokenFallback(chainId, network) {
1587
+ const net = network?.toLowerCase();
1588
+ return net === "bitcoin" || net === "mainnet" ? MAINNET_LOCKED_TOKEN[chainId] : void 0;
1589
+ }
1240
1590
  var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
1241
1591
  var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
1242
1592
  function arkadeLeg(r, address, expectedSats) {
@@ -1250,10 +1600,12 @@ function arkadeLeg(r, address, expectedSats) {
1250
1600
  unilateralClaimDelay: r.unilateral_claim_delay,
1251
1601
  unilateralRefundDelay: r.unilateral_refund_delay,
1252
1602
  unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
1253
- expectedSats
1603
+ expectedSats,
1604
+ arkadeHtlcScriptVersion: r.arkade_htlc_script_version
1254
1605
  });
1255
1606
  }
1256
1607
  function evmLeg(args) {
1608
+ const createdAtMs = args.createdAt ? Date.parse(args.createdAt) : Number.NaN;
1257
1609
  return {
1258
1610
  ledger: "evm",
1259
1611
  chainId: args.chainId,
@@ -1261,15 +1613,20 @@ function evmLeg(args) {
1261
1613
  preimageHash: ensure0x(args.hashLock),
1262
1614
  claimAddress: ensure0x(args.claimAddress),
1263
1615
  expectedAmount: BigInt(args.expectedSats),
1264
- expectedToken: args.token ? ensure0x(args.token) : void 0
1616
+ expectedToken: args.token ? ensure0x(args.token) : void 0,
1617
+ sender: args.sender ? ensure0x(args.sender) : void 0,
1618
+ timelockSec: args.timelockSec,
1619
+ createdAtMs: Number.isNaN(createdAtMs) ? void 0 : createdAtMs
1265
1620
  };
1266
1621
  }
1267
- function bitcoinLeg(address, hashLock, expectedSats) {
1622
+ var CLIENT_FUNDING_MIN_CONFIRMATIONS = 1;
1623
+ function bitcoinLeg(address, hashLock, expectedSats, minConfirmations) {
1268
1624
  return {
1269
1625
  ledger: "bitcoin",
1270
1626
  address,
1271
1627
  preimageHash: strip0x2(hashLock),
1272
- expectedSats
1628
+ expectedSats,
1629
+ minConfirmations
1273
1630
  };
1274
1631
  }
1275
1632
  function swapToTracked(stored) {
@@ -1287,7 +1644,11 @@ function swapToTracked(stored) {
1287
1644
  claimAddress: r.client_evm_address,
1288
1645
  // the client claims the server's EVM HTLC
1289
1646
  expectedSats: r.evm_expected_sats,
1290
- token: r.wbtc_address
1647
+ token: r.wbtc_address,
1648
+ sender: r.server_evm_address,
1649
+ // the server funded it
1650
+ timelockSec: r.evm_refund_locktime,
1651
+ createdAt: r.created_at
1291
1652
  }),
1292
1653
  clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1293
1654
  serverRefundLocktime: ms(r.evm_refund_locktime)
@@ -1302,8 +1663,14 @@ function swapToTracked(stored) {
1302
1663
  hashLock: r.hash_lock,
1303
1664
  claimAddress: r.server_evm_address,
1304
1665
  // the server claims the client's EVM HTLC
1305
- expectedSats: r.evm_expected_sats
1306
- // evm_to_arkade doesn't expose the locked token — verify amount only.
1666
+ expectedSats: r.evm_expected_sats,
1667
+ // evm_to_arkade doesn't expose the locked token — fall back to the
1668
+ // per-chain mainnet constant so the isActive tuple is complete.
1669
+ token: lockedTokenFallback(r.evm_chain_id, r.network),
1670
+ sender: r.client_evm_address,
1671
+ // the client funded it
1672
+ timelockSec: r.evm_refund_locktime,
1673
+ createdAt: r.created_at
1307
1674
  }),
1308
1675
  serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
1309
1676
  clientRefundLocktime: ms(r.evm_refund_locktime),
@@ -1316,7 +1683,8 @@ function swapToTracked(stored) {
1316
1683
  clientHtlc: bitcoinLeg(
1317
1684
  r.btc_htlc_address,
1318
1685
  r.evm_hash_lock,
1319
- Number(r.source_amount)
1686
+ Number(r.source_amount),
1687
+ CLIENT_FUNDING_MIN_CONFIRMATIONS
1320
1688
  ),
1321
1689
  serverHtlc: evmLeg({
1322
1690
  chainId: r.evm_chain_id,
@@ -1325,7 +1693,11 @@ function swapToTracked(stored) {
1325
1693
  claimAddress: r.client_evm_address,
1326
1694
  // the client claims the server's EVM HTLC
1327
1695
  expectedSats: r.evm_expected_sats,
1328
- token: r.wbtc_address
1696
+ token: r.wbtc_address,
1697
+ sender: r.server_evm_address,
1698
+ // the server funded it
1699
+ timelockSec: r.evm_refund_locktime,
1700
+ createdAt: r.created_at
1329
1701
  }),
1330
1702
  clientRefundLocktime: ms(r.btc_refund_locktime),
1331
1703
  serverRefundLocktime: ms(r.evm_refund_locktime)
@@ -1341,7 +1713,11 @@ function swapToTracked(stored) {
1341
1713
  claimAddress: r.server_evm_address,
1342
1714
  // the server claims the client's EVM HTLC
1343
1715
  expectedSats: r.evm_expected_sats,
1344
- token: r.wbtc_address
1716
+ token: r.wbtc_address,
1717
+ sender: r.client_evm_address,
1718
+ // the client funded it
1719
+ timelockSec: r.evm_refund_locktime,
1720
+ createdAt: r.created_at
1345
1721
  }),
1346
1722
  serverHtlc: bitcoinLeg(
1347
1723
  r.btc_htlc_address,
@@ -1361,7 +1737,8 @@ function swapToTracked(stored) {
1361
1737
  clientHtlc: bitcoinLeg(
1362
1738
  r.btc_htlc_address,
1363
1739
  r.hash_lock,
1364
- Number(r.source_amount)
1740
+ Number(r.source_amount),
1741
+ CLIENT_FUNDING_MIN_CONFIRMATIONS
1365
1742
  ),
1366
1743
  serverHtlc: buildArkadeVhtlcRef({
1367
1744
  senderPk: r.server_vhtlc_pk,
@@ -1375,7 +1752,8 @@ function swapToTracked(stored) {
1375
1752
  unilateralClaimDelay: r.unilateral_claim_delay,
1376
1753
  unilateralRefundDelay: r.unilateral_refund_delay,
1377
1754
  unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
1378
- expectedSats: Number(r.target_amount)
1755
+ expectedSats: Number(r.target_amount),
1756
+ arkadeHtlcScriptVersion: r.arkade_htlc_script_version
1379
1757
  }),
1380
1758
  clientRefundLocktime: ms(r.btc_refund_locktime),
1381
1759
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
@@ -1409,8 +1787,14 @@ function swapToTracked(stored) {
1409
1787
  hashLock: r.hash_lock,
1410
1788
  claimAddress: r.server_evm_address,
1411
1789
  // the server claims the client's leg
1412
- expectedSats: r.evm_expected_sats
1413
- // evm_to_lightning doesn't expose the locked token — verify amount only.
1790
+ expectedSats: r.evm_expected_sats,
1791
+ // evm_to_lightning doesn't expose the locked token — fall back to the
1792
+ // per-chain mainnet constant so the isActive tuple is complete.
1793
+ token: lockedTokenFallback(r.evm_chain_id, r.network),
1794
+ sender: r.client_evm_address,
1795
+ // the client funded it
1796
+ timelockSec: r.evm_refund_locktime,
1797
+ createdAt: r.created_at
1414
1798
  }),
1415
1799
  clientRefundLocktime: ms(r.evm_refund_locktime),
1416
1800
  serverRefundLocktime: 0
@@ -1443,7 +1827,11 @@ function swapToTracked(stored) {
1443
1827
  claimAddress: r.client_evm_address,
1444
1828
  // the client claims the server's leg
1445
1829
  expectedSats: r.evm_expected_sats,
1446
- token: r.wbtc_address
1830
+ token: r.wbtc_address,
1831
+ sender: r.server_evm_address,
1832
+ // the server funded it
1833
+ timelockSec: r.evm_refund_locktime,
1834
+ createdAt: r.created_at
1447
1835
  }),
1448
1836
  clientRefundLocktime: 0,
1449
1837
  // no on-chain client leg
@@ -1462,11 +1850,14 @@ var SwapTracker = class {
1462
1850
  #lastActions = /* @__PURE__ */ new Map();
1463
1851
  #subscribers = /* @__PURE__ */ new Set();
1464
1852
  #refreshIntervalMs;
1853
+ #atRiskReconcileIntervalMs;
1854
+ #lastAtRiskReconcileAt = 0;
1465
1855
  #eventUnsubs = [];
1466
1856
  #timer;
1467
1857
  constructor(managers, options) {
1468
1858
  this.#managers = managers;
1469
1859
  this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
1860
+ this.#atRiskReconcileIntervalMs = options?.atRiskReconcileIntervalMs ?? 6e4;
1470
1861
  }
1471
1862
  /**
1472
1863
  * Whether every on-chain leg of `swap` is observable — i.e. the tracker can
@@ -1496,6 +1887,7 @@ var SwapTracker = class {
1496
1887
  }
1497
1888
  await this.#refreshManagers();
1498
1889
  this.#recomputeAll();
1890
+ this.#lastAtRiskReconcileAt = Date.now();
1499
1891
  if (this.#refreshIntervalMs > 0)
1500
1892
  this.#timer = setInterval(
1501
1893
  () => void this.#tick(),
@@ -1535,25 +1927,73 @@ var SwapTracker = class {
1535
1927
  await Promise.all([...managers].map((manager) => manager.refresh()));
1536
1928
  this.#recompute(swap);
1537
1929
  }
1930
+ /**
1931
+ * The periodic tick: recompute every swap from extrapolated clocks (pure
1932
+ * local work), and — at most every `atRiskReconcileIntervalMs` — re-read the
1933
+ * chain for at-risk swaps. This is the trust boundary: the happy path rides
1934
+ * server hints, but once the client's funds sit in an HTLC, refund
1935
+ * availability (and the counterparty leg's state) is verified against the
1936
+ * chain on a cadence the server can't influence.
1937
+ */
1538
1938
  async #tick() {
1539
- await this.#refreshManagers();
1939
+ const now = Date.now();
1940
+ if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs) {
1941
+ this.#lastAtRiskReconcileAt = now;
1942
+ const legs = [...this.#swaps.values()].filter((swap) => this.#isAtRisk(swap)).flatMap((swap) => legsOf(swap));
1943
+ await Promise.all(
1944
+ legs.map(
1945
+ (leg) => this.#managerFor(leg).reconcile(leg).catch((error) => {
1946
+ console.warn("SwapTracker: at-risk reconcile failed:", error);
1947
+ })
1948
+ )
1949
+ );
1950
+ }
1540
1951
  this.#recomputeAll();
1541
1952
  }
1542
1953
  /**
1543
- * Re-poll the managers that have a registered leg — seeding/advancing their
1544
- * clocks and reconciling observations. Managers with nothing tracked are
1545
- * skipped: refreshing them still hits their clock source (e.g. Arkade/Bitcoin
1546
- * MTP via the API), so a swap that doesn't touch a ledger can't fail on that
1954
+ * Whether a swap needs chain-verified monitoring independent of the server:
1955
+ * its client leg holds (or held) funds on-chain, so knowing when a refund
1956
+ * unlocks — or that the counterparty resolved a leg — must not depend on a
1957
+ * hint arriving. An unfunded client leg (`absent`) is server-hint territory;
1958
+ * an unseeded one (`undefined`, e.g. the prime failed on a flaky RPC) counts
1959
+ * as at-risk so the tick heals it. A swap with no on-chain client leg
1960
+ * (paying via Lightning) is at-risk while tracked: once the invoice is paid
1961
+ * there is no on-chain trace of the client's exposure, and the server leg is
1962
+ * the only thing left to watch.
1963
+ */
1964
+ #isAtRisk(swap) {
1965
+ if (!swap.clientHtlc) return true;
1966
+ const obs = this.#managerFor(swap.clientHtlc).getState(swap.clientHtlc);
1967
+ return obs !== "absent";
1968
+ }
1969
+ /**
1970
+ * Refresh the managers that have a registered leg — seeding their clocks and
1971
+ * reconciling observations (the on-demand full read backing startTracking's
1972
+ * prime and track()'s seed). Managers with nothing tracked are skipped:
1973
+ * refreshing them still hits their clock source (e.g. Arkade/Bitcoin MTP via
1974
+ * the API), so a swap that doesn't touch a ledger can't fail on that
1547
1975
  * ledger's endpoint being down (the default client builds every manager).
1548
1976
  */
1549
1977
  async #refreshManagers() {
1550
1978
  const active = /* @__PURE__ */ new Set();
1551
1979
  for (const swap of this.#swaps.values())
1552
1980
  for (const leg of legsOf(swap)) active.add(leg.ledger);
1553
- const managers = new Set(
1554
- [...active].map((ledger) => this.#managers.get(ledger)).filter((m) => m !== void 0)
1981
+ const byManager = /* @__PURE__ */ new Map();
1982
+ for (const ledger of active) {
1983
+ const manager = this.#managers.get(ledger);
1984
+ if (manager && !byManager.has(manager)) byManager.set(manager, ledger);
1985
+ }
1986
+ const ledgers = [...byManager.values()];
1987
+ const results = await Promise.allSettled(
1988
+ [...byManager.keys()].map((manager) => manager.refresh())
1555
1989
  );
1556
- await Promise.all([...managers].map((manager) => manager.refresh()));
1990
+ results.forEach((result, index) => {
1991
+ if (result.status === "rejected")
1992
+ console.warn(
1993
+ `SwapTracker: refreshing ${ledgers[index]} failed:`,
1994
+ result.reason
1995
+ );
1996
+ });
1557
1997
  }
1558
1998
  /** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
1559
1999
  trackedSwapIds() {
@@ -1569,9 +2009,17 @@ var SwapTracker = class {
1569
2009
  async applyHint(swapId, opts) {
1570
2010
  const swap = this.#swaps.get(swapId);
1571
2011
  if (!swap) return;
1572
- await Promise.all(
1573
- legsOf(swap).map((leg) => this.#managerFor(leg).reconcile(leg))
2012
+ const legs = legsOf(swap);
2013
+ const results = await Promise.allSettled(
2014
+ legs.map((leg) => this.#managerFor(leg).reconcile(leg))
1574
2015
  );
2016
+ results.forEach((result, index) => {
2017
+ if (result.status === "rejected")
2018
+ console.warn(
2019
+ `SwapTracker: hint reconcile failed for ${legs[index].ledger}:`,
2020
+ result.reason
2021
+ );
2022
+ });
1575
2023
  this.#recompute(swap, opts?.force ?? false);
1576
2024
  }
1577
2025
  /** Notify `cb` of the current action for each tracked swap, then on every change. */
@@ -1629,15 +2077,21 @@ var SwapTracker = class {
1629
2077
  }
1630
2078
  const status = deriveSwapStatus({ clientHtlc, serverHtlc });
1631
2079
  if (status === void 0) return;
1632
- const actions = deriveSwapActions({
1633
- status,
1634
- clientChainNow,
1635
- serverChainNow,
1636
- clientRefundLocktime: swap.clientRefundLocktime,
1637
- serverRefundLocktime: swap.serverRefundLocktime,
1638
- // Pay-on-Lightning swaps have no client-funded on-chain leg.
1639
- clientFunds: swap.clientHtlc !== void 0
1640
- });
2080
+ const actions = {
2081
+ ...deriveSwapActions({
2082
+ status,
2083
+ clientChainNow,
2084
+ serverChainNow,
2085
+ clientRefundLocktime: swap.clientRefundLocktime,
2086
+ serverRefundLocktime: swap.serverRefundLocktime,
2087
+ // Pay-on-Lightning swaps have no client-funded on-chain leg.
2088
+ clientFunds: swap.clientHtlc !== void 0
2089
+ }),
2090
+ // Expose the raw observations so consumers can render progress from
2091
+ // chain facts (which leg is funded/spent) even when the server is
2092
+ // stale/unreachable — without interpreting the SwapStatus enum.
2093
+ observations: { clientHtlc, serverHtlc }
2094
+ };
1641
2095
  const previous = this.#lastActions.get(swap.swapId);
1642
2096
  if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
1643
2097
  return;
@@ -1668,7 +2122,9 @@ var Client = class {
1668
2122
  #tracker;
1669
2123
  /** Hint-driven auto-claim worker; only built when `withAutoClaim` opted in. */
1670
2124
  #worker;
1671
- #started = false;
2125
+ #startPromise;
2126
+ /** Swap ids whose settled status was already re-fetched this session. */
2127
+ #settledSyncDone = /* @__PURE__ */ new Set();
1672
2128
  constructor(...args) {
1673
2129
  if (args[0] instanceof LegacyClient) {
1674
2130
  this.#legacy = args[0];
@@ -2002,33 +2458,69 @@ var Client = class {
2002
2458
  * Subscribe with {@link subscribeToActions}; release with
2003
2459
  * {@link stopTracking}.
2004
2460
  */
2005
- async startTracking() {
2461
+ startTracking() {
2006
2462
  if (!this.#tracking.enabled)
2007
- throw new Error(
2008
- "tracking is disabled \u2014 remove .withoutTracking() to enable it"
2463
+ return Promise.reject(
2464
+ new Error(
2465
+ "tracking is disabled \u2014 remove .withoutTracking() to enable it"
2466
+ )
2009
2467
  );
2010
- if (this.#started) return;
2011
- this.#started = true;
2468
+ this.#startPromise ??= this.#doStartTracking().catch((error) => {
2469
+ this.#startPromise = void 0;
2470
+ throw error;
2471
+ });
2472
+ return this.#startPromise;
2473
+ }
2474
+ async #doStartTracking() {
2012
2475
  try {
2013
2476
  const tracker = new SwapTracker(await this.#ensureManagers(), {
2014
2477
  refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
2015
2478
  });
2016
2479
  this.#tracker = tracker;
2017
2480
  await tracker.startTracking(await this.#loadTrackedSwaps());
2481
+ tracker.subscribeToActions(
2482
+ (swapId, actions) => this.#syncSettledStatus(swapId, actions)
2483
+ );
2018
2484
  this.#startWorker(tracker);
2019
2485
  } catch (error) {
2020
2486
  this.#worker?.stop();
2021
2487
  this.#worker = void 0;
2022
2488
  this.#tracker?.stop();
2023
2489
  this.#tracker = void 0;
2024
- this.#started = false;
2025
2490
  throw error;
2026
2491
  }
2027
2492
  }
2493
+ /**
2494
+ * The chain says this swap is finished (`none`) — if the STORED status still
2495
+ * reads active, re-fetch and persist it so the next session's
2496
+ * {@link #loadTrackedSwaps} settled-filter catches it (instead of tracking
2497
+ * and scanning the swap once per session forever). Fire-and-forget with a
2498
+ * per-session dedupe: a failed fetch just means one more one-scan round next
2499
+ * session, which is exactly the current behavior. A swap whose stored status
2500
+ * is already settled costs nothing (no server call).
2501
+ */
2502
+ #syncSettledStatus(swapId, actions) {
2503
+ if (actions.recommended !== "none") return;
2504
+ if (this.#settledSyncDone.has(swapId)) return;
2505
+ this.#settledSyncDone.add(swapId);
2506
+ void (async () => {
2507
+ const stored = (await this.listAllSwaps()).find(
2508
+ (s) => s.response.id === swapId
2509
+ );
2510
+ if (!stored || SETTLED_STORED_STATUSES.has(stored.response.status))
2511
+ return;
2512
+ await this.getSwap(swapId, { updateStorage: true });
2513
+ })().catch((error) => {
2514
+ console.warn(
2515
+ `Client: refreshing settled status of swap ${swapId} failed:`,
2516
+ error
2517
+ );
2518
+ });
2519
+ }
2028
2520
  /** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
2029
2521
  async #loadTrackedSwaps() {
2030
2522
  const swaps = await this.listAllSwaps();
2031
- return swaps.map(swapToTracked).filter((s) => s !== void 0);
2523
+ return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
2032
2524
  }
2033
2525
  /**
2034
2526
  * Run a create, then fold the new swap into tracking.
@@ -2090,27 +2582,29 @@ var Client = class {
2090
2582
  }
2091
2583
  }
2092
2584
  /**
2093
- * If auto-claim was opted in, wire the server status WebSocket into a
2094
- * {@link SwapWorker}: hints trigger a chain re-verify, and a swap the chain
2095
- * confirms claimable is auto-claimed; `fund`/refund actions are surfaced via the
2096
- * configured `onActionRequired`. A no-op unless `withAutoClaim` was set and a
2097
- * base URL is configured (the WebSocket needs a server to reach).
2585
+ * Wire the server status WebSocket into a {@link SwapWorker} whenever a server
2586
+ * URL is configured: pushed status transitions trigger a targeted chain
2587
+ * re-verify (`applyHint`), which is what lets the chain pollers stay slow.
2588
+ * Auto-EXECUTION on top of that is opt-in: only with `withAutoClaim` does the
2589
+ * worker get an `execute` (claims) and an `onActionRequired` surface —
2590
+ * otherwise it runs observe-only, hints in, no spends.
2098
2591
  */
2099
2592
  #startWorker(tracker) {
2593
+ const serverUrl = this.#tracking.serverUrl;
2594
+ if (!serverUrl) return;
2100
2595
  const autoClaim = this.#tracking.autoClaim;
2101
- if (!autoClaim) return;
2102
- if (!this.baseUrl) return;
2103
- const hintSource = new WsStatusSource({ serverUrl: this.baseUrl });
2596
+ const hintSource = new WsStatusSource({ serverUrl });
2104
2597
  const worker = new SwapWorker({
2105
2598
  tracker,
2106
2599
  hintSource,
2107
- execute: async (swapId, actionId) => {
2600
+ execute: autoClaim ? async (swapId, actionId) => {
2108
2601
  if (actionId !== "claim")
2109
2602
  throw new Error(`refusing to auto-run action '${actionId}'`);
2110
2603
  const result = await this.claim(swapId);
2111
- if (!result.success) throw new Error(result.message ?? "claim failed");
2112
- },
2113
- onActionRequired: autoClaim.onActionRequired
2604
+ if (!result.success)
2605
+ throw new Error(result.message ?? "claim failed");
2606
+ } : void 0,
2607
+ onActionRequired: autoClaim?.onActionRequired
2114
2608
  });
2115
2609
  this.#worker = worker;
2116
2610
  worker.start();
@@ -2131,7 +2625,7 @@ var Client = class {
2131
2625
  this.#worker = void 0;
2132
2626
  this.#tracker?.stop();
2133
2627
  this.#tracker = void 0;
2134
- this.#started = false;
2628
+ this.#startPromise = void 0;
2135
2629
  }
2136
2630
  /**
2137
2631
  * Resolve the per-ledger managers once — the explicit override if given, else
@@ -2146,7 +2640,7 @@ var Client = class {
2146
2640
  return this.#managers;
2147
2641
  }
2148
2642
  const managers = /* @__PURE__ */ new Map();
2149
- const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
2643
+ const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
2150
2644
  const chainTime = async () => (await this.getMtp()).mtp * 1e3;
2151
2645
  const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
2152
2646
  if (arkadeUrl) {
@@ -2162,7 +2656,8 @@ var Client = class {
2162
2656
  "bitcoin",
2163
2657
  await BitcoinContractManager.create({
2164
2658
  esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
2165
- chainTime
2659
+ chainTime,
2660
+ minConfirmations: bitcoinMinConfirmations
2166
2661
  })
2167
2662
  );
2168
2663
  this.#managers = managers;
@@ -2174,8 +2669,10 @@ var ClientBuilder = class {
2174
2669
  // Observe-mode tracking is on by default; the built client auto-builds its
2175
2670
  // managers from config unless disabled or given an explicit override.
2176
2671
  #trackingEnabled = true;
2672
+ #serverUrl;
2177
2673
  #arkadeServerUrl;
2178
2674
  #esploraUrl;
2675
+ #bitcoinMinConfirmations;
2179
2676
  #evmRpcUrls;
2180
2677
  #managers;
2181
2678
  #autoClaim;
@@ -2216,6 +2713,7 @@ var ClientBuilder = class {
2216
2713
  }
2217
2714
  withBaseUrl(...args) {
2218
2715
  this.#inner.withBaseUrl(...args);
2716
+ this.#serverUrl = args[0];
2219
2717
  return this;
2220
2718
  }
2221
2719
  withReferralCode(...args) {
@@ -2239,6 +2737,18 @@ var ClientBuilder = class {
2239
2737
  this.#esploraUrl = url;
2240
2738
  return this;
2241
2739
  }
2740
+ /**
2741
+ * Confirmations a Bitcoin funding tx needs before it observes as `confirmed`
2742
+ * (gating e.g. the evm→bitcoin claim). Default `0`: claim as soon as the
2743
+ * funding hits the mempool, which TRUSTS the funder not to double-spend it
2744
+ * (claiming publishes the preimage, so a funder that then replaced its
2745
+ * funding could take both legs). Set `1` (or more) for a block-depth policy
2746
+ * that doesn't rely on the funder's good behaviour.
2747
+ */
2748
+ withBitcoinMinConfirmations(minConfirmations) {
2749
+ this.#bitcoinMinConfirmations = minConfirmations;
2750
+ return this;
2751
+ }
2242
2752
  /**
2243
2753
  * Override the Ark server URL. Optional for tracking — it otherwise defaults to
2244
2754
  * the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
@@ -2279,14 +2789,24 @@ var ClientBuilder = class {
2279
2789
  async build() {
2280
2790
  return new Client(await this.#inner.build(), {
2281
2791
  enabled: this.#trackingEnabled,
2792
+ serverUrl: this.#serverUrl,
2282
2793
  managers: this.#managers,
2283
2794
  arkadeServerUrl: this.#arkadeServerUrl,
2284
2795
  esploraUrl: this.#esploraUrl,
2796
+ bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
2285
2797
  evmRpcUrls: this.#evmRpcUrls,
2286
2798
  autoClaim: this.#autoClaim
2287
2799
  });
2288
2800
  }
2289
2801
  };
2802
+ var SETTLED_STORED_STATUSES = /* @__PURE__ */ new Set([
2803
+ "serverredeemed",
2804
+ "clientrefunded",
2805
+ "clientrefundedserverfunded",
2806
+ "clientrefundedserverrefunded",
2807
+ "clientredeemedandclientrefunded",
2808
+ "expired"
2809
+ ]);
2290
2810
  function createdSwapId(result) {
2291
2811
  const id = result?.response?.id;
2292
2812
  return typeof id === "string" ? id : void 0;
@@ -2312,6 +2832,7 @@ export {
2312
2832
  evmReaderFromClient,
2313
2833
  htlcFactsFromEsploraTxs,
2314
2834
  htlcKey,
2835
+ htlcQueryKey,
2315
2836
  swapToTracked
2316
2837
  };
2317
2838
  //# sourceMappingURL=index.js.map