@satora/swap 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +737 -208
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -87
- package/dist/index.d.ts +301 -87
- package/dist/index.js +720 -199
- 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
|
}
|
|
@@ -115,12 +131,14 @@ __export(src_exports, {
|
|
|
115
131
|
evmReaderFromClient: () => evmReaderFromClient,
|
|
116
132
|
htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
|
|
117
133
|
htlcKey: () => htlcKey,
|
|
134
|
+
htlcQueryKey: () => htlcQueryKey,
|
|
118
135
|
swapToTracked: () => swapToTracked
|
|
119
136
|
});
|
|
120
137
|
module.exports = __toCommonJS(src_exports);
|
|
121
138
|
__reExport(src_exports, require("@lendasat/lendaswap-sdk-pure"), module.exports);
|
|
122
139
|
|
|
123
140
|
// src/actions/derive.ts
|
|
141
|
+
var FUNDING_REAP_GRACE_MS = 60 * 60 * 1e3;
|
|
124
142
|
function deriveSwapActions(input) {
|
|
125
143
|
const {
|
|
126
144
|
status,
|
|
@@ -135,12 +153,23 @@ function deriveSwapActions(input) {
|
|
|
135
153
|
if (!clientFunds) {
|
|
136
154
|
const wait = {
|
|
137
155
|
id: "wait",
|
|
156
|
+
waitingOn: "client_payment",
|
|
138
157
|
recommended: true,
|
|
139
158
|
automation: "auto",
|
|
140
159
|
reason: "Waiting for the Lightning payment to be locked in."
|
|
141
160
|
};
|
|
142
161
|
return { recommended: "wait", actions: [wait] };
|
|
143
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
|
+
}
|
|
144
173
|
if (clientChainNow >= clientRefundLocktime) return { actions: [] };
|
|
145
174
|
const fund = {
|
|
146
175
|
id: "fund",
|
|
@@ -154,6 +183,7 @@ function deriveSwapActions(input) {
|
|
|
154
183
|
case "clientfundingseen": {
|
|
155
184
|
const wait = {
|
|
156
185
|
id: "wait",
|
|
186
|
+
waitingOn: "client_funding_confirmation",
|
|
157
187
|
recommended: true,
|
|
158
188
|
automation: "auto",
|
|
159
189
|
reason: "Waiting for your funding transaction to confirm."
|
|
@@ -173,6 +203,7 @@ function deriveSwapActions(input) {
|
|
|
173
203
|
}
|
|
174
204
|
const wait = {
|
|
175
205
|
id: "wait",
|
|
206
|
+
waitingOn: "server_funding",
|
|
176
207
|
recommended: true,
|
|
177
208
|
automation: "auto",
|
|
178
209
|
reason: "Waiting for the server to fund the swap."
|
|
@@ -203,6 +234,7 @@ function deriveSwapActions(input) {
|
|
|
203
234
|
if (!clientFunds) {
|
|
204
235
|
const done = {
|
|
205
236
|
id: "none",
|
|
237
|
+
outcome: "refunded",
|
|
206
238
|
recommended: true,
|
|
207
239
|
automation: "auto",
|
|
208
240
|
reason: "The claim window passed \u2014 your Lightning payment will be refunded automatically."
|
|
@@ -221,6 +253,7 @@ function deriveSwapActions(input) {
|
|
|
221
253
|
}
|
|
222
254
|
const wait = {
|
|
223
255
|
id: "wait",
|
|
256
|
+
waitingOn: "refund_timelock",
|
|
224
257
|
recommended: true,
|
|
225
258
|
automation: "auto",
|
|
226
259
|
reason: "Wait for your refund timelock \u2014 do not claim, as revealing the preimage would let the server take your deposit."
|
|
@@ -240,6 +273,7 @@ function deriveSwapActions(input) {
|
|
|
240
273
|
case "clientredeeming": {
|
|
241
274
|
const wait = {
|
|
242
275
|
id: "wait",
|
|
276
|
+
waitingOn: "claim_confirmation",
|
|
243
277
|
recommended: true,
|
|
244
278
|
automation: "auto",
|
|
245
279
|
reason: "Redeeming your funds \u2014 waiting for the claim to confirm."
|
|
@@ -250,6 +284,7 @@ function deriveSwapActions(input) {
|
|
|
250
284
|
case "serverredeemed": {
|
|
251
285
|
const done = {
|
|
252
286
|
id: "none",
|
|
287
|
+
outcome: "completed",
|
|
253
288
|
recommended: true,
|
|
254
289
|
automation: "auto",
|
|
255
290
|
reason: "Swap complete \u2014 you've received your funds."
|
|
@@ -259,6 +294,7 @@ function deriveSwapActions(input) {
|
|
|
259
294
|
case "expired": {
|
|
260
295
|
const done = {
|
|
261
296
|
id: "none",
|
|
297
|
+
outcome: "expired",
|
|
262
298
|
recommended: true,
|
|
263
299
|
automation: "auto",
|
|
264
300
|
reason: "The swap expired without funding \u2014 nothing was locked."
|
|
@@ -270,6 +306,7 @@ function deriveSwapActions(input) {
|
|
|
270
306
|
case "clientrefundedserverfunded": {
|
|
271
307
|
const done = {
|
|
272
308
|
id: "none",
|
|
309
|
+
outcome: "refunded",
|
|
273
310
|
recommended: true,
|
|
274
311
|
automation: "auto",
|
|
275
312
|
reason: "Refunded \u2014 your deposit was returned."
|
|
@@ -279,6 +316,8 @@ function deriveSwapActions(input) {
|
|
|
279
316
|
case "clientredeemedandclientrefunded": {
|
|
280
317
|
const done = {
|
|
281
318
|
id: "none",
|
|
319
|
+
// The client holds both sides' funds — "completed" from its viewpoint.
|
|
320
|
+
outcome: "completed",
|
|
282
321
|
recommended: true,
|
|
283
322
|
automation: "auto",
|
|
284
323
|
reason: "This swap ended in an anomalous state (redeemed and refunded); no action needed."
|
|
@@ -301,6 +340,7 @@ function deriveSwapActions(input) {
|
|
|
301
340
|
}
|
|
302
341
|
const wait = {
|
|
303
342
|
id: "wait",
|
|
343
|
+
waitingOn: "refund_timelock",
|
|
304
344
|
recommended: true,
|
|
305
345
|
automation: "auto",
|
|
306
346
|
reason: "The swap did not complete; waiting for the refund timelock to reclaim your deposit."
|
|
@@ -400,7 +440,7 @@ function payOnLightningStatus(serverHtlc) {
|
|
|
400
440
|
}
|
|
401
441
|
|
|
402
442
|
// src/client.ts
|
|
403
|
-
var
|
|
443
|
+
var import_lendaswap_sdk_pure2 = require("@lendasat/lendaswap-sdk-pure");
|
|
404
444
|
|
|
405
445
|
// src/contracts/arkade-manager.ts
|
|
406
446
|
var import_sdk2 = require("@arkade-os/sdk");
|
|
@@ -465,6 +505,7 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
465
505
|
/** pkScript → the verified preimage recovered from a claim spend. */
|
|
466
506
|
#preimages = /* @__PURE__ */ new Map();
|
|
467
507
|
#listeners = /* @__PURE__ */ new Set();
|
|
508
|
+
/** Last MTP reading (ms) + when it was fetched, for extrapolation. */
|
|
468
509
|
#now;
|
|
469
510
|
constructor(deps) {
|
|
470
511
|
this.#indexer = deps.indexer;
|
|
@@ -490,7 +531,6 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
490
531
|
`ArkadeContractManager can't track a '${ref.ledger}' HTLC`
|
|
491
532
|
);
|
|
492
533
|
this.#refs.set(ref.script, ref);
|
|
493
|
-
await this.#reconcileRef(ref);
|
|
494
534
|
}
|
|
495
535
|
async unregister(ref) {
|
|
496
536
|
if (ref.ledger !== "arkade") return;
|
|
@@ -502,23 +542,27 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
502
542
|
return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
|
|
503
543
|
}
|
|
504
544
|
chainNow(_ref) {
|
|
505
|
-
|
|
545
|
+
if (!this.#now) return void 0;
|
|
546
|
+
return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
|
|
506
547
|
}
|
|
507
548
|
onEvent(cb) {
|
|
508
549
|
this.#listeners.add(cb);
|
|
509
550
|
return () => this.#listeners.delete(cb);
|
|
510
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
|
+
*/
|
|
511
557
|
async refresh() {
|
|
512
|
-
|
|
513
|
-
await Promise.all(
|
|
514
|
-
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
515
|
-
);
|
|
558
|
+
await this.#scanAll();
|
|
516
559
|
}
|
|
560
|
+
/** Targeted verify (hint / pre-action path) — never gated. */
|
|
517
561
|
async reconcile(ref) {
|
|
518
562
|
if (ref.ledger !== "arkade") return;
|
|
519
563
|
const tracked = this.#refs.get(ref.script);
|
|
520
564
|
if (!tracked) return;
|
|
521
|
-
|
|
565
|
+
await this.#readClock();
|
|
522
566
|
await this.#reconcileRef(tracked);
|
|
523
567
|
}
|
|
524
568
|
dispose() {
|
|
@@ -528,6 +572,18 @@ var ArkadeContractManager = class _ArkadeContractManager {
|
|
|
528
572
|
getPreimage(ref) {
|
|
529
573
|
return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
|
|
530
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
|
+
}
|
|
531
587
|
/** Read the VHTLC's vtxos and map them to an observation. */
|
|
532
588
|
async #reconcileRef(ref) {
|
|
533
589
|
const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
|
|
@@ -620,6 +676,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
620
676
|
/** address → the preimage a claim revealed. */
|
|
621
677
|
#preimages = /* @__PURE__ */ new Map();
|
|
622
678
|
#listeners = /* @__PURE__ */ new Set();
|
|
679
|
+
/** Last MTP reading (ms) + when it was fetched, for extrapolation. */
|
|
623
680
|
#now;
|
|
624
681
|
constructor(deps) {
|
|
625
682
|
this.#reader = deps.reader;
|
|
@@ -631,7 +688,9 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
631
688
|
static async create(config) {
|
|
632
689
|
const { esploraReader: esploraReader2 } = await Promise.resolve().then(() => (init_bitcoin_reader_esplora(), bitcoin_reader_esplora_exports));
|
|
633
690
|
return _BitcoinContractManager.fromDeps({
|
|
634
|
-
reader: esploraReader2(config.esploraUrl
|
|
691
|
+
reader: esploraReader2(config.esploraUrl, void 0, {
|
|
692
|
+
minConfirmations: config.minConfirmations
|
|
693
|
+
}),
|
|
635
694
|
chainTime: config.chainTime
|
|
636
695
|
});
|
|
637
696
|
}
|
|
@@ -656,14 +715,15 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
656
715
|
return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
|
|
657
716
|
}
|
|
658
717
|
chainNow(_ref) {
|
|
659
|
-
|
|
718
|
+
if (!this.#now) return void 0;
|
|
719
|
+
return this.#now.mtpMs + Math.max(0, Date.now() - this.#now.fetchedAtMs);
|
|
660
720
|
}
|
|
661
721
|
onEvent(cb) {
|
|
662
722
|
this.#listeners.add(cb);
|
|
663
723
|
return () => this.#listeners.delete(cb);
|
|
664
724
|
}
|
|
665
725
|
async refresh() {
|
|
666
|
-
|
|
726
|
+
await this.#readClock();
|
|
667
727
|
await Promise.all(
|
|
668
728
|
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
669
729
|
);
|
|
@@ -672,9 +732,14 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
672
732
|
if (ref.ledger !== "bitcoin") return;
|
|
673
733
|
const tracked = this.#refs.get(ref.address);
|
|
674
734
|
if (!tracked) return;
|
|
675
|
-
|
|
735
|
+
await this.#readClock();
|
|
676
736
|
await this.#reconcileRef(tracked);
|
|
677
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
|
+
}
|
|
678
743
|
dispose() {
|
|
679
744
|
this.#listeners.clear();
|
|
680
745
|
}
|
|
@@ -683,7 +748,10 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
683
748
|
return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
|
|
684
749
|
}
|
|
685
750
|
async #reconcileRef(ref) {
|
|
686
|
-
const facts = await this.#reader.getHtlcFacts(
|
|
751
|
+
const facts = await this.#reader.getHtlcFacts(
|
|
752
|
+
ref.address,
|
|
753
|
+
ref.minConfirmations
|
|
754
|
+
);
|
|
687
755
|
const { observation, preimage } = bitcoinObservation(
|
|
688
756
|
facts,
|
|
689
757
|
import_base5.hex.decode(ref.preimageHash),
|
|
@@ -732,14 +800,59 @@ function htlcKey(ref) {
|
|
|
732
800
|
return `arkade:${ref.script}`;
|
|
733
801
|
case "bitcoin":
|
|
734
802
|
return `bitcoin:${ref.address}`;
|
|
735
|
-
case "evm":
|
|
736
|
-
|
|
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
|
+
}
|
|
737
817
|
case "lightning":
|
|
738
818
|
return `lightning:${ref.paymentHash}`;
|
|
739
819
|
}
|
|
740
820
|
}
|
|
741
821
|
|
|
742
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;
|
|
743
856
|
var EvmContractManager = class _EvmContractManager {
|
|
744
857
|
ledger = "evm";
|
|
745
858
|
#readers;
|
|
@@ -749,14 +862,8 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
749
862
|
#obs = /* @__PURE__ */ new Map();
|
|
750
863
|
/** htlcKey → the preimage a claim revealed. */
|
|
751
864
|
#preimages = /* @__PURE__ */ new Map();
|
|
752
|
-
/** chainId → its
|
|
865
|
+
/** chainId → its last clock reading (extrapolated in {@link chainNow}). */
|
|
753
866
|
#now = /* @__PURE__ */ new Map();
|
|
754
|
-
/** chainId → unsubscribe for its (single, shared) watch. */
|
|
755
|
-
#watchUnsubs = /* @__PURE__ */ new Map();
|
|
756
|
-
/** chainId → a block-event reconcile is currently running (coalescing). */
|
|
757
|
-
#reconciling = /* @__PURE__ */ new Set();
|
|
758
|
-
/** chainId → a block event arrived mid-reconcile; run exactly one more. */
|
|
759
|
-
#reconcileAgain = /* @__PURE__ */ new Set();
|
|
760
867
|
#listeners = /* @__PURE__ */ new Set();
|
|
761
868
|
constructor(deps) {
|
|
762
869
|
this.#readers = deps.readers;
|
|
@@ -775,8 +882,6 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
775
882
|
`no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
|
|
776
883
|
);
|
|
777
884
|
this.#refs.set(htlcKey(ref), ref);
|
|
778
|
-
this.#ensureWatch(ref.chainId);
|
|
779
|
-
await this.#reconcileChain(ref.chainId);
|
|
780
885
|
}
|
|
781
886
|
async unregister(ref) {
|
|
782
887
|
if (ref.ledger !== "evm") return;
|
|
@@ -784,100 +889,176 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
784
889
|
this.#refs.delete(key);
|
|
785
890
|
this.#obs.delete(key);
|
|
786
891
|
this.#preimages.delete(key);
|
|
787
|
-
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
|
|
788
|
-
this.#watchUnsubs.get(ref.chainId)?.();
|
|
789
|
-
this.#watchUnsubs.delete(ref.chainId);
|
|
892
|
+
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId))
|
|
790
893
|
this.#now.delete(ref.chainId);
|
|
791
|
-
this.#reconcileAgain.delete(ref.chainId);
|
|
792
|
-
}
|
|
793
894
|
}
|
|
794
895
|
getState(ref) {
|
|
795
896
|
return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
|
|
796
897
|
}
|
|
797
898
|
chainNow(ref) {
|
|
798
|
-
|
|
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);
|
|
799
903
|
}
|
|
800
904
|
onEvent(cb) {
|
|
801
905
|
this.#listeners.add(cb);
|
|
802
906
|
return () => this.#listeners.delete(cb);
|
|
803
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
|
+
*/
|
|
804
914
|
async refresh() {
|
|
805
|
-
const chainIds =
|
|
806
|
-
|
|
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
|
+
});
|
|
807
928
|
}
|
|
929
|
+
/** Targeted verify (hint / pre-action path) — never gated. */
|
|
808
930
|
async reconcile(ref) {
|
|
809
931
|
if (ref.ledger !== "evm") return;
|
|
810
932
|
const tracked = this.#refs.get(htlcKey(ref));
|
|
811
933
|
if (!tracked) return;
|
|
812
934
|
const reader = this.#readers.get(tracked.chainId);
|
|
813
935
|
if (!reader) return;
|
|
814
|
-
this.#
|
|
815
|
-
await this.#
|
|
936
|
+
await this.#readClock(reader, tracked.chainId);
|
|
937
|
+
await this.#reconcileRefs(reader, tracked.chainId, [tracked]);
|
|
816
938
|
}
|
|
817
939
|
dispose() {
|
|
818
|
-
for (const unsub of this.#watchUnsubs.values()) unsub();
|
|
819
|
-
this.#watchUnsubs.clear();
|
|
820
|
-
this.#reconciling.clear();
|
|
821
|
-
this.#reconcileAgain.clear();
|
|
822
940
|
this.#listeners.clear();
|
|
823
941
|
}
|
|
824
942
|
/** The preimage a claim revealed on this HTLC, if one was seen. */
|
|
825
943
|
getPreimage(ref) {
|
|
826
944
|
return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
|
|
827
945
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Reconcile a chain in response to a block/log event, coalesced: at most one
|
|
839
|
-
* reconcile runs per chain at a time, with at most one queued rerun to pick up
|
|
840
|
-
* changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
|
|
841
|
-
* redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
|
|
842
|
-
* transient RPC failure in a background reconcile from becoming an unhandled
|
|
843
|
-
* rejection. `register`/`refresh` still await `#reconcileChain` directly.
|
|
844
|
-
*/
|
|
845
|
-
#scheduleReconcile(chainId) {
|
|
846
|
-
if (this.#reconciling.has(chainId)) {
|
|
847
|
-
this.#reconcileAgain.add(chainId);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
this.#reconciling.add(chainId);
|
|
851
|
-
void this.#reconcileChain(chainId).catch((error) => {
|
|
852
|
-
console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
|
|
853
|
-
}).finally(() => {
|
|
854
|
-
this.#reconciling.delete(chainId);
|
|
855
|
-
if (this.#reconcileAgain.delete(chainId))
|
|
856
|
-
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()
|
|
857
953
|
});
|
|
858
954
|
}
|
|
859
955
|
/** Refresh one chain's clock and re-observe every HTLC tracked on it. */
|
|
860
956
|
async #reconcileChain(chainId) {
|
|
861
957
|
const reader = this.#readers.get(chainId);
|
|
862
958
|
if (!reader) return;
|
|
863
|
-
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);
|
|
864
1003
|
await Promise.all(
|
|
865
|
-
[
|
|
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
|
+
)
|
|
866
1010
|
);
|
|
867
1011
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
|
873
1030
|
);
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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;
|
|
881
1062
|
}
|
|
882
1063
|
/** Update an observation and notify on change; never downgrade a resolved spend. */
|
|
883
1064
|
#set(key, observation) {
|
|
@@ -892,6 +1073,19 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
892
1073
|
for (const listener of this.#listeners) listener(ref, observation);
|
|
893
1074
|
}
|
|
894
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
|
+
}
|
|
895
1089
|
|
|
896
1090
|
// src/contracts/evm-reader-viem.ts
|
|
897
1091
|
var import_viem = require("viem");
|
|
@@ -903,82 +1097,185 @@ var DEFAULT_EVM_RPCS = {
|
|
|
903
1097
|
"https://tenderly.rpc.polygon.community",
|
|
904
1098
|
"https://polygon-bor-rpc.publicnode.com"
|
|
905
1099
|
],
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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).
|
|
911
1105
|
42161: [
|
|
912
|
-
"https://arbitrum
|
|
913
|
-
"https://
|
|
914
|
-
"https://
|
|
1106
|
+
"https://arb1.arbitrum.io/rpc",
|
|
1107
|
+
"https://arbitrum.drpc.org",
|
|
1108
|
+
"https://arbitrum-one-rpc.publicnode.com"
|
|
915
1109
|
]
|
|
916
1110
|
};
|
|
917
1111
|
var SWAP_CREATED = (0, import_viem.parseAbiItem)(
|
|
918
|
-
"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)"
|
|
919
1113
|
);
|
|
920
1114
|
var SWAP_REDEEMED = (0, import_viem.parseAbiItem)(
|
|
921
|
-
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
|
|
1115
|
+
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 indexed key, bytes32 preimage)"
|
|
922
1116
|
);
|
|
923
1117
|
var SWAP_REFUNDED = (0, import_viem.parseAbiItem)(
|
|
924
|
-
"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)"
|
|
925
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)"
|
|
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
|
+
}
|
|
926
1142
|
function evmReaderFromClient(client) {
|
|
927
1143
|
return {
|
|
928
|
-
async
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
+
]
|
|
949
1195
|
})
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
kind: "created",
|
|
956
|
-
amount: args?.amount ?? 0n,
|
|
957
|
-
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]
|
|
958
1201
|
});
|
|
1202
|
+
results.set(activeKey(queries[i]), decodeIsActive(data));
|
|
1203
|
+
};
|
|
1204
|
+
if (queries.length === 1) {
|
|
1205
|
+
await callOne(0);
|
|
1206
|
+
return results;
|
|
959
1207
|
}
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
|
|
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)));
|
|
963
1237
|
}
|
|
964
|
-
|
|
965
|
-
return events;
|
|
1238
|
+
return results;
|
|
966
1239
|
},
|
|
967
|
-
async
|
|
1240
|
+
async getLatestBlock() {
|
|
968
1241
|
const block = await client.getBlock();
|
|
969
|
-
return
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1242
|
+
return {
|
|
1243
|
+
timeMs: Number(block.timestamp) * 1e3,
|
|
1244
|
+
number: block.number ?? 0n
|
|
1245
|
+
};
|
|
973
1246
|
}
|
|
974
1247
|
};
|
|
975
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
|
+
}
|
|
976
1276
|
function createEvmRpcReader(rpcUrls) {
|
|
977
1277
|
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
978
|
-
const transport = urls.length > 1 ? (0, import_viem.fallback)(
|
|
979
|
-
urls.map((url) => (0, import_viem.http)(url)),
|
|
980
|
-
{ rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
|
|
981
|
-
) : (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]);
|
|
982
1279
|
const client = (0, import_viem.createPublicClient)({ transport });
|
|
983
1280
|
return evmReaderFromClient(client);
|
|
984
1281
|
}
|
|
@@ -1036,6 +1333,10 @@ var SwapWorker = class {
|
|
|
1036
1333
|
});
|
|
1037
1334
|
})
|
|
1038
1335
|
);
|
|
1336
|
+
if (this.#hintSource.onReconnect)
|
|
1337
|
+
this.#unsubs.push(
|
|
1338
|
+
this.#hintSource.onReconnect(() => this.#reverifyAllTracked())
|
|
1339
|
+
);
|
|
1039
1340
|
this.#hintSource.start();
|
|
1040
1341
|
this.#unsubs.push(
|
|
1041
1342
|
this.#tracker.subscribeToActions(
|
|
@@ -1053,6 +1354,17 @@ var SwapWorker = class {
|
|
|
1053
1354
|
this.#retries.clear();
|
|
1054
1355
|
this.#executing.clear();
|
|
1055
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
|
+
}
|
|
1056
1368
|
#onActions(swapId, actions) {
|
|
1057
1369
|
const recommended = actions.recommended;
|
|
1058
1370
|
if (recommended === "none") {
|
|
@@ -1061,7 +1373,7 @@ var SwapWorker = class {
|
|
|
1061
1373
|
return;
|
|
1062
1374
|
}
|
|
1063
1375
|
this.#hintSource.subscribe([swapId]);
|
|
1064
|
-
if (recommended && AUTO_EXECUTABLE.has(recommended)) {
|
|
1376
|
+
if (recommended && AUTO_EXECUTABLE.has(recommended) && this.#execute) {
|
|
1065
1377
|
this.#autoExecute(swapId, actions);
|
|
1066
1378
|
return;
|
|
1067
1379
|
}
|
|
@@ -1074,11 +1386,13 @@ var SwapWorker = class {
|
|
|
1074
1386
|
this.#onActionRequired?.(swapId, actions);
|
|
1075
1387
|
}
|
|
1076
1388
|
#autoExecute(swapId, actions) {
|
|
1389
|
+
const execute = this.#execute;
|
|
1390
|
+
if (!execute) return;
|
|
1077
1391
|
if (this.#executing.has(swapId)) return;
|
|
1078
1392
|
const actionId = actions.recommended;
|
|
1079
1393
|
if (!actionId) return;
|
|
1080
1394
|
this.#executing.add(swapId);
|
|
1081
|
-
void
|
|
1395
|
+
void execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
|
|
1082
1396
|
console.warn(`SwapWorker: ${actionId}(${swapId}) failed:`, error);
|
|
1083
1397
|
this.#scheduleRetry(swapId, actions);
|
|
1084
1398
|
}).finally(() => this.#executing.delete(swapId));
|
|
@@ -1143,14 +1457,17 @@ var WsStatusSource = class {
|
|
|
1143
1457
|
/**
|
|
1144
1458
|
* Ids we want but have no slot for. Promoted FIFO as slots free (the worker
|
|
1145
1459
|
* unsubscribes each swap when it reaches a terminal action, so they do free).
|
|
1146
|
-
* A
|
|
1147
|
-
* on the tracker's
|
|
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.
|
|
1148
1462
|
*/
|
|
1149
1463
|
#pending = /* @__PURE__ */ new Set();
|
|
1150
1464
|
#listeners = /* @__PURE__ */ new Set();
|
|
1465
|
+
#reconnectListeners = /* @__PURE__ */ new Set();
|
|
1151
1466
|
#ws;
|
|
1152
1467
|
#open = false;
|
|
1153
1468
|
#stopped = false;
|
|
1469
|
+
/** A connect failed or the socket dropped — the next open is a RE-connect. */
|
|
1470
|
+
#hadDisruption = false;
|
|
1154
1471
|
#backoffMs;
|
|
1155
1472
|
#reconnectTimer;
|
|
1156
1473
|
constructor(options) {
|
|
@@ -1180,6 +1497,18 @@ var WsStatusSource = class {
|
|
|
1180
1497
|
this.#listeners.add(cb);
|
|
1181
1498
|
return () => this.#listeners.delete(cb);
|
|
1182
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
|
+
}
|
|
1183
1512
|
/**
|
|
1184
1513
|
* Track these swap ids — subscribe on the socket now if open, else on next open.
|
|
1185
1514
|
*
|
|
@@ -1243,6 +1572,10 @@ var WsStatusSource = class {
|
|
|
1243
1572
|
this.#open = true;
|
|
1244
1573
|
if (this.#active.size > 0)
|
|
1245
1574
|
this.#sendFrame("subscribe", [...this.#active]);
|
|
1575
|
+
if (this.#hadDisruption) {
|
|
1576
|
+
this.#hadDisruption = false;
|
|
1577
|
+
for (const cb of this.#reconnectListeners) cb();
|
|
1578
|
+
}
|
|
1246
1579
|
};
|
|
1247
1580
|
ws.onmessage = (event) => this.#handleMessage(event.data);
|
|
1248
1581
|
ws.onclose = () => this.#onDisconnect();
|
|
@@ -1251,6 +1584,7 @@ var WsStatusSource = class {
|
|
|
1251
1584
|
#onDisconnect() {
|
|
1252
1585
|
this.#open = false;
|
|
1253
1586
|
this.#ws = void 0;
|
|
1587
|
+
this.#hadDisruption = true;
|
|
1254
1588
|
if (this.#stopped || this.#reconnectTimer) return;
|
|
1255
1589
|
const delay = this.#backoffMs;
|
|
1256
1590
|
this.#backoffMs = Math.min(this.#backoffMs * 2, this.#reconnectMaxMs);
|
|
@@ -1303,6 +1637,7 @@ function defaultWebSocketFactory(url) {
|
|
|
1303
1637
|
|
|
1304
1638
|
// src/contracts/arkade-vhtlc.ts
|
|
1305
1639
|
var import_sdk3 = require("@arkade-os/sdk");
|
|
1640
|
+
var import_lendaswap_sdk_pure = require("@lendasat/lendaswap-sdk-pure");
|
|
1306
1641
|
var import_legacy2 = require("@noble/hashes/legacy.js");
|
|
1307
1642
|
var import_base6 = require("@scure/base");
|
|
1308
1643
|
function strip0x(value) {
|
|
@@ -1329,7 +1664,18 @@ function buildArkadeVhtlcRef(input) {
|
|
|
1329
1664
|
input.unilateralRefundWithoutReceiverDelay
|
|
1330
1665
|
)
|
|
1331
1666
|
};
|
|
1332
|
-
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
|
+
})();
|
|
1333
1679
|
return {
|
|
1334
1680
|
ledger: "arkade",
|
|
1335
1681
|
script: import_base6.hex.encode(vhtlc.pkScript),
|
|
@@ -1342,6 +1688,18 @@ function buildArkadeVhtlcRef(input) {
|
|
|
1342
1688
|
|
|
1343
1689
|
// src/tracker/from-swap.ts
|
|
1344
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
|
+
}
|
|
1345
1703
|
var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
|
|
1346
1704
|
var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
|
|
1347
1705
|
function arkadeLeg(r, address, expectedSats) {
|
|
@@ -1355,10 +1713,12 @@ function arkadeLeg(r, address, expectedSats) {
|
|
|
1355
1713
|
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1356
1714
|
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1357
1715
|
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1358
|
-
expectedSats
|
|
1716
|
+
expectedSats,
|
|
1717
|
+
arkadeHtlcScriptVersion: r.arkade_htlc_script_version
|
|
1359
1718
|
});
|
|
1360
1719
|
}
|
|
1361
1720
|
function evmLeg(args) {
|
|
1721
|
+
const createdAtMs = args.createdAt ? Date.parse(args.createdAt) : Number.NaN;
|
|
1362
1722
|
return {
|
|
1363
1723
|
ledger: "evm",
|
|
1364
1724
|
chainId: args.chainId,
|
|
@@ -1366,15 +1726,20 @@ function evmLeg(args) {
|
|
|
1366
1726
|
preimageHash: ensure0x(args.hashLock),
|
|
1367
1727
|
claimAddress: ensure0x(args.claimAddress),
|
|
1368
1728
|
expectedAmount: BigInt(args.expectedSats),
|
|
1369
|
-
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
|
|
1370
1733
|
};
|
|
1371
1734
|
}
|
|
1372
|
-
|
|
1735
|
+
var CLIENT_FUNDING_MIN_CONFIRMATIONS = 1;
|
|
1736
|
+
function bitcoinLeg(address, hashLock, expectedSats, minConfirmations) {
|
|
1373
1737
|
return {
|
|
1374
1738
|
ledger: "bitcoin",
|
|
1375
1739
|
address,
|
|
1376
1740
|
preimageHash: strip0x2(hashLock),
|
|
1377
|
-
expectedSats
|
|
1741
|
+
expectedSats,
|
|
1742
|
+
minConfirmations
|
|
1378
1743
|
};
|
|
1379
1744
|
}
|
|
1380
1745
|
function swapToTracked(stored) {
|
|
@@ -1392,7 +1757,11 @@ function swapToTracked(stored) {
|
|
|
1392
1757
|
claimAddress: r.client_evm_address,
|
|
1393
1758
|
// the client claims the server's EVM HTLC
|
|
1394
1759
|
expectedSats: r.evm_expected_sats,
|
|
1395
|
-
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
|
|
1396
1765
|
}),
|
|
1397
1766
|
clientRefundLocktime: ms(r.vhtlc_refund_locktime),
|
|
1398
1767
|
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
@@ -1407,8 +1776,14 @@ function swapToTracked(stored) {
|
|
|
1407
1776
|
hashLock: r.hash_lock,
|
|
1408
1777
|
claimAddress: r.server_evm_address,
|
|
1409
1778
|
// the server claims the client's EVM HTLC
|
|
1410
|
-
expectedSats: r.evm_expected_sats
|
|
1411
|
-
// 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
|
|
1412
1787
|
}),
|
|
1413
1788
|
serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
|
|
1414
1789
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
@@ -1421,7 +1796,8 @@ function swapToTracked(stored) {
|
|
|
1421
1796
|
clientHtlc: bitcoinLeg(
|
|
1422
1797
|
r.btc_htlc_address,
|
|
1423
1798
|
r.evm_hash_lock,
|
|
1424
|
-
Number(r.source_amount)
|
|
1799
|
+
Number(r.source_amount),
|
|
1800
|
+
CLIENT_FUNDING_MIN_CONFIRMATIONS
|
|
1425
1801
|
),
|
|
1426
1802
|
serverHtlc: evmLeg({
|
|
1427
1803
|
chainId: r.evm_chain_id,
|
|
@@ -1430,7 +1806,11 @@ function swapToTracked(stored) {
|
|
|
1430
1806
|
claimAddress: r.client_evm_address,
|
|
1431
1807
|
// the client claims the server's EVM HTLC
|
|
1432
1808
|
expectedSats: r.evm_expected_sats,
|
|
1433
|
-
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
|
|
1434
1814
|
}),
|
|
1435
1815
|
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1436
1816
|
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
@@ -1446,7 +1826,11 @@ function swapToTracked(stored) {
|
|
|
1446
1826
|
claimAddress: r.server_evm_address,
|
|
1447
1827
|
// the server claims the client's EVM HTLC
|
|
1448
1828
|
expectedSats: r.evm_expected_sats,
|
|
1449
|
-
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
|
|
1450
1834
|
}),
|
|
1451
1835
|
serverHtlc: bitcoinLeg(
|
|
1452
1836
|
r.btc_htlc_address,
|
|
@@ -1466,7 +1850,8 @@ function swapToTracked(stored) {
|
|
|
1466
1850
|
clientHtlc: bitcoinLeg(
|
|
1467
1851
|
r.btc_htlc_address,
|
|
1468
1852
|
r.hash_lock,
|
|
1469
|
-
Number(r.source_amount)
|
|
1853
|
+
Number(r.source_amount),
|
|
1854
|
+
CLIENT_FUNDING_MIN_CONFIRMATIONS
|
|
1470
1855
|
),
|
|
1471
1856
|
serverHtlc: buildArkadeVhtlcRef({
|
|
1472
1857
|
senderPk: r.server_vhtlc_pk,
|
|
@@ -1480,7 +1865,8 @@ function swapToTracked(stored) {
|
|
|
1480
1865
|
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1481
1866
|
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1482
1867
|
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1483
|
-
expectedSats: Number(r.target_amount)
|
|
1868
|
+
expectedSats: Number(r.target_amount),
|
|
1869
|
+
arkadeHtlcScriptVersion: r.arkade_htlc_script_version
|
|
1484
1870
|
}),
|
|
1485
1871
|
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1486
1872
|
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
@@ -1514,8 +1900,14 @@ function swapToTracked(stored) {
|
|
|
1514
1900
|
hashLock: r.hash_lock,
|
|
1515
1901
|
claimAddress: r.server_evm_address,
|
|
1516
1902
|
// the server claims the client's leg
|
|
1517
|
-
expectedSats: r.evm_expected_sats
|
|
1518
|
-
// 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
|
|
1519
1911
|
}),
|
|
1520
1912
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1521
1913
|
serverRefundLocktime: 0
|
|
@@ -1548,7 +1940,11 @@ function swapToTracked(stored) {
|
|
|
1548
1940
|
claimAddress: r.client_evm_address,
|
|
1549
1941
|
// the client claims the server's leg
|
|
1550
1942
|
expectedSats: r.evm_expected_sats,
|
|
1551
|
-
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
|
|
1552
1948
|
}),
|
|
1553
1949
|
clientRefundLocktime: 0,
|
|
1554
1950
|
// no on-chain client leg
|
|
@@ -1567,11 +1963,14 @@ var SwapTracker = class {
|
|
|
1567
1963
|
#lastActions = /* @__PURE__ */ new Map();
|
|
1568
1964
|
#subscribers = /* @__PURE__ */ new Set();
|
|
1569
1965
|
#refreshIntervalMs;
|
|
1966
|
+
#atRiskReconcileIntervalMs;
|
|
1967
|
+
#lastAtRiskReconcileAt = 0;
|
|
1570
1968
|
#eventUnsubs = [];
|
|
1571
1969
|
#timer;
|
|
1572
1970
|
constructor(managers, options) {
|
|
1573
1971
|
this.#managers = managers;
|
|
1574
1972
|
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
1973
|
+
this.#atRiskReconcileIntervalMs = options?.atRiskReconcileIntervalMs ?? 6e4;
|
|
1575
1974
|
}
|
|
1576
1975
|
/**
|
|
1577
1976
|
* Whether every on-chain leg of `swap` is observable — i.e. the tracker can
|
|
@@ -1601,6 +2000,7 @@ var SwapTracker = class {
|
|
|
1601
2000
|
}
|
|
1602
2001
|
await this.#refreshManagers();
|
|
1603
2002
|
this.#recomputeAll();
|
|
2003
|
+
this.#lastAtRiskReconcileAt = Date.now();
|
|
1604
2004
|
if (this.#refreshIntervalMs > 0)
|
|
1605
2005
|
this.#timer = setInterval(
|
|
1606
2006
|
() => void this.#tick(),
|
|
@@ -1640,25 +2040,73 @@ var SwapTracker = class {
|
|
|
1640
2040
|
await Promise.all([...managers].map((manager) => manager.refresh()));
|
|
1641
2041
|
this.#recompute(swap);
|
|
1642
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
|
+
*/
|
|
1643
2051
|
async #tick() {
|
|
1644
|
-
|
|
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
|
+
}
|
|
1645
2064
|
this.#recomputeAll();
|
|
1646
2065
|
}
|
|
1647
2066
|
/**
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
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
|
|
1652
2088
|
* ledger's endpoint being down (the default client builds every manager).
|
|
1653
2089
|
*/
|
|
1654
2090
|
async #refreshManagers() {
|
|
1655
2091
|
const active = /* @__PURE__ */ new Set();
|
|
1656
2092
|
for (const swap of this.#swaps.values())
|
|
1657
2093
|
for (const leg of legsOf(swap)) active.add(leg.ledger);
|
|
1658
|
-
const
|
|
1659
|
-
|
|
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())
|
|
1660
2102
|
);
|
|
1661
|
-
|
|
2103
|
+
results.forEach((result, index) => {
|
|
2104
|
+
if (result.status === "rejected")
|
|
2105
|
+
console.warn(
|
|
2106
|
+
`SwapTracker: refreshing ${ledgers[index]} failed:`,
|
|
2107
|
+
result.reason
|
|
2108
|
+
);
|
|
2109
|
+
});
|
|
1662
2110
|
}
|
|
1663
2111
|
/** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
|
|
1664
2112
|
trackedSwapIds() {
|
|
@@ -1674,9 +2122,17 @@ var SwapTracker = class {
|
|
|
1674
2122
|
async applyHint(swapId, opts) {
|
|
1675
2123
|
const swap = this.#swaps.get(swapId);
|
|
1676
2124
|
if (!swap) return;
|
|
1677
|
-
|
|
1678
|
-
|
|
2125
|
+
const legs = legsOf(swap);
|
|
2126
|
+
const results = await Promise.allSettled(
|
|
2127
|
+
legs.map((leg) => this.#managerFor(leg).reconcile(leg))
|
|
1679
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
|
+
});
|
|
1680
2136
|
this.#recompute(swap, opts?.force ?? false);
|
|
1681
2137
|
}
|
|
1682
2138
|
/** Notify `cb` of the current action for each tracked swap, then on every change. */
|
|
@@ -1734,15 +2190,21 @@ var SwapTracker = class {
|
|
|
1734
2190
|
}
|
|
1735
2191
|
const status = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
1736
2192
|
if (status === void 0) return;
|
|
1737
|
-
const actions =
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
+
};
|
|
1746
2208
|
const previous = this.#lastActions.get(swap.swapId);
|
|
1747
2209
|
if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
1748
2210
|
return;
|
|
@@ -1773,15 +2235,17 @@ var Client = class {
|
|
|
1773
2235
|
#tracker;
|
|
1774
2236
|
/** Hint-driven auto-claim worker; only built when `withAutoClaim` opted in. */
|
|
1775
2237
|
#worker;
|
|
1776
|
-
#
|
|
2238
|
+
#startPromise;
|
|
2239
|
+
/** Swap ids whose settled status was already re-fetched this session. */
|
|
2240
|
+
#settledSyncDone = /* @__PURE__ */ new Set();
|
|
1777
2241
|
constructor(...args) {
|
|
1778
|
-
if (args[0] instanceof
|
|
2242
|
+
if (args[0] instanceof import_lendaswap_sdk_pure2.Client) {
|
|
1779
2243
|
this.#legacy = args[0];
|
|
1780
2244
|
this.#tracking = args[1] ?? {
|
|
1781
2245
|
enabled: false
|
|
1782
2246
|
};
|
|
1783
2247
|
} else {
|
|
1784
|
-
this.#legacy = new
|
|
2248
|
+
this.#legacy = new import_lendaswap_sdk_pure2.Client(
|
|
1785
2249
|
...args
|
|
1786
2250
|
);
|
|
1787
2251
|
this.#tracking = { enabled: false };
|
|
@@ -2107,33 +2571,69 @@ var Client = class {
|
|
|
2107
2571
|
* Subscribe with {@link subscribeToActions}; release with
|
|
2108
2572
|
* {@link stopTracking}.
|
|
2109
2573
|
*/
|
|
2110
|
-
|
|
2574
|
+
startTracking() {
|
|
2111
2575
|
if (!this.#tracking.enabled)
|
|
2112
|
-
|
|
2113
|
-
|
|
2576
|
+
return Promise.reject(
|
|
2577
|
+
new Error(
|
|
2578
|
+
"tracking is disabled \u2014 remove .withoutTracking() to enable it"
|
|
2579
|
+
)
|
|
2114
2580
|
);
|
|
2115
|
-
|
|
2116
|
-
|
|
2581
|
+
this.#startPromise ??= this.#doStartTracking().catch((error) => {
|
|
2582
|
+
this.#startPromise = void 0;
|
|
2583
|
+
throw error;
|
|
2584
|
+
});
|
|
2585
|
+
return this.#startPromise;
|
|
2586
|
+
}
|
|
2587
|
+
async #doStartTracking() {
|
|
2117
2588
|
try {
|
|
2118
2589
|
const tracker = new SwapTracker(await this.#ensureManagers(), {
|
|
2119
2590
|
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
2120
2591
|
});
|
|
2121
2592
|
this.#tracker = tracker;
|
|
2122
2593
|
await tracker.startTracking(await this.#loadTrackedSwaps());
|
|
2594
|
+
tracker.subscribeToActions(
|
|
2595
|
+
(swapId, actions) => this.#syncSettledStatus(swapId, actions)
|
|
2596
|
+
);
|
|
2123
2597
|
this.#startWorker(tracker);
|
|
2124
2598
|
} catch (error) {
|
|
2125
2599
|
this.#worker?.stop();
|
|
2126
2600
|
this.#worker = void 0;
|
|
2127
2601
|
this.#tracker?.stop();
|
|
2128
2602
|
this.#tracker = void 0;
|
|
2129
|
-
this.#started = false;
|
|
2130
2603
|
throw error;
|
|
2131
2604
|
}
|
|
2132
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
|
+
}
|
|
2133
2633
|
/** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
|
|
2134
2634
|
async #loadTrackedSwaps() {
|
|
2135
2635
|
const swaps = await this.listAllSwaps();
|
|
2136
|
-
return swaps.map(swapToTracked).filter((s) => s !== void 0);
|
|
2636
|
+
return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
|
|
2137
2637
|
}
|
|
2138
2638
|
/**
|
|
2139
2639
|
* Run a create, then fold the new swap into tracking.
|
|
@@ -2195,27 +2695,29 @@ var Client = class {
|
|
|
2195
2695
|
}
|
|
2196
2696
|
}
|
|
2197
2697
|
/**
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
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.
|
|
2203
2704
|
*/
|
|
2204
2705
|
#startWorker(tracker) {
|
|
2706
|
+
const serverUrl = this.#tracking.serverUrl;
|
|
2707
|
+
if (!serverUrl) return;
|
|
2205
2708
|
const autoClaim = this.#tracking.autoClaim;
|
|
2206
|
-
|
|
2207
|
-
if (!this.baseUrl) return;
|
|
2208
|
-
const hintSource = new WsStatusSource({ serverUrl: this.baseUrl });
|
|
2709
|
+
const hintSource = new WsStatusSource({ serverUrl });
|
|
2209
2710
|
const worker = new SwapWorker({
|
|
2210
2711
|
tracker,
|
|
2211
2712
|
hintSource,
|
|
2212
|
-
execute: async (swapId, actionId) => {
|
|
2713
|
+
execute: autoClaim ? async (swapId, actionId) => {
|
|
2213
2714
|
if (actionId !== "claim")
|
|
2214
2715
|
throw new Error(`refusing to auto-run action '${actionId}'`);
|
|
2215
2716
|
const result = await this.claim(swapId);
|
|
2216
|
-
if (!result.success)
|
|
2217
|
-
|
|
2218
|
-
|
|
2717
|
+
if (!result.success)
|
|
2718
|
+
throw new Error(result.message ?? "claim failed");
|
|
2719
|
+
} : void 0,
|
|
2720
|
+
onActionRequired: autoClaim?.onActionRequired
|
|
2219
2721
|
});
|
|
2220
2722
|
this.#worker = worker;
|
|
2221
2723
|
worker.start();
|
|
@@ -2236,7 +2738,7 @@ var Client = class {
|
|
|
2236
2738
|
this.#worker = void 0;
|
|
2237
2739
|
this.#tracker?.stop();
|
|
2238
2740
|
this.#tracker = void 0;
|
|
2239
|
-
this.#
|
|
2741
|
+
this.#startPromise = void 0;
|
|
2240
2742
|
}
|
|
2241
2743
|
/**
|
|
2242
2744
|
* Resolve the per-ledger managers once — the explicit override if given, else
|
|
@@ -2251,7 +2753,7 @@ var Client = class {
|
|
|
2251
2753
|
return this.#managers;
|
|
2252
2754
|
}
|
|
2253
2755
|
const managers = /* @__PURE__ */ new Map();
|
|
2254
|
-
const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
|
|
2756
|
+
const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
|
|
2255
2757
|
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
2256
2758
|
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
2257
2759
|
if (arkadeUrl) {
|
|
@@ -2267,7 +2769,8 @@ var Client = class {
|
|
|
2267
2769
|
"bitcoin",
|
|
2268
2770
|
await BitcoinContractManager.create({
|
|
2269
2771
|
esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
|
|
2270
|
-
chainTime
|
|
2772
|
+
chainTime,
|
|
2773
|
+
minConfirmations: bitcoinMinConfirmations
|
|
2271
2774
|
})
|
|
2272
2775
|
);
|
|
2273
2776
|
this.#managers = managers;
|
|
@@ -2275,12 +2778,14 @@ var Client = class {
|
|
|
2275
2778
|
}
|
|
2276
2779
|
};
|
|
2277
2780
|
var ClientBuilder = class {
|
|
2278
|
-
#inner =
|
|
2781
|
+
#inner = import_lendaswap_sdk_pure2.Client.builder();
|
|
2279
2782
|
// Observe-mode tracking is on by default; the built client auto-builds its
|
|
2280
2783
|
// managers from config unless disabled or given an explicit override.
|
|
2281
2784
|
#trackingEnabled = true;
|
|
2785
|
+
#serverUrl;
|
|
2282
2786
|
#arkadeServerUrl;
|
|
2283
2787
|
#esploraUrl;
|
|
2788
|
+
#bitcoinMinConfirmations;
|
|
2284
2789
|
#evmRpcUrls;
|
|
2285
2790
|
#managers;
|
|
2286
2791
|
#autoClaim;
|
|
@@ -2321,6 +2826,7 @@ var ClientBuilder = class {
|
|
|
2321
2826
|
}
|
|
2322
2827
|
withBaseUrl(...args) {
|
|
2323
2828
|
this.#inner.withBaseUrl(...args);
|
|
2829
|
+
this.#serverUrl = args[0];
|
|
2324
2830
|
return this;
|
|
2325
2831
|
}
|
|
2326
2832
|
withReferralCode(...args) {
|
|
@@ -2344,6 +2850,18 @@ var ClientBuilder = class {
|
|
|
2344
2850
|
this.#esploraUrl = url;
|
|
2345
2851
|
return this;
|
|
2346
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
|
+
}
|
|
2347
2865
|
/**
|
|
2348
2866
|
* Override the Ark server URL. Optional for tracking — it otherwise defaults to
|
|
2349
2867
|
* the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
|
|
@@ -2384,14 +2902,24 @@ var ClientBuilder = class {
|
|
|
2384
2902
|
async build() {
|
|
2385
2903
|
return new Client(await this.#inner.build(), {
|
|
2386
2904
|
enabled: this.#trackingEnabled,
|
|
2905
|
+
serverUrl: this.#serverUrl,
|
|
2387
2906
|
managers: this.#managers,
|
|
2388
2907
|
arkadeServerUrl: this.#arkadeServerUrl,
|
|
2389
2908
|
esploraUrl: this.#esploraUrl,
|
|
2909
|
+
bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
|
|
2390
2910
|
evmRpcUrls: this.#evmRpcUrls,
|
|
2391
2911
|
autoClaim: this.#autoClaim
|
|
2392
2912
|
});
|
|
2393
2913
|
}
|
|
2394
2914
|
};
|
|
2915
|
+
var SETTLED_STORED_STATUSES = /* @__PURE__ */ new Set([
|
|
2916
|
+
"serverredeemed",
|
|
2917
|
+
"clientrefunded",
|
|
2918
|
+
"clientrefundedserverfunded",
|
|
2919
|
+
"clientrefundedserverrefunded",
|
|
2920
|
+
"clientredeemedandclientrefunded",
|
|
2921
|
+
"expired"
|
|
2922
|
+
]);
|
|
2395
2923
|
function createdSwapId(result) {
|
|
2396
2924
|
const id = result?.response?.id;
|
|
2397
2925
|
return typeof id === "string" ? id : void 0;
|
|
@@ -2421,6 +2949,7 @@ init_bitcoin_reader_esplora();
|
|
|
2421
2949
|
evmReaderFromClient,
|
|
2422
2950
|
htlcFactsFromEsploraTxs,
|
|
2423
2951
|
htlcKey,
|
|
2952
|
+
htlcQueryKey,
|
|
2424
2953
|
swapToTracked,
|
|
2425
2954
|
...require("@lendasat/lendaswap-sdk-pure")
|
|
2426
2955
|
});
|