@satora/swap 0.3.1 → 1.0.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
@@ -3,6 +3,9 @@ import {
3
3
  esploraReader,
4
4
  htlcFactsFromEsploraTxs
5
5
  } from "./chunk-EIWELJJK.js";
6
+ import {
7
+ electrumReader
8
+ } from "./chunk-VHXEU534.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;
@@ -563,10 +571,24 @@ var BitcoinContractManager = class _BitcoinContractManager {
563
571
  }
564
572
  static async create(config) {
565
573
  const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-HV6CANAA.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-4DOJ7NGH.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,27 +1807,6 @@ 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":
1821
- return {
1822
- 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)
1839
- };
1840
1810
  default:
1841
1811
  return void 0;
1842
1812
  }
@@ -2347,12 +2317,6 @@ var Client = class {
2347
2317
  );
2348
2318
  }
2349
2319
  /** 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
2320
  createBitcoinToEvmSwap(...args) {
2357
2321
  return this.#trackAfterCreate(this.#legacy.createBitcoinToEvmSwap(...args));
2358
2322
  }
@@ -2369,22 +2333,6 @@ var Client = class {
2369
2333
  );
2370
2334
  }
2371
2335
  /** Delegated to the legacy client (migration checkpoint). */
2372
- createArkadeToLightningSwap(...args) {
2373
- return this.#trackAfterCreate(
2374
- this.#legacy.createArkadeToLightningSwap(...args)
2375
- );
2376
- }
2377
- /** 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
2336
  createEvmToArkadeSwapGeneric(...args) {
2389
2337
  return this.#trackAfterCreate(
2390
2338
  this.#legacy.createEvmToArkadeSwapGeneric(...args)
@@ -2395,12 +2343,6 @@ var Client = class {
2395
2343
  return this.#trackAfterCreate(this.#legacy.createEvmToBitcoinSwap(...args));
2396
2344
  }
2397
2345
  /** 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
2346
  getCoordinatorFundingCallDataPermit2(...args) {
2405
2347
  return this.#legacy.getCoordinatorFundingCallDataPermit2(...args);
2406
2348
  }
@@ -2520,7 +2462,17 @@ var Client = class {
2520
2462
  /** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
2521
2463
  async #loadTrackedSwaps() {
2522
2464
  const swaps = await this.listAllSwaps();
2523
- return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
2465
+ return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map((s) => {
2466
+ try {
2467
+ return swapToTracked(s);
2468
+ } catch (error) {
2469
+ console.warn(
2470
+ `Client: skipping untrackable stored swap ${s.response.id}:`,
2471
+ error
2472
+ );
2473
+ return void 0;
2474
+ }
2475
+ }).filter((s) => s !== void 0);
2524
2476
  }
2525
2477
  /**
2526
2478
  * Run a create, then fold the new swap into tracking.
@@ -2640,7 +2592,14 @@ var Client = class {
2640
2592
  return this.#managers;
2641
2593
  }
2642
2594
  const managers = /* @__PURE__ */ new Map();
2643
- const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
2595
+ const {
2596
+ arkadeServerUrl,
2597
+ evmRpcUrls,
2598
+ esploraUrl,
2599
+ electrumWsUrl,
2600
+ bitcoinNetwork,
2601
+ bitcoinMinConfirmations
2602
+ } = this.#tracking;
2644
2603
  const chainTime = async () => (await this.getMtp()).mtp * 1e3;
2645
2604
  const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
2646
2605
  if (arkadeUrl) {
@@ -2652,10 +2611,13 @@ var Client = class {
2652
2611
  const readers = defaultEvmReaders(evmRpcUrls);
2653
2612
  if (readers.size > 0)
2654
2613
  managers.set("evm", EvmContractManager.fromDeps({ readers }));
2614
+ const network = bitcoinNetwork ?? "mainnet";
2655
2615
  managers.set(
2656
2616
  "bitcoin",
2657
2617
  await BitcoinContractManager.create({
2658
2618
  esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
2619
+ electrumWsUrl: electrumWsUrl ?? DEFAULT_ELECTRUM_WS_URLS[network],
2620
+ network,
2659
2621
  chainTime,
2660
2622
  minConfirmations: bitcoinMinConfirmations
2661
2623
  })
@@ -2672,6 +2634,8 @@ var ClientBuilder = class {
2672
2634
  #serverUrl;
2673
2635
  #arkadeServerUrl;
2674
2636
  #esploraUrl;
2637
+ #electrumWsUrl;
2638
+ #bitcoinNetwork;
2675
2639
  #bitcoinMinConfirmations;
2676
2640
  #evmRpcUrls;
2677
2641
  #managers;
@@ -2737,6 +2701,28 @@ var ClientBuilder = class {
2737
2701
  this.#esploraUrl = url;
2738
2702
  return this;
2739
2703
  }
2704
+ /**
2705
+ * Set an Electrum server WebSocket URL (a Fulcrum `ws`/`wss` port) for
2706
+ * Bitcoin claim/refund chain access. When set, the underlying client
2707
+ * prefers it over Esplora for lookups and broadcasts, and waits for HTLC
2708
+ * fundings via `blockchain.scripthash.subscribe` push instead of polling.
2709
+ * Esplora remains the fallback whenever the Electrum server errors.
2710
+ */
2711
+ withElectrumWsUrl(url) {
2712
+ this.#inner.withElectrumWsUrl(url);
2713
+ this.#electrumWsUrl = url;
2714
+ return this;
2715
+ }
2716
+ /**
2717
+ * Declare the Bitcoin network of this deployment's HTLC addresses. Only
2718
+ * needed together with {@link withElectrumWsUrl} on a non-mainnet
2719
+ * deployment — the Electrum reader decodes addresses to scripthashes and
2720
+ * defaults to mainnet parameters.
2721
+ */
2722
+ withBitcoinNetwork(network) {
2723
+ this.#bitcoinNetwork = network;
2724
+ return this;
2725
+ }
2740
2726
  /**
2741
2727
  * Confirmations a Bitcoin funding tx needs before it observes as `confirmed`
2742
2728
  * (gating e.g. the evm→bitcoin claim). Default `0`: claim as soon as the
@@ -2793,6 +2779,8 @@ var ClientBuilder = class {
2793
2779
  managers: this.#managers,
2794
2780
  arkadeServerUrl: this.#arkadeServerUrl,
2795
2781
  esploraUrl: this.#esploraUrl,
2782
+ electrumWsUrl: this.#electrumWsUrl,
2783
+ bitcoinNetwork: this.#bitcoinNetwork,
2796
2784
  bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
2797
2785
  evmRpcUrls: this.#evmRpcUrls,
2798
2786
  autoClaim: this.#autoClaim
@@ -2828,6 +2816,7 @@ export {
2828
2816
  defaultEvmReaders,
2829
2817
  deriveSwapActions,
2830
2818
  deriveSwapStatus,
2819
+ electrumReader,
2831
2820
  esploraReader,
2832
2821
  evmReaderFromClient,
2833
2822
  htlcFactsFromEsploraTxs,