@satora/swap 0.0.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,2317 @@
1
+ import {
2
+ DEFAULT_ESPLORA_URLS,
3
+ esploraReader,
4
+ htlcFactsFromEsploraTxs
5
+ } from "./chunk-J35RZBDW.js";
6
+
1
7
  // src/index.ts
2
8
  export * from "@lendasat/lendaswap-sdk-pure";
9
+
10
+ // src/actions/derive.ts
11
+ function deriveSwapActions(input) {
12
+ const {
13
+ status,
14
+ clientChainNow,
15
+ serverChainNow,
16
+ clientRefundLocktime,
17
+ serverRefundLocktime,
18
+ clientFunds = true
19
+ } = input;
20
+ switch (status) {
21
+ case "pending": {
22
+ if (!clientFunds) {
23
+ const wait = {
24
+ id: "wait",
25
+ recommended: true,
26
+ automation: "auto",
27
+ reason: "Waiting for the Lightning payment to be locked in."
28
+ };
29
+ return { recommended: "wait", actions: [wait] };
30
+ }
31
+ if (clientChainNow >= clientRefundLocktime) return { actions: [] };
32
+ const fund = {
33
+ id: "fund",
34
+ recommended: true,
35
+ // Funding spends new user money, so it is never auto-run.
36
+ automation: "manual",
37
+ reason: "Send the deposit to start the swap."
38
+ };
39
+ return { recommended: "fund", actions: [fund] };
40
+ }
41
+ case "clientfundingseen": {
42
+ const wait = {
43
+ id: "wait",
44
+ recommended: true,
45
+ automation: "auto",
46
+ reason: "Waiting for your funding transaction to confirm."
47
+ };
48
+ return { recommended: "wait", actions: [wait] };
49
+ }
50
+ case "clientfunded": {
51
+ const refundUnlocked = clientChainNow >= clientRefundLocktime;
52
+ if (refundUnlocked) {
53
+ const refund2 = {
54
+ id: "refund_unilateral",
55
+ recommended: true,
56
+ automation: "confirm",
57
+ reason: "Reclaim your deposit."
58
+ };
59
+ return { recommended: "refund_unilateral", actions: [refund2] };
60
+ }
61
+ const wait = {
62
+ id: "wait",
63
+ recommended: true,
64
+ automation: "auto",
65
+ reason: "Waiting for the server to fund the swap."
66
+ };
67
+ const refund = {
68
+ id: "refund_unilateral",
69
+ recommended: false,
70
+ automation: "confirm",
71
+ reason: "Refund becomes available once the timelock passes.",
72
+ blockedBy: {
73
+ kind: "timelock_not_expired",
74
+ message: "The refund timelock has not passed yet."
75
+ }
76
+ };
77
+ return { recommended: "wait", actions: [wait, refund] };
78
+ }
79
+ case "serverfunded": {
80
+ const claimWindowClosed = serverChainNow >= serverRefundLocktime;
81
+ if (!claimWindowClosed) {
82
+ const claim = {
83
+ id: "claim",
84
+ recommended: true,
85
+ automation: "auto",
86
+ reason: "Redeem the swap with your preimage."
87
+ };
88
+ return { recommended: "claim", actions: [claim] };
89
+ }
90
+ if (!clientFunds) {
91
+ const done = {
92
+ id: "none",
93
+ recommended: true,
94
+ automation: "auto",
95
+ reason: "The claim window passed \u2014 your Lightning payment will be refunded automatically."
96
+ };
97
+ return { recommended: "none", actions: [done] };
98
+ }
99
+ const refundUnlocked = clientChainNow >= clientRefundLocktime;
100
+ if (refundUnlocked) {
101
+ const refund2 = {
102
+ id: "refund_unilateral",
103
+ recommended: true,
104
+ automation: "confirm",
105
+ reason: "Reclaim your deposit \u2014 do not claim, as revealing the preimage would let the server take it."
106
+ };
107
+ return { recommended: "refund_unilateral", actions: [refund2] };
108
+ }
109
+ const wait = {
110
+ id: "wait",
111
+ recommended: true,
112
+ automation: "auto",
113
+ reason: "Wait for your refund timelock \u2014 do not claim, as revealing the preimage would let the server take your deposit."
114
+ };
115
+ const refund = {
116
+ id: "refund_unilateral",
117
+ recommended: false,
118
+ automation: "confirm",
119
+ reason: "Refund becomes available once the timelock passes.",
120
+ blockedBy: {
121
+ kind: "timelock_not_expired",
122
+ message: "The refund timelock has not passed yet."
123
+ }
124
+ };
125
+ return { recommended: "wait", actions: [wait, refund] };
126
+ }
127
+ case "clientredeeming": {
128
+ const wait = {
129
+ id: "wait",
130
+ recommended: true,
131
+ automation: "auto",
132
+ reason: "Redeeming your funds \u2014 waiting for the claim to confirm."
133
+ };
134
+ return { recommended: "wait", actions: [wait] };
135
+ }
136
+ case "clientredeemed":
137
+ case "serverredeemed": {
138
+ const done = {
139
+ id: "none",
140
+ recommended: true,
141
+ automation: "auto",
142
+ reason: "Swap complete \u2014 you've received your funds."
143
+ };
144
+ return { recommended: "none", actions: [done] };
145
+ }
146
+ case "expired": {
147
+ const done = {
148
+ id: "none",
149
+ recommended: true,
150
+ automation: "auto",
151
+ reason: "The swap expired without funding \u2014 nothing was locked."
152
+ };
153
+ return { recommended: "none", actions: [done] };
154
+ }
155
+ case "clientrefunded":
156
+ case "clientrefundedserverrefunded":
157
+ case "clientrefundedserverfunded": {
158
+ const done = {
159
+ id: "none",
160
+ recommended: true,
161
+ automation: "auto",
162
+ reason: "Refunded \u2014 your deposit was returned."
163
+ };
164
+ return { recommended: "none", actions: [done] };
165
+ }
166
+ case "clientredeemedandclientrefunded": {
167
+ const done = {
168
+ id: "none",
169
+ recommended: true,
170
+ automation: "auto",
171
+ reason: "This swap ended in an anomalous state (redeemed and refunded); no action needed."
172
+ };
173
+ return { recommended: "none", actions: [done] };
174
+ }
175
+ case "clientfundedserverrefunded":
176
+ case "serverwontfund":
177
+ case "clientfundedtoolate":
178
+ case "clientinvalidfunded": {
179
+ const refundUnlocked = clientChainNow >= clientRefundLocktime;
180
+ if (refundUnlocked) {
181
+ const refund2 = {
182
+ id: "refund_unilateral",
183
+ recommended: true,
184
+ automation: "confirm",
185
+ reason: "Reclaim your deposit \u2014 the swap did not complete."
186
+ };
187
+ return { recommended: "refund_unilateral", actions: [refund2] };
188
+ }
189
+ const wait = {
190
+ id: "wait",
191
+ recommended: true,
192
+ automation: "auto",
193
+ reason: "The swap did not complete; waiting for the refund timelock to reclaim your deposit."
194
+ };
195
+ const refund = {
196
+ id: "refund_unilateral",
197
+ recommended: false,
198
+ automation: "confirm",
199
+ reason: "Refund becomes available once the timelock passes.",
200
+ blockedBy: {
201
+ kind: "timelock_not_expired",
202
+ message: "The refund timelock has not passed yet."
203
+ }
204
+ };
205
+ return { recommended: "wait", actions: [wait, refund] };
206
+ }
207
+ default:
208
+ return assertNever(status);
209
+ }
210
+ }
211
+ function assertNever(status) {
212
+ throw new Error(`unhandled swap status: ${String(status)}`);
213
+ }
214
+
215
+ // src/actions/status.ts
216
+ function deriveSwapStatus(obs) {
217
+ const { clientHtlc, serverHtlc } = obs;
218
+ if (serverHtlc === void 0 && clientHtlc !== void 0) {
219
+ return receiveOnLightningStatus(clientHtlc);
220
+ }
221
+ if (clientHtlc === void 0 && serverHtlc !== void 0) {
222
+ return payOnLightningStatus(serverHtlc);
223
+ }
224
+ if (clientHtlc === void 0 || serverHtlc === void 0) return void 0;
225
+ if (clientHtlc === "spent_claim" && serverHtlc === "spent_claim") {
226
+ return "serverredeemed";
227
+ }
228
+ if (clientHtlc === "spent_refund" && serverHtlc === "spent_claim") {
229
+ return "clientredeemedandclientrefunded";
230
+ }
231
+ if (clientHtlc === "spent_refund" && serverHtlc === "spent_refund") {
232
+ return "clientrefundedserverrefunded";
233
+ }
234
+ if (clientHtlc === "spent_refund" && serverHtlc === "confirmed") {
235
+ return "clientrefundedserverfunded";
236
+ }
237
+ if (clientHtlc === "spent_refund" && (serverHtlc === "absent" || serverHtlc === "mempool")) {
238
+ return "clientrefunded";
239
+ }
240
+ if (clientHtlc === "confirmed" && serverHtlc === "spent_claim") {
241
+ return "clientredeemed";
242
+ }
243
+ if (clientHtlc === "confirmed" && serverHtlc === "spent_refund") {
244
+ return "clientfundedserverrefunded";
245
+ }
246
+ if (clientHtlc === "invalid") return "clientinvalidfunded";
247
+ if (clientHtlc === "absent") return "pending";
248
+ if (clientHtlc === "mempool") return "clientfundingseen";
249
+ if (clientHtlc === "confirmed") {
250
+ return serverHtlc === "confirmed" ? "serverfunded" : "clientfunded";
251
+ }
252
+ return void 0;
253
+ }
254
+ function receiveOnLightningStatus(clientHtlc) {
255
+ switch (clientHtlc) {
256
+ case "spent_claim":
257
+ return "serverredeemed";
258
+ // server swept the deposit ⟹ it paid the invoice
259
+ case "spent_refund":
260
+ return "clientrefunded";
261
+ case "invalid":
262
+ return "clientinvalidfunded";
263
+ case "confirmed":
264
+ return "clientfunded";
265
+ // funded; waiting for the server to pay + sweep
266
+ case "mempool":
267
+ return "clientfundingseen";
268
+ case "absent":
269
+ return "pending";
270
+ }
271
+ }
272
+ function payOnLightningStatus(serverHtlc) {
273
+ switch (serverHtlc) {
274
+ case "spent_claim":
275
+ return "clientredeemed";
276
+ // client claimed the on-chain leg ⟹ complete
277
+ case "confirmed":
278
+ return "serverfunded";
279
+ // ready to claim
280
+ case "spent_refund":
281
+ return "clientrefunded";
282
+ case "invalid":
283
+ case "mempool":
284
+ case "absent":
285
+ return "pending";
286
+ }
287
+ }
288
+
289
+ // src/client.ts
290
+ import {
291
+ Client as LegacyClient
292
+ } from "@lendasat/lendaswap-sdk-pure";
293
+
294
+ // src/contracts/arkade-manager.ts
295
+ import { RestIndexerProvider } from "@arkade-os/sdk";
296
+ import { hex as hex2 } from "@scure/base";
297
+
298
+ // src/contracts/arkade.ts
299
+ import {
300
+ ConditionWitness,
301
+ getArkPsbtFields,
302
+ Transaction
303
+ } from "@arkade-os/sdk";
304
+ import { base64 } from "@scure/base";
305
+
306
+ // src/contracts/preimage.ts
307
+ import { ripemd160 } from "@noble/hashes/legacy.js";
308
+ import { sha256 } from "@noble/hashes/sha2.js";
309
+ import { hex } from "@scure/base";
310
+ function preimageMatches(element, hashLock) {
311
+ const digest = hashLock.length === 20 ? ripemd160(sha256(element)) : sha256(element);
312
+ return hex.encode(digest) === hex.encode(hashLock);
313
+ }
314
+
315
+ // src/contracts/arkade.ts
316
+ function arkadeObservation(facts) {
317
+ if (facts.spend === "claim") return "spent_claim";
318
+ if (facts.spend === "refund") return "spent_refund";
319
+ if (!facts.funded) return "absent";
320
+ return facts.sufficient === false ? "invalid" : "confirmed";
321
+ }
322
+ function classifyArkadeSpend(encodedSpendTx, paymentHash) {
323
+ const tx = Transaction.fromPSBT(base64.decode(encodedSpendTx));
324
+ for (const witness of getArkPsbtFields(tx, 0, ConditionWitness)) {
325
+ for (const element of witness) {
326
+ if (element.length === 32 && preimageMatches(element, paymentHash)) {
327
+ return { spend: "claim", preimage: element };
328
+ }
329
+ }
330
+ }
331
+ return { spend: "refund" };
332
+ }
333
+ async function fetchArkadeSpend(source, spentByTxid, paymentHash) {
334
+ const { txs } = await source.getVirtualTxs([spentByTxid]);
335
+ const encoded = txs[0];
336
+ return encoded ? classifyArkadeSpend(encoded, paymentHash) : void 0;
337
+ }
338
+
339
+ // src/contracts/arkade-manager.ts
340
+ function isFunded(vtxo) {
341
+ const { state } = vtxo.virtualStatus;
342
+ return state === "preconfirmed" || state === "settled";
343
+ }
344
+ function spendTxid(vtxos) {
345
+ const spent = vtxos.find(
346
+ (v) => v.arkTxId || v.spentBy || v.isSpent || v.virtualStatus.state === "spent"
347
+ );
348
+ return spent?.arkTxId ?? spent?.spentBy;
349
+ }
350
+ var ArkadeContractManager = class _ArkadeContractManager {
351
+ ledger = "arkade";
352
+ #indexer;
353
+ #chainTime;
354
+ /** pkScript → the ref we're tracking. */
355
+ #refs = /* @__PURE__ */ new Map();
356
+ /** pkScript → last known observation. */
357
+ #obs = /* @__PURE__ */ new Map();
358
+ /** pkScript → the verified preimage recovered from a claim spend. */
359
+ #preimages = /* @__PURE__ */ new Map();
360
+ #listeners = /* @__PURE__ */ new Set();
361
+ #now;
362
+ constructor(deps) {
363
+ this.#indexer = deps.indexer;
364
+ this.#chainTime = deps.chainTime;
365
+ }
366
+ static fromDeps(deps) {
367
+ return new _ArkadeContractManager(deps);
368
+ }
369
+ /** Construct a manager backed by a `RestIndexerProvider` over the Ark server. */
370
+ static async create(config) {
371
+ const indexer = new RestIndexerProvider(config.serverUrl);
372
+ return _ArkadeContractManager.fromDeps({
373
+ indexer,
374
+ chainTime: config.chainTime
375
+ });
376
+ }
377
+ canObserve(ref) {
378
+ return ref.ledger === "arkade";
379
+ }
380
+ async register(ref) {
381
+ if (ref.ledger !== "arkade")
382
+ throw new Error(
383
+ `ArkadeContractManager can't track a '${ref.ledger}' HTLC`
384
+ );
385
+ this.#refs.set(ref.script, ref);
386
+ await this.#reconcileRef(ref);
387
+ }
388
+ async unregister(ref) {
389
+ if (ref.ledger !== "arkade") return;
390
+ this.#refs.delete(ref.script);
391
+ this.#obs.delete(ref.script);
392
+ this.#preimages.delete(ref.script);
393
+ }
394
+ getState(ref) {
395
+ return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
396
+ }
397
+ chainNow(_ref) {
398
+ return this.#now;
399
+ }
400
+ onEvent(cb) {
401
+ this.#listeners.add(cb);
402
+ return () => this.#listeners.delete(cb);
403
+ }
404
+ async refresh() {
405
+ if (this.#chainTime) this.#now = await this.#chainTime();
406
+ await Promise.all(
407
+ [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
408
+ );
409
+ }
410
+ async reconcile(ref) {
411
+ if (ref.ledger !== "arkade") return;
412
+ const tracked = this.#refs.get(ref.script);
413
+ if (!tracked) return;
414
+ if (this.#chainTime) this.#now = await this.#chainTime();
415
+ await this.#reconcileRef(tracked);
416
+ }
417
+ dispose() {
418
+ this.#listeners.clear();
419
+ }
420
+ /** The verified preimage recovered from a claim spend, if one was seen. */
421
+ getPreimage(ref) {
422
+ return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
423
+ }
424
+ /** Read the VHTLC's vtxos and map them to an observation. */
425
+ async #reconcileRef(ref) {
426
+ const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
427
+ const spend = spendTxid(vtxos);
428
+ if (spend) {
429
+ const resolved = await fetchArkadeSpend(
430
+ this.#indexer,
431
+ spend,
432
+ hex2.decode(ref.preimageHash)
433
+ );
434
+ if (resolved) {
435
+ if (resolved.spend === "claim")
436
+ this.#preimages.set(ref.script, resolved.preimage);
437
+ this.#set(
438
+ ref.script,
439
+ arkadeObservation({ funded: true, spend: resolved.spend })
440
+ );
441
+ return;
442
+ }
443
+ }
444
+ const funded = vtxos.filter(isFunded);
445
+ const total = funded.reduce((sum, vtxo) => sum + vtxo.value, 0);
446
+ this.#set(
447
+ ref.script,
448
+ arkadeObservation({
449
+ funded: funded.length > 0,
450
+ sufficient: total >= ref.expectedSats
451
+ })
452
+ );
453
+ }
454
+ /** Update an observation and notify on change; never downgrade a resolved spend. */
455
+ #set(script, observation) {
456
+ const ref = this.#refs.get(script);
457
+ if (!ref) return;
458
+ const current = this.#obs.get(script);
459
+ if (current === observation) return;
460
+ const spendStates = ["spent_claim", "spent_refund"];
461
+ if (current && spendStates.includes(current) && !spendStates.includes(observation))
462
+ return;
463
+ this.#obs.set(script, observation);
464
+ for (const listener of this.#listeners) listener(ref, observation);
465
+ }
466
+ };
467
+
468
+ // src/contracts/arkade-network.ts
469
+ var DEFAULT_ARKADE_URLS = {
470
+ bitcoin: "https://arkade.computer",
471
+ mainnet: "https://arkade.computer",
472
+ signet: "https://mutinynet.arkade.sh",
473
+ mutinynet: "https://mutinynet.arkade.sh"
474
+ };
475
+ function defaultArkadeServerUrl(network) {
476
+ return DEFAULT_ARKADE_URLS[network.toLowerCase()];
477
+ }
478
+
479
+ // src/contracts/bitcoin-manager.ts
480
+ import { hex as hex4 } from "@scure/base";
481
+
482
+ // src/contracts/bitcoin.ts
483
+ import { hex as hex3 } from "@scure/base";
484
+ function classifyBitcoinSpend(witness, paymentHash) {
485
+ for (const item of witness) {
486
+ if (item.length !== 64) continue;
487
+ const bytes = hex3.decode(item);
488
+ if (preimageMatches(bytes, paymentHash))
489
+ return { spend: "claim", preimage: bytes };
490
+ }
491
+ return { spend: "refund" };
492
+ }
493
+ function bitcoinObservation(facts, paymentHash, expectedSats) {
494
+ if (facts.spendWitness) {
495
+ const spend = classifyBitcoinSpend(facts.spendWitness, paymentHash);
496
+ return spend.spend === "claim" ? { observation: "spent_claim", preimage: spend.preimage } : { observation: "spent_refund" };
497
+ }
498
+ if (facts.funding === "confirmed" && facts.fundedSats < expectedSats) {
499
+ return { observation: "invalid" };
500
+ }
501
+ return { observation: facts.funding };
502
+ }
503
+
504
+ // src/contracts/bitcoin-manager.ts
505
+ var BitcoinContractManager = class _BitcoinContractManager {
506
+ ledger = "bitcoin";
507
+ #reader;
508
+ #chainTime;
509
+ /** address → the ref we're tracking. */
510
+ #refs = /* @__PURE__ */ new Map();
511
+ /** address → last known observation. */
512
+ #obs = /* @__PURE__ */ new Map();
513
+ /** address → the preimage a claim revealed. */
514
+ #preimages = /* @__PURE__ */ new Map();
515
+ #listeners = /* @__PURE__ */ new Set();
516
+ #now;
517
+ constructor(deps) {
518
+ this.#reader = deps.reader;
519
+ this.#chainTime = deps.chainTime;
520
+ }
521
+ static fromDeps(deps) {
522
+ return new _BitcoinContractManager(deps);
523
+ }
524
+ static async create(config) {
525
+ const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-GRUP7RAG.js");
526
+ return _BitcoinContractManager.fromDeps({
527
+ reader: esploraReader2(config.esploraUrl),
528
+ chainTime: config.chainTime
529
+ });
530
+ }
531
+ canObserve(ref) {
532
+ return ref.ledger === "bitcoin";
533
+ }
534
+ async register(ref) {
535
+ if (ref.ledger !== "bitcoin")
536
+ throw new Error(
537
+ `BitcoinContractManager can't track a '${ref.ledger}' HTLC`
538
+ );
539
+ this.#refs.set(ref.address, ref);
540
+ await this.#reconcileRef(ref);
541
+ }
542
+ async unregister(ref) {
543
+ if (ref.ledger !== "bitcoin") return;
544
+ this.#refs.delete(ref.address);
545
+ this.#obs.delete(ref.address);
546
+ this.#preimages.delete(ref.address);
547
+ }
548
+ getState(ref) {
549
+ return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
550
+ }
551
+ chainNow(_ref) {
552
+ return this.#now;
553
+ }
554
+ onEvent(cb) {
555
+ this.#listeners.add(cb);
556
+ return () => this.#listeners.delete(cb);
557
+ }
558
+ async refresh() {
559
+ if (this.#chainTime) this.#now = await this.#chainTime();
560
+ await Promise.all(
561
+ [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
562
+ );
563
+ }
564
+ async reconcile(ref) {
565
+ if (ref.ledger !== "bitcoin") return;
566
+ const tracked = this.#refs.get(ref.address);
567
+ if (!tracked) return;
568
+ if (this.#chainTime) this.#now = await this.#chainTime();
569
+ await this.#reconcileRef(tracked);
570
+ }
571
+ dispose() {
572
+ this.#listeners.clear();
573
+ }
574
+ /** The preimage a claim revealed on this HTLC, if one was seen. */
575
+ getPreimage(ref) {
576
+ return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
577
+ }
578
+ async #reconcileRef(ref) {
579
+ const facts = await this.#reader.getHtlcFacts(ref.address);
580
+ const { observation, preimage } = bitcoinObservation(
581
+ facts,
582
+ hex4.decode(ref.preimageHash),
583
+ ref.expectedSats
584
+ );
585
+ if (preimage) this.#preimages.set(ref.address, preimage);
586
+ this.#set(ref.address, observation);
587
+ }
588
+ /** Update an observation and notify on change; never downgrade a resolved spend. */
589
+ #set(address, observation) {
590
+ const ref = this.#refs.get(address);
591
+ if (!ref) return;
592
+ const current = this.#obs.get(address);
593
+ if (current === observation) return;
594
+ const spendStates = ["spent_claim", "spent_refund"];
595
+ if (current && spendStates.includes(current) && !spendStates.includes(observation))
596
+ return;
597
+ this.#obs.set(address, observation);
598
+ for (const listener of this.#listeners) listener(ref, observation);
599
+ }
600
+ };
601
+
602
+ // src/contracts/evm.ts
603
+ function evmObservation(events, expected, pending = false) {
604
+ const redeemed = events.find((e) => e.kind === "redeemed");
605
+ if (redeemed?.kind === "redeemed")
606
+ return { observation: "spent_claim", preimage: redeemed.preimage };
607
+ if (events.some((e) => e.kind === "refunded"))
608
+ return { observation: "spent_refund" };
609
+ const created = events.find((e) => e.kind === "created");
610
+ if (created?.kind === "created") {
611
+ const amountOk = created.amount >= expected.amount;
612
+ const tokenOk = expected.token === void 0 || created.token.toLowerCase() === expected.token.toLowerCase();
613
+ return { observation: amountOk && tokenOk ? "confirmed" : "invalid" };
614
+ }
615
+ return { observation: pending ? "mempool" : "absent" };
616
+ }
617
+
618
+ // src/contracts/types.ts
619
+ function htlcKey(ref) {
620
+ switch (ref.ledger) {
621
+ case "arkade":
622
+ return `arkade:${ref.script}`;
623
+ case "bitcoin":
624
+ return `bitcoin:${ref.address}`;
625
+ case "evm":
626
+ return `evm:${ref.chainId}:${ref.preimageHash}`;
627
+ case "lightning":
628
+ return `lightning:${ref.paymentHash}`;
629
+ }
630
+ }
631
+
632
+ // src/contracts/evm-manager.ts
633
+ var EvmContractManager = class _EvmContractManager {
634
+ ledger = "evm";
635
+ #readers;
636
+ /** htlcKey → the ref we're tracking. */
637
+ #refs = /* @__PURE__ */ new Map();
638
+ /** htlcKey → last known observation. */
639
+ #obs = /* @__PURE__ */ new Map();
640
+ /** htlcKey → the preimage a claim revealed. */
641
+ #preimages = /* @__PURE__ */ new Map();
642
+ /** chainId → its latest block time (ms). */
643
+ #now = /* @__PURE__ */ new Map();
644
+ /** chainId → unsubscribe for its (single, shared) watch. */
645
+ #watchUnsubs = /* @__PURE__ */ new Map();
646
+ /** chainId → a block-event reconcile is currently running (coalescing). */
647
+ #reconciling = /* @__PURE__ */ new Set();
648
+ /** chainId → a block event arrived mid-reconcile; run exactly one more. */
649
+ #reconcileAgain = /* @__PURE__ */ new Set();
650
+ #listeners = /* @__PURE__ */ new Set();
651
+ constructor(deps) {
652
+ this.#readers = deps.readers;
653
+ }
654
+ static fromDeps(deps) {
655
+ return new _EvmContractManager(deps);
656
+ }
657
+ canObserve(ref) {
658
+ return ref.ledger === "evm" && this.#readers.has(ref.chainId);
659
+ }
660
+ async register(ref) {
661
+ if (ref.ledger !== "evm")
662
+ throw new Error(`EvmContractManager can't track a '${ref.ledger}' HTLC`);
663
+ if (!this.#readers.has(ref.chainId))
664
+ throw new Error(
665
+ `no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
666
+ );
667
+ this.#refs.set(htlcKey(ref), ref);
668
+ this.#ensureWatch(ref.chainId);
669
+ await this.#reconcileChain(ref.chainId);
670
+ }
671
+ async unregister(ref) {
672
+ if (ref.ledger !== "evm") return;
673
+ const key = htlcKey(ref);
674
+ this.#refs.delete(key);
675
+ this.#obs.delete(key);
676
+ this.#preimages.delete(key);
677
+ if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId)) {
678
+ this.#watchUnsubs.get(ref.chainId)?.();
679
+ this.#watchUnsubs.delete(ref.chainId);
680
+ this.#now.delete(ref.chainId);
681
+ this.#reconcileAgain.delete(ref.chainId);
682
+ }
683
+ }
684
+ getState(ref) {
685
+ return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
686
+ }
687
+ chainNow(ref) {
688
+ return ref.ledger === "evm" ? this.#now.get(ref.chainId) : void 0;
689
+ }
690
+ onEvent(cb) {
691
+ this.#listeners.add(cb);
692
+ return () => this.#listeners.delete(cb);
693
+ }
694
+ async refresh() {
695
+ const chainIds = new Set([...this.#refs.values()].map((r) => r.chainId));
696
+ await Promise.all([...chainIds].map((c) => this.#reconcileChain(c)));
697
+ }
698
+ async reconcile(ref) {
699
+ if (ref.ledger !== "evm") return;
700
+ const tracked = this.#refs.get(htlcKey(ref));
701
+ if (!tracked) return;
702
+ const reader = this.#readers.get(tracked.chainId);
703
+ if (!reader) return;
704
+ this.#now.set(tracked.chainId, await reader.getBlockTimeMs());
705
+ await this.#reconcileRef(reader, tracked);
706
+ }
707
+ dispose() {
708
+ for (const unsub of this.#watchUnsubs.values()) unsub();
709
+ this.#watchUnsubs.clear();
710
+ this.#reconciling.clear();
711
+ this.#reconcileAgain.clear();
712
+ this.#listeners.clear();
713
+ }
714
+ /** The preimage a claim revealed on this HTLC, if one was seen. */
715
+ getPreimage(ref) {
716
+ return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
717
+ }
718
+ #ensureWatch(chainId) {
719
+ if (this.#watchUnsubs.has(chainId)) return;
720
+ const reader = this.#readers.get(chainId);
721
+ if (!reader) return;
722
+ this.#watchUnsubs.set(
723
+ chainId,
724
+ reader.watch(() => this.#scheduleReconcile(chainId))
725
+ );
726
+ }
727
+ /**
728
+ * Reconcile a chain in response to a block/log event, coalesced: at most one
729
+ * reconcile runs per chain at a time, with at most one queued rerun to pick up
730
+ * changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
731
+ * redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
732
+ * transient RPC failure in a background reconcile from becoming an unhandled
733
+ * rejection. `register`/`refresh` still await `#reconcileChain` directly.
734
+ */
735
+ #scheduleReconcile(chainId) {
736
+ if (this.#reconciling.has(chainId)) {
737
+ this.#reconcileAgain.add(chainId);
738
+ return;
739
+ }
740
+ this.#reconciling.add(chainId);
741
+ void this.#reconcileChain(chainId).catch((error) => {
742
+ console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
743
+ }).finally(() => {
744
+ this.#reconciling.delete(chainId);
745
+ if (this.#reconcileAgain.delete(chainId))
746
+ this.#scheduleReconcile(chainId);
747
+ });
748
+ }
749
+ /** Refresh one chain's clock and re-observe every HTLC tracked on it. */
750
+ async #reconcileChain(chainId) {
751
+ const reader = this.#readers.get(chainId);
752
+ if (!reader) return;
753
+ this.#now.set(chainId, await reader.getBlockTimeMs());
754
+ await Promise.all(
755
+ [...this.#refs.values()].filter((r) => r.chainId === chainId).map((r) => this.#reconcileRef(reader, r))
756
+ );
757
+ }
758
+ async #reconcileRef(reader, ref) {
759
+ const events = await reader.getHtlcEvents(
760
+ ref.htlc,
761
+ ref.preimageHash,
762
+ ref.claimAddress
763
+ );
764
+ const { observation, preimage } = evmObservation(events, {
765
+ amount: ref.expectedAmount,
766
+ token: ref.expectedToken
767
+ });
768
+ const key = htlcKey(ref);
769
+ if (preimage) this.#preimages.set(key, preimage);
770
+ this.#set(key, observation);
771
+ }
772
+ /** Update an observation and notify on change; never downgrade a resolved spend. */
773
+ #set(key, observation) {
774
+ const ref = this.#refs.get(key);
775
+ if (!ref) return;
776
+ const current = this.#obs.get(key);
777
+ if (current === observation) return;
778
+ const spendStates = ["spent_claim", "spent_refund"];
779
+ if (current && spendStates.includes(current) && !spendStates.includes(observation))
780
+ return;
781
+ this.#obs.set(key, observation);
782
+ for (const listener of this.#listeners) listener(ref, observation);
783
+ }
784
+ };
785
+
786
+ // src/contracts/evm-reader-viem.ts
787
+ import {
788
+ createPublicClient,
789
+ fallback,
790
+ http,
791
+ parseAbiItem
792
+ } from "viem";
793
+ var DEFAULT_EVM_RPCS = {
794
+ // Polygon — viem's default (polygon.drpc.org) misbehaves for some calls, so
795
+ // list working public RPCs explicitly.
796
+ 137: [
797
+ "https://polygon.drpc.org",
798
+ "https://tenderly.rpc.polygon.community",
799
+ "https://polygon-bor-rpc.publicnode.com"
800
+ ],
801
+ 1: [
802
+ "https://ethereum-rpc.publicnode.com",
803
+ "https://rpc.ankr.com/eth",
804
+ "https://eth.drpc.org"
805
+ ],
806
+ 42161: [
807
+ "https://arbitrum-one-rpc.publicnode.com",
808
+ "https://rpc.ankr.com/arbitrum",
809
+ "https://arb1.arbitrum.io/rpc"
810
+ ]
811
+ };
812
+ var SWAP_CREATED = parseAbiItem(
813
+ "event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock)"
814
+ );
815
+ var SWAP_REDEEMED = parseAbiItem(
816
+ "event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
817
+ );
818
+ var SWAP_REFUNDED = parseAbiItem(
819
+ "event SwapRefunded(bytes32 indexed preimageHash)"
820
+ );
821
+ function evmReaderFromClient(client) {
822
+ return {
823
+ async getHtlcEvents(htlc, preimageHash, claimAddress) {
824
+ const [created, redeemed, refunded] = await Promise.all([
825
+ // Filter by claimAddress too (also indexed), so only the HTLC actually
826
+ // claimable on the swap's terms is seen.
827
+ client.getLogs({
828
+ address: htlc,
829
+ event: SWAP_CREATED,
830
+ args: { preimageHash, claimAddress },
831
+ fromBlock: "earliest"
832
+ }),
833
+ client.getLogs({
834
+ address: htlc,
835
+ event: SWAP_REDEEMED,
836
+ args: { preimageHash },
837
+ fromBlock: "earliest"
838
+ }),
839
+ client.getLogs({
840
+ address: htlc,
841
+ event: SWAP_REFUNDED,
842
+ args: { preimageHash },
843
+ fromBlock: "earliest"
844
+ })
845
+ ]);
846
+ const events = [];
847
+ for (const log of created) {
848
+ const args = log.args;
849
+ events.push({
850
+ kind: "created",
851
+ amount: args?.amount ?? 0n,
852
+ token: args?.token ?? "0x"
853
+ });
854
+ }
855
+ for (const log of redeemed) {
856
+ const preimage = log.args?.preimage;
857
+ if (preimage) events.push({ kind: "redeemed", preimage });
858
+ }
859
+ if (refunded.length > 0) events.push({ kind: "refunded" });
860
+ return events;
861
+ },
862
+ async getBlockTimeMs() {
863
+ const block = await client.getBlock();
864
+ return Number(block.timestamp) * 1e3;
865
+ },
866
+ watch(cb) {
867
+ return client.watchBlocks({ onBlock: () => cb() });
868
+ }
869
+ };
870
+ }
871
+ function createEvmRpcReader(rpcUrls) {
872
+ const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
873
+ const transport = urls.length > 1 ? fallback(
874
+ urls.map((url) => http(url)),
875
+ { rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
876
+ ) : http(urls[0]);
877
+ const client = createPublicClient({ transport });
878
+ return evmReaderFromClient(client);
879
+ }
880
+ function defaultEvmReaders(overrides) {
881
+ const chainIds = /* @__PURE__ */ new Set([
882
+ ...Object.keys(DEFAULT_EVM_RPCS).map(Number),
883
+ ...Object.keys(overrides ?? {}).map(Number)
884
+ ]);
885
+ const readers = /* @__PURE__ */ new Map();
886
+ for (const chainId of chainIds) {
887
+ const override = overrides?.[chainId];
888
+ const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
889
+ const urls = override ? [override, ...defaults] : defaults;
890
+ if (urls.length > 0) readers.set(chainId, createEvmRpcReader(urls));
891
+ }
892
+ return readers;
893
+ }
894
+
895
+ // src/hints/swap-worker.ts
896
+ var AUTO_EXECUTABLE = /* @__PURE__ */ new Set([
897
+ "claim"
898
+ ]);
899
+ var SwapWorker = class {
900
+ #tracker;
901
+ #hintSource;
902
+ #execute;
903
+ #onActionRequired;
904
+ #maxAttempts;
905
+ #baseDelayMs;
906
+ #maxDelayMs;
907
+ /** Swaps with an auto action in flight — guards against double-runs. */
908
+ #executing = /* @__PURE__ */ new Set();
909
+ /** Swaps with a scheduled auto-claim retry (backoff timer + attempt count). */
910
+ #retries = /* @__PURE__ */ new Map();
911
+ #unsubs = [];
912
+ #started = false;
913
+ constructor(options) {
914
+ this.#tracker = options.tracker;
915
+ this.#hintSource = options.hintSource;
916
+ this.#execute = options.execute;
917
+ this.#onActionRequired = options.onActionRequired;
918
+ this.#maxAttempts = options.retry?.maxAttempts ?? 5;
919
+ this.#baseDelayMs = options.retry?.baseDelayMs ?? 2e3;
920
+ this.#maxDelayMs = options.retry?.maxDelayMs ?? 3e4;
921
+ }
922
+ /** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
923
+ start() {
924
+ if (this.#started) return;
925
+ this.#started = true;
926
+ this.#hintSource.subscribe(this.#tracker.trackedSwapIds());
927
+ this.#unsubs.push(
928
+ this.#hintSource.onStatus(({ swapId }) => {
929
+ void this.#tracker.applyHint(swapId).catch((error) => {
930
+ console.warn(`SwapWorker: applyHint(${swapId}) failed:`, error);
931
+ });
932
+ })
933
+ );
934
+ this.#hintSource.start();
935
+ this.#unsubs.push(
936
+ this.#tracker.subscribeToActions(
937
+ (swapId, actions) => this.#onActions(swapId, actions)
938
+ )
939
+ );
940
+ }
941
+ /** Stop reacting, cancel pending retries, and close the hint feed. */
942
+ stop() {
943
+ this.#started = false;
944
+ for (const unsub of this.#unsubs) unsub();
945
+ this.#unsubs = [];
946
+ this.#hintSource.stop();
947
+ for (const { timer } of this.#retries.values()) clearTimeout(timer);
948
+ this.#retries.clear();
949
+ this.#executing.clear();
950
+ }
951
+ #onActions(swapId, actions) {
952
+ const recommended = actions.recommended;
953
+ if (recommended === "none") {
954
+ this.#hintSource.unsubscribe([swapId]);
955
+ this.#cancelRetry(swapId);
956
+ return;
957
+ }
958
+ this.#hintSource.subscribe([swapId]);
959
+ if (recommended && AUTO_EXECUTABLE.has(recommended)) {
960
+ this.#autoExecute(swapId, actions);
961
+ return;
962
+ }
963
+ this.#cancelRetry(swapId);
964
+ if (!recommended) return;
965
+ const automation = actions.actions.find(
966
+ (a) => a.id === recommended
967
+ )?.automation;
968
+ if (automation === "manual" || automation === "confirm")
969
+ this.#onActionRequired?.(swapId, actions);
970
+ }
971
+ #autoExecute(swapId, actions) {
972
+ if (this.#executing.has(swapId)) return;
973
+ const actionId = actions.recommended;
974
+ if (!actionId) return;
975
+ this.#executing.add(swapId);
976
+ void this.#execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
977
+ console.warn(`SwapWorker: ${actionId}(${swapId}) failed:`, error);
978
+ this.#scheduleRetry(swapId, actions);
979
+ }).finally(() => this.#executing.delete(swapId));
980
+ }
981
+ /** Schedule the next backoff retry, or give up and surface once attempts run out. */
982
+ #scheduleRetry(swapId, actions) {
983
+ const attempts = (this.#retries.get(swapId)?.attempts ?? 0) + 1;
984
+ if (attempts > this.#maxAttempts) {
985
+ console.warn(
986
+ `SwapWorker: auto-claim for ${swapId} gave up after ${this.#maxAttempts} attempts`
987
+ );
988
+ this.#cancelRetry(swapId);
989
+ this.#onActionRequired?.(swapId, actions);
990
+ return;
991
+ }
992
+ this.#clearTimer(swapId);
993
+ const delay = Math.min(
994
+ this.#baseDelayMs * 2 ** (attempts - 1),
995
+ this.#maxDelayMs
996
+ );
997
+ const timer = setTimeout(() => this.#retry(swapId, actions), delay);
998
+ this.#retries.set(swapId, { attempts, timer });
999
+ }
1000
+ /**
1001
+ * A retry fires: re-verify against chain (forced re-emit), which drives
1002
+ * {@link #onActions} to re-run the claim only if it is still recommended. If the
1003
+ * re-verify itself fails we can't safely retry, so back off and try the cycle
1004
+ * again.
1005
+ */
1006
+ #retry(swapId, actions) {
1007
+ void this.#tracker.applyHint(swapId, { force: true }).catch((error) => {
1008
+ console.warn(
1009
+ `SwapWorker: re-verify before retrying ${swapId} failed:`,
1010
+ error
1011
+ );
1012
+ this.#scheduleRetry(swapId, actions);
1013
+ });
1014
+ }
1015
+ #cancelRetry(swapId) {
1016
+ this.#clearTimer(swapId);
1017
+ this.#retries.delete(swapId);
1018
+ }
1019
+ #clearTimer(swapId) {
1020
+ const existing = this.#retries.get(swapId);
1021
+ if (existing) clearTimeout(existing.timer);
1022
+ }
1023
+ };
1024
+
1025
+ // src/hints/ws-status-source.ts
1026
+ var MAX_ARGS_PER_FRAME = 64;
1027
+ var MAX_SUBSCRIPTIONS = 256;
1028
+ var WsStatusSource = class {
1029
+ #url;
1030
+ #factory;
1031
+ #reconnectMinMs;
1032
+ #reconnectMaxMs;
1033
+ /**
1034
+ * Ids actually subscribed on the wire — held at or below the server's
1035
+ * per-connection cap, and resent in full on every (re)connect.
1036
+ */
1037
+ #active = /* @__PURE__ */ new Set();
1038
+ /**
1039
+ * Ids we want but have no slot for. Promoted FIFO as slots free (the worker
1040
+ * unsubscribes each swap when it reaches a terminal action, so they do free).
1041
+ * A pending swap is not broken, only slower: it gets no hints, so it advances
1042
+ * on the tracker's chain poll instead.
1043
+ */
1044
+ #pending = /* @__PURE__ */ new Set();
1045
+ #listeners = /* @__PURE__ */ new Set();
1046
+ #ws;
1047
+ #open = false;
1048
+ #stopped = false;
1049
+ #backoffMs;
1050
+ #reconnectTimer;
1051
+ constructor(options) {
1052
+ this.#url = toWsUrl(options.serverUrl);
1053
+ this.#factory = options.webSocketFactory ?? defaultWebSocketFactory;
1054
+ this.#reconnectMinMs = options.reconnectMinMs ?? 1e3;
1055
+ this.#reconnectMaxMs = options.reconnectMaxMs ?? 3e4;
1056
+ this.#backoffMs = this.#reconnectMinMs;
1057
+ }
1058
+ /** Open the connection and begin (re)subscribing. Idempotent. */
1059
+ start() {
1060
+ if (this.#ws || this.#reconnectTimer) return;
1061
+ this.#stopped = false;
1062
+ this.#connect();
1063
+ }
1064
+ /** Close the connection and stop reconnecting. */
1065
+ stop() {
1066
+ this.#stopped = true;
1067
+ if (this.#reconnectTimer) clearTimeout(this.#reconnectTimer);
1068
+ this.#reconnectTimer = void 0;
1069
+ this.#open = false;
1070
+ this.#ws?.close();
1071
+ this.#ws = void 0;
1072
+ }
1073
+ /** Emit for each pushed status update. Returns an unsubscribe fn. */
1074
+ onStatus(cb) {
1075
+ this.#listeners.add(cb);
1076
+ return () => this.#listeners.delete(cb);
1077
+ }
1078
+ /**
1079
+ * Track these swap ids — subscribe on the socket now if open, else on next open.
1080
+ *
1081
+ * Enforces the server's per-connection cap locally. The server silently stops
1082
+ * adding once it is full (it breaks out of the loop but still acks every id),
1083
+ * so sending more would leave us believing ids are subscribed that will never
1084
+ * receive an update. Overflow is queued instead and promoted as slots free.
1085
+ */
1086
+ subscribe(swapIds) {
1087
+ const fresh = swapIds.filter(
1088
+ (id) => !this.#active.has(id) && !this.#pending.has(id)
1089
+ );
1090
+ if (fresh.length === 0) return;
1091
+ const admitted = [];
1092
+ for (const id of fresh) {
1093
+ if (this.#active.size < MAX_SUBSCRIPTIONS) {
1094
+ this.#active.add(id);
1095
+ admitted.push(id);
1096
+ } else {
1097
+ this.#pending.add(id);
1098
+ }
1099
+ }
1100
+ if (this.#pending.size > 0)
1101
+ console.warn(
1102
+ `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`
1103
+ );
1104
+ if (this.#open && admitted.length > 0)
1105
+ this.#sendFrame("subscribe", admitted);
1106
+ }
1107
+ /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
1108
+ unsubscribe(swapIds) {
1109
+ const dropped = [];
1110
+ for (const id of swapIds) {
1111
+ if (this.#active.delete(id)) dropped.push(id);
1112
+ else this.#pending.delete(id);
1113
+ }
1114
+ if (this.#open && dropped.length > 0)
1115
+ this.#sendFrame("unsubscribe", dropped);
1116
+ this.#promotePending();
1117
+ }
1118
+ /** Move queued ids into any free slots, subscribing them if the socket is open. */
1119
+ #promotePending() {
1120
+ if (this.#pending.size === 0) return;
1121
+ const promoted = [];
1122
+ for (const id of this.#pending) {
1123
+ if (this.#active.size >= MAX_SUBSCRIPTIONS) break;
1124
+ this.#pending.delete(id);
1125
+ this.#active.add(id);
1126
+ promoted.push(id);
1127
+ }
1128
+ if (this.#open && promoted.length > 0)
1129
+ this.#sendFrame("subscribe", promoted);
1130
+ }
1131
+ #connect() {
1132
+ this.#reconnectTimer = void 0;
1133
+ const ws = this.#factory(this.#url);
1134
+ this.#ws = ws;
1135
+ ws.onopen = () => {
1136
+ this.#backoffMs = this.#reconnectMinMs;
1137
+ this.#promotePending();
1138
+ this.#open = true;
1139
+ if (this.#active.size > 0)
1140
+ this.#sendFrame("subscribe", [...this.#active]);
1141
+ };
1142
+ ws.onmessage = (event) => this.#handleMessage(event.data);
1143
+ ws.onclose = () => this.#onDisconnect();
1144
+ ws.onerror = () => this.#ws?.close();
1145
+ }
1146
+ #onDisconnect() {
1147
+ this.#open = false;
1148
+ this.#ws = void 0;
1149
+ if (this.#stopped || this.#reconnectTimer) return;
1150
+ const delay = this.#backoffMs;
1151
+ this.#backoffMs = Math.min(this.#backoffMs * 2, this.#reconnectMaxMs);
1152
+ this.#reconnectTimer = setTimeout(() => this.#connect(), delay);
1153
+ }
1154
+ #handleMessage(data) {
1155
+ if (typeof data !== "string") return;
1156
+ let frame;
1157
+ try {
1158
+ frame = JSON.parse(data);
1159
+ } catch {
1160
+ return;
1161
+ }
1162
+ if (!frame || typeof frame !== "object") return;
1163
+ const f = frame;
1164
+ if (f.data && typeof f.data === "object") {
1165
+ for (const [swapId, status] of Object.entries(
1166
+ f.data
1167
+ ))
1168
+ this.#emit({ swapId, status });
1169
+ return;
1170
+ }
1171
+ if (typeof f.message === "string")
1172
+ console.warn(`WsStatusSource: server error: ${f.message}`);
1173
+ }
1174
+ #emit(update) {
1175
+ for (const cb of this.#listeners) cb(update);
1176
+ }
1177
+ #sendFrame(op, ids) {
1178
+ for (let i = 0; i < ids.length; i += MAX_ARGS_PER_FRAME) {
1179
+ const args = ids.slice(i, i + MAX_ARGS_PER_FRAME);
1180
+ this.#ws?.send(JSON.stringify({ op, channel: "swap_status", args }));
1181
+ }
1182
+ }
1183
+ };
1184
+ function toWsUrl(serverUrl) {
1185
+ const url = new URL(serverUrl);
1186
+ const scheme = url.protocol === "https:" ? "wss" : "ws";
1187
+ const path = url.pathname.replace(/\/+$/, "");
1188
+ return `${scheme}://${url.host}${path}/ws`;
1189
+ }
1190
+ function defaultWebSocketFactory(url) {
1191
+ const WS = globalThis.WebSocket;
1192
+ if (!WS)
1193
+ throw new Error(
1194
+ "no global WebSocket available \u2014 pass options.webSocketFactory (Node < 22)"
1195
+ );
1196
+ return new WS(url);
1197
+ }
1198
+
1199
+ // src/contracts/arkade-vhtlc.ts
1200
+ import { VHTLC, VHTLCContractHandler } from "@arkade-os/sdk";
1201
+ import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
1202
+ import { hex as hex5 } from "@scure/base";
1203
+ function strip0x(value) {
1204
+ return value.startsWith("0x") ? value.slice(2) : value;
1205
+ }
1206
+ function xOnly(pubKeyHex) {
1207
+ const bytes = hex5.decode(strip0x(pubKeyHex));
1208
+ if (bytes.length === 33) return bytes.slice(1);
1209
+ if (bytes.length === 32) return bytes;
1210
+ throw new Error(`invalid public key length: ${bytes.length}`);
1211
+ }
1212
+ var seconds = (value) => ({ type: "seconds", value: BigInt(value) });
1213
+ function buildArkadeVhtlcRef(input) {
1214
+ const hashLockBytes = hex5.decode(strip0x(input.hashLock));
1215
+ const params = {
1216
+ sender: xOnly(input.senderPk),
1217
+ receiver: xOnly(input.receiverPk),
1218
+ server: xOnly(input.serverPk),
1219
+ preimageHash: hashLockBytes.length === 20 ? hashLockBytes : ripemd1602(hashLockBytes),
1220
+ refundLocktime: BigInt(input.refundLocktime),
1221
+ unilateralClaimDelay: seconds(input.unilateralClaimDelay),
1222
+ unilateralRefundDelay: seconds(input.unilateralRefundDelay),
1223
+ unilateralRefundWithoutReceiverDelay: seconds(
1224
+ input.unilateralRefundWithoutReceiverDelay
1225
+ )
1226
+ };
1227
+ const vhtlc = new VHTLC.Script(params);
1228
+ return {
1229
+ ledger: "arkade",
1230
+ script: hex5.encode(vhtlc.pkScript),
1231
+ address: input.address,
1232
+ preimageHash: strip0x(input.hashLock),
1233
+ expectedSats: input.expectedSats,
1234
+ params: VHTLCContractHandler.serializeParams(params)
1235
+ };
1236
+ }
1237
+
1238
+ // src/tracker/from-swap.ts
1239
+ var ms = (seconds2) => seconds2 * 1e3;
1240
+ var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
1241
+ var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
1242
+ function arkadeLeg(r, address, expectedSats) {
1243
+ return buildArkadeVhtlcRef({
1244
+ senderPk: r.sender_pk,
1245
+ receiverPk: r.receiver_pk,
1246
+ serverPk: r.arkade_server_pk,
1247
+ hashLock: r.hash_lock,
1248
+ address,
1249
+ refundLocktime: r.vhtlc_refund_locktime,
1250
+ unilateralClaimDelay: r.unilateral_claim_delay,
1251
+ unilateralRefundDelay: r.unilateral_refund_delay,
1252
+ unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
1253
+ expectedSats
1254
+ });
1255
+ }
1256
+ function evmLeg(args) {
1257
+ return {
1258
+ ledger: "evm",
1259
+ chainId: args.chainId,
1260
+ htlc: ensure0x(args.htlc),
1261
+ preimageHash: ensure0x(args.hashLock),
1262
+ claimAddress: ensure0x(args.claimAddress),
1263
+ expectedAmount: BigInt(args.expectedSats),
1264
+ expectedToken: args.token ? ensure0x(args.token) : void 0
1265
+ };
1266
+ }
1267
+ function bitcoinLeg(address, hashLock, expectedSats) {
1268
+ return {
1269
+ ledger: "bitcoin",
1270
+ address,
1271
+ preimageHash: strip0x2(hashLock),
1272
+ expectedSats
1273
+ };
1274
+ }
1275
+ function swapToTracked(stored) {
1276
+ const r = stored.response;
1277
+ switch (r.direction) {
1278
+ // Client sends Arkade (funds the VHTLC with source_amount sats) and claims EVM.
1279
+ case "arkade_to_evm":
1280
+ return {
1281
+ swapId: r.id,
1282
+ clientHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.source_amount)),
1283
+ serverHtlc: evmLeg({
1284
+ chainId: r.evm_chain_id,
1285
+ htlc: r.evm_htlc_address,
1286
+ hashLock: r.hash_lock,
1287
+ claimAddress: r.client_evm_address,
1288
+ // the client claims the server's EVM HTLC
1289
+ expectedSats: r.evm_expected_sats,
1290
+ token: r.wbtc_address
1291
+ }),
1292
+ clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1293
+ serverRefundLocktime: ms(r.evm_refund_locktime)
1294
+ };
1295
+ // Client sends EVM and claims the Arkade VHTLC (server funds it with target_amount sats).
1296
+ case "evm_to_arkade":
1297
+ return {
1298
+ swapId: r.id,
1299
+ clientHtlc: evmLeg({
1300
+ chainId: r.evm_chain_id,
1301
+ htlc: r.evm_htlc_address,
1302
+ hashLock: r.hash_lock,
1303
+ claimAddress: r.server_evm_address,
1304
+ // the server claims the client's EVM HTLC
1305
+ expectedSats: r.evm_expected_sats
1306
+ // evm_to_arkade doesn't expose the locked token — verify amount only.
1307
+ }),
1308
+ serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
1309
+ clientRefundLocktime: ms(r.evm_refund_locktime),
1310
+ serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1311
+ };
1312
+ // Client sends BTC (funds the on-chain HTLC with source_amount sats) and claims EVM.
1313
+ case "bitcoin_to_evm":
1314
+ return {
1315
+ swapId: r.id,
1316
+ clientHtlc: bitcoinLeg(
1317
+ r.btc_htlc_address,
1318
+ r.evm_hash_lock,
1319
+ Number(r.source_amount)
1320
+ ),
1321
+ serverHtlc: evmLeg({
1322
+ chainId: r.evm_chain_id,
1323
+ htlc: r.evm_htlc_address,
1324
+ hashLock: r.evm_hash_lock,
1325
+ claimAddress: r.client_evm_address,
1326
+ // the client claims the server's EVM HTLC
1327
+ expectedSats: r.evm_expected_sats,
1328
+ token: r.wbtc_address
1329
+ }),
1330
+ clientRefundLocktime: ms(r.btc_refund_locktime),
1331
+ serverRefundLocktime: ms(r.evm_refund_locktime)
1332
+ };
1333
+ // Client sends EVM and claims the BTC HTLC (server funds it with target_amount sats).
1334
+ case "evm_to_bitcoin":
1335
+ return {
1336
+ swapId: r.id,
1337
+ clientHtlc: evmLeg({
1338
+ chainId: r.evm_chain_id,
1339
+ htlc: r.evm_htlc_address,
1340
+ hashLock: r.evm_hash_lock,
1341
+ claimAddress: r.server_evm_address,
1342
+ // the server claims the client's EVM HTLC
1343
+ expectedSats: r.evm_expected_sats,
1344
+ token: r.wbtc_address
1345
+ }),
1346
+ serverHtlc: bitcoinLeg(
1347
+ r.btc_htlc_address,
1348
+ r.evm_hash_lock,
1349
+ Number(r.target_amount)
1350
+ ),
1351
+ clientRefundLocktime: ms(r.evm_refund_locktime),
1352
+ serverRefundLocktime: ms(r.btc_refund_locktime)
1353
+ };
1354
+ // Client sends BTC (funds the on-chain HTLC) and claims the Arkade VHTLC (the
1355
+ // server funds it). Unlike the other Arkade directions, the VHTLC's receiver is
1356
+ // the client's own key (not in the response) and its funder is `server_vhtlc_pk`;
1357
+ // the hash lock is a 20-byte HASH160, which buildArkadeVhtlcRef takes as-is.
1358
+ case "btc_to_arkade":
1359
+ return {
1360
+ swapId: r.id,
1361
+ clientHtlc: bitcoinLeg(
1362
+ r.btc_htlc_address,
1363
+ r.hash_lock,
1364
+ Number(r.source_amount)
1365
+ ),
1366
+ serverHtlc: buildArkadeVhtlcRef({
1367
+ senderPk: r.server_vhtlc_pk,
1368
+ // the server funds the VHTLC
1369
+ receiverPk: stored.publicKey,
1370
+ // the client claims it
1371
+ serverPk: r.arkade_server_pk,
1372
+ hashLock: r.hash_lock,
1373
+ address: r.arkade_vhtlc_address,
1374
+ refundLocktime: r.vhtlc_refund_locktime,
1375
+ unilateralClaimDelay: r.unilateral_claim_delay,
1376
+ unilateralRefundDelay: r.unilateral_refund_delay,
1377
+ unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
1378
+ expectedSats: Number(r.target_amount)
1379
+ }),
1380
+ clientRefundLocktime: ms(r.btc_refund_locktime),
1381
+ serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1382
+ };
1383
+ // ─── Lightning: one on-chain leg, one off-chain LN payment ───────────────
1384
+ // Receive-on-Lightning: the client funds an on-chain HTLC and is paid over
1385
+ // Lightning. No server leg to watch — the server sweeping the client's leg
1386
+ // (with the preimage it got by paying the invoice) is the done signal.
1387
+ // Client funds the Arkade VHTLC; Boltz pays the invoice. The deposit is
1388
+ // `boltz_amount_sats` (source minus fees) — the amount actually locked, not
1389
+ // `source_amount` — else the client's own funding reads as under-funded.
1390
+ case "arkade_to_lightning":
1391
+ return {
1392
+ swapId: r.id,
1393
+ clientHtlc: arkadeLeg(
1394
+ r,
1395
+ r.arkade_vhtlc_address,
1396
+ Number(r.boltz_amount_sats)
1397
+ ),
1398
+ clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1399
+ serverRefundLocktime: 0
1400
+ // no on-chain server leg
1401
+ };
1402
+ // Client funds the EVM HTLC (server claims it after paying the invoice).
1403
+ case "evm_to_lightning":
1404
+ return {
1405
+ swapId: r.id,
1406
+ clientHtlc: evmLeg({
1407
+ chainId: r.evm_chain_id,
1408
+ htlc: r.evm_htlc_address,
1409
+ hashLock: r.hash_lock,
1410
+ claimAddress: r.server_evm_address,
1411
+ // the server claims the client's leg
1412
+ expectedSats: r.evm_expected_sats
1413
+ // evm_to_lightning doesn't expose the locked token — verify amount only.
1414
+ }),
1415
+ clientRefundLocktime: ms(r.evm_refund_locktime),
1416
+ serverRefundLocktime: 0
1417
+ // no on-chain server leg
1418
+ };
1419
+ // Pay-on-Lightning: the client pays a Lightning invoice (off-chain, nothing to
1420
+ // watch) and claims an on-chain HTLC. No client-funded leg — the client's
1421
+ // claim completes the swap; a hold invoice that never settles auto-unwinds.
1422
+ // Client pays the invoice, then claims the Arkade VHTLC (target_amount).
1423
+ case "lightning_to_arkade":
1424
+ return {
1425
+ swapId: r.id,
1426
+ serverHtlc: arkadeLeg(
1427
+ r,
1428
+ r.arkade_vhtlc_address,
1429
+ Number(r.target_amount)
1430
+ ),
1431
+ clientRefundLocktime: 0,
1432
+ // no on-chain client leg
1433
+ serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1434
+ };
1435
+ // Client pays the invoice, then claims the EVM HTLC.
1436
+ case "lightning_to_evm":
1437
+ return {
1438
+ swapId: r.id,
1439
+ serverHtlc: evmLeg({
1440
+ chainId: r.evm_chain_id,
1441
+ htlc: r.evm_htlc_address,
1442
+ hashLock: r.hash_lock,
1443
+ claimAddress: r.client_evm_address,
1444
+ // the client claims the server's leg
1445
+ expectedSats: r.evm_expected_sats,
1446
+ token: r.wbtc_address
1447
+ }),
1448
+ clientRefundLocktime: 0,
1449
+ // no on-chain client leg
1450
+ serverRefundLocktime: ms(r.evm_refund_locktime)
1451
+ };
1452
+ default:
1453
+ return void 0;
1454
+ }
1455
+ }
1456
+
1457
+ // src/tracker/swap-tracker.ts
1458
+ var SwapTracker = class {
1459
+ #managers;
1460
+ #swaps = /* @__PURE__ */ new Map();
1461
+ /** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
1462
+ #lastActions = /* @__PURE__ */ new Map();
1463
+ #subscribers = /* @__PURE__ */ new Set();
1464
+ #refreshIntervalMs;
1465
+ #eventUnsubs = [];
1466
+ #timer;
1467
+ constructor(managers, options) {
1468
+ this.#managers = managers;
1469
+ this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
1470
+ }
1471
+ /**
1472
+ * Whether every on-chain leg of `swap` is observable — i.e. the tracker can
1473
+ * derive its status from chain. `false` means a leg is on a ledger/chain this
1474
+ * client can't reach (no manager, or an EVM chain with no RPC), so the swap
1475
+ * can't be tracked. Pure predicate — callers decide the policy (skip vs reject).
1476
+ */
1477
+ canObserve(swap) {
1478
+ return legsOf(swap).every(
1479
+ (leg) => this.#managers.get(leg.ledger)?.canObserve(leg) ?? false
1480
+ );
1481
+ }
1482
+ /** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
1483
+ async startTracking(swaps) {
1484
+ for (const swap of swaps) {
1485
+ if (!this.canObserve(swap)) {
1486
+ console.warn(
1487
+ `SwapTracker: skipping swap ${swap.swapId} \u2014 a leg is on a chain this client can't reach (configure it to track this swap)`
1488
+ );
1489
+ continue;
1490
+ }
1491
+ this.#swaps.set(swap.swapId, swap);
1492
+ for (const leg of legsOf(swap)) await this.#managerFor(leg).register(leg);
1493
+ }
1494
+ for (const manager of new Set(this.#managers.values())) {
1495
+ this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
1496
+ }
1497
+ await this.#refreshManagers();
1498
+ this.#recomputeAll();
1499
+ if (this.#refreshIntervalMs > 0)
1500
+ this.#timer = setInterval(
1501
+ () => void this.#tick(),
1502
+ this.#refreshIntervalMs
1503
+ );
1504
+ }
1505
+ /**
1506
+ * Track one more swap after {@link startTracking} — register its on-chain
1507
+ * leg(s), seed the managers they touch, and derive its first action. Idempotent:
1508
+ * a swap already tracked, or one already seen through to a terminal action, is
1509
+ * ignored. New swaps are picked up by the existing per-manager event listeners
1510
+ * and poll timer, so nothing else needs re-wiring. Lets a swap created after
1511
+ * start (e.g. via `createSwap`) be tracked without a restart.
1512
+ */
1513
+ async track(swap) {
1514
+ if (this.#swaps.has(swap.swapId) || this.#lastActions.has(swap.swapId))
1515
+ return;
1516
+ if (!this.canObserve(swap)) {
1517
+ console.warn(
1518
+ `SwapTracker: skipping swap ${swap.swapId} \u2014 a leg is on a chain this client can't reach (configure it to track this swap)`
1519
+ );
1520
+ return;
1521
+ }
1522
+ this.#swaps.set(swap.swapId, swap);
1523
+ const registered = [];
1524
+ try {
1525
+ for (const leg of legsOf(swap)) {
1526
+ await this.#managerFor(leg).register(leg);
1527
+ registered.push(leg);
1528
+ }
1529
+ } catch (error) {
1530
+ this.#swaps.delete(swap.swapId);
1531
+ for (const leg of registered) void this.#managerFor(leg).unregister(leg);
1532
+ throw error;
1533
+ }
1534
+ const managers = new Set(legsOf(swap).map((leg) => this.#managerFor(leg)));
1535
+ await Promise.all([...managers].map((manager) => manager.refresh()));
1536
+ this.#recompute(swap);
1537
+ }
1538
+ async #tick() {
1539
+ await this.#refreshManagers();
1540
+ this.#recomputeAll();
1541
+ }
1542
+ /**
1543
+ * Re-poll the managers that have a registered leg — seeding/advancing their
1544
+ * clocks and reconciling observations. Managers with nothing tracked are
1545
+ * skipped: refreshing them still hits their clock source (e.g. Arkade/Bitcoin
1546
+ * MTP via the API), so a swap that doesn't touch a ledger can't fail on that
1547
+ * ledger's endpoint being down (the default client builds every manager).
1548
+ */
1549
+ async #refreshManagers() {
1550
+ const active = /* @__PURE__ */ new Set();
1551
+ for (const swap of this.#swaps.values())
1552
+ for (const leg of legsOf(swap)) active.add(leg.ledger);
1553
+ const managers = new Set(
1554
+ [...active].map((ledger) => this.#managers.get(ledger)).filter((m) => m !== void 0)
1555
+ );
1556
+ await Promise.all([...managers].map((manager) => manager.refresh()));
1557
+ }
1558
+ /** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
1559
+ trackedSwapIds() {
1560
+ return [...this.#swaps.keys()];
1561
+ }
1562
+ /**
1563
+ * Re-verify one swap against the chain right now — the hook a faster server
1564
+ * status hint drives. Reconciles just that swap's legs (targeted, on the
1565
+ * managers), then recomputes its action. The hint is only a *trigger*: the
1566
+ * chain stays the source of truth, so a premature hint that the chain doesn't
1567
+ * yet reflect changes nothing. A no-op for an untracked swap.
1568
+ */
1569
+ async applyHint(swapId, opts) {
1570
+ const swap = this.#swaps.get(swapId);
1571
+ if (!swap) return;
1572
+ await Promise.all(
1573
+ legsOf(swap).map((leg) => this.#managerFor(leg).reconcile(leg))
1574
+ );
1575
+ this.#recompute(swap, opts?.force ?? false);
1576
+ }
1577
+ /** Notify `cb` of the current action for each tracked swap, then on every change. */
1578
+ subscribeToActions(cb) {
1579
+ this.#subscribers.add(cb);
1580
+ for (const [swapId, actions] of this.#lastActions) cb(swapId, actions);
1581
+ return () => this.#subscribers.delete(cb);
1582
+ }
1583
+ /**
1584
+ * Stop tracking: unregister every still-tracked leg (releasing its manager's
1585
+ * watch — e.g. the EVM per-chain block watch that only `unregister` tears down),
1586
+ * drop the poll timer, and detach event listeners + subscribers. Mirrors
1587
+ * `startTracking`'s `register`, so no manager keeps watching a stopped tracker's
1588
+ * swaps. The managers themselves are not owned here, so they are unregistered but
1589
+ * never disposed.
1590
+ */
1591
+ stop() {
1592
+ if (this.#timer) clearInterval(this.#timer);
1593
+ this.#timer = void 0;
1594
+ for (const unsub of this.#eventUnsubs) unsub();
1595
+ this.#eventUnsubs = [];
1596
+ for (const swap of this.#swaps.values())
1597
+ for (const leg of legsOf(swap))
1598
+ void this.#managerFor(leg).unregister(leg);
1599
+ this.#swaps.clear();
1600
+ this.#subscribers.clear();
1601
+ }
1602
+ #managerFor(ref) {
1603
+ const manager = this.#managers.get(ref.ledger);
1604
+ if (!manager)
1605
+ throw new Error(`no ContractManager for ledger '${ref.ledger}'`);
1606
+ return manager;
1607
+ }
1608
+ #recomputeAll() {
1609
+ for (const swap of this.#swaps.values()) this.#recompute(swap);
1610
+ }
1611
+ #recompute(swap, force = false) {
1612
+ let clientHtlc;
1613
+ let serverHtlc;
1614
+ let clientChainNow = 0;
1615
+ let serverChainNow = 0;
1616
+ if (swap.clientHtlc) {
1617
+ const m = this.#managerFor(swap.clientHtlc);
1618
+ clientHtlc = m.getState(swap.clientHtlc);
1619
+ const now = m.chainNow(swap.clientHtlc);
1620
+ if (clientHtlc === void 0 || now === void 0) return;
1621
+ clientChainNow = now;
1622
+ }
1623
+ if (swap.serverHtlc) {
1624
+ const m = this.#managerFor(swap.serverHtlc);
1625
+ serverHtlc = m.getState(swap.serverHtlc);
1626
+ const now = m.chainNow(swap.serverHtlc);
1627
+ if (serverHtlc === void 0 || now === void 0) return;
1628
+ serverChainNow = now;
1629
+ }
1630
+ const status = deriveSwapStatus({ clientHtlc, serverHtlc });
1631
+ if (status === void 0) return;
1632
+ const actions = deriveSwapActions({
1633
+ status,
1634
+ clientChainNow,
1635
+ serverChainNow,
1636
+ clientRefundLocktime: swap.clientRefundLocktime,
1637
+ serverRefundLocktime: swap.serverRefundLocktime,
1638
+ // Pay-on-Lightning swaps have no client-funded on-chain leg.
1639
+ clientFunds: swap.clientHtlc !== void 0
1640
+ });
1641
+ const previous = this.#lastActions.get(swap.swapId);
1642
+ if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
1643
+ return;
1644
+ this.#lastActions.set(swap.swapId, actions);
1645
+ for (const cb of this.#subscribers) cb(swap.swapId, actions);
1646
+ if (actions.recommended === "none") this.#untrack(swap);
1647
+ }
1648
+ #untrack(swap) {
1649
+ this.#swaps.delete(swap.swapId);
1650
+ for (const leg of legsOf(swap)) void this.#managerFor(leg).unregister(leg);
1651
+ }
1652
+ };
1653
+ function legsOf(swap) {
1654
+ const legs = [];
1655
+ if (swap.clientHtlc) legs.push(swap.clientHtlc);
1656
+ if (swap.serverHtlc) legs.push(swap.serverHtlc);
1657
+ return legs;
1658
+ }
1659
+
1660
+ // src/client.ts
1661
+ var Client = class {
1662
+ /** The wrapped legacy client. Calls are forwarded here until migrated. */
1663
+ #legacy;
1664
+ /** Tracking configuration; managers are built lazily from it on first use. */
1665
+ #tracking;
1666
+ /** Per-ledger monitors, resolved once from {@link #tracking}. */
1667
+ #managers;
1668
+ #tracker;
1669
+ /** Hint-driven auto-claim worker; only built when `withAutoClaim` opted in. */
1670
+ #worker;
1671
+ #started = false;
1672
+ constructor(...args) {
1673
+ if (args[0] instanceof LegacyClient) {
1674
+ this.#legacy = args[0];
1675
+ this.#tracking = args[1] ?? {
1676
+ enabled: false
1677
+ };
1678
+ } else {
1679
+ this.#legacy = new LegacyClient(
1680
+ ...args
1681
+ );
1682
+ this.#tracking = { enabled: false };
1683
+ }
1684
+ }
1685
+ /** Start building a {@link Client}. */
1686
+ static builder() {
1687
+ return new ClientBuilder();
1688
+ }
1689
+ /** Escape hatch: the underlying legacy client, during migration. */
1690
+ get legacy() {
1691
+ return this.#legacy;
1692
+ }
1693
+ // --- delegated legacy surface (getters) ---
1694
+ /** Delegated to the legacy client (migration checkpoint). */
1695
+ get api() {
1696
+ return this.#legacy.api;
1697
+ }
1698
+ /** Delegated to the legacy client (migration checkpoint). */
1699
+ get baseUrl() {
1700
+ return this.#legacy.baseUrl;
1701
+ }
1702
+ /** Delegated to the legacy client (migration checkpoint). */
1703
+ get cctpInbound() {
1704
+ return this.#legacy.cctpInbound;
1705
+ }
1706
+ /** Delegated to the legacy client (migration checkpoint). */
1707
+ get swapStorage() {
1708
+ return this.#legacy.swapStorage;
1709
+ }
1710
+ // --- delegated legacy surface (methods) ---
1711
+ /** Delegated to the legacy client (migration checkpoint). */
1712
+ subscribeToSwaps(...args) {
1713
+ return this.#legacy.subscribeToSwaps(...args);
1714
+ }
1715
+ /** Delegated to the legacy client (migration checkpoint). */
1716
+ unsubscribeFromSwaps(...args) {
1717
+ return this.#legacy.unsubscribeFromSwaps(...args);
1718
+ }
1719
+ /** Delegated to the legacy client (migration checkpoint). */
1720
+ closeSwapStatusSocket(...args) {
1721
+ return this.#legacy.closeSwapStatusSocket(...args);
1722
+ }
1723
+ /** Delegated to the legacy client (migration checkpoint). */
1724
+ getMnemonic(...args) {
1725
+ return this.#legacy.getMnemonic(...args);
1726
+ }
1727
+ /** Delegated to the legacy client (migration checkpoint). */
1728
+ loadMnemonic(...args) {
1729
+ return this.#legacy.loadMnemonic(...args);
1730
+ }
1731
+ /** Delegated to the legacy client (migration checkpoint). */
1732
+ getNostrKeyHex(...args) {
1733
+ return this.#legacy.getNostrKeyHex(...args);
1734
+ }
1735
+ /** Delegated to the legacy client (migration checkpoint). */
1736
+ getUserIdXpub(...args) {
1737
+ return this.#legacy.getUserIdXpub(...args);
1738
+ }
1739
+ /** Delegated to the legacy client (migration checkpoint). */
1740
+ deriveSwapParams(...args) {
1741
+ return this.#legacy.deriveSwapParams(...args);
1742
+ }
1743
+ /** Delegated to the legacy client (migration checkpoint). */
1744
+ deriveSwapParamsAtIndex(...args) {
1745
+ return this.#legacy.deriveSwapParamsAtIndex(...args);
1746
+ }
1747
+ /** Delegated to the legacy client (migration checkpoint). */
1748
+ getEvmAddress(...args) {
1749
+ return this.#legacy.getEvmAddress(...args);
1750
+ }
1751
+ /** Delegated to the legacy client (migration checkpoint). */
1752
+ getKeyIndex(...args) {
1753
+ return this.#legacy.getKeyIndex(...args);
1754
+ }
1755
+ /** Delegated to the legacy client (migration checkpoint). */
1756
+ setKeyIndex(...args) {
1757
+ return this.#legacy.setKeyIndex(...args);
1758
+ }
1759
+ /** Delegated to the legacy client (migration checkpoint). */
1760
+ getStatus(...args) {
1761
+ return this.#legacy.getStatus(...args);
1762
+ }
1763
+ /** Delegated to the legacy client (migration checkpoint). */
1764
+ healthCheck(...args) {
1765
+ return this.#legacy.healthCheck(...args);
1766
+ }
1767
+ /** Delegated to the legacy client (migration checkpoint). */
1768
+ getVersion(...args) {
1769
+ return this.#legacy.getVersion(...args);
1770
+ }
1771
+ /** Delegated to the legacy client (migration checkpoint). */
1772
+ getSupportAgents(...args) {
1773
+ return this.#legacy.getSupportAgents(...args);
1774
+ }
1775
+ /** Delegated to the legacy client (migration checkpoint). */
1776
+ getMtp(...args) {
1777
+ return this.#legacy.getMtp(...args);
1778
+ }
1779
+ /** Delegated to the legacy client (migration checkpoint). */
1780
+ getTokens(...args) {
1781
+ return this.#legacy.getTokens(...args);
1782
+ }
1783
+ /** Delegated to the legacy client (migration checkpoint). */
1784
+ getSwapPairs(...args) {
1785
+ return this.#legacy.getSwapPairs(...args);
1786
+ }
1787
+ /** Delegated to the legacy client (migration checkpoint). */
1788
+ getChainConfig(...args) {
1789
+ return this.#legacy.getChainConfig(...args);
1790
+ }
1791
+ /** Delegated to the legacy client (migration checkpoint). */
1792
+ getReferralFee(...args) {
1793
+ return this.#legacy.getReferralFee(...args);
1794
+ }
1795
+ /** Delegated to the legacy client (migration checkpoint). */
1796
+ getNetworkFees(...args) {
1797
+ return this.#legacy.getNetworkFees(...args);
1798
+ }
1799
+ /** Delegated to the legacy client (migration checkpoint). */
1800
+ getDexQuote(...args) {
1801
+ return this.#legacy.getDexQuote(...args);
1802
+ }
1803
+ /** Delegated to the legacy client (migration checkpoint). */
1804
+ composeQuote(...args) {
1805
+ return this.#legacy.composeQuote(...args);
1806
+ }
1807
+ /** Delegated to the legacy client (migration checkpoint). */
1808
+ getQuote(...args) {
1809
+ return this.#legacy.getQuote(...args);
1810
+ }
1811
+ /** Delegated to the legacy client (migration checkpoint). */
1812
+ getSwap(...args) {
1813
+ return this.#legacy.getSwap(...args);
1814
+ }
1815
+ /** Delegated to the legacy client (migration checkpoint). */
1816
+ getBulkStatus(...args) {
1817
+ return this.#legacy.getBulkStatus(...args);
1818
+ }
1819
+ /** Delegated to the legacy client (migration checkpoint). */
1820
+ getStoredSwap(...args) {
1821
+ return this.#legacy.getStoredSwap(...args);
1822
+ }
1823
+ /** Delegated to the legacy client (migration checkpoint). */
1824
+ listAllSwaps(...args) {
1825
+ return this.#legacy.listAllSwaps(...args);
1826
+ }
1827
+ /** Delegated to the legacy client (migration checkpoint). */
1828
+ deleteSwap(...args) {
1829
+ return this.#legacy.deleteSwap(...args);
1830
+ }
1831
+ /** Delegated to the legacy client (migration checkpoint). */
1832
+ clearSwapStorage(...args) {
1833
+ return this.#legacy.clearSwapStorage(...args);
1834
+ }
1835
+ /** Delegated to the legacy client (migration checkpoint). */
1836
+ recoverSwaps(...args) {
1837
+ return this.#legacy.recoverSwaps(...args);
1838
+ }
1839
+ /** Delegated to the legacy client (migration checkpoint). */
1840
+ recoverAllSwaps(...args) {
1841
+ return this.#legacy.recoverAllSwaps(...args);
1842
+ }
1843
+ /** Delegated to the legacy client (migration checkpoint). */
1844
+ amountsForSwap(...args) {
1845
+ return this.#legacy.amountsForSwap(...args);
1846
+ }
1847
+ /** Delegated to the legacy client (migration checkpoint). */
1848
+ claim(...args) {
1849
+ return this.#legacy.claim(...args);
1850
+ }
1851
+ /** Delegated to the legacy client (migration checkpoint). */
1852
+ claimViaGasless(...args) {
1853
+ return this.#legacy.claimViaGasless(...args);
1854
+ }
1855
+ /** Delegated to the legacy client (migration checkpoint). */
1856
+ claimArkade(...args) {
1857
+ return this.#legacy.claimArkade(...args);
1858
+ }
1859
+ /** Delegated to the legacy client (migration checkpoint). */
1860
+ continueArkadeClaimSwap(...args) {
1861
+ return this.#legacy.continueArkadeClaimSwap(...args);
1862
+ }
1863
+ /** Delegated to the legacy client (migration checkpoint). */
1864
+ refundSwap(...args) {
1865
+ return this.#legacy.refundSwap(...args);
1866
+ }
1867
+ /** Delegated to the legacy client (migration checkpoint). */
1868
+ getCollabRefundEvmParams(...args) {
1869
+ return this.#legacy.getCollabRefundEvmParams(...args);
1870
+ }
1871
+ /** Delegated to the legacy client (migration checkpoint). */
1872
+ buildCollabRefundEvmTypedData(...args) {
1873
+ return this.#legacy.buildCollabRefundEvmTypedData(...args);
1874
+ }
1875
+ /** Delegated to the legacy client (migration checkpoint). */
1876
+ collabRefundEvmSwap(...args) {
1877
+ return this.#legacy.collabRefundEvmSwap(...args);
1878
+ }
1879
+ /** Delegated to the legacy client (migration checkpoint). */
1880
+ submitCollabRefundEvm(...args) {
1881
+ return this.#legacy.submitCollabRefundEvm(...args);
1882
+ }
1883
+ /** Delegated to the legacy client (migration checkpoint). */
1884
+ createSwap(...args) {
1885
+ return this.#trackAfterCreate(this.#legacy.createSwap(...args));
1886
+ }
1887
+ /** Delegated to the legacy client (migration checkpoint). */
1888
+ createArkadeToEvmSwapGeneric(...args) {
1889
+ return this.#trackAfterCreate(
1890
+ this.#legacy.createArkadeToEvmSwapGeneric(...args)
1891
+ );
1892
+ }
1893
+ /** Delegated to the legacy client (migration checkpoint). */
1894
+ createLightningToEvmSwapGeneric(...args) {
1895
+ return this.#trackAfterCreate(
1896
+ this.#legacy.createLightningToEvmSwapGeneric(...args)
1897
+ );
1898
+ }
1899
+ /** Delegated to the legacy client (migration checkpoint). */
1900
+ createBitcoinToEvmSwap(...args) {
1901
+ return this.#trackAfterCreate(this.#legacy.createBitcoinToEvmSwap(...args));
1902
+ }
1903
+ /** Delegated to the legacy client (migration checkpoint). */
1904
+ createBitcoinToArkadeSwap(...args) {
1905
+ return this.#trackAfterCreate(
1906
+ this.#legacy.createBitcoinToArkadeSwap(...args)
1907
+ );
1908
+ }
1909
+ /** Delegated to the legacy client (migration checkpoint). */
1910
+ createLightningToArkadeSwap(...args) {
1911
+ return this.#trackAfterCreate(
1912
+ this.#legacy.createLightningToArkadeSwap(...args)
1913
+ );
1914
+ }
1915
+ /** Delegated to the legacy client (migration checkpoint). */
1916
+ createArkadeToLightningSwap(...args) {
1917
+ return this.#trackAfterCreate(
1918
+ this.#legacy.createArkadeToLightningSwap(...args)
1919
+ );
1920
+ }
1921
+ /** Delegated to the legacy client (migration checkpoint). */
1922
+ getArkadeToLightningQuote(...args) {
1923
+ return this.#legacy.getArkadeToLightningQuote(...args);
1924
+ }
1925
+ /** Delegated to the legacy client (migration checkpoint). */
1926
+ retryArkadeToLightningSwap(...args) {
1927
+ return this.#trackAfterRetry(
1928
+ this.#legacy.retryArkadeToLightningSwap(...args)
1929
+ );
1930
+ }
1931
+ /** Delegated to the legacy client (migration checkpoint). */
1932
+ createEvmToArkadeSwapGeneric(...args) {
1933
+ return this.#trackAfterCreate(
1934
+ this.#legacy.createEvmToArkadeSwapGeneric(...args)
1935
+ );
1936
+ }
1937
+ /** Delegated to the legacy client (migration checkpoint). */
1938
+ createEvmToBitcoinSwap(...args) {
1939
+ return this.#trackAfterCreate(this.#legacy.createEvmToBitcoinSwap(...args));
1940
+ }
1941
+ /** Delegated to the legacy client (migration checkpoint). */
1942
+ createEvmToLightningSwapGeneric(...args) {
1943
+ return this.#trackAfterCreate(
1944
+ this.#legacy.createEvmToLightningSwapGeneric(...args)
1945
+ );
1946
+ }
1947
+ /** Delegated to the legacy client (migration checkpoint). */
1948
+ getCoordinatorFundingCallDataPermit2(...args) {
1949
+ return this.#legacy.getCoordinatorFundingCallDataPermit2(...args);
1950
+ }
1951
+ /** Delegated to the legacy client (migration checkpoint). */
1952
+ getPermit2FundingParamsUnsigned(...args) {
1953
+ return this.#legacy.getPermit2FundingParamsUnsigned(...args);
1954
+ }
1955
+ /** Delegated to the legacy client (migration checkpoint). */
1956
+ fundSwap(...args) {
1957
+ return this.#legacy.fundSwap(...args);
1958
+ }
1959
+ /** Delegated to the legacy client (migration checkpoint). */
1960
+ refundEvmWithSigner(...args) {
1961
+ return this.#legacy.refundEvmWithSigner(...args);
1962
+ }
1963
+ /** Delegated to the legacy client (migration checkpoint). */
1964
+ collabRefundEvmWithSigner(...args) {
1965
+ return this.#legacy.collabRefundEvmWithSigner(...args);
1966
+ }
1967
+ /** Delegated to the legacy client (migration checkpoint). */
1968
+ fundSwapGasless(...args) {
1969
+ return this.#legacy.fundSwapGasless(...args);
1970
+ }
1971
+ /** Delegated to the legacy client (migration checkpoint). */
1972
+ getSwapDepositorKey(...args) {
1973
+ return this.#legacy.getSwapDepositorKey(...args);
1974
+ }
1975
+ /** Delegated to the legacy client (migration checkpoint). */
1976
+ getEvmDepositorKey(...args) {
1977
+ return this.#legacy.getEvmDepositorKey(...args);
1978
+ }
1979
+ /** Delegated to the legacy client (migration checkpoint). */
1980
+ getRefundedEvmSwapContinuation(...args) {
1981
+ return this.#legacy.getRefundedEvmSwapContinuation(...args);
1982
+ }
1983
+ /** Delegated to the legacy client (migration checkpoint). */
1984
+ continueRefundedEvmSwap(...args) {
1985
+ return this.#trackAfterRetry(this.#legacy.continueRefundedEvmSwap(...args));
1986
+ }
1987
+ /** Delegated to the legacy client (migration checkpoint). */
1988
+ recoverGaslessFunds(...args) {
1989
+ return this.#legacy.recoverGaslessFunds(...args);
1990
+ }
1991
+ /** Delegated to the legacy client (migration checkpoint). */
1992
+ hasReceivedVtxo(...args) {
1993
+ return this.#legacy.hasReceivedVtxo(...args);
1994
+ }
1995
+ // --- Satora-native features go below ---
1996
+ /**
1997
+ * Start observing the user's active swaps and deriving each one's next action.
1998
+ *
1999
+ * Loads stored swaps, maps the ones whose ledgers are observable to
2000
+ * {@link TrackedSwap}s, and runs the per-ledger monitors. Idempotent. Requires
2001
+ * the client to have been built with {@link ClientBuilder.withTracking}.
2002
+ * Subscribe with {@link subscribeToActions}; release with
2003
+ * {@link stopTracking}.
2004
+ */
2005
+ async startTracking() {
2006
+ if (!this.#tracking.enabled)
2007
+ throw new Error(
2008
+ "tracking is disabled \u2014 remove .withoutTracking() to enable it"
2009
+ );
2010
+ if (this.#started) return;
2011
+ this.#started = true;
2012
+ try {
2013
+ const tracker = new SwapTracker(await this.#ensureManagers(), {
2014
+ refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
2015
+ });
2016
+ this.#tracker = tracker;
2017
+ await tracker.startTracking(await this.#loadTrackedSwaps());
2018
+ this.#startWorker(tracker);
2019
+ } catch (error) {
2020
+ this.#worker?.stop();
2021
+ this.#worker = void 0;
2022
+ this.#tracker?.stop();
2023
+ this.#tracker = void 0;
2024
+ this.#started = false;
2025
+ throw error;
2026
+ }
2027
+ }
2028
+ /** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
2029
+ async #loadTrackedSwaps() {
2030
+ const swaps = await this.listAllSwaps();
2031
+ return swaps.map(swapToTracked).filter((s) => s !== void 0);
2032
+ }
2033
+ /**
2034
+ * Run a create, then fold the new swap into tracking.
2035
+ *
2036
+ * When tracking is active, first REJECT a swap this client can't reach: a leg on
2037
+ * an unconfigured chain can be neither observed nor claimed, so creating it would
2038
+ * strand funds. Throwing here (before the caller funds it) is deliberately
2039
+ * stricter than {@link startTracking}, which only *skips* such a pre-existing
2040
+ * swap. Otherwise fold the new swap(s) in — fire-and-forget, so a tracking hiccup
2041
+ * never fails or delays the create; a no-op when tracking isn't running (a later
2042
+ * {@link startTracking} picks it up from storage).
2043
+ */
2044
+ async #trackAfterCreate(op) {
2045
+ const result = await op;
2046
+ const tracker = this.#tracker;
2047
+ if (!tracker) return result;
2048
+ let swaps;
2049
+ try {
2050
+ swaps = await this.#loadTrackedSwaps();
2051
+ } catch (error) {
2052
+ console.warn("Client: loading swaps after create failed:", error);
2053
+ return result;
2054
+ }
2055
+ const created = swaps.find((s) => s.swapId === createdSwapId(result));
2056
+ if (created && !tracker.canObserve(created))
2057
+ throw new Error(
2058
+ `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.`
2059
+ );
2060
+ void this.#trackAll(tracker, swaps);
2061
+ return result;
2062
+ }
2063
+ /**
2064
+ * Run a retry, then fold its replacement swap into tracking.
2065
+ *
2066
+ * Unlike a create there is no unreachable-leg
2067
+ * rejection: by the time the retry returns, the old VHTLC has already been
2068
+ * collab-refunded into the new one, so refusing here can't protect any funds
2069
+ * — and {@link SwapTracker.track} skips a swap it can't observe anyway.
2070
+ */
2071
+ async #trackAfterRetry(op) {
2072
+ const result = await op;
2073
+ const tracker = this.#tracker;
2074
+ if (!tracker) return result;
2075
+ try {
2076
+ void this.#trackAll(tracker, await this.#loadTrackedSwaps());
2077
+ } catch (error) {
2078
+ console.warn("Client: loading swaps after retry failed:", error);
2079
+ }
2080
+ return result;
2081
+ }
2082
+ /** Track each given swap the tracker isn't already watching (isolated per swap). */
2083
+ async #trackAll(tracker, swaps) {
2084
+ for (const swap of swaps) {
2085
+ try {
2086
+ await tracker.track(swap);
2087
+ } catch (error) {
2088
+ console.warn(`Client: tracking swap ${swap.swapId} failed:`, error);
2089
+ }
2090
+ }
2091
+ }
2092
+ /**
2093
+ * If auto-claim was opted in, wire the server status WebSocket into a
2094
+ * {@link SwapWorker}: hints trigger a chain re-verify, and a swap the chain
2095
+ * confirms claimable is auto-claimed; `fund`/refund actions are surfaced via the
2096
+ * configured `onActionRequired`. A no-op unless `withAutoClaim` was set and a
2097
+ * base URL is configured (the WebSocket needs a server to reach).
2098
+ */
2099
+ #startWorker(tracker) {
2100
+ const autoClaim = this.#tracking.autoClaim;
2101
+ if (!autoClaim) return;
2102
+ if (!this.baseUrl) return;
2103
+ const hintSource = new WsStatusSource({ serverUrl: this.baseUrl });
2104
+ const worker = new SwapWorker({
2105
+ tracker,
2106
+ hintSource,
2107
+ execute: async (swapId, actionId) => {
2108
+ if (actionId !== "claim")
2109
+ throw new Error(`refusing to auto-run action '${actionId}'`);
2110
+ const result = await this.claim(swapId);
2111
+ if (!result.success) throw new Error(result.message ?? "claim failed");
2112
+ },
2113
+ onActionRequired: autoClaim.onActionRequired
2114
+ });
2115
+ this.#worker = worker;
2116
+ worker.start();
2117
+ }
2118
+ /**
2119
+ * Subscribe to next-action updates: `cb` fires with the current action for each
2120
+ * tracked swap immediately, then on every change. Call after
2121
+ * {@link startTracking}. Returns an unsubscribe fn.
2122
+ */
2123
+ subscribeToActions(cb) {
2124
+ if (!this.#tracker)
2125
+ throw new Error("call startTracking() before subscribeToActions()");
2126
+ return this.#tracker.subscribeToActions(cb);
2127
+ }
2128
+ /** Stop tracking and drop subscribers. The managers themselves are not disposed. */
2129
+ stopTracking() {
2130
+ this.#worker?.stop();
2131
+ this.#worker = void 0;
2132
+ this.#tracker?.stop();
2133
+ this.#tracker = void 0;
2134
+ this.#started = false;
2135
+ }
2136
+ /**
2137
+ * Resolve the per-ledger managers once — the explicit override if given, else
2138
+ * auto-built from config: an {@link ArkadeContractManager} from the configured
2139
+ * Ark server URL (defaulting to the mainnet server), clocked by `getMtp`, and an
2140
+ * {@link EvmContractManager} from the tested default RPCs.
2141
+ */
2142
+ async #ensureManagers() {
2143
+ if (this.#managers) return this.#managers;
2144
+ if (this.#tracking.managers) {
2145
+ this.#managers = this.#tracking.managers;
2146
+ return this.#managers;
2147
+ }
2148
+ const managers = /* @__PURE__ */ new Map();
2149
+ const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
2150
+ const chainTime = async () => (await this.getMtp()).mtp * 1e3;
2151
+ const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
2152
+ if (arkadeUrl) {
2153
+ managers.set(
2154
+ "arkade",
2155
+ await ArkadeContractManager.create({ serverUrl: arkadeUrl, chainTime })
2156
+ );
2157
+ }
2158
+ const readers = defaultEvmReaders(evmRpcUrls);
2159
+ if (readers.size > 0)
2160
+ managers.set("evm", EvmContractManager.fromDeps({ readers }));
2161
+ managers.set(
2162
+ "bitcoin",
2163
+ await BitcoinContractManager.create({
2164
+ esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
2165
+ chainTime
2166
+ })
2167
+ );
2168
+ this.#managers = managers;
2169
+ return managers;
2170
+ }
2171
+ };
2172
+ var ClientBuilder = class {
2173
+ #inner = LegacyClient.builder();
2174
+ // Observe-mode tracking is on by default; the built client auto-builds its
2175
+ // managers from config unless disabled or given an explicit override.
2176
+ #trackingEnabled = true;
2177
+ #arkadeServerUrl;
2178
+ #esploraUrl;
2179
+ #evmRpcUrls;
2180
+ #managers;
2181
+ #autoClaim;
2182
+ /**
2183
+ * Override the EVM JSON-RPC endpoint per chainId. Optional — tracking uses
2184
+ * tested public defaults otherwise; an override is tried first, with the
2185
+ * defaults kept as fallbacks.
2186
+ */
2187
+ withEvmRpcUrls(urls) {
2188
+ this.#evmRpcUrls = urls;
2189
+ return this;
2190
+ }
2191
+ /**
2192
+ * Opt in to hint-driven auto-claim. Tracking then also subscribes to the
2193
+ * server status WebSocket (a faster trigger than the chain poll) and, when the
2194
+ * chain confirms a swap is claimable, claims it automatically. Actions that
2195
+ * need the user — a manual `fund`, or a refund to confirm — are surfaced via
2196
+ * `onActionRequired` instead of being run. Off by default: this auto-spends on
2197
+ * the user's behalf, so it must be explicit.
2198
+ */
2199
+ withAutoClaim(options) {
2200
+ this.#autoClaim = { onActionRequired: options?.onActionRequired };
2201
+ return this;
2202
+ }
2203
+ /** Turn observe-mode tracking off. */
2204
+ withoutTracking() {
2205
+ this.#trackingEnabled = false;
2206
+ return this;
2207
+ }
2208
+ /**
2209
+ * Advanced: supply per-ledger {@link ContractManager}s explicitly instead of
2210
+ * letting the client auto-build them from config (useful for tests or custom
2211
+ * chain sources).
2212
+ */
2213
+ withContractManagers(managers) {
2214
+ this.#managers = managers;
2215
+ return this;
2216
+ }
2217
+ withBaseUrl(...args) {
2218
+ this.#inner.withBaseUrl(...args);
2219
+ return this;
2220
+ }
2221
+ withReferralCode(...args) {
2222
+ this.#inner.withReferralCode(...args);
2223
+ return this;
2224
+ }
2225
+ withOrgCode(...args) {
2226
+ this.#inner.withOrgCode(...args);
2227
+ return this;
2228
+ }
2229
+ withDefaultHeaders(...args) {
2230
+ this.#inner.withDefaultHeaders(...args);
2231
+ return this;
2232
+ }
2233
+ /**
2234
+ * Override the Esplora REST URL. Optional for tracking — the Bitcoin manager
2235
+ * otherwise defaults to mainnet mempool.space; set this for a dev/regtest node.
2236
+ */
2237
+ withEsploraUrl(url) {
2238
+ this.#inner.withEsploraUrl(url);
2239
+ this.#esploraUrl = url;
2240
+ return this;
2241
+ }
2242
+ /**
2243
+ * Override the Ark server URL. Optional for tracking — it otherwise defaults to
2244
+ * the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
2245
+ */
2246
+ withArkadeServerUrl(url) {
2247
+ this.#inner.withArkadeServerUrl(url);
2248
+ this.#arkadeServerUrl = url;
2249
+ return this;
2250
+ }
2251
+ withSignerStorage(...args) {
2252
+ this.#inner.withSignerStorage(...args);
2253
+ return this;
2254
+ }
2255
+ withSwapStorage(...args) {
2256
+ this.#inner.withSwapStorage(...args);
2257
+ return this;
2258
+ }
2259
+ withMnemonic(...args) {
2260
+ this.#inner.withMnemonic(...args);
2261
+ return this;
2262
+ }
2263
+ withXprv(...args) {
2264
+ this.#inner.withXprv(...args);
2265
+ return this;
2266
+ }
2267
+ withAa(...args) {
2268
+ this.#inner.withAa(...args);
2269
+ return this;
2270
+ }
2271
+ withLogger(...args) {
2272
+ this.#inner.withLogger(...args);
2273
+ return this;
2274
+ }
2275
+ withLogLevel(...args) {
2276
+ this.#inner.withLogLevel(...args);
2277
+ return this;
2278
+ }
2279
+ async build() {
2280
+ return new Client(await this.#inner.build(), {
2281
+ enabled: this.#trackingEnabled,
2282
+ managers: this.#managers,
2283
+ arkadeServerUrl: this.#arkadeServerUrl,
2284
+ esploraUrl: this.#esploraUrl,
2285
+ evmRpcUrls: this.#evmRpcUrls,
2286
+ autoClaim: this.#autoClaim
2287
+ });
2288
+ }
2289
+ };
2290
+ function createdSwapId(result) {
2291
+ const id = result?.response?.id;
2292
+ return typeof id === "string" ? id : void 0;
2293
+ }
2294
+ export {
2295
+ ArkadeContractManager,
2296
+ BitcoinContractManager,
2297
+ Client,
2298
+ ClientBuilder,
2299
+ DEFAULT_ESPLORA_URLS,
2300
+ DEFAULT_EVM_RPCS,
2301
+ EvmContractManager,
2302
+ SwapTracker,
2303
+ SwapWorker,
2304
+ WsStatusSource,
2305
+ buildArkadeVhtlcRef,
2306
+ createEvmRpcReader,
2307
+ defaultArkadeServerUrl,
2308
+ defaultEvmReaders,
2309
+ deriveSwapActions,
2310
+ deriveSwapStatus,
2311
+ esploraReader,
2312
+ evmReaderFromClient,
2313
+ htlcFactsFromEsploraTxs,
2314
+ htlcKey,
2315
+ swapToTracked
2316
+ };
3
2317
  //# sourceMappingURL=index.js.map