@satora/swap 0.0.5 → 0.1.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.cjs CHANGED
@@ -3,6 +3,13 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
6
13
  var __copyProps = (to, from, except, desc) => {
7
14
  if (from && typeof from === "object" || typeof from === "function") {
8
15
  for (let key of __getOwnPropNames(from))
@@ -14,12 +21,1861 @@ var __copyProps = (to, from, except, desc) => {
14
21
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
15
22
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
16
23
 
24
+ // src/contracts/bitcoin-reader-esplora.ts
25
+ var bitcoin_reader_esplora_exports = {};
26
+ __export(bitcoin_reader_esplora_exports, {
27
+ DEFAULT_ESPLORA_URLS: () => DEFAULT_ESPLORA_URLS,
28
+ esploraReader: () => esploraReader,
29
+ htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs
30
+ });
31
+ function htlcFactsFromEsploraTxs(txs, address) {
32
+ for (const tx of txs) {
33
+ const spend = tx.vin.find(
34
+ (vin) => vin.prevout?.scriptpubkey_address === address
35
+ );
36
+ if (spend)
37
+ return {
38
+ funding: "confirmed",
39
+ fundedSats: 0,
40
+ // already spent — the amount no longer matters
41
+ spendWitness: spend.witness ?? []
42
+ };
43
+ }
44
+ const funding = txs.find(
45
+ (tx) => tx.vout.some((vout) => vout.scriptpubkey_address === address)
46
+ );
47
+ if (funding) {
48
+ const fundedSats = funding.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
49
+ return {
50
+ funding: funding.status?.confirmed ? "confirmed" : "mempool",
51
+ fundedSats
52
+ };
53
+ }
54
+ return { funding: "absent", fundedSats: 0 };
55
+ }
56
+ function esploraReader(esploraUrls, fetchImpl = fetch) {
57
+ const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(
58
+ (url) => url.replace(/\/+$/, "")
59
+ );
60
+ let start = 0;
61
+ return {
62
+ async getHtlcFacts(address) {
63
+ const from = start++ % bases.length;
64
+ let lastError;
65
+ for (let i = 0; i < bases.length; i++) {
66
+ const base = bases[(from + i) % bases.length];
67
+ try {
68
+ const res = await fetchImpl(
69
+ `${base}/address/${encodeURIComponent(address)}/txs`
70
+ );
71
+ if (!res.ok) throw new Error(`esplora ${res.status} at ${base}`);
72
+ return htlcFactsFromEsploraTxs(
73
+ await res.json(),
74
+ address
75
+ );
76
+ } catch (error) {
77
+ lastError = error;
78
+ }
79
+ }
80
+ throw lastError ?? new Error("no esplora endpoints configured");
81
+ }
82
+ };
83
+ }
84
+ var DEFAULT_ESPLORA_URLS;
85
+ var init_bitcoin_reader_esplora = __esm({
86
+ "src/contracts/bitcoin-reader-esplora.ts"() {
87
+ "use strict";
88
+ DEFAULT_ESPLORA_URLS = [
89
+ "https://mempool.space/api",
90
+ "https://blockstream.info/api"
91
+ ];
92
+ }
93
+ });
94
+
17
95
  // src/index.ts
18
96
  var src_exports = {};
97
+ __export(src_exports, {
98
+ ArkadeContractManager: () => ArkadeContractManager,
99
+ BitcoinContractManager: () => BitcoinContractManager,
100
+ Client: () => Client,
101
+ ClientBuilder: () => ClientBuilder,
102
+ DEFAULT_ESPLORA_URLS: () => DEFAULT_ESPLORA_URLS,
103
+ DEFAULT_EVM_RPCS: () => DEFAULT_EVM_RPCS,
104
+ EvmContractManager: () => EvmContractManager,
105
+ SwapTracker: () => SwapTracker,
106
+ buildArkadeVhtlcRef: () => buildArkadeVhtlcRef,
107
+ createEvmRpcReader: () => createEvmRpcReader,
108
+ defaultArkadeServerUrl: () => defaultArkadeServerUrl,
109
+ defaultEvmReaders: () => defaultEvmReaders,
110
+ deriveSwapActions: () => deriveSwapActions,
111
+ deriveSwapStatus: () => deriveSwapStatus,
112
+ esploraReader: () => esploraReader,
113
+ evmReaderFromClient: () => evmReaderFromClient,
114
+ htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
115
+ htlcKey: () => htlcKey,
116
+ swapToTracked: () => swapToTracked
117
+ });
19
118
  module.exports = __toCommonJS(src_exports);
20
119
  __reExport(src_exports, require("@lendasat/lendaswap-sdk-pure"), module.exports);
120
+
121
+ // src/actions/derive.ts
122
+ function deriveSwapActions(input) {
123
+ const {
124
+ status,
125
+ clientChainNow,
126
+ serverChainNow,
127
+ clientRefundLocktime,
128
+ serverRefundLocktime,
129
+ clientFunds = true
130
+ } = input;
131
+ switch (status) {
132
+ case "pending": {
133
+ if (!clientFunds) {
134
+ const wait = {
135
+ id: "wait",
136
+ recommended: true,
137
+ automation: "auto",
138
+ reason: "Waiting for the Lightning payment to be locked in."
139
+ };
140
+ return { recommended: "wait", actions: [wait] };
141
+ }
142
+ if (clientChainNow >= clientRefundLocktime) return { actions: [] };
143
+ const fund = {
144
+ id: "fund",
145
+ recommended: true,
146
+ // Funding spends new user money, so it is never auto-run.
147
+ automation: "manual",
148
+ reason: "Send the deposit to start the swap."
149
+ };
150
+ return { recommended: "fund", actions: [fund] };
151
+ }
152
+ case "clientfundingseen": {
153
+ const wait = {
154
+ id: "wait",
155
+ recommended: true,
156
+ automation: "auto",
157
+ reason: "Waiting for your funding transaction to confirm."
158
+ };
159
+ return { recommended: "wait", actions: [wait] };
160
+ }
161
+ case "clientfunded": {
162
+ const refundUnlocked = clientChainNow >= clientRefundLocktime;
163
+ if (refundUnlocked) {
164
+ const refund2 = {
165
+ id: "refund_unilateral",
166
+ recommended: true,
167
+ automation: "confirm",
168
+ reason: "Reclaim your deposit."
169
+ };
170
+ return { recommended: "refund_unilateral", actions: [refund2] };
171
+ }
172
+ const wait = {
173
+ id: "wait",
174
+ recommended: true,
175
+ automation: "auto",
176
+ reason: "Waiting for the server to fund the swap."
177
+ };
178
+ const refund = {
179
+ id: "refund_unilateral",
180
+ recommended: false,
181
+ automation: "confirm",
182
+ reason: "Refund becomes available once the timelock passes.",
183
+ blockedBy: {
184
+ kind: "timelock_not_expired",
185
+ message: "The refund timelock has not passed yet."
186
+ }
187
+ };
188
+ return { recommended: "wait", actions: [wait, refund] };
189
+ }
190
+ case "serverfunded": {
191
+ const claimWindowClosed = serverChainNow >= serverRefundLocktime;
192
+ if (!claimWindowClosed) {
193
+ const claim = {
194
+ id: "claim",
195
+ recommended: true,
196
+ automation: "auto",
197
+ reason: "Redeem the swap with your preimage."
198
+ };
199
+ return { recommended: "claim", actions: [claim] };
200
+ }
201
+ if (!clientFunds) {
202
+ const done = {
203
+ id: "none",
204
+ recommended: true,
205
+ automation: "auto",
206
+ reason: "The claim window passed \u2014 your Lightning payment will be refunded automatically."
207
+ };
208
+ return { recommended: "none", actions: [done] };
209
+ }
210
+ const refundUnlocked = clientChainNow >= clientRefundLocktime;
211
+ if (refundUnlocked) {
212
+ const refund2 = {
213
+ id: "refund_unilateral",
214
+ recommended: true,
215
+ automation: "confirm",
216
+ reason: "Reclaim your deposit \u2014 do not claim, as revealing the preimage would let the server take it."
217
+ };
218
+ return { recommended: "refund_unilateral", actions: [refund2] };
219
+ }
220
+ const wait = {
221
+ id: "wait",
222
+ recommended: true,
223
+ automation: "auto",
224
+ reason: "Wait for your refund timelock \u2014 do not claim, as revealing the preimage would let the server take your deposit."
225
+ };
226
+ const refund = {
227
+ id: "refund_unilateral",
228
+ recommended: false,
229
+ automation: "confirm",
230
+ reason: "Refund becomes available once the timelock passes.",
231
+ blockedBy: {
232
+ kind: "timelock_not_expired",
233
+ message: "The refund timelock has not passed yet."
234
+ }
235
+ };
236
+ return { recommended: "wait", actions: [wait, refund] };
237
+ }
238
+ case "clientredeeming": {
239
+ const wait = {
240
+ id: "wait",
241
+ recommended: true,
242
+ automation: "auto",
243
+ reason: "Redeeming your funds \u2014 waiting for the claim to confirm."
244
+ };
245
+ return { recommended: "wait", actions: [wait] };
246
+ }
247
+ case "clientredeemed":
248
+ case "serverredeemed": {
249
+ const done = {
250
+ id: "none",
251
+ recommended: true,
252
+ automation: "auto",
253
+ reason: "Swap complete \u2014 you've received your funds."
254
+ };
255
+ return { recommended: "none", actions: [done] };
256
+ }
257
+ case "expired": {
258
+ const done = {
259
+ id: "none",
260
+ recommended: true,
261
+ automation: "auto",
262
+ reason: "The swap expired without funding \u2014 nothing was locked."
263
+ };
264
+ return { recommended: "none", actions: [done] };
265
+ }
266
+ case "clientrefunded":
267
+ case "clientrefundedserverrefunded":
268
+ case "clientrefundedserverfunded": {
269
+ const done = {
270
+ id: "none",
271
+ recommended: true,
272
+ automation: "auto",
273
+ reason: "Refunded \u2014 your deposit was returned."
274
+ };
275
+ return { recommended: "none", actions: [done] };
276
+ }
277
+ case "clientredeemedandclientrefunded": {
278
+ const done = {
279
+ id: "none",
280
+ recommended: true,
281
+ automation: "auto",
282
+ reason: "This swap ended in an anomalous state (redeemed and refunded); no action needed."
283
+ };
284
+ return { recommended: "none", actions: [done] };
285
+ }
286
+ case "clientfundedserverrefunded":
287
+ case "serverwontfund":
288
+ case "clientfundedtoolate":
289
+ case "clientinvalidfunded": {
290
+ const refundUnlocked = clientChainNow >= clientRefundLocktime;
291
+ if (refundUnlocked) {
292
+ const refund2 = {
293
+ id: "refund_unilateral",
294
+ recommended: true,
295
+ automation: "confirm",
296
+ reason: "Reclaim your deposit \u2014 the swap did not complete."
297
+ };
298
+ return { recommended: "refund_unilateral", actions: [refund2] };
299
+ }
300
+ const wait = {
301
+ id: "wait",
302
+ recommended: true,
303
+ automation: "auto",
304
+ reason: "The swap did not complete; waiting for the refund timelock to reclaim your deposit."
305
+ };
306
+ const refund = {
307
+ id: "refund_unilateral",
308
+ recommended: false,
309
+ automation: "confirm",
310
+ reason: "Refund becomes available once the timelock passes.",
311
+ blockedBy: {
312
+ kind: "timelock_not_expired",
313
+ message: "The refund timelock has not passed yet."
314
+ }
315
+ };
316
+ return { recommended: "wait", actions: [wait, refund] };
317
+ }
318
+ default:
319
+ return assertNever(status);
320
+ }
321
+ }
322
+ function assertNever(status) {
323
+ throw new Error(`unhandled swap status: ${String(status)}`);
324
+ }
325
+
326
+ // src/actions/status.ts
327
+ function deriveSwapStatus(obs) {
328
+ const { clientHtlc, serverHtlc } = obs;
329
+ if (serverHtlc === void 0 && clientHtlc !== void 0) {
330
+ return receiveOnLightningStatus(clientHtlc);
331
+ }
332
+ if (clientHtlc === void 0 && serverHtlc !== void 0) {
333
+ return payOnLightningStatus(serverHtlc);
334
+ }
335
+ if (clientHtlc === void 0 || serverHtlc === void 0) return void 0;
336
+ if (clientHtlc === "spent_claim" && serverHtlc === "spent_claim") {
337
+ return "serverredeemed";
338
+ }
339
+ if (clientHtlc === "spent_refund" && serverHtlc === "spent_claim") {
340
+ return "clientredeemedandclientrefunded";
341
+ }
342
+ if (clientHtlc === "spent_refund" && serverHtlc === "spent_refund") {
343
+ return "clientrefundedserverrefunded";
344
+ }
345
+ if (clientHtlc === "spent_refund" && serverHtlc === "confirmed") {
346
+ return "clientrefundedserverfunded";
347
+ }
348
+ if (clientHtlc === "spent_refund" && (serverHtlc === "absent" || serverHtlc === "mempool")) {
349
+ return "clientrefunded";
350
+ }
351
+ if (clientHtlc === "confirmed" && serverHtlc === "spent_claim") {
352
+ return "clientredeemed";
353
+ }
354
+ if (clientHtlc === "confirmed" && serverHtlc === "spent_refund") {
355
+ return "clientfundedserverrefunded";
356
+ }
357
+ if (clientHtlc === "invalid") return "clientinvalidfunded";
358
+ if (clientHtlc === "absent") return "pending";
359
+ if (clientHtlc === "mempool") return "clientfundingseen";
360
+ if (clientHtlc === "confirmed") {
361
+ return serverHtlc === "confirmed" ? "serverfunded" : "clientfunded";
362
+ }
363
+ return void 0;
364
+ }
365
+ function receiveOnLightningStatus(clientHtlc) {
366
+ switch (clientHtlc) {
367
+ case "spent_claim":
368
+ return "serverredeemed";
369
+ // server swept the deposit ⟹ it paid the invoice
370
+ case "spent_refund":
371
+ return "clientrefunded";
372
+ case "invalid":
373
+ return "clientinvalidfunded";
374
+ case "confirmed":
375
+ return "clientfunded";
376
+ // funded; waiting for the server to pay + sweep
377
+ case "mempool":
378
+ return "clientfundingseen";
379
+ case "absent":
380
+ return "pending";
381
+ }
382
+ }
383
+ function payOnLightningStatus(serverHtlc) {
384
+ switch (serverHtlc) {
385
+ case "spent_claim":
386
+ return "clientredeemed";
387
+ // client claimed the on-chain leg ⟹ complete
388
+ case "confirmed":
389
+ return "serverfunded";
390
+ // ready to claim
391
+ case "spent_refund":
392
+ return "clientrefunded";
393
+ case "invalid":
394
+ case "mempool":
395
+ case "absent":
396
+ return "pending";
397
+ }
398
+ }
399
+
400
+ // src/client.ts
401
+ var import_lendaswap_sdk_pure = require("@lendasat/lendaswap-sdk-pure");
402
+
403
+ // src/contracts/arkade-manager.ts
404
+ var import_sdk2 = require("@arkade-os/sdk");
405
+ var import_base3 = require("@scure/base");
406
+
407
+ // src/contracts/arkade.ts
408
+ var import_sdk = require("@arkade-os/sdk");
409
+ var import_base2 = require("@scure/base");
410
+
411
+ // src/contracts/preimage.ts
412
+ var import_legacy = require("@noble/hashes/legacy.js");
413
+ var import_sha2 = require("@noble/hashes/sha2.js");
414
+ var import_base = require("@scure/base");
415
+ function preimageMatches(element, hashLock) {
416
+ const digest = hashLock.length === 20 ? (0, import_legacy.ripemd160)((0, import_sha2.sha256)(element)) : (0, import_sha2.sha256)(element);
417
+ return import_base.hex.encode(digest) === import_base.hex.encode(hashLock);
418
+ }
419
+
420
+ // src/contracts/arkade.ts
421
+ function arkadeObservation(facts) {
422
+ if (facts.spend === "claim") return "spent_claim";
423
+ if (facts.spend === "refund") return "spent_refund";
424
+ if (!facts.funded) return "absent";
425
+ return facts.sufficient === false ? "invalid" : "confirmed";
426
+ }
427
+ function classifyArkadeSpend(encodedSpendTx, paymentHash) {
428
+ const tx = import_sdk.Transaction.fromPSBT(import_base2.base64.decode(encodedSpendTx));
429
+ for (const witness of (0, import_sdk.getArkPsbtFields)(tx, 0, import_sdk.ConditionWitness)) {
430
+ for (const element of witness) {
431
+ if (element.length === 32 && preimageMatches(element, paymentHash)) {
432
+ return { spend: "claim", preimage: element };
433
+ }
434
+ }
435
+ }
436
+ return { spend: "refund" };
437
+ }
438
+ async function fetchArkadeSpend(source, spentByTxid, paymentHash) {
439
+ const { txs } = await source.getVirtualTxs([spentByTxid]);
440
+ const encoded = txs[0];
441
+ return encoded ? classifyArkadeSpend(encoded, paymentHash) : void 0;
442
+ }
443
+
444
+ // src/contracts/arkade-manager.ts
445
+ function isFunded(vtxo) {
446
+ const { state } = vtxo.virtualStatus;
447
+ return state === "preconfirmed" || state === "settled";
448
+ }
449
+ function spendTxid(vtxos) {
450
+ const spent = vtxos.find(
451
+ (v) => v.arkTxId || v.spentBy || v.isSpent || v.virtualStatus.state === "spent"
452
+ );
453
+ return spent?.arkTxId ?? spent?.spentBy;
454
+ }
455
+ var ArkadeContractManager = class _ArkadeContractManager {
456
+ ledger = "arkade";
457
+ #indexer;
458
+ #chainTime;
459
+ /** pkScript → the ref we're tracking. */
460
+ #refs = /* @__PURE__ */ new Map();
461
+ /** pkScript → last known observation. */
462
+ #obs = /* @__PURE__ */ new Map();
463
+ /** pkScript → the verified preimage recovered from a claim spend. */
464
+ #preimages = /* @__PURE__ */ new Map();
465
+ #listeners = /* @__PURE__ */ new Set();
466
+ #now;
467
+ constructor(deps) {
468
+ this.#indexer = deps.indexer;
469
+ this.#chainTime = deps.chainTime;
470
+ }
471
+ static fromDeps(deps) {
472
+ return new _ArkadeContractManager(deps);
473
+ }
474
+ /** Construct a manager backed by a `RestIndexerProvider` over the Ark server. */
475
+ static async create(config) {
476
+ const indexer = new import_sdk2.RestIndexerProvider(config.serverUrl);
477
+ return _ArkadeContractManager.fromDeps({
478
+ indexer,
479
+ chainTime: config.chainTime
480
+ });
481
+ }
482
+ async register(ref) {
483
+ if (ref.ledger !== "arkade")
484
+ throw new Error(
485
+ `ArkadeContractManager can't track a '${ref.ledger}' HTLC`
486
+ );
487
+ this.#refs.set(ref.script, ref);
488
+ await this.#reconcileRef(ref);
489
+ }
490
+ async unregister(ref) {
491
+ if (ref.ledger !== "arkade") return;
492
+ this.#refs.delete(ref.script);
493
+ this.#obs.delete(ref.script);
494
+ this.#preimages.delete(ref.script);
495
+ }
496
+ getState(ref) {
497
+ return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
498
+ }
499
+ chainNow(_ref) {
500
+ return this.#now;
501
+ }
502
+ onEvent(cb) {
503
+ this.#listeners.add(cb);
504
+ return () => this.#listeners.delete(cb);
505
+ }
506
+ async refresh() {
507
+ if (this.#chainTime) this.#now = await this.#chainTime();
508
+ await Promise.all(
509
+ [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
510
+ );
511
+ }
512
+ dispose() {
513
+ this.#listeners.clear();
514
+ }
515
+ /** The verified preimage recovered from a claim spend, if one was seen. */
516
+ getPreimage(ref) {
517
+ return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
518
+ }
519
+ /** Read the VHTLC's vtxos and map them to an observation. */
520
+ async #reconcileRef(ref) {
521
+ const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
522
+ const spend = spendTxid(vtxos);
523
+ if (spend) {
524
+ const resolved = await fetchArkadeSpend(
525
+ this.#indexer,
526
+ spend,
527
+ import_base3.hex.decode(ref.preimageHash)
528
+ );
529
+ if (resolved) {
530
+ if (resolved.spend === "claim")
531
+ this.#preimages.set(ref.script, resolved.preimage);
532
+ this.#set(
533
+ ref.script,
534
+ arkadeObservation({ funded: true, spend: resolved.spend })
535
+ );
536
+ return;
537
+ }
538
+ }
539
+ const funded = vtxos.filter(isFunded);
540
+ const total = funded.reduce((sum, vtxo) => sum + vtxo.value, 0);
541
+ this.#set(
542
+ ref.script,
543
+ arkadeObservation({
544
+ funded: funded.length > 0,
545
+ sufficient: total >= ref.expectedSats
546
+ })
547
+ );
548
+ }
549
+ /** Update an observation and notify on change; never downgrade a resolved spend. */
550
+ #set(script, observation) {
551
+ const ref = this.#refs.get(script);
552
+ if (!ref) return;
553
+ const current = this.#obs.get(script);
554
+ if (current === observation) return;
555
+ const spendStates = ["spent_claim", "spent_refund"];
556
+ if (current && spendStates.includes(current) && !spendStates.includes(observation))
557
+ return;
558
+ this.#obs.set(script, observation);
559
+ for (const listener of this.#listeners) listener(ref, observation);
560
+ }
561
+ };
562
+
563
+ // src/contracts/arkade-network.ts
564
+ var DEFAULT_ARKADE_URLS = {
565
+ bitcoin: "https://arkade.computer",
566
+ mainnet: "https://arkade.computer",
567
+ signet: "https://mutinynet.arkade.sh",
568
+ mutinynet: "https://mutinynet.arkade.sh"
569
+ };
570
+ function defaultArkadeServerUrl(network) {
571
+ return DEFAULT_ARKADE_URLS[network.toLowerCase()];
572
+ }
573
+
574
+ // src/contracts/bitcoin-manager.ts
575
+ var import_base5 = require("@scure/base");
576
+
577
+ // src/contracts/bitcoin.ts
578
+ var import_base4 = require("@scure/base");
579
+ function classifyBitcoinSpend(witness, paymentHash) {
580
+ for (const item of witness) {
581
+ if (item.length !== 64) continue;
582
+ const bytes = import_base4.hex.decode(item);
583
+ if (preimageMatches(bytes, paymentHash))
584
+ return { spend: "claim", preimage: bytes };
585
+ }
586
+ return { spend: "refund" };
587
+ }
588
+ function bitcoinObservation(facts, paymentHash, expectedSats) {
589
+ if (facts.spendWitness) {
590
+ const spend = classifyBitcoinSpend(facts.spendWitness, paymentHash);
591
+ return spend.spend === "claim" ? { observation: "spent_claim", preimage: spend.preimage } : { observation: "spent_refund" };
592
+ }
593
+ if (facts.funding === "confirmed" && facts.fundedSats < expectedSats) {
594
+ return { observation: "invalid" };
595
+ }
596
+ return { observation: facts.funding };
597
+ }
598
+
599
+ // src/contracts/bitcoin-manager.ts
600
+ var BitcoinContractManager = class _BitcoinContractManager {
601
+ ledger = "bitcoin";
602
+ #reader;
603
+ #chainTime;
604
+ /** address → the ref we're tracking. */
605
+ #refs = /* @__PURE__ */ new Map();
606
+ /** address → last known observation. */
607
+ #obs = /* @__PURE__ */ new Map();
608
+ /** address → the preimage a claim revealed. */
609
+ #preimages = /* @__PURE__ */ new Map();
610
+ #listeners = /* @__PURE__ */ new Set();
611
+ #now;
612
+ constructor(deps) {
613
+ this.#reader = deps.reader;
614
+ this.#chainTime = deps.chainTime;
615
+ }
616
+ static fromDeps(deps) {
617
+ return new _BitcoinContractManager(deps);
618
+ }
619
+ static async create(config) {
620
+ const { esploraReader: esploraReader2 } = await Promise.resolve().then(() => (init_bitcoin_reader_esplora(), bitcoin_reader_esplora_exports));
621
+ return _BitcoinContractManager.fromDeps({
622
+ reader: esploraReader2(config.esploraUrl),
623
+ chainTime: config.chainTime
624
+ });
625
+ }
626
+ async register(ref) {
627
+ if (ref.ledger !== "bitcoin")
628
+ throw new Error(
629
+ `BitcoinContractManager can't track a '${ref.ledger}' HTLC`
630
+ );
631
+ this.#refs.set(ref.address, ref);
632
+ await this.#reconcileRef(ref);
633
+ }
634
+ async unregister(ref) {
635
+ if (ref.ledger !== "bitcoin") return;
636
+ this.#refs.delete(ref.address);
637
+ this.#obs.delete(ref.address);
638
+ this.#preimages.delete(ref.address);
639
+ }
640
+ getState(ref) {
641
+ return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
642
+ }
643
+ chainNow(_ref) {
644
+ return this.#now;
645
+ }
646
+ onEvent(cb) {
647
+ this.#listeners.add(cb);
648
+ return () => this.#listeners.delete(cb);
649
+ }
650
+ async refresh() {
651
+ if (this.#chainTime) this.#now = await this.#chainTime();
652
+ await Promise.all(
653
+ [...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
654
+ );
655
+ }
656
+ dispose() {
657
+ this.#listeners.clear();
658
+ }
659
+ /** The preimage a claim revealed on this HTLC, if one was seen. */
660
+ getPreimage(ref) {
661
+ return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
662
+ }
663
+ async #reconcileRef(ref) {
664
+ const facts = await this.#reader.getHtlcFacts(ref.address);
665
+ const { observation, preimage } = bitcoinObservation(
666
+ facts,
667
+ import_base5.hex.decode(ref.preimageHash),
668
+ ref.expectedSats
669
+ );
670
+ if (preimage) this.#preimages.set(ref.address, preimage);
671
+ this.#set(ref.address, observation);
672
+ }
673
+ /** Update an observation and notify on change; never downgrade a resolved spend. */
674
+ #set(address, observation) {
675
+ const ref = this.#refs.get(address);
676
+ if (!ref) return;
677
+ const current = this.#obs.get(address);
678
+ if (current === observation) return;
679
+ const spendStates = ["spent_claim", "spent_refund"];
680
+ if (current && spendStates.includes(current) && !spendStates.includes(observation))
681
+ return;
682
+ this.#obs.set(address, observation);
683
+ for (const listener of this.#listeners) listener(ref, observation);
684
+ }
685
+ };
686
+
687
+ // src/client.ts
688
+ init_bitcoin_reader_esplora();
689
+
690
+ // src/contracts/evm.ts
691
+ function evmObservation(events, expected, pending = false) {
692
+ const redeemed = events.find((e) => e.kind === "redeemed");
693
+ if (redeemed?.kind === "redeemed")
694
+ return { observation: "spent_claim", preimage: redeemed.preimage };
695
+ if (events.some((e) => e.kind === "refunded"))
696
+ return { observation: "spent_refund" };
697
+ const created = events.find((e) => e.kind === "created");
698
+ if (created?.kind === "created") {
699
+ const amountOk = created.amount >= expected.amount;
700
+ const tokenOk = expected.token === void 0 || created.token.toLowerCase() === expected.token.toLowerCase();
701
+ return { observation: amountOk && tokenOk ? "confirmed" : "invalid" };
702
+ }
703
+ return { observation: pending ? "mempool" : "absent" };
704
+ }
705
+
706
+ // src/contracts/types.ts
707
+ function htlcKey(ref) {
708
+ switch (ref.ledger) {
709
+ case "arkade":
710
+ return `arkade:${ref.script}`;
711
+ case "bitcoin":
712
+ return `bitcoin:${ref.address}`;
713
+ case "evm":
714
+ return `evm:${ref.chainId}:${ref.preimageHash}`;
715
+ case "lightning":
716
+ return `lightning:${ref.paymentHash}`;
717
+ }
718
+ }
719
+
720
+ // src/contracts/evm-manager.ts
721
+ var EvmContractManager = class _EvmContractManager {
722
+ ledger = "evm";
723
+ #readers;
724
+ /** htlcKey → the ref we're tracking. */
725
+ #refs = /* @__PURE__ */ new Map();
726
+ /** htlcKey → last known observation. */
727
+ #obs = /* @__PURE__ */ new Map();
728
+ /** htlcKey → the preimage a claim revealed. */
729
+ #preimages = /* @__PURE__ */ new Map();
730
+ /** chainId → its latest block time (ms). */
731
+ #now = /* @__PURE__ */ new Map();
732
+ /** chainId → unsubscribe for its (single, shared) watch. */
733
+ #watchUnsubs = /* @__PURE__ */ new Map();
734
+ /** chainId → a block-event reconcile is currently running (coalescing). */
735
+ #reconciling = /* @__PURE__ */ new Set();
736
+ /** chainId → a block event arrived mid-reconcile; run exactly one more. */
737
+ #reconcileAgain = /* @__PURE__ */ new Set();
738
+ #listeners = /* @__PURE__ */ new Set();
739
+ constructor(deps) {
740
+ this.#readers = deps.readers;
741
+ }
742
+ static fromDeps(deps) {
743
+ return new _EvmContractManager(deps);
744
+ }
745
+ async register(ref) {
746
+ if (ref.ledger !== "evm")
747
+ throw new Error(`EvmContractManager can't track a '${ref.ledger}' HTLC`);
748
+ if (!this.#readers.has(ref.chainId))
749
+ throw new Error(
750
+ `no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
751
+ );
752
+ this.#refs.set(htlcKey(ref), ref);
753
+ this.#ensureWatch(ref.chainId);
754
+ await this.#reconcileChain(ref.chainId);
755
+ }
756
+ async unregister(ref) {
757
+ if (ref.ledger !== "evm") return;
758
+ const key = htlcKey(ref);
759
+ this.#refs.delete(key);
760
+ this.#obs.delete(key);
761
+ this.#preimages.delete(key);
762
+ if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId)) {
763
+ this.#watchUnsubs.get(ref.chainId)?.();
764
+ this.#watchUnsubs.delete(ref.chainId);
765
+ this.#now.delete(ref.chainId);
766
+ this.#reconcileAgain.delete(ref.chainId);
767
+ }
768
+ }
769
+ getState(ref) {
770
+ return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
771
+ }
772
+ chainNow(ref) {
773
+ return ref.ledger === "evm" ? this.#now.get(ref.chainId) : void 0;
774
+ }
775
+ onEvent(cb) {
776
+ this.#listeners.add(cb);
777
+ return () => this.#listeners.delete(cb);
778
+ }
779
+ async refresh() {
780
+ const chainIds = new Set([...this.#refs.values()].map((r) => r.chainId));
781
+ await Promise.all([...chainIds].map((c) => this.#reconcileChain(c)));
782
+ }
783
+ dispose() {
784
+ for (const unsub of this.#watchUnsubs.values()) unsub();
785
+ this.#watchUnsubs.clear();
786
+ this.#reconciling.clear();
787
+ this.#reconcileAgain.clear();
788
+ this.#listeners.clear();
789
+ }
790
+ /** The preimage a claim revealed on this HTLC, if one was seen. */
791
+ getPreimage(ref) {
792
+ return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
793
+ }
794
+ #ensureWatch(chainId) {
795
+ if (this.#watchUnsubs.has(chainId)) return;
796
+ const reader = this.#readers.get(chainId);
797
+ if (!reader) return;
798
+ this.#watchUnsubs.set(
799
+ chainId,
800
+ reader.watch(() => this.#scheduleReconcile(chainId))
801
+ );
802
+ }
803
+ /**
804
+ * Reconcile a chain in response to a block/log event, coalesced: at most one
805
+ * reconcile runs per chain at a time, with at most one queued rerun to pick up
806
+ * changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
807
+ * redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
808
+ * transient RPC failure in a background reconcile from becoming an unhandled
809
+ * rejection. `register`/`refresh` still await `#reconcileChain` directly.
810
+ */
811
+ #scheduleReconcile(chainId) {
812
+ if (this.#reconciling.has(chainId)) {
813
+ this.#reconcileAgain.add(chainId);
814
+ return;
815
+ }
816
+ this.#reconciling.add(chainId);
817
+ void this.#reconcileChain(chainId).catch((error) => {
818
+ console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
819
+ }).finally(() => {
820
+ this.#reconciling.delete(chainId);
821
+ if (this.#reconcileAgain.delete(chainId))
822
+ this.#scheduleReconcile(chainId);
823
+ });
824
+ }
825
+ /** Refresh one chain's clock and re-observe every HTLC tracked on it. */
826
+ async #reconcileChain(chainId) {
827
+ const reader = this.#readers.get(chainId);
828
+ if (!reader) return;
829
+ this.#now.set(chainId, await reader.getBlockTimeMs());
830
+ await Promise.all(
831
+ [...this.#refs.values()].filter((r) => r.chainId === chainId).map((r) => this.#reconcileRef(reader, r))
832
+ );
833
+ }
834
+ async #reconcileRef(reader, ref) {
835
+ const events = await reader.getHtlcEvents(
836
+ ref.htlc,
837
+ ref.preimageHash,
838
+ ref.claimAddress
839
+ );
840
+ const { observation, preimage } = evmObservation(events, {
841
+ amount: ref.expectedAmount,
842
+ token: ref.expectedToken
843
+ });
844
+ const key = htlcKey(ref);
845
+ if (preimage) this.#preimages.set(key, preimage);
846
+ this.#set(key, observation);
847
+ }
848
+ /** Update an observation and notify on change; never downgrade a resolved spend. */
849
+ #set(key, observation) {
850
+ const ref = this.#refs.get(key);
851
+ if (!ref) return;
852
+ const current = this.#obs.get(key);
853
+ if (current === observation) return;
854
+ const spendStates = ["spent_claim", "spent_refund"];
855
+ if (current && spendStates.includes(current) && !spendStates.includes(observation))
856
+ return;
857
+ this.#obs.set(key, observation);
858
+ for (const listener of this.#listeners) listener(ref, observation);
859
+ }
860
+ };
861
+
862
+ // src/contracts/evm-reader-viem.ts
863
+ var import_viem = require("viem");
864
+ var DEFAULT_EVM_RPCS = {
865
+ // Polygon — viem's default (polygon.drpc.org) misbehaves for some calls, so
866
+ // list working public RPCs explicitly.
867
+ 137: [
868
+ "https://polygon.drpc.org",
869
+ "https://tenderly.rpc.polygon.community",
870
+ "https://polygon-bor-rpc.publicnode.com"
871
+ ],
872
+ 1: [
873
+ "https://ethereum-rpc.publicnode.com",
874
+ "https://rpc.ankr.com/eth",
875
+ "https://eth.drpc.org"
876
+ ],
877
+ 42161: [
878
+ "https://arbitrum-one-rpc.publicnode.com",
879
+ "https://rpc.ankr.com/arbitrum",
880
+ "https://arb1.arbitrum.io/rpc"
881
+ ]
882
+ };
883
+ var SWAP_CREATED = (0, import_viem.parseAbiItem)(
884
+ "event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock)"
885
+ );
886
+ var SWAP_REDEEMED = (0, import_viem.parseAbiItem)(
887
+ "event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
888
+ );
889
+ var SWAP_REFUNDED = (0, import_viem.parseAbiItem)(
890
+ "event SwapRefunded(bytes32 indexed preimageHash)"
891
+ );
892
+ function evmReaderFromClient(client) {
893
+ return {
894
+ async getHtlcEvents(htlc, preimageHash, claimAddress) {
895
+ const [created, redeemed, refunded] = await Promise.all([
896
+ // Filter by claimAddress too (also indexed), so only the HTLC actually
897
+ // claimable on the swap's terms is seen.
898
+ client.getLogs({
899
+ address: htlc,
900
+ event: SWAP_CREATED,
901
+ args: { preimageHash, claimAddress },
902
+ fromBlock: "earliest"
903
+ }),
904
+ client.getLogs({
905
+ address: htlc,
906
+ event: SWAP_REDEEMED,
907
+ args: { preimageHash },
908
+ fromBlock: "earliest"
909
+ }),
910
+ client.getLogs({
911
+ address: htlc,
912
+ event: SWAP_REFUNDED,
913
+ args: { preimageHash },
914
+ fromBlock: "earliest"
915
+ })
916
+ ]);
917
+ const events = [];
918
+ for (const log of created) {
919
+ const args = log.args;
920
+ events.push({
921
+ kind: "created",
922
+ amount: args?.amount ?? 0n,
923
+ token: args?.token ?? "0x"
924
+ });
925
+ }
926
+ for (const log of redeemed) {
927
+ const preimage = log.args?.preimage;
928
+ if (preimage) events.push({ kind: "redeemed", preimage });
929
+ }
930
+ if (refunded.length > 0) events.push({ kind: "refunded" });
931
+ return events;
932
+ },
933
+ async getBlockTimeMs() {
934
+ const block = await client.getBlock();
935
+ return Number(block.timestamp) * 1e3;
936
+ },
937
+ watch(cb) {
938
+ return client.watchBlocks({ onBlock: () => cb() });
939
+ }
940
+ };
941
+ }
942
+ function createEvmRpcReader(rpcUrls) {
943
+ const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
944
+ const transport = urls.length > 1 ? (0, import_viem.fallback)(
945
+ urls.map((url) => (0, import_viem.http)(url)),
946
+ { rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
947
+ ) : (0, import_viem.http)(urls[0]);
948
+ const client = (0, import_viem.createPublicClient)({ transport });
949
+ return evmReaderFromClient(client);
950
+ }
951
+ function defaultEvmReaders(overrides) {
952
+ const chainIds = /* @__PURE__ */ new Set([
953
+ ...Object.keys(DEFAULT_EVM_RPCS).map(Number),
954
+ ...Object.keys(overrides ?? {}).map(Number)
955
+ ]);
956
+ const readers = /* @__PURE__ */ new Map();
957
+ for (const chainId of chainIds) {
958
+ const override = overrides?.[chainId];
959
+ const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
960
+ const urls = override ? [override, ...defaults] : defaults;
961
+ if (urls.length > 0) readers.set(chainId, createEvmRpcReader(urls));
962
+ }
963
+ return readers;
964
+ }
965
+
966
+ // src/contracts/arkade-vhtlc.ts
967
+ var import_sdk3 = require("@arkade-os/sdk");
968
+ var import_legacy2 = require("@noble/hashes/legacy.js");
969
+ var import_base6 = require("@scure/base");
970
+ function strip0x(value) {
971
+ return value.startsWith("0x") ? value.slice(2) : value;
972
+ }
973
+ function xOnly(pubKeyHex) {
974
+ const bytes = import_base6.hex.decode(strip0x(pubKeyHex));
975
+ if (bytes.length === 33) return bytes.slice(1);
976
+ if (bytes.length === 32) return bytes;
977
+ throw new Error(`invalid public key length: ${bytes.length}`);
978
+ }
979
+ var seconds = (value) => ({ type: "seconds", value: BigInt(value) });
980
+ function buildArkadeVhtlcRef(input) {
981
+ const hashLockBytes = import_base6.hex.decode(strip0x(input.hashLock));
982
+ const params = {
983
+ sender: xOnly(input.senderPk),
984
+ receiver: xOnly(input.receiverPk),
985
+ server: xOnly(input.serverPk),
986
+ preimageHash: hashLockBytes.length === 20 ? hashLockBytes : (0, import_legacy2.ripemd160)(hashLockBytes),
987
+ refundLocktime: BigInt(input.refundLocktime),
988
+ unilateralClaimDelay: seconds(input.unilateralClaimDelay),
989
+ unilateralRefundDelay: seconds(input.unilateralRefundDelay),
990
+ unilateralRefundWithoutReceiverDelay: seconds(
991
+ input.unilateralRefundWithoutReceiverDelay
992
+ )
993
+ };
994
+ const vhtlc = new import_sdk3.VHTLC.Script(params);
995
+ return {
996
+ ledger: "arkade",
997
+ script: import_base6.hex.encode(vhtlc.pkScript),
998
+ address: input.address,
999
+ preimageHash: strip0x(input.hashLock),
1000
+ expectedSats: input.expectedSats,
1001
+ params: import_sdk3.VHTLCContractHandler.serializeParams(params)
1002
+ };
1003
+ }
1004
+
1005
+ // src/tracker/from-swap.ts
1006
+ var ms = (seconds2) => seconds2 * 1e3;
1007
+ var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
1008
+ var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
1009
+ function arkadeLeg(r, address, expectedSats) {
1010
+ return buildArkadeVhtlcRef({
1011
+ senderPk: r.sender_pk,
1012
+ receiverPk: r.receiver_pk,
1013
+ serverPk: r.arkade_server_pk,
1014
+ hashLock: r.hash_lock,
1015
+ address,
1016
+ refundLocktime: r.vhtlc_refund_locktime,
1017
+ unilateralClaimDelay: r.unilateral_claim_delay,
1018
+ unilateralRefundDelay: r.unilateral_refund_delay,
1019
+ unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
1020
+ expectedSats
1021
+ });
1022
+ }
1023
+ function evmLeg(args) {
1024
+ return {
1025
+ ledger: "evm",
1026
+ chainId: args.chainId,
1027
+ htlc: ensure0x(args.htlc),
1028
+ preimageHash: ensure0x(args.hashLock),
1029
+ claimAddress: ensure0x(args.claimAddress),
1030
+ expectedAmount: BigInt(args.expectedSats),
1031
+ expectedToken: args.token ? ensure0x(args.token) : void 0
1032
+ };
1033
+ }
1034
+ function bitcoinLeg(address, hashLock, expectedSats) {
1035
+ return {
1036
+ ledger: "bitcoin",
1037
+ address,
1038
+ preimageHash: strip0x2(hashLock),
1039
+ expectedSats
1040
+ };
1041
+ }
1042
+ function swapToTracked(stored) {
1043
+ const r = stored.response;
1044
+ switch (r.direction) {
1045
+ // Client sends Arkade (funds the VHTLC with source_amount sats) and claims EVM.
1046
+ case "arkade_to_evm":
1047
+ return {
1048
+ swapId: r.id,
1049
+ clientHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.source_amount)),
1050
+ serverHtlc: evmLeg({
1051
+ chainId: r.evm_chain_id,
1052
+ htlc: r.evm_htlc_address,
1053
+ hashLock: r.hash_lock,
1054
+ claimAddress: r.client_evm_address,
1055
+ // the client claims the server's EVM HTLC
1056
+ expectedSats: r.evm_expected_sats,
1057
+ token: r.wbtc_address
1058
+ }),
1059
+ clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1060
+ serverRefundLocktime: ms(r.evm_refund_locktime)
1061
+ };
1062
+ // Client sends EVM and claims the Arkade VHTLC (server funds it with target_amount sats).
1063
+ case "evm_to_arkade":
1064
+ return {
1065
+ swapId: r.id,
1066
+ clientHtlc: evmLeg({
1067
+ chainId: r.evm_chain_id,
1068
+ htlc: r.evm_htlc_address,
1069
+ hashLock: r.hash_lock,
1070
+ claimAddress: r.server_evm_address,
1071
+ // the server claims the client's EVM HTLC
1072
+ expectedSats: r.evm_expected_sats
1073
+ // evm_to_arkade doesn't expose the locked token — verify amount only.
1074
+ }),
1075
+ serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
1076
+ clientRefundLocktime: ms(r.evm_refund_locktime),
1077
+ serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1078
+ };
1079
+ // Client sends BTC (funds the on-chain HTLC with source_amount sats) and claims EVM.
1080
+ case "bitcoin_to_evm":
1081
+ return {
1082
+ swapId: r.id,
1083
+ clientHtlc: bitcoinLeg(
1084
+ r.btc_htlc_address,
1085
+ r.evm_hash_lock,
1086
+ Number(r.source_amount)
1087
+ ),
1088
+ serverHtlc: evmLeg({
1089
+ chainId: r.evm_chain_id,
1090
+ htlc: r.evm_htlc_address,
1091
+ hashLock: r.evm_hash_lock,
1092
+ claimAddress: r.client_evm_address,
1093
+ // the client claims the server's EVM HTLC
1094
+ expectedSats: r.evm_expected_sats,
1095
+ token: r.wbtc_address
1096
+ }),
1097
+ clientRefundLocktime: ms(r.btc_refund_locktime),
1098
+ serverRefundLocktime: ms(r.evm_refund_locktime)
1099
+ };
1100
+ // Client sends EVM and claims the BTC HTLC (server funds it with target_amount sats).
1101
+ case "evm_to_bitcoin":
1102
+ return {
1103
+ swapId: r.id,
1104
+ clientHtlc: evmLeg({
1105
+ chainId: r.evm_chain_id,
1106
+ htlc: r.evm_htlc_address,
1107
+ hashLock: r.evm_hash_lock,
1108
+ claimAddress: r.server_evm_address,
1109
+ // the server claims the client's EVM HTLC
1110
+ expectedSats: r.evm_expected_sats,
1111
+ token: r.wbtc_address
1112
+ }),
1113
+ serverHtlc: bitcoinLeg(
1114
+ r.btc_htlc_address,
1115
+ r.evm_hash_lock,
1116
+ Number(r.target_amount)
1117
+ ),
1118
+ clientRefundLocktime: ms(r.evm_refund_locktime),
1119
+ serverRefundLocktime: ms(r.btc_refund_locktime)
1120
+ };
1121
+ // Client sends BTC (funds the on-chain HTLC) and claims the Arkade VHTLC (the
1122
+ // server funds it). Unlike the other Arkade directions, the VHTLC's receiver is
1123
+ // the client's own key (not in the response) and its funder is `server_vhtlc_pk`;
1124
+ // the hash lock is a 20-byte HASH160, which buildArkadeVhtlcRef takes as-is.
1125
+ case "btc_to_arkade":
1126
+ return {
1127
+ swapId: r.id,
1128
+ clientHtlc: bitcoinLeg(
1129
+ r.btc_htlc_address,
1130
+ r.hash_lock,
1131
+ Number(r.source_amount)
1132
+ ),
1133
+ serverHtlc: buildArkadeVhtlcRef({
1134
+ senderPk: r.server_vhtlc_pk,
1135
+ // the server funds the VHTLC
1136
+ receiverPk: stored.publicKey,
1137
+ // the client claims it
1138
+ serverPk: r.arkade_server_pk,
1139
+ hashLock: r.hash_lock,
1140
+ address: r.arkade_vhtlc_address,
1141
+ refundLocktime: r.vhtlc_refund_locktime,
1142
+ unilateralClaimDelay: r.unilateral_claim_delay,
1143
+ unilateralRefundDelay: r.unilateral_refund_delay,
1144
+ unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
1145
+ expectedSats: Number(r.target_amount)
1146
+ }),
1147
+ clientRefundLocktime: ms(r.btc_refund_locktime),
1148
+ serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1149
+ };
1150
+ // ─── Lightning: one on-chain leg, one off-chain LN payment ───────────────
1151
+ // Receive-on-Lightning: the client funds an on-chain HTLC and is paid over
1152
+ // Lightning. No server leg to watch — the server sweeping the client's leg
1153
+ // (with the preimage it got by paying the invoice) is the done signal.
1154
+ // Client funds the Arkade VHTLC; Boltz pays the invoice. The deposit is
1155
+ // `boltz_amount_sats` (source minus fees) — the amount actually locked, not
1156
+ // `source_amount` — else the client's own funding reads as under-funded.
1157
+ case "arkade_to_lightning":
1158
+ return {
1159
+ swapId: r.id,
1160
+ clientHtlc: arkadeLeg(
1161
+ r,
1162
+ r.arkade_vhtlc_address,
1163
+ Number(r.boltz_amount_sats)
1164
+ ),
1165
+ clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1166
+ serverRefundLocktime: 0
1167
+ // no on-chain server leg
1168
+ };
1169
+ // Client funds the EVM HTLC (server claims it after paying the invoice).
1170
+ case "evm_to_lightning":
1171
+ return {
1172
+ swapId: r.id,
1173
+ clientHtlc: evmLeg({
1174
+ chainId: r.evm_chain_id,
1175
+ htlc: r.evm_htlc_address,
1176
+ hashLock: r.hash_lock,
1177
+ claimAddress: r.server_evm_address,
1178
+ // the server claims the client's leg
1179
+ expectedSats: r.evm_expected_sats
1180
+ // evm_to_lightning doesn't expose the locked token — verify amount only.
1181
+ }),
1182
+ clientRefundLocktime: ms(r.evm_refund_locktime),
1183
+ serverRefundLocktime: 0
1184
+ // no on-chain server leg
1185
+ };
1186
+ // Pay-on-Lightning: the client pays a Lightning invoice (off-chain, nothing to
1187
+ // watch) and claims an on-chain HTLC. No client-funded leg — the client's
1188
+ // claim completes the swap; a hold invoice that never settles auto-unwinds.
1189
+ // Client pays the invoice, then claims the Arkade VHTLC (target_amount).
1190
+ case "lightning_to_arkade":
1191
+ return {
1192
+ swapId: r.id,
1193
+ serverHtlc: arkadeLeg(
1194
+ r,
1195
+ r.arkade_vhtlc_address,
1196
+ Number(r.target_amount)
1197
+ ),
1198
+ clientRefundLocktime: 0,
1199
+ // no on-chain client leg
1200
+ serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1201
+ };
1202
+ // Client pays the invoice, then claims the EVM HTLC.
1203
+ case "lightning_to_evm":
1204
+ return {
1205
+ swapId: r.id,
1206
+ serverHtlc: evmLeg({
1207
+ chainId: r.evm_chain_id,
1208
+ htlc: r.evm_htlc_address,
1209
+ hashLock: r.hash_lock,
1210
+ claimAddress: r.client_evm_address,
1211
+ // the client claims the server's leg
1212
+ expectedSats: r.evm_expected_sats,
1213
+ token: r.wbtc_address
1214
+ }),
1215
+ clientRefundLocktime: 0,
1216
+ // no on-chain client leg
1217
+ serverRefundLocktime: ms(r.evm_refund_locktime)
1218
+ };
1219
+ default:
1220
+ return void 0;
1221
+ }
1222
+ }
1223
+
1224
+ // src/tracker/swap-tracker.ts
1225
+ var SwapTracker = class {
1226
+ #managers;
1227
+ #swaps = /* @__PURE__ */ new Map();
1228
+ /** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
1229
+ #lastActions = /* @__PURE__ */ new Map();
1230
+ #subscribers = /* @__PURE__ */ new Set();
1231
+ #refreshIntervalMs;
1232
+ #eventUnsubs = [];
1233
+ #timer;
1234
+ constructor(managers, options) {
1235
+ this.#managers = managers;
1236
+ this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
1237
+ }
1238
+ /** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
1239
+ async startTracking(swaps) {
1240
+ for (const swap of swaps) {
1241
+ this.#swaps.set(swap.swapId, swap);
1242
+ for (const leg of legsOf(swap)) await this.#managerFor(leg).register(leg);
1243
+ }
1244
+ for (const manager of new Set(this.#managers.values())) {
1245
+ this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
1246
+ }
1247
+ await this.#refreshManagers();
1248
+ this.#recomputeAll();
1249
+ if (this.#refreshIntervalMs > 0)
1250
+ this.#timer = setInterval(
1251
+ () => void this.#tick(),
1252
+ this.#refreshIntervalMs
1253
+ );
1254
+ }
1255
+ async #tick() {
1256
+ await this.#refreshManagers();
1257
+ this.#recomputeAll();
1258
+ }
1259
+ /**
1260
+ * Re-poll the managers that have a registered leg — seeding/advancing their
1261
+ * clocks and reconciling observations. Managers with nothing tracked are
1262
+ * skipped: refreshing them still hits their clock source (e.g. Arkade/Bitcoin
1263
+ * MTP via the API), so a swap that doesn't touch a ledger can't fail on that
1264
+ * ledger's endpoint being down (the default client builds every manager).
1265
+ */
1266
+ async #refreshManagers() {
1267
+ const active = /* @__PURE__ */ new Set();
1268
+ for (const swap of this.#swaps.values())
1269
+ for (const leg of legsOf(swap)) active.add(leg.ledger);
1270
+ const managers = new Set(
1271
+ [...active].map((ledger) => this.#managers.get(ledger)).filter((m) => m !== void 0)
1272
+ );
1273
+ await Promise.all([...managers].map((manager) => manager.refresh()));
1274
+ }
1275
+ /** Notify `cb` of the current action for each tracked swap, then on every change. */
1276
+ subscribeToActions(cb) {
1277
+ this.#subscribers.add(cb);
1278
+ for (const [swapId, actions] of this.#lastActions) cb(swapId, actions);
1279
+ return () => this.#subscribers.delete(cb);
1280
+ }
1281
+ /**
1282
+ * Stop tracking: unregister every still-tracked leg (releasing its manager's
1283
+ * watch — e.g. the EVM per-chain block watch that only `unregister` tears down),
1284
+ * drop the poll timer, and detach event listeners + subscribers. Mirrors
1285
+ * `startTracking`'s `register`, so no manager keeps watching a stopped tracker's
1286
+ * swaps. The managers themselves are not owned here, so they are unregistered but
1287
+ * never disposed.
1288
+ */
1289
+ stop() {
1290
+ if (this.#timer) clearInterval(this.#timer);
1291
+ this.#timer = void 0;
1292
+ for (const unsub of this.#eventUnsubs) unsub();
1293
+ this.#eventUnsubs = [];
1294
+ for (const swap of this.#swaps.values())
1295
+ for (const leg of legsOf(swap))
1296
+ void this.#managerFor(leg).unregister(leg);
1297
+ this.#swaps.clear();
1298
+ this.#subscribers.clear();
1299
+ }
1300
+ #managerFor(ref) {
1301
+ const manager = this.#managers.get(ref.ledger);
1302
+ if (!manager)
1303
+ throw new Error(`no ContractManager for ledger '${ref.ledger}'`);
1304
+ return manager;
1305
+ }
1306
+ #recomputeAll() {
1307
+ for (const swap of this.#swaps.values()) this.#recompute(swap);
1308
+ }
1309
+ #recompute(swap) {
1310
+ let clientHtlc;
1311
+ let serverHtlc;
1312
+ let clientChainNow = 0;
1313
+ let serverChainNow = 0;
1314
+ if (swap.clientHtlc) {
1315
+ const m = this.#managerFor(swap.clientHtlc);
1316
+ clientHtlc = m.getState(swap.clientHtlc);
1317
+ const now = m.chainNow(swap.clientHtlc);
1318
+ if (clientHtlc === void 0 || now === void 0) return;
1319
+ clientChainNow = now;
1320
+ }
1321
+ if (swap.serverHtlc) {
1322
+ const m = this.#managerFor(swap.serverHtlc);
1323
+ serverHtlc = m.getState(swap.serverHtlc);
1324
+ const now = m.chainNow(swap.serverHtlc);
1325
+ if (serverHtlc === void 0 || now === void 0) return;
1326
+ serverChainNow = now;
1327
+ }
1328
+ const status = deriveSwapStatus({ clientHtlc, serverHtlc });
1329
+ if (status === void 0) return;
1330
+ const actions = deriveSwapActions({
1331
+ status,
1332
+ clientChainNow,
1333
+ serverChainNow,
1334
+ clientRefundLocktime: swap.clientRefundLocktime,
1335
+ serverRefundLocktime: swap.serverRefundLocktime,
1336
+ // Pay-on-Lightning swaps have no client-funded on-chain leg.
1337
+ clientFunds: swap.clientHtlc !== void 0
1338
+ });
1339
+ const previous = this.#lastActions.get(swap.swapId);
1340
+ if (previous && JSON.stringify(previous) === JSON.stringify(actions))
1341
+ return;
1342
+ this.#lastActions.set(swap.swapId, actions);
1343
+ for (const cb of this.#subscribers) cb(swap.swapId, actions);
1344
+ if (actions.recommended === "none") this.#untrack(swap);
1345
+ }
1346
+ #untrack(swap) {
1347
+ this.#swaps.delete(swap.swapId);
1348
+ for (const leg of legsOf(swap)) void this.#managerFor(leg).unregister(leg);
1349
+ }
1350
+ };
1351
+ function legsOf(swap) {
1352
+ const legs = [];
1353
+ if (swap.clientHtlc) legs.push(swap.clientHtlc);
1354
+ if (swap.serverHtlc) legs.push(swap.serverHtlc);
1355
+ return legs;
1356
+ }
1357
+
1358
+ // src/client.ts
1359
+ var Client = class {
1360
+ /** The wrapped legacy client. Calls are forwarded here until migrated. */
1361
+ #legacy;
1362
+ /** Tracking configuration; managers are built lazily from it on first use. */
1363
+ #tracking;
1364
+ /** Per-ledger monitors, resolved once from {@link #tracking}. */
1365
+ #managers;
1366
+ #tracker;
1367
+ #started = false;
1368
+ constructor(...args) {
1369
+ if (args[0] instanceof import_lendaswap_sdk_pure.Client) {
1370
+ this.#legacy = args[0];
1371
+ this.#tracking = args[1] ?? {
1372
+ enabled: false
1373
+ };
1374
+ } else {
1375
+ this.#legacy = new import_lendaswap_sdk_pure.Client(
1376
+ ...args
1377
+ );
1378
+ this.#tracking = { enabled: false };
1379
+ }
1380
+ }
1381
+ /** Start building a {@link Client}. */
1382
+ static builder() {
1383
+ return new ClientBuilder();
1384
+ }
1385
+ /** Escape hatch: the underlying legacy client, during migration. */
1386
+ get legacy() {
1387
+ return this.#legacy;
1388
+ }
1389
+ // --- delegated legacy surface (getters) ---
1390
+ /** Delegated to the legacy client (migration checkpoint). */
1391
+ get api() {
1392
+ return this.#legacy.api;
1393
+ }
1394
+ /** Delegated to the legacy client (migration checkpoint). */
1395
+ get baseUrl() {
1396
+ return this.#legacy.baseUrl;
1397
+ }
1398
+ /** Delegated to the legacy client (migration checkpoint). */
1399
+ get cctpInbound() {
1400
+ return this.#legacy.cctpInbound;
1401
+ }
1402
+ /** Delegated to the legacy client (migration checkpoint). */
1403
+ get swapStorage() {
1404
+ return this.#legacy.swapStorage;
1405
+ }
1406
+ // --- delegated legacy surface (methods) ---
1407
+ /** Delegated to the legacy client (migration checkpoint). */
1408
+ subscribeToSwaps(...args) {
1409
+ return this.#legacy.subscribeToSwaps(...args);
1410
+ }
1411
+ /** Delegated to the legacy client (migration checkpoint). */
1412
+ unsubscribeFromSwaps(...args) {
1413
+ return this.#legacy.unsubscribeFromSwaps(...args);
1414
+ }
1415
+ /** Delegated to the legacy client (migration checkpoint). */
1416
+ closeSwapStatusSocket(...args) {
1417
+ return this.#legacy.closeSwapStatusSocket(...args);
1418
+ }
1419
+ /** Delegated to the legacy client (migration checkpoint). */
1420
+ getMnemonic(...args) {
1421
+ return this.#legacy.getMnemonic(...args);
1422
+ }
1423
+ /** Delegated to the legacy client (migration checkpoint). */
1424
+ loadMnemonic(...args) {
1425
+ return this.#legacy.loadMnemonic(...args);
1426
+ }
1427
+ /** Delegated to the legacy client (migration checkpoint). */
1428
+ getNostrKeyHex(...args) {
1429
+ return this.#legacy.getNostrKeyHex(...args);
1430
+ }
1431
+ /** Delegated to the legacy client (migration checkpoint). */
1432
+ getUserIdXpub(...args) {
1433
+ return this.#legacy.getUserIdXpub(...args);
1434
+ }
1435
+ /** Delegated to the legacy client (migration checkpoint). */
1436
+ deriveSwapParams(...args) {
1437
+ return this.#legacy.deriveSwapParams(...args);
1438
+ }
1439
+ /** Delegated to the legacy client (migration checkpoint). */
1440
+ deriveSwapParamsAtIndex(...args) {
1441
+ return this.#legacy.deriveSwapParamsAtIndex(...args);
1442
+ }
1443
+ /** Delegated to the legacy client (migration checkpoint). */
1444
+ getEvmAddress(...args) {
1445
+ return this.#legacy.getEvmAddress(...args);
1446
+ }
1447
+ /** Delegated to the legacy client (migration checkpoint). */
1448
+ getKeyIndex(...args) {
1449
+ return this.#legacy.getKeyIndex(...args);
1450
+ }
1451
+ /** Delegated to the legacy client (migration checkpoint). */
1452
+ setKeyIndex(...args) {
1453
+ return this.#legacy.setKeyIndex(...args);
1454
+ }
1455
+ /** Delegated to the legacy client (migration checkpoint). */
1456
+ getStatus(...args) {
1457
+ return this.#legacy.getStatus(...args);
1458
+ }
1459
+ /** Delegated to the legacy client (migration checkpoint). */
1460
+ healthCheck(...args) {
1461
+ return this.#legacy.healthCheck(...args);
1462
+ }
1463
+ /** Delegated to the legacy client (migration checkpoint). */
1464
+ getVersion(...args) {
1465
+ return this.#legacy.getVersion(...args);
1466
+ }
1467
+ /** Delegated to the legacy client (migration checkpoint). */
1468
+ getSupportAgents(...args) {
1469
+ return this.#legacy.getSupportAgents(...args);
1470
+ }
1471
+ /** Delegated to the legacy client (migration checkpoint). */
1472
+ getMtp(...args) {
1473
+ return this.#legacy.getMtp(...args);
1474
+ }
1475
+ /** Delegated to the legacy client (migration checkpoint). */
1476
+ getTokens(...args) {
1477
+ return this.#legacy.getTokens(...args);
1478
+ }
1479
+ /** Delegated to the legacy client (migration checkpoint). */
1480
+ getSwapPairs(...args) {
1481
+ return this.#legacy.getSwapPairs(...args);
1482
+ }
1483
+ /** Delegated to the legacy client (migration checkpoint). */
1484
+ getChainConfig(...args) {
1485
+ return this.#legacy.getChainConfig(...args);
1486
+ }
1487
+ /** Delegated to the legacy client (migration checkpoint). */
1488
+ getReferralFee(...args) {
1489
+ return this.#legacy.getReferralFee(...args);
1490
+ }
1491
+ /** Delegated to the legacy client (migration checkpoint). */
1492
+ getNetworkFees(...args) {
1493
+ return this.#legacy.getNetworkFees(...args);
1494
+ }
1495
+ /** Delegated to the legacy client (migration checkpoint). */
1496
+ getDexQuote(...args) {
1497
+ return this.#legacy.getDexQuote(...args);
1498
+ }
1499
+ /** Delegated to the legacy client (migration checkpoint). */
1500
+ composeQuote(...args) {
1501
+ return this.#legacy.composeQuote(...args);
1502
+ }
1503
+ /** Delegated to the legacy client (migration checkpoint). */
1504
+ getQuote(...args) {
1505
+ return this.#legacy.getQuote(...args);
1506
+ }
1507
+ /** Delegated to the legacy client (migration checkpoint). */
1508
+ getSwap(...args) {
1509
+ return this.#legacy.getSwap(...args);
1510
+ }
1511
+ /** Delegated to the legacy client (migration checkpoint). */
1512
+ getBulkStatus(...args) {
1513
+ return this.#legacy.getBulkStatus(...args);
1514
+ }
1515
+ /** Delegated to the legacy client (migration checkpoint). */
1516
+ getStoredSwap(...args) {
1517
+ return this.#legacy.getStoredSwap(...args);
1518
+ }
1519
+ /** Delegated to the legacy client (migration checkpoint). */
1520
+ listAllSwaps(...args) {
1521
+ return this.#legacy.listAllSwaps(...args);
1522
+ }
1523
+ /** Delegated to the legacy client (migration checkpoint). */
1524
+ deleteSwap(...args) {
1525
+ return this.#legacy.deleteSwap(...args);
1526
+ }
1527
+ /** Delegated to the legacy client (migration checkpoint). */
1528
+ clearSwapStorage(...args) {
1529
+ return this.#legacy.clearSwapStorage(...args);
1530
+ }
1531
+ /** Delegated to the legacy client (migration checkpoint). */
1532
+ recoverSwaps(...args) {
1533
+ return this.#legacy.recoverSwaps(...args);
1534
+ }
1535
+ /** Delegated to the legacy client (migration checkpoint). */
1536
+ recoverAllSwaps(...args) {
1537
+ return this.#legacy.recoverAllSwaps(...args);
1538
+ }
1539
+ /** Delegated to the legacy client (migration checkpoint). */
1540
+ amountsForSwap(...args) {
1541
+ return this.#legacy.amountsForSwap(...args);
1542
+ }
1543
+ /** Delegated to the legacy client (migration checkpoint). */
1544
+ claim(...args) {
1545
+ return this.#legacy.claim(...args);
1546
+ }
1547
+ /** Delegated to the legacy client (migration checkpoint). */
1548
+ claimViaGasless(...args) {
1549
+ return this.#legacy.claimViaGasless(...args);
1550
+ }
1551
+ /** Delegated to the legacy client (migration checkpoint). */
1552
+ claimArkade(...args) {
1553
+ return this.#legacy.claimArkade(...args);
1554
+ }
1555
+ /** Delegated to the legacy client (migration checkpoint). */
1556
+ continueArkadeClaimSwap(...args) {
1557
+ return this.#legacy.continueArkadeClaimSwap(...args);
1558
+ }
1559
+ /** Delegated to the legacy client (migration checkpoint). */
1560
+ refundSwap(...args) {
1561
+ return this.#legacy.refundSwap(...args);
1562
+ }
1563
+ /** Delegated to the legacy client (migration checkpoint). */
1564
+ getCollabRefundEvmParams(...args) {
1565
+ return this.#legacy.getCollabRefundEvmParams(...args);
1566
+ }
1567
+ /** Delegated to the legacy client (migration checkpoint). */
1568
+ buildCollabRefundEvmTypedData(...args) {
1569
+ return this.#legacy.buildCollabRefundEvmTypedData(...args);
1570
+ }
1571
+ /** Delegated to the legacy client (migration checkpoint). */
1572
+ collabRefundEvmSwap(...args) {
1573
+ return this.#legacy.collabRefundEvmSwap(...args);
1574
+ }
1575
+ /** Delegated to the legacy client (migration checkpoint). */
1576
+ submitCollabRefundEvm(...args) {
1577
+ return this.#legacy.submitCollabRefundEvm(...args);
1578
+ }
1579
+ /** Delegated to the legacy client (migration checkpoint). */
1580
+ createSwap(...args) {
1581
+ return this.#legacy.createSwap(...args);
1582
+ }
1583
+ /** Delegated to the legacy client (migration checkpoint). */
1584
+ createArkadeToEvmSwapGeneric(...args) {
1585
+ return this.#legacy.createArkadeToEvmSwapGeneric(...args);
1586
+ }
1587
+ /** Delegated to the legacy client (migration checkpoint). */
1588
+ createLightningToEvmSwapGeneric(...args) {
1589
+ return this.#legacy.createLightningToEvmSwapGeneric(...args);
1590
+ }
1591
+ /** Delegated to the legacy client (migration checkpoint). */
1592
+ createBitcoinToEvmSwap(...args) {
1593
+ return this.#legacy.createBitcoinToEvmSwap(...args);
1594
+ }
1595
+ /** Delegated to the legacy client (migration checkpoint). */
1596
+ createBitcoinToArkadeSwap(...args) {
1597
+ return this.#legacy.createBitcoinToArkadeSwap(...args);
1598
+ }
1599
+ /** Delegated to the legacy client (migration checkpoint). */
1600
+ createLightningToArkadeSwap(...args) {
1601
+ return this.#legacy.createLightningToArkadeSwap(...args);
1602
+ }
1603
+ /** Delegated to the legacy client (migration checkpoint). */
1604
+ createArkadeToLightningSwap(...args) {
1605
+ return this.#legacy.createArkadeToLightningSwap(...args);
1606
+ }
1607
+ /** Delegated to the legacy client (migration checkpoint). */
1608
+ getArkadeToLightningQuote(...args) {
1609
+ return this.#legacy.getArkadeToLightningQuote(...args);
1610
+ }
1611
+ /** Delegated to the legacy client (migration checkpoint). */
1612
+ retryArkadeToLightningSwap(...args) {
1613
+ return this.#legacy.retryArkadeToLightningSwap(...args);
1614
+ }
1615
+ /** Delegated to the legacy client (migration checkpoint). */
1616
+ createEvmToArkadeSwapGeneric(...args) {
1617
+ return this.#legacy.createEvmToArkadeSwapGeneric(...args);
1618
+ }
1619
+ /** Delegated to the legacy client (migration checkpoint). */
1620
+ createEvmToBitcoinSwap(...args) {
1621
+ return this.#legacy.createEvmToBitcoinSwap(...args);
1622
+ }
1623
+ /** Delegated to the legacy client (migration checkpoint). */
1624
+ createEvmToLightningSwapGeneric(...args) {
1625
+ return this.#legacy.createEvmToLightningSwapGeneric(...args);
1626
+ }
1627
+ /** Delegated to the legacy client (migration checkpoint). */
1628
+ getCoordinatorFundingCallDataPermit2(...args) {
1629
+ return this.#legacy.getCoordinatorFundingCallDataPermit2(...args);
1630
+ }
1631
+ /** Delegated to the legacy client (migration checkpoint). */
1632
+ getPermit2FundingParamsUnsigned(...args) {
1633
+ return this.#legacy.getPermit2FundingParamsUnsigned(...args);
1634
+ }
1635
+ /** Delegated to the legacy client (migration checkpoint). */
1636
+ fundSwap(...args) {
1637
+ return this.#legacy.fundSwap(...args);
1638
+ }
1639
+ /** Delegated to the legacy client (migration checkpoint). */
1640
+ refundEvmWithSigner(...args) {
1641
+ return this.#legacy.refundEvmWithSigner(...args);
1642
+ }
1643
+ /** Delegated to the legacy client (migration checkpoint). */
1644
+ collabRefundEvmWithSigner(...args) {
1645
+ return this.#legacy.collabRefundEvmWithSigner(...args);
1646
+ }
1647
+ /** Delegated to the legacy client (migration checkpoint). */
1648
+ fundSwapGasless(...args) {
1649
+ return this.#legacy.fundSwapGasless(...args);
1650
+ }
1651
+ /** Delegated to the legacy client (migration checkpoint). */
1652
+ getSwapDepositorKey(...args) {
1653
+ return this.#legacy.getSwapDepositorKey(...args);
1654
+ }
1655
+ /** Delegated to the legacy client (migration checkpoint). */
1656
+ getEvmDepositorKey(...args) {
1657
+ return this.#legacy.getEvmDepositorKey(...args);
1658
+ }
1659
+ /** Delegated to the legacy client (migration checkpoint). */
1660
+ recoverGaslessFunds(...args) {
1661
+ return this.#legacy.recoverGaslessFunds(...args);
1662
+ }
1663
+ /** Delegated to the legacy client (migration checkpoint). */
1664
+ hasReceivedVtxo(...args) {
1665
+ return this.#legacy.hasReceivedVtxo(...args);
1666
+ }
1667
+ // --- Satora-native features go below ---
1668
+ /**
1669
+ * Start observing the user's active swaps and deriving each one's next action.
1670
+ *
1671
+ * Loads stored swaps, maps the ones whose ledgers are observable to
1672
+ * {@link TrackedSwap}s, and runs the per-ledger monitors. Idempotent. Requires
1673
+ * the client to have been built with {@link ClientBuilder.withTracking}.
1674
+ * Subscribe with {@link subscribeToActions}; release with
1675
+ * {@link stopTracking}.
1676
+ */
1677
+ async startTracking() {
1678
+ if (!this.#tracking.enabled)
1679
+ throw new Error(
1680
+ "tracking is disabled \u2014 remove .withoutTracking() to enable it"
1681
+ );
1682
+ if (this.#started) return;
1683
+ this.#started = true;
1684
+ try {
1685
+ const tracker = new SwapTracker(await this.#ensureManagers(), {
1686
+ refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
1687
+ });
1688
+ this.#tracker = tracker;
1689
+ const swaps = await this.listAllSwaps();
1690
+ const tracked = swaps.map(swapToTracked).filter((s) => s !== void 0);
1691
+ await tracker.startTracking(tracked);
1692
+ } catch (error) {
1693
+ this.#tracker?.stop();
1694
+ this.#tracker = void 0;
1695
+ this.#started = false;
1696
+ throw error;
1697
+ }
1698
+ }
1699
+ /**
1700
+ * Subscribe to next-action updates: `cb` fires with the current action for each
1701
+ * tracked swap immediately, then on every change. Call after
1702
+ * {@link startTracking}. Returns an unsubscribe fn.
1703
+ */
1704
+ subscribeToActions(cb) {
1705
+ if (!this.#tracker)
1706
+ throw new Error("call startTracking() before subscribeToActions()");
1707
+ return this.#tracker.subscribeToActions(cb);
1708
+ }
1709
+ /** Stop tracking and drop subscribers. The managers themselves are not disposed. */
1710
+ stopTracking() {
1711
+ this.#tracker?.stop();
1712
+ this.#tracker = void 0;
1713
+ this.#started = false;
1714
+ }
1715
+ /**
1716
+ * Resolve the per-ledger managers once — the explicit override if given, else
1717
+ * auto-built from config: an {@link ArkadeContractManager} from the configured
1718
+ * Ark server URL (defaulting to the mainnet server), clocked by `getMtp`, and an
1719
+ * {@link EvmContractManager} from the tested default RPCs.
1720
+ */
1721
+ async #ensureManagers() {
1722
+ if (this.#managers) return this.#managers;
1723
+ if (this.#tracking.managers) {
1724
+ this.#managers = this.#tracking.managers;
1725
+ return this.#managers;
1726
+ }
1727
+ const managers = /* @__PURE__ */ new Map();
1728
+ const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
1729
+ const chainTime = async () => (await this.getMtp()).mtp * 1e3;
1730
+ const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
1731
+ if (arkadeUrl) {
1732
+ managers.set(
1733
+ "arkade",
1734
+ await ArkadeContractManager.create({ serverUrl: arkadeUrl, chainTime })
1735
+ );
1736
+ }
1737
+ const readers = defaultEvmReaders(evmRpcUrls);
1738
+ if (readers.size > 0)
1739
+ managers.set("evm", EvmContractManager.fromDeps({ readers }));
1740
+ managers.set(
1741
+ "bitcoin",
1742
+ await BitcoinContractManager.create({
1743
+ esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
1744
+ chainTime
1745
+ })
1746
+ );
1747
+ this.#managers = managers;
1748
+ return managers;
1749
+ }
1750
+ };
1751
+ var ClientBuilder = class {
1752
+ #inner = import_lendaswap_sdk_pure.Client.builder();
1753
+ // Observe-mode tracking is on by default; the built client auto-builds its
1754
+ // managers from config unless disabled or given an explicit override.
1755
+ #trackingEnabled = true;
1756
+ #arkadeServerUrl;
1757
+ #esploraUrl;
1758
+ #evmRpcUrls;
1759
+ #managers;
1760
+ /**
1761
+ * Override the EVM JSON-RPC endpoint per chainId. Optional — tracking uses
1762
+ * tested public defaults otherwise; an override is tried first, with the
1763
+ * defaults kept as fallbacks.
1764
+ */
1765
+ withEvmRpcUrls(urls) {
1766
+ this.#evmRpcUrls = urls;
1767
+ return this;
1768
+ }
1769
+ /** Turn observe-mode tracking off. */
1770
+ withoutTracking() {
1771
+ this.#trackingEnabled = false;
1772
+ return this;
1773
+ }
1774
+ /**
1775
+ * Advanced: supply per-ledger {@link ContractManager}s explicitly instead of
1776
+ * letting the client auto-build them from config (useful for tests or custom
1777
+ * chain sources).
1778
+ */
1779
+ withContractManagers(managers) {
1780
+ this.#managers = managers;
1781
+ return this;
1782
+ }
1783
+ withBaseUrl(...args) {
1784
+ this.#inner.withBaseUrl(...args);
1785
+ return this;
1786
+ }
1787
+ withReferralCode(...args) {
1788
+ this.#inner.withReferralCode(...args);
1789
+ return this;
1790
+ }
1791
+ withOrgCode(...args) {
1792
+ this.#inner.withOrgCode(...args);
1793
+ return this;
1794
+ }
1795
+ withDefaultHeaders(...args) {
1796
+ this.#inner.withDefaultHeaders(...args);
1797
+ return this;
1798
+ }
1799
+ /**
1800
+ * Override the Esplora REST URL. Optional for tracking — the Bitcoin manager
1801
+ * otherwise defaults to mainnet mempool.space; set this for a dev/regtest node.
1802
+ */
1803
+ withEsploraUrl(url) {
1804
+ this.#inner.withEsploraUrl(url);
1805
+ this.#esploraUrl = url;
1806
+ return this;
1807
+ }
1808
+ /**
1809
+ * Override the Ark server URL. Optional for tracking — it otherwise defaults to
1810
+ * the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
1811
+ */
1812
+ withArkadeServerUrl(url) {
1813
+ this.#inner.withArkadeServerUrl(url);
1814
+ this.#arkadeServerUrl = url;
1815
+ return this;
1816
+ }
1817
+ withSignerStorage(...args) {
1818
+ this.#inner.withSignerStorage(...args);
1819
+ return this;
1820
+ }
1821
+ withSwapStorage(...args) {
1822
+ this.#inner.withSwapStorage(...args);
1823
+ return this;
1824
+ }
1825
+ withMnemonic(...args) {
1826
+ this.#inner.withMnemonic(...args);
1827
+ return this;
1828
+ }
1829
+ withXprv(...args) {
1830
+ this.#inner.withXprv(...args);
1831
+ return this;
1832
+ }
1833
+ withAa(...args) {
1834
+ this.#inner.withAa(...args);
1835
+ return this;
1836
+ }
1837
+ withLogger(...args) {
1838
+ this.#inner.withLogger(...args);
1839
+ return this;
1840
+ }
1841
+ withLogLevel(...args) {
1842
+ this.#inner.withLogLevel(...args);
1843
+ return this;
1844
+ }
1845
+ async build() {
1846
+ return new Client(await this.#inner.build(), {
1847
+ enabled: this.#trackingEnabled,
1848
+ managers: this.#managers,
1849
+ arkadeServerUrl: this.#arkadeServerUrl,
1850
+ esploraUrl: this.#esploraUrl,
1851
+ evmRpcUrls: this.#evmRpcUrls
1852
+ });
1853
+ }
1854
+ };
1855
+
1856
+ // src/index.ts
1857
+ init_bitcoin_reader_esplora();
21
1858
  // Annotate the CommonJS export names for ESM import in node:
22
1859
  0 && (module.exports = {
1860
+ ArkadeContractManager,
1861
+ BitcoinContractManager,
1862
+ Client,
1863
+ ClientBuilder,
1864
+ DEFAULT_ESPLORA_URLS,
1865
+ DEFAULT_EVM_RPCS,
1866
+ EvmContractManager,
1867
+ SwapTracker,
1868
+ buildArkadeVhtlcRef,
1869
+ createEvmRpcReader,
1870
+ defaultArkadeServerUrl,
1871
+ defaultEvmReaders,
1872
+ deriveSwapActions,
1873
+ deriveSwapStatus,
1874
+ esploraReader,
1875
+ evmReaderFromClient,
1876
+ htlcFactsFromEsploraTxs,
1877
+ htlcKey,
1878
+ swapToTracked,
23
1879
  ...require("@lendasat/lendaswap-sdk-pure")
24
1880
  });
25
1881
  //# sourceMappingURL=index.cjs.map