@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/README.md +1 -1
- package/dist/bitcoin-reader-electrum-KEI7VWW6.js +8 -0
- package/dist/bitcoin-reader-esplora-B2BDEI6Z.js +13 -0
- package/dist/{chunk-FTPIQSRW.js → chunk-3ZMSZSAV.js} +11 -6
- package/dist/chunk-3ZMSZSAV.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 +246 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -71
- package/dist/index.d.ts +112 -71
- package/dist/index.js +237 -46
- 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-KEI7VWW6.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,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) =>
|
|
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 ??
|
|
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
|
|
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
|
-
{
|
|
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
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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)
|
|
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
|
-
|
|
2146
|
-
//
|
|
2147
|
-
token
|
|
2148
|
-
|
|
2149
|
-
// the
|
|
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,
|
|
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
|
-
|
|
2196
|
-
// the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
|
2554
|
-
if (
|
|
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
|
|
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
|
|
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 ??
|
|
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 =
|
|
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
|
});
|