@satora/swap 1.1.0 → 1.2.1
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 +145 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -4
- package/dist/index.d.ts +117 -4
- package/dist/index.js +144 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
2704
|
-
*
|
|
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
|
|
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
|
-
|
|
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,
|