@satora/swap 1.3.3 → 1.5.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/bitcoin-reader-electrum-Z2YCNDLB.js +8 -0
- package/dist/bitcoin-reader-esplora-B2BDEI6Z.js +13 -0
- package/dist/{chunk-FTPIQSRW.js → chunk-INWVLX4A.js} +6 -3
- package/dist/chunk-INWVLX4A.js.map +1 -0
- package/dist/{chunk-THI4UO6R.js → chunk-LSCRTQOS.js} +6 -3
- package/dist/chunk-LSCRTQOS.js.map +1 -0
- package/dist/index.cjs +266 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -71
- package/dist/index.d.ts +115 -71
- package/dist/index.js +265 -41
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/bitcoin-reader-electrum-PY7DHD6T.js +0 -7
- package/dist/bitcoin-reader-esplora-ZLPMM5FF.js +0 -11
- package/dist/chunk-FTPIQSRW.js.map +0 -1
- package/dist/chunk-THI4UO6R.js.map +0 -1
- /package/dist/{bitcoin-reader-electrum-PY7DHD6T.js.map → bitcoin-reader-electrum-Z2YCNDLB.js.map} +0 -0
- /package/dist/{bitcoin-reader-esplora-ZLPMM5FF.js.map → bitcoin-reader-esplora-B2BDEI6Z.js.map} +0 -0
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 ??
|
|
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,7 +148,6 @@ 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
152
|
const scriptFor = (address) => btc.OutScript.encode(
|
|
151
153
|
btc.Address(toBtcSignerNetwork(network)).decode(address)
|
|
@@ -231,7 +233,7 @@ function electrumReader(client, opts) {
|
|
|
231
233
|
}
|
|
232
234
|
return {
|
|
233
235
|
async getHtlcFacts(address, minConfirmations) {
|
|
234
|
-
const required = minConfirmations ??
|
|
236
|
+
const required = minConfirmations ?? resolveMinConfirmations(opts?.minConfirmations);
|
|
235
237
|
try {
|
|
236
238
|
return await getHtlcFactsElectrum(address, required);
|
|
237
239
|
} catch (error) {
|
|
@@ -273,6 +275,7 @@ var init_bitcoin_reader_electrum = __esm({
|
|
|
273
275
|
import_sha22 = require("@noble/hashes/sha2.js");
|
|
274
276
|
import_base5 = require("@scure/base");
|
|
275
277
|
btc = __toESM(require("@scure/btc-signer"), 1);
|
|
278
|
+
init_bitcoin_reader_esplora();
|
|
276
279
|
REGTEST_NETWORK = {
|
|
277
280
|
bech32: "bcrt",
|
|
278
281
|
pubKeyHash: 111,
|
|
@@ -308,6 +311,7 @@ __export(src_exports, {
|
|
|
308
311
|
htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
|
|
309
312
|
htlcKey: () => htlcKey,
|
|
310
313
|
htlcQueryKey: () => htlcQueryKey,
|
|
314
|
+
resolveMinConfirmations: () => resolveMinConfirmations,
|
|
311
315
|
swapToTracked: () => swapToTracked
|
|
312
316
|
});
|
|
313
317
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -322,7 +326,8 @@ function deriveSwapActions(input) {
|
|
|
322
326
|
serverChainNow,
|
|
323
327
|
clientRefundLocktime,
|
|
324
328
|
serverRefundLocktime,
|
|
325
|
-
clientFunds = true
|
|
329
|
+
clientFunds = true,
|
|
330
|
+
serverFunds = true
|
|
326
331
|
} = input;
|
|
327
332
|
switch (status) {
|
|
328
333
|
case "pending": {
|
|
@@ -407,6 +412,36 @@ function deriveSwapActions(input) {
|
|
|
407
412
|
return { recommended: "wait", actions: [wait, refund] };
|
|
408
413
|
}
|
|
409
414
|
case "serverfunded": {
|
|
415
|
+
if (!serverFunds) {
|
|
416
|
+
const refundUnlocked2 = clientChainNow >= clientRefundLocktime;
|
|
417
|
+
if (refundUnlocked2) {
|
|
418
|
+
const refund3 = {
|
|
419
|
+
id: "refund_unilateral",
|
|
420
|
+
recommended: true,
|
|
421
|
+
automation: "confirm",
|
|
422
|
+
reason: "Reclaim your deposit."
|
|
423
|
+
};
|
|
424
|
+
return { recommended: "refund_unilateral", actions: [refund3] };
|
|
425
|
+
}
|
|
426
|
+
const wait2 = {
|
|
427
|
+
id: "wait",
|
|
428
|
+
waitingOn: "server_funding",
|
|
429
|
+
recommended: true,
|
|
430
|
+
automation: "auto",
|
|
431
|
+
reason: "Waiting for the Lightning payment to settle."
|
|
432
|
+
};
|
|
433
|
+
const refund2 = {
|
|
434
|
+
id: "refund_unilateral",
|
|
435
|
+
recommended: false,
|
|
436
|
+
automation: "confirm",
|
|
437
|
+
reason: "Refund becomes available once the timelock passes.",
|
|
438
|
+
blockedBy: {
|
|
439
|
+
kind: "timelock_not_expired",
|
|
440
|
+
message: "The refund timelock has not passed yet."
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
return { recommended: "wait", actions: [wait2, refund2] };
|
|
444
|
+
}
|
|
410
445
|
const claimWindowClosed = serverChainNow >= serverRefundLocktime;
|
|
411
446
|
if (!claimWindowClosed) {
|
|
412
447
|
const claim = {
|
|
@@ -1081,7 +1116,8 @@ function refQueryKey(ref) {
|
|
|
1081
1116
|
var AVG_BLOCK_MS = {
|
|
1082
1117
|
1: 12e3,
|
|
1083
1118
|
137: 2100,
|
|
1084
|
-
42161: 250
|
|
1119
|
+
42161: 250,
|
|
1120
|
+
30: 3e4
|
|
1085
1121
|
};
|
|
1086
1122
|
var SCAN_MARGIN_BLOCKS = 1000n;
|
|
1087
1123
|
var EvmContractManager = class _EvmContractManager {
|
|
@@ -1337,7 +1373,9 @@ var DEFAULT_EVM_RPCS = {
|
|
|
1337
1373
|
"https://arb1.arbitrum.io/rpc",
|
|
1338
1374
|
"https://arbitrum.drpc.org",
|
|
1339
1375
|
"https://arbitrum-one-rpc.publicnode.com"
|
|
1340
|
-
]
|
|
1376
|
+
],
|
|
1377
|
+
// Rootstock: the public node, HTTP only (no WebSocket endpoint exists).
|
|
1378
|
+
30: ["https://public-node.rsk.co"]
|
|
1341
1379
|
};
|
|
1342
1380
|
var SWAP_CREATED = (0, import_viem.parseAbiItem)(
|
|
1343
1381
|
"event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock, bytes32 key)"
|
|
@@ -1370,25 +1408,52 @@ function activeKey(q) {
|
|
|
1370
1408
|
}
|
|
1371
1409
|
});
|
|
1372
1410
|
}
|
|
1373
|
-
function evmReaderFromClient(client) {
|
|
1411
|
+
function evmReaderFromClient(client, options = {}) {
|
|
1412
|
+
const { maxBlockRange } = options;
|
|
1413
|
+
async function getLogs(filter, fromBlock) {
|
|
1414
|
+
if (maxBlockRange === void 0) {
|
|
1415
|
+
return client.request({
|
|
1416
|
+
method: "eth_getLogs",
|
|
1417
|
+
params: [{ ...filter, fromBlock: (0, import_viem.numberToHex)(fromBlock) }]
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
const latest = (await client.getBlock()).number;
|
|
1421
|
+
if (latest === null) {
|
|
1422
|
+
throw new Error("latest block has no number");
|
|
1423
|
+
}
|
|
1424
|
+
const logs = [];
|
|
1425
|
+
for (let from = fromBlock; from <= latest; from += maxBlockRange) {
|
|
1426
|
+
const to = from + maxBlockRange - 1n < latest ? from + maxBlockRange - 1n : latest;
|
|
1427
|
+
logs.push(
|
|
1428
|
+
...await client.request({
|
|
1429
|
+
method: "eth_getLogs",
|
|
1430
|
+
params: [
|
|
1431
|
+
{
|
|
1432
|
+
...filter,
|
|
1433
|
+
fromBlock: (0, import_viem.numberToHex)(from),
|
|
1434
|
+
toBlock: (0, import_viem.numberToHex)(to)
|
|
1435
|
+
}
|
|
1436
|
+
]
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
return logs;
|
|
1441
|
+
}
|
|
1374
1442
|
return {
|
|
1375
1443
|
async getHtlcEventsBatch(queries, fromBlock = 0n) {
|
|
1376
1444
|
const results = /* @__PURE__ */ new Map();
|
|
1377
1445
|
if (queries.length === 0) return results;
|
|
1378
1446
|
for (const q of queries) results.set(htlcQueryKey(q), []);
|
|
1379
|
-
const logs = await
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
]
|
|
1391
|
-
});
|
|
1447
|
+
const logs = await getLogs(
|
|
1448
|
+
{
|
|
1449
|
+
address: unique(queries.map((q) => q.htlc)),
|
|
1450
|
+
topics: [
|
|
1451
|
+
HTLC_EVENT_TOPICS,
|
|
1452
|
+
unique(queries.map((q) => q.preimageHash))
|
|
1453
|
+
]
|
|
1454
|
+
},
|
|
1455
|
+
fromBlock
|
|
1456
|
+
);
|
|
1392
1457
|
const grouped = /* @__PURE__ */ new Map();
|
|
1393
1458
|
for (const log of logs) {
|
|
1394
1459
|
const decoded = tryDecode(log);
|
|
@@ -1504,12 +1569,15 @@ function toHtlcEvent(decoded, query) {
|
|
|
1504
1569
|
function unique(values) {
|
|
1505
1570
|
return [...new Set(values)];
|
|
1506
1571
|
}
|
|
1507
|
-
function createEvmRpcReader(rpcUrls) {
|
|
1572
|
+
function createEvmRpcReader(rpcUrls, options = {}) {
|
|
1508
1573
|
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
1509
1574
|
const transport = urls.length > 1 ? (0, import_viem.fallback)(urls.map((url) => (0, import_viem.http)(url))) : (0, import_viem.http)(urls[0]);
|
|
1510
1575
|
const client = (0, import_viem.createPublicClient)({ transport });
|
|
1511
|
-
return evmReaderFromClient(client);
|
|
1576
|
+
return evmReaderFromClient(client, options);
|
|
1512
1577
|
}
|
|
1578
|
+
var EVM_LOG_RANGE_LIMITS = {
|
|
1579
|
+
30: 2000n
|
|
1580
|
+
};
|
|
1513
1581
|
function defaultEvmReaders(overrides) {
|
|
1514
1582
|
const chainIds = /* @__PURE__ */ new Set([
|
|
1515
1583
|
...Object.keys(DEFAULT_EVM_RPCS).map(Number),
|
|
@@ -1520,7 +1588,13 @@ function defaultEvmReaders(overrides) {
|
|
|
1520
1588
|
const override = overrides?.[chainId];
|
|
1521
1589
|
const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
|
|
1522
1590
|
const urls = override ? [override, ...defaults] : defaults;
|
|
1523
|
-
if (urls.length > 0)
|
|
1591
|
+
if (urls.length > 0)
|
|
1592
|
+
readers.set(
|
|
1593
|
+
chainId,
|
|
1594
|
+
createEvmRpcReader(urls, {
|
|
1595
|
+
maxBlockRange: EVM_LOG_RANGE_LIMITS[chainId]
|
|
1596
|
+
})
|
|
1597
|
+
);
|
|
1524
1598
|
}
|
|
1525
1599
|
return readers;
|
|
1526
1600
|
}
|
|
@@ -1617,8 +1691,10 @@ var HintTracker = class {
|
|
|
1617
1691
|
serverChainNow: now,
|
|
1618
1692
|
clientRefundLocktime: swap.clientRefundLocktime,
|
|
1619
1693
|
serverRefundLocktime: swap.serverRefundLocktime,
|
|
1620
|
-
// Pay-on-Lightning swaps have no client-funded on-chain leg
|
|
1621
|
-
|
|
1694
|
+
// Pay-on-Lightning swaps have no client-funded on-chain leg;
|
|
1695
|
+
// receive-on-Lightning swaps have no server-funded one.
|
|
1696
|
+
clientFunds: swap.clientHtlc !== void 0,
|
|
1697
|
+
serverFunds: swap.serverHtlc !== void 0
|
|
1622
1698
|
});
|
|
1623
1699
|
const previous = this.#lastActions.get(swap.swapId);
|
|
1624
1700
|
if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
@@ -2241,6 +2317,29 @@ function swapToTracked(stored) {
|
|
|
2241
2317
|
// no on-chain client leg
|
|
2242
2318
|
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
2243
2319
|
};
|
|
2320
|
+
// Client funds the EVM HTLC (evm_expected_sats of the BTC-pegged token);
|
|
2321
|
+
// the server pays the Lightning invoice off-chain and claims the HTLC —
|
|
2322
|
+
// no server leg to watch.
|
|
2323
|
+
case "evm_to_lightning":
|
|
2324
|
+
return {
|
|
2325
|
+
swapId: r.id,
|
|
2326
|
+
clientHtlc: evmLeg({
|
|
2327
|
+
chainId: r.evm_chain_id,
|
|
2328
|
+
htlc: r.evm_htlc_address,
|
|
2329
|
+
hashLock: r.hash_lock,
|
|
2330
|
+
claimAddress: r.server_evm_address,
|
|
2331
|
+
// the server claims the client's EVM HTLC
|
|
2332
|
+
expectedSats: r.evm_expected_sats,
|
|
2333
|
+
token: r.wbtc_address,
|
|
2334
|
+
sender: r.evm_coordinator_address,
|
|
2335
|
+
// the coordinator locks on the client's behalf
|
|
2336
|
+
timelockSec: r.evm_refund_locktime,
|
|
2337
|
+
createdAt: r.created_at
|
|
2338
|
+
}),
|
|
2339
|
+
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
2340
|
+
serverRefundLocktime: 0
|
|
2341
|
+
// no on-chain server leg
|
|
2342
|
+
};
|
|
2244
2343
|
// Client funds the Arkade VHTLC (source_amount); the server pays the
|
|
2245
2344
|
// Lightning invoice off-chain and claims the VHTLC — no server leg to
|
|
2246
2345
|
// watch.
|
|
@@ -2262,21 +2361,38 @@ function swapToTracked(stored) {
|
|
|
2262
2361
|
}
|
|
2263
2362
|
|
|
2264
2363
|
// src/tracker/swap-tracker.ts
|
|
2364
|
+
var CLAIM_SUBMITTED = /* @__PURE__ */ new Set([
|
|
2365
|
+
"clientredeeming",
|
|
2366
|
+
"clientredeemed",
|
|
2367
|
+
"serverredeemed"
|
|
2368
|
+
]);
|
|
2369
|
+
var DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS = 6e4;
|
|
2370
|
+
var RETRY_UNREGISTERED_INTERVAL_MS = 6e4;
|
|
2265
2371
|
var SwapTracker = class {
|
|
2266
2372
|
#managers;
|
|
2267
2373
|
#swaps = /* @__PURE__ */ new Map();
|
|
2374
|
+
/**
|
|
2375
|
+
* Swaps whose startup registration failed (a chain read that errored).
|
|
2376
|
+
* Retried every minute; dropping them would leave an active swap
|
|
2377
|
+
* unwatched for the whole session with no claim or refund surfaced.
|
|
2378
|
+
*/
|
|
2379
|
+
#unregistered = /* @__PURE__ */ new Map();
|
|
2268
2380
|
/** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
|
|
2269
2381
|
#lastActions = /* @__PURE__ */ new Map();
|
|
2382
|
+
/** The latest server status hint per swap, for {@link #withClaimHint}. */
|
|
2383
|
+
#hintStatuses = /* @__PURE__ */ new Map();
|
|
2270
2384
|
#subscribers = /* @__PURE__ */ new Set();
|
|
2271
2385
|
#refreshIntervalMs;
|
|
2272
2386
|
#atRiskReconcileIntervalMs;
|
|
2273
2387
|
#lastAtRiskReconcileAt = 0;
|
|
2388
|
+
#lastRetryAt = 0;
|
|
2274
2389
|
#eventUnsubs = [];
|
|
2275
2390
|
#timer;
|
|
2276
2391
|
constructor(managers, options) {
|
|
2277
2392
|
this.#managers = managers;
|
|
2278
2393
|
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
2279
|
-
|
|
2394
|
+
const atRisk = options?.atRiskReconcileIntervalMs ?? DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS;
|
|
2395
|
+
this.#atRiskReconcileIntervalMs = typeof atRisk === "function" ? atRisk : () => atRisk;
|
|
2280
2396
|
}
|
|
2281
2397
|
/**
|
|
2282
2398
|
* Whether every on-chain leg of `swap` is observable — i.e. the tracker can
|
|
@@ -2299,7 +2415,22 @@ var SwapTracker = class {
|
|
|
2299
2415
|
continue;
|
|
2300
2416
|
}
|
|
2301
2417
|
this.#swaps.set(swap.swapId, swap);
|
|
2302
|
-
|
|
2418
|
+
const registered = [];
|
|
2419
|
+
try {
|
|
2420
|
+
for (const leg of legsOf(swap)) {
|
|
2421
|
+
await this.#managerFor(leg).register(leg);
|
|
2422
|
+
registered.push(leg);
|
|
2423
|
+
}
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
this.#swaps.delete(swap.swapId);
|
|
2426
|
+
for (const leg of registered)
|
|
2427
|
+
void this.#managerFor(leg).unregister(leg);
|
|
2428
|
+
this.#unregistered.set(swap.swapId, swap);
|
|
2429
|
+
console.warn(
|
|
2430
|
+
`SwapTracker: swap ${swap.swapId} not tracked yet, registering its legs failed (will retry):`,
|
|
2431
|
+
error
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2303
2434
|
}
|
|
2304
2435
|
for (const manager of new Set(this.#managers.values())) {
|
|
2305
2436
|
this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
|
|
@@ -2307,6 +2438,7 @@ var SwapTracker = class {
|
|
|
2307
2438
|
await this.#refreshManagers();
|
|
2308
2439
|
this.#recomputeAll();
|
|
2309
2440
|
this.#lastAtRiskReconcileAt = Date.now();
|
|
2441
|
+
this.#lastRetryAt = this.#lastAtRiskReconcileAt;
|
|
2310
2442
|
if (this.#refreshIntervalMs > 0)
|
|
2311
2443
|
this.#timer = setInterval(
|
|
2312
2444
|
() => void this.#tick(),
|
|
@@ -2356,7 +2488,11 @@ var SwapTracker = class {
|
|
|
2356
2488
|
*/
|
|
2357
2489
|
async #tick() {
|
|
2358
2490
|
const now = Date.now();
|
|
2359
|
-
if (now - this.#
|
|
2491
|
+
if (now - this.#lastRetryAt >= RETRY_UNREGISTERED_INTERVAL_MS) {
|
|
2492
|
+
this.#lastRetryAt = now;
|
|
2493
|
+
await this.#retryUnregistered();
|
|
2494
|
+
}
|
|
2495
|
+
if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs()) {
|
|
2360
2496
|
this.#lastAtRiskReconcileAt = now;
|
|
2361
2497
|
const legs = [...this.#swaps.values()].filter((swap) => this.#isAtRisk(swap)).flatMap((swap) => legsOf(swap));
|
|
2362
2498
|
await Promise.all(
|
|
@@ -2369,6 +2505,25 @@ var SwapTracker = class {
|
|
|
2369
2505
|
}
|
|
2370
2506
|
this.#recomputeAll();
|
|
2371
2507
|
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Second attempt at swaps whose startup registration failed, on its own
|
|
2510
|
+
* cadence so a slow at-risk interval can't delay it. `track` is idempotent
|
|
2511
|
+
* and rolls back on failure, so a chain that is still down just leaves the
|
|
2512
|
+
* swap here for the next round.
|
|
2513
|
+
*/
|
|
2514
|
+
async #retryUnregistered() {
|
|
2515
|
+
for (const [swapId, swap] of this.#unregistered) {
|
|
2516
|
+
try {
|
|
2517
|
+
await this.track(swap);
|
|
2518
|
+
this.#unregistered.delete(swapId);
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
console.warn(
|
|
2521
|
+
`SwapTracker: retrying registration of ${swapId} failed:`,
|
|
2522
|
+
error
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2372
2527
|
/**
|
|
2373
2528
|
* Whether a swap needs chain-verified monitoring independent of the server:
|
|
2374
2529
|
* its client leg holds (or held) funds on-chain, so knowing when a refund
|
|
@@ -2428,6 +2583,7 @@ var SwapTracker = class {
|
|
|
2428
2583
|
async applyHint(swapId, opts) {
|
|
2429
2584
|
const swap = this.#swaps.get(swapId);
|
|
2430
2585
|
if (!swap) return;
|
|
2586
|
+
if (opts?.status !== void 0) this.#hintStatuses.set(swapId, opts.status);
|
|
2431
2587
|
const legs = legsOf(swap);
|
|
2432
2588
|
const results = await Promise.allSettled(
|
|
2433
2589
|
legs.map((leg) => this.#managerFor(leg).reconcile(leg))
|
|
@@ -2464,6 +2620,7 @@ var SwapTracker = class {
|
|
|
2464
2620
|
for (const leg of legsOf(swap))
|
|
2465
2621
|
void this.#managerFor(leg).unregister(leg);
|
|
2466
2622
|
this.#swaps.clear();
|
|
2623
|
+
this.#unregistered.clear();
|
|
2467
2624
|
this.#subscribers.clear();
|
|
2468
2625
|
}
|
|
2469
2626
|
#managerFor(ref) {
|
|
@@ -2475,6 +2632,28 @@ var SwapTracker = class {
|
|
|
2475
2632
|
#recomputeAll() {
|
|
2476
2633
|
for (const swap of this.#swaps.values()) this.#recompute(swap);
|
|
2477
2634
|
}
|
|
2635
|
+
/**
|
|
2636
|
+
* A server hint that the claim was submitted overrides a chain view that
|
|
2637
|
+
* still says "claim now". The claim is relayed by the server, so once it
|
|
2638
|
+
* reports the redeem in flight, another claim can only be refused; and a
|
|
2639
|
+
* chain that cannot be read (no `eth_getLogs` on the public Rootstock RPCs)
|
|
2640
|
+
* keeps its stale pre-claim observation for good. The chain still wins once
|
|
2641
|
+
* it moves: a fresh `spent_claim` derives past the hint on its own, and a
|
|
2642
|
+
* refund observed on chain is not overridden.
|
|
2643
|
+
*
|
|
2644
|
+
* The override lasts only while the server's claim window is open. Once the
|
|
2645
|
+
* server can refund its leg, the chain view is what protects the client: the
|
|
2646
|
+
* `serverfunded` rules never claim then and lead to the client's own refund
|
|
2647
|
+
* at its timelock. A claim that really landed shows up on chain long before
|
|
2648
|
+
* that; a hint that never did must not hide the refund.
|
|
2649
|
+
*/
|
|
2650
|
+
#withClaimHint(swap, derived, serverChainNow) {
|
|
2651
|
+
const hint = this.#hintStatuses.get(swap.swapId);
|
|
2652
|
+
if (hint === void 0 || !CLAIM_SUBMITTED.has(hint)) return derived;
|
|
2653
|
+
if (derived !== "serverfunded") return derived;
|
|
2654
|
+
const claimWindowOpen = serverChainNow < swap.serverRefundLocktime;
|
|
2655
|
+
return claimWindowOpen ? hint : derived;
|
|
2656
|
+
}
|
|
2478
2657
|
#recompute(swap, force = false) {
|
|
2479
2658
|
let clientHtlc;
|
|
2480
2659
|
let serverHtlc;
|
|
@@ -2494,8 +2673,9 @@ var SwapTracker = class {
|
|
|
2494
2673
|
if (serverHtlc === void 0 || now === void 0) return;
|
|
2495
2674
|
serverChainNow = now;
|
|
2496
2675
|
}
|
|
2497
|
-
const
|
|
2498
|
-
if (
|
|
2676
|
+
const derived = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
2677
|
+
if (derived === void 0) return;
|
|
2678
|
+
const status = this.#withClaimHint(swap, derived, serverChainNow);
|
|
2499
2679
|
const actions = {
|
|
2500
2680
|
...deriveSwapActions({
|
|
2501
2681
|
status,
|
|
@@ -2503,8 +2683,10 @@ var SwapTracker = class {
|
|
|
2503
2683
|
serverChainNow,
|
|
2504
2684
|
clientRefundLocktime: swap.clientRefundLocktime,
|
|
2505
2685
|
serverRefundLocktime: swap.serverRefundLocktime,
|
|
2506
|
-
// Pay-on-Lightning swaps have no client-funded on-chain leg
|
|
2507
|
-
|
|
2686
|
+
// Pay-on-Lightning swaps have no client-funded on-chain leg;
|
|
2687
|
+
// receive-on-Lightning swaps have no server-funded one.
|
|
2688
|
+
clientFunds: swap.clientHtlc !== void 0,
|
|
2689
|
+
serverFunds: swap.serverHtlc !== void 0
|
|
2508
2690
|
}),
|
|
2509
2691
|
// Expose the raw observations so consumers can render progress from
|
|
2510
2692
|
// chain facts (which leg is funded/spent) even when the server is
|
|
@@ -2544,12 +2726,16 @@ var Client = class {
|
|
|
2544
2726
|
#startPromise;
|
|
2545
2727
|
/** Swap ids whose settled status was already re-fetched this session. */
|
|
2546
2728
|
#settledSyncDone = /* @__PURE__ */ new Set();
|
|
2729
|
+
/** Read per chain read, so it can change without rebuilding the client. */
|
|
2730
|
+
#bitcoinMinConfirmations;
|
|
2547
2731
|
constructor(...args) {
|
|
2548
2732
|
if (args[0] instanceof import_lendaswap_sdk_pure2.Client) {
|
|
2549
2733
|
this.#legacy = args[0];
|
|
2550
2734
|
this.#tracking = args[1] ?? {
|
|
2551
2735
|
enabled: false
|
|
2552
2736
|
};
|
|
2737
|
+
assertMinConfirmations(this.#tracking.bitcoinMinConfirmations);
|
|
2738
|
+
this.#bitcoinMinConfirmations = this.#tracking.bitcoinMinConfirmations;
|
|
2553
2739
|
} else {
|
|
2554
2740
|
this.#legacy = new import_lendaswap_sdk_pure2.Client(
|
|
2555
2741
|
...args
|
|
@@ -2796,6 +2982,12 @@ var Client = class {
|
|
|
2796
2982
|
);
|
|
2797
2983
|
}
|
|
2798
2984
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
2985
|
+
createEvmToLightningSwap(...args) {
|
|
2986
|
+
return this.#trackAfterCreate(
|
|
2987
|
+
this.#legacy.createEvmToLightningSwap(...args)
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
2799
2991
|
createEvmToArkadeSwapGeneric(...args) {
|
|
2800
2992
|
return this.#trackAfterCreate(
|
|
2801
2993
|
this.#legacy.createEvmToArkadeSwapGeneric(...args)
|
|
@@ -2854,6 +3046,22 @@ var Client = class {
|
|
|
2854
3046
|
return this.#legacy.hasReceivedVtxo(...args);
|
|
2855
3047
|
}
|
|
2856
3048
|
// --- Satora-native features go below ---
|
|
3049
|
+
/** Confirmations a server-funded Bitcoin HTLC needs before it is claimable. */
|
|
3050
|
+
getBitcoinMinConfirmations() {
|
|
3051
|
+
return this.#bitcoinMinConfirmations;
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* At `0` the claim goes out against a mempool funding, trusting the funder
|
|
3055
|
+
* not to replace it once the claim reveals the preimage.
|
|
3056
|
+
*
|
|
3057
|
+
* Only the chain monitors read a depth, so this does nothing unless the
|
|
3058
|
+
* client was built with {@link ClientBuilder.withChainVerifiedTracking}.
|
|
3059
|
+
* Takes effect on subsequent reads, including swaps already tracked.
|
|
3060
|
+
*/
|
|
3061
|
+
setBitcoinMinConfirmations(minConfirmations) {
|
|
3062
|
+
assertMinConfirmations(minConfirmations);
|
|
3063
|
+
this.#bitcoinMinConfirmations = minConfirmations;
|
|
3064
|
+
}
|
|
2857
3065
|
/**
|
|
2858
3066
|
* Start observing the user's active swaps and deriving each one's next action.
|
|
2859
3067
|
*
|
|
@@ -2882,8 +3090,18 @@ var Client = class {
|
|
|
2882
3090
|
async #doStartTracking() {
|
|
2883
3091
|
try {
|
|
2884
3092
|
const chainVerified = this.#tracking.chainVerified || this.#tracking.managers !== void 0;
|
|
3093
|
+
if (!chainVerified && (this.#bitcoinMinConfirmations ?? 0) > 0)
|
|
3094
|
+
console.warn(
|
|
3095
|
+
"Client: bitcoinMinConfirmations is set but tracking follows server hints, which carry no depth. Build with ClientBuilder.withChainVerifiedTracking() for the depth to gate the claim."
|
|
3096
|
+
);
|
|
2885
3097
|
const tracker = chainVerified ? new SwapTracker(await this.#ensureManagers(), {
|
|
2886
|
-
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
3098
|
+
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3,
|
|
3099
|
+
// Depth is only re-derived on a chain read, and nothing pushes on a
|
|
3100
|
+
// new block (an Electrum scripthash status doesn't change once the
|
|
3101
|
+
// funding is in a block), so a deep policy would otherwise sit out
|
|
3102
|
+
// the default interval after the block that satisfied it. Read per
|
|
3103
|
+
// tick, like the depth itself, so a setter call after start counts.
|
|
3104
|
+
atRiskReconcileIntervalMs: () => (this.#bitcoinMinConfirmations ?? 0) > 1 ? 15e3 : DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS
|
|
2887
3105
|
}) : new HintTracker({
|
|
2888
3106
|
fetchStatus: async (swapId) => (await this.getSwap(swapId, { updateStorage: true })).status,
|
|
2889
3107
|
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
@@ -3069,8 +3287,7 @@ var Client = class {
|
|
|
3069
3287
|
evmRpcUrls,
|
|
3070
3288
|
esploraUrl,
|
|
3071
3289
|
electrumWsUrl,
|
|
3072
|
-
bitcoinNetwork
|
|
3073
|
-
bitcoinMinConfirmations
|
|
3290
|
+
bitcoinNetwork
|
|
3074
3291
|
} = this.#tracking;
|
|
3075
3292
|
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
3076
3293
|
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
@@ -3091,7 +3308,7 @@ var Client = class {
|
|
|
3091
3308
|
electrumWsUrl: electrumWsUrl ?? import_lendaswap_sdk_pure2.DEFAULT_ELECTRUM_WS_URLS[network],
|
|
3092
3309
|
network,
|
|
3093
3310
|
chainTime,
|
|
3094
|
-
minConfirmations: bitcoinMinConfirmations
|
|
3311
|
+
minConfirmations: () => this.#bitcoinMinConfirmations
|
|
3095
3312
|
})
|
|
3096
3313
|
);
|
|
3097
3314
|
this.#managers = managers;
|
|
@@ -3212,9 +3429,11 @@ var ClientBuilder = class {
|
|
|
3212
3429
|
* funding hits the mempool, which TRUSTS the funder not to double-spend it
|
|
3213
3430
|
* (claiming publishes the preimage, so a funder that then replaced its
|
|
3214
3431
|
* funding could take both legs). Set `1` (or more) for a block-depth policy
|
|
3215
|
-
* that doesn't rely on the funder's good behaviour.
|
|
3432
|
+
* that doesn't rely on the funder's good behaviour. Read only by the chain
|
|
3433
|
+
* monitors, so it needs {@link withChainVerifiedTracking} to have any effect.
|
|
3216
3434
|
*/
|
|
3217
3435
|
withBitcoinMinConfirmations(minConfirmations) {
|
|
3436
|
+
assertMinConfirmations(minConfirmations);
|
|
3218
3437
|
this.#bitcoinMinConfirmations = minConfirmations;
|
|
3219
3438
|
return this;
|
|
3220
3439
|
}
|
|
@@ -3271,6 +3490,13 @@ var ClientBuilder = class {
|
|
|
3271
3490
|
});
|
|
3272
3491
|
}
|
|
3273
3492
|
};
|
|
3493
|
+
function assertMinConfirmations(value) {
|
|
3494
|
+
if (value === void 0) return;
|
|
3495
|
+
if (!Number.isInteger(value) || value < 0)
|
|
3496
|
+
throw new Error(
|
|
3497
|
+
`bitcoinMinConfirmations must be a non-negative integer, got ${value}`
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3274
3500
|
function legacyBaseUrl(legacy) {
|
|
3275
3501
|
try {
|
|
3276
3502
|
return legacy.baseUrl;
|
|
@@ -3319,6 +3545,7 @@ init_bitcoin_reader_esplora();
|
|
|
3319
3545
|
htlcFactsFromEsploraTxs,
|
|
3320
3546
|
htlcKey,
|
|
3321
3547
|
htlcQueryKey,
|
|
3548
|
+
resolveMinConfirmations,
|
|
3322
3549
|
swapToTracked,
|
|
3323
3550
|
...require("@lendasat/lendaswap-sdk-pure")
|
|
3324
3551
|
});
|