@satora/swap 0.3.2 → 1.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.js CHANGED
@@ -2,7 +2,10 @@ import {
2
2
  DEFAULT_ESPLORA_URLS,
3
3
  esploraReader,
4
4
  htlcFactsFromEsploraTxs
5
- } from "./chunk-EIWELJJK.js";
5
+ } from "./chunk-THI4UO6R.js";
6
+ import {
7
+ electrumReader
8
+ } from "./chunk-FTPIQSRW.js";
6
9
 
7
10
  // src/index.ts
8
11
  export * from "@lendasat/lendaswap-sdk-pure";
@@ -311,6 +314,7 @@ function payOnLightningStatus(serverHtlc) {
311
314
 
312
315
  // src/client.ts
313
316
  import {
317
+ DEFAULT_ELECTRUM_WS_URLS,
314
318
  Client as LegacyClient
315
319
  } from "@lendasat/lendaswap-sdk-pure";
316
320
 
@@ -344,10 +348,12 @@ function arkadeObservation(facts) {
344
348
  }
345
349
  function classifyArkadeSpend(encodedSpendTx, paymentHash) {
346
350
  const tx = Transaction.fromPSBT(base64.decode(encodedSpendTx));
347
- for (const witness of getArkPsbtFields(tx, 0, ConditionWitness)) {
348
- for (const element of witness) {
349
- if (element.length === 32 && preimageMatches(element, paymentHash)) {
350
- return { spend: "claim", preimage: element };
351
+ for (let inputIndex = 0; inputIndex < tx.inputsLength; inputIndex++) {
352
+ for (const witness of getArkPsbtFields(tx, inputIndex, ConditionWitness)) {
353
+ for (const element of witness) {
354
+ if (element.length === 32 && preimageMatches(element, paymentHash)) {
355
+ return { spend: "claim", preimage: element };
356
+ }
351
357
  }
352
358
  }
353
359
  }
@@ -551,6 +557,8 @@ var BitcoinContractManager = class _BitcoinContractManager {
551
557
  #obs = /* @__PURE__ */ new Map();
552
558
  /** address → the preimage a claim revealed. */
553
559
  #preimages = /* @__PURE__ */ new Map();
560
+ /** address → push-subscription teardown (only when the reader supports push). */
561
+ #subscriptions = /* @__PURE__ */ new Map();
554
562
  #listeners = /* @__PURE__ */ new Set();
555
563
  /** Last MTP reading (ms) + when it was fetched, for extrapolation. */
556
564
  #now;
@@ -562,11 +570,25 @@ var BitcoinContractManager = class _BitcoinContractManager {
562
570
  return new _BitcoinContractManager(deps);
563
571
  }
564
572
  static async create(config) {
565
- const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-HV6CANAA.js");
573
+ const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-ZLPMM5FF.js");
574
+ let reader = esploraReader2(
575
+ config.esploraUrl,
576
+ void 0,
577
+ { minConfirmations: config.minConfirmations }
578
+ );
579
+ if (config.electrumWsUrl) {
580
+ const [{ electrumReader: electrumReader2 }, { ElectrumWsClient }] = await Promise.all([
581
+ import("./bitcoin-reader-electrum-PY7DHD6T.js"),
582
+ import("@lendasat/lendaswap-sdk-pure")
583
+ ]);
584
+ reader = electrumReader2(new ElectrumWsClient(config.electrumWsUrl), {
585
+ network: config.network,
586
+ minConfirmations: config.minConfirmations,
587
+ fallback: reader
588
+ });
589
+ }
566
590
  return _BitcoinContractManager.fromDeps({
567
- reader: esploraReader2(config.esploraUrl, void 0, {
568
- minConfirmations: config.minConfirmations
569
- }),
591
+ reader,
570
592
  chainTime: config.chainTime
571
593
  });
572
594
  }
@@ -579,10 +601,25 @@ var BitcoinContractManager = class _BitcoinContractManager {
579
601
  `BitcoinContractManager can't track a '${ref.ledger}' HTLC`
580
602
  );
581
603
  this.#refs.set(ref.address, ref);
604
+ if (this.#reader.subscribe && !this.#subscriptions.has(ref.address)) {
605
+ this.#subscriptions.set(
606
+ ref.address,
607
+ this.#reader.subscribe(ref.address, () => {
608
+ this.#reconcileRef(ref).catch((error) => {
609
+ console.warn(
610
+ "BitcoinContractManager: push reconcile failed:",
611
+ error instanceof Error ? error.message : error
612
+ );
613
+ });
614
+ })
615
+ );
616
+ }
582
617
  await this.#reconcileRef(ref);
583
618
  }
584
619
  async unregister(ref) {
585
620
  if (ref.ledger !== "bitcoin") return;
621
+ this.#subscriptions.get(ref.address)?.();
622
+ this.#subscriptions.delete(ref.address);
586
623
  this.#refs.delete(ref.address);
587
624
  this.#obs.delete(ref.address);
588
625
  this.#preimages.delete(ref.address);
@@ -617,6 +654,8 @@ var BitcoinContractManager = class _BitcoinContractManager {
617
654
  this.#now = { mtpMs: await this.#chainTime(), fetchedAtMs: Date.now() };
618
655
  }
619
656
  dispose() {
657
+ for (const unsubscribe of this.#subscriptions.values()) unsubscribe();
658
+ this.#subscriptions.clear();
620
659
  this.#listeners.clear();
621
660
  }
622
661
  /** The preimage a claim revealed on this HTLC, if one was seen. */
@@ -1519,9 +1558,8 @@ function defaultWebSocketFactory(url) {
1519
1558
  }
1520
1559
 
1521
1560
  // src/contracts/arkade-vhtlc.ts
1522
- import { VHTLC, VHTLCContractHandler } from "@arkade-os/sdk";
1561
+ import { VHTLCContractHandler } from "@arkade-os/sdk";
1523
1562
  import {
1524
- ARKADE_HTLC_SCRIPT_VERSION_LEGACY,
1525
1563
  ARKADE_HTLC_SCRIPT_VERSION_STRICT,
1526
1564
  StrictVhtlcScript
1527
1565
  } from "@lendasat/lendaswap-sdk-pure";
@@ -1551,18 +1589,12 @@ function buildArkadeVhtlcRef(input) {
1551
1589
  input.unilateralRefundWithoutReceiverDelay
1552
1590
  )
1553
1591
  };
1554
- const vhtlc = (() => {
1555
- switch (input.arkadeHtlcScriptVersion ?? ARKADE_HTLC_SCRIPT_VERSION_LEGACY) {
1556
- case ARKADE_HTLC_SCRIPT_VERSION_LEGACY:
1557
- return new VHTLC.Script(params);
1558
- case ARKADE_HTLC_SCRIPT_VERSION_STRICT:
1559
- return new StrictVhtlcScript(params);
1560
- default:
1561
- throw new Error(
1562
- `Unsupported Arkade HTLC script version: ${input.arkadeHtlcScriptVersion}`
1563
- );
1564
- }
1565
- })();
1592
+ if (input.arkadeHtlcScriptVersion !== void 0 && input.arkadeHtlcScriptVersion !== ARKADE_HTLC_SCRIPT_VERSION_STRICT) {
1593
+ throw new Error(
1594
+ `Unsupported Arkade HTLC script version: ${input.arkadeHtlcScriptVersion}`
1595
+ );
1596
+ }
1597
+ const vhtlc = new StrictVhtlcScript(params);
1566
1598
  return {
1567
1599
  ledger: "arkade",
1568
1600
  script: hex5.encode(vhtlc.pkScript),
@@ -1759,47 +1791,6 @@ function swapToTracked(stored) {
1759
1791
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1760
1792
  };
1761
1793
  // ─── Lightning: one on-chain leg, one off-chain LN payment ───────────────
1762
- // Receive-on-Lightning: the client funds an on-chain HTLC and is paid over
1763
- // Lightning. No server leg to watch — the server sweeping the client's leg
1764
- // (with the preimage it got by paying the invoice) is the done signal.
1765
- // Client funds the Arkade VHTLC; Boltz pays the invoice. The deposit is
1766
- // `boltz_amount_sats` (source minus fees) — the amount actually locked, not
1767
- // `source_amount` — else the client's own funding reads as under-funded.
1768
- case "arkade_to_lightning":
1769
- return {
1770
- swapId: r.id,
1771
- clientHtlc: arkadeLeg(
1772
- r,
1773
- r.arkade_vhtlc_address,
1774
- Number(r.boltz_amount_sats)
1775
- ),
1776
- clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1777
- serverRefundLocktime: 0
1778
- // no on-chain server leg
1779
- };
1780
- // Client funds the EVM HTLC (server claims it after paying the invoice).
1781
- case "evm_to_lightning":
1782
- return {
1783
- swapId: r.id,
1784
- clientHtlc: evmLeg({
1785
- chainId: r.evm_chain_id,
1786
- htlc: r.evm_htlc_address,
1787
- hashLock: r.hash_lock,
1788
- claimAddress: r.server_evm_address,
1789
- // the server claims the client's leg
1790
- expectedSats: r.evm_expected_sats,
1791
- // evm_to_lightning doesn't expose the locked token — fall back to the
1792
- // per-chain mainnet constant so the isActive tuple is complete.
1793
- token: lockedTokenFallback(r.evm_chain_id, r.network),
1794
- sender: r.client_evm_address,
1795
- // the client funded it
1796
- timelockSec: r.evm_refund_locktime,
1797
- createdAt: r.created_at
1798
- }),
1799
- clientRefundLocktime: ms(r.evm_refund_locktime),
1800
- serverRefundLocktime: 0
1801
- // no on-chain server leg
1802
- };
1803
1794
  // Pay-on-Lightning: the client pays a Lightning invoice (off-chain, nothing to
1804
1795
  // watch) and claims an on-chain HTLC. No client-funded leg — the client's
1805
1796
  // claim completes the swap; a hold invoice that never settles auto-unwinds.
@@ -1816,26 +1807,20 @@ function swapToTracked(stored) {
1816
1807
  // no on-chain client leg
1817
1808
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1818
1809
  };
1819
- // Client pays the invoice, then claims the EVM HTLC.
1820
- case "lightning_to_evm":
1810
+ // Client funds the Arkade VHTLC (source_amount); the server pays the
1811
+ // Lightning invoice off-chain and claims the VHTLC — no server leg to
1812
+ // watch.
1813
+ case "arkade_to_lightning":
1821
1814
  return {
1822
1815
  swapId: r.id,
1823
- serverHtlc: evmLeg({
1824
- chainId: r.evm_chain_id,
1825
- htlc: r.evm_htlc_address,
1826
- hashLock: r.hash_lock,
1827
- claimAddress: r.client_evm_address,
1828
- // the client claims the server's leg
1829
- expectedSats: r.evm_expected_sats,
1830
- token: r.wbtc_address,
1831
- sender: r.server_evm_address,
1832
- // the server funded it
1833
- timelockSec: r.evm_refund_locktime,
1834
- createdAt: r.created_at
1835
- }),
1836
- clientRefundLocktime: 0,
1837
- // no on-chain client leg
1838
- serverRefundLocktime: ms(r.evm_refund_locktime)
1816
+ clientHtlc: arkadeLeg(
1817
+ r,
1818
+ r.arkade_vhtlc_address,
1819
+ Number(r.source_amount)
1820
+ ),
1821
+ clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1822
+ serverRefundLocktime: 0
1823
+ // no on-chain server leg
1839
1824
  };
1840
1825
  default:
1841
1826
  return void 0;
@@ -2347,12 +2332,6 @@ var Client = class {
2347
2332
  );
2348
2333
  }
2349
2334
  /** Delegated to the legacy client (migration checkpoint). */
2350
- createLightningToEvmSwapGeneric(...args) {
2351
- return this.#trackAfterCreate(
2352
- this.#legacy.createLightningToEvmSwapGeneric(...args)
2353
- );
2354
- }
2355
- /** Delegated to the legacy client (migration checkpoint). */
2356
2335
  createBitcoinToEvmSwap(...args) {
2357
2336
  return this.#trackAfterCreate(this.#legacy.createBitcoinToEvmSwap(...args));
2358
2337
  }
@@ -2375,16 +2354,6 @@ var Client = class {
2375
2354
  );
2376
2355
  }
2377
2356
  /** Delegated to the legacy client (migration checkpoint). */
2378
- getArkadeToLightningQuote(...args) {
2379
- return this.#legacy.getArkadeToLightningQuote(...args);
2380
- }
2381
- /** Delegated to the legacy client (migration checkpoint). */
2382
- retryArkadeToLightningSwap(...args) {
2383
- return this.#trackAfterRetry(
2384
- this.#legacy.retryArkadeToLightningSwap(...args)
2385
- );
2386
- }
2387
- /** Delegated to the legacy client (migration checkpoint). */
2388
2357
  createEvmToArkadeSwapGeneric(...args) {
2389
2358
  return this.#trackAfterCreate(
2390
2359
  this.#legacy.createEvmToArkadeSwapGeneric(...args)
@@ -2395,12 +2364,6 @@ var Client = class {
2395
2364
  return this.#trackAfterCreate(this.#legacy.createEvmToBitcoinSwap(...args));
2396
2365
  }
2397
2366
  /** Delegated to the legacy client (migration checkpoint). */
2398
- createEvmToLightningSwapGeneric(...args) {
2399
- return this.#trackAfterCreate(
2400
- this.#legacy.createEvmToLightningSwapGeneric(...args)
2401
- );
2402
- }
2403
- /** Delegated to the legacy client (migration checkpoint). */
2404
2367
  getCoordinatorFundingCallDataPermit2(...args) {
2405
2368
  return this.#legacy.getCoordinatorFundingCallDataPermit2(...args);
2406
2369
  }
@@ -2520,7 +2483,17 @@ var Client = class {
2520
2483
  /** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
2521
2484
  async #loadTrackedSwaps() {
2522
2485
  const swaps = await this.listAllSwaps();
2523
- return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
2486
+ return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map((s) => {
2487
+ try {
2488
+ return swapToTracked(s);
2489
+ } catch (error) {
2490
+ console.warn(
2491
+ `Client: skipping untrackable stored swap ${s.response.id}:`,
2492
+ error
2493
+ );
2494
+ return void 0;
2495
+ }
2496
+ }).filter((s) => s !== void 0);
2524
2497
  }
2525
2498
  /**
2526
2499
  * Run a create, then fold the new swap into tracking.
@@ -2640,7 +2613,14 @@ var Client = class {
2640
2613
  return this.#managers;
2641
2614
  }
2642
2615
  const managers = /* @__PURE__ */ new Map();
2643
- const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
2616
+ const {
2617
+ arkadeServerUrl,
2618
+ evmRpcUrls,
2619
+ esploraUrl,
2620
+ electrumWsUrl,
2621
+ bitcoinNetwork,
2622
+ bitcoinMinConfirmations
2623
+ } = this.#tracking;
2644
2624
  const chainTime = async () => (await this.getMtp()).mtp * 1e3;
2645
2625
  const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
2646
2626
  if (arkadeUrl) {
@@ -2652,10 +2632,13 @@ var Client = class {
2652
2632
  const readers = defaultEvmReaders(evmRpcUrls);
2653
2633
  if (readers.size > 0)
2654
2634
  managers.set("evm", EvmContractManager.fromDeps({ readers }));
2635
+ const network = bitcoinNetwork ?? "mainnet";
2655
2636
  managers.set(
2656
2637
  "bitcoin",
2657
2638
  await BitcoinContractManager.create({
2658
2639
  esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
2640
+ electrumWsUrl: electrumWsUrl ?? DEFAULT_ELECTRUM_WS_URLS[network],
2641
+ network,
2659
2642
  chainTime,
2660
2643
  minConfirmations: bitcoinMinConfirmations
2661
2644
  })
@@ -2672,6 +2655,8 @@ var ClientBuilder = class {
2672
2655
  #serverUrl;
2673
2656
  #arkadeServerUrl;
2674
2657
  #esploraUrl;
2658
+ #electrumWsUrl;
2659
+ #bitcoinNetwork;
2675
2660
  #bitcoinMinConfirmations;
2676
2661
  #evmRpcUrls;
2677
2662
  #managers;
@@ -2737,6 +2722,28 @@ var ClientBuilder = class {
2737
2722
  this.#esploraUrl = url;
2738
2723
  return this;
2739
2724
  }
2725
+ /**
2726
+ * Set an Electrum server WebSocket URL (a Fulcrum `ws`/`wss` port) for
2727
+ * Bitcoin claim/refund chain access. When set, the underlying client
2728
+ * prefers it over Esplora for lookups and broadcasts, and waits for HTLC
2729
+ * fundings via `blockchain.scripthash.subscribe` push instead of polling.
2730
+ * Esplora remains the fallback whenever the Electrum server errors.
2731
+ */
2732
+ withElectrumWsUrl(url) {
2733
+ this.#inner.withElectrumWsUrl(url);
2734
+ this.#electrumWsUrl = url;
2735
+ return this;
2736
+ }
2737
+ /**
2738
+ * Declare the Bitcoin network of this deployment's HTLC addresses. Only
2739
+ * needed together with {@link withElectrumWsUrl} on a non-mainnet
2740
+ * deployment — the Electrum reader decodes addresses to scripthashes and
2741
+ * defaults to mainnet parameters.
2742
+ */
2743
+ withBitcoinNetwork(network) {
2744
+ this.#bitcoinNetwork = network;
2745
+ return this;
2746
+ }
2740
2747
  /**
2741
2748
  * Confirmations a Bitcoin funding tx needs before it observes as `confirmed`
2742
2749
  * (gating e.g. the evm→bitcoin claim). Default `0`: claim as soon as the
@@ -2793,6 +2800,8 @@ var ClientBuilder = class {
2793
2800
  managers: this.#managers,
2794
2801
  arkadeServerUrl: this.#arkadeServerUrl,
2795
2802
  esploraUrl: this.#esploraUrl,
2803
+ electrumWsUrl: this.#electrumWsUrl,
2804
+ bitcoinNetwork: this.#bitcoinNetwork,
2796
2805
  bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
2797
2806
  evmRpcUrls: this.#evmRpcUrls,
2798
2807
  autoClaim: this.#autoClaim
@@ -2828,6 +2837,7 @@ export {
2828
2837
  defaultEvmReaders,
2829
2838
  deriveSwapActions,
2830
2839
  deriveSwapStatus,
2840
+ electrumReader,
2831
2841
  esploraReader,
2832
2842
  evmReaderFromClient,
2833
2843
  htlcFactsFromEsploraTxs,