@satora/swap 1.1.0 → 1.2.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.cjs CHANGED
@@ -292,6 +292,7 @@ __export(src_exports, {
292
292
  DEFAULT_ESPLORA_URLS: () => DEFAULT_ESPLORA_URLS,
293
293
  DEFAULT_EVM_RPCS: () => DEFAULT_EVM_RPCS,
294
294
  EvmContractManager: () => EvmContractManager,
295
+ HintTracker: () => HintTracker,
295
296
  SwapTracker: () => SwapTracker,
296
297
  SwapWorker: () => SwapWorker,
297
298
  WsStatusSource: () => WsStatusSource,
@@ -1504,6 +1505,113 @@ function defaultEvmReaders(overrides) {
1504
1505
  return readers;
1505
1506
  }
1506
1507
 
1508
+ // src/hints/hint-tracker.ts
1509
+ var HintTracker = class {
1510
+ #fetchStatus;
1511
+ #refreshIntervalMs;
1512
+ #swaps = /* @__PURE__ */ new Map();
1513
+ /** Last server-reported status per swap (seeded from storage, updated by hints). */
1514
+ #statuses = /* @__PURE__ */ new Map();
1515
+ /** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
1516
+ #lastActions = /* @__PURE__ */ new Map();
1517
+ #subscribers = /* @__PURE__ */ new Set();
1518
+ #timer;
1519
+ constructor(options) {
1520
+ this.#fetchStatus = options.fetchStatus;
1521
+ this.#refreshIntervalMs = options.refreshIntervalMs ?? 5e3;
1522
+ }
1523
+ /**
1524
+ * Always `true`: with no chain readers there is no reachability constraint.
1525
+ * (Claim/refund execution has its own chain configuration, independent of
1526
+ * tracking.)
1527
+ */
1528
+ canObserve(_swap) {
1529
+ return true;
1530
+ }
1531
+ /** Register the swaps, derive each one's first action from its stored status. */
1532
+ async startTracking(swaps) {
1533
+ for (const swap of swaps) this.#admit(swap);
1534
+ this.#recomputeAll();
1535
+ if (this.#refreshIntervalMs > 0)
1536
+ this.#timer = setInterval(
1537
+ () => this.#recomputeAll(),
1538
+ this.#refreshIntervalMs
1539
+ );
1540
+ }
1541
+ /**
1542
+ * Track one more swap after {@link startTracking}. Idempotent: a swap already
1543
+ * tracked, or one already seen through to a terminal action, is ignored.
1544
+ */
1545
+ async track(swap) {
1546
+ if (this.#swaps.has(swap.swapId) || this.#lastActions.has(swap.swapId))
1547
+ return;
1548
+ this.#admit(swap);
1549
+ this.#recompute(swap);
1550
+ }
1551
+ #admit(swap) {
1552
+ this.#swaps.set(swap.swapId, swap);
1553
+ if (swap.storedStatus !== void 0 && !this.#statuses.has(swap.swapId))
1554
+ this.#statuses.set(swap.swapId, swap.storedStatus);
1555
+ }
1556
+ /** The swap ids currently tracked — e.g. to subscribe them to the hint feed. */
1557
+ trackedSwapIds() {
1558
+ return [...this.#swaps.keys()];
1559
+ }
1560
+ /**
1561
+ * Apply a server status to one swap and recompute its action. A hint that
1562
+ * carries the pushed `status` is applied as-is; without one (the worker's
1563
+ * retry and post-reconnect paths) the status is re-fetched from the server.
1564
+ * A no-op for an untracked swap.
1565
+ */
1566
+ async applyHint(swapId, opts) {
1567
+ const swap = this.#swaps.get(swapId);
1568
+ if (!swap) return;
1569
+ const status = opts?.status ?? await this.#fetchStatus(swapId);
1570
+ if (!this.#swaps.has(swapId)) return;
1571
+ this.#statuses.set(swapId, status);
1572
+ this.#recompute(swap, opts?.force ?? false);
1573
+ }
1574
+ /** Notify `cb` of the current action for each tracked swap, then on every change. */
1575
+ subscribeToActions(cb) {
1576
+ this.#subscribers.add(cb);
1577
+ for (const [swapId, actions] of this.#lastActions) cb(swapId, actions);
1578
+ return () => this.#subscribers.delete(cb);
1579
+ }
1580
+ /** Stop tracking: drop the tick timer, tracked swaps, and subscribers. */
1581
+ stop() {
1582
+ if (this.#timer) clearInterval(this.#timer);
1583
+ this.#timer = void 0;
1584
+ this.#swaps.clear();
1585
+ this.#subscribers.clear();
1586
+ }
1587
+ #recomputeAll() {
1588
+ for (const swap of this.#swaps.values()) this.#recompute(swap);
1589
+ }
1590
+ #recompute(swap, force = false) {
1591
+ const status = this.#statuses.get(swap.swapId);
1592
+ if (status === void 0) return;
1593
+ const now = Date.now();
1594
+ const actions = deriveSwapActions({
1595
+ status,
1596
+ clientChainNow: now,
1597
+ serverChainNow: now,
1598
+ clientRefundLocktime: swap.clientRefundLocktime,
1599
+ serverRefundLocktime: swap.serverRefundLocktime,
1600
+ // Pay-on-Lightning swaps have no client-funded on-chain leg.
1601
+ clientFunds: swap.clientHtlc !== void 0
1602
+ });
1603
+ const previous = this.#lastActions.get(swap.swapId);
1604
+ if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
1605
+ return;
1606
+ this.#lastActions.set(swap.swapId, actions);
1607
+ for (const cb of this.#subscribers) cb(swap.swapId, actions);
1608
+ if (actions.recommended === "none") {
1609
+ this.#swaps.delete(swap.swapId);
1610
+ this.#statuses.delete(swap.swapId);
1611
+ }
1612
+ }
1613
+ };
1614
+
1507
1615
  // src/hints/swap-worker.ts
1508
1616
  var AUTO_EXECUTABLE = /* @__PURE__ */ new Set([
1509
1617
  "claim"
@@ -1537,8 +1645,8 @@ var SwapWorker = class {
1537
1645
  this.#started = true;
1538
1646
  this.#hintSource.subscribe(this.#tracker.trackedSwapIds());
1539
1647
  this.#unsubs.push(
1540
- this.#hintSource.onStatus(({ swapId }) => {
1541
- void this.#tracker.applyHint(swapId).catch((error) => {
1648
+ this.#hintSource.onStatus(({ swapId, status }) => {
1649
+ void this.#tracker.applyHint(swapId, { status }).catch((error) => {
1542
1650
  console.warn(`SwapWorker: applyHint(${swapId}) failed:`, error);
1543
1651
  });
1544
1652
  })
@@ -2700,8 +2808,11 @@ var Client = class {
2700
2808
  /**
2701
2809
  * Start observing the user's active swaps and deriving each one's next action.
2702
2810
  *
2703
- * Loads stored swaps, maps the ones whose ledgers are observable to
2704
- * {@link TrackedSwap}s, and runs the per-ledger monitors. Idempotent. Requires
2811
+ * Loads stored swaps, maps them to {@link TrackedSwap}s, and derives each
2812
+ * one's actions — by default from the server's status hints alone (TEMP while
2813
+ * the chain monitors are buggy; see
2814
+ * {@link ClientBuilder.withChainVerifiedTracking} to re-enable the per-ledger
2815
+ * chain monitors). Idempotent. Requires
2705
2816
  * the client to have been built with {@link ClientBuilder.withTracking}.
2706
2817
  * Subscribe with {@link subscribeToActions}; release with
2707
2818
  * {@link stopTracking}.
@@ -2721,7 +2832,11 @@ var Client = class {
2721
2832
  }
2722
2833
  async #doStartTracking() {
2723
2834
  try {
2724
- const tracker = new SwapTracker(await this.#ensureManagers(), {
2835
+ const chainVerified = this.#tracking.chainVerified || this.#tracking.managers !== void 0;
2836
+ const tracker = chainVerified ? new SwapTracker(await this.#ensureManagers(), {
2837
+ refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
2838
+ }) : new HintTracker({
2839
+ fetchStatus: async (swapId) => (await this.getSwap(swapId, { updateStorage: true })).status,
2725
2840
  refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
2726
2841
  });
2727
2842
  this.#tracker = tracker;
@@ -2770,7 +2885,9 @@ var Client = class {
2770
2885
  const swaps = await this.listAllSwaps();
2771
2886
  return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map((s) => {
2772
2887
  try {
2773
- return swapToTracked(s);
2888
+ const tracked = swapToTracked(s);
2889
+ if (!tracked) return void 0;
2890
+ return { ...tracked, storedStatus: s.response.status };
2774
2891
  } catch (error) {
2775
2892
  console.warn(
2776
2893
  `Client: skipping untrackable stored swap ${s.response.id}:`,
@@ -2848,7 +2965,7 @@ var Client = class {
2848
2965
  * otherwise it runs observe-only, hints in, no spends.
2849
2966
  */
2850
2967
  #startWorker(tracker) {
2851
- const serverUrl = this.#tracking.serverUrl;
2968
+ const serverUrl = this.#tracking.serverUrl ?? legacyBaseUrl(this.#legacy);
2852
2969
  if (!serverUrl) return;
2853
2970
  const autoClaim = this.#tracking.autoClaim;
2854
2971
  const hintSource = new WsStatusSource({ serverUrl });
@@ -2946,6 +3063,7 @@ var ClientBuilder = class {
2946
3063
  #evmRpcUrls;
2947
3064
  #managers;
2948
3065
  #autoClaim;
3066
+ #chainVerified = false;
2949
3067
  /**
2950
3068
  * Override the EVM JSON-RPC endpoint per chainId. Optional — tracking uses
2951
3069
  * tested public defaults otherwise; an override is tried first, with the
@@ -2972,6 +3090,16 @@ var ClientBuilder = class {
2972
3090
  this.#trackingEnabled = false;
2973
3091
  return this;
2974
3092
  }
3093
+ /**
3094
+ * Verify swap state against the chain (per-ledger contract managers) instead
3095
+ * of trusting the server's status hints. TEMPORARILY off by default while the
3096
+ * chain monitors are being fixed — without it, tracking derives actions from
3097
+ * the server's pushed status alone, with zero chain access.
3098
+ */
3099
+ withChainVerifiedTracking() {
3100
+ this.#chainVerified = true;
3101
+ return this;
3102
+ }
2975
3103
  /**
2976
3104
  * Advanced: supply per-ledger {@link ContractManager}s explicitly instead of
2977
3105
  * letting the client auto-build them from config (useful for tests or custom
@@ -3089,10 +3217,18 @@ var ClientBuilder = class {
3089
3217
  bitcoinNetwork: this.#bitcoinNetwork,
3090
3218
  bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
3091
3219
  evmRpcUrls: this.#evmRpcUrls,
3092
- autoClaim: this.#autoClaim
3220
+ autoClaim: this.#autoClaim,
3221
+ chainVerified: this.#chainVerified
3093
3222
  });
3094
3223
  }
3095
3224
  };
3225
+ function legacyBaseUrl(legacy) {
3226
+ try {
3227
+ return legacy.baseUrl;
3228
+ } catch {
3229
+ return void 0;
3230
+ }
3231
+ }
3096
3232
  var SETTLED_STORED_STATUSES = /* @__PURE__ */ new Set([
3097
3233
  "serverredeemed",
3098
3234
  "clientrefunded",
@@ -3118,6 +3254,7 @@ init_bitcoin_reader_esplora();
3118
3254
  DEFAULT_ESPLORA_URLS,
3119
3255
  DEFAULT_EVM_RPCS,
3120
3256
  EvmContractManager,
3257
+ HintTracker,
3121
3258
  SwapTracker,
3122
3259
  SwapWorker,
3123
3260
  WsStatusSource,