@satora/swap 0.1.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/README.md +4 -0
- package/dist/{bitcoin-reader-esplora-GRUP7RAG.js → bitcoin-reader-esplora-HV6CANAA.js} +2 -2
- package/dist/{chunk-J35RZBDW.js → chunk-EIWELJJK.js} +25 -9
- package/dist/chunk-EIWELJJK.js.map +1 -0
- package/dist/index.cjs +1277 -202
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +520 -85
- package/dist/index.d.ts +520 -85
- package/dist/index.js +1258 -193
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-J35RZBDW.js.map +0 -1
- /package/dist/{bitcoin-reader-esplora-GRUP7RAG.js.map → bitcoin-reader-esplora-HV6CANAA.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,12 +2,13 @@ import {
|
|
|
2
2
|
DEFAULT_ESPLORA_URLS,
|
|
3
3
|
esploraReader,
|
|
4
4
|
htlcFactsFromEsploraTxs
|
|
5
|
-
} from "./chunk-
|
|
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;
|
|
@@ -374,13 +398,15 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
374
398
|
chainTime: config.chainTime
|
|
375
399
|
});
|
|
376
400
|
}
|
|
401
|
+
canObserve(ref) {
|
|
402
|
+
return ref.ledger === "arkade";
|
|
403
|
+
}
|
|
377
404
|
async register(ref) {
|
|
378
405
|
if (ref.ledger !== "arkade")
|
|
379
406
|
throw new Error(
|
|
380
407
|
`ArkadeContractManager can't track a '${ref.ledger}' HTLC`
|
|
381
408
|
);
|
|
382
409
|
this.#refs.set(ref.script, ref);
|
|
383
|
-
await this.#reconcileRef(ref);
|
|
384
410
|
}
|
|
385
411
|
async unregister(ref) {
|
|
386
412
|
if (ref.ledger !== "arkade") return;
|
|
@@ -392,17 +418,28 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
392
418
|
return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
|
|
393
419
|
}
|
|
394
420
|
chainNow(_ref) {
|
|
395
|
-
|
|
421
|
+
if (!this.#now) return void 0;
|
|
422
|
+
return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
|
|
396
423
|
}
|
|
397
424
|
onEvent(cb) {
|
|
398
425
|
this.#listeners.add(cb);
|
|
399
426
|
return () => this.#listeners.delete(cb);
|
|
400
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
|
+
*/
|
|
401
433
|
async refresh() {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
434
|
+
await this.#scanAll();
|
|
435
|
+
}
|
|
436
|
+
/** Targeted verify (hint / pre-action path) — never gated. */
|
|
437
|
+
async reconcile(ref) {
|
|
438
|
+
if (ref.ledger !== "arkade") return;
|
|
439
|
+
const tracked = this.#refs.get(ref.script);
|
|
440
|
+
if (!tracked) return;
|
|
441
|
+
await this.#readClock();
|
|
442
|
+
await this.#reconcileRef(tracked);
|
|
406
443
|
}
|
|
407
444
|
dispose() {
|
|
408
445
|
this.#listeners.clear();
|
|
@@ -411,6 +448,18 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
411
448
|
getPreimage(ref) {
|
|
412
449
|
return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
|
|
413
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
|
+
}
|
|
414
463
|
/** Read the VHTLC's vtxos and map them to an observation. */
|
|
415
464
|
async #reconcileRef(ref) {
|
|
416
465
|
const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
|
|
@@ -503,6 +552,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
503
552
|
/** address → the preimage a claim revealed. */
|
|
504
553
|
#preimages = /* @__PURE__ */ new Map();
|
|
505
554
|
#listeners = /* @__PURE__ */ new Set();
|
|
555
|
+
/** Last MTP reading (ms) + when it was fetched, for extrapolation. */
|
|
506
556
|
#now;
|
|
507
557
|
constructor(deps) {
|
|
508
558
|
this.#reader = deps.reader;
|
|
@@ -512,12 +562,17 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
512
562
|
return new _BitcoinContractManager(deps);
|
|
513
563
|
}
|
|
514
564
|
static async create(config) {
|
|
515
|
-
const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-
|
|
565
|
+
const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-HV6CANAA.js");
|
|
516
566
|
return _BitcoinContractManager.fromDeps({
|
|
517
|
-
reader: esploraReader2(config.esploraUrl
|
|
567
|
+
reader: esploraReader2(config.esploraUrl, void 0, {
|
|
568
|
+
minConfirmations: config.minConfirmations
|
|
569
|
+
}),
|
|
518
570
|
chainTime: config.chainTime
|
|
519
571
|
});
|
|
520
572
|
}
|
|
573
|
+
canObserve(ref) {
|
|
574
|
+
return ref.ledger === "bitcoin";
|
|
575
|
+
}
|
|
521
576
|
async register(ref) {
|
|
522
577
|
if (ref.ledger !== "bitcoin")
|
|
523
578
|
throw new Error(
|
|
@@ -536,18 +591,31 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
536
591
|
return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
|
|
537
592
|
}
|
|
538
593
|
chainNow(_ref) {
|
|
539
|
-
|
|
594
|
+
if (!this.#now) return void 0;
|
|
595
|
+
return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
|
|
540
596
|
}
|
|
541
597
|
onEvent(cb) {
|
|
542
598
|
this.#listeners.add(cb);
|
|
543
599
|
return () => this.#listeners.delete(cb);
|
|
544
600
|
}
|
|
545
601
|
async refresh() {
|
|
546
|
-
|
|
602
|
+
await this.#readClock();
|
|
547
603
|
await Promise.all(
|
|
548
604
|
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
549
605
|
);
|
|
550
606
|
}
|
|
607
|
+
async reconcile(ref) {
|
|
608
|
+
if (ref.ledger !== "bitcoin") return;
|
|
609
|
+
const tracked = this.#refs.get(ref.address);
|
|
610
|
+
if (!tracked) return;
|
|
611
|
+
await this.#readClock();
|
|
612
|
+
await this.#reconcileRef(tracked);
|
|
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
|
+
}
|
|
551
619
|
dispose() {
|
|
552
620
|
this.#listeners.clear();
|
|
553
621
|
}
|
|
@@ -556,7 +624,10 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
556
624
|
return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
|
|
557
625
|
}
|
|
558
626
|
async #reconcileRef(ref) {
|
|
559
|
-
const facts = await this.#reader.getHtlcFacts(
|
|
627
|
+
const facts = await this.#reader.getHtlcFacts(
|
|
628
|
+
ref.address,
|
|
629
|
+
ref.minConfirmations
|
|
630
|
+
);
|
|
560
631
|
const { observation, preimage } = bitcoinObservation(
|
|
561
632
|
facts,
|
|
562
633
|
hex4.decode(ref.preimageHash),
|
|
@@ -602,14 +673,59 @@ function htlcKey(ref) {
|
|
|
602
673
|
return `arkade:${ref.script}`;
|
|
603
674
|
case "bitcoin":
|
|
604
675
|
return `bitcoin:${ref.address}`;
|
|
605
|
-
case "evm":
|
|
606
|
-
|
|
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
|
+
}
|
|
607
690
|
case "lightning":
|
|
608
691
|
return `lightning:${ref.paymentHash}`;
|
|
609
692
|
}
|
|
610
693
|
}
|
|
611
694
|
|
|
612
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;
|
|
613
729
|
var EvmContractManager = class _EvmContractManager {
|
|
614
730
|
ledger = "evm";
|
|
615
731
|
#readers;
|
|
@@ -619,14 +735,8 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
619
735
|
#obs = /* @__PURE__ */ new Map();
|
|
620
736
|
/** htlcKey → the preimage a claim revealed. */
|
|
621
737
|
#preimages = /* @__PURE__ */ new Map();
|
|
622
|
-
/** chainId → its
|
|
738
|
+
/** chainId → its last clock reading (extrapolated in {@link chainNow}). */
|
|
623
739
|
#now = /* @__PURE__ */ new Map();
|
|
624
|
-
/** chainId → unsubscribe for its (single, shared) watch. */
|
|
625
|
-
#watchUnsubs = /* @__PURE__ */ new Map();
|
|
626
|
-
/** chainId → a block-event reconcile is currently running (coalescing). */
|
|
627
|
-
#reconciling = /* @__PURE__ */ new Set();
|
|
628
|
-
/** chainId → a block event arrived mid-reconcile; run exactly one more. */
|
|
629
|
-
#reconcileAgain = /* @__PURE__ */ new Set();
|
|
630
740
|
#listeners = /* @__PURE__ */ new Set();
|
|
631
741
|
constructor(deps) {
|
|
632
742
|
this.#readers = deps.readers;
|
|
@@ -634,6 +744,9 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
634
744
|
static fromDeps(deps) {
|
|
635
745
|
return new _EvmContractManager(deps);
|
|
636
746
|
}
|
|
747
|
+
canObserve(ref) {
|
|
748
|
+
return ref.ledger === "evm" && this.#readers.has(ref.chainId);
|
|
749
|
+
}
|
|
637
750
|
async register(ref) {
|
|
638
751
|
if (ref.ledger !== "evm")
|
|
639
752
|
throw new Error(`EvmContractManager can't track a '${ref.ledger}' HTLC`);
|
|
@@ -642,8 +755,6 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
642
755
|
`no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
|
|
643
756
|
);
|
|
644
757
|
this.#refs.set(htlcKey(ref), ref);
|
|
645
|
-
this.#ensureWatch(ref.chainId);
|
|
646
|
-
await this.#reconcileChain(ref.chainId);
|
|
647
758
|
}
|
|
648
759
|
async unregister(ref) {
|
|
649
760
|
if (ref.ledger !== "evm") return;
|
|
@@ -651,91 +762,176 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
651
762
|
this.#refs.delete(key);
|
|
652
763
|
this.#obs.delete(key);
|
|
653
764
|
this.#preimages.delete(key);
|
|
654
|
-
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
|
|
655
|
-
this.#watchUnsubs.get(ref.chainId)?.();
|
|
656
|
-
this.#watchUnsubs.delete(ref.chainId);
|
|
765
|
+
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
|
|
657
766
|
this.#now.delete(ref.chainId);
|
|
658
|
-
this.#reconcileAgain.delete(ref.chainId);
|
|
659
|
-
}
|
|
660
767
|
}
|
|
661
768
|
getState(ref) {
|
|
662
769
|
return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
|
|
663
770
|
}
|
|
664
771
|
chainNow(ref) {
|
|
665
|
-
|
|
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);
|
|
666
776
|
}
|
|
667
777
|
onEvent(cb) {
|
|
668
778
|
this.#listeners.add(cb);
|
|
669
779
|
return () => this.#listeners.delete(cb);
|
|
670
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
|
+
*/
|
|
671
787
|
async refresh() {
|
|
672
|
-
const chainIds =
|
|
673
|
-
|
|
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
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/** Targeted verify (hint / pre-action path) — never gated. */
|
|
803
|
+
async reconcile(ref) {
|
|
804
|
+
if (ref.ledger !== "evm") return;
|
|
805
|
+
const tracked = this.#refs.get(htlcKey(ref));
|
|
806
|
+
if (!tracked) return;
|
|
807
|
+
const reader = this.#readers.get(tracked.chainId);
|
|
808
|
+
if (!reader) return;
|
|
809
|
+
await this.#readClock(reader, tracked.chainId);
|
|
810
|
+
await this.#reconcileRefs(reader, tracked.chainId, [tracked]);
|
|
674
811
|
}
|
|
675
812
|
dispose() {
|
|
676
|
-
for (const unsub of this.#watchUnsubs.values()) unsub();
|
|
677
|
-
this.#watchUnsubs.clear();
|
|
678
|
-
this.#reconciling.clear();
|
|
679
|
-
this.#reconcileAgain.clear();
|
|
680
813
|
this.#listeners.clear();
|
|
681
814
|
}
|
|
682
815
|
/** The preimage a claim revealed on this HTLC, if one was seen. */
|
|
683
816
|
getPreimage(ref) {
|
|
684
817
|
return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
|
|
685
818
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Reconcile a chain in response to a block/log event, coalesced: at most one
|
|
697
|
-
* reconcile runs per chain at a time, with at most one queued rerun to pick up
|
|
698
|
-
* changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
|
|
699
|
-
* redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
|
|
700
|
-
* transient RPC failure in a background reconcile from becoming an unhandled
|
|
701
|
-
* rejection. `register`/`refresh` still await `#reconcileChain` directly.
|
|
702
|
-
*/
|
|
703
|
-
#scheduleReconcile(chainId) {
|
|
704
|
-
if (this.#reconciling.has(chainId)) {
|
|
705
|
-
this.#reconcileAgain.add(chainId);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
this.#reconciling.add(chainId);
|
|
709
|
-
void this.#reconcileChain(chainId).catch((error) => {
|
|
710
|
-
console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
|
|
711
|
-
}).finally(() => {
|
|
712
|
-
this.#reconciling.delete(chainId);
|
|
713
|
-
if (this.#reconcileAgain.delete(chainId))
|
|
714
|
-
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()
|
|
715
826
|
});
|
|
716
827
|
}
|
|
717
828
|
/** Refresh one chain's clock and re-observe every HTLC tracked on it. */
|
|
718
829
|
async #reconcileChain(chainId) {
|
|
719
830
|
const reader = this.#readers.get(chainId);
|
|
720
831
|
if (!reader) return;
|
|
721
|
-
this.#
|
|
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);
|
|
722
876
|
await Promise.all(
|
|
723
|
-
[
|
|
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
|
+
)
|
|
724
883
|
);
|
|
725
884
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
|
731
903
|
);
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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;
|
|
739
935
|
}
|
|
740
936
|
/** Update an observation and notify on change; never downgrade a resolved spend. */
|
|
741
937
|
#set(key, observation) {
|
|
@@ -750,13 +946,31 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
750
946
|
for (const listener of this.#listeners) listener(ref, observation);
|
|
751
947
|
}
|
|
752
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
|
+
}
|
|
753
962
|
|
|
754
963
|
// src/contracts/evm-reader-viem.ts
|
|
755
964
|
import {
|
|
756
965
|
createPublicClient,
|
|
966
|
+
decodeEventLog,
|
|
967
|
+
decodeFunctionResult,
|
|
968
|
+
encodeFunctionData,
|
|
757
969
|
fallback,
|
|
758
970
|
http,
|
|
759
|
-
|
|
971
|
+
numberToHex,
|
|
972
|
+
parseAbiItem,
|
|
973
|
+
toEventSelector
|
|
760
974
|
} from "viem";
|
|
761
975
|
var DEFAULT_EVM_RPCS = {
|
|
762
976
|
// Polygon — viem's default (polygon.drpc.org) misbehaves for some calls, so
|
|
@@ -766,82 +980,185 @@ var DEFAULT_EVM_RPCS = {
|
|
|
766
980
|
"https://tenderly.rpc.polygon.community",
|
|
767
981
|
"https://polygon-bor-rpc.publicnode.com"
|
|
768
982
|
],
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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).
|
|
774
988
|
42161: [
|
|
775
|
-
"https://arbitrum
|
|
776
|
-
"https://
|
|
777
|
-
"https://
|
|
989
|
+
"https://arb1.arbitrum.io/rpc",
|
|
990
|
+
"https://arbitrum.drpc.org",
|
|
991
|
+
"https://arbitrum-one-rpc.publicnode.com"
|
|
778
992
|
]
|
|
779
993
|
};
|
|
780
994
|
var SWAP_CREATED = parseAbiItem(
|
|
781
|
-
"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)"
|
|
782
996
|
);
|
|
783
997
|
var SWAP_REDEEMED = parseAbiItem(
|
|
784
|
-
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
|
|
998
|
+
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 indexed key, bytes32 preimage)"
|
|
785
999
|
);
|
|
786
1000
|
var SWAP_REFUNDED = parseAbiItem(
|
|
787
|
-
"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)"
|
|
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)"
|
|
788
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
|
+
}
|
|
789
1025
|
function evmReaderFromClient(client) {
|
|
790
1026
|
return {
|
|
791
|
-
async
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
+
]
|
|
812
1078
|
})
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
kind: "created",
|
|
819
|
-
amount: args?.amount ?? 0n,
|
|
820
|
-
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]
|
|
821
1084
|
});
|
|
1085
|
+
results.set(activeKey(queries[i]), decodeIsActive(data));
|
|
1086
|
+
};
|
|
1087
|
+
if (queries.length === 1) {
|
|
1088
|
+
await callOne(0);
|
|
1089
|
+
return results;
|
|
822
1090
|
}
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
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)));
|
|
826
1120
|
}
|
|
827
|
-
|
|
828
|
-
return events;
|
|
1121
|
+
return results;
|
|
829
1122
|
},
|
|
830
|
-
async
|
|
1123
|
+
async getLatestBlock() {
|
|
831
1124
|
const block = await client.getBlock();
|
|
832
|
-
return
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1125
|
+
return {
|
|
1126
|
+
timeMs: Number(block.timestamp) * 1e3,
|
|
1127
|
+
number: block.number ?? 0n
|
|
1128
|
+
};
|
|
836
1129
|
}
|
|
837
1130
|
};
|
|
838
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
|
+
}
|
|
839
1159
|
function createEvmRpcReader(rpcUrls) {
|
|
840
1160
|
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
841
|
-
const transport = urls.length > 1 ? fallback(
|
|
842
|
-
urls.map((url) => http(url)),
|
|
843
|
-
{ rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
|
|
844
|
-
) : http(urls[0]);
|
|
1161
|
+
const transport = urls.length > 1 ? fallback(urls.map((url) => http(url))) : http(urls[0]);
|
|
845
1162
|
const client = createPublicClient({ transport });
|
|
846
1163
|
return evmReaderFromClient(client);
|
|
847
1164
|
}
|
|
@@ -860,8 +1177,354 @@ function defaultEvmReaders(overrides) {
|
|
|
860
1177
|
return readers;
|
|
861
1178
|
}
|
|
862
1179
|
|
|
1180
|
+
// src/hints/swap-worker.ts
|
|
1181
|
+
var AUTO_EXECUTABLE = /* @__PURE__ */ new Set([
|
|
1182
|
+
"claim"
|
|
1183
|
+
]);
|
|
1184
|
+
var SwapWorker = class {
|
|
1185
|
+
#tracker;
|
|
1186
|
+
#hintSource;
|
|
1187
|
+
#execute;
|
|
1188
|
+
#onActionRequired;
|
|
1189
|
+
#maxAttempts;
|
|
1190
|
+
#baseDelayMs;
|
|
1191
|
+
#maxDelayMs;
|
|
1192
|
+
/** Swaps with an auto action in flight — guards against double-runs. */
|
|
1193
|
+
#executing = /* @__PURE__ */ new Set();
|
|
1194
|
+
/** Swaps with a scheduled auto-claim retry (backoff timer + attempt count). */
|
|
1195
|
+
#retries = /* @__PURE__ */ new Map();
|
|
1196
|
+
#unsubs = [];
|
|
1197
|
+
#started = false;
|
|
1198
|
+
constructor(options) {
|
|
1199
|
+
this.#tracker = options.tracker;
|
|
1200
|
+
this.#hintSource = options.hintSource;
|
|
1201
|
+
this.#execute = options.execute;
|
|
1202
|
+
this.#onActionRequired = options.onActionRequired;
|
|
1203
|
+
this.#maxAttempts = options.retry?.maxAttempts ?? 5;
|
|
1204
|
+
this.#baseDelayMs = options.retry?.baseDelayMs ?? 2e3;
|
|
1205
|
+
this.#maxDelayMs = options.retry?.maxDelayMs ?? 3e4;
|
|
1206
|
+
}
|
|
1207
|
+
/** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
|
|
1208
|
+
start() {
|
|
1209
|
+
if (this.#started) return;
|
|
1210
|
+
this.#started = true;
|
|
1211
|
+
this.#hintSource.subscribe(this.#tracker.trackedSwapIds());
|
|
1212
|
+
this.#unsubs.push(
|
|
1213
|
+
this.#hintSource.onStatus(({ swapId }) => {
|
|
1214
|
+
void this.#tracker.applyHint(swapId).catch((error) => {
|
|
1215
|
+
console.warn(`SwapWorker: applyHint(${swapId}) failed:`, error);
|
|
1216
|
+
});
|
|
1217
|
+
})
|
|
1218
|
+
);
|
|
1219
|
+
if (this.#hintSource.onReconnect)
|
|
1220
|
+
this.#unsubs.push(
|
|
1221
|
+
this.#hintSource.onReconnect(() => this.#reverifyAllTracked())
|
|
1222
|
+
);
|
|
1223
|
+
this.#hintSource.start();
|
|
1224
|
+
this.#unsubs.push(
|
|
1225
|
+
this.#tracker.subscribeToActions(
|
|
1226
|
+
(swapId, actions) => this.#onActions(swapId, actions)
|
|
1227
|
+
)
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
/** Stop reacting, cancel pending retries, and close the hint feed. */
|
|
1231
|
+
stop() {
|
|
1232
|
+
this.#started = false;
|
|
1233
|
+
for (const unsub of this.#unsubs) unsub();
|
|
1234
|
+
this.#unsubs = [];
|
|
1235
|
+
this.#hintSource.stop();
|
|
1236
|
+
for (const { timer } of this.#retries.values()) clearTimeout(timer);
|
|
1237
|
+
this.#retries.clear();
|
|
1238
|
+
this.#executing.clear();
|
|
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
|
+
}
|
|
1251
|
+
#onActions(swapId, actions) {
|
|
1252
|
+
const recommended = actions.recommended;
|
|
1253
|
+
if (recommended === "none") {
|
|
1254
|
+
this.#hintSource.unsubscribe([swapId]);
|
|
1255
|
+
this.#cancelRetry(swapId);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
this.#hintSource.subscribe([swapId]);
|
|
1259
|
+
if (recommended && AUTO_EXECUTABLE.has(recommended) && this.#execute) {
|
|
1260
|
+
this.#autoExecute(swapId, actions);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
this.#cancelRetry(swapId);
|
|
1264
|
+
if (!recommended) return;
|
|
1265
|
+
const automation = actions.actions.find(
|
|
1266
|
+
(a) => a.id === recommended
|
|
1267
|
+
)?.automation;
|
|
1268
|
+
if (automation === "manual" || automation === "confirm")
|
|
1269
|
+
this.#onActionRequired?.(swapId, actions);
|
|
1270
|
+
}
|
|
1271
|
+
#autoExecute(swapId, actions) {
|
|
1272
|
+
const execute = this.#execute;
|
|
1273
|
+
if (!execute) return;
|
|
1274
|
+
if (this.#executing.has(swapId)) return;
|
|
1275
|
+
const actionId = actions.recommended;
|
|
1276
|
+
if (!actionId) return;
|
|
1277
|
+
this.#executing.add(swapId);
|
|
1278
|
+
void execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
|
|
1279
|
+
console.warn(`SwapWorker: ${actionId}(${swapId}) failed:`, error);
|
|
1280
|
+
this.#scheduleRetry(swapId, actions);
|
|
1281
|
+
}).finally(() => this.#executing.delete(swapId));
|
|
1282
|
+
}
|
|
1283
|
+
/** Schedule the next backoff retry, or give up and surface once attempts run out. */
|
|
1284
|
+
#scheduleRetry(swapId, actions) {
|
|
1285
|
+
const attempts = (this.#retries.get(swapId)?.attempts ?? 0) + 1;
|
|
1286
|
+
if (attempts > this.#maxAttempts) {
|
|
1287
|
+
console.warn(
|
|
1288
|
+
`SwapWorker: auto-claim for ${swapId} gave up after ${this.#maxAttempts} attempts`
|
|
1289
|
+
);
|
|
1290
|
+
this.#cancelRetry(swapId);
|
|
1291
|
+
this.#onActionRequired?.(swapId, actions);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.#clearTimer(swapId);
|
|
1295
|
+
const delay = Math.min(
|
|
1296
|
+
this.#baseDelayMs * 2 ** (attempts - 1),
|
|
1297
|
+
this.#maxDelayMs
|
|
1298
|
+
);
|
|
1299
|
+
const timer = setTimeout(() => this.#retry(swapId, actions), delay);
|
|
1300
|
+
this.#retries.set(swapId, { attempts, timer });
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* A retry fires: re-verify against chain (forced re-emit), which drives
|
|
1304
|
+
* {@link #onActions} to re-run the claim only if it is still recommended. If the
|
|
1305
|
+
* re-verify itself fails we can't safely retry, so back off and try the cycle
|
|
1306
|
+
* again.
|
|
1307
|
+
*/
|
|
1308
|
+
#retry(swapId, actions) {
|
|
1309
|
+
void this.#tracker.applyHint(swapId, { force: true }).catch((error) => {
|
|
1310
|
+
console.warn(
|
|
1311
|
+
`SwapWorker: re-verify before retrying ${swapId} failed:`,
|
|
1312
|
+
error
|
|
1313
|
+
);
|
|
1314
|
+
this.#scheduleRetry(swapId, actions);
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
#cancelRetry(swapId) {
|
|
1318
|
+
this.#clearTimer(swapId);
|
|
1319
|
+
this.#retries.delete(swapId);
|
|
1320
|
+
}
|
|
1321
|
+
#clearTimer(swapId) {
|
|
1322
|
+
const existing = this.#retries.get(swapId);
|
|
1323
|
+
if (existing) clearTimeout(existing.timer);
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
// src/hints/ws-status-source.ts
|
|
1328
|
+
var MAX_ARGS_PER_FRAME = 64;
|
|
1329
|
+
var MAX_SUBSCRIPTIONS = 256;
|
|
1330
|
+
var WsStatusSource = class {
|
|
1331
|
+
#url;
|
|
1332
|
+
#factory;
|
|
1333
|
+
#reconnectMinMs;
|
|
1334
|
+
#reconnectMaxMs;
|
|
1335
|
+
/**
|
|
1336
|
+
* Ids actually subscribed on the wire — held at or below the server's
|
|
1337
|
+
* per-connection cap, and resent in full on every (re)connect.
|
|
1338
|
+
*/
|
|
1339
|
+
#active = /* @__PURE__ */ new Set();
|
|
1340
|
+
/**
|
|
1341
|
+
* Ids we want but have no slot for. Promoted FIFO as slots free (the worker
|
|
1342
|
+
* unsubscribes each swap when it reaches a terminal action, so they do free).
|
|
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.
|
|
1345
|
+
*/
|
|
1346
|
+
#pending = /* @__PURE__ */ new Set();
|
|
1347
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
1348
|
+
#reconnectListeners = /* @__PURE__ */ new Set();
|
|
1349
|
+
#ws;
|
|
1350
|
+
#open = false;
|
|
1351
|
+
#stopped = false;
|
|
1352
|
+
/** A connect failed or the socket dropped — the next open is a RE-connect. */
|
|
1353
|
+
#hadDisruption = false;
|
|
1354
|
+
#backoffMs;
|
|
1355
|
+
#reconnectTimer;
|
|
1356
|
+
constructor(options) {
|
|
1357
|
+
this.#url = toWsUrl(options.serverUrl);
|
|
1358
|
+
this.#factory = options.webSocketFactory ?? defaultWebSocketFactory;
|
|
1359
|
+
this.#reconnectMinMs = options.reconnectMinMs ?? 1e3;
|
|
1360
|
+
this.#reconnectMaxMs = options.reconnectMaxMs ?? 3e4;
|
|
1361
|
+
this.#backoffMs = this.#reconnectMinMs;
|
|
1362
|
+
}
|
|
1363
|
+
/** Open the connection and begin (re)subscribing. Idempotent. */
|
|
1364
|
+
start() {
|
|
1365
|
+
if (this.#ws || this.#reconnectTimer) return;
|
|
1366
|
+
this.#stopped = false;
|
|
1367
|
+
this.#connect();
|
|
1368
|
+
}
|
|
1369
|
+
/** Close the connection and stop reconnecting. */
|
|
1370
|
+
stop() {
|
|
1371
|
+
this.#stopped = true;
|
|
1372
|
+
if (this.#reconnectTimer) clearTimeout(this.#reconnectTimer);
|
|
1373
|
+
this.#reconnectTimer = void 0;
|
|
1374
|
+
this.#open = false;
|
|
1375
|
+
this.#ws?.close();
|
|
1376
|
+
this.#ws = void 0;
|
|
1377
|
+
}
|
|
1378
|
+
/** Emit for each pushed status update. Returns an unsubscribe fn. */
|
|
1379
|
+
onStatus(cb) {
|
|
1380
|
+
this.#listeners.add(cb);
|
|
1381
|
+
return () => this.#listeners.delete(cb);
|
|
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
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Track these swap ids — subscribe on the socket now if open, else on next open.
|
|
1397
|
+
*
|
|
1398
|
+
* Enforces the server's per-connection cap locally. The server silently stops
|
|
1399
|
+
* adding once it is full (it breaks out of the loop but still acks every id),
|
|
1400
|
+
* so sending more would leave us believing ids are subscribed that will never
|
|
1401
|
+
* receive an update. Overflow is queued instead and promoted as slots free.
|
|
1402
|
+
*/
|
|
1403
|
+
subscribe(swapIds) {
|
|
1404
|
+
const fresh = swapIds.filter(
|
|
1405
|
+
(id) => !this.#active.has(id) && !this.#pending.has(id)
|
|
1406
|
+
);
|
|
1407
|
+
if (fresh.length === 0) return;
|
|
1408
|
+
const admitted = [];
|
|
1409
|
+
for (const id of fresh) {
|
|
1410
|
+
if (this.#active.size < MAX_SUBSCRIPTIONS) {
|
|
1411
|
+
this.#active.add(id);
|
|
1412
|
+
admitted.push(id);
|
|
1413
|
+
} else {
|
|
1414
|
+
this.#pending.add(id);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (this.#pending.size > 0)
|
|
1418
|
+
console.warn(
|
|
1419
|
+
`WsStatusSource: at the server cap of ${MAX_SUBSCRIPTIONS} subscriptions; ${this.#pending.size} swap(s) queued for a slot and will advance on the chain poll until then`
|
|
1420
|
+
);
|
|
1421
|
+
if (this.#open && admitted.length > 0)
|
|
1422
|
+
this.#sendFrame("subscribe", admitted);
|
|
1423
|
+
}
|
|
1424
|
+
/** Stop tracking these swap ids, promoting queued ones into the freed slots. */
|
|
1425
|
+
unsubscribe(swapIds) {
|
|
1426
|
+
const dropped = [];
|
|
1427
|
+
for (const id of swapIds) {
|
|
1428
|
+
if (this.#active.delete(id)) dropped.push(id);
|
|
1429
|
+
else this.#pending.delete(id);
|
|
1430
|
+
}
|
|
1431
|
+
if (this.#open && dropped.length > 0)
|
|
1432
|
+
this.#sendFrame("unsubscribe", dropped);
|
|
1433
|
+
this.#promotePending();
|
|
1434
|
+
}
|
|
1435
|
+
/** Move queued ids into any free slots, subscribing them if the socket is open. */
|
|
1436
|
+
#promotePending() {
|
|
1437
|
+
if (this.#pending.size === 0) return;
|
|
1438
|
+
const promoted = [];
|
|
1439
|
+
for (const id of this.#pending) {
|
|
1440
|
+
if (this.#active.size >= MAX_SUBSCRIPTIONS) break;
|
|
1441
|
+
this.#pending.delete(id);
|
|
1442
|
+
this.#active.add(id);
|
|
1443
|
+
promoted.push(id);
|
|
1444
|
+
}
|
|
1445
|
+
if (this.#open && promoted.length > 0)
|
|
1446
|
+
this.#sendFrame("subscribe", promoted);
|
|
1447
|
+
}
|
|
1448
|
+
#connect() {
|
|
1449
|
+
this.#reconnectTimer = void 0;
|
|
1450
|
+
const ws = this.#factory(this.#url);
|
|
1451
|
+
this.#ws = ws;
|
|
1452
|
+
ws.onopen = () => {
|
|
1453
|
+
this.#backoffMs = this.#reconnectMinMs;
|
|
1454
|
+
this.#promotePending();
|
|
1455
|
+
this.#open = true;
|
|
1456
|
+
if (this.#active.size > 0)
|
|
1457
|
+
this.#sendFrame("subscribe", [...this.#active]);
|
|
1458
|
+
if (this.#hadDisruption) {
|
|
1459
|
+
this.#hadDisruption = false;
|
|
1460
|
+
for (const cb of this.#reconnectListeners) cb();
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
ws.onmessage = (event) => this.#handleMessage(event.data);
|
|
1464
|
+
ws.onclose = () => this.#onDisconnect();
|
|
1465
|
+
ws.onerror = () => this.#ws?.close();
|
|
1466
|
+
}
|
|
1467
|
+
#onDisconnect() {
|
|
1468
|
+
this.#open = false;
|
|
1469
|
+
this.#ws = void 0;
|
|
1470
|
+
this.#hadDisruption = true;
|
|
1471
|
+
if (this.#stopped || this.#reconnectTimer) return;
|
|
1472
|
+
const delay = this.#backoffMs;
|
|
1473
|
+
this.#backoffMs = Math.min(this.#backoffMs * 2, this.#reconnectMaxMs);
|
|
1474
|
+
this.#reconnectTimer = setTimeout(() => this.#connect(), delay);
|
|
1475
|
+
}
|
|
1476
|
+
#handleMessage(data) {
|
|
1477
|
+
if (typeof data !== "string") return;
|
|
1478
|
+
let frame;
|
|
1479
|
+
try {
|
|
1480
|
+
frame = JSON.parse(data);
|
|
1481
|
+
} catch {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (!frame || typeof frame !== "object") return;
|
|
1485
|
+
const f = frame;
|
|
1486
|
+
if (f.data && typeof f.data === "object") {
|
|
1487
|
+
for (const [swapId, status] of Object.entries(
|
|
1488
|
+
f.data
|
|
1489
|
+
))
|
|
1490
|
+
this.#emit({ swapId, status });
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (typeof f.message === "string")
|
|
1494
|
+
console.warn(`WsStatusSource: server error: ${f.message}`);
|
|
1495
|
+
}
|
|
1496
|
+
#emit(update) {
|
|
1497
|
+
for (const cb of this.#listeners) cb(update);
|
|
1498
|
+
}
|
|
1499
|
+
#sendFrame(op, ids) {
|
|
1500
|
+
for (let i = 0; i < ids.length; i += MAX_ARGS_PER_FRAME) {
|
|
1501
|
+
const args = ids.slice(i, i + MAX_ARGS_PER_FRAME);
|
|
1502
|
+
this.#ws?.send(JSON.stringify({ op, channel: "swap_status", args }));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
function toWsUrl(serverUrl) {
|
|
1507
|
+
const url = new URL(serverUrl);
|
|
1508
|
+
const scheme = url.protocol === "https:" ? "wss" : "ws";
|
|
1509
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
1510
|
+
return `${scheme}://${url.host}${path}/ws`;
|
|
1511
|
+
}
|
|
1512
|
+
function defaultWebSocketFactory(url) {
|
|
1513
|
+
const WS = globalThis.WebSocket;
|
|
1514
|
+
if (!WS)
|
|
1515
|
+
throw new Error(
|
|
1516
|
+
"no global WebSocket available \u2014 pass options.webSocketFactory (Node < 22)"
|
|
1517
|
+
);
|
|
1518
|
+
return new WS(url);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
863
1521
|
// src/contracts/arkade-vhtlc.ts
|
|
864
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";
|
|
865
1528
|
import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
|
|
866
1529
|
import { hex as hex5 } from "@scure/base";
|
|
867
1530
|
function strip0x(value) {
|
|
@@ -888,7 +1551,18 @@ function buildArkadeVhtlcRef(input) {
|
|
|
888
1551
|
input.unilateralRefundWithoutReceiverDelay
|
|
889
1552
|
)
|
|
890
1553
|
};
|
|
891
|
-
const vhtlc =
|
|
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
|
+
})();
|
|
892
1566
|
return {
|
|
893
1567
|
ledger: "arkade",
|
|
894
1568
|
script: hex5.encode(vhtlc.pkScript),
|
|
@@ -901,6 +1575,18 @@ function buildArkadeVhtlcRef(input) {
|
|
|
901
1575
|
|
|
902
1576
|
// src/tracker/from-swap.ts
|
|
903
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
|
+
}
|
|
904
1590
|
var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
|
|
905
1591
|
var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
|
|
906
1592
|
function arkadeLeg(r, address, expectedSats) {
|
|
@@ -914,10 +1600,12 @@ function arkadeLeg(r, address, expectedSats) {
|
|
|
914
1600
|
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
915
1601
|
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
916
1602
|
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
917
|
-
expectedSats
|
|
1603
|
+
expectedSats,
|
|
1604
|
+
arkadeHtlcScriptVersion: r.arkade_htlc_script_version
|
|
918
1605
|
});
|
|
919
1606
|
}
|
|
920
1607
|
function evmLeg(args) {
|
|
1608
|
+
const createdAtMs = args.createdAt ? Date.parse(args.createdAt) : Number.NaN;
|
|
921
1609
|
return {
|
|
922
1610
|
ledger: "evm",
|
|
923
1611
|
chainId: args.chainId,
|
|
@@ -925,15 +1613,20 @@ function evmLeg(args) {
|
|
|
925
1613
|
preimageHash: ensure0x(args.hashLock),
|
|
926
1614
|
claimAddress: ensure0x(args.claimAddress),
|
|
927
1615
|
expectedAmount: BigInt(args.expectedSats),
|
|
928
|
-
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
|
|
929
1620
|
};
|
|
930
1621
|
}
|
|
931
|
-
|
|
1622
|
+
var CLIENT_FUNDING_MIN_CONFIRMATIONS = 1;
|
|
1623
|
+
function bitcoinLeg(address, hashLock, expectedSats, minConfirmations) {
|
|
932
1624
|
return {
|
|
933
1625
|
ledger: "bitcoin",
|
|
934
1626
|
address,
|
|
935
1627
|
preimageHash: strip0x2(hashLock),
|
|
936
|
-
expectedSats
|
|
1628
|
+
expectedSats,
|
|
1629
|
+
minConfirmations
|
|
937
1630
|
};
|
|
938
1631
|
}
|
|
939
1632
|
function swapToTracked(stored) {
|
|
@@ -951,7 +1644,11 @@ function swapToTracked(stored) {
|
|
|
951
1644
|
claimAddress: r.client_evm_address,
|
|
952
1645
|
// the client claims the server's EVM HTLC
|
|
953
1646
|
expectedSats: r.evm_expected_sats,
|
|
954
|
-
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
|
|
955
1652
|
}),
|
|
956
1653
|
clientRefundLocktime: ms(r.vhtlc_refund_locktime),
|
|
957
1654
|
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
@@ -966,8 +1663,14 @@ function swapToTracked(stored) {
|
|
|
966
1663
|
hashLock: r.hash_lock,
|
|
967
1664
|
claimAddress: r.server_evm_address,
|
|
968
1665
|
// the server claims the client's EVM HTLC
|
|
969
|
-
expectedSats: r.evm_expected_sats
|
|
970
|
-
// evm_to_arkade doesn't expose the locked token —
|
|
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
|
|
971
1674
|
}),
|
|
972
1675
|
serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
|
|
973
1676
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
@@ -980,7 +1683,8 @@ function swapToTracked(stored) {
|
|
|
980
1683
|
clientHtlc: bitcoinLeg(
|
|
981
1684
|
r.btc_htlc_address,
|
|
982
1685
|
r.evm_hash_lock,
|
|
983
|
-
Number(r.source_amount)
|
|
1686
|
+
Number(r.source_amount),
|
|
1687
|
+
CLIENT_FUNDING_MIN_CONFIRMATIONS
|
|
984
1688
|
),
|
|
985
1689
|
serverHtlc: evmLeg({
|
|
986
1690
|
chainId: r.evm_chain_id,
|
|
@@ -989,7 +1693,11 @@ function swapToTracked(stored) {
|
|
|
989
1693
|
claimAddress: r.client_evm_address,
|
|
990
1694
|
// the client claims the server's EVM HTLC
|
|
991
1695
|
expectedSats: r.evm_expected_sats,
|
|
992
|
-
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
|
|
993
1701
|
}),
|
|
994
1702
|
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
995
1703
|
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
@@ -1005,7 +1713,11 @@ function swapToTracked(stored) {
|
|
|
1005
1713
|
claimAddress: r.server_evm_address,
|
|
1006
1714
|
// the server claims the client's EVM HTLC
|
|
1007
1715
|
expectedSats: r.evm_expected_sats,
|
|
1008
|
-
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
|
|
1009
1721
|
}),
|
|
1010
1722
|
serverHtlc: bitcoinLeg(
|
|
1011
1723
|
r.btc_htlc_address,
|
|
@@ -1025,7 +1737,8 @@ function swapToTracked(stored) {
|
|
|
1025
1737
|
clientHtlc: bitcoinLeg(
|
|
1026
1738
|
r.btc_htlc_address,
|
|
1027
1739
|
r.hash_lock,
|
|
1028
|
-
Number(r.source_amount)
|
|
1740
|
+
Number(r.source_amount),
|
|
1741
|
+
CLIENT_FUNDING_MIN_CONFIRMATIONS
|
|
1029
1742
|
),
|
|
1030
1743
|
serverHtlc: buildArkadeVhtlcRef({
|
|
1031
1744
|
senderPk: r.server_vhtlc_pk,
|
|
@@ -1039,7 +1752,8 @@ function swapToTracked(stored) {
|
|
|
1039
1752
|
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1040
1753
|
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1041
1754
|
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1042
|
-
expectedSats: Number(r.target_amount)
|
|
1755
|
+
expectedSats: Number(r.target_amount),
|
|
1756
|
+
arkadeHtlcScriptVersion: r.arkade_htlc_script_version
|
|
1043
1757
|
}),
|
|
1044
1758
|
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1045
1759
|
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
@@ -1073,8 +1787,14 @@ function swapToTracked(stored) {
|
|
|
1073
1787
|
hashLock: r.hash_lock,
|
|
1074
1788
|
claimAddress: r.server_evm_address,
|
|
1075
1789
|
// the server claims the client's leg
|
|
1076
|
-
expectedSats: r.evm_expected_sats
|
|
1077
|
-
// evm_to_lightning doesn't expose the locked token —
|
|
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
|
|
1078
1798
|
}),
|
|
1079
1799
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1080
1800
|
serverRefundLocktime: 0
|
|
@@ -1107,7 +1827,11 @@ function swapToTracked(stored) {
|
|
|
1107
1827
|
claimAddress: r.client_evm_address,
|
|
1108
1828
|
// the client claims the server's leg
|
|
1109
1829
|
expectedSats: r.evm_expected_sats,
|
|
1110
|
-
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
|
|
1111
1835
|
}),
|
|
1112
1836
|
clientRefundLocktime: 0,
|
|
1113
1837
|
// no on-chain client leg
|
|
@@ -1126,15 +1850,35 @@ var SwapTracker = class {
|
|
|
1126
1850
|
#lastActions = /* @__PURE__ */ new Map();
|
|
1127
1851
|
#subscribers = /* @__PURE__ */ new Set();
|
|
1128
1852
|
#refreshIntervalMs;
|
|
1853
|
+
#atRiskReconcileIntervalMs;
|
|
1854
|
+
#lastAtRiskReconcileAt = 0;
|
|
1129
1855
|
#eventUnsubs = [];
|
|
1130
1856
|
#timer;
|
|
1131
1857
|
constructor(managers, options) {
|
|
1132
1858
|
this.#managers = managers;
|
|
1133
1859
|
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
1860
|
+
this.#atRiskReconcileIntervalMs = options?.atRiskReconcileIntervalMs ?? 6e4;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Whether every on-chain leg of `swap` is observable — i.e. the tracker can
|
|
1864
|
+
* derive its status from chain. `false` means a leg is on a ledger/chain this
|
|
1865
|
+
* client can't reach (no manager, or an EVM chain with no RPC), so the swap
|
|
1866
|
+
* can't be tracked. Pure predicate — callers decide the policy (skip vs reject).
|
|
1867
|
+
*/
|
|
1868
|
+
canObserve(swap) {
|
|
1869
|
+
return legsOf(swap).every(
|
|
1870
|
+
(leg) => this.#managers.get(leg.ledger)?.canObserve(leg) ?? false
|
|
1871
|
+
);
|
|
1134
1872
|
}
|
|
1135
1873
|
/** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
|
|
1136
1874
|
async startTracking(swaps) {
|
|
1137
1875
|
for (const swap of swaps) {
|
|
1876
|
+
if (!this.canObserve(swap)) {
|
|
1877
|
+
console.warn(
|
|
1878
|
+
`SwapTracker: skipping swap ${swap.swapId} \u2014 a leg is on a chain this client can't reach (configure it to track this swap)`
|
|
1879
|
+
);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1138
1882
|
this.#swaps.set(swap.swapId, swap);
|
|
1139
1883
|
for (const leg of legsOf(swap)) await this.#managerFor(leg).register(leg);
|
|
1140
1884
|
}
|
|
@@ -1143,31 +1887,140 @@ var SwapTracker = class {
|
|
|
1143
1887
|
}
|
|
1144
1888
|
await this.#refreshManagers();
|
|
1145
1889
|
this.#recomputeAll();
|
|
1890
|
+
this.#lastAtRiskReconcileAt = Date.now();
|
|
1146
1891
|
if (this.#refreshIntervalMs > 0)
|
|
1147
1892
|
this.#timer = setInterval(
|
|
1148
1893
|
() => void this.#tick(),
|
|
1149
1894
|
this.#refreshIntervalMs
|
|
1150
1895
|
);
|
|
1151
1896
|
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Track one more swap after {@link startTracking} — register its on-chain
|
|
1899
|
+
* leg(s), seed the managers they touch, and derive its first action. Idempotent:
|
|
1900
|
+
* a swap already tracked, or one already seen through to a terminal action, is
|
|
1901
|
+
* ignored. New swaps are picked up by the existing per-manager event listeners
|
|
1902
|
+
* and poll timer, so nothing else needs re-wiring. Lets a swap created after
|
|
1903
|
+
* start (e.g. via `createSwap`) be tracked without a restart.
|
|
1904
|
+
*/
|
|
1905
|
+
async track(swap) {
|
|
1906
|
+
if (this.#swaps.has(swap.swapId) || this.#lastActions.has(swap.swapId))
|
|
1907
|
+
return;
|
|
1908
|
+
if (!this.canObserve(swap)) {
|
|
1909
|
+
console.warn(
|
|
1910
|
+
`SwapTracker: skipping swap ${swap.swapId} \u2014 a leg is on a chain this client can't reach (configure it to track this swap)`
|
|
1911
|
+
);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
this.#swaps.set(swap.swapId, swap);
|
|
1915
|
+
const registered = [];
|
|
1916
|
+
try {
|
|
1917
|
+
for (const leg of legsOf(swap)) {
|
|
1918
|
+
await this.#managerFor(leg).register(leg);
|
|
1919
|
+
registered.push(leg);
|
|
1920
|
+
}
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
this.#swaps.delete(swap.swapId);
|
|
1923
|
+
for (const leg of registered) void this.#managerFor(leg).unregister(leg);
|
|
1924
|
+
throw error;
|
|
1925
|
+
}
|
|
1926
|
+
const managers = new Set(legsOf(swap).map((leg) => this.#managerFor(leg)));
|
|
1927
|
+
await Promise.all([...managers].map((manager) => manager.refresh()));
|
|
1928
|
+
this.#recompute(swap);
|
|
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
|
+
*/
|
|
1152
1938
|
async #tick() {
|
|
1153
|
-
|
|
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
|
+
}
|
|
1154
1951
|
this.#recomputeAll();
|
|
1155
1952
|
}
|
|
1156
1953
|
/**
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
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
|
|
1161
1975
|
* ledger's endpoint being down (the default client builds every manager).
|
|
1162
1976
|
*/
|
|
1163
1977
|
async #refreshManagers() {
|
|
1164
1978
|
const active = /* @__PURE__ */ new Set();
|
|
1165
1979
|
for (const swap of this.#swaps.values())
|
|
1166
1980
|
for (const leg of legsOf(swap)) active.add(leg.ledger);
|
|
1167
|
-
const
|
|
1168
|
-
|
|
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())
|
|
1169
1989
|
);
|
|
1170
|
-
|
|
1990
|
+
results.forEach((result, index) => {
|
|
1991
|
+
if (result.status === "rejected")
|
|
1992
|
+
console.warn(
|
|
1993
|
+
`SwapTracker: refreshing ${ledgers[index]} failed:`,
|
|
1994
|
+
result.reason
|
|
1995
|
+
);
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
/** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
|
|
1999
|
+
trackedSwapIds() {
|
|
2000
|
+
return [...this.#swaps.keys()];
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Re-verify one swap against the chain right now — the hook a faster server
|
|
2004
|
+
* status hint drives. Reconciles just that swap's legs (targeted, on the
|
|
2005
|
+
* managers), then recomputes its action. The hint is only a *trigger*: the
|
|
2006
|
+
* chain stays the source of truth, so a premature hint that the chain doesn't
|
|
2007
|
+
* yet reflect changes nothing. A no-op for an untracked swap.
|
|
2008
|
+
*/
|
|
2009
|
+
async applyHint(swapId, opts) {
|
|
2010
|
+
const swap = this.#swaps.get(swapId);
|
|
2011
|
+
if (!swap) return;
|
|
2012
|
+
const legs = legsOf(swap);
|
|
2013
|
+
const results = await Promise.allSettled(
|
|
2014
|
+
legs.map((leg) => this.#managerFor(leg).reconcile(leg))
|
|
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
|
+
});
|
|
2023
|
+
this.#recompute(swap, opts?.force ?? false);
|
|
1171
2024
|
}
|
|
1172
2025
|
/** Notify `cb` of the current action for each tracked swap, then on every change. */
|
|
1173
2026
|
subscribeToActions(cb) {
|
|
@@ -1203,7 +2056,7 @@ var SwapTracker = class {
|
|
|
1203
2056
|
#recomputeAll() {
|
|
1204
2057
|
for (const swap of this.#swaps.values()) this.#recompute(swap);
|
|
1205
2058
|
}
|
|
1206
|
-
#recompute(swap) {
|
|
2059
|
+
#recompute(swap, force = false) {
|
|
1207
2060
|
let clientHtlc;
|
|
1208
2061
|
let serverHtlc;
|
|
1209
2062
|
let clientChainNow = 0;
|
|
@@ -1224,17 +2077,23 @@ var SwapTracker = class {
|
|
|
1224
2077
|
}
|
|
1225
2078
|
const status = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
1226
2079
|
if (status === void 0) return;
|
|
1227
|
-
const actions =
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
+
};
|
|
1236
2095
|
const previous = this.#lastActions.get(swap.swapId);
|
|
1237
|
-
if (previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
2096
|
+
if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
1238
2097
|
return;
|
|
1239
2098
|
this.#lastActions.set(swap.swapId, actions);
|
|
1240
2099
|
for (const cb of this.#subscribers) cb(swap.swapId, actions);
|
|
@@ -1261,7 +2120,11 @@ var Client = class {
|
|
|
1261
2120
|
/** Per-ledger monitors, resolved once from {@link #tracking}. */
|
|
1262
2121
|
#managers;
|
|
1263
2122
|
#tracker;
|
|
1264
|
-
|
|
2123
|
+
/** Hint-driven auto-claim worker; only built when `withAutoClaim` opted in. */
|
|
2124
|
+
#worker;
|
|
2125
|
+
#startPromise;
|
|
2126
|
+
/** Swap ids whose settled status was already re-fetched this session. */
|
|
2127
|
+
#settledSyncDone = /* @__PURE__ */ new Set();
|
|
1265
2128
|
constructor(...args) {
|
|
1266
2129
|
if (args[0] instanceof LegacyClient) {
|
|
1267
2130
|
this.#legacy = args[0];
|
|
@@ -1475,31 +2338,41 @@ var Client = class {
|
|
|
1475
2338
|
}
|
|
1476
2339
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1477
2340
|
createSwap(...args) {
|
|
1478
|
-
return this.#legacy.createSwap(...args);
|
|
2341
|
+
return this.#trackAfterCreate(this.#legacy.createSwap(...args));
|
|
1479
2342
|
}
|
|
1480
2343
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1481
2344
|
createArkadeToEvmSwapGeneric(...args) {
|
|
1482
|
-
return this.#
|
|
2345
|
+
return this.#trackAfterCreate(
|
|
2346
|
+
this.#legacy.createArkadeToEvmSwapGeneric(...args)
|
|
2347
|
+
);
|
|
1483
2348
|
}
|
|
1484
2349
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1485
2350
|
createLightningToEvmSwapGeneric(...args) {
|
|
1486
|
-
return this.#
|
|
2351
|
+
return this.#trackAfterCreate(
|
|
2352
|
+
this.#legacy.createLightningToEvmSwapGeneric(...args)
|
|
2353
|
+
);
|
|
1487
2354
|
}
|
|
1488
2355
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1489
2356
|
createBitcoinToEvmSwap(...args) {
|
|
1490
|
-
return this.#legacy.createBitcoinToEvmSwap(...args);
|
|
2357
|
+
return this.#trackAfterCreate(this.#legacy.createBitcoinToEvmSwap(...args));
|
|
1491
2358
|
}
|
|
1492
2359
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1493
2360
|
createBitcoinToArkadeSwap(...args) {
|
|
1494
|
-
return this.#
|
|
2361
|
+
return this.#trackAfterCreate(
|
|
2362
|
+
this.#legacy.createBitcoinToArkadeSwap(...args)
|
|
2363
|
+
);
|
|
1495
2364
|
}
|
|
1496
2365
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1497
2366
|
createLightningToArkadeSwap(...args) {
|
|
1498
|
-
return this.#
|
|
2367
|
+
return this.#trackAfterCreate(
|
|
2368
|
+
this.#legacy.createLightningToArkadeSwap(...args)
|
|
2369
|
+
);
|
|
1499
2370
|
}
|
|
1500
2371
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1501
2372
|
createArkadeToLightningSwap(...args) {
|
|
1502
|
-
return this.#
|
|
2373
|
+
return this.#trackAfterCreate(
|
|
2374
|
+
this.#legacy.createArkadeToLightningSwap(...args)
|
|
2375
|
+
);
|
|
1503
2376
|
}
|
|
1504
2377
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1505
2378
|
getArkadeToLightningQuote(...args) {
|
|
@@ -1507,19 +2380,25 @@ var Client = class {
|
|
|
1507
2380
|
}
|
|
1508
2381
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1509
2382
|
retryArkadeToLightningSwap(...args) {
|
|
1510
|
-
return this.#
|
|
2383
|
+
return this.#trackAfterRetry(
|
|
2384
|
+
this.#legacy.retryArkadeToLightningSwap(...args)
|
|
2385
|
+
);
|
|
1511
2386
|
}
|
|
1512
2387
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1513
2388
|
createEvmToArkadeSwapGeneric(...args) {
|
|
1514
|
-
return this.#
|
|
2389
|
+
return this.#trackAfterCreate(
|
|
2390
|
+
this.#legacy.createEvmToArkadeSwapGeneric(...args)
|
|
2391
|
+
);
|
|
1515
2392
|
}
|
|
1516
2393
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1517
2394
|
createEvmToBitcoinSwap(...args) {
|
|
1518
|
-
return this.#legacy.createEvmToBitcoinSwap(...args);
|
|
2395
|
+
return this.#trackAfterCreate(this.#legacy.createEvmToBitcoinSwap(...args));
|
|
1519
2396
|
}
|
|
1520
2397
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1521
2398
|
createEvmToLightningSwapGeneric(...args) {
|
|
1522
|
-
return this.#
|
|
2399
|
+
return this.#trackAfterCreate(
|
|
2400
|
+
this.#legacy.createEvmToLightningSwapGeneric(...args)
|
|
2401
|
+
);
|
|
1523
2402
|
}
|
|
1524
2403
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1525
2404
|
getCoordinatorFundingCallDataPermit2(...args) {
|
|
@@ -1554,6 +2433,14 @@ var Client = class {
|
|
|
1554
2433
|
return this.#legacy.getEvmDepositorKey(...args);
|
|
1555
2434
|
}
|
|
1556
2435
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
2436
|
+
getRefundedEvmSwapContinuation(...args) {
|
|
2437
|
+
return this.#legacy.getRefundedEvmSwapContinuation(...args);
|
|
2438
|
+
}
|
|
2439
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
2440
|
+
continueRefundedEvmSwap(...args) {
|
|
2441
|
+
return this.#trackAfterRetry(this.#legacy.continueRefundedEvmSwap(...args));
|
|
2442
|
+
}
|
|
2443
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1557
2444
|
recoverGaslessFunds(...args) {
|
|
1558
2445
|
return this.#legacy.recoverGaslessFunds(...args);
|
|
1559
2446
|
}
|
|
@@ -1571,28 +2458,157 @@ var Client = class {
|
|
|
1571
2458
|
* Subscribe with {@link subscribeToActions}; release with
|
|
1572
2459
|
* {@link stopTracking}.
|
|
1573
2460
|
*/
|
|
1574
|
-
|
|
2461
|
+
startTracking() {
|
|
1575
2462
|
if (!this.#tracking.enabled)
|
|
1576
|
-
|
|
1577
|
-
|
|
2463
|
+
return Promise.reject(
|
|
2464
|
+
new Error(
|
|
2465
|
+
"tracking is disabled \u2014 remove .withoutTracking() to enable it"
|
|
2466
|
+
)
|
|
1578
2467
|
);
|
|
1579
|
-
|
|
1580
|
-
|
|
2468
|
+
this.#startPromise ??= this.#doStartTracking().catch((error) => {
|
|
2469
|
+
this.#startPromise = void 0;
|
|
2470
|
+
throw error;
|
|
2471
|
+
});
|
|
2472
|
+
return this.#startPromise;
|
|
2473
|
+
}
|
|
2474
|
+
async #doStartTracking() {
|
|
1581
2475
|
try {
|
|
1582
2476
|
const tracker = new SwapTracker(await this.#ensureManagers(), {
|
|
1583
2477
|
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
1584
2478
|
});
|
|
1585
2479
|
this.#tracker = tracker;
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2480
|
+
await tracker.startTracking(await this.#loadTrackedSwaps());
|
|
2481
|
+
tracker.subscribeToActions(
|
|
2482
|
+
(swapId, actions) => this.#syncSettledStatus(swapId, actions)
|
|
2483
|
+
);
|
|
2484
|
+
this.#startWorker(tracker);
|
|
1589
2485
|
} catch (error) {
|
|
2486
|
+
this.#worker?.stop();
|
|
2487
|
+
this.#worker = void 0;
|
|
1590
2488
|
this.#tracker?.stop();
|
|
1591
2489
|
this.#tracker = void 0;
|
|
1592
|
-
this.#started = false;
|
|
1593
2490
|
throw error;
|
|
1594
2491
|
}
|
|
1595
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
|
+
}
|
|
2520
|
+
/** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
|
|
2521
|
+
async #loadTrackedSwaps() {
|
|
2522
|
+
const swaps = await this.listAllSwaps();
|
|
2523
|
+
return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Run a create, then fold the new swap into tracking.
|
|
2527
|
+
*
|
|
2528
|
+
* When tracking is active, first REJECT a swap this client can't reach: a leg on
|
|
2529
|
+
* an unconfigured chain can be neither observed nor claimed, so creating it would
|
|
2530
|
+
* strand funds. Throwing here (before the caller funds it) is deliberately
|
|
2531
|
+
* stricter than {@link startTracking}, which only *skips* such a pre-existing
|
|
2532
|
+
* swap. Otherwise fold the new swap(s) in — fire-and-forget, so a tracking hiccup
|
|
2533
|
+
* never fails or delays the create; a no-op when tracking isn't running (a later
|
|
2534
|
+
* {@link startTracking} picks it up from storage).
|
|
2535
|
+
*/
|
|
2536
|
+
async #trackAfterCreate(op) {
|
|
2537
|
+
const result = await op;
|
|
2538
|
+
const tracker = this.#tracker;
|
|
2539
|
+
if (!tracker) return result;
|
|
2540
|
+
let swaps;
|
|
2541
|
+
try {
|
|
2542
|
+
swaps = await this.#loadTrackedSwaps();
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
console.warn("Client: loading swaps after create failed:", error);
|
|
2545
|
+
return result;
|
|
2546
|
+
}
|
|
2547
|
+
const created = swaps.find((s) => s.swapId === createdSwapId(result));
|
|
2548
|
+
if (created && !tracker.canObserve(created))
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
`Refusing swap ${created.swapId}: a leg is on a chain this client can't reach, so it can be neither tracked nor claimed. Configure the chain (e.g. ClientBuilder.withEvmRpcUrls) before creating it.`
|
|
2551
|
+
);
|
|
2552
|
+
void this.#trackAll(tracker, swaps);
|
|
2553
|
+
return result;
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Run a retry, then fold its replacement swap into tracking.
|
|
2557
|
+
*
|
|
2558
|
+
* Unlike a create there is no unreachable-leg
|
|
2559
|
+
* rejection: by the time the retry returns, the old VHTLC has already been
|
|
2560
|
+
* collab-refunded into the new one, so refusing here can't protect any funds
|
|
2561
|
+
* — and {@link SwapTracker.track} skips a swap it can't observe anyway.
|
|
2562
|
+
*/
|
|
2563
|
+
async #trackAfterRetry(op) {
|
|
2564
|
+
const result = await op;
|
|
2565
|
+
const tracker = this.#tracker;
|
|
2566
|
+
if (!tracker) return result;
|
|
2567
|
+
try {
|
|
2568
|
+
void this.#trackAll(tracker, await this.#loadTrackedSwaps());
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
console.warn("Client: loading swaps after retry failed:", error);
|
|
2571
|
+
}
|
|
2572
|
+
return result;
|
|
2573
|
+
}
|
|
2574
|
+
/** Track each given swap the tracker isn't already watching (isolated per swap). */
|
|
2575
|
+
async #trackAll(tracker, swaps) {
|
|
2576
|
+
for (const swap of swaps) {
|
|
2577
|
+
try {
|
|
2578
|
+
await tracker.track(swap);
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
console.warn(`Client: tracking swap ${swap.swapId} failed:`, error);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
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.
|
|
2591
|
+
*/
|
|
2592
|
+
#startWorker(tracker) {
|
|
2593
|
+
const serverUrl = this.#tracking.serverUrl;
|
|
2594
|
+
if (!serverUrl) return;
|
|
2595
|
+
const autoClaim = this.#tracking.autoClaim;
|
|
2596
|
+
const hintSource = new WsStatusSource({ serverUrl });
|
|
2597
|
+
const worker = new SwapWorker({
|
|
2598
|
+
tracker,
|
|
2599
|
+
hintSource,
|
|
2600
|
+
execute: autoClaim ? async (swapId, actionId) => {
|
|
2601
|
+
if (actionId !== "claim")
|
|
2602
|
+
throw new Error(`refusing to auto-run action '${actionId}'`);
|
|
2603
|
+
const result = await this.claim(swapId);
|
|
2604
|
+
if (!result.success)
|
|
2605
|
+
throw new Error(result.message ?? "claim failed");
|
|
2606
|
+
} : void 0,
|
|
2607
|
+
onActionRequired: autoClaim?.onActionRequired
|
|
2608
|
+
});
|
|
2609
|
+
this.#worker = worker;
|
|
2610
|
+
worker.start();
|
|
2611
|
+
}
|
|
1596
2612
|
/**
|
|
1597
2613
|
* Subscribe to next-action updates: `cb` fires with the current action for each
|
|
1598
2614
|
* tracked swap immediately, then on every change. Call after
|
|
@@ -1605,9 +2621,11 @@ var Client = class {
|
|
|
1605
2621
|
}
|
|
1606
2622
|
/** Stop tracking and drop subscribers. The managers themselves are not disposed. */
|
|
1607
2623
|
stopTracking() {
|
|
2624
|
+
this.#worker?.stop();
|
|
2625
|
+
this.#worker = void 0;
|
|
1608
2626
|
this.#tracker?.stop();
|
|
1609
2627
|
this.#tracker = void 0;
|
|
1610
|
-
this.#
|
|
2628
|
+
this.#startPromise = void 0;
|
|
1611
2629
|
}
|
|
1612
2630
|
/**
|
|
1613
2631
|
* Resolve the per-ledger managers once — the explicit override if given, else
|
|
@@ -1622,7 +2640,7 @@ var Client = class {
|
|
|
1622
2640
|
return this.#managers;
|
|
1623
2641
|
}
|
|
1624
2642
|
const managers = /* @__PURE__ */ new Map();
|
|
1625
|
-
const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
|
|
2643
|
+
const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
|
|
1626
2644
|
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
1627
2645
|
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
1628
2646
|
if (arkadeUrl) {
|
|
@@ -1638,7 +2656,8 @@ var Client = class {
|
|
|
1638
2656
|
"bitcoin",
|
|
1639
2657
|
await BitcoinContractManager.create({
|
|
1640
2658
|
esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
|
|
1641
|
-
chainTime
|
|
2659
|
+
chainTime,
|
|
2660
|
+
minConfirmations: bitcoinMinConfirmations
|
|
1642
2661
|
})
|
|
1643
2662
|
);
|
|
1644
2663
|
this.#managers = managers;
|
|
@@ -1650,10 +2669,13 @@ var ClientBuilder = class {
|
|
|
1650
2669
|
// Observe-mode tracking is on by default; the built client auto-builds its
|
|
1651
2670
|
// managers from config unless disabled or given an explicit override.
|
|
1652
2671
|
#trackingEnabled = true;
|
|
2672
|
+
#serverUrl;
|
|
1653
2673
|
#arkadeServerUrl;
|
|
1654
2674
|
#esploraUrl;
|
|
2675
|
+
#bitcoinMinConfirmations;
|
|
1655
2676
|
#evmRpcUrls;
|
|
1656
2677
|
#managers;
|
|
2678
|
+
#autoClaim;
|
|
1657
2679
|
/**
|
|
1658
2680
|
* Override the EVM JSON-RPC endpoint per chainId. Optional — tracking uses
|
|
1659
2681
|
* tested public defaults otherwise; an override is tried first, with the
|
|
@@ -1663,6 +2685,18 @@ var ClientBuilder = class {
|
|
|
1663
2685
|
this.#evmRpcUrls = urls;
|
|
1664
2686
|
return this;
|
|
1665
2687
|
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Opt in to hint-driven auto-claim. Tracking then also subscribes to the
|
|
2690
|
+
* server status WebSocket (a faster trigger than the chain poll) and, when the
|
|
2691
|
+
* chain confirms a swap is claimable, claims it automatically. Actions that
|
|
2692
|
+
* need the user — a manual `fund`, or a refund to confirm — are surfaced via
|
|
2693
|
+
* `onActionRequired` instead of being run. Off by default: this auto-spends on
|
|
2694
|
+
* the user's behalf, so it must be explicit.
|
|
2695
|
+
*/
|
|
2696
|
+
withAutoClaim(options) {
|
|
2697
|
+
this.#autoClaim = { onActionRequired: options?.onActionRequired };
|
|
2698
|
+
return this;
|
|
2699
|
+
}
|
|
1666
2700
|
/** Turn observe-mode tracking off. */
|
|
1667
2701
|
withoutTracking() {
|
|
1668
2702
|
this.#trackingEnabled = false;
|
|
@@ -1679,6 +2713,7 @@ var ClientBuilder = class {
|
|
|
1679
2713
|
}
|
|
1680
2714
|
withBaseUrl(...args) {
|
|
1681
2715
|
this.#inner.withBaseUrl(...args);
|
|
2716
|
+
this.#serverUrl = args[0];
|
|
1682
2717
|
return this;
|
|
1683
2718
|
}
|
|
1684
2719
|
withReferralCode(...args) {
|
|
@@ -1702,6 +2737,18 @@ var ClientBuilder = class {
|
|
|
1702
2737
|
this.#esploraUrl = url;
|
|
1703
2738
|
return this;
|
|
1704
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
|
+
}
|
|
1705
2752
|
/**
|
|
1706
2753
|
* Override the Ark server URL. Optional for tracking — it otherwise defaults to
|
|
1707
2754
|
* the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
|
|
@@ -1742,13 +2789,28 @@ var ClientBuilder = class {
|
|
|
1742
2789
|
async build() {
|
|
1743
2790
|
return new Client(await this.#inner.build(), {
|
|
1744
2791
|
enabled: this.#trackingEnabled,
|
|
2792
|
+
serverUrl: this.#serverUrl,
|
|
1745
2793
|
managers: this.#managers,
|
|
1746
2794
|
arkadeServerUrl: this.#arkadeServerUrl,
|
|
1747
2795
|
esploraUrl: this.#esploraUrl,
|
|
1748
|
-
|
|
2796
|
+
bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
|
|
2797
|
+
evmRpcUrls: this.#evmRpcUrls,
|
|
2798
|
+
autoClaim: this.#autoClaim
|
|
1749
2799
|
});
|
|
1750
2800
|
}
|
|
1751
2801
|
};
|
|
2802
|
+
var SETTLED_STORED_STATUSES = /* @__PURE__ */ new Set([
|
|
2803
|
+
"serverredeemed",
|
|
2804
|
+
"clientrefunded",
|
|
2805
|
+
"clientrefundedserverfunded",
|
|
2806
|
+
"clientrefundedserverrefunded",
|
|
2807
|
+
"clientredeemedandclientrefunded",
|
|
2808
|
+
"expired"
|
|
2809
|
+
]);
|
|
2810
|
+
function createdSwapId(result) {
|
|
2811
|
+
const id = result?.response?.id;
|
|
2812
|
+
return typeof id === "string" ? id : void 0;
|
|
2813
|
+
}
|
|
1752
2814
|
export {
|
|
1753
2815
|
ArkadeContractManager,
|
|
1754
2816
|
BitcoinContractManager,
|
|
@@ -1758,6 +2820,8 @@ export {
|
|
|
1758
2820
|
DEFAULT_EVM_RPCS,
|
|
1759
2821
|
EvmContractManager,
|
|
1760
2822
|
SwapTracker,
|
|
2823
|
+
SwapWorker,
|
|
2824
|
+
WsStatusSource,
|
|
1761
2825
|
buildArkadeVhtlcRef,
|
|
1762
2826
|
createEvmRpcReader,
|
|
1763
2827
|
defaultArkadeServerUrl,
|
|
@@ -1768,6 +2832,7 @@ export {
|
|
|
1768
2832
|
evmReaderFromClient,
|
|
1769
2833
|
htlcFactsFromEsploraTxs,
|
|
1770
2834
|
htlcKey,
|
|
2835
|
+
htlcQueryKey,
|
|
1771
2836
|
swapToTracked
|
|
1772
2837
|
};
|
|
1773
2838
|
//# sourceMappingURL=index.js.map
|