@satora/swap 1.4.0 → 1.6.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
@@ -36,8 +36,12 @@ var bitcoin_reader_esplora_exports = {};
36
36
  __export(bitcoin_reader_esplora_exports, {
37
37
  DEFAULT_ESPLORA_URLS: () => DEFAULT_ESPLORA_URLS,
38
38
  esploraReader: () => esploraReader,
39
- htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs
39
+ htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
40
+ resolveMinConfirmations: () => resolveMinConfirmations
40
41
  });
42
+ function resolveMinConfirmations(source) {
43
+ return (typeof source === "function" ? source() : source) ?? 0;
44
+ }
41
45
  function htlcFactsFromEsploraTxs(txs, address, opts) {
42
46
  for (const tx of txs) {
43
47
  const spend = tx.vin.find(
@@ -80,11 +84,10 @@ function esploraReader(esploraUrls, fetchImpl = fetch, policy) {
80
84
  const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(
81
85
  (url) => url.replace(/\/+$/, "")
82
86
  );
83
- const minConf = policy?.minConfirmations ?? 0;
84
87
  let start = 0;
85
88
  return {
86
89
  async getHtlcFacts(address, minConfirmations) {
87
- const required = minConfirmations ?? minConf;
90
+ const required = minConfirmations ?? resolveMinConfirmations(policy?.minConfirmations);
88
91
  const from = start++ % bases.length;
89
92
  let lastError;
90
93
  for (let i = 0; i < bases.length; i++) {
@@ -145,11 +148,12 @@ function scriptHashOf(script) {
145
148
  }
146
149
  function electrumReader(client, opts) {
147
150
  const network = opts?.network ?? "mainnet";
148
- const defaultMinConf = opts?.minConfirmations ?? 0;
149
151
  const fallback2 = opts?.fallback;
150
- const scriptFor = (address) => btc.OutScript.encode(
151
- btc.Address(toBtcSignerNetwork(network)).decode(address)
152
- );
152
+ const scriptFor = (address) => {
153
+ const decoded = btc.Address(toBtcSignerNetwork(network)).decode(address);
154
+ if (!decoded) throw new Error(`Unrecognised Bitcoin address: ${address}`);
155
+ return btc.OutScript.encode(decoded);
156
+ };
153
157
  async function getHtlcFactsElectrum(address, required) {
154
158
  const script = scriptFor(address);
155
159
  const scripthash = scriptHashOf(script);
@@ -231,7 +235,7 @@ function electrumReader(client, opts) {
231
235
  }
232
236
  return {
233
237
  async getHtlcFacts(address, minConfirmations) {
234
- const required = minConfirmations ?? defaultMinConf;
238
+ const required = minConfirmations ?? resolveMinConfirmations(opts?.minConfirmations);
235
239
  try {
236
240
  return await getHtlcFactsElectrum(address, required);
237
241
  } catch (error) {
@@ -273,6 +277,7 @@ var init_bitcoin_reader_electrum = __esm({
273
277
  import_sha22 = require("@noble/hashes/sha2.js");
274
278
  import_base5 = require("@scure/base");
275
279
  btc = __toESM(require("@scure/btc-signer"), 1);
280
+ init_bitcoin_reader_esplora();
276
281
  REGTEST_NETWORK = {
277
282
  bech32: "bcrt",
278
283
  pubKeyHash: 111,
@@ -308,6 +313,7 @@ __export(src_exports, {
308
313
  htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
309
314
  htlcKey: () => htlcKey,
310
315
  htlcQueryKey: () => htlcQueryKey,
316
+ resolveMinConfirmations: () => resolveMinConfirmations,
311
317
  swapToTracked: () => swapToTracked
312
318
  });
313
319
  module.exports = __toCommonJS(src_exports);
@@ -667,7 +673,7 @@ function payOnLightningStatus(serverHtlc) {
667
673
  }
668
674
 
669
675
  // src/client.ts
670
- var import_lendaswap_sdk_pure2 = require("@lendasat/lendaswap-sdk-pure");
676
+ var import_lendaswap_sdk_pure3 = require("@lendasat/lendaswap-sdk-pure");
671
677
 
672
678
  // src/contracts/arkade-manager.ts
673
679
  var import_sdk2 = require("@arkade-os/sdk");
@@ -1112,7 +1118,10 @@ function refQueryKey(ref) {
1112
1118
  var AVG_BLOCK_MS = {
1113
1119
  1: 12e3,
1114
1120
  137: 2100,
1115
- 42161: 250
1121
+ 42161: 250,
1122
+ 30: 3e4,
1123
+ 31: 3e4
1124
+ // Rootstock testnet: same merge-mined cadence
1116
1125
  };
1117
1126
  var SCAN_MARGIN_BLOCKS = 1000n;
1118
1127
  var EvmContractManager = class _EvmContractManager {
@@ -1293,7 +1302,10 @@ var EvmContractManager = class _EvmContractManager {
1293
1302
  for (const ref of refs) {
1294
1303
  const { observation, preimage } = evmObservation(
1295
1304
  events.get(refQueryKey(ref)) ?? [],
1296
- { amount: ref.expectedAmount, token: ref.expectedToken }
1305
+ {
1306
+ amount: ref.minAmount ?? ref.expectedAmount,
1307
+ token: ref.expectedToken
1308
+ }
1297
1309
  );
1298
1310
  const key = htlcKey(ref);
1299
1311
  if (preimage) this.#preimages.set(key, preimage);
@@ -1368,7 +1380,10 @@ var DEFAULT_EVM_RPCS = {
1368
1380
  "https://arb1.arbitrum.io/rpc",
1369
1381
  "https://arbitrum.drpc.org",
1370
1382
  "https://arbitrum-one-rpc.publicnode.com"
1371
- ]
1383
+ ],
1384
+ // Rootstock: the public node, HTTP only (no WebSocket endpoint exists).
1385
+ 30: ["https://public-node.rsk.co"],
1386
+ 31: ["https://public-node.testnet.rsk.co"]
1372
1387
  };
1373
1388
  var SWAP_CREATED = (0, import_viem.parseAbiItem)(
1374
1389
  "event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock, bytes32 key)"
@@ -1401,25 +1416,52 @@ function activeKey(q) {
1401
1416
  }
1402
1417
  });
1403
1418
  }
1404
- function evmReaderFromClient(client) {
1419
+ function evmReaderFromClient(client, options = {}) {
1420
+ const { maxBlockRange } = options;
1421
+ async function getLogs(filter, fromBlock) {
1422
+ if (maxBlockRange === void 0) {
1423
+ return client.request({
1424
+ method: "eth_getLogs",
1425
+ params: [{ ...filter, fromBlock: (0, import_viem.numberToHex)(fromBlock) }]
1426
+ });
1427
+ }
1428
+ const latest = (await client.getBlock()).number;
1429
+ if (latest === null) {
1430
+ throw new Error("latest block has no number");
1431
+ }
1432
+ const logs = [];
1433
+ for (let from = fromBlock; from <= latest; from += maxBlockRange) {
1434
+ const to = from + maxBlockRange - 1n < latest ? from + maxBlockRange - 1n : latest;
1435
+ logs.push(
1436
+ ...await client.request({
1437
+ method: "eth_getLogs",
1438
+ params: [
1439
+ {
1440
+ ...filter,
1441
+ fromBlock: (0, import_viem.numberToHex)(from),
1442
+ toBlock: (0, import_viem.numberToHex)(to)
1443
+ }
1444
+ ]
1445
+ })
1446
+ );
1447
+ }
1448
+ return logs;
1449
+ }
1405
1450
  return {
1406
1451
  async getHtlcEventsBatch(queries, fromBlock = 0n) {
1407
1452
  const results = /* @__PURE__ */ new Map();
1408
1453
  if (queries.length === 0) return results;
1409
1454
  for (const q of queries) results.set(htlcQueryKey(q), []);
1410
- const logs = await client.request({
1411
- method: "eth_getLogs",
1412
- params: [
1413
- {
1414
- address: unique(queries.map((q) => q.htlc)),
1415
- topics: [
1416
- HTLC_EVENT_TOPICS,
1417
- unique(queries.map((q) => q.preimageHash))
1418
- ],
1419
- fromBlock: (0, import_viem.numberToHex)(fromBlock)
1420
- }
1421
- ]
1422
- });
1455
+ const logs = await getLogs(
1456
+ {
1457
+ address: unique(queries.map((q) => q.htlc)),
1458
+ topics: [
1459
+ HTLC_EVENT_TOPICS,
1460
+ unique(queries.map((q) => q.preimageHash))
1461
+ ]
1462
+ },
1463
+ fromBlock
1464
+ );
1423
1465
  const grouped = /* @__PURE__ */ new Map();
1424
1466
  for (const log of logs) {
1425
1467
  const decoded = tryDecode(log);
@@ -1535,12 +1577,16 @@ function toHtlcEvent(decoded, query) {
1535
1577
  function unique(values) {
1536
1578
  return [...new Set(values)];
1537
1579
  }
1538
- function createEvmRpcReader(rpcUrls) {
1580
+ function createEvmRpcReader(rpcUrls, options = {}) {
1539
1581
  const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
1540
1582
  const transport = urls.length > 1 ? (0, import_viem.fallback)(urls.map((url) => (0, import_viem.http)(url))) : (0, import_viem.http)(urls[0]);
1541
1583
  const client = (0, import_viem.createPublicClient)({ transport });
1542
- return evmReaderFromClient(client);
1584
+ return evmReaderFromClient(client, options);
1543
1585
  }
1586
+ var EVM_LOG_RANGE_LIMITS = {
1587
+ 30: 2000n,
1588
+ 31: 2000n
1589
+ };
1544
1590
  function defaultEvmReaders(overrides) {
1545
1591
  const chainIds = /* @__PURE__ */ new Set([
1546
1592
  ...Object.keys(DEFAULT_EVM_RPCS).map(Number),
@@ -1551,7 +1597,13 @@ function defaultEvmReaders(overrides) {
1551
1597
  const override = overrides?.[chainId];
1552
1598
  const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
1553
1599
  const urls = override ? [override, ...defaults] : defaults;
1554
- if (urls.length > 0) readers.set(chainId, createEvmRpcReader(urls));
1600
+ if (urls.length > 0)
1601
+ readers.set(
1602
+ chainId,
1603
+ createEvmRpcReader(urls, {
1604
+ maxBlockRange: EVM_LOG_RANGE_LIMITS[chainId]
1605
+ })
1606
+ );
1555
1607
  }
1556
1608
  return readers;
1557
1609
  }
@@ -2006,6 +2058,9 @@ function defaultWebSocketFactory(url) {
2006
2058
  return new WS(url);
2007
2059
  }
2008
2060
 
2061
+ // src/tracker/from-swap.ts
2062
+ var import_lendaswap_sdk_pure2 = require("@lendasat/lendaswap-sdk-pure");
2063
+
2009
2064
  // src/contracts/arkade-vhtlc.ts
2010
2065
  var import_sdk3 = require("@arkade-os/sdk");
2011
2066
  var import_lendaswap_sdk_pure = require("@lendasat/lendaswap-sdk-pure");
@@ -2091,6 +2146,7 @@ function evmLeg(args) {
2091
2146
  preimageHash: ensure0x(args.hashLock),
2092
2147
  claimAddress: ensure0x(args.claimAddress),
2093
2148
  expectedAmount: BigInt(args.expectedSats),
2149
+ minAmount: args.minAmount,
2094
2150
  expectedToken: args.token ? ensure0x(args.token) : void 0,
2095
2151
  sender: args.sender ? ensure0x(args.sender) : void 0,
2096
2152
  timelockSec: args.timelockSec,
@@ -2098,6 +2154,13 @@ function evmLeg(args) {
2098
2154
  };
2099
2155
  }
2100
2156
  var CLIENT_FUNDING_MIN_CONFIRMATIONS = 1;
2157
+ var FEE_ROUNDING_SLACK_SATS = 2;
2158
+ function minServerPayout(r) {
2159
+ return Math.max(
2160
+ 0,
2161
+ Number(r.target_amount) - r.fee_sats - FEE_ROUNDING_SLACK_SATS
2162
+ );
2163
+ }
2101
2164
  function bitcoinLeg(address, hashLock, expectedSats, minConfirmations) {
2102
2165
  return {
2103
2166
  ledger: "bitcoin",
@@ -2142,15 +2205,19 @@ function swapToTracked(stored) {
2142
2205
  claimAddress: r.server_evm_address,
2143
2206
  // the server claims the client's EVM HTLC
2144
2207
  expectedSats: r.evm_expected_sats,
2145
- // evm_to_arkade doesn't expose the locked token — fall back to the
2146
- // per-chain mainnet constant so the isActive tuple is complete.
2147
- token: lockedTokenFallback(r.evm_chain_id, r.network),
2148
- sender: r.client_evm_address,
2149
- // the client funded it
2208
+ minAmount: 0n,
2209
+ // see SHORT_LOCK_RULE
2210
+ // A native lock's asset word is zero; an ERC20 lock's token is not
2211
+ // on this response yet, so fall back to the per-chain mainnet
2212
+ // constant to complete the isActive tuple.
2213
+ token: r.evm_htlc_kind === "native" ? import_lendaswap_sdk_pure2.NATIVE_TOKEN_ADDRESS : lockedTokenFallback(r.evm_chain_id, r.network),
2214
+ // The coordinator created the HTLC on the client's behalf, so it
2215
+ // is the lock's sender (refund address), as on evm_to_lightning.
2216
+ sender: r.evm_coordinator_address,
2150
2217
  timelockSec: r.evm_refund_locktime,
2151
2218
  createdAt: r.created_at
2152
2219
  }),
2153
- serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
2220
+ serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, minServerPayout(r)),
2154
2221
  clientRefundLocktime: ms(r.evm_refund_locktime),
2155
2222
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
2156
2223
  };
@@ -2191,16 +2258,19 @@ function swapToTracked(stored) {
2191
2258
  claimAddress: r.server_evm_address,
2192
2259
  // the server claims the client's EVM HTLC
2193
2260
  expectedSats: r.evm_expected_sats,
2261
+ minAmount: 0n,
2262
+ // see SHORT_LOCK_RULE
2194
2263
  token: r.wbtc_address,
2195
- sender: r.client_evm_address,
2196
- // the client funded it
2264
+ // The coordinator created the HTLC on the client's behalf, so it
2265
+ // is the lock's sender (refund address).
2266
+ sender: r.evm_coordinator_address,
2197
2267
  timelockSec: r.evm_refund_locktime,
2198
2268
  createdAt: r.created_at
2199
2269
  }),
2200
2270
  serverHtlc: bitcoinLeg(
2201
2271
  r.btc_htlc_address,
2202
2272
  r.evm_hash_lock,
2203
- Number(r.target_amount)
2273
+ minServerPayout(r)
2204
2274
  ),
2205
2275
  clientRefundLocktime: ms(r.evm_refund_locktime),
2206
2276
  serverRefundLocktime: ms(r.btc_refund_locktime)
@@ -2318,21 +2388,38 @@ function swapToTracked(stored) {
2318
2388
  }
2319
2389
 
2320
2390
  // src/tracker/swap-tracker.ts
2391
+ var CLAIM_SUBMITTED = /* @__PURE__ */ new Set([
2392
+ "clientredeeming",
2393
+ "clientredeemed",
2394
+ "serverredeemed"
2395
+ ]);
2396
+ var DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS = 6e4;
2397
+ var RETRY_UNREGISTERED_INTERVAL_MS = 6e4;
2321
2398
  var SwapTracker = class {
2322
2399
  #managers;
2323
2400
  #swaps = /* @__PURE__ */ new Map();
2401
+ /**
2402
+ * Swaps whose startup registration failed (a chain read that errored).
2403
+ * Retried every minute; dropping them would leave an active swap
2404
+ * unwatched for the whole session with no claim or refund surfaced.
2405
+ */
2406
+ #unregistered = /* @__PURE__ */ new Map();
2324
2407
  /** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
2325
2408
  #lastActions = /* @__PURE__ */ new Map();
2409
+ /** The latest server status hint per swap, for {@link #withClaimHint}. */
2410
+ #hintStatuses = /* @__PURE__ */ new Map();
2326
2411
  #subscribers = /* @__PURE__ */ new Set();
2327
2412
  #refreshIntervalMs;
2328
2413
  #atRiskReconcileIntervalMs;
2329
2414
  #lastAtRiskReconcileAt = 0;
2415
+ #lastRetryAt = 0;
2330
2416
  #eventUnsubs = [];
2331
2417
  #timer;
2332
2418
  constructor(managers, options) {
2333
2419
  this.#managers = managers;
2334
2420
  this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
2335
- this.#atRiskReconcileIntervalMs = options?.atRiskReconcileIntervalMs ?? 6e4;
2421
+ const atRisk = options?.atRiskReconcileIntervalMs ?? DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS;
2422
+ this.#atRiskReconcileIntervalMs = typeof atRisk === "function" ? atRisk : () => atRisk;
2336
2423
  }
2337
2424
  /**
2338
2425
  * Whether every on-chain leg of `swap` is observable — i.e. the tracker can
@@ -2355,7 +2442,22 @@ var SwapTracker = class {
2355
2442
  continue;
2356
2443
  }
2357
2444
  this.#swaps.set(swap.swapId, swap);
2358
- for (const leg of legsOf(swap)) await this.#managerFor(leg).register(leg);
2445
+ const registered = [];
2446
+ try {
2447
+ for (const leg of legsOf(swap)) {
2448
+ await this.#managerFor(leg).register(leg);
2449
+ registered.push(leg);
2450
+ }
2451
+ } catch (error) {
2452
+ this.#swaps.delete(swap.swapId);
2453
+ for (const leg of registered)
2454
+ void this.#managerFor(leg).unregister(leg);
2455
+ this.#unregistered.set(swap.swapId, swap);
2456
+ console.warn(
2457
+ `SwapTracker: swap ${swap.swapId} not tracked yet, registering its legs failed (will retry):`,
2458
+ error
2459
+ );
2460
+ }
2359
2461
  }
2360
2462
  for (const manager of new Set(this.#managers.values())) {
2361
2463
  this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
@@ -2363,6 +2465,7 @@ var SwapTracker = class {
2363
2465
  await this.#refreshManagers();
2364
2466
  this.#recomputeAll();
2365
2467
  this.#lastAtRiskReconcileAt = Date.now();
2468
+ this.#lastRetryAt = this.#lastAtRiskReconcileAt;
2366
2469
  if (this.#refreshIntervalMs > 0)
2367
2470
  this.#timer = setInterval(
2368
2471
  () => void this.#tick(),
@@ -2412,7 +2515,11 @@ var SwapTracker = class {
2412
2515
  */
2413
2516
  async #tick() {
2414
2517
  const now = Date.now();
2415
- if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs) {
2518
+ if (now - this.#lastRetryAt >= RETRY_UNREGISTERED_INTERVAL_MS) {
2519
+ this.#lastRetryAt = now;
2520
+ await this.#retryUnregistered();
2521
+ }
2522
+ if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs()) {
2416
2523
  this.#lastAtRiskReconcileAt = now;
2417
2524
  const legs = [...this.#swaps.values()].filter((swap) => this.#isAtRisk(swap)).flatMap((swap) => legsOf(swap));
2418
2525
  await Promise.all(
@@ -2425,6 +2532,25 @@ var SwapTracker = class {
2425
2532
  }
2426
2533
  this.#recomputeAll();
2427
2534
  }
2535
+ /**
2536
+ * Second attempt at swaps whose startup registration failed, on its own
2537
+ * cadence so a slow at-risk interval can't delay it. `track` is idempotent
2538
+ * and rolls back on failure, so a chain that is still down just leaves the
2539
+ * swap here for the next round.
2540
+ */
2541
+ async #retryUnregistered() {
2542
+ for (const [swapId, swap] of this.#unregistered) {
2543
+ try {
2544
+ await this.track(swap);
2545
+ this.#unregistered.delete(swapId);
2546
+ } catch (error) {
2547
+ console.warn(
2548
+ `SwapTracker: retrying registration of ${swapId} failed:`,
2549
+ error
2550
+ );
2551
+ }
2552
+ }
2553
+ }
2428
2554
  /**
2429
2555
  * Whether a swap needs chain-verified monitoring independent of the server:
2430
2556
  * its client leg holds (or held) funds on-chain, so knowing when a refund
@@ -2484,6 +2610,7 @@ var SwapTracker = class {
2484
2610
  async applyHint(swapId, opts) {
2485
2611
  const swap = this.#swaps.get(swapId);
2486
2612
  if (!swap) return;
2613
+ if (opts?.status !== void 0) this.#hintStatuses.set(swapId, opts.status);
2487
2614
  const legs = legsOf(swap);
2488
2615
  const results = await Promise.allSettled(
2489
2616
  legs.map((leg) => this.#managerFor(leg).reconcile(leg))
@@ -2520,6 +2647,7 @@ var SwapTracker = class {
2520
2647
  for (const leg of legsOf(swap))
2521
2648
  void this.#managerFor(leg).unregister(leg);
2522
2649
  this.#swaps.clear();
2650
+ this.#unregistered.clear();
2523
2651
  this.#subscribers.clear();
2524
2652
  }
2525
2653
  #managerFor(ref) {
@@ -2531,6 +2659,28 @@ var SwapTracker = class {
2531
2659
  #recomputeAll() {
2532
2660
  for (const swap of this.#swaps.values()) this.#recompute(swap);
2533
2661
  }
2662
+ /**
2663
+ * A server hint that the claim was submitted overrides a chain view that
2664
+ * still says "claim now". The claim is relayed by the server, so once it
2665
+ * reports the redeem in flight, another claim can only be refused; and a
2666
+ * chain that cannot be read (no `eth_getLogs` on the public Rootstock RPCs)
2667
+ * keeps its stale pre-claim observation for good. The chain still wins once
2668
+ * it moves: a fresh `spent_claim` derives past the hint on its own, and a
2669
+ * refund observed on chain is not overridden.
2670
+ *
2671
+ * The override lasts only while the server's claim window is open. Once the
2672
+ * server can refund its leg, the chain view is what protects the client: the
2673
+ * `serverfunded` rules never claim then and lead to the client's own refund
2674
+ * at its timelock. A claim that really landed shows up on chain long before
2675
+ * that; a hint that never did must not hide the refund.
2676
+ */
2677
+ #withClaimHint(swap, derived, serverChainNow) {
2678
+ const hint = this.#hintStatuses.get(swap.swapId);
2679
+ if (hint === void 0 || !CLAIM_SUBMITTED.has(hint)) return derived;
2680
+ if (derived !== "serverfunded") return derived;
2681
+ const claimWindowOpen = serverChainNow < swap.serverRefundLocktime;
2682
+ return claimWindowOpen ? hint : derived;
2683
+ }
2534
2684
  #recompute(swap, force = false) {
2535
2685
  let clientHtlc;
2536
2686
  let serverHtlc;
@@ -2550,8 +2700,9 @@ var SwapTracker = class {
2550
2700
  if (serverHtlc === void 0 || now === void 0) return;
2551
2701
  serverChainNow = now;
2552
2702
  }
2553
- const status = deriveSwapStatus({ clientHtlc, serverHtlc });
2554
- if (status === void 0) return;
2703
+ const derived = deriveSwapStatus({ clientHtlc, serverHtlc });
2704
+ if (derived === void 0) return;
2705
+ const status = this.#withClaimHint(swap, derived, serverChainNow);
2555
2706
  const actions = {
2556
2707
  ...deriveSwapActions({
2557
2708
  status,
@@ -2602,14 +2753,18 @@ var Client = class {
2602
2753
  #startPromise;
2603
2754
  /** Swap ids whose settled status was already re-fetched this session. */
2604
2755
  #settledSyncDone = /* @__PURE__ */ new Set();
2756
+ /** Read per chain read, so it can change without rebuilding the client. */
2757
+ #bitcoinMinConfirmations;
2605
2758
  constructor(...args) {
2606
- if (args[0] instanceof import_lendaswap_sdk_pure2.Client) {
2759
+ if (args[0] instanceof import_lendaswap_sdk_pure3.Client) {
2607
2760
  this.#legacy = args[0];
2608
2761
  this.#tracking = args[1] ?? {
2609
2762
  enabled: false
2610
2763
  };
2764
+ assertMinConfirmations(this.#tracking.bitcoinMinConfirmations);
2765
+ this.#bitcoinMinConfirmations = this.#tracking.bitcoinMinConfirmations;
2611
2766
  } else {
2612
- this.#legacy = new import_lendaswap_sdk_pure2.Client(
2767
+ this.#legacy = new import_lendaswap_sdk_pure3.Client(
2613
2768
  ...args
2614
2769
  );
2615
2770
  this.#tracking = { enabled: false };
@@ -2790,6 +2945,10 @@ var Client = class {
2790
2945
  return this.#legacy.claim(...args);
2791
2946
  }
2792
2947
  /** Delegated to the legacy client (migration checkpoint). */
2948
+ claimEvmWithSigner(...args) {
2949
+ return this.#legacy.claimEvmWithSigner(...args);
2950
+ }
2951
+ /** Delegated to the legacy client (migration checkpoint). */
2793
2952
  claimViaGasless(...args) {
2794
2953
  return this.#legacy.claimViaGasless(...args);
2795
2954
  }
@@ -2918,6 +3077,22 @@ var Client = class {
2918
3077
  return this.#legacy.hasReceivedVtxo(...args);
2919
3078
  }
2920
3079
  // --- Satora-native features go below ---
3080
+ /** Confirmations a server-funded Bitcoin HTLC needs before it is claimable. */
3081
+ getBitcoinMinConfirmations() {
3082
+ return this.#bitcoinMinConfirmations;
3083
+ }
3084
+ /**
3085
+ * At `0` the claim goes out against a mempool funding, trusting the funder
3086
+ * not to replace it once the claim reveals the preimage.
3087
+ *
3088
+ * Only the chain monitors read a depth, so this does nothing unless the
3089
+ * client was built with {@link ClientBuilder.withChainVerifiedTracking}.
3090
+ * Takes effect on subsequent reads, including swaps already tracked.
3091
+ */
3092
+ setBitcoinMinConfirmations(minConfirmations) {
3093
+ assertMinConfirmations(minConfirmations);
3094
+ this.#bitcoinMinConfirmations = minConfirmations;
3095
+ }
2921
3096
  /**
2922
3097
  * Start observing the user's active swaps and deriving each one's next action.
2923
3098
  *
@@ -2946,8 +3121,18 @@ var Client = class {
2946
3121
  async #doStartTracking() {
2947
3122
  try {
2948
3123
  const chainVerified = this.#tracking.chainVerified || this.#tracking.managers !== void 0;
3124
+ if (!chainVerified && (this.#bitcoinMinConfirmations ?? 0) > 0)
3125
+ console.warn(
3126
+ "Client: bitcoinMinConfirmations is set but tracking follows server hints, which carry no depth. Build with ClientBuilder.withChainVerifiedTracking() for the depth to gate the claim."
3127
+ );
2949
3128
  const tracker = chainVerified ? new SwapTracker(await this.#ensureManagers(), {
2950
- refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
3129
+ refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3,
3130
+ // Depth is only re-derived on a chain read, and nothing pushes on a
3131
+ // new block (an Electrum scripthash status doesn't change once the
3132
+ // funding is in a block), so a deep policy would otherwise sit out
3133
+ // the default interval after the block that satisfied it. Read per
3134
+ // tick, like the depth itself, so a setter call after start counts.
3135
+ atRiskReconcileIntervalMs: () => (this.#bitcoinMinConfirmations ?? 0) > 1 ? 15e3 : DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS
2951
3136
  }) : new HintTracker({
2952
3137
  fetchStatus: async (swapId) => (await this.getSwap(swapId, { updateStorage: true })).status,
2953
3138
  refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
@@ -3133,8 +3318,7 @@ var Client = class {
3133
3318
  evmRpcUrls,
3134
3319
  esploraUrl,
3135
3320
  electrumWsUrl,
3136
- bitcoinNetwork,
3137
- bitcoinMinConfirmations
3321
+ bitcoinNetwork
3138
3322
  } = this.#tracking;
3139
3323
  const chainTime = async () => (await this.getMtp()).mtp * 1e3;
3140
3324
  const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
@@ -3152,10 +3336,10 @@ var Client = class {
3152
3336
  "bitcoin",
3153
3337
  await BitcoinContractManager.create({
3154
3338
  esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
3155
- electrumWsUrl: electrumWsUrl ?? import_lendaswap_sdk_pure2.DEFAULT_ELECTRUM_WS_URLS[network],
3339
+ electrumWsUrl: electrumWsUrl ?? import_lendaswap_sdk_pure3.DEFAULT_ELECTRUM_WS_URLS[network],
3156
3340
  network,
3157
3341
  chainTime,
3158
- minConfirmations: bitcoinMinConfirmations
3342
+ minConfirmations: () => this.#bitcoinMinConfirmations
3159
3343
  })
3160
3344
  );
3161
3345
  this.#managers = managers;
@@ -3163,7 +3347,7 @@ var Client = class {
3163
3347
  }
3164
3348
  };
3165
3349
  var ClientBuilder = class {
3166
- #inner = import_lendaswap_sdk_pure2.Client.builder();
3350
+ #inner = import_lendaswap_sdk_pure3.Client.builder();
3167
3351
  // Observe-mode tracking is on by default; the built client auto-builds its
3168
3352
  // managers from config unless disabled or given an explicit override.
3169
3353
  #trackingEnabled = true;
@@ -3276,9 +3460,11 @@ var ClientBuilder = class {
3276
3460
  * funding hits the mempool, which TRUSTS the funder not to double-spend it
3277
3461
  * (claiming publishes the preimage, so a funder that then replaced its
3278
3462
  * funding could take both legs). Set `1` (or more) for a block-depth policy
3279
- * that doesn't rely on the funder's good behaviour.
3463
+ * that doesn't rely on the funder's good behaviour. Read only by the chain
3464
+ * monitors, so it needs {@link withChainVerifiedTracking} to have any effect.
3280
3465
  */
3281
3466
  withBitcoinMinConfirmations(minConfirmations) {
3467
+ assertMinConfirmations(minConfirmations);
3282
3468
  this.#bitcoinMinConfirmations = minConfirmations;
3283
3469
  return this;
3284
3470
  }
@@ -3335,6 +3521,13 @@ var ClientBuilder = class {
3335
3521
  });
3336
3522
  }
3337
3523
  };
3524
+ function assertMinConfirmations(value) {
3525
+ if (value === void 0) return;
3526
+ if (!Number.isInteger(value) || value < 0)
3527
+ throw new Error(
3528
+ `bitcoinMinConfirmations must be a non-negative integer, got ${value}`
3529
+ );
3530
+ }
3338
3531
  function legacyBaseUrl(legacy) {
3339
3532
  try {
3340
3533
  return legacy.baseUrl;
@@ -3383,6 +3576,7 @@ init_bitcoin_reader_esplora();
3383
3576
  htlcFactsFromEsploraTxs,
3384
3577
  htlcKey,
3385
3578
  htlcQueryKey,
3579
+ resolveMinConfirmations,
3386
3580
  swapToTracked,
3387
3581
  ...require("@lendasat/lendaswap-sdk-pure")
3388
3582
  });