@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.cjs
CHANGED
|
@@ -28,7 +28,7 @@ __export(bitcoin_reader_esplora_exports, {
|
|
|
28
28
|
esploraReader: () => esploraReader,
|
|
29
29
|
htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs
|
|
30
30
|
});
|
|
31
|
-
function htlcFactsFromEsploraTxs(txs, address) {
|
|
31
|
+
function htlcFactsFromEsploraTxs(txs, address, opts) {
|
|
32
32
|
for (const tx of txs) {
|
|
33
33
|
const spend = tx.vin.find(
|
|
34
34
|
(vin) => vin.prevout?.scriptpubkey_address === address
|
|
@@ -46,20 +46,29 @@ function htlcFactsFromEsploraTxs(txs, address) {
|
|
|
46
46
|
);
|
|
47
47
|
if (funding) {
|
|
48
48
|
const fundedSats = funding.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
|
|
49
|
+
const minConf = opts?.minConfirmations ?? 0;
|
|
50
|
+
let deepEnough;
|
|
51
|
+
if (funding.status?.confirmed) {
|
|
52
|
+
deepEnough = minConf <= 1 || opts?.tipHeight !== void 0 && funding.status.block_height !== void 0 && opts.tipHeight - funding.status.block_height + 1 >= minConf;
|
|
53
|
+
} else {
|
|
54
|
+
deepEnough = minConf === 0;
|
|
55
|
+
}
|
|
49
56
|
return {
|
|
50
|
-
funding:
|
|
57
|
+
funding: deepEnough ? "confirmed" : "mempool",
|
|
51
58
|
fundedSats
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
return { funding: "absent", fundedSats: 0 };
|
|
55
62
|
}
|
|
56
|
-
function esploraReader(esploraUrls, fetchImpl = fetch) {
|
|
63
|
+
function esploraReader(esploraUrls, fetchImpl = fetch, policy) {
|
|
57
64
|
const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(
|
|
58
65
|
(url) => url.replace(/\/+$/, "")
|
|
59
66
|
);
|
|
67
|
+
const minConf = policy?.minConfirmations ?? 0;
|
|
60
68
|
let start = 0;
|
|
61
69
|
return {
|
|
62
|
-
async getHtlcFacts(address) {
|
|
70
|
+
async getHtlcFacts(address, minConfirmations) {
|
|
71
|
+
const required = minConfirmations ?? minConf;
|
|
63
72
|
const from = start++ % bases.length;
|
|
64
73
|
let lastError;
|
|
65
74
|
for (let i = 0; i < bases.length; i++) {
|
|
@@ -69,10 +78,17 @@ function esploraReader(esploraUrls, fetchImpl = fetch) {
|
|
|
69
78
|
`${base}/address/${encodeURIComponent(address)}/txs`
|
|
70
79
|
);
|
|
71
80
|
if (!res.ok) throw new Error(`esplora ${res.status} at ${base}`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
const txs = await res.json();
|
|
82
|
+
let tipHeight;
|
|
83
|
+
if (required > 1) {
|
|
84
|
+
const tip = await fetchImpl(`${base}/blocks/tip/height`);
|
|
85
|
+
if (!tip.ok) throw new Error(`esplora ${tip.status} at ${base}`);
|
|
86
|
+
tipHeight = Number(await tip.text());
|
|
87
|
+
}
|
|
88
|
+
return htlcFactsFromEsploraTxs(txs, address, {
|
|
89
|
+
minConfirmations: required,
|
|
90
|
+
tipHeight
|
|
91
|
+
});
|
|
76
92
|
} catch (error) {
|
|
77
93
|
lastError = error;
|
|
78
94
|
}
|
|
@@ -103,6 +119,8 @@ __export(src_exports, {
|
|
|
103
119
|
DEFAULT_EVM_RPCS: () => DEFAULT_EVM_RPCS,
|
|
104
120
|
EvmContractManager: () => EvmContractManager,
|
|
105
121
|
SwapTracker: () => SwapTracker,
|
|
122
|
+
SwapWorker: () => SwapWorker,
|
|
123
|
+
WsStatusSource: () => WsStatusSource,
|
|
106
124
|
buildArkadeVhtlcRef: () => buildArkadeVhtlcRef,
|
|
107
125
|
createEvmRpcReader: () => createEvmRpcReader,
|
|
108
126
|
defaultArkadeServerUrl: () => defaultArkadeServerUrl,
|
|
@@ -113,12 +131,14 @@ __export(src_exports, {
|
|
|
113
131
|
evmReaderFromClient: () => evmReaderFromClient,
|
|
114
132
|
htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
|
|
115
133
|
htlcKey: () => htlcKey,
|
|
134
|
+
htlcQueryKey: () => htlcQueryKey,
|
|
116
135
|
swapToTracked: () => swapToTracked
|
|
117
136
|
});
|
|
118
137
|
module.exports = __toCommonJS(src_exports);
|
|
119
138
|
__reExport(src_exports, require("@lendasat/lendaswap-sdk-pure"), module.exports);
|
|
120
139
|
|
|
121
140
|
// src/actions/derive.ts
|
|
141
|
+
var FUNDING_REAP_GRACE_MS = 60 * 60 * 1e3;
|
|
122
142
|
function deriveSwapActions(input) {
|
|
123
143
|
const {
|
|
124
144
|
status,
|
|
@@ -133,12 +153,23 @@ function deriveSwapActions(input) {
|
|
|
133
153
|
if (!clientFunds) {
|
|
134
154
|
const wait = {
|
|
135
155
|
id: "wait",
|
|
156
|
+
waitingOn: "client_payment",
|
|
136
157
|
recommended: true,
|
|
137
158
|
automation: "auto",
|
|
138
159
|
reason: "Waiting for the Lightning payment to be locked in."
|
|
139
160
|
};
|
|
140
161
|
return { recommended: "wait", actions: [wait] };
|
|
141
162
|
}
|
|
163
|
+
if (clientChainNow >= clientRefundLocktime + FUNDING_REAP_GRACE_MS) {
|
|
164
|
+
const done = {
|
|
165
|
+
id: "none",
|
|
166
|
+
outcome: "expired",
|
|
167
|
+
recommended: true,
|
|
168
|
+
automation: "auto",
|
|
169
|
+
reason: "The funding window closed."
|
|
170
|
+
};
|
|
171
|
+
return { recommended: "none", actions: [done] };
|
|
172
|
+
}
|
|
142
173
|
if (clientChainNow >= clientRefundLocktime) return { actions: [] };
|
|
143
174
|
const fund = {
|
|
144
175
|
id: "fund",
|
|
@@ -152,6 +183,7 @@ function deriveSwapActions(input) {
|
|
|
152
183
|
case "clientfundingseen": {
|
|
153
184
|
const wait = {
|
|
154
185
|
id: "wait",
|
|
186
|
+
waitingOn: "client_funding_confirmation",
|
|
155
187
|
recommended: true,
|
|
156
188
|
automation: "auto",
|
|
157
189
|
reason: "Waiting for your funding transaction to confirm."
|
|
@@ -171,6 +203,7 @@ function deriveSwapActions(input) {
|
|
|
171
203
|
}
|
|
172
204
|
const wait = {
|
|
173
205
|
id: "wait",
|
|
206
|
+
waitingOn: "server_funding",
|
|
174
207
|
recommended: true,
|
|
175
208
|
automation: "auto",
|
|
176
209
|
reason: "Waiting for the server to fund the swap."
|
|
@@ -201,6 +234,7 @@ function deriveSwapActions(input) {
|
|
|
201
234
|
if (!clientFunds) {
|
|
202
235
|
const done = {
|
|
203
236
|
id: "none",
|
|
237
|
+
outcome: "refunded",
|
|
204
238
|
recommended: true,
|
|
205
239
|
automation: "auto",
|
|
206
240
|
reason: "The claim window passed \u2014 your Lightning payment will be refunded automatically."
|
|
@@ -219,6 +253,7 @@ function deriveSwapActions(input) {
|
|
|
219
253
|
}
|
|
220
254
|
const wait = {
|
|
221
255
|
id: "wait",
|
|
256
|
+
waitingOn: "refund_timelock",
|
|
222
257
|
recommended: true,
|
|
223
258
|
automation: "auto",
|
|
224
259
|
reason: "Wait for your refund timelock \u2014 do not claim, as revealing the preimage would let the server take your deposit."
|
|
@@ -238,6 +273,7 @@ function deriveSwapActions(input) {
|
|
|
238
273
|
case "clientredeeming": {
|
|
239
274
|
const wait = {
|
|
240
275
|
id: "wait",
|
|
276
|
+
waitingOn: "claim_confirmation",
|
|
241
277
|
recommended: true,
|
|
242
278
|
automation: "auto",
|
|
243
279
|
reason: "Redeeming your funds \u2014 waiting for the claim to confirm."
|
|
@@ -248,6 +284,7 @@ function deriveSwapActions(input) {
|
|
|
248
284
|
case "serverredeemed": {
|
|
249
285
|
const done = {
|
|
250
286
|
id: "none",
|
|
287
|
+
outcome: "completed",
|
|
251
288
|
recommended: true,
|
|
252
289
|
automation: "auto",
|
|
253
290
|
reason: "Swap complete \u2014 you've received your funds."
|
|
@@ -257,6 +294,7 @@ function deriveSwapActions(input) {
|
|
|
257
294
|
case "expired": {
|
|
258
295
|
const done = {
|
|
259
296
|
id: "none",
|
|
297
|
+
outcome: "expired",
|
|
260
298
|
recommended: true,
|
|
261
299
|
automation: "auto",
|
|
262
300
|
reason: "The swap expired without funding \u2014 nothing was locked."
|
|
@@ -268,6 +306,7 @@ function deriveSwapActions(input) {
|
|
|
268
306
|
case "clientrefundedserverfunded": {
|
|
269
307
|
const done = {
|
|
270
308
|
id: "none",
|
|
309
|
+
outcome: "refunded",
|
|
271
310
|
recommended: true,
|
|
272
311
|
automation: "auto",
|
|
273
312
|
reason: "Refunded \u2014 your deposit was returned."
|
|
@@ -277,6 +316,8 @@ function deriveSwapActions(input) {
|
|
|
277
316
|
case "clientredeemedandclientrefunded": {
|
|
278
317
|
const done = {
|
|
279
318
|
id: "none",
|
|
319
|
+
// The client holds both sides' funds — "completed" from its viewpoint.
|
|
320
|
+
outcome: "completed",
|
|
280
321
|
recommended: true,
|
|
281
322
|
automation: "auto",
|
|
282
323
|
reason: "This swap ended in an anomalous state (redeemed and refunded); no action needed."
|
|
@@ -299,6 +340,7 @@ function deriveSwapActions(input) {
|
|
|
299
340
|
}
|
|
300
341
|
const wait = {
|
|
301
342
|
id: "wait",
|
|
343
|
+
waitingOn: "refund_timelock",
|
|
302
344
|
recommended: true,
|
|
303
345
|
automation: "auto",
|
|
304
346
|
reason: "The swap did not complete; waiting for the refund timelock to reclaim your deposit."
|
|
@@ -398,7 +440,7 @@ function payOnLightningStatus(serverHtlc) {
|
|
|
398
440
|
}
|
|
399
441
|
|
|
400
442
|
// src/client.ts
|
|
401
|
-
var
|
|
443
|
+
var import_lendaswap_sdk_pure2 = require("@lendasat/lendaswap-sdk-pure");
|
|
402
444
|
|
|
403
445
|
// src/contracts/arkade-manager.ts
|
|
404
446
|
var import_sdk2 = require("@arkade-os/sdk");
|
|
@@ -463,6 +505,7 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
463
505
|
/** pkScript → the verified preimage recovered from a claim spend. */
|
|
464
506
|
#preimages = /* @__PURE__ */ new Map();
|
|
465
507
|
#listeners = /* @__PURE__ */ new Set();
|
|
508
|
+
/** Last MTP reading (ms) + when it was fetched, for extrapolation. */
|
|
466
509
|
#now;
|
|
467
510
|
constructor(deps) {
|
|
468
511
|
this.#indexer = deps.indexer;
|
|
@@ -479,13 +522,15 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
479
522
|
chainTime: config.chainTime
|
|
480
523
|
});
|
|
481
524
|
}
|
|
525
|
+
canObserve(ref) {
|
|
526
|
+
return ref.ledger === "arkade";
|
|
527
|
+
}
|
|
482
528
|
async register(ref) {
|
|
483
529
|
if (ref.ledger !== "arkade")
|
|
484
530
|
throw new Error(
|
|
485
531
|
`ArkadeContractManager can't track a '${ref.ledger}' HTLC`
|
|
486
532
|
);
|
|
487
533
|
this.#refs.set(ref.script, ref);
|
|
488
|
-
await this.#reconcileRef(ref);
|
|
489
534
|
}
|
|
490
535
|
async unregister(ref) {
|
|
491
536
|
if (ref.ledger !== "arkade") return;
|
|
@@ -497,17 +542,28 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
497
542
|
return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
|
|
498
543
|
}
|
|
499
544
|
chainNow(_ref) {
|
|
500
|
-
|
|
545
|
+
if (!this.#now) return void 0;
|
|
546
|
+
return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
|
|
501
547
|
}
|
|
502
548
|
onEvent(cb) {
|
|
503
549
|
this.#listeners.add(cb);
|
|
504
550
|
return () => this.#listeners.delete(cb);
|
|
505
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* Full reconcile of every tracked ref (clock + vtxos). Called on demand
|
|
554
|
+
* (tracker start / newly tracked swap), NOT periodically: onward, hints and
|
|
555
|
+
* the tracker's at-risk safety net drive targeted `reconcile`s.
|
|
556
|
+
*/
|
|
506
557
|
async refresh() {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
558
|
+
await this.#scanAll();
|
|
559
|
+
}
|
|
560
|
+
/** Targeted verify (hint / pre-action path) — never gated. */
|
|
561
|
+
async reconcile(ref) {
|
|
562
|
+
if (ref.ledger !== "arkade") return;
|
|
563
|
+
const tracked = this.#refs.get(ref.script);
|
|
564
|
+
if (!tracked) return;
|
|
565
|
+
await this.#readClock();
|
|
566
|
+
await this.#reconcileRef(tracked);
|
|
511
567
|
}
|
|
512
568
|
dispose() {
|
|
513
569
|
this.#listeners.clear();
|
|
@@ -516,6 +572,18 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
516
572
|
getPreimage(ref) {
|
|
517
573
|
return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
|
|
518
574
|
}
|
|
575
|
+
/** Read and store the MTP clock (with fetch time, for extrapolation). */
|
|
576
|
+
async #readClock() {
|
|
577
|
+
if (!this.#chainTime) return;
|
|
578
|
+
this.#now = { mtpMs: await this.#chainTime(), fetchedAtMs: Date.now() };
|
|
579
|
+
}
|
|
580
|
+
/** Full scan of every tracked ref (clock + vtxos). */
|
|
581
|
+
async #scanAll() {
|
|
582
|
+
await this.#readClock();
|
|
583
|
+
await Promise.all(
|
|
584
|
+
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
585
|
+
);
|
|
586
|
+
}
|
|
519
587
|
/** Read the VHTLC's vtxos and map them to an observation. */
|
|
520
588
|
async #reconcileRef(ref) {
|
|
521
589
|
const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
|
|
@@ -608,6 +676,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
608
676
|
/** address → the preimage a claim revealed. */
|
|
609
677
|
#preimages = /* @__PURE__ */ new Map();
|
|
610
678
|
#listeners = /* @__PURE__ */ new Set();
|
|
679
|
+
/** Last MTP reading (ms) + when it was fetched, for extrapolation. */
|
|
611
680
|
#now;
|
|
612
681
|
constructor(deps) {
|
|
613
682
|
this.#reader = deps.reader;
|
|
@@ -619,10 +688,15 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
619
688
|
static async create(config) {
|
|
620
689
|
const { esploraReader: esploraReader2 } = await Promise.resolve().then(() => (init_bitcoin_reader_esplora(), bitcoin_reader_esplora_exports));
|
|
621
690
|
return _BitcoinContractManager.fromDeps({
|
|
622
|
-
reader: esploraReader2(config.esploraUrl
|
|
691
|
+
reader: esploraReader2(config.esploraUrl, void 0, {
|
|
692
|
+
minConfirmations: config.minConfirmations
|
|
693
|
+
}),
|
|
623
694
|
chainTime: config.chainTime
|
|
624
695
|
});
|
|
625
696
|
}
|
|
697
|
+
canObserve(ref) {
|
|
698
|
+
return ref.ledger === "bitcoin";
|
|
699
|
+
}
|
|
626
700
|
async register(ref) {
|
|
627
701
|
if (ref.ledger !== "bitcoin")
|
|
628
702
|
throw new Error(
|
|
@@ -641,18 +715,31 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
641
715
|
return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
|
|
642
716
|
}
|
|
643
717
|
chainNow(_ref) {
|
|
644
|
-
|
|
718
|
+
if (!this.#now) return void 0;
|
|
719
|
+
return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
|
|
645
720
|
}
|
|
646
721
|
onEvent(cb) {
|
|
647
722
|
this.#listeners.add(cb);
|
|
648
723
|
return () => this.#listeners.delete(cb);
|
|
649
724
|
}
|
|
650
725
|
async refresh() {
|
|
651
|
-
|
|
726
|
+
await this.#readClock();
|
|
652
727
|
await Promise.all(
|
|
653
728
|
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
654
729
|
);
|
|
655
730
|
}
|
|
731
|
+
async reconcile(ref) {
|
|
732
|
+
if (ref.ledger !== "bitcoin") return;
|
|
733
|
+
const tracked = this.#refs.get(ref.address);
|
|
734
|
+
if (!tracked) return;
|
|
735
|
+
await this.#readClock();
|
|
736
|
+
await this.#reconcileRef(tracked);
|
|
737
|
+
}
|
|
738
|
+
/** Read and store the MTP clock (with fetch time, for extrapolation). */
|
|
739
|
+
async #readClock() {
|
|
740
|
+
if (!this.#chainTime) return;
|
|
741
|
+
this.#now = { mtpMs: await this.#chainTime(), fetchedAtMs: Date.now() };
|
|
742
|
+
}
|
|
656
743
|
dispose() {
|
|
657
744
|
this.#listeners.clear();
|
|
658
745
|
}
|
|
@@ -661,7 +748,10 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
661
748
|
return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
|
|
662
749
|
}
|
|
663
750
|
async #reconcileRef(ref) {
|
|
664
|
-
const facts = await this.#reader.getHtlcFacts(
|
|
751
|
+
const facts = await this.#reader.getHtlcFacts(
|
|
752
|
+
ref.address,
|
|
753
|
+
ref.minConfirmations
|
|
754
|
+
);
|
|
665
755
|
const { observation, preimage } = bitcoinObservation(
|
|
666
756
|
facts,
|
|
667
757
|
import_base5.hex.decode(ref.preimageHash),
|
|
@@ -710,14 +800,59 @@ function htlcKey(ref) {
|
|
|
710
800
|
return `arkade:${ref.script}`;
|
|
711
801
|
case "bitcoin":
|
|
712
802
|
return `bitcoin:${ref.address}`;
|
|
713
|
-
case "evm":
|
|
714
|
-
|
|
803
|
+
case "evm": {
|
|
804
|
+
const base = `evm:${ref.chainId}:${ref.htlc.toLowerCase()}:${ref.preimageHash.toLowerCase()}`;
|
|
805
|
+
if (!ref.expectedToken || !ref.sender || ref.timelockSec === void 0) {
|
|
806
|
+
return base;
|
|
807
|
+
}
|
|
808
|
+
return [
|
|
809
|
+
base,
|
|
810
|
+
ref.expectedAmount,
|
|
811
|
+
ref.expectedToken.toLowerCase(),
|
|
812
|
+
ref.sender.toLowerCase(),
|
|
813
|
+
ref.claimAddress.toLowerCase(),
|
|
814
|
+
ref.timelockSec
|
|
815
|
+
].join(":");
|
|
816
|
+
}
|
|
715
817
|
case "lightning":
|
|
716
818
|
return `lightning:${ref.paymentHash}`;
|
|
717
819
|
}
|
|
718
820
|
}
|
|
719
821
|
|
|
720
822
|
// src/contracts/evm-manager.ts
|
|
823
|
+
function htlcQueryKey(q) {
|
|
824
|
+
const base = `${q.htlc.toLowerCase()}:${q.preimageHash.toLowerCase()}`;
|
|
825
|
+
if (!q.terms || !q.claimAddress) return base;
|
|
826
|
+
const { amount, token, sender, timelockSec } = q.terms;
|
|
827
|
+
return [
|
|
828
|
+
base,
|
|
829
|
+
amount,
|
|
830
|
+
token.toLowerCase(),
|
|
831
|
+
sender.toLowerCase(),
|
|
832
|
+
q.claimAddress.toLowerCase(),
|
|
833
|
+
timelockSec
|
|
834
|
+
].join(":");
|
|
835
|
+
}
|
|
836
|
+
function refQueryKey(ref) {
|
|
837
|
+
const full = activeQuery(ref);
|
|
838
|
+
return htlcQueryKey({
|
|
839
|
+
htlc: ref.htlc,
|
|
840
|
+
preimageHash: ref.preimageHash,
|
|
841
|
+
claimAddress: ref.claimAddress,
|
|
842
|
+
terms: full && {
|
|
843
|
+
amount: full.amount,
|
|
844
|
+
token: full.token,
|
|
845
|
+
sender: full.sender,
|
|
846
|
+
timelockSec: full.timelockSec
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
var AVG_BLOCK_MS = {
|
|
851
|
+
1: 12e3,
|
|
852
|
+
137: 2100,
|
|
853
|
+
42161: 250
|
|
854
|
+
};
|
|
855
|
+
var SCAN_MARGIN_BLOCKS = 1000n;
|
|
721
856
|
var EvmContractManager = class _EvmContractManager {
|
|
722
857
|
ledger = "evm";
|
|
723
858
|
#readers;
|
|
@@ -727,14 +862,8 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
727
862
|
#obs = /* @__PURE__ */ new Map();
|
|
728
863
|
/** htlcKey → the preimage a claim revealed. */
|
|
729
864
|
#preimages = /* @__PURE__ */ new Map();
|
|
730
|
-
/** chainId → its
|
|
865
|
+
/** chainId → its last clock reading (extrapolated in {@link chainNow}). */
|
|
731
866
|
#now = /* @__PURE__ */ new Map();
|
|
732
|
-
/** chainId → unsubscribe for its (single, shared) watch. */
|
|
733
|
-
#watchUnsubs = /* @__PURE__ */ new Map();
|
|
734
|
-
/** chainId → a block-event reconcile is currently running (coalescing). */
|
|
735
|
-
#reconciling = /* @__PURE__ */ new Set();
|
|
736
|
-
/** chainId → a block event arrived mid-reconcile; run exactly one more. */
|
|
737
|
-
#reconcileAgain = /* @__PURE__ */ new Set();
|
|
738
867
|
#listeners = /* @__PURE__ */ new Set();
|
|
739
868
|
constructor(deps) {
|
|
740
869
|
this.#readers = deps.readers;
|
|
@@ -742,6 +871,9 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
742
871
|
static fromDeps(deps) {
|
|
743
872
|
return new _EvmContractManager(deps);
|
|
744
873
|
}
|
|
874
|
+
canObserve(ref) {
|
|
875
|
+
return ref.ledger === "evm" && this.#readers.has(ref.chainId);
|
|
876
|
+
}
|
|
745
877
|
async register(ref) {
|
|
746
878
|
if (ref.ledger !== "evm")
|
|
747
879
|
throw new Error(`EvmContractManager can't track a '${ref.ledger}' HTLC`);
|
|
@@ -750,8 +882,6 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
750
882
|
`no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
|
|
751
883
|
);
|
|
752
884
|
this.#refs.set(htlcKey(ref), ref);
|
|
753
|
-
this.#ensureWatch(ref.chainId);
|
|
754
|
-
await this.#reconcileChain(ref.chainId);
|
|
755
885
|
}
|
|
756
886
|
async unregister(ref) {
|
|
757
887
|
if (ref.ledger !== "evm") return;
|
|
@@ -759,91 +889,176 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
759
889
|
this.#refs.delete(key);
|
|
760
890
|
this.#obs.delete(key);
|
|
761
891
|
this.#preimages.delete(key);
|
|
762
|
-
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
|
|
763
|
-
this.#watchUnsubs.get(ref.chainId)?.();
|
|
764
|
-
this.#watchUnsubs.delete(ref.chainId);
|
|
892
|
+
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
|
|
765
893
|
this.#now.delete(ref.chainId);
|
|
766
|
-
this.#reconcileAgain.delete(ref.chainId);
|
|
767
|
-
}
|
|
768
894
|
}
|
|
769
895
|
getState(ref) {
|
|
770
896
|
return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
|
|
771
897
|
}
|
|
772
898
|
chainNow(ref) {
|
|
773
|
-
|
|
899
|
+
if (ref.ledger !== "evm") return void 0;
|
|
900
|
+
const clock = this.#now.get(ref.chainId);
|
|
901
|
+
if (!clock) return void 0;
|
|
902
|
+
return clock.blockTimeMs + Math.max(0, Date.now() - clock.fetchedAtMs);
|
|
774
903
|
}
|
|
775
904
|
onEvent(cb) {
|
|
776
905
|
this.#listeners.add(cb);
|
|
777
906
|
return () => this.#listeners.delete(cb);
|
|
778
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Full reconcile of every tracked chain — seeds clocks + observations. Called
|
|
910
|
+
* on demand (tracker start / newly tracked swap), NOT periodically: onward,
|
|
911
|
+
* hints drive targeted `reconcile`s and the tracker's at-risk safety net
|
|
912
|
+
* re-reads only swaps with client funds on the line.
|
|
913
|
+
*/
|
|
779
914
|
async refresh() {
|
|
780
|
-
const chainIds =
|
|
781
|
-
|
|
915
|
+
const chainIds = [
|
|
916
|
+
...new Set([...this.#refs.values()].map((r) => r.chainId))
|
|
917
|
+
];
|
|
918
|
+
const results = await Promise.allSettled(
|
|
919
|
+
chainIds.map((c) => this.#reconcileChain(c))
|
|
920
|
+
);
|
|
921
|
+
results.forEach((result, index) => {
|
|
922
|
+
if (result.status === "rejected")
|
|
923
|
+
console.warn(
|
|
924
|
+
`EVM reconcile failed for chain ${chainIds[index]}:`,
|
|
925
|
+
result.reason
|
|
926
|
+
);
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/** Targeted verify (hint / pre-action path) — never gated. */
|
|
930
|
+
async reconcile(ref) {
|
|
931
|
+
if (ref.ledger !== "evm") return;
|
|
932
|
+
const tracked = this.#refs.get(htlcKey(ref));
|
|
933
|
+
if (!tracked) return;
|
|
934
|
+
const reader = this.#readers.get(tracked.chainId);
|
|
935
|
+
if (!reader) return;
|
|
936
|
+
await this.#readClock(reader, tracked.chainId);
|
|
937
|
+
await this.#reconcileRefs(reader, tracked.chainId, [tracked]);
|
|
782
938
|
}
|
|
783
939
|
dispose() {
|
|
784
|
-
for (const unsub of this.#watchUnsubs.values()) unsub();
|
|
785
|
-
this.#watchUnsubs.clear();
|
|
786
|
-
this.#reconciling.clear();
|
|
787
|
-
this.#reconcileAgain.clear();
|
|
788
940
|
this.#listeners.clear();
|
|
789
941
|
}
|
|
790
942
|
/** The preimage a claim revealed on this HTLC, if one was seen. */
|
|
791
943
|
getPreimage(ref) {
|
|
792
944
|
return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
|
|
793
945
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Reconcile a chain in response to a block/log event, coalesced: at most one
|
|
805
|
-
* reconcile runs per chain at a time, with at most one queued rerun to pick up
|
|
806
|
-
* changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
|
|
807
|
-
* redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
|
|
808
|
-
* transient RPC failure in a background reconcile from becoming an unhandled
|
|
809
|
-
* rejection. `register`/`refresh` still await `#reconcileChain` directly.
|
|
810
|
-
*/
|
|
811
|
-
#scheduleReconcile(chainId) {
|
|
812
|
-
if (this.#reconciling.has(chainId)) {
|
|
813
|
-
this.#reconcileAgain.add(chainId);
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
this.#reconciling.add(chainId);
|
|
817
|
-
void this.#reconcileChain(chainId).catch((error) => {
|
|
818
|
-
console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
|
|
819
|
-
}).finally(() => {
|
|
820
|
-
this.#reconciling.delete(chainId);
|
|
821
|
-
if (this.#reconcileAgain.delete(chainId))
|
|
822
|
-
this.#scheduleReconcile(chainId);
|
|
946
|
+
/** Read and store a chain's clock (with its fetch time, for extrapolation). */
|
|
947
|
+
async #readClock(reader, chainId) {
|
|
948
|
+
const block = await reader.getLatestBlock();
|
|
949
|
+
this.#now.set(chainId, {
|
|
950
|
+
blockTimeMs: block.timeMs,
|
|
951
|
+
blockNumber: block.number,
|
|
952
|
+
fetchedAtMs: Date.now()
|
|
823
953
|
});
|
|
824
954
|
}
|
|
825
955
|
/** Refresh one chain's clock and re-observe every HTLC tracked on it. */
|
|
826
956
|
async #reconcileChain(chainId) {
|
|
827
957
|
const reader = this.#readers.get(chainId);
|
|
828
958
|
if (!reader) return;
|
|
829
|
-
this.#
|
|
959
|
+
await this.#readClock(reader, chainId);
|
|
960
|
+
const refs = [...this.#refs.values()].filter((r) => r.chainId === chainId);
|
|
961
|
+
await this.#reconcileRefs(reader, chainId, refs);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Re-observe the given refs. Two tiers:
|
|
965
|
+
*
|
|
966
|
+
* 1. Refs with the full contract tuple get the cheap `isActive` check —
|
|
967
|
+
* `true` proves open-and-on-expected-terms (the swap key hashes them) →
|
|
968
|
+
* `confirmed`, no log scan at all. This is the common case.
|
|
969
|
+
* 2. Everything else — an incomplete tuple, an inactive-and-unclassified
|
|
970
|
+
* HTLC (never funded vs claimed vs refunded), or an `isActive` transport
|
|
971
|
+
* failure (e.g. no Multicall3 on a dev chain) — is classified from one
|
|
972
|
+
* batched log read, lower-bounded by the swaps' creation-block estimate.
|
|
973
|
+
*
|
|
974
|
+
* A ref already latched on a spend is skipped entirely: terminal per leg,
|
|
975
|
+
* nothing left to learn.
|
|
976
|
+
*/
|
|
977
|
+
async #reconcileRefs(reader, chainId, refs) {
|
|
978
|
+
const open = refs.filter((r) => !this.#isSpent(r));
|
|
979
|
+
if (open.length === 0) return;
|
|
980
|
+
const fast = open.filter((r) => activeQuery(r) !== void 0);
|
|
981
|
+
let needLogs = open.filter((r) => activeQuery(r) === void 0);
|
|
982
|
+
if (fast.length > 0) {
|
|
983
|
+
try {
|
|
984
|
+
const active = await reader.isActiveBatch(
|
|
985
|
+
fast.map((r) => activeQuery(r))
|
|
986
|
+
);
|
|
987
|
+
for (const ref of fast) {
|
|
988
|
+
if (active.get(refQueryKey(ref)) === true)
|
|
989
|
+
this.#set(htlcKey(ref), "confirmed");
|
|
990
|
+
else needLogs.push(ref);
|
|
991
|
+
}
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.warn(
|
|
994
|
+
`EVM isActive check failed for chain ${chainId} \u2014 falling back to logs:`,
|
|
995
|
+
error
|
|
996
|
+
);
|
|
997
|
+
needLogs = open;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (needLogs.length === 0) return;
|
|
1001
|
+
const known = needLogs.filter((r) => r.createdAtMs !== void 0);
|
|
1002
|
+
const unknown = needLogs.filter((r) => r.createdAtMs === void 0);
|
|
830
1003
|
await Promise.all(
|
|
831
|
-
[
|
|
1004
|
+
[
|
|
1005
|
+
[known, this.#estimateFromBlock(chainId, known)],
|
|
1006
|
+
[unknown, 0n]
|
|
1007
|
+
].filter(([refs2]) => refs2.length > 0).map(
|
|
1008
|
+
([refs2, fromBlock]) => this.#classifyFromLogs(reader, refs2, fromBlock)
|
|
1009
|
+
)
|
|
832
1010
|
);
|
|
833
1011
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1012
|
+
/** Read the refs' lifecycle logs (from `fromBlock`) and set observations. */
|
|
1013
|
+
async #classifyFromLogs(reader, refs, fromBlock) {
|
|
1014
|
+
const events = await reader.getHtlcEventsBatch(
|
|
1015
|
+
refs.map((r) => {
|
|
1016
|
+
const full = activeQuery(r);
|
|
1017
|
+
return {
|
|
1018
|
+
htlc: r.htlc,
|
|
1019
|
+
preimageHash: r.preimageHash,
|
|
1020
|
+
claimAddress: r.claimAddress,
|
|
1021
|
+
terms: full && {
|
|
1022
|
+
amount: full.amount,
|
|
1023
|
+
token: full.token,
|
|
1024
|
+
sender: full.sender,
|
|
1025
|
+
timelockSec: full.timelockSec
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}),
|
|
1029
|
+
fromBlock
|
|
839
1030
|
);
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1031
|
+
for (const ref of refs) {
|
|
1032
|
+
const { observation, preimage } = evmObservation(
|
|
1033
|
+
events.get(refQueryKey(ref)) ?? [],
|
|
1034
|
+
{ amount: ref.expectedAmount, token: ref.expectedToken }
|
|
1035
|
+
);
|
|
1036
|
+
const key = htlcKey(ref);
|
|
1037
|
+
if (preimage) this.#preimages.set(key, preimage);
|
|
1038
|
+
this.#set(key, observation);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
#isSpent(ref) {
|
|
1042
|
+
const obs = this.#obs.get(htlcKey(ref));
|
|
1043
|
+
return obs === "spent_claim" || obs === "spent_refund";
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* The block to scan logs from: roughly where the OLDEST of the given swaps
|
|
1047
|
+
* was created (an HTLC has no events before its swap existed). Only a lower
|
|
1048
|
+
* bound, so estimation errs early — ×1.5 on elapsed time plus a fixed block
|
|
1049
|
+
* margin. Falls back to genesis when the creation time, clock, or the
|
|
1050
|
+
* chain's block time is unknown.
|
|
1051
|
+
*/
|
|
1052
|
+
#estimateFromBlock(chainId, refs) {
|
|
1053
|
+
const clock = this.#now.get(chainId);
|
|
1054
|
+
const avgBlockMs = AVG_BLOCK_MS[chainId];
|
|
1055
|
+
const createdAts = refs.map((r) => r.createdAtMs ?? 0);
|
|
1056
|
+
const oldestMs = Math.min(...createdAts);
|
|
1057
|
+
if (!clock || !avgBlockMs || oldestMs <= 0) return 0n;
|
|
1058
|
+
const elapsedMs = Math.max(0, clock.blockTimeMs - oldestMs);
|
|
1059
|
+
const blocksBehind = BigInt(Math.ceil(elapsedMs / avgBlockMs * 1.5)) + SCAN_MARGIN_BLOCKS;
|
|
1060
|
+
const from = clock.blockNumber - blocksBehind;
|
|
1061
|
+
return from > 0n ? from : 0n;
|
|
847
1062
|
}
|
|
848
1063
|
/** Update an observation and notify on change; never downgrade a resolved spend. */
|
|
849
1064
|
#set(key, observation) {
|
|
@@ -858,6 +1073,19 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
858
1073
|
for (const listener of this.#listeners) listener(ref, observation);
|
|
859
1074
|
}
|
|
860
1075
|
};
|
|
1076
|
+
function activeQuery(ref) {
|
|
1077
|
+
if (ref.sender === void 0 || ref.expectedToken === void 0 || ref.timelockSec === void 0)
|
|
1078
|
+
return void 0;
|
|
1079
|
+
return {
|
|
1080
|
+
htlc: ref.htlc,
|
|
1081
|
+
preimageHash: ref.preimageHash,
|
|
1082
|
+
amount: ref.expectedAmount,
|
|
1083
|
+
token: ref.expectedToken,
|
|
1084
|
+
sender: ref.sender,
|
|
1085
|
+
claimAddress: ref.claimAddress,
|
|
1086
|
+
timelockSec: ref.timelockSec
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
861
1089
|
|
|
862
1090
|
// src/contracts/evm-reader-viem.ts
|
|
863
1091
|
var import_viem = require("viem");
|
|
@@ -869,82 +1097,185 @@ var DEFAULT_EVM_RPCS = {
|
|
|
869
1097
|
"https://tenderly.rpc.polygon.community",
|
|
870
1098
|
"https://polygon-bor-rpc.publicnode.com"
|
|
871
1099
|
],
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1100
|
+
// No publicnode: it rate-limits by IP across its whole fleet (403s), and as
|
|
1101
|
+
// a PRIMARY that costs a full retry cycle per read before failing over.
|
|
1102
|
+
// Both entries verified for eth_getLogs + eth_call from a browser origin.
|
|
1103
|
+
1: ["https://eth.drpc.org", "https://rpc.mevblocker.io"],
|
|
1104
|
+
// The official gateway first; publicnode last (see above).
|
|
877
1105
|
42161: [
|
|
878
|
-
"https://arbitrum
|
|
879
|
-
"https://
|
|
880
|
-
"https://
|
|
1106
|
+
"https://arb1.arbitrum.io/rpc",
|
|
1107
|
+
"https://arbitrum.drpc.org",
|
|
1108
|
+
"https://arbitrum-one-rpc.publicnode.com"
|
|
881
1109
|
]
|
|
882
1110
|
};
|
|
883
1111
|
var SWAP_CREATED = (0, import_viem.parseAbiItem)(
|
|
884
|
-
"event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock)"
|
|
1112
|
+
"event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock, bytes32 key)"
|
|
885
1113
|
);
|
|
886
1114
|
var SWAP_REDEEMED = (0, import_viem.parseAbiItem)(
|
|
887
|
-
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
|
|
1115
|
+
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 indexed key, bytes32 preimage)"
|
|
888
1116
|
);
|
|
889
1117
|
var SWAP_REFUNDED = (0, import_viem.parseAbiItem)(
|
|
890
|
-
"event SwapRefunded(bytes32 indexed preimageHash)"
|
|
1118
|
+
"event SwapRefunded(bytes32 indexed preimageHash, bytes32 indexed key)"
|
|
1119
|
+
);
|
|
1120
|
+
var HTLC_EVENTS_ABI = [SWAP_CREATED, SWAP_REDEEMED, SWAP_REFUNDED];
|
|
1121
|
+
var HTLC_EVENT_TOPICS = HTLC_EVENTS_ABI.map(import_viem.toEventSelector);
|
|
1122
|
+
var IS_ACTIVE = (0, import_viem.parseAbiItem)(
|
|
1123
|
+
"function isActive(bytes32 preimageHash, uint256 amount, address token, address sender, address claimAddress, uint256 timelock) view returns (bool)"
|
|
1124
|
+
);
|
|
1125
|
+
var MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
1126
|
+
var AGGREGATE3 = (0, import_viem.parseAbiItem)(
|
|
1127
|
+
"function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)"
|
|
891
1128
|
);
|
|
1129
|
+
function activeKey(q) {
|
|
1130
|
+
return htlcQueryKey({
|
|
1131
|
+
htlc: q.htlc,
|
|
1132
|
+
preimageHash: q.preimageHash,
|
|
1133
|
+
claimAddress: q.claimAddress,
|
|
1134
|
+
terms: {
|
|
1135
|
+
amount: q.amount,
|
|
1136
|
+
token: q.token,
|
|
1137
|
+
sender: q.sender,
|
|
1138
|
+
timelockSec: q.timelockSec
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
892
1142
|
function evmReaderFromClient(client) {
|
|
893
1143
|
return {
|
|
894
|
-
async
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1144
|
+
async getHtlcEventsBatch(queries, fromBlock = 0n) {
|
|
1145
|
+
const results = /* @__PURE__ */ new Map();
|
|
1146
|
+
if (queries.length === 0) return results;
|
|
1147
|
+
for (const q of queries) results.set(htlcQueryKey(q), []);
|
|
1148
|
+
const logs = await client.request({
|
|
1149
|
+
method: "eth_getLogs",
|
|
1150
|
+
params: [
|
|
1151
|
+
{
|
|
1152
|
+
address: unique(queries.map((q) => q.htlc)),
|
|
1153
|
+
topics: [
|
|
1154
|
+
HTLC_EVENT_TOPICS,
|
|
1155
|
+
unique(queries.map((q) => q.preimageHash))
|
|
1156
|
+
],
|
|
1157
|
+
fromBlock: (0, import_viem.numberToHex)(fromBlock)
|
|
1158
|
+
}
|
|
1159
|
+
]
|
|
1160
|
+
});
|
|
1161
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const log of logs) {
|
|
1163
|
+
const decoded = tryDecode(log);
|
|
1164
|
+
if (!decoded) continue;
|
|
1165
|
+
const group = `${log.address.toLowerCase()}:${decoded.preimageHash.toLowerCase()}`;
|
|
1166
|
+
const existing = grouped.get(group);
|
|
1167
|
+
if (existing) existing.push(decoded);
|
|
1168
|
+
else grouped.set(group, [decoded]);
|
|
1169
|
+
}
|
|
1170
|
+
for (const query of queries) {
|
|
1171
|
+
const group = `${query.htlc.toLowerCase()}:${query.preimageHash.toLowerCase()}`;
|
|
1172
|
+
const events = results.get(htlcQueryKey(query));
|
|
1173
|
+
if (!events) continue;
|
|
1174
|
+
for (const decoded of grouped.get(group) ?? []) {
|
|
1175
|
+
const event = toHtlcEvent(decoded, query);
|
|
1176
|
+
if (event) events.push(event);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return results;
|
|
1180
|
+
},
|
|
1181
|
+
async isActiveBatch(queries) {
|
|
1182
|
+
const results = /* @__PURE__ */ new Map();
|
|
1183
|
+
if (queries.length === 0) return results;
|
|
1184
|
+
const calldatas = queries.map(
|
|
1185
|
+
(q) => (0, import_viem.encodeFunctionData)({
|
|
1186
|
+
abi: [IS_ACTIVE],
|
|
1187
|
+
args: [
|
|
1188
|
+
q.preimageHash,
|
|
1189
|
+
q.amount,
|
|
1190
|
+
q.token,
|
|
1191
|
+
q.sender,
|
|
1192
|
+
q.claimAddress,
|
|
1193
|
+
BigInt(q.timelockSec)
|
|
1194
|
+
]
|
|
915
1195
|
})
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
kind: "created",
|
|
922
|
-
amount: args?.amount ?? 0n,
|
|
923
|
-
token: args?.token ?? "0x"
|
|
1196
|
+
);
|
|
1197
|
+
const callOne = async (i) => {
|
|
1198
|
+
const { data } = await client.call({
|
|
1199
|
+
to: queries[i].htlc,
|
|
1200
|
+
data: calldatas[i]
|
|
924
1201
|
});
|
|
1202
|
+
results.set(activeKey(queries[i]), decodeIsActive(data));
|
|
1203
|
+
};
|
|
1204
|
+
if (queries.length === 1) {
|
|
1205
|
+
await callOne(0);
|
|
1206
|
+
return results;
|
|
925
1207
|
}
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
|
|
1208
|
+
try {
|
|
1209
|
+
const { data } = await client.call({
|
|
1210
|
+
to: MULTICALL3_ADDRESS,
|
|
1211
|
+
data: (0, import_viem.encodeFunctionData)({
|
|
1212
|
+
abi: [AGGREGATE3],
|
|
1213
|
+
args: [
|
|
1214
|
+
queries.map((q, i) => ({
|
|
1215
|
+
target: q.htlc,
|
|
1216
|
+
allowFailure: true,
|
|
1217
|
+
callData: calldatas[i]
|
|
1218
|
+
}))
|
|
1219
|
+
]
|
|
1220
|
+
})
|
|
1221
|
+
});
|
|
1222
|
+
if (data === void 0 || data === "0x")
|
|
1223
|
+
throw new Error("empty multicall result");
|
|
1224
|
+
const decoded = (0, import_viem.decodeFunctionResult)({
|
|
1225
|
+
abi: [AGGREGATE3],
|
|
1226
|
+
data
|
|
1227
|
+
});
|
|
1228
|
+
queries.forEach((q, i) => {
|
|
1229
|
+
const r = decoded[i];
|
|
1230
|
+
results.set(
|
|
1231
|
+
activeKey(q),
|
|
1232
|
+
r?.success === true && decodeIsActive(r.returnData)
|
|
1233
|
+
);
|
|
1234
|
+
});
|
|
1235
|
+
} catch {
|
|
1236
|
+
await Promise.all(queries.map((_, i) => callOne(i)));
|
|
929
1237
|
}
|
|
930
|
-
|
|
931
|
-
return events;
|
|
1238
|
+
return results;
|
|
932
1239
|
},
|
|
933
|
-
async
|
|
1240
|
+
async getLatestBlock() {
|
|
934
1241
|
const block = await client.getBlock();
|
|
935
|
-
return
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1242
|
+
return {
|
|
1243
|
+
timeMs: Number(block.timestamp) * 1e3,
|
|
1244
|
+
number: block.number ?? 0n
|
|
1245
|
+
};
|
|
939
1246
|
}
|
|
940
1247
|
};
|
|
941
1248
|
}
|
|
1249
|
+
function decodeIsActive(data) {
|
|
1250
|
+
if (data === void 0 || data === "0x") return false;
|
|
1251
|
+
return (0, import_viem.decodeFunctionResult)({ abi: [IS_ACTIVE], data }) === true;
|
|
1252
|
+
}
|
|
1253
|
+
function tryDecode(log) {
|
|
1254
|
+
try {
|
|
1255
|
+
const { eventName, args } = (0, import_viem.decodeEventLog)({
|
|
1256
|
+
abi: HTLC_EVENTS_ABI,
|
|
1257
|
+
data: log.data,
|
|
1258
|
+
topics: log.topics
|
|
1259
|
+
});
|
|
1260
|
+
return { eventName, ...args };
|
|
1261
|
+
} catch {
|
|
1262
|
+
return void 0;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
function toHtlcEvent(decoded, query) {
|
|
1266
|
+
if (decoded.eventName === "SwapCreated") {
|
|
1267
|
+
if (decoded.claimAddress.toLowerCase() !== query.claimAddress.toLowerCase())
|
|
1268
|
+
return void 0;
|
|
1269
|
+
return { kind: "created", amount: decoded.amount, token: decoded.token };
|
|
1270
|
+
}
|
|
1271
|
+
return decoded.eventName === "SwapRedeemed" ? { kind: "redeemed", preimage: decoded.preimage } : { kind: "refunded" };
|
|
1272
|
+
}
|
|
1273
|
+
function unique(values) {
|
|
1274
|
+
return [...new Set(values)];
|
|
1275
|
+
}
|
|
942
1276
|
function createEvmRpcReader(rpcUrls) {
|
|
943
1277
|
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
944
|
-
const transport = urls.length > 1 ? (0, import_viem.fallback)(
|
|
945
|
-
urls.map((url) => (0, import_viem.http)(url)),
|
|
946
|
-
{ rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
|
|
947
|
-
) : (0, import_viem.http)(urls[0]);
|
|
1278
|
+
const transport = urls.length > 1 ? (0, import_viem.fallback)(urls.map((url) => (0, import_viem.http)(url))) : (0, import_viem.http)(urls[0]);
|
|
948
1279
|
const client = (0, import_viem.createPublicClient)({ transport });
|
|
949
1280
|
return evmReaderFromClient(client);
|
|
950
1281
|
}
|
|
@@ -963,8 +1294,350 @@ function defaultEvmReaders(overrides) {
|
|
|
963
1294
|
return readers;
|
|
964
1295
|
}
|
|
965
1296
|
|
|
1297
|
+
// src/hints/swap-worker.ts
|
|
1298
|
+
var AUTO_EXECUTABLE = /* @__PURE__ */ new Set([
|
|
1299
|
+
"claim"
|
|
1300
|
+
]);
|
|
1301
|
+
var SwapWorker = class {
|
|
1302
|
+
#tracker;
|
|
1303
|
+
#hintSource;
|
|
1304
|
+
#execute;
|
|
1305
|
+
#onActionRequired;
|
|
1306
|
+
#maxAttempts;
|
|
1307
|
+
#baseDelayMs;
|
|
1308
|
+
#maxDelayMs;
|
|
1309
|
+
/** Swaps with an auto action in flight — guards against double-runs. */
|
|
1310
|
+
#executing = /* @__PURE__ */ new Set();
|
|
1311
|
+
/** Swaps with a scheduled auto-claim retry (backoff timer + attempt count). */
|
|
1312
|
+
#retries = /* @__PURE__ */ new Map();
|
|
1313
|
+
#unsubs = [];
|
|
1314
|
+
#started = false;
|
|
1315
|
+
constructor(options) {
|
|
1316
|
+
this.#tracker = options.tracker;
|
|
1317
|
+
this.#hintSource = options.hintSource;
|
|
1318
|
+
this.#execute = options.execute;
|
|
1319
|
+
this.#onActionRequired = options.onActionRequired;
|
|
1320
|
+
this.#maxAttempts = options.retry?.maxAttempts ?? 5;
|
|
1321
|
+
this.#baseDelayMs = options.retry?.baseDelayMs ?? 2e3;
|
|
1322
|
+
this.#maxDelayMs = options.retry?.maxDelayMs ?? 3e4;
|
|
1323
|
+
}
|
|
1324
|
+
/** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
|
|
1325
|
+
start() {
|
|
1326
|
+
if (this.#started) return;
|
|
1327
|
+
this.#started = true;
|
|
1328
|
+
this.#hintSource.subscribe(this.#tracker.trackedSwapIds());
|
|
1329
|
+
this.#unsubs.push(
|
|
1330
|
+
this.#hintSource.onStatus(({ swapId }) => {
|
|
1331
|
+
void this.#tracker.applyHint(swapId).catch((error) => {
|
|
1332
|
+
console.warn(`SwapWorker: applyHint(${swapId}) failed:`, error);
|
|
1333
|
+
});
|
|
1334
|
+
})
|
|
1335
|
+
);
|
|
1336
|
+
if (this.#hintSource.onReconnect)
|
|
1337
|
+
this.#unsubs.push(
|
|
1338
|
+
this.#hintSource.onReconnect(() => this.#reverifyAllTracked())
|
|
1339
|
+
);
|
|
1340
|
+
this.#hintSource.start();
|
|
1341
|
+
this.#unsubs.push(
|
|
1342
|
+
this.#tracker.subscribeToActions(
|
|
1343
|
+
(swapId, actions) => this.#onActions(swapId, actions)
|
|
1344
|
+
)
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
/** Stop reacting, cancel pending retries, and close the hint feed. */
|
|
1348
|
+
stop() {
|
|
1349
|
+
this.#started = false;
|
|
1350
|
+
for (const unsub of this.#unsubs) unsub();
|
|
1351
|
+
this.#unsubs = [];
|
|
1352
|
+
this.#hintSource.stop();
|
|
1353
|
+
for (const { timer } of this.#retries.values()) clearTimeout(timer);
|
|
1354
|
+
this.#retries.clear();
|
|
1355
|
+
this.#executing.clear();
|
|
1356
|
+
}
|
|
1357
|
+
/** Chain-reconcile every tracked swap (the post-outage catch-up sweep). */
|
|
1358
|
+
#reverifyAllTracked() {
|
|
1359
|
+
for (const swapId of this.#tracker.trackedSwapIds()) {
|
|
1360
|
+
void this.#tracker.applyHint(swapId).catch((error) => {
|
|
1361
|
+
console.warn(
|
|
1362
|
+
`SwapWorker: post-reconnect re-verify of ${swapId} failed:`,
|
|
1363
|
+
error
|
|
1364
|
+
);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
#onActions(swapId, actions) {
|
|
1369
|
+
const recommended = actions.recommended;
|
|
1370
|
+
if (recommended === "none") {
|
|
1371
|
+
this.#hintSource.unsubscribe([swapId]);
|
|
1372
|
+
this.#cancelRetry(swapId);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
this.#hintSource.subscribe([swapId]);
|
|
1376
|
+
if (recommended && AUTO_EXECUTABLE.has(recommended) && this.#execute) {
|
|
1377
|
+
this.#autoExecute(swapId, actions);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
this.#cancelRetry(swapId);
|
|
1381
|
+
if (!recommended) return;
|
|
1382
|
+
const automation = actions.actions.find(
|
|
1383
|
+
(a) => a.id === recommended
|
|
1384
|
+
)?.automation;
|
|
1385
|
+
if (automation === "manual" || automation === "confirm")
|
|
1386
|
+
this.#onActionRequired?.(swapId, actions);
|
|
1387
|
+
}
|
|
1388
|
+
#autoExecute(swapId, actions) {
|
|
1389
|
+
const execute = this.#execute;
|
|
1390
|
+
if (!execute) return;
|
|
1391
|
+
if (this.#executing.has(swapId)) return;
|
|
1392
|
+
const actionId = actions.recommended;
|
|
1393
|
+
if (!actionId) return;
|
|
1394
|
+
this.#executing.add(swapId);
|
|
1395
|
+
void execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
|
|
1396
|
+
console.warn(`SwapWorker: ${actionId}(${swapId}) failed:`, error);
|
|
1397
|
+
this.#scheduleRetry(swapId, actions);
|
|
1398
|
+
}).finally(() => this.#executing.delete(swapId));
|
|
1399
|
+
}
|
|
1400
|
+
/** Schedule the next backoff retry, or give up and surface once attempts run out. */
|
|
1401
|
+
#scheduleRetry(swapId, actions) {
|
|
1402
|
+
const attempts = (this.#retries.get(swapId)?.attempts ?? 0) + 1;
|
|
1403
|
+
if (attempts > this.#maxAttempts) {
|
|
1404
|
+
console.warn(
|
|
1405
|
+
`SwapWorker: auto-claim for ${swapId} gave up after ${this.#maxAttempts} attempts`
|
|
1406
|
+
);
|
|
1407
|
+
this.#cancelRetry(swapId);
|
|
1408
|
+
this.#onActionRequired?.(swapId, actions);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
this.#clearTimer(swapId);
|
|
1412
|
+
const delay = Math.min(
|
|
1413
|
+
this.#baseDelayMs * 2 ** (attempts - 1),
|
|
1414
|
+
this.#maxDelayMs
|
|
1415
|
+
);
|
|
1416
|
+
const timer = setTimeout(() => this.#retry(swapId, actions), delay);
|
|
1417
|
+
this.#retries.set(swapId, { attempts, timer });
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* A retry fires: re-verify against chain (forced re-emit), which drives
|
|
1421
|
+
* {@link #onActions} to re-run the claim only if it is still recommended. If the
|
|
1422
|
+
* re-verify itself fails we can't safely retry, so back off and try the cycle
|
|
1423
|
+
* again.
|
|
1424
|
+
*/
|
|
1425
|
+
#retry(swapId, actions) {
|
|
1426
|
+
void this.#tracker.applyHint(swapId, { force: true }).catch((error) => {
|
|
1427
|
+
console.warn(
|
|
1428
|
+
`SwapWorker: re-verify before retrying ${swapId} failed:`,
|
|
1429
|
+
error
|
|
1430
|
+
);
|
|
1431
|
+
this.#scheduleRetry(swapId, actions);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
#cancelRetry(swapId) {
|
|
1435
|
+
this.#clearTimer(swapId);
|
|
1436
|
+
this.#retries.delete(swapId);
|
|
1437
|
+
}
|
|
1438
|
+
#clearTimer(swapId) {
|
|
1439
|
+
const existing = this.#retries.get(swapId);
|
|
1440
|
+
if (existing) clearTimeout(existing.timer);
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
// src/hints/ws-status-source.ts
|
|
1445
|
+
var MAX_ARGS_PER_FRAME = 64;
|
|
1446
|
+
var MAX_SUBSCRIPTIONS = 256;
|
|
1447
|
+
var WsStatusSource = class {
|
|
1448
|
+
#url;
|
|
1449
|
+
#factory;
|
|
1450
|
+
#reconnectMinMs;
|
|
1451
|
+
#reconnectMaxMs;
|
|
1452
|
+
/**
|
|
1453
|
+
* Ids actually subscribed on the wire — held at or below the server's
|
|
1454
|
+
* per-connection cap, and resent in full on every (re)connect.
|
|
1455
|
+
*/
|
|
1456
|
+
#active = /* @__PURE__ */ new Set();
|
|
1457
|
+
/**
|
|
1458
|
+
* Ids we want but have no slot for. Promoted FIFO as slots free (the worker
|
|
1459
|
+
* unsubscribes each swap when it reaches a terminal action, so they do free).
|
|
1460
|
+
* A queued swap is not broken, only slower: it gets no hints, so it advances
|
|
1461
|
+
* on the tracker's at-risk reconciles and the worker's reconnect sweep.
|
|
1462
|
+
*/
|
|
1463
|
+
#pending = /* @__PURE__ */ new Set();
|
|
1464
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
1465
|
+
#reconnectListeners = /* @__PURE__ */ new Set();
|
|
1466
|
+
#ws;
|
|
1467
|
+
#open = false;
|
|
1468
|
+
#stopped = false;
|
|
1469
|
+
/** A connect failed or the socket dropped — the next open is a RE-connect. */
|
|
1470
|
+
#hadDisruption = false;
|
|
1471
|
+
#backoffMs;
|
|
1472
|
+
#reconnectTimer;
|
|
1473
|
+
constructor(options) {
|
|
1474
|
+
this.#url = toWsUrl(options.serverUrl);
|
|
1475
|
+
this.#factory = options.webSocketFactory ?? defaultWebSocketFactory;
|
|
1476
|
+
this.#reconnectMinMs = options.reconnectMinMs ?? 1e3;
|
|
1477
|
+
this.#reconnectMaxMs = options.reconnectMaxMs ?? 3e4;
|
|
1478
|
+
this.#backoffMs = this.#reconnectMinMs;
|
|
1479
|
+
}
|
|
1480
|
+
/** Open the connection and begin (re)subscribing. Idempotent. */
|
|
1481
|
+
start() {
|
|
1482
|
+
if (this.#ws || this.#reconnectTimer) return;
|
|
1483
|
+
this.#stopped = false;
|
|
1484
|
+
this.#connect();
|
|
1485
|
+
}
|
|
1486
|
+
/** Close the connection and stop reconnecting. */
|
|
1487
|
+
stop() {
|
|
1488
|
+
this.#stopped = true;
|
|
1489
|
+
if (this.#reconnectTimer) clearTimeout(this.#reconnectTimer);
|
|
1490
|
+
this.#reconnectTimer = void 0;
|
|
1491
|
+
this.#open = false;
|
|
1492
|
+
this.#ws?.close();
|
|
1493
|
+
this.#ws = void 0;
|
|
1494
|
+
}
|
|
1495
|
+
/** Emit for each pushed status update. Returns an unsubscribe fn. */
|
|
1496
|
+
onStatus(cb) {
|
|
1497
|
+
this.#listeners.add(cb);
|
|
1498
|
+
return () => this.#listeners.delete(cb);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Emit whenever the socket comes (back) up after a disruption — a drop, or a
|
|
1502
|
+
* first connect that had to be retried. NOT emitted on a pristine first open:
|
|
1503
|
+
* hints were never live before it, so there is no gap to recover from. While
|
|
1504
|
+
* the socket was down, pushed transitions were lost for good (the server only
|
|
1505
|
+
* snapshots on subscribe), so a consumer should re-verify its world against
|
|
1506
|
+
* the chain on this signal.
|
|
1507
|
+
*/
|
|
1508
|
+
onReconnect(cb) {
|
|
1509
|
+
this.#reconnectListeners.add(cb);
|
|
1510
|
+
return () => this.#reconnectListeners.delete(cb);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Track these swap ids — subscribe on the socket now if open, else on next open.
|
|
1514
|
+
*
|
|
1515
|
+
* Enforces the server's per-connection cap locally. The server silently stops
|
|
1516
|
+
* adding once it is full (it breaks out of the loop but still acks every id),
|
|
1517
|
+
* so sending more would leave us believing ids are subscribed that will never
|
|
1518
|
+
* receive an update. Overflow is queued instead and promoted as slots free.
|
|
1519
|
+
*/
|
|
1520
|
+
subscribe(swapIds) {
|
|
1521
|
+
const fresh = swapIds.filter(
|
|
1522
|
+
(id) => !this.#active.has(id) && !this.#pending.has(id)
|
|
1523
|
+
);
|
|
1524
|
+
if (fresh.length === 0) return;
|
|
1525
|
+
const admitted = [];
|
|
1526
|
+
for (const id of fresh) {
|
|
1527
|
+
if (this.#active.size < MAX_SUBSCRIPTIONS) {
|
|
1528
|
+
this.#active.add(id);
|
|
1529
|
+
admitted.push(id);
|
|
1530
|
+
} else {
|
|
1531
|
+
this.#pending.add(id);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (this.#pending.size > 0)
|
|
1535
|
+
console.warn(
|
|
1536
|
+
`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`
|
|
1537
|
+
);
|
|
1538
|
+
if (this.#open && admitted.length > 0)
|
|
1539
|
+
this.#sendFrame("subscribe", admitted);
|
|
1540
|
+
}
|
|
1541
|
+
/** Stop tracking these swap ids, promoting queued ones into the freed slots. */
|
|
1542
|
+
unsubscribe(swapIds) {
|
|
1543
|
+
const dropped = [];
|
|
1544
|
+
for (const id of swapIds) {
|
|
1545
|
+
if (this.#active.delete(id)) dropped.push(id);
|
|
1546
|
+
else this.#pending.delete(id);
|
|
1547
|
+
}
|
|
1548
|
+
if (this.#open && dropped.length > 0)
|
|
1549
|
+
this.#sendFrame("unsubscribe", dropped);
|
|
1550
|
+
this.#promotePending();
|
|
1551
|
+
}
|
|
1552
|
+
/** Move queued ids into any free slots, subscribing them if the socket is open. */
|
|
1553
|
+
#promotePending() {
|
|
1554
|
+
if (this.#pending.size === 0) return;
|
|
1555
|
+
const promoted = [];
|
|
1556
|
+
for (const id of this.#pending) {
|
|
1557
|
+
if (this.#active.size >= MAX_SUBSCRIPTIONS) break;
|
|
1558
|
+
this.#pending.delete(id);
|
|
1559
|
+
this.#active.add(id);
|
|
1560
|
+
promoted.push(id);
|
|
1561
|
+
}
|
|
1562
|
+
if (this.#open && promoted.length > 0)
|
|
1563
|
+
this.#sendFrame("subscribe", promoted);
|
|
1564
|
+
}
|
|
1565
|
+
#connect() {
|
|
1566
|
+
this.#reconnectTimer = void 0;
|
|
1567
|
+
const ws = this.#factory(this.#url);
|
|
1568
|
+
this.#ws = ws;
|
|
1569
|
+
ws.onopen = () => {
|
|
1570
|
+
this.#backoffMs = this.#reconnectMinMs;
|
|
1571
|
+
this.#promotePending();
|
|
1572
|
+
this.#open = true;
|
|
1573
|
+
if (this.#active.size > 0)
|
|
1574
|
+
this.#sendFrame("subscribe", [...this.#active]);
|
|
1575
|
+
if (this.#hadDisruption) {
|
|
1576
|
+
this.#hadDisruption = false;
|
|
1577
|
+
for (const cb of this.#reconnectListeners) cb();
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
ws.onmessage = (event) => this.#handleMessage(event.data);
|
|
1581
|
+
ws.onclose = () => this.#onDisconnect();
|
|
1582
|
+
ws.onerror = () => this.#ws?.close();
|
|
1583
|
+
}
|
|
1584
|
+
#onDisconnect() {
|
|
1585
|
+
this.#open = false;
|
|
1586
|
+
this.#ws = void 0;
|
|
1587
|
+
this.#hadDisruption = true;
|
|
1588
|
+
if (this.#stopped || this.#reconnectTimer) return;
|
|
1589
|
+
const delay = this.#backoffMs;
|
|
1590
|
+
this.#backoffMs = Math.min(this.#backoffMs * 2, this.#reconnectMaxMs);
|
|
1591
|
+
this.#reconnectTimer = setTimeout(() => this.#connect(), delay);
|
|
1592
|
+
}
|
|
1593
|
+
#handleMessage(data) {
|
|
1594
|
+
if (typeof data !== "string") return;
|
|
1595
|
+
let frame;
|
|
1596
|
+
try {
|
|
1597
|
+
frame = JSON.parse(data);
|
|
1598
|
+
} catch {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (!frame || typeof frame !== "object") return;
|
|
1602
|
+
const f = frame;
|
|
1603
|
+
if (f.data && typeof f.data === "object") {
|
|
1604
|
+
for (const [swapId, status] of Object.entries(
|
|
1605
|
+
f.data
|
|
1606
|
+
))
|
|
1607
|
+
this.#emit({ swapId, status });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (typeof f.message === "string")
|
|
1611
|
+
console.warn(`WsStatusSource: server error: ${f.message}`);
|
|
1612
|
+
}
|
|
1613
|
+
#emit(update) {
|
|
1614
|
+
for (const cb of this.#listeners) cb(update);
|
|
1615
|
+
}
|
|
1616
|
+
#sendFrame(op, ids) {
|
|
1617
|
+
for (let i = 0; i < ids.length; i += MAX_ARGS_PER_FRAME) {
|
|
1618
|
+
const args = ids.slice(i, i + MAX_ARGS_PER_FRAME);
|
|
1619
|
+
this.#ws?.send(JSON.stringify({ op, channel: "swap_status", args }));
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
function toWsUrl(serverUrl) {
|
|
1624
|
+
const url = new URL(serverUrl);
|
|
1625
|
+
const scheme = url.protocol === "https:" ? "wss" : "ws";
|
|
1626
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
1627
|
+
return `${scheme}://${url.host}${path}/ws`;
|
|
1628
|
+
}
|
|
1629
|
+
function defaultWebSocketFactory(url) {
|
|
1630
|
+
const WS = globalThis.WebSocket;
|
|
1631
|
+
if (!WS)
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
"no global WebSocket available \u2014 pass options.webSocketFactory (Node < 22)"
|
|
1634
|
+
);
|
|
1635
|
+
return new WS(url);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
966
1638
|
// src/contracts/arkade-vhtlc.ts
|
|
967
1639
|
var import_sdk3 = require("@arkade-os/sdk");
|
|
1640
|
+
var import_lendaswap_sdk_pure = require("@lendasat/lendaswap-sdk-pure");
|
|
968
1641
|
var import_legacy2 = require("@noble/hashes/legacy.js");
|
|
969
1642
|
var import_base6 = require("@scure/base");
|
|
970
1643
|
function strip0x(value) {
|
|
@@ -991,7 +1664,18 @@ function buildArkadeVhtlcRef(input) {
|
|
|
991
1664
|
input.unilateralRefundWithoutReceiverDelay
|
|
992
1665
|
)
|
|
993
1666
|
};
|
|
994
|
-
const vhtlc =
|
|
1667
|
+
const vhtlc = (() => {
|
|
1668
|
+
switch (input.arkadeHtlcScriptVersion ?? import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_LEGACY) {
|
|
1669
|
+
case import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_LEGACY:
|
|
1670
|
+
return new import_sdk3.VHTLC.Script(params);
|
|
1671
|
+
case import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_STRICT:
|
|
1672
|
+
return new import_lendaswap_sdk_pure.StrictVhtlcScript(params);
|
|
1673
|
+
default:
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
`Unsupported Arkade HTLC script version: ${input.arkadeHtlcScriptVersion}`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
})();
|
|
995
1679
|
return {
|
|
996
1680
|
ledger: "arkade",
|
|
997
1681
|
script: import_base6.hex.encode(vhtlc.pkScript),
|
|
@@ -1004,6 +1688,18 @@ function buildArkadeVhtlcRef(input) {
|
|
|
1004
1688
|
|
|
1005
1689
|
// src/tracker/from-swap.ts
|
|
1006
1690
|
var ms = (seconds2) => seconds2 * 1e3;
|
|
1691
|
+
var MAINNET_LOCKED_TOKEN = {
|
|
1692
|
+
1: "0x18084fba666a33d37592fa2633fd49a74dd93a88",
|
|
1693
|
+
// tBTC
|
|
1694
|
+
42161: "0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40",
|
|
1695
|
+
// tBTC
|
|
1696
|
+
137: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6"
|
|
1697
|
+
// WBTC
|
|
1698
|
+
};
|
|
1699
|
+
function lockedTokenFallback(chainId, network) {
|
|
1700
|
+
const net = network?.toLowerCase();
|
|
1701
|
+
return net === "bitcoin" || net === "mainnet" ? MAINNET_LOCKED_TOKEN[chainId] : void 0;
|
|
1702
|
+
}
|
|
1007
1703
|
var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
|
|
1008
1704
|
var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
|
|
1009
1705
|
function arkadeLeg(r, address, expectedSats) {
|
|
@@ -1017,10 +1713,12 @@ function arkadeLeg(r, address, expectedSats) {
|
|
|
1017
1713
|
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1018
1714
|
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1019
1715
|
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1020
|
-
expectedSats
|
|
1716
|
+
expectedSats,
|
|
1717
|
+
arkadeHtlcScriptVersion: r.arkade_htlc_script_version
|
|
1021
1718
|
});
|
|
1022
1719
|
}
|
|
1023
1720
|
function evmLeg(args) {
|
|
1721
|
+
const createdAtMs = args.createdAt ? Date.parse(args.createdAt) : Number.NaN;
|
|
1024
1722
|
return {
|
|
1025
1723
|
ledger: "evm",
|
|
1026
1724
|
chainId: args.chainId,
|
|
@@ -1028,15 +1726,20 @@ function evmLeg(args) {
|
|
|
1028
1726
|
preimageHash: ensure0x(args.hashLock),
|
|
1029
1727
|
claimAddress: ensure0x(args.claimAddress),
|
|
1030
1728
|
expectedAmount: BigInt(args.expectedSats),
|
|
1031
|
-
expectedToken: args.token ? ensure0x(args.token) : void 0
|
|
1729
|
+
expectedToken: args.token ? ensure0x(args.token) : void 0,
|
|
1730
|
+
sender: args.sender ? ensure0x(args.sender) : void 0,
|
|
1731
|
+
timelockSec: args.timelockSec,
|
|
1732
|
+
createdAtMs: Number.isNaN(createdAtMs) ? void 0 : createdAtMs
|
|
1032
1733
|
};
|
|
1033
1734
|
}
|
|
1034
|
-
|
|
1735
|
+
var CLIENT_FUNDING_MIN_CONFIRMATIONS = 1;
|
|
1736
|
+
function bitcoinLeg(address, hashLock, expectedSats, minConfirmations) {
|
|
1035
1737
|
return {
|
|
1036
1738
|
ledger: "bitcoin",
|
|
1037
1739
|
address,
|
|
1038
1740
|
preimageHash: strip0x2(hashLock),
|
|
1039
|
-
expectedSats
|
|
1741
|
+
expectedSats,
|
|
1742
|
+
minConfirmations
|
|
1040
1743
|
};
|
|
1041
1744
|
}
|
|
1042
1745
|
function swapToTracked(stored) {
|
|
@@ -1054,7 +1757,11 @@ function swapToTracked(stored) {
|
|
|
1054
1757
|
claimAddress: r.client_evm_address,
|
|
1055
1758
|
// the client claims the server's EVM HTLC
|
|
1056
1759
|
expectedSats: r.evm_expected_sats,
|
|
1057
|
-
token: r.wbtc_address
|
|
1760
|
+
token: r.wbtc_address,
|
|
1761
|
+
sender: r.server_evm_address,
|
|
1762
|
+
// the server funded it
|
|
1763
|
+
timelockSec: r.evm_refund_locktime,
|
|
1764
|
+
createdAt: r.created_at
|
|
1058
1765
|
}),
|
|
1059
1766
|
clientRefundLocktime: ms(r.vhtlc_refund_locktime),
|
|
1060
1767
|
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
@@ -1069,8 +1776,14 @@ function swapToTracked(stored) {
|
|
|
1069
1776
|
hashLock: r.hash_lock,
|
|
1070
1777
|
claimAddress: r.server_evm_address,
|
|
1071
1778
|
// the server claims the client's EVM HTLC
|
|
1072
|
-
expectedSats: r.evm_expected_sats
|
|
1073
|
-
// evm_to_arkade doesn't expose the locked token —
|
|
1779
|
+
expectedSats: r.evm_expected_sats,
|
|
1780
|
+
// evm_to_arkade doesn't expose the locked token — fall back to the
|
|
1781
|
+
// per-chain mainnet constant so the isActive tuple is complete.
|
|
1782
|
+
token: lockedTokenFallback(r.evm_chain_id, r.network),
|
|
1783
|
+
sender: r.client_evm_address,
|
|
1784
|
+
// the client funded it
|
|
1785
|
+
timelockSec: r.evm_refund_locktime,
|
|
1786
|
+
createdAt: r.created_at
|
|
1074
1787
|
}),
|
|
1075
1788
|
serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
|
|
1076
1789
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
@@ -1083,7 +1796,8 @@ function swapToTracked(stored) {
|
|
|
1083
1796
|
clientHtlc: bitcoinLeg(
|
|
1084
1797
|
r.btc_htlc_address,
|
|
1085
1798
|
r.evm_hash_lock,
|
|
1086
|
-
Number(r.source_amount)
|
|
1799
|
+
Number(r.source_amount),
|
|
1800
|
+
CLIENT_FUNDING_MIN_CONFIRMATIONS
|
|
1087
1801
|
),
|
|
1088
1802
|
serverHtlc: evmLeg({
|
|
1089
1803
|
chainId: r.evm_chain_id,
|
|
@@ -1092,7 +1806,11 @@ function swapToTracked(stored) {
|
|
|
1092
1806
|
claimAddress: r.client_evm_address,
|
|
1093
1807
|
// the client claims the server's EVM HTLC
|
|
1094
1808
|
expectedSats: r.evm_expected_sats,
|
|
1095
|
-
token: r.wbtc_address
|
|
1809
|
+
token: r.wbtc_address,
|
|
1810
|
+
sender: r.server_evm_address,
|
|
1811
|
+
// the server funded it
|
|
1812
|
+
timelockSec: r.evm_refund_locktime,
|
|
1813
|
+
createdAt: r.created_at
|
|
1096
1814
|
}),
|
|
1097
1815
|
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1098
1816
|
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
@@ -1108,7 +1826,11 @@ function swapToTracked(stored) {
|
|
|
1108
1826
|
claimAddress: r.server_evm_address,
|
|
1109
1827
|
// the server claims the client's EVM HTLC
|
|
1110
1828
|
expectedSats: r.evm_expected_sats,
|
|
1111
|
-
token: r.wbtc_address
|
|
1829
|
+
token: r.wbtc_address,
|
|
1830
|
+
sender: r.client_evm_address,
|
|
1831
|
+
// the client funded it
|
|
1832
|
+
timelockSec: r.evm_refund_locktime,
|
|
1833
|
+
createdAt: r.created_at
|
|
1112
1834
|
}),
|
|
1113
1835
|
serverHtlc: bitcoinLeg(
|
|
1114
1836
|
r.btc_htlc_address,
|
|
@@ -1128,7 +1850,8 @@ function swapToTracked(stored) {
|
|
|
1128
1850
|
clientHtlc: bitcoinLeg(
|
|
1129
1851
|
r.btc_htlc_address,
|
|
1130
1852
|
r.hash_lock,
|
|
1131
|
-
Number(r.source_amount)
|
|
1853
|
+
Number(r.source_amount),
|
|
1854
|
+
CLIENT_FUNDING_MIN_CONFIRMATIONS
|
|
1132
1855
|
),
|
|
1133
1856
|
serverHtlc: buildArkadeVhtlcRef({
|
|
1134
1857
|
senderPk: r.server_vhtlc_pk,
|
|
@@ -1142,7 +1865,8 @@ function swapToTracked(stored) {
|
|
|
1142
1865
|
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1143
1866
|
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1144
1867
|
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1145
|
-
expectedSats: Number(r.target_amount)
|
|
1868
|
+
expectedSats: Number(r.target_amount),
|
|
1869
|
+
arkadeHtlcScriptVersion: r.arkade_htlc_script_version
|
|
1146
1870
|
}),
|
|
1147
1871
|
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1148
1872
|
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
@@ -1176,8 +1900,14 @@ function swapToTracked(stored) {
|
|
|
1176
1900
|
hashLock: r.hash_lock,
|
|
1177
1901
|
claimAddress: r.server_evm_address,
|
|
1178
1902
|
// the server claims the client's leg
|
|
1179
|
-
expectedSats: r.evm_expected_sats
|
|
1180
|
-
// evm_to_lightning doesn't expose the locked token —
|
|
1903
|
+
expectedSats: r.evm_expected_sats,
|
|
1904
|
+
// evm_to_lightning doesn't expose the locked token — fall back to the
|
|
1905
|
+
// per-chain mainnet constant so the isActive tuple is complete.
|
|
1906
|
+
token: lockedTokenFallback(r.evm_chain_id, r.network),
|
|
1907
|
+
sender: r.client_evm_address,
|
|
1908
|
+
// the client funded it
|
|
1909
|
+
timelockSec: r.evm_refund_locktime,
|
|
1910
|
+
createdAt: r.created_at
|
|
1181
1911
|
}),
|
|
1182
1912
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1183
1913
|
serverRefundLocktime: 0
|
|
@@ -1210,7 +1940,11 @@ function swapToTracked(stored) {
|
|
|
1210
1940
|
claimAddress: r.client_evm_address,
|
|
1211
1941
|
// the client claims the server's leg
|
|
1212
1942
|
expectedSats: r.evm_expected_sats,
|
|
1213
|
-
token: r.wbtc_address
|
|
1943
|
+
token: r.wbtc_address,
|
|
1944
|
+
sender: r.server_evm_address,
|
|
1945
|
+
// the server funded it
|
|
1946
|
+
timelockSec: r.evm_refund_locktime,
|
|
1947
|
+
createdAt: r.created_at
|
|
1214
1948
|
}),
|
|
1215
1949
|
clientRefundLocktime: 0,
|
|
1216
1950
|
// no on-chain client leg
|
|
@@ -1229,15 +1963,35 @@ var SwapTracker = class {
|
|
|
1229
1963
|
#lastActions = /* @__PURE__ */ new Map();
|
|
1230
1964
|
#subscribers = /* @__PURE__ */ new Set();
|
|
1231
1965
|
#refreshIntervalMs;
|
|
1966
|
+
#atRiskReconcileIntervalMs;
|
|
1967
|
+
#lastAtRiskReconcileAt = 0;
|
|
1232
1968
|
#eventUnsubs = [];
|
|
1233
1969
|
#timer;
|
|
1234
1970
|
constructor(managers, options) {
|
|
1235
1971
|
this.#managers = managers;
|
|
1236
1972
|
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
1973
|
+
this.#atRiskReconcileIntervalMs = options?.atRiskReconcileIntervalMs ?? 6e4;
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Whether every on-chain leg of `swap` is observable — i.e. the tracker can
|
|
1977
|
+
* derive its status from chain. `false` means a leg is on a ledger/chain this
|
|
1978
|
+
* client can't reach (no manager, or an EVM chain with no RPC), so the swap
|
|
1979
|
+
* can't be tracked. Pure predicate — callers decide the policy (skip vs reject).
|
|
1980
|
+
*/
|
|
1981
|
+
canObserve(swap) {
|
|
1982
|
+
return legsOf(swap).every(
|
|
1983
|
+
(leg) => this.#managers.get(leg.ledger)?.canObserve(leg) ?? false
|
|
1984
|
+
);
|
|
1237
1985
|
}
|
|
1238
1986
|
/** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
|
|
1239
1987
|
async startTracking(swaps) {
|
|
1240
1988
|
for (const swap of swaps) {
|
|
1989
|
+
if (!this.canObserve(swap)) {
|
|
1990
|
+
console.warn(
|
|
1991
|
+
`SwapTracker: skipping swap ${swap.swapId} \u2014 a leg is on a chain this client can't reach (configure it to track this swap)`
|
|
1992
|
+
);
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1241
1995
|
this.#swaps.set(swap.swapId, swap);
|
|
1242
1996
|
for (const leg of legsOf(swap)) await this.#managerFor(leg).register(leg);
|
|
1243
1997
|
}
|
|
@@ -1246,31 +2000,140 @@ var SwapTracker = class {
|
|
|
1246
2000
|
}
|
|
1247
2001
|
await this.#refreshManagers();
|
|
1248
2002
|
this.#recomputeAll();
|
|
2003
|
+
this.#lastAtRiskReconcileAt = Date.now();
|
|
1249
2004
|
if (this.#refreshIntervalMs > 0)
|
|
1250
2005
|
this.#timer = setInterval(
|
|
1251
2006
|
() => void this.#tick(),
|
|
1252
2007
|
this.#refreshIntervalMs
|
|
1253
2008
|
);
|
|
1254
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Track one more swap after {@link startTracking} — register its on-chain
|
|
2012
|
+
* leg(s), seed the managers they touch, and derive its first action. Idempotent:
|
|
2013
|
+
* a swap already tracked, or one already seen through to a terminal action, is
|
|
2014
|
+
* ignored. New swaps are picked up by the existing per-manager event listeners
|
|
2015
|
+
* and poll timer, so nothing else needs re-wiring. Lets a swap created after
|
|
2016
|
+
* start (e.g. via `createSwap`) be tracked without a restart.
|
|
2017
|
+
*/
|
|
2018
|
+
async track(swap) {
|
|
2019
|
+
if (this.#swaps.has(swap.swapId) || this.#lastActions.has(swap.swapId))
|
|
2020
|
+
return;
|
|
2021
|
+
if (!this.canObserve(swap)) {
|
|
2022
|
+
console.warn(
|
|
2023
|
+
`SwapTracker: skipping swap ${swap.swapId} \u2014 a leg is on a chain this client can't reach (configure it to track this swap)`
|
|
2024
|
+
);
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
this.#swaps.set(swap.swapId, swap);
|
|
2028
|
+
const registered = [];
|
|
2029
|
+
try {
|
|
2030
|
+
for (const leg of legsOf(swap)) {
|
|
2031
|
+
await this.#managerFor(leg).register(leg);
|
|
2032
|
+
registered.push(leg);
|
|
2033
|
+
}
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
this.#swaps.delete(swap.swapId);
|
|
2036
|
+
for (const leg of registered) void this.#managerFor(leg).unregister(leg);
|
|
2037
|
+
throw error;
|
|
2038
|
+
}
|
|
2039
|
+
const managers = new Set(legsOf(swap).map((leg) => this.#managerFor(leg)));
|
|
2040
|
+
await Promise.all([...managers].map((manager) => manager.refresh()));
|
|
2041
|
+
this.#recompute(swap);
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* The periodic tick: recompute every swap from extrapolated clocks (pure
|
|
2045
|
+
* local work), and — at most every `atRiskReconcileIntervalMs` — re-read the
|
|
2046
|
+
* chain for at-risk swaps. This is the trust boundary: the happy path rides
|
|
2047
|
+
* server hints, but once the client's funds sit in an HTLC, refund
|
|
2048
|
+
* availability (and the counterparty leg's state) is verified against the
|
|
2049
|
+
* chain on a cadence the server can't influence.
|
|
2050
|
+
*/
|
|
1255
2051
|
async #tick() {
|
|
1256
|
-
|
|
2052
|
+
const now = Date.now();
|
|
2053
|
+
if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs) {
|
|
2054
|
+
this.#lastAtRiskReconcileAt = now;
|
|
2055
|
+
const legs = [...this.#swaps.values()].filter((swap) => this.#isAtRisk(swap)).flatMap((swap) => legsOf(swap));
|
|
2056
|
+
await Promise.all(
|
|
2057
|
+
legs.map(
|
|
2058
|
+
(leg) => this.#managerFor(leg).reconcile(leg).catch((error) => {
|
|
2059
|
+
console.warn("SwapTracker: at-risk reconcile failed:", error);
|
|
2060
|
+
})
|
|
2061
|
+
)
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
1257
2064
|
this.#recomputeAll();
|
|
1258
2065
|
}
|
|
1259
2066
|
/**
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
2067
|
+
* Whether a swap needs chain-verified monitoring independent of the server:
|
|
2068
|
+
* its client leg holds (or held) funds on-chain, so knowing when a refund
|
|
2069
|
+
* unlocks — or that the counterparty resolved a leg — must not depend on a
|
|
2070
|
+
* hint arriving. An unfunded client leg (`absent`) is server-hint territory;
|
|
2071
|
+
* an unseeded one (`undefined`, e.g. the prime failed on a flaky RPC) counts
|
|
2072
|
+
* as at-risk so the tick heals it. A swap with no on-chain client leg
|
|
2073
|
+
* (paying via Lightning) is at-risk while tracked: once the invoice is paid
|
|
2074
|
+
* there is no on-chain trace of the client's exposure, and the server leg is
|
|
2075
|
+
* the only thing left to watch.
|
|
2076
|
+
*/
|
|
2077
|
+
#isAtRisk(swap) {
|
|
2078
|
+
if (!swap.clientHtlc) return true;
|
|
2079
|
+
const obs = this.#managerFor(swap.clientHtlc).getState(swap.clientHtlc);
|
|
2080
|
+
return obs !== "absent";
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Refresh the managers that have a registered leg — seeding their clocks and
|
|
2084
|
+
* reconciling observations (the on-demand full read backing startTracking's
|
|
2085
|
+
* prime and track()'s seed). Managers with nothing tracked are skipped:
|
|
2086
|
+
* refreshing them still hits their clock source (e.g. Arkade/Bitcoin MTP via
|
|
2087
|
+
* the API), so a swap that doesn't touch a ledger can't fail on that
|
|
1264
2088
|
* ledger's endpoint being down (the default client builds every manager).
|
|
1265
2089
|
*/
|
|
1266
2090
|
async #refreshManagers() {
|
|
1267
2091
|
const active = /* @__PURE__ */ new Set();
|
|
1268
2092
|
for (const swap of this.#swaps.values())
|
|
1269
2093
|
for (const leg of legsOf(swap)) active.add(leg.ledger);
|
|
1270
|
-
const
|
|
1271
|
-
|
|
2094
|
+
const byManager = /* @__PURE__ */ new Map();
|
|
2095
|
+
for (const ledger of active) {
|
|
2096
|
+
const manager = this.#managers.get(ledger);
|
|
2097
|
+
if (manager && !byManager.has(manager)) byManager.set(manager, ledger);
|
|
2098
|
+
}
|
|
2099
|
+
const ledgers = [...byManager.values()];
|
|
2100
|
+
const results = await Promise.allSettled(
|
|
2101
|
+
[...byManager.keys()].map((manager) => manager.refresh())
|
|
1272
2102
|
);
|
|
1273
|
-
|
|
2103
|
+
results.forEach((result, index) => {
|
|
2104
|
+
if (result.status === "rejected")
|
|
2105
|
+
console.warn(
|
|
2106
|
+
`SwapTracker: refreshing ${ledgers[index]} failed:`,
|
|
2107
|
+
result.reason
|
|
2108
|
+
);
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
/** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
|
|
2112
|
+
trackedSwapIds() {
|
|
2113
|
+
return [...this.#swaps.keys()];
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Re-verify one swap against the chain right now — the hook a faster server
|
|
2117
|
+
* status hint drives. Reconciles just that swap's legs (targeted, on the
|
|
2118
|
+
* managers), then recomputes its action. The hint is only a *trigger*: the
|
|
2119
|
+
* chain stays the source of truth, so a premature hint that the chain doesn't
|
|
2120
|
+
* yet reflect changes nothing. A no-op for an untracked swap.
|
|
2121
|
+
*/
|
|
2122
|
+
async applyHint(swapId, opts) {
|
|
2123
|
+
const swap = this.#swaps.get(swapId);
|
|
2124
|
+
if (!swap) return;
|
|
2125
|
+
const legs = legsOf(swap);
|
|
2126
|
+
const results = await Promise.allSettled(
|
|
2127
|
+
legs.map((leg) => this.#managerFor(leg).reconcile(leg))
|
|
2128
|
+
);
|
|
2129
|
+
results.forEach((result, index) => {
|
|
2130
|
+
if (result.status === "rejected")
|
|
2131
|
+
console.warn(
|
|
2132
|
+
`SwapTracker: hint reconcile failed for ${legs[index].ledger}:`,
|
|
2133
|
+
result.reason
|
|
2134
|
+
);
|
|
2135
|
+
});
|
|
2136
|
+
this.#recompute(swap, opts?.force ?? false);
|
|
1274
2137
|
}
|
|
1275
2138
|
/** Notify `cb` of the current action for each tracked swap, then on every change. */
|
|
1276
2139
|
subscribeToActions(cb) {
|
|
@@ -1306,7 +2169,7 @@ var SwapTracker = class {
|
|
|
1306
2169
|
#recomputeAll() {
|
|
1307
2170
|
for (const swap of this.#swaps.values()) this.#recompute(swap);
|
|
1308
2171
|
}
|
|
1309
|
-
#recompute(swap) {
|
|
2172
|
+
#recompute(swap, force = false) {
|
|
1310
2173
|
let clientHtlc;
|
|
1311
2174
|
let serverHtlc;
|
|
1312
2175
|
let clientChainNow = 0;
|
|
@@ -1327,17 +2190,23 @@ var SwapTracker = class {
|
|
|
1327
2190
|
}
|
|
1328
2191
|
const status = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
1329
2192
|
if (status === void 0) return;
|
|
1330
|
-
const actions =
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
2193
|
+
const actions = {
|
|
2194
|
+
...deriveSwapActions({
|
|
2195
|
+
status,
|
|
2196
|
+
clientChainNow,
|
|
2197
|
+
serverChainNow,
|
|
2198
|
+
clientRefundLocktime: swap.clientRefundLocktime,
|
|
2199
|
+
serverRefundLocktime: swap.serverRefundLocktime,
|
|
2200
|
+
// Pay-on-Lightning swaps have no client-funded on-chain leg.
|
|
2201
|
+
clientFunds: swap.clientHtlc !== void 0
|
|
2202
|
+
}),
|
|
2203
|
+
// Expose the raw observations so consumers can render progress from
|
|
2204
|
+
// chain facts (which leg is funded/spent) even when the server is
|
|
2205
|
+
// stale/unreachable — without interpreting the SwapStatus enum.
|
|
2206
|
+
observations: { clientHtlc, serverHtlc }
|
|
2207
|
+
};
|
|
1339
2208
|
const previous = this.#lastActions.get(swap.swapId);
|
|
1340
|
-
if (previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
2209
|
+
if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
1341
2210
|
return;
|
|
1342
2211
|
this.#lastActions.set(swap.swapId, actions);
|
|
1343
2212
|
for (const cb of this.#subscribers) cb(swap.swapId, actions);
|
|
@@ -1364,15 +2233,19 @@ var Client = class {
|
|
|
1364
2233
|
/** Per-ledger monitors, resolved once from {@link #tracking}. */
|
|
1365
2234
|
#managers;
|
|
1366
2235
|
#tracker;
|
|
1367
|
-
|
|
2236
|
+
/** Hint-driven auto-claim worker; only built when `withAutoClaim` opted in. */
|
|
2237
|
+
#worker;
|
|
2238
|
+
#startPromise;
|
|
2239
|
+
/** Swap ids whose settled status was already re-fetched this session. */
|
|
2240
|
+
#settledSyncDone = /* @__PURE__ */ new Set();
|
|
1368
2241
|
constructor(...args) {
|
|
1369
|
-
if (args[0] instanceof
|
|
2242
|
+
if (args[0] instanceof import_lendaswap_sdk_pure2.Client) {
|
|
1370
2243
|
this.#legacy = args[0];
|
|
1371
2244
|
this.#tracking = args[1] ?? {
|
|
1372
2245
|
enabled: false
|
|
1373
2246
|
};
|
|
1374
2247
|
} else {
|
|
1375
|
-
this.#legacy = new
|
|
2248
|
+
this.#legacy = new import_lendaswap_sdk_pure2.Client(
|
|
1376
2249
|
...args
|
|
1377
2250
|
);
|
|
1378
2251
|
this.#tracking = { enabled: false };
|
|
@@ -1578,31 +2451,41 @@ var Client = class {
|
|
|
1578
2451
|
}
|
|
1579
2452
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1580
2453
|
createSwap(...args) {
|
|
1581
|
-
return this.#legacy.createSwap(...args);
|
|
2454
|
+
return this.#trackAfterCreate(this.#legacy.createSwap(...args));
|
|
1582
2455
|
}
|
|
1583
2456
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1584
2457
|
createArkadeToEvmSwapGeneric(...args) {
|
|
1585
|
-
return this.#
|
|
2458
|
+
return this.#trackAfterCreate(
|
|
2459
|
+
this.#legacy.createArkadeToEvmSwapGeneric(...args)
|
|
2460
|
+
);
|
|
1586
2461
|
}
|
|
1587
2462
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1588
2463
|
createLightningToEvmSwapGeneric(...args) {
|
|
1589
|
-
return this.#
|
|
2464
|
+
return this.#trackAfterCreate(
|
|
2465
|
+
this.#legacy.createLightningToEvmSwapGeneric(...args)
|
|
2466
|
+
);
|
|
1590
2467
|
}
|
|
1591
2468
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1592
2469
|
createBitcoinToEvmSwap(...args) {
|
|
1593
|
-
return this.#legacy.createBitcoinToEvmSwap(...args);
|
|
2470
|
+
return this.#trackAfterCreate(this.#legacy.createBitcoinToEvmSwap(...args));
|
|
1594
2471
|
}
|
|
1595
2472
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1596
2473
|
createBitcoinToArkadeSwap(...args) {
|
|
1597
|
-
return this.#
|
|
2474
|
+
return this.#trackAfterCreate(
|
|
2475
|
+
this.#legacy.createBitcoinToArkadeSwap(...args)
|
|
2476
|
+
);
|
|
1598
2477
|
}
|
|
1599
2478
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1600
2479
|
createLightningToArkadeSwap(...args) {
|
|
1601
|
-
return this.#
|
|
2480
|
+
return this.#trackAfterCreate(
|
|
2481
|
+
this.#legacy.createLightningToArkadeSwap(...args)
|
|
2482
|
+
);
|
|
1602
2483
|
}
|
|
1603
2484
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1604
2485
|
createArkadeToLightningSwap(...args) {
|
|
1605
|
-
return this.#
|
|
2486
|
+
return this.#trackAfterCreate(
|
|
2487
|
+
this.#legacy.createArkadeToLightningSwap(...args)
|
|
2488
|
+
);
|
|
1606
2489
|
}
|
|
1607
2490
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1608
2491
|
getArkadeToLightningQuote(...args) {
|
|
@@ -1610,19 +2493,25 @@ var Client = class {
|
|
|
1610
2493
|
}
|
|
1611
2494
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1612
2495
|
retryArkadeToLightningSwap(...args) {
|
|
1613
|
-
return this.#
|
|
2496
|
+
return this.#trackAfterRetry(
|
|
2497
|
+
this.#legacy.retryArkadeToLightningSwap(...args)
|
|
2498
|
+
);
|
|
1614
2499
|
}
|
|
1615
2500
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1616
2501
|
createEvmToArkadeSwapGeneric(...args) {
|
|
1617
|
-
return this.#
|
|
2502
|
+
return this.#trackAfterCreate(
|
|
2503
|
+
this.#legacy.createEvmToArkadeSwapGeneric(...args)
|
|
2504
|
+
);
|
|
1618
2505
|
}
|
|
1619
2506
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1620
2507
|
createEvmToBitcoinSwap(...args) {
|
|
1621
|
-
return this.#legacy.createEvmToBitcoinSwap(...args);
|
|
2508
|
+
return this.#trackAfterCreate(this.#legacy.createEvmToBitcoinSwap(...args));
|
|
1622
2509
|
}
|
|
1623
2510
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1624
2511
|
createEvmToLightningSwapGeneric(...args) {
|
|
1625
|
-
return this.#
|
|
2512
|
+
return this.#trackAfterCreate(
|
|
2513
|
+
this.#legacy.createEvmToLightningSwapGeneric(...args)
|
|
2514
|
+
);
|
|
1626
2515
|
}
|
|
1627
2516
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
1628
2517
|
getCoordinatorFundingCallDataPermit2(...args) {
|
|
@@ -1657,6 +2546,14 @@ var Client = class {
|
|
|
1657
2546
|
return this.#legacy.getEvmDepositorKey(...args);
|
|
1658
2547
|
}
|
|
1659
2548
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
2549
|
+
getRefundedEvmSwapContinuation(...args) {
|
|
2550
|
+
return this.#legacy.getRefundedEvmSwapContinuation(...args);
|
|
2551
|
+
}
|
|
2552
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
2553
|
+
continueRefundedEvmSwap(...args) {
|
|
2554
|
+
return this.#trackAfterRetry(this.#legacy.continueRefundedEvmSwap(...args));
|
|
2555
|
+
}
|
|
2556
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1660
2557
|
recoverGaslessFunds(...args) {
|
|
1661
2558
|
return this.#legacy.recoverGaslessFunds(...args);
|
|
1662
2559
|
}
|
|
@@ -1674,28 +2571,157 @@ var Client = class {
|
|
|
1674
2571
|
* Subscribe with {@link subscribeToActions}; release with
|
|
1675
2572
|
* {@link stopTracking}.
|
|
1676
2573
|
*/
|
|
1677
|
-
|
|
2574
|
+
startTracking() {
|
|
1678
2575
|
if (!this.#tracking.enabled)
|
|
1679
|
-
|
|
1680
|
-
|
|
2576
|
+
return Promise.reject(
|
|
2577
|
+
new Error(
|
|
2578
|
+
"tracking is disabled \u2014 remove .withoutTracking() to enable it"
|
|
2579
|
+
)
|
|
1681
2580
|
);
|
|
1682
|
-
|
|
1683
|
-
|
|
2581
|
+
this.#startPromise ??= this.#doStartTracking().catch((error) => {
|
|
2582
|
+
this.#startPromise = void 0;
|
|
2583
|
+
throw error;
|
|
2584
|
+
});
|
|
2585
|
+
return this.#startPromise;
|
|
2586
|
+
}
|
|
2587
|
+
async #doStartTracking() {
|
|
1684
2588
|
try {
|
|
1685
2589
|
const tracker = new SwapTracker(await this.#ensureManagers(), {
|
|
1686
2590
|
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
1687
2591
|
});
|
|
1688
2592
|
this.#tracker = tracker;
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2593
|
+
await tracker.startTracking(await this.#loadTrackedSwaps());
|
|
2594
|
+
tracker.subscribeToActions(
|
|
2595
|
+
(swapId, actions) => this.#syncSettledStatus(swapId, actions)
|
|
2596
|
+
);
|
|
2597
|
+
this.#startWorker(tracker);
|
|
1692
2598
|
} catch (error) {
|
|
2599
|
+
this.#worker?.stop();
|
|
2600
|
+
this.#worker = void 0;
|
|
1693
2601
|
this.#tracker?.stop();
|
|
1694
2602
|
this.#tracker = void 0;
|
|
1695
|
-
this.#started = false;
|
|
1696
2603
|
throw error;
|
|
1697
2604
|
}
|
|
1698
2605
|
}
|
|
2606
|
+
/**
|
|
2607
|
+
* The chain says this swap is finished (`none`) — if the STORED status still
|
|
2608
|
+
* reads active, re-fetch and persist it so the next session's
|
|
2609
|
+
* {@link #loadTrackedSwaps} settled-filter catches it (instead of tracking
|
|
2610
|
+
* and scanning the swap once per session forever). Fire-and-forget with a
|
|
2611
|
+
* per-session dedupe: a failed fetch just means one more one-scan round next
|
|
2612
|
+
* session, which is exactly the current behavior. A swap whose stored status
|
|
2613
|
+
* is already settled costs nothing (no server call).
|
|
2614
|
+
*/
|
|
2615
|
+
#syncSettledStatus(swapId, actions) {
|
|
2616
|
+
if (actions.recommended !== "none") return;
|
|
2617
|
+
if (this.#settledSyncDone.has(swapId)) return;
|
|
2618
|
+
this.#settledSyncDone.add(swapId);
|
|
2619
|
+
void (async () => {
|
|
2620
|
+
const stored = (await this.listAllSwaps()).find(
|
|
2621
|
+
(s) => s.response.id === swapId
|
|
2622
|
+
);
|
|
2623
|
+
if (!stored || SETTLED_STORED_STATUSES.has(stored.response.status))
|
|
2624
|
+
return;
|
|
2625
|
+
await this.getSwap(swapId, { updateStorage: true });
|
|
2626
|
+
})().catch((error) => {
|
|
2627
|
+
console.warn(
|
|
2628
|
+
`Client: refreshing settled status of swap ${swapId} failed:`,
|
|
2629
|
+
error
|
|
2630
|
+
);
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
/** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
|
|
2634
|
+
async #loadTrackedSwaps() {
|
|
2635
|
+
const swaps = await this.listAllSwaps();
|
|
2636
|
+
return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Run a create, then fold the new swap into tracking.
|
|
2640
|
+
*
|
|
2641
|
+
* When tracking is active, first REJECT a swap this client can't reach: a leg on
|
|
2642
|
+
* an unconfigured chain can be neither observed nor claimed, so creating it would
|
|
2643
|
+
* strand funds. Throwing here (before the caller funds it) is deliberately
|
|
2644
|
+
* stricter than {@link startTracking}, which only *skips* such a pre-existing
|
|
2645
|
+
* swap. Otherwise fold the new swap(s) in — fire-and-forget, so a tracking hiccup
|
|
2646
|
+
* never fails or delays the create; a no-op when tracking isn't running (a later
|
|
2647
|
+
* {@link startTracking} picks it up from storage).
|
|
2648
|
+
*/
|
|
2649
|
+
async #trackAfterCreate(op) {
|
|
2650
|
+
const result = await op;
|
|
2651
|
+
const tracker = this.#tracker;
|
|
2652
|
+
if (!tracker) return result;
|
|
2653
|
+
let swaps;
|
|
2654
|
+
try {
|
|
2655
|
+
swaps = await this.#loadTrackedSwaps();
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
console.warn("Client: loading swaps after create failed:", error);
|
|
2658
|
+
return result;
|
|
2659
|
+
}
|
|
2660
|
+
const created = swaps.find((s) => s.swapId === createdSwapId(result));
|
|
2661
|
+
if (created && !tracker.canObserve(created))
|
|
2662
|
+
throw new Error(
|
|
2663
|
+
`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.`
|
|
2664
|
+
);
|
|
2665
|
+
void this.#trackAll(tracker, swaps);
|
|
2666
|
+
return result;
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Run a retry, then fold its replacement swap into tracking.
|
|
2670
|
+
*
|
|
2671
|
+
* Unlike a create there is no unreachable-leg
|
|
2672
|
+
* rejection: by the time the retry returns, the old VHTLC has already been
|
|
2673
|
+
* collab-refunded into the new one, so refusing here can't protect any funds
|
|
2674
|
+
* — and {@link SwapTracker.track} skips a swap it can't observe anyway.
|
|
2675
|
+
*/
|
|
2676
|
+
async #trackAfterRetry(op) {
|
|
2677
|
+
const result = await op;
|
|
2678
|
+
const tracker = this.#tracker;
|
|
2679
|
+
if (!tracker) return result;
|
|
2680
|
+
try {
|
|
2681
|
+
void this.#trackAll(tracker, await this.#loadTrackedSwaps());
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
console.warn("Client: loading swaps after retry failed:", error);
|
|
2684
|
+
}
|
|
2685
|
+
return result;
|
|
2686
|
+
}
|
|
2687
|
+
/** Track each given swap the tracker isn't already watching (isolated per swap). */
|
|
2688
|
+
async #trackAll(tracker, swaps) {
|
|
2689
|
+
for (const swap of swaps) {
|
|
2690
|
+
try {
|
|
2691
|
+
await tracker.track(swap);
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
console.warn(`Client: tracking swap ${swap.swapId} failed:`, error);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Wire the server status WebSocket into a {@link SwapWorker} whenever a server
|
|
2699
|
+
* URL is configured: pushed status transitions trigger a targeted chain
|
|
2700
|
+
* re-verify (`applyHint`), which is what lets the chain pollers stay slow.
|
|
2701
|
+
* Auto-EXECUTION on top of that is opt-in: only with `withAutoClaim` does the
|
|
2702
|
+
* worker get an `execute` (claims) and an `onActionRequired` surface —
|
|
2703
|
+
* otherwise it runs observe-only, hints in, no spends.
|
|
2704
|
+
*/
|
|
2705
|
+
#startWorker(tracker) {
|
|
2706
|
+
const serverUrl = this.#tracking.serverUrl;
|
|
2707
|
+
if (!serverUrl) return;
|
|
2708
|
+
const autoClaim = this.#tracking.autoClaim;
|
|
2709
|
+
const hintSource = new WsStatusSource({ serverUrl });
|
|
2710
|
+
const worker = new SwapWorker({
|
|
2711
|
+
tracker,
|
|
2712
|
+
hintSource,
|
|
2713
|
+
execute: autoClaim ? async (swapId, actionId) => {
|
|
2714
|
+
if (actionId !== "claim")
|
|
2715
|
+
throw new Error(`refusing to auto-run action '${actionId}'`);
|
|
2716
|
+
const result = await this.claim(swapId);
|
|
2717
|
+
if (!result.success)
|
|
2718
|
+
throw new Error(result.message ?? "claim failed");
|
|
2719
|
+
} : void 0,
|
|
2720
|
+
onActionRequired: autoClaim?.onActionRequired
|
|
2721
|
+
});
|
|
2722
|
+
this.#worker = worker;
|
|
2723
|
+
worker.start();
|
|
2724
|
+
}
|
|
1699
2725
|
/**
|
|
1700
2726
|
* Subscribe to next-action updates: `cb` fires with the current action for each
|
|
1701
2727
|
* tracked swap immediately, then on every change. Call after
|
|
@@ -1708,9 +2734,11 @@ var Client = class {
|
|
|
1708
2734
|
}
|
|
1709
2735
|
/** Stop tracking and drop subscribers. The managers themselves are not disposed. */
|
|
1710
2736
|
stopTracking() {
|
|
2737
|
+
this.#worker?.stop();
|
|
2738
|
+
this.#worker = void 0;
|
|
1711
2739
|
this.#tracker?.stop();
|
|
1712
2740
|
this.#tracker = void 0;
|
|
1713
|
-
this.#
|
|
2741
|
+
this.#startPromise = void 0;
|
|
1714
2742
|
}
|
|
1715
2743
|
/**
|
|
1716
2744
|
* Resolve the per-ledger managers once — the explicit override if given, else
|
|
@@ -1725,7 +2753,7 @@ var Client = class {
|
|
|
1725
2753
|
return this.#managers;
|
|
1726
2754
|
}
|
|
1727
2755
|
const managers = /* @__PURE__ */ new Map();
|
|
1728
|
-
const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
|
|
2756
|
+
const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
|
|
1729
2757
|
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
1730
2758
|
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
1731
2759
|
if (arkadeUrl) {
|
|
@@ -1741,7 +2769,8 @@ var Client = class {
|
|
|
1741
2769
|
"bitcoin",
|
|
1742
2770
|
await BitcoinContractManager.create({
|
|
1743
2771
|
esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
|
|
1744
|
-
chainTime
|
|
2772
|
+
chainTime,
|
|
2773
|
+
minConfirmations: bitcoinMinConfirmations
|
|
1745
2774
|
})
|
|
1746
2775
|
);
|
|
1747
2776
|
this.#managers = managers;
|
|
@@ -1749,14 +2778,17 @@ var Client = class {
|
|
|
1749
2778
|
}
|
|
1750
2779
|
};
|
|
1751
2780
|
var ClientBuilder = class {
|
|
1752
|
-
#inner =
|
|
2781
|
+
#inner = import_lendaswap_sdk_pure2.Client.builder();
|
|
1753
2782
|
// Observe-mode tracking is on by default; the built client auto-builds its
|
|
1754
2783
|
// managers from config unless disabled or given an explicit override.
|
|
1755
2784
|
#trackingEnabled = true;
|
|
2785
|
+
#serverUrl;
|
|
1756
2786
|
#arkadeServerUrl;
|
|
1757
2787
|
#esploraUrl;
|
|
2788
|
+
#bitcoinMinConfirmations;
|
|
1758
2789
|
#evmRpcUrls;
|
|
1759
2790
|
#managers;
|
|
2791
|
+
#autoClaim;
|
|
1760
2792
|
/**
|
|
1761
2793
|
* Override the EVM JSON-RPC endpoint per chainId. Optional — tracking uses
|
|
1762
2794
|
* tested public defaults otherwise; an override is tried first, with the
|
|
@@ -1766,6 +2798,18 @@ var ClientBuilder = class {
|
|
|
1766
2798
|
this.#evmRpcUrls = urls;
|
|
1767
2799
|
return this;
|
|
1768
2800
|
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Opt in to hint-driven auto-claim. Tracking then also subscribes to the
|
|
2803
|
+
* server status WebSocket (a faster trigger than the chain poll) and, when the
|
|
2804
|
+
* chain confirms a swap is claimable, claims it automatically. Actions that
|
|
2805
|
+
* need the user — a manual `fund`, or a refund to confirm — are surfaced via
|
|
2806
|
+
* `onActionRequired` instead of being run. Off by default: this auto-spends on
|
|
2807
|
+
* the user's behalf, so it must be explicit.
|
|
2808
|
+
*/
|
|
2809
|
+
withAutoClaim(options) {
|
|
2810
|
+
this.#autoClaim = { onActionRequired: options?.onActionRequired };
|
|
2811
|
+
return this;
|
|
2812
|
+
}
|
|
1769
2813
|
/** Turn observe-mode tracking off. */
|
|
1770
2814
|
withoutTracking() {
|
|
1771
2815
|
this.#trackingEnabled = false;
|
|
@@ -1782,6 +2826,7 @@ var ClientBuilder = class {
|
|
|
1782
2826
|
}
|
|
1783
2827
|
withBaseUrl(...args) {
|
|
1784
2828
|
this.#inner.withBaseUrl(...args);
|
|
2829
|
+
this.#serverUrl = args[0];
|
|
1785
2830
|
return this;
|
|
1786
2831
|
}
|
|
1787
2832
|
withReferralCode(...args) {
|
|
@@ -1805,6 +2850,18 @@ var ClientBuilder = class {
|
|
|
1805
2850
|
this.#esploraUrl = url;
|
|
1806
2851
|
return this;
|
|
1807
2852
|
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Confirmations a Bitcoin funding tx needs before it observes as `confirmed`
|
|
2855
|
+
* (gating e.g. the evm→bitcoin claim). Default `0`: claim as soon as the
|
|
2856
|
+
* funding hits the mempool, which TRUSTS the funder not to double-spend it
|
|
2857
|
+
* (claiming publishes the preimage, so a funder that then replaced its
|
|
2858
|
+
* funding could take both legs). Set `1` (or more) for a block-depth policy
|
|
2859
|
+
* that doesn't rely on the funder's good behaviour.
|
|
2860
|
+
*/
|
|
2861
|
+
withBitcoinMinConfirmations(minConfirmations) {
|
|
2862
|
+
this.#bitcoinMinConfirmations = minConfirmations;
|
|
2863
|
+
return this;
|
|
2864
|
+
}
|
|
1808
2865
|
/**
|
|
1809
2866
|
* Override the Ark server URL. Optional for tracking — it otherwise defaults to
|
|
1810
2867
|
* the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
|
|
@@ -1845,13 +2902,28 @@ var ClientBuilder = class {
|
|
|
1845
2902
|
async build() {
|
|
1846
2903
|
return new Client(await this.#inner.build(), {
|
|
1847
2904
|
enabled: this.#trackingEnabled,
|
|
2905
|
+
serverUrl: this.#serverUrl,
|
|
1848
2906
|
managers: this.#managers,
|
|
1849
2907
|
arkadeServerUrl: this.#arkadeServerUrl,
|
|
1850
2908
|
esploraUrl: this.#esploraUrl,
|
|
1851
|
-
|
|
2909
|
+
bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
|
|
2910
|
+
evmRpcUrls: this.#evmRpcUrls,
|
|
2911
|
+
autoClaim: this.#autoClaim
|
|
1852
2912
|
});
|
|
1853
2913
|
}
|
|
1854
2914
|
};
|
|
2915
|
+
var SETTLED_STORED_STATUSES = /* @__PURE__ */ new Set([
|
|
2916
|
+
"serverredeemed",
|
|
2917
|
+
"clientrefunded",
|
|
2918
|
+
"clientrefundedserverfunded",
|
|
2919
|
+
"clientrefundedserverrefunded",
|
|
2920
|
+
"clientredeemedandclientrefunded",
|
|
2921
|
+
"expired"
|
|
2922
|
+
]);
|
|
2923
|
+
function createdSwapId(result) {
|
|
2924
|
+
const id = result?.response?.id;
|
|
2925
|
+
return typeof id === "string" ? id : void 0;
|
|
2926
|
+
}
|
|
1855
2927
|
|
|
1856
2928
|
// src/index.ts
|
|
1857
2929
|
init_bitcoin_reader_esplora();
|
|
@@ -1865,6 +2937,8 @@ init_bitcoin_reader_esplora();
|
|
|
1865
2937
|
DEFAULT_EVM_RPCS,
|
|
1866
2938
|
EvmContractManager,
|
|
1867
2939
|
SwapTracker,
|
|
2940
|
+
SwapWorker,
|
|
2941
|
+
WsStatusSource,
|
|
1868
2942
|
buildArkadeVhtlcRef,
|
|
1869
2943
|
createEvmRpcReader,
|
|
1870
2944
|
defaultArkadeServerUrl,
|
|
@@ -1875,6 +2949,7 @@ init_bitcoin_reader_esplora();
|
|
|
1875
2949
|
evmReaderFromClient,
|
|
1876
2950
|
htlcFactsFromEsploraTxs,
|
|
1877
2951
|
htlcKey,
|
|
2952
|
+
htlcQueryKey,
|
|
1878
2953
|
swapToTracked,
|
|
1879
2954
|
...require("@lendasat/lendaswap-sdk-pure")
|
|
1880
2955
|
});
|