@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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __esm = (fn, res) => function __init() {
7
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -19,6 +21,14 @@ var __copyProps = (to, from, except, desc) => {
19
21
  return to;
20
22
  };
21
23
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
22
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
33
 
24
34
  // src/contracts/bitcoin-reader-esplora.ts
@@ -108,6 +118,157 @@ var init_bitcoin_reader_esplora = __esm({
108
118
  }
109
119
  });
110
120
 
121
+ // src/contracts/bitcoin-reader-electrum.ts
122
+ var bitcoin_reader_electrum_exports = {};
123
+ __export(bitcoin_reader_electrum_exports, {
124
+ electrumReader: () => electrumReader
125
+ });
126
+ function toBtcSignerNetwork(network) {
127
+ switch (network) {
128
+ case "mainnet":
129
+ return btc.NETWORK;
130
+ case "testnet":
131
+ case "signet":
132
+ return btc.TEST_NETWORK;
133
+ case "regtest":
134
+ return REGTEST_NETWORK;
135
+ }
136
+ }
137
+ function scriptHashOf(script) {
138
+ return import_base5.hex.encode((0, import_sha22.sha256)(script).reverse());
139
+ }
140
+ function electrumReader(client, opts) {
141
+ const network = opts?.network ?? "mainnet";
142
+ const defaultMinConf = opts?.minConfirmations ?? 0;
143
+ const fallback2 = opts?.fallback;
144
+ const scriptFor = (address) => btc.OutScript.encode(
145
+ btc.Address(toBtcSignerNetwork(network)).decode(address)
146
+ );
147
+ async function getHtlcFactsElectrum(address, required) {
148
+ const script = scriptFor(address);
149
+ const scripthash = scriptHashOf(script);
150
+ const history = await client.request(
151
+ "blockchain.scripthash.get_history",
152
+ [scripthash]
153
+ );
154
+ if (!history || history.length === 0)
155
+ return { funding: "absent", fundedSats: 0 };
156
+ const txs = await Promise.all(
157
+ history.map(async (entry) => {
158
+ const raw = await client.request("blockchain.transaction.get", [
159
+ entry.tx_hash
160
+ ]);
161
+ return {
162
+ entry,
163
+ tx: btc.Transaction.fromRaw(import_base5.hex.decode(raw), {
164
+ allowUnknownOutputs: true,
165
+ allowUnknownInputs: true,
166
+ disableScriptCheck: true
167
+ })
168
+ };
169
+ })
170
+ );
171
+ const scriptHex = import_base5.hex.encode(script);
172
+ const outputIndicesPaying = (tx) => {
173
+ const indices = [];
174
+ for (let i = 0; i < tx.outputsLength; i++) {
175
+ const out = tx.getOutput(i);
176
+ if (out.script && import_base5.hex.encode(out.script) === scriptHex) indices.push(i);
177
+ }
178
+ return indices;
179
+ };
180
+ const funding = txs.find(({ tx }) => outputIndicesPaying(tx).length > 0);
181
+ if (!funding) return { funding: "absent", fundedSats: 0 };
182
+ const fundedVouts = outputIndicesPaying(funding.tx);
183
+ for (const { tx } of txs) {
184
+ for (let i = 0; i < tx.inputsLength; i++) {
185
+ const input = tx.getInput(i);
186
+ if (input.txid && import_base5.hex.encode(input.txid) === funding.entry.tx_hash && input.index !== void 0 && fundedVouts.includes(input.index)) {
187
+ return {
188
+ funding: "confirmed",
189
+ fundedSats: 0,
190
+ // already spent — the amount no longer matters
191
+ spendWitness: (input.finalScriptWitness ?? []).map(
192
+ (item) => import_base5.hex.encode(item)
193
+ )
194
+ };
195
+ }
196
+ }
197
+ }
198
+ let fundedSats = 0;
199
+ for (const i of fundedVouts)
200
+ fundedSats += Number(funding.tx.getOutput(i).amount ?? 0n);
201
+ const height = funding.entry.height;
202
+ const isConfirmed = height > 0;
203
+ let deepEnough;
204
+ if (isConfirmed) {
205
+ if (required <= 1) {
206
+ deepEnough = true;
207
+ } else {
208
+ const tip = await client.request(
209
+ "blockchain.headers.subscribe",
210
+ []
211
+ );
212
+ deepEnough = tip.height - height + 1 >= required;
213
+ }
214
+ } else {
215
+ deepEnough = required === 0;
216
+ }
217
+ return { funding: deepEnough ? "confirmed" : "mempool", fundedSats };
218
+ }
219
+ return {
220
+ async getHtlcFacts(address, minConfirmations) {
221
+ const required = minConfirmations ?? defaultMinConf;
222
+ try {
223
+ return await getHtlcFactsElectrum(address, required);
224
+ } catch (error) {
225
+ if (!fallback2) throw error;
226
+ console.warn(
227
+ "electrumReader: falling back to secondary reader:",
228
+ error instanceof Error ? error.message : error
229
+ );
230
+ return fallback2.getHtlcFacts(address, minConfirmations);
231
+ }
232
+ },
233
+ subscribe(address, onChange) {
234
+ let scripthash;
235
+ try {
236
+ scripthash = scriptHashOf(scriptFor(address));
237
+ } catch (error) {
238
+ console.warn(
239
+ "electrumReader: subscribe skipped (address decode failed):",
240
+ error instanceof Error ? error.message : error
241
+ );
242
+ return () => {
243
+ };
244
+ }
245
+ const handler = () => onChange();
246
+ client.subscribeScriptHash(scripthash, handler).catch((error) => {
247
+ console.warn(
248
+ "electrumReader: scripthash subscribe failed:",
249
+ error instanceof Error ? error.message : error
250
+ );
251
+ });
252
+ return () => client.unsubscribeScriptHash(scripthash, handler);
253
+ }
254
+ };
255
+ }
256
+ var import_sha22, import_base5, btc, REGTEST_NETWORK;
257
+ var init_bitcoin_reader_electrum = __esm({
258
+ "src/contracts/bitcoin-reader-electrum.ts"() {
259
+ "use strict";
260
+ import_sha22 = require("@noble/hashes/sha2.js");
261
+ import_base5 = require("@scure/base");
262
+ btc = __toESM(require("@scure/btc-signer"), 1);
263
+ REGTEST_NETWORK = {
264
+ bech32: "bcrt",
265
+ pubKeyHash: 111,
266
+ scriptHash: 196,
267
+ wif: 239
268
+ };
269
+ }
270
+ });
271
+
111
272
  // src/index.ts
112
273
  var src_exports = {};
113
274
  __export(src_exports, {
@@ -127,6 +288,7 @@ __export(src_exports, {
127
288
  defaultEvmReaders: () => defaultEvmReaders,
128
289
  deriveSwapActions: () => deriveSwapActions,
129
290
  deriveSwapStatus: () => deriveSwapStatus,
291
+ electrumReader: () => electrumReader,
130
292
  esploraReader: () => esploraReader,
131
293
  evmReaderFromClient: () => evmReaderFromClient,
132
294
  htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
@@ -468,10 +630,12 @@ function arkadeObservation(facts) {
468
630
  }
469
631
  function classifyArkadeSpend(encodedSpendTx, paymentHash) {
470
632
  const tx = import_sdk.Transaction.fromPSBT(import_base2.base64.decode(encodedSpendTx));
471
- for (const witness of (0, import_sdk.getArkPsbtFields)(tx, 0, import_sdk.ConditionWitness)) {
472
- for (const element of witness) {
473
- if (element.length === 32 && preimageMatches(element, paymentHash)) {
474
- return { spend: "claim", preimage: element };
633
+ for (let inputIndex = 0; inputIndex < tx.inputsLength; inputIndex++) {
634
+ for (const witness of (0, import_sdk.getArkPsbtFields)(tx, inputIndex, import_sdk.ConditionWitness)) {
635
+ for (const element of witness) {
636
+ if (element.length === 32 && preimageMatches(element, paymentHash)) {
637
+ return { spend: "claim", preimage: element };
638
+ }
475
639
  }
476
640
  }
477
641
  }
@@ -640,7 +804,7 @@ function defaultArkadeServerUrl(network) {
640
804
  }
641
805
 
642
806
  // src/contracts/bitcoin-manager.ts
643
- var import_base5 = require("@scure/base");
807
+ var import_base6 = require("@scure/base");
644
808
 
645
809
  // src/contracts/bitcoin.ts
646
810
  var import_base4 = require("@scure/base");
@@ -675,6 +839,8 @@ var BitcoinContractManager = class _BitcoinContractManager {
675
839
  #obs = /* @__PURE__ */ new Map();
676
840
  /** address → the preimage a claim revealed. */
677
841
  #preimages = /* @__PURE__ */ new Map();
842
+ /** address → push-subscription teardown (only when the reader supports push). */
843
+ #subscriptions = /* @__PURE__ */ new Map();
678
844
  #listeners = /* @__PURE__ */ new Set();
679
845
  /** Last MTP reading (ms) + when it was fetched, for extrapolation. */
680
846
  #now;
@@ -687,10 +853,24 @@ var BitcoinContractManager = class _BitcoinContractManager {
687
853
  }
688
854
  static async create(config) {
689
855
  const { esploraReader: esploraReader2 } = await Promise.resolve().then(() => (init_bitcoin_reader_esplora(), bitcoin_reader_esplora_exports));
856
+ let reader = esploraReader2(
857
+ config.esploraUrl,
858
+ void 0,
859
+ { minConfirmations: config.minConfirmations }
860
+ );
861
+ if (config.electrumWsUrl) {
862
+ const [{ electrumReader: electrumReader2 }, { ElectrumWsClient }] = await Promise.all([
863
+ Promise.resolve().then(() => (init_bitcoin_reader_electrum(), bitcoin_reader_electrum_exports)),
864
+ import("@lendasat/lendaswap-sdk-pure")
865
+ ]);
866
+ reader = electrumReader2(new ElectrumWsClient(config.electrumWsUrl), {
867
+ network: config.network,
868
+ minConfirmations: config.minConfirmations,
869
+ fallback: reader
870
+ });
871
+ }
690
872
  return _BitcoinContractManager.fromDeps({
691
- reader: esploraReader2(config.esploraUrl, void 0, {
692
- minConfirmations: config.minConfirmations
693
- }),
873
+ reader,
694
874
  chainTime: config.chainTime
695
875
  });
696
876
  }
@@ -703,10 +883,25 @@ var BitcoinContractManager = class _BitcoinContractManager {
703
883
  `BitcoinContractManager can't track a '${ref.ledger}' HTLC`
704
884
  );
705
885
  this.#refs.set(ref.address, ref);
886
+ if (this.#reader.subscribe && !this.#subscriptions.has(ref.address)) {
887
+ this.#subscriptions.set(
888
+ ref.address,
889
+ this.#reader.subscribe(ref.address, () => {
890
+ this.#reconcileRef(ref).catch((error) => {
891
+ console.warn(
892
+ "BitcoinContractManager: push reconcile failed:",
893
+ error instanceof Error ? error.message : error
894
+ );
895
+ });
896
+ })
897
+ );
898
+ }
706
899
  await this.#reconcileRef(ref);
707
900
  }
708
901
  async unregister(ref) {
709
902
  if (ref.ledger !== "bitcoin") return;
903
+ this.#subscriptions.get(ref.address)?.();
904
+ this.#subscriptions.delete(ref.address);
710
905
  this.#refs.delete(ref.address);
711
906
  this.#obs.delete(ref.address);
712
907
  this.#preimages.delete(ref.address);
@@ -741,6 +936,8 @@ var BitcoinContractManager = class _BitcoinContractManager {
741
936
  this.#now = { mtpMs: await this.#chainTime(), fetchedAtMs: Date.now() };
742
937
  }
743
938
  dispose() {
939
+ for (const unsubscribe of this.#subscriptions.values()) unsubscribe();
940
+ this.#subscriptions.clear();
744
941
  this.#listeners.clear();
745
942
  }
746
943
  /** The preimage a claim revealed on this HTLC, if one was seen. */
@@ -754,7 +951,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
754
951
  );
755
952
  const { observation, preimage } = bitcoinObservation(
756
953
  facts,
757
- import_base5.hex.decode(ref.preimageHash),
954
+ import_base6.hex.decode(ref.preimageHash),
758
955
  ref.expectedSats
759
956
  );
760
957
  if (preimage) this.#preimages.set(ref.address, preimage);
@@ -1639,19 +1836,19 @@ function defaultWebSocketFactory(url) {
1639
1836
  var import_sdk3 = require("@arkade-os/sdk");
1640
1837
  var import_lendaswap_sdk_pure = require("@lendasat/lendaswap-sdk-pure");
1641
1838
  var import_legacy2 = require("@noble/hashes/legacy.js");
1642
- var import_base6 = require("@scure/base");
1839
+ var import_base7 = require("@scure/base");
1643
1840
  function strip0x(value) {
1644
1841
  return value.startsWith("0x") ? value.slice(2) : value;
1645
1842
  }
1646
1843
  function xOnly(pubKeyHex) {
1647
- const bytes = import_base6.hex.decode(strip0x(pubKeyHex));
1844
+ const bytes = import_base7.hex.decode(strip0x(pubKeyHex));
1648
1845
  if (bytes.length === 33) return bytes.slice(1);
1649
1846
  if (bytes.length === 32) return bytes;
1650
1847
  throw new Error(`invalid public key length: ${bytes.length}`);
1651
1848
  }
1652
1849
  var seconds = (value) => ({ type: "seconds", value: BigInt(value) });
1653
1850
  function buildArkadeVhtlcRef(input) {
1654
- const hashLockBytes = import_base6.hex.decode(strip0x(input.hashLock));
1851
+ const hashLockBytes = import_base7.hex.decode(strip0x(input.hashLock));
1655
1852
  const params = {
1656
1853
  sender: xOnly(input.senderPk),
1657
1854
  receiver: xOnly(input.receiverPk),
@@ -1664,21 +1861,15 @@ function buildArkadeVhtlcRef(input) {
1664
1861
  input.unilateralRefundWithoutReceiverDelay
1665
1862
  )
1666
1863
  };
1667
- const vhtlc = (() => {
1668
- switch (input.arkadeHtlcScriptVersion ?? import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_LEGACY) {
1669
- case import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_LEGACY:
1670
- return new import_sdk3.VHTLC.Script(params);
1671
- case import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_STRICT:
1672
- return new import_lendaswap_sdk_pure.StrictVhtlcScript(params);
1673
- default:
1674
- throw new Error(
1675
- `Unsupported Arkade HTLC script version: ${input.arkadeHtlcScriptVersion}`
1676
- );
1677
- }
1678
- })();
1864
+ if (input.arkadeHtlcScriptVersion !== void 0 && input.arkadeHtlcScriptVersion !== import_lendaswap_sdk_pure.ARKADE_HTLC_SCRIPT_VERSION_STRICT) {
1865
+ throw new Error(
1866
+ `Unsupported Arkade HTLC script version: ${input.arkadeHtlcScriptVersion}`
1867
+ );
1868
+ }
1869
+ const vhtlc = new import_lendaswap_sdk_pure.StrictVhtlcScript(params);
1679
1870
  return {
1680
1871
  ledger: "arkade",
1681
- script: import_base6.hex.encode(vhtlc.pkScript),
1872
+ script: import_base7.hex.encode(vhtlc.pkScript),
1682
1873
  address: input.address,
1683
1874
  preimageHash: strip0x(input.hashLock),
1684
1875
  expectedSats: input.expectedSats,
@@ -1872,47 +2063,6 @@ function swapToTracked(stored) {
1872
2063
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1873
2064
  };
1874
2065
  // ─── Lightning: one on-chain leg, one off-chain LN payment ───────────────
1875
- // Receive-on-Lightning: the client funds an on-chain HTLC and is paid over
1876
- // Lightning. No server leg to watch — the server sweeping the client's leg
1877
- // (with the preimage it got by paying the invoice) is the done signal.
1878
- // Client funds the Arkade VHTLC; Boltz pays the invoice. The deposit is
1879
- // `boltz_amount_sats` (source minus fees) — the amount actually locked, not
1880
- // `source_amount` — else the client's own funding reads as under-funded.
1881
- case "arkade_to_lightning":
1882
- return {
1883
- swapId: r.id,
1884
- clientHtlc: arkadeLeg(
1885
- r,
1886
- r.arkade_vhtlc_address,
1887
- Number(r.boltz_amount_sats)
1888
- ),
1889
- clientRefundLocktime: ms(r.vhtlc_refund_locktime),
1890
- serverRefundLocktime: 0
1891
- // no on-chain server leg
1892
- };
1893
- // Client funds the EVM HTLC (server claims it after paying the invoice).
1894
- case "evm_to_lightning":
1895
- return {
1896
- swapId: r.id,
1897
- clientHtlc: evmLeg({
1898
- chainId: r.evm_chain_id,
1899
- htlc: r.evm_htlc_address,
1900
- hashLock: r.hash_lock,
1901
- claimAddress: r.server_evm_address,
1902
- // the server claims the client's leg
1903
- expectedSats: r.evm_expected_sats,
1904
- // evm_to_lightning doesn't expose the locked token — fall back to the
1905
- // per-chain mainnet constant so the isActive tuple is complete.
1906
- token: lockedTokenFallback(r.evm_chain_id, r.network),
1907
- sender: r.client_evm_address,
1908
- // the client funded it
1909
- timelockSec: r.evm_refund_locktime,
1910
- createdAt: r.created_at
1911
- }),
1912
- clientRefundLocktime: ms(r.evm_refund_locktime),
1913
- serverRefundLocktime: 0
1914
- // no on-chain server leg
1915
- };
1916
2066
  // Pay-on-Lightning: the client pays a Lightning invoice (off-chain, nothing to
1917
2067
  // watch) and claims an on-chain HTLC. No client-funded leg — the client's
1918
2068
  // claim completes the swap; a hold invoice that never settles auto-unwinds.
@@ -1929,27 +2079,6 @@ function swapToTracked(stored) {
1929
2079
  // no on-chain client leg
1930
2080
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
1931
2081
  };
1932
- // Client pays the invoice, then claims the EVM HTLC.
1933
- case "lightning_to_evm":
1934
- return {
1935
- swapId: r.id,
1936
- serverHtlc: evmLeg({
1937
- chainId: r.evm_chain_id,
1938
- htlc: r.evm_htlc_address,
1939
- hashLock: r.hash_lock,
1940
- claimAddress: r.client_evm_address,
1941
- // the client claims the server's leg
1942
- expectedSats: r.evm_expected_sats,
1943
- token: r.wbtc_address,
1944
- sender: r.server_evm_address,
1945
- // the server funded it
1946
- timelockSec: r.evm_refund_locktime,
1947
- createdAt: r.created_at
1948
- }),
1949
- clientRefundLocktime: 0,
1950
- // no on-chain client leg
1951
- serverRefundLocktime: ms(r.evm_refund_locktime)
1952
- };
1953
2082
  default:
1954
2083
  return void 0;
1955
2084
  }
@@ -2460,12 +2589,6 @@ var Client = class {
2460
2589
  );
2461
2590
  }
2462
2591
  /** Delegated to the legacy client (migration checkpoint). */
2463
- createLightningToEvmSwapGeneric(...args) {
2464
- return this.#trackAfterCreate(
2465
- this.#legacy.createLightningToEvmSwapGeneric(...args)
2466
- );
2467
- }
2468
- /** Delegated to the legacy client (migration checkpoint). */
2469
2592
  createBitcoinToEvmSwap(...args) {
2470
2593
  return this.#trackAfterCreate(this.#legacy.createBitcoinToEvmSwap(...args));
2471
2594
  }
@@ -2482,22 +2605,6 @@ var Client = class {
2482
2605
  );
2483
2606
  }
2484
2607
  /** Delegated to the legacy client (migration checkpoint). */
2485
- createArkadeToLightningSwap(...args) {
2486
- return this.#trackAfterCreate(
2487
- this.#legacy.createArkadeToLightningSwap(...args)
2488
- );
2489
- }
2490
- /** Delegated to the legacy client (migration checkpoint). */
2491
- getArkadeToLightningQuote(...args) {
2492
- return this.#legacy.getArkadeToLightningQuote(...args);
2493
- }
2494
- /** Delegated to the legacy client (migration checkpoint). */
2495
- retryArkadeToLightningSwap(...args) {
2496
- return this.#trackAfterRetry(
2497
- this.#legacy.retryArkadeToLightningSwap(...args)
2498
- );
2499
- }
2500
- /** Delegated to the legacy client (migration checkpoint). */
2501
2608
  createEvmToArkadeSwapGeneric(...args) {
2502
2609
  return this.#trackAfterCreate(
2503
2610
  this.#legacy.createEvmToArkadeSwapGeneric(...args)
@@ -2508,12 +2615,6 @@ var Client = class {
2508
2615
  return this.#trackAfterCreate(this.#legacy.createEvmToBitcoinSwap(...args));
2509
2616
  }
2510
2617
  /** Delegated to the legacy client (migration checkpoint). */
2511
- createEvmToLightningSwapGeneric(...args) {
2512
- return this.#trackAfterCreate(
2513
- this.#legacy.createEvmToLightningSwapGeneric(...args)
2514
- );
2515
- }
2516
- /** Delegated to the legacy client (migration checkpoint). */
2517
2618
  getCoordinatorFundingCallDataPermit2(...args) {
2518
2619
  return this.#legacy.getCoordinatorFundingCallDataPermit2(...args);
2519
2620
  }
@@ -2633,7 +2734,17 @@ var Client = class {
2633
2734
  /** Every stored swap whose ledgers are observable, mapped to a {@link TrackedSwap}. */
2634
2735
  async #loadTrackedSwaps() {
2635
2736
  const swaps = await this.listAllSwaps();
2636
- return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map(swapToTracked).filter((s) => s !== void 0);
2737
+ return swaps.filter((s) => !SETTLED_STORED_STATUSES.has(s.response.status)).map((s) => {
2738
+ try {
2739
+ return swapToTracked(s);
2740
+ } catch (error) {
2741
+ console.warn(
2742
+ `Client: skipping untrackable stored swap ${s.response.id}:`,
2743
+ error
2744
+ );
2745
+ return void 0;
2746
+ }
2747
+ }).filter((s) => s !== void 0);
2637
2748
  }
2638
2749
  /**
2639
2750
  * Run a create, then fold the new swap into tracking.
@@ -2753,7 +2864,14 @@ var Client = class {
2753
2864
  return this.#managers;
2754
2865
  }
2755
2866
  const managers = /* @__PURE__ */ new Map();
2756
- const { arkadeServerUrl, evmRpcUrls, esploraUrl, bitcoinMinConfirmations } = this.#tracking;
2867
+ const {
2868
+ arkadeServerUrl,
2869
+ evmRpcUrls,
2870
+ esploraUrl,
2871
+ electrumWsUrl,
2872
+ bitcoinNetwork,
2873
+ bitcoinMinConfirmations
2874
+ } = this.#tracking;
2757
2875
  const chainTime = async () => (await this.getMtp()).mtp * 1e3;
2758
2876
  const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
2759
2877
  if (arkadeUrl) {
@@ -2765,10 +2883,13 @@ var Client = class {
2765
2883
  const readers = defaultEvmReaders(evmRpcUrls);
2766
2884
  if (readers.size > 0)
2767
2885
  managers.set("evm", EvmContractManager.fromDeps({ readers }));
2886
+ const network = bitcoinNetwork ?? "mainnet";
2768
2887
  managers.set(
2769
2888
  "bitcoin",
2770
2889
  await BitcoinContractManager.create({
2771
2890
  esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
2891
+ electrumWsUrl: electrumWsUrl ?? import_lendaswap_sdk_pure2.DEFAULT_ELECTRUM_WS_URLS[network],
2892
+ network,
2772
2893
  chainTime,
2773
2894
  minConfirmations: bitcoinMinConfirmations
2774
2895
  })
@@ -2785,6 +2906,8 @@ var ClientBuilder = class {
2785
2906
  #serverUrl;
2786
2907
  #arkadeServerUrl;
2787
2908
  #esploraUrl;
2909
+ #electrumWsUrl;
2910
+ #bitcoinNetwork;
2788
2911
  #bitcoinMinConfirmations;
2789
2912
  #evmRpcUrls;
2790
2913
  #managers;
@@ -2850,6 +2973,28 @@ var ClientBuilder = class {
2850
2973
  this.#esploraUrl = url;
2851
2974
  return this;
2852
2975
  }
2976
+ /**
2977
+ * Set an Electrum server WebSocket URL (a Fulcrum `ws`/`wss` port) for
2978
+ * Bitcoin claim/refund chain access. When set, the underlying client
2979
+ * prefers it over Esplora for lookups and broadcasts, and waits for HTLC
2980
+ * fundings via `blockchain.scripthash.subscribe` push instead of polling.
2981
+ * Esplora remains the fallback whenever the Electrum server errors.
2982
+ */
2983
+ withElectrumWsUrl(url) {
2984
+ this.#inner.withElectrumWsUrl(url);
2985
+ this.#electrumWsUrl = url;
2986
+ return this;
2987
+ }
2988
+ /**
2989
+ * Declare the Bitcoin network of this deployment's HTLC addresses. Only
2990
+ * needed together with {@link withElectrumWsUrl} on a non-mainnet
2991
+ * deployment — the Electrum reader decodes addresses to scripthashes and
2992
+ * defaults to mainnet parameters.
2993
+ */
2994
+ withBitcoinNetwork(network) {
2995
+ this.#bitcoinNetwork = network;
2996
+ return this;
2997
+ }
2853
2998
  /**
2854
2999
  * Confirmations a Bitcoin funding tx needs before it observes as `confirmed`
2855
3000
  * (gating e.g. the evm→bitcoin claim). Default `0`: claim as soon as the
@@ -2906,6 +3051,8 @@ var ClientBuilder = class {
2906
3051
  managers: this.#managers,
2907
3052
  arkadeServerUrl: this.#arkadeServerUrl,
2908
3053
  esploraUrl: this.#esploraUrl,
3054
+ electrumWsUrl: this.#electrumWsUrl,
3055
+ bitcoinNetwork: this.#bitcoinNetwork,
2909
3056
  bitcoinMinConfirmations: this.#bitcoinMinConfirmations,
2910
3057
  evmRpcUrls: this.#evmRpcUrls,
2911
3058
  autoClaim: this.#autoClaim
@@ -2926,6 +3073,7 @@ function createdSwapId(result) {
2926
3073
  }
2927
3074
 
2928
3075
  // src/index.ts
3076
+ init_bitcoin_reader_electrum();
2929
3077
  init_bitcoin_reader_esplora();
2930
3078
  // Annotate the CommonJS export names for ESM import in node:
2931
3079
  0 && (module.exports = {
@@ -2945,6 +3093,7 @@ init_bitcoin_reader_esplora();
2945
3093
  defaultEvmReaders,
2946
3094
  deriveSwapActions,
2947
3095
  deriveSwapStatus,
3096
+ electrumReader,
2948
3097
  esploraReader,
2949
3098
  evmReaderFromClient,
2950
3099
  htlcFactsFromEsploraTxs,