@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.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
electrumReader
|
|
3
|
+
} from "./chunk-INWVLX4A.js";
|
|
1
4
|
import {
|
|
2
5
|
DEFAULT_ESPLORA_URLS,
|
|
3
6
|
esploraReader,
|
|
4
|
-
htlcFactsFromEsploraTxs
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
electrumReader
|
|
8
|
-
} from "./chunk-FTPIQSRW.js";
|
|
7
|
+
htlcFactsFromEsploraTxs,
|
|
8
|
+
resolveMinConfirmations
|
|
9
|
+
} from "./chunk-LSCRTQOS.js";
|
|
9
10
|
|
|
10
11
|
// src/index.ts
|
|
11
12
|
export * from "@lendasat/lendaswap-sdk-pure";
|
|
@@ -19,7 +20,8 @@ function deriveSwapActions(input) {
|
|
|
19
20
|
serverChainNow,
|
|
20
21
|
clientRefundLocktime,
|
|
21
22
|
serverRefundLocktime,
|
|
22
|
-
clientFunds = true
|
|
23
|
+
clientFunds = true,
|
|
24
|
+
serverFunds = true
|
|
23
25
|
} = input;
|
|
24
26
|
switch (status) {
|
|
25
27
|
case "pending": {
|
|
@@ -104,6 +106,36 @@ function deriveSwapActions(input) {
|
|
|
104
106
|
return { recommended: "wait", actions: [wait, refund] };
|
|
105
107
|
}
|
|
106
108
|
case "serverfunded": {
|
|
109
|
+
if (!serverFunds) {
|
|
110
|
+
const refundUnlocked2 = clientChainNow >= clientRefundLocktime;
|
|
111
|
+
if (refundUnlocked2) {
|
|
112
|
+
const refund3 = {
|
|
113
|
+
id: "refund_unilateral",
|
|
114
|
+
recommended: true,
|
|
115
|
+
automation: "confirm",
|
|
116
|
+
reason: "Reclaim your deposit."
|
|
117
|
+
};
|
|
118
|
+
return { recommended: "refund_unilateral", actions: [refund3] };
|
|
119
|
+
}
|
|
120
|
+
const wait2 = {
|
|
121
|
+
id: "wait",
|
|
122
|
+
waitingOn: "server_funding",
|
|
123
|
+
recommended: true,
|
|
124
|
+
automation: "auto",
|
|
125
|
+
reason: "Waiting for the Lightning payment to settle."
|
|
126
|
+
};
|
|
127
|
+
const refund2 = {
|
|
128
|
+
id: "refund_unilateral",
|
|
129
|
+
recommended: false,
|
|
130
|
+
automation: "confirm",
|
|
131
|
+
reason: "Refund becomes available once the timelock passes.",
|
|
132
|
+
blockedBy: {
|
|
133
|
+
kind: "timelock_not_expired",
|
|
134
|
+
message: "The refund timelock has not passed yet."
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
return { recommended: "wait", actions: [wait2, refund2] };
|
|
138
|
+
}
|
|
107
139
|
const claimWindowClosed = serverChainNow >= serverRefundLocktime;
|
|
108
140
|
if (!claimWindowClosed) {
|
|
109
141
|
const claim = {
|
|
@@ -590,7 +622,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
590
622
|
return new _BitcoinContractManager(deps);
|
|
591
623
|
}
|
|
592
624
|
static async create(config) {
|
|
593
|
-
const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-
|
|
625
|
+
const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-B2BDEI6Z.js");
|
|
594
626
|
let reader = esploraReader2(
|
|
595
627
|
config.esploraUrl,
|
|
596
628
|
void 0,
|
|
@@ -598,7 +630,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
598
630
|
);
|
|
599
631
|
if (config.electrumWsUrl) {
|
|
600
632
|
const [{ electrumReader: electrumReader2 }, { ElectrumWsClient }] = await Promise.all([
|
|
601
|
-
import("./bitcoin-reader-electrum-
|
|
633
|
+
import("./bitcoin-reader-electrum-Z2YCNDLB.js"),
|
|
602
634
|
import("@lendasat/lendaswap-sdk-pure")
|
|
603
635
|
]);
|
|
604
636
|
reader = electrumReader2(new ElectrumWsClient(config.electrumWsUrl), {
|
|
@@ -782,7 +814,8 @@ function refQueryKey(ref) {
|
|
|
782
814
|
var AVG_BLOCK_MS = {
|
|
783
815
|
1: 12e3,
|
|
784
816
|
137: 2100,
|
|
785
|
-
42161: 250
|
|
817
|
+
42161: 250,
|
|
818
|
+
30: 3e4
|
|
786
819
|
};
|
|
787
820
|
var SCAN_MARGIN_BLOCKS = 1000n;
|
|
788
821
|
var EvmContractManager = class _EvmContractManager {
|
|
@@ -1048,7 +1081,9 @@ var DEFAULT_EVM_RPCS = {
|
|
|
1048
1081
|
"https://arb1.arbitrum.io/rpc",
|
|
1049
1082
|
"https://arbitrum.drpc.org",
|
|
1050
1083
|
"https://arbitrum-one-rpc.publicnode.com"
|
|
1051
|
-
]
|
|
1084
|
+
],
|
|
1085
|
+
// Rootstock: the public node, HTTP only (no WebSocket endpoint exists).
|
|
1086
|
+
30: ["https://public-node.rsk.co"]
|
|
1052
1087
|
};
|
|
1053
1088
|
var SWAP_CREATED = parseAbiItem(
|
|
1054
1089
|
"event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock, bytes32 key)"
|
|
@@ -1081,25 +1116,52 @@ function activeKey(q) {
|
|
|
1081
1116
|
}
|
|
1082
1117
|
});
|
|
1083
1118
|
}
|
|
1084
|
-
function evmReaderFromClient(client) {
|
|
1119
|
+
function evmReaderFromClient(client, options = {}) {
|
|
1120
|
+
const { maxBlockRange } = options;
|
|
1121
|
+
async function getLogs(filter, fromBlock) {
|
|
1122
|
+
if (maxBlockRange === void 0) {
|
|
1123
|
+
return client.request({
|
|
1124
|
+
method: "eth_getLogs",
|
|
1125
|
+
params: [{ ...filter, fromBlock: numberToHex(fromBlock) }]
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
const latest = (await client.getBlock()).number;
|
|
1129
|
+
if (latest === null) {
|
|
1130
|
+
throw new Error("latest block has no number");
|
|
1131
|
+
}
|
|
1132
|
+
const logs = [];
|
|
1133
|
+
for (let from = fromBlock; from <= latest; from += maxBlockRange) {
|
|
1134
|
+
const to = from + maxBlockRange - 1n < latest ? from + maxBlockRange - 1n : latest;
|
|
1135
|
+
logs.push(
|
|
1136
|
+
...await client.request({
|
|
1137
|
+
method: "eth_getLogs",
|
|
1138
|
+
params: [
|
|
1139
|
+
{
|
|
1140
|
+
...filter,
|
|
1141
|
+
fromBlock: numberToHex(from),
|
|
1142
|
+
toBlock: numberToHex(to)
|
|
1143
|
+
}
|
|
1144
|
+
]
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
return logs;
|
|
1149
|
+
}
|
|
1085
1150
|
return {
|
|
1086
1151
|
async getHtlcEventsBatch(queries, fromBlock = 0n) {
|
|
1087
1152
|
const results = /* @__PURE__ */ new Map();
|
|
1088
1153
|
if (queries.length === 0) return results;
|
|
1089
1154
|
for (const q of queries) results.set(htlcQueryKey(q), []);
|
|
1090
|
-
const logs = await
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
]
|
|
1102
|
-
});
|
|
1155
|
+
const logs = await getLogs(
|
|
1156
|
+
{
|
|
1157
|
+
address: unique(queries.map((q) => q.htlc)),
|
|
1158
|
+
topics: [
|
|
1159
|
+
HTLC_EVENT_TOPICS,
|
|
1160
|
+
unique(queries.map((q) => q.preimageHash))
|
|
1161
|
+
]
|
|
1162
|
+
},
|
|
1163
|
+
fromBlock
|
|
1164
|
+
);
|
|
1103
1165
|
const grouped = /* @__PURE__ */ new Map();
|
|
1104
1166
|
for (const log of logs) {
|
|
1105
1167
|
const decoded = tryDecode(log);
|
|
@@ -1215,12 +1277,15 @@ function toHtlcEvent(decoded, query) {
|
|
|
1215
1277
|
function unique(values) {
|
|
1216
1278
|
return [...new Set(values)];
|
|
1217
1279
|
}
|
|
1218
|
-
function createEvmRpcReader(rpcUrls) {
|
|
1280
|
+
function createEvmRpcReader(rpcUrls, options = {}) {
|
|
1219
1281
|
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
1220
1282
|
const transport = urls.length > 1 ? fallback(urls.map((url) => http(url))) : http(urls[0]);
|
|
1221
1283
|
const client = createPublicClient({ transport });
|
|
1222
|
-
return evmReaderFromClient(client);
|
|
1284
|
+
return evmReaderFromClient(client, options);
|
|
1223
1285
|
}
|
|
1286
|
+
var EVM_LOG_RANGE_LIMITS = {
|
|
1287
|
+
30: 2000n
|
|
1288
|
+
};
|
|
1224
1289
|
function defaultEvmReaders(overrides) {
|
|
1225
1290
|
const chainIds = /* @__PURE__ */ new Set([
|
|
1226
1291
|
...Object.keys(DEFAULT_EVM_RPCS).map(Number),
|
|
@@ -1231,7 +1296,13 @@ function defaultEvmReaders(overrides) {
|
|
|
1231
1296
|
const override = overrides?.[chainId];
|
|
1232
1297
|
const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
|
|
1233
1298
|
const urls = override ? [override, ...defaults] : defaults;
|
|
1234
|
-
if (urls.length > 0)
|
|
1299
|
+
if (urls.length > 0)
|
|
1300
|
+
readers.set(
|
|
1301
|
+
chainId,
|
|
1302
|
+
createEvmRpcReader(urls, {
|
|
1303
|
+
maxBlockRange: EVM_LOG_RANGE_LIMITS[chainId]
|
|
1304
|
+
})
|
|
1305
|
+
);
|
|
1235
1306
|
}
|
|
1236
1307
|
return readers;
|
|
1237
1308
|
}
|
|
@@ -1328,8 +1399,10 @@ var HintTracker = class {
|
|
|
1328
1399
|
serverChainNow: now,
|
|
1329
1400
|
clientRefundLocktime: swap.clientRefundLocktime,
|
|
1330
1401
|
serverRefundLocktime: swap.serverRefundLocktime,
|
|
1331
|
-
// Pay-on-Lightning swaps have no client-funded on-chain leg
|
|
1332
|
-
|
|
1402
|
+
// Pay-on-Lightning swaps have no client-funded on-chain leg;
|
|
1403
|
+
// receive-on-Lightning swaps have no server-funded one.
|
|
1404
|
+
clientFunds: swap.clientHtlc !== void 0,
|
|
1405
|
+
serverFunds: swap.serverHtlc !== void 0
|
|
1333
1406
|
});
|
|
1334
1407
|
const previous = this.#lastActions.get(swap.swapId);
|
|
1335
1408
|
if (!force && previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
@@ -1955,6 +2028,29 @@ function swapToTracked(stored) {
|
|
|
1955
2028
|
// no on-chain client leg
|
|
1956
2029
|
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
1957
2030
|
};
|
|
2031
|
+
// Client funds the EVM HTLC (evm_expected_sats of the BTC-pegged token);
|
|
2032
|
+
// the server pays the Lightning invoice off-chain and claims the HTLC —
|
|
2033
|
+
// no server leg to watch.
|
|
2034
|
+
case "evm_to_lightning":
|
|
2035
|
+
return {
|
|
2036
|
+
swapId: r.id,
|
|
2037
|
+
clientHtlc: evmLeg({
|
|
2038
|
+
chainId: r.evm_chain_id,
|
|
2039
|
+
htlc: r.evm_htlc_address,
|
|
2040
|
+
hashLock: r.hash_lock,
|
|
2041
|
+
claimAddress: r.server_evm_address,
|
|
2042
|
+
// the server claims the client's EVM HTLC
|
|
2043
|
+
expectedSats: r.evm_expected_sats,
|
|
2044
|
+
token: r.wbtc_address,
|
|
2045
|
+
sender: r.evm_coordinator_address,
|
|
2046
|
+
// the coordinator locks on the client's behalf
|
|
2047
|
+
timelockSec: r.evm_refund_locktime,
|
|
2048
|
+
createdAt: r.created_at
|
|
2049
|
+
}),
|
|
2050
|
+
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
2051
|
+
serverRefundLocktime: 0
|
|
2052
|
+
// no on-chain server leg
|
|
2053
|
+
};
|
|
1958
2054
|
// Client funds the Arkade VHTLC (source_amount); the server pays the
|
|
1959
2055
|
// Lightning invoice off-chain and claims the VHTLC — no server leg to
|
|
1960
2056
|
// watch.
|
|
@@ -1976,21 +2072,38 @@ function swapToTracked(stored) {
|
|
|
1976
2072
|
}
|
|
1977
2073
|
|
|
1978
2074
|
// src/tracker/swap-tracker.ts
|
|
2075
|
+
var CLAIM_SUBMITTED = /* @__PURE__ */ new Set([
|
|
2076
|
+
"clientredeeming",
|
|
2077
|
+
"clientredeemed",
|
|
2078
|
+
"serverredeemed"
|
|
2079
|
+
]);
|
|
2080
|
+
var DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS = 6e4;
|
|
2081
|
+
var RETRY_UNREGISTERED_INTERVAL_MS = 6e4;
|
|
1979
2082
|
var SwapTracker = class {
|
|
1980
2083
|
#managers;
|
|
1981
2084
|
#swaps = /* @__PURE__ */ new Map();
|
|
2085
|
+
/**
|
|
2086
|
+
* Swaps whose startup registration failed (a chain read that errored).
|
|
2087
|
+
* Retried every minute; dropping them would leave an active swap
|
|
2088
|
+
* unwatched for the whole session with no claim or refund surfaced.
|
|
2089
|
+
*/
|
|
2090
|
+
#unregistered = /* @__PURE__ */ new Map();
|
|
1982
2091
|
/** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
|
|
1983
2092
|
#lastActions = /* @__PURE__ */ new Map();
|
|
2093
|
+
/** The latest server status hint per swap, for {@link #withClaimHint}. */
|
|
2094
|
+
#hintStatuses = /* @__PURE__ */ new Map();
|
|
1984
2095
|
#subscribers = /* @__PURE__ */ new Set();
|
|
1985
2096
|
#refreshIntervalMs;
|
|
1986
2097
|
#atRiskReconcileIntervalMs;
|
|
1987
2098
|
#lastAtRiskReconcileAt = 0;
|
|
2099
|
+
#lastRetryAt = 0;
|
|
1988
2100
|
#eventUnsubs = [];
|
|
1989
2101
|
#timer;
|
|
1990
2102
|
constructor(managers, options) {
|
|
1991
2103
|
this.#managers = managers;
|
|
1992
2104
|
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
1993
|
-
|
|
2105
|
+
const atRisk = options?.atRiskReconcileIntervalMs ?? DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS;
|
|
2106
|
+
this.#atRiskReconcileIntervalMs = typeof atRisk === "function" ? atRisk : () => atRisk;
|
|
1994
2107
|
}
|
|
1995
2108
|
/**
|
|
1996
2109
|
* Whether every on-chain leg of `swap` is observable — i.e. the tracker can
|
|
@@ -2013,7 +2126,22 @@ var SwapTracker = class {
|
|
|
2013
2126
|
continue;
|
|
2014
2127
|
}
|
|
2015
2128
|
this.#swaps.set(swap.swapId, swap);
|
|
2016
|
-
|
|
2129
|
+
const registered = [];
|
|
2130
|
+
try {
|
|
2131
|
+
for (const leg of legsOf(swap)) {
|
|
2132
|
+
await this.#managerFor(leg).register(leg);
|
|
2133
|
+
registered.push(leg);
|
|
2134
|
+
}
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
this.#swaps.delete(swap.swapId);
|
|
2137
|
+
for (const leg of registered)
|
|
2138
|
+
void this.#managerFor(leg).unregister(leg);
|
|
2139
|
+
this.#unregistered.set(swap.swapId, swap);
|
|
2140
|
+
console.warn(
|
|
2141
|
+
`SwapTracker: swap ${swap.swapId} not tracked yet, registering its legs failed (will retry):`,
|
|
2142
|
+
error
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2017
2145
|
}
|
|
2018
2146
|
for (const manager of new Set(this.#managers.values())) {
|
|
2019
2147
|
this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
|
|
@@ -2021,6 +2149,7 @@ var SwapTracker = class {
|
|
|
2021
2149
|
await this.#refreshManagers();
|
|
2022
2150
|
this.#recomputeAll();
|
|
2023
2151
|
this.#lastAtRiskReconcileAt = Date.now();
|
|
2152
|
+
this.#lastRetryAt = this.#lastAtRiskReconcileAt;
|
|
2024
2153
|
if (this.#refreshIntervalMs > 0)
|
|
2025
2154
|
this.#timer = setInterval(
|
|
2026
2155
|
() => void this.#tick(),
|
|
@@ -2070,7 +2199,11 @@ var SwapTracker = class {
|
|
|
2070
2199
|
*/
|
|
2071
2200
|
async #tick() {
|
|
2072
2201
|
const now = Date.now();
|
|
2073
|
-
if (now - this.#
|
|
2202
|
+
if (now - this.#lastRetryAt >= RETRY_UNREGISTERED_INTERVAL_MS) {
|
|
2203
|
+
this.#lastRetryAt = now;
|
|
2204
|
+
await this.#retryUnregistered();
|
|
2205
|
+
}
|
|
2206
|
+
if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs()) {
|
|
2074
2207
|
this.#lastAtRiskReconcileAt = now;
|
|
2075
2208
|
const legs = [...this.#swaps.values()].filter((swap) => this.#isAtRisk(swap)).flatMap((swap) => legsOf(swap));
|
|
2076
2209
|
await Promise.all(
|
|
@@ -2083,6 +2216,25 @@ var SwapTracker = class {
|
|
|
2083
2216
|
}
|
|
2084
2217
|
this.#recomputeAll();
|
|
2085
2218
|
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Second attempt at swaps whose startup registration failed, on its own
|
|
2221
|
+
* cadence so a slow at-risk interval can't delay it. `track` is idempotent
|
|
2222
|
+
* and rolls back on failure, so a chain that is still down just leaves the
|
|
2223
|
+
* swap here for the next round.
|
|
2224
|
+
*/
|
|
2225
|
+
async #retryUnregistered() {
|
|
2226
|
+
for (const [swapId, swap] of this.#unregistered) {
|
|
2227
|
+
try {
|
|
2228
|
+
await this.track(swap);
|
|
2229
|
+
this.#unregistered.delete(swapId);
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
console.warn(
|
|
2232
|
+
`SwapTracker: retrying registration of ${swapId} failed:`,
|
|
2233
|
+
error
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2086
2238
|
/**
|
|
2087
2239
|
* Whether a swap needs chain-verified monitoring independent of the server:
|
|
2088
2240
|
* its client leg holds (or held) funds on-chain, so knowing when a refund
|
|
@@ -2142,6 +2294,7 @@ var SwapTracker = class {
|
|
|
2142
2294
|
async applyHint(swapId, opts) {
|
|
2143
2295
|
const swap = this.#swaps.get(swapId);
|
|
2144
2296
|
if (!swap) return;
|
|
2297
|
+
if (opts?.status !== void 0) this.#hintStatuses.set(swapId, opts.status);
|
|
2145
2298
|
const legs = legsOf(swap);
|
|
2146
2299
|
const results = await Promise.allSettled(
|
|
2147
2300
|
legs.map((leg) => this.#managerFor(leg).reconcile(leg))
|
|
@@ -2178,6 +2331,7 @@ var SwapTracker = class {
|
|
|
2178
2331
|
for (const leg of legsOf(swap))
|
|
2179
2332
|
void this.#managerFor(leg).unregister(leg);
|
|
2180
2333
|
this.#swaps.clear();
|
|
2334
|
+
this.#unregistered.clear();
|
|
2181
2335
|
this.#subscribers.clear();
|
|
2182
2336
|
}
|
|
2183
2337
|
#managerFor(ref) {
|
|
@@ -2189,6 +2343,28 @@ var SwapTracker = class {
|
|
|
2189
2343
|
#recomputeAll() {
|
|
2190
2344
|
for (const swap of this.#swaps.values()) this.#recompute(swap);
|
|
2191
2345
|
}
|
|
2346
|
+
/**
|
|
2347
|
+
* A server hint that the claim was submitted overrides a chain view that
|
|
2348
|
+
* still says "claim now". The claim is relayed by the server, so once it
|
|
2349
|
+
* reports the redeem in flight, another claim can only be refused; and a
|
|
2350
|
+
* chain that cannot be read (no `eth_getLogs` on the public Rootstock RPCs)
|
|
2351
|
+
* keeps its stale pre-claim observation for good. The chain still wins once
|
|
2352
|
+
* it moves: a fresh `spent_claim` derives past the hint on its own, and a
|
|
2353
|
+
* refund observed on chain is not overridden.
|
|
2354
|
+
*
|
|
2355
|
+
* The override lasts only while the server's claim window is open. Once the
|
|
2356
|
+
* server can refund its leg, the chain view is what protects the client: the
|
|
2357
|
+
* `serverfunded` rules never claim then and lead to the client's own refund
|
|
2358
|
+
* at its timelock. A claim that really landed shows up on chain long before
|
|
2359
|
+
* that; a hint that never did must not hide the refund.
|
|
2360
|
+
*/
|
|
2361
|
+
#withClaimHint(swap, derived, serverChainNow) {
|
|
2362
|
+
const hint = this.#hintStatuses.get(swap.swapId);
|
|
2363
|
+
if (hint === void 0 || !CLAIM_SUBMITTED.has(hint)) return derived;
|
|
2364
|
+
if (derived !== "serverfunded") return derived;
|
|
2365
|
+
const claimWindowOpen = serverChainNow < swap.serverRefundLocktime;
|
|
2366
|
+
return claimWindowOpen ? hint : derived;
|
|
2367
|
+
}
|
|
2192
2368
|
#recompute(swap, force = false) {
|
|
2193
2369
|
let clientHtlc;
|
|
2194
2370
|
let serverHtlc;
|
|
@@ -2208,8 +2384,9 @@ var SwapTracker = class {
|
|
|
2208
2384
|
if (serverHtlc === void 0 || now === void 0) return;
|
|
2209
2385
|
serverChainNow = now;
|
|
2210
2386
|
}
|
|
2211
|
-
const
|
|
2212
|
-
if (
|
|
2387
|
+
const derived = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
2388
|
+
if (derived === void 0) return;
|
|
2389
|
+
const status = this.#withClaimHint(swap, derived, serverChainNow);
|
|
2213
2390
|
const actions = {
|
|
2214
2391
|
...deriveSwapActions({
|
|
2215
2392
|
status,
|
|
@@ -2217,8 +2394,10 @@ var SwapTracker = class {
|
|
|
2217
2394
|
serverChainNow,
|
|
2218
2395
|
clientRefundLocktime: swap.clientRefundLocktime,
|
|
2219
2396
|
serverRefundLocktime: swap.serverRefundLocktime,
|
|
2220
|
-
// Pay-on-Lightning swaps have no client-funded on-chain leg
|
|
2221
|
-
|
|
2397
|
+
// Pay-on-Lightning swaps have no client-funded on-chain leg;
|
|
2398
|
+
// receive-on-Lightning swaps have no server-funded one.
|
|
2399
|
+
clientFunds: swap.clientHtlc !== void 0,
|
|
2400
|
+
serverFunds: swap.serverHtlc !== void 0
|
|
2222
2401
|
}),
|
|
2223
2402
|
// Expose the raw observations so consumers can render progress from
|
|
2224
2403
|
// chain facts (which leg is funded/spent) even when the server is
|
|
@@ -2258,12 +2437,16 @@ var Client = class {
|
|
|
2258
2437
|
#startPromise;
|
|
2259
2438
|
/** Swap ids whose settled status was already re-fetched this session. */
|
|
2260
2439
|
#settledSyncDone = /* @__PURE__ */ new Set();
|
|
2440
|
+
/** Read per chain read, so it can change without rebuilding the client. */
|
|
2441
|
+
#bitcoinMinConfirmations;
|
|
2261
2442
|
constructor(...args) {
|
|
2262
2443
|
if (args[0] instanceof LegacyClient) {
|
|
2263
2444
|
this.#legacy = args[0];
|
|
2264
2445
|
this.#tracking = args[1] ?? {
|
|
2265
2446
|
enabled: false
|
|
2266
2447
|
};
|
|
2448
|
+
assertMinConfirmations(this.#tracking.bitcoinMinConfirmations);
|
|
2449
|
+
this.#bitcoinMinConfirmations = this.#tracking.bitcoinMinConfirmations;
|
|
2267
2450
|
} else {
|
|
2268
2451
|
this.#legacy = new LegacyClient(
|
|
2269
2452
|
...args
|
|
@@ -2510,6 +2693,12 @@ var Client = class {
|
|
|
2510
2693
|
);
|
|
2511
2694
|
}
|
|
2512
2695
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
2696
|
+
createEvmToLightningSwap(...args) {
|
|
2697
|
+
return this.#trackAfterCreate(
|
|
2698
|
+
this.#legacy.createEvmToLightningSwap(...args)
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
2513
2702
|
createEvmToArkadeSwapGeneric(...args) {
|
|
2514
2703
|
return this.#trackAfterCreate(
|
|
2515
2704
|
this.#legacy.createEvmToArkadeSwapGeneric(...args)
|
|
@@ -2568,6 +2757,22 @@ var Client = class {
|
|
|
2568
2757
|
return this.#legacy.hasReceivedVtxo(...args);
|
|
2569
2758
|
}
|
|
2570
2759
|
// --- Satora-native features go below ---
|
|
2760
|
+
/** Confirmations a server-funded Bitcoin HTLC needs before it is claimable. */
|
|
2761
|
+
getBitcoinMinConfirmations() {
|
|
2762
|
+
return this.#bitcoinMinConfirmations;
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* At `0` the claim goes out against a mempool funding, trusting the funder
|
|
2766
|
+
* not to replace it once the claim reveals the preimage.
|
|
2767
|
+
*
|
|
2768
|
+
* Only the chain monitors read a depth, so this does nothing unless the
|
|
2769
|
+
* client was built with {@link ClientBuilder.withChainVerifiedTracking}.
|
|
2770
|
+
* Takes effect on subsequent reads, including swaps already tracked.
|
|
2771
|
+
*/
|
|
2772
|
+
setBitcoinMinConfirmations(minConfirmations) {
|
|
2773
|
+
assertMinConfirmations(minConfirmations);
|
|
2774
|
+
this.#bitcoinMinConfirmations = minConfirmations;
|
|
2775
|
+
}
|
|
2571
2776
|
/**
|
|
2572
2777
|
* Start observing the user's active swaps and deriving each one's next action.
|
|
2573
2778
|
*
|
|
@@ -2596,8 +2801,18 @@ var Client = class {
|
|
|
2596
2801
|
async #doStartTracking() {
|
|
2597
2802
|
try {
|
|
2598
2803
|
const chainVerified = this.#tracking.chainVerified || this.#tracking.managers !== void 0;
|
|
2804
|
+
if (!chainVerified && (this.#bitcoinMinConfirmations ?? 0) > 0)
|
|
2805
|
+
console.warn(
|
|
2806
|
+
"Client: bitcoinMinConfirmations is set but tracking follows server hints, which carry no depth. Build with ClientBuilder.withChainVerifiedTracking() for the depth to gate the claim."
|
|
2807
|
+
);
|
|
2599
2808
|
const tracker = chainVerified ? new SwapTracker(await this.#ensureManagers(), {
|
|
2600
|
-
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
2809
|
+
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3,
|
|
2810
|
+
// Depth is only re-derived on a chain read, and nothing pushes on a
|
|
2811
|
+
// new block (an Electrum scripthash status doesn't change once the
|
|
2812
|
+
// funding is in a block), so a deep policy would otherwise sit out
|
|
2813
|
+
// the default interval after the block that satisfied it. Read per
|
|
2814
|
+
// tick, like the depth itself, so a setter call after start counts.
|
|
2815
|
+
atRiskReconcileIntervalMs: () => (this.#bitcoinMinConfirmations ?? 0) > 1 ? 15e3 : DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS
|
|
2601
2816
|
}) : new HintTracker({
|
|
2602
2817
|
fetchStatus: async (swapId) => (await this.getSwap(swapId, { updateStorage: true })).status,
|
|
2603
2818
|
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
@@ -2783,8 +2998,7 @@ var Client = class {
|
|
|
2783
2998
|
evmRpcUrls,
|
|
2784
2999
|
esploraUrl,
|
|
2785
3000
|
electrumWsUrl,
|
|
2786
|
-
bitcoinNetwork
|
|
2787
|
-
bitcoinMinConfirmations
|
|
3001
|
+
bitcoinNetwork
|
|
2788
3002
|
} = this.#tracking;
|
|
2789
3003
|
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
2790
3004
|
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
@@ -2805,7 +3019,7 @@ var Client = class {
|
|
|
2805
3019
|
electrumWsUrl: electrumWsUrl ?? DEFAULT_ELECTRUM_WS_URLS[network],
|
|
2806
3020
|
network,
|
|
2807
3021
|
chainTime,
|
|
2808
|
-
minConfirmations: bitcoinMinConfirmations
|
|
3022
|
+
minConfirmations: () => this.#bitcoinMinConfirmations
|
|
2809
3023
|
})
|
|
2810
3024
|
);
|
|
2811
3025
|
this.#managers = managers;
|
|
@@ -2926,9 +3140,11 @@ var ClientBuilder = class {
|
|
|
2926
3140
|
* funding hits the mempool, which TRUSTS the funder not to double-spend it
|
|
2927
3141
|
* (claiming publishes the preimage, so a funder that then replaced its
|
|
2928
3142
|
* funding could take both legs). Set `1` (or more) for a block-depth policy
|
|
2929
|
-
* that doesn't rely on the funder's good behaviour.
|
|
3143
|
+
* that doesn't rely on the funder's good behaviour. Read only by the chain
|
|
3144
|
+
* monitors, so it needs {@link withChainVerifiedTracking} to have any effect.
|
|
2930
3145
|
*/
|
|
2931
3146
|
withBitcoinMinConfirmations(minConfirmations) {
|
|
3147
|
+
assertMinConfirmations(minConfirmations);
|
|
2932
3148
|
this.#bitcoinMinConfirmations = minConfirmations;
|
|
2933
3149
|
return this;
|
|
2934
3150
|
}
|
|
@@ -2985,6 +3201,13 @@ var ClientBuilder = class {
|
|
|
2985
3201
|
});
|
|
2986
3202
|
}
|
|
2987
3203
|
};
|
|
3204
|
+
function assertMinConfirmations(value) {
|
|
3205
|
+
if (value === void 0) return;
|
|
3206
|
+
if (!Number.isInteger(value) || value < 0)
|
|
3207
|
+
throw new Error(
|
|
3208
|
+
`bitcoinMinConfirmations must be a non-negative integer, got ${value}`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
2988
3211
|
function legacyBaseUrl(legacy) {
|
|
2989
3212
|
try {
|
|
2990
3213
|
return legacy.baseUrl;
|
|
@@ -3028,6 +3251,7 @@ export {
|
|
|
3028
3251
|
htlcFactsFromEsploraTxs,
|
|
3029
3252
|
htlcKey,
|
|
3030
3253
|
htlcQueryKey,
|
|
3254
|
+
resolveMinConfirmations,
|
|
3031
3255
|
swapToTracked
|
|
3032
3256
|
};
|
|
3033
3257
|
//# sourceMappingURL=index.js.map
|