@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.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
electrumReader
|
|
3
|
+
} from "./chunk-3ZMSZSAV.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";
|
|
@@ -621,7 +622,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
621
622
|
return new _BitcoinContractManager(deps);
|
|
622
623
|
}
|
|
623
624
|
static async create(config) {
|
|
624
|
-
const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-
|
|
625
|
+
const { esploraReader: esploraReader2 } = await import("./bitcoin-reader-esplora-B2BDEI6Z.js");
|
|
625
626
|
let reader = esploraReader2(
|
|
626
627
|
config.esploraUrl,
|
|
627
628
|
void 0,
|
|
@@ -629,7 +630,7 @@ var BitcoinContractManager = class _BitcoinContractManager {
|
|
|
629
630
|
);
|
|
630
631
|
if (config.electrumWsUrl) {
|
|
631
632
|
const [{ electrumReader: electrumReader2 }, { ElectrumWsClient }] = await Promise.all([
|
|
632
|
-
import("./bitcoin-reader-electrum-
|
|
633
|
+
import("./bitcoin-reader-electrum-KEI7VWW6.js"),
|
|
633
634
|
import("@lendasat/lendaswap-sdk-pure")
|
|
634
635
|
]);
|
|
635
636
|
reader = electrumReader2(new ElectrumWsClient(config.electrumWsUrl), {
|
|
@@ -813,7 +814,10 @@ function refQueryKey(ref) {
|
|
|
813
814
|
var AVG_BLOCK_MS = {
|
|
814
815
|
1: 12e3,
|
|
815
816
|
137: 2100,
|
|
816
|
-
42161: 250
|
|
817
|
+
42161: 250,
|
|
818
|
+
30: 3e4,
|
|
819
|
+
31: 3e4
|
|
820
|
+
// Rootstock testnet: same merge-mined cadence
|
|
817
821
|
};
|
|
818
822
|
var SCAN_MARGIN_BLOCKS = 1000n;
|
|
819
823
|
var EvmContractManager = class _EvmContractManager {
|
|
@@ -994,7 +998,10 @@ var EvmContractManager = class _EvmContractManager {
|
|
|
994
998
|
for (const ref of refs) {
|
|
995
999
|
const { observation, preimage } = evmObservation(
|
|
996
1000
|
events.get(refQueryKey(ref)) ?? [],
|
|
997
|
-
{
|
|
1001
|
+
{
|
|
1002
|
+
amount: ref.minAmount ?? ref.expectedAmount,
|
|
1003
|
+
token: ref.expectedToken
|
|
1004
|
+
}
|
|
998
1005
|
);
|
|
999
1006
|
const key = htlcKey(ref);
|
|
1000
1007
|
if (preimage) this.#preimages.set(key, preimage);
|
|
@@ -1079,7 +1086,10 @@ var DEFAULT_EVM_RPCS = {
|
|
|
1079
1086
|
"https://arb1.arbitrum.io/rpc",
|
|
1080
1087
|
"https://arbitrum.drpc.org",
|
|
1081
1088
|
"https://arbitrum-one-rpc.publicnode.com"
|
|
1082
|
-
]
|
|
1089
|
+
],
|
|
1090
|
+
// Rootstock: the public node, HTTP only (no WebSocket endpoint exists).
|
|
1091
|
+
30: ["https://public-node.rsk.co"],
|
|
1092
|
+
31: ["https://public-node.testnet.rsk.co"]
|
|
1083
1093
|
};
|
|
1084
1094
|
var SWAP_CREATED = parseAbiItem(
|
|
1085
1095
|
"event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock, bytes32 key)"
|
|
@@ -1112,25 +1122,52 @@ function activeKey(q) {
|
|
|
1112
1122
|
}
|
|
1113
1123
|
});
|
|
1114
1124
|
}
|
|
1115
|
-
function evmReaderFromClient(client) {
|
|
1125
|
+
function evmReaderFromClient(client, options = {}) {
|
|
1126
|
+
const { maxBlockRange } = options;
|
|
1127
|
+
async function getLogs(filter, fromBlock) {
|
|
1128
|
+
if (maxBlockRange === void 0) {
|
|
1129
|
+
return client.request({
|
|
1130
|
+
method: "eth_getLogs",
|
|
1131
|
+
params: [{ ...filter, fromBlock: numberToHex(fromBlock) }]
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
const latest = (await client.getBlock()).number;
|
|
1135
|
+
if (latest === null) {
|
|
1136
|
+
throw new Error("latest block has no number");
|
|
1137
|
+
}
|
|
1138
|
+
const logs = [];
|
|
1139
|
+
for (let from = fromBlock; from <= latest; from += maxBlockRange) {
|
|
1140
|
+
const to = from + maxBlockRange - 1n < latest ? from + maxBlockRange - 1n : latest;
|
|
1141
|
+
logs.push(
|
|
1142
|
+
...await client.request({
|
|
1143
|
+
method: "eth_getLogs",
|
|
1144
|
+
params: [
|
|
1145
|
+
{
|
|
1146
|
+
...filter,
|
|
1147
|
+
fromBlock: numberToHex(from),
|
|
1148
|
+
toBlock: numberToHex(to)
|
|
1149
|
+
}
|
|
1150
|
+
]
|
|
1151
|
+
})
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
return logs;
|
|
1155
|
+
}
|
|
1116
1156
|
return {
|
|
1117
1157
|
async getHtlcEventsBatch(queries, fromBlock = 0n) {
|
|
1118
1158
|
const results = /* @__PURE__ */ new Map();
|
|
1119
1159
|
if (queries.length === 0) return results;
|
|
1120
1160
|
for (const q of queries) results.set(htlcQueryKey(q), []);
|
|
1121
|
-
const logs = await
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
]
|
|
1133
|
-
});
|
|
1161
|
+
const logs = await getLogs(
|
|
1162
|
+
{
|
|
1163
|
+
address: unique(queries.map((q) => q.htlc)),
|
|
1164
|
+
topics: [
|
|
1165
|
+
HTLC_EVENT_TOPICS,
|
|
1166
|
+
unique(queries.map((q) => q.preimageHash))
|
|
1167
|
+
]
|
|
1168
|
+
},
|
|
1169
|
+
fromBlock
|
|
1170
|
+
);
|
|
1134
1171
|
const grouped = /* @__PURE__ */ new Map();
|
|
1135
1172
|
for (const log of logs) {
|
|
1136
1173
|
const decoded = tryDecode(log);
|
|
@@ -1246,12 +1283,16 @@ function toHtlcEvent(decoded, query) {
|
|
|
1246
1283
|
function unique(values) {
|
|
1247
1284
|
return [...new Set(values)];
|
|
1248
1285
|
}
|
|
1249
|
-
function createEvmRpcReader(rpcUrls) {
|
|
1286
|
+
function createEvmRpcReader(rpcUrls, options = {}) {
|
|
1250
1287
|
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
1251
1288
|
const transport = urls.length > 1 ? fallback(urls.map((url) => http(url))) : http(urls[0]);
|
|
1252
1289
|
const client = createPublicClient({ transport });
|
|
1253
|
-
return evmReaderFromClient(client);
|
|
1290
|
+
return evmReaderFromClient(client, options);
|
|
1254
1291
|
}
|
|
1292
|
+
var EVM_LOG_RANGE_LIMITS = {
|
|
1293
|
+
30: 2000n,
|
|
1294
|
+
31: 2000n
|
|
1295
|
+
};
|
|
1255
1296
|
function defaultEvmReaders(overrides) {
|
|
1256
1297
|
const chainIds = /* @__PURE__ */ new Set([
|
|
1257
1298
|
...Object.keys(DEFAULT_EVM_RPCS).map(Number),
|
|
@@ -1262,7 +1303,13 @@ function defaultEvmReaders(overrides) {
|
|
|
1262
1303
|
const override = overrides?.[chainId];
|
|
1263
1304
|
const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
|
|
1264
1305
|
const urls = override ? [override, ...defaults] : defaults;
|
|
1265
|
-
if (urls.length > 0)
|
|
1306
|
+
if (urls.length > 0)
|
|
1307
|
+
readers.set(
|
|
1308
|
+
chainId,
|
|
1309
|
+
createEvmRpcReader(urls, {
|
|
1310
|
+
maxBlockRange: EVM_LOG_RANGE_LIMITS[chainId]
|
|
1311
|
+
})
|
|
1312
|
+
);
|
|
1266
1313
|
}
|
|
1267
1314
|
return readers;
|
|
1268
1315
|
}
|
|
@@ -1717,6 +1764,11 @@ function defaultWebSocketFactory(url) {
|
|
|
1717
1764
|
return new WS(url);
|
|
1718
1765
|
}
|
|
1719
1766
|
|
|
1767
|
+
// src/tracker/from-swap.ts
|
|
1768
|
+
import {
|
|
1769
|
+
NATIVE_TOKEN_ADDRESS
|
|
1770
|
+
} from "@lendasat/lendaswap-sdk-pure";
|
|
1771
|
+
|
|
1720
1772
|
// src/contracts/arkade-vhtlc.ts
|
|
1721
1773
|
import { VHTLCContractHandler } from "@arkade-os/sdk";
|
|
1722
1774
|
import {
|
|
@@ -1805,6 +1857,7 @@ function evmLeg(args) {
|
|
|
1805
1857
|
preimageHash: ensure0x(args.hashLock),
|
|
1806
1858
|
claimAddress: ensure0x(args.claimAddress),
|
|
1807
1859
|
expectedAmount: BigInt(args.expectedSats),
|
|
1860
|
+
minAmount: args.minAmount,
|
|
1808
1861
|
expectedToken: args.token ? ensure0x(args.token) : void 0,
|
|
1809
1862
|
sender: args.sender ? ensure0x(args.sender) : void 0,
|
|
1810
1863
|
timelockSec: args.timelockSec,
|
|
@@ -1812,6 +1865,13 @@ function evmLeg(args) {
|
|
|
1812
1865
|
};
|
|
1813
1866
|
}
|
|
1814
1867
|
var CLIENT_FUNDING_MIN_CONFIRMATIONS = 1;
|
|
1868
|
+
var FEE_ROUNDING_SLACK_SATS = 2;
|
|
1869
|
+
function minServerPayout(r) {
|
|
1870
|
+
return Math.max(
|
|
1871
|
+
0,
|
|
1872
|
+
Number(r.target_amount) - r.fee_sats - FEE_ROUNDING_SLACK_SATS
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1815
1875
|
function bitcoinLeg(address, hashLock, expectedSats, minConfirmations) {
|
|
1816
1876
|
return {
|
|
1817
1877
|
ledger: "bitcoin",
|
|
@@ -1856,15 +1916,19 @@ function swapToTracked(stored) {
|
|
|
1856
1916
|
claimAddress: r.server_evm_address,
|
|
1857
1917
|
// the server claims the client's EVM HTLC
|
|
1858
1918
|
expectedSats: r.evm_expected_sats,
|
|
1859
|
-
|
|
1860
|
-
//
|
|
1861
|
-
token
|
|
1862
|
-
|
|
1863
|
-
// the
|
|
1919
|
+
minAmount: 0n,
|
|
1920
|
+
// see SHORT_LOCK_RULE
|
|
1921
|
+
// A native lock's asset word is zero; an ERC20 lock's token is not
|
|
1922
|
+
// on this response yet, so fall back to the per-chain mainnet
|
|
1923
|
+
// constant to complete the isActive tuple.
|
|
1924
|
+
token: r.evm_htlc_kind === "native" ? NATIVE_TOKEN_ADDRESS : lockedTokenFallback(r.evm_chain_id, r.network),
|
|
1925
|
+
// The coordinator created the HTLC on the client's behalf, so it
|
|
1926
|
+
// is the lock's sender (refund address), as on evm_to_lightning.
|
|
1927
|
+
sender: r.evm_coordinator_address,
|
|
1864
1928
|
timelockSec: r.evm_refund_locktime,
|
|
1865
1929
|
createdAt: r.created_at
|
|
1866
1930
|
}),
|
|
1867
|
-
serverHtlc: arkadeLeg(r, r.btc_vhtlc_address,
|
|
1931
|
+
serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, minServerPayout(r)),
|
|
1868
1932
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1869
1933
|
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
1870
1934
|
};
|
|
@@ -1905,16 +1969,19 @@ function swapToTracked(stored) {
|
|
|
1905
1969
|
claimAddress: r.server_evm_address,
|
|
1906
1970
|
// the server claims the client's EVM HTLC
|
|
1907
1971
|
expectedSats: r.evm_expected_sats,
|
|
1972
|
+
minAmount: 0n,
|
|
1973
|
+
// see SHORT_LOCK_RULE
|
|
1908
1974
|
token: r.wbtc_address,
|
|
1909
|
-
|
|
1910
|
-
// the
|
|
1975
|
+
// The coordinator created the HTLC on the client's behalf, so it
|
|
1976
|
+
// is the lock's sender (refund address).
|
|
1977
|
+
sender: r.evm_coordinator_address,
|
|
1911
1978
|
timelockSec: r.evm_refund_locktime,
|
|
1912
1979
|
createdAt: r.created_at
|
|
1913
1980
|
}),
|
|
1914
1981
|
serverHtlc: bitcoinLeg(
|
|
1915
1982
|
r.btc_htlc_address,
|
|
1916
1983
|
r.evm_hash_lock,
|
|
1917
|
-
|
|
1984
|
+
minServerPayout(r)
|
|
1918
1985
|
),
|
|
1919
1986
|
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1920
1987
|
serverRefundLocktime: ms(r.btc_refund_locktime)
|
|
@@ -2032,21 +2099,38 @@ function swapToTracked(stored) {
|
|
|
2032
2099
|
}
|
|
2033
2100
|
|
|
2034
2101
|
// src/tracker/swap-tracker.ts
|
|
2102
|
+
var CLAIM_SUBMITTED = /* @__PURE__ */ new Set([
|
|
2103
|
+
"clientredeeming",
|
|
2104
|
+
"clientredeemed",
|
|
2105
|
+
"serverredeemed"
|
|
2106
|
+
]);
|
|
2107
|
+
var DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS = 6e4;
|
|
2108
|
+
var RETRY_UNREGISTERED_INTERVAL_MS = 6e4;
|
|
2035
2109
|
var SwapTracker = class {
|
|
2036
2110
|
#managers;
|
|
2037
2111
|
#swaps = /* @__PURE__ */ new Map();
|
|
2112
|
+
/**
|
|
2113
|
+
* Swaps whose startup registration failed (a chain read that errored).
|
|
2114
|
+
* Retried every minute; dropping them would leave an active swap
|
|
2115
|
+
* unwatched for the whole session with no claim or refund surfaced.
|
|
2116
|
+
*/
|
|
2117
|
+
#unregistered = /* @__PURE__ */ new Map();
|
|
2038
2118
|
/** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
|
|
2039
2119
|
#lastActions = /* @__PURE__ */ new Map();
|
|
2120
|
+
/** The latest server status hint per swap, for {@link #withClaimHint}. */
|
|
2121
|
+
#hintStatuses = /* @__PURE__ */ new Map();
|
|
2040
2122
|
#subscribers = /* @__PURE__ */ new Set();
|
|
2041
2123
|
#refreshIntervalMs;
|
|
2042
2124
|
#atRiskReconcileIntervalMs;
|
|
2043
2125
|
#lastAtRiskReconcileAt = 0;
|
|
2126
|
+
#lastRetryAt = 0;
|
|
2044
2127
|
#eventUnsubs = [];
|
|
2045
2128
|
#timer;
|
|
2046
2129
|
constructor(managers, options) {
|
|
2047
2130
|
this.#managers = managers;
|
|
2048
2131
|
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
2049
|
-
|
|
2132
|
+
const atRisk = options?.atRiskReconcileIntervalMs ?? DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS;
|
|
2133
|
+
this.#atRiskReconcileIntervalMs = typeof atRisk === "function" ? atRisk : () => atRisk;
|
|
2050
2134
|
}
|
|
2051
2135
|
/**
|
|
2052
2136
|
* Whether every on-chain leg of `swap` is observable — i.e. the tracker can
|
|
@@ -2069,7 +2153,22 @@ var SwapTracker = class {
|
|
|
2069
2153
|
continue;
|
|
2070
2154
|
}
|
|
2071
2155
|
this.#swaps.set(swap.swapId, swap);
|
|
2072
|
-
|
|
2156
|
+
const registered = [];
|
|
2157
|
+
try {
|
|
2158
|
+
for (const leg of legsOf(swap)) {
|
|
2159
|
+
await this.#managerFor(leg).register(leg);
|
|
2160
|
+
registered.push(leg);
|
|
2161
|
+
}
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
this.#swaps.delete(swap.swapId);
|
|
2164
|
+
for (const leg of registered)
|
|
2165
|
+
void this.#managerFor(leg).unregister(leg);
|
|
2166
|
+
this.#unregistered.set(swap.swapId, swap);
|
|
2167
|
+
console.warn(
|
|
2168
|
+
`SwapTracker: swap ${swap.swapId} not tracked yet, registering its legs failed (will retry):`,
|
|
2169
|
+
error
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2073
2172
|
}
|
|
2074
2173
|
for (const manager of new Set(this.#managers.values())) {
|
|
2075
2174
|
this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
|
|
@@ -2077,6 +2176,7 @@ var SwapTracker = class {
|
|
|
2077
2176
|
await this.#refreshManagers();
|
|
2078
2177
|
this.#recomputeAll();
|
|
2079
2178
|
this.#lastAtRiskReconcileAt = Date.now();
|
|
2179
|
+
this.#lastRetryAt = this.#lastAtRiskReconcileAt;
|
|
2080
2180
|
if (this.#refreshIntervalMs > 0)
|
|
2081
2181
|
this.#timer = setInterval(
|
|
2082
2182
|
() => void this.#tick(),
|
|
@@ -2126,7 +2226,11 @@ var SwapTracker = class {
|
|
|
2126
2226
|
*/
|
|
2127
2227
|
async #tick() {
|
|
2128
2228
|
const now = Date.now();
|
|
2129
|
-
if (now - this.#
|
|
2229
|
+
if (now - this.#lastRetryAt >= RETRY_UNREGISTERED_INTERVAL_MS) {
|
|
2230
|
+
this.#lastRetryAt = now;
|
|
2231
|
+
await this.#retryUnregistered();
|
|
2232
|
+
}
|
|
2233
|
+
if (now - this.#lastAtRiskReconcileAt >= this.#atRiskReconcileIntervalMs()) {
|
|
2130
2234
|
this.#lastAtRiskReconcileAt = now;
|
|
2131
2235
|
const legs = [...this.#swaps.values()].filter((swap) => this.#isAtRisk(swap)).flatMap((swap) => legsOf(swap));
|
|
2132
2236
|
await Promise.all(
|
|
@@ -2139,6 +2243,25 @@ var SwapTracker = class {
|
|
|
2139
2243
|
}
|
|
2140
2244
|
this.#recomputeAll();
|
|
2141
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Second attempt at swaps whose startup registration failed, on its own
|
|
2248
|
+
* cadence so a slow at-risk interval can't delay it. `track` is idempotent
|
|
2249
|
+
* and rolls back on failure, so a chain that is still down just leaves the
|
|
2250
|
+
* swap here for the next round.
|
|
2251
|
+
*/
|
|
2252
|
+
async #retryUnregistered() {
|
|
2253
|
+
for (const [swapId, swap] of this.#unregistered) {
|
|
2254
|
+
try {
|
|
2255
|
+
await this.track(swap);
|
|
2256
|
+
this.#unregistered.delete(swapId);
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
console.warn(
|
|
2259
|
+
`SwapTracker: retrying registration of ${swapId} failed:`,
|
|
2260
|
+
error
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2142
2265
|
/**
|
|
2143
2266
|
* Whether a swap needs chain-verified monitoring independent of the server:
|
|
2144
2267
|
* its client leg holds (or held) funds on-chain, so knowing when a refund
|
|
@@ -2198,6 +2321,7 @@ var SwapTracker = class {
|
|
|
2198
2321
|
async applyHint(swapId, opts) {
|
|
2199
2322
|
const swap = this.#swaps.get(swapId);
|
|
2200
2323
|
if (!swap) return;
|
|
2324
|
+
if (opts?.status !== void 0) this.#hintStatuses.set(swapId, opts.status);
|
|
2201
2325
|
const legs = legsOf(swap);
|
|
2202
2326
|
const results = await Promise.allSettled(
|
|
2203
2327
|
legs.map((leg) => this.#managerFor(leg).reconcile(leg))
|
|
@@ -2234,6 +2358,7 @@ var SwapTracker = class {
|
|
|
2234
2358
|
for (const leg of legsOf(swap))
|
|
2235
2359
|
void this.#managerFor(leg).unregister(leg);
|
|
2236
2360
|
this.#swaps.clear();
|
|
2361
|
+
this.#unregistered.clear();
|
|
2237
2362
|
this.#subscribers.clear();
|
|
2238
2363
|
}
|
|
2239
2364
|
#managerFor(ref) {
|
|
@@ -2245,6 +2370,28 @@ var SwapTracker = class {
|
|
|
2245
2370
|
#recomputeAll() {
|
|
2246
2371
|
for (const swap of this.#swaps.values()) this.#recompute(swap);
|
|
2247
2372
|
}
|
|
2373
|
+
/**
|
|
2374
|
+
* A server hint that the claim was submitted overrides a chain view that
|
|
2375
|
+
* still says "claim now". The claim is relayed by the server, so once it
|
|
2376
|
+
* reports the redeem in flight, another claim can only be refused; and a
|
|
2377
|
+
* chain that cannot be read (no `eth_getLogs` on the public Rootstock RPCs)
|
|
2378
|
+
* keeps its stale pre-claim observation for good. The chain still wins once
|
|
2379
|
+
* it moves: a fresh `spent_claim` derives past the hint on its own, and a
|
|
2380
|
+
* refund observed on chain is not overridden.
|
|
2381
|
+
*
|
|
2382
|
+
* The override lasts only while the server's claim window is open. Once the
|
|
2383
|
+
* server can refund its leg, the chain view is what protects the client: the
|
|
2384
|
+
* `serverfunded` rules never claim then and lead to the client's own refund
|
|
2385
|
+
* at its timelock. A claim that really landed shows up on chain long before
|
|
2386
|
+
* that; a hint that never did must not hide the refund.
|
|
2387
|
+
*/
|
|
2388
|
+
#withClaimHint(swap, derived, serverChainNow) {
|
|
2389
|
+
const hint = this.#hintStatuses.get(swap.swapId);
|
|
2390
|
+
if (hint === void 0 || !CLAIM_SUBMITTED.has(hint)) return derived;
|
|
2391
|
+
if (derived !== "serverfunded") return derived;
|
|
2392
|
+
const claimWindowOpen = serverChainNow < swap.serverRefundLocktime;
|
|
2393
|
+
return claimWindowOpen ? hint : derived;
|
|
2394
|
+
}
|
|
2248
2395
|
#recompute(swap, force = false) {
|
|
2249
2396
|
let clientHtlc;
|
|
2250
2397
|
let serverHtlc;
|
|
@@ -2264,8 +2411,9 @@ var SwapTracker = class {
|
|
|
2264
2411
|
if (serverHtlc === void 0 || now === void 0) return;
|
|
2265
2412
|
serverChainNow = now;
|
|
2266
2413
|
}
|
|
2267
|
-
const
|
|
2268
|
-
if (
|
|
2414
|
+
const derived = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
2415
|
+
if (derived === void 0) return;
|
|
2416
|
+
const status = this.#withClaimHint(swap, derived, serverChainNow);
|
|
2269
2417
|
const actions = {
|
|
2270
2418
|
...deriveSwapActions({
|
|
2271
2419
|
status,
|
|
@@ -2316,12 +2464,16 @@ var Client = class {
|
|
|
2316
2464
|
#startPromise;
|
|
2317
2465
|
/** Swap ids whose settled status was already re-fetched this session. */
|
|
2318
2466
|
#settledSyncDone = /* @__PURE__ */ new Set();
|
|
2467
|
+
/** Read per chain read, so it can change without rebuilding the client. */
|
|
2468
|
+
#bitcoinMinConfirmations;
|
|
2319
2469
|
constructor(...args) {
|
|
2320
2470
|
if (args[0] instanceof LegacyClient) {
|
|
2321
2471
|
this.#legacy = args[0];
|
|
2322
2472
|
this.#tracking = args[1] ?? {
|
|
2323
2473
|
enabled: false
|
|
2324
2474
|
};
|
|
2475
|
+
assertMinConfirmations(this.#tracking.bitcoinMinConfirmations);
|
|
2476
|
+
this.#bitcoinMinConfirmations = this.#tracking.bitcoinMinConfirmations;
|
|
2325
2477
|
} else {
|
|
2326
2478
|
this.#legacy = new LegacyClient(
|
|
2327
2479
|
...args
|
|
@@ -2504,6 +2656,10 @@ var Client = class {
|
|
|
2504
2656
|
return this.#legacy.claim(...args);
|
|
2505
2657
|
}
|
|
2506
2658
|
/** Delegated to the legacy client (migration checkpoint). */
|
|
2659
|
+
claimEvmWithSigner(...args) {
|
|
2660
|
+
return this.#legacy.claimEvmWithSigner(...args);
|
|
2661
|
+
}
|
|
2662
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
2507
2663
|
claimViaGasless(...args) {
|
|
2508
2664
|
return this.#legacy.claimViaGasless(...args);
|
|
2509
2665
|
}
|
|
@@ -2632,6 +2788,22 @@ var Client = class {
|
|
|
2632
2788
|
return this.#legacy.hasReceivedVtxo(...args);
|
|
2633
2789
|
}
|
|
2634
2790
|
// --- Satora-native features go below ---
|
|
2791
|
+
/** Confirmations a server-funded Bitcoin HTLC needs before it is claimable. */
|
|
2792
|
+
getBitcoinMinConfirmations() {
|
|
2793
|
+
return this.#bitcoinMinConfirmations;
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* At `0` the claim goes out against a mempool funding, trusting the funder
|
|
2797
|
+
* not to replace it once the claim reveals the preimage.
|
|
2798
|
+
*
|
|
2799
|
+
* Only the chain monitors read a depth, so this does nothing unless the
|
|
2800
|
+
* client was built with {@link ClientBuilder.withChainVerifiedTracking}.
|
|
2801
|
+
* Takes effect on subsequent reads, including swaps already tracked.
|
|
2802
|
+
*/
|
|
2803
|
+
setBitcoinMinConfirmations(minConfirmations) {
|
|
2804
|
+
assertMinConfirmations(minConfirmations);
|
|
2805
|
+
this.#bitcoinMinConfirmations = minConfirmations;
|
|
2806
|
+
}
|
|
2635
2807
|
/**
|
|
2636
2808
|
* Start observing the user's active swaps and deriving each one's next action.
|
|
2637
2809
|
*
|
|
@@ -2660,8 +2832,18 @@ var Client = class {
|
|
|
2660
2832
|
async #doStartTracking() {
|
|
2661
2833
|
try {
|
|
2662
2834
|
const chainVerified = this.#tracking.chainVerified || this.#tracking.managers !== void 0;
|
|
2835
|
+
if (!chainVerified && (this.#bitcoinMinConfirmations ?? 0) > 0)
|
|
2836
|
+
console.warn(
|
|
2837
|
+
"Client: bitcoinMinConfirmations is set but tracking follows server hints, which carry no depth. Build with ClientBuilder.withChainVerifiedTracking() for the depth to gate the claim."
|
|
2838
|
+
);
|
|
2663
2839
|
const tracker = chainVerified ? new SwapTracker(await this.#ensureManagers(), {
|
|
2664
|
-
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
2840
|
+
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3,
|
|
2841
|
+
// Depth is only re-derived on a chain read, and nothing pushes on a
|
|
2842
|
+
// new block (an Electrum scripthash status doesn't change once the
|
|
2843
|
+
// funding is in a block), so a deep policy would otherwise sit out
|
|
2844
|
+
// the default interval after the block that satisfied it. Read per
|
|
2845
|
+
// tick, like the depth itself, so a setter call after start counts.
|
|
2846
|
+
atRiskReconcileIntervalMs: () => (this.#bitcoinMinConfirmations ?? 0) > 1 ? 15e3 : DEFAULT_AT_RISK_RECONCILE_INTERVAL_MS
|
|
2665
2847
|
}) : new HintTracker({
|
|
2666
2848
|
fetchStatus: async (swapId) => (await this.getSwap(swapId, { updateStorage: true })).status,
|
|
2667
2849
|
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
@@ -2847,8 +3029,7 @@ var Client = class {
|
|
|
2847
3029
|
evmRpcUrls,
|
|
2848
3030
|
esploraUrl,
|
|
2849
3031
|
electrumWsUrl,
|
|
2850
|
-
bitcoinNetwork
|
|
2851
|
-
bitcoinMinConfirmations
|
|
3032
|
+
bitcoinNetwork
|
|
2852
3033
|
} = this.#tracking;
|
|
2853
3034
|
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
2854
3035
|
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
@@ -2869,7 +3050,7 @@ var Client = class {
|
|
|
2869
3050
|
electrumWsUrl: electrumWsUrl ?? DEFAULT_ELECTRUM_WS_URLS[network],
|
|
2870
3051
|
network,
|
|
2871
3052
|
chainTime,
|
|
2872
|
-
minConfirmations: bitcoinMinConfirmations
|
|
3053
|
+
minConfirmations: () => this.#bitcoinMinConfirmations
|
|
2873
3054
|
})
|
|
2874
3055
|
);
|
|
2875
3056
|
this.#managers = managers;
|
|
@@ -2990,9 +3171,11 @@ var ClientBuilder = class {
|
|
|
2990
3171
|
* funding hits the mempool, which TRUSTS the funder not to double-spend it
|
|
2991
3172
|
* (claiming publishes the preimage, so a funder that then replaced its
|
|
2992
3173
|
* funding could take both legs). Set `1` (or more) for a block-depth policy
|
|
2993
|
-
* that doesn't rely on the funder's good behaviour.
|
|
3174
|
+
* that doesn't rely on the funder's good behaviour. Read only by the chain
|
|
3175
|
+
* monitors, so it needs {@link withChainVerifiedTracking} to have any effect.
|
|
2994
3176
|
*/
|
|
2995
3177
|
withBitcoinMinConfirmations(minConfirmations) {
|
|
3178
|
+
assertMinConfirmations(minConfirmations);
|
|
2996
3179
|
this.#bitcoinMinConfirmations = minConfirmations;
|
|
2997
3180
|
return this;
|
|
2998
3181
|
}
|
|
@@ -3049,6 +3232,13 @@ var ClientBuilder = class {
|
|
|
3049
3232
|
});
|
|
3050
3233
|
}
|
|
3051
3234
|
};
|
|
3235
|
+
function assertMinConfirmations(value) {
|
|
3236
|
+
if (value === void 0) return;
|
|
3237
|
+
if (!Number.isInteger(value) || value < 0)
|
|
3238
|
+
throw new Error(
|
|
3239
|
+
`bitcoinMinConfirmations must be a non-negative integer, got ${value}`
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3052
3242
|
function legacyBaseUrl(legacy) {
|
|
3053
3243
|
try {
|
|
3054
3244
|
return legacy.baseUrl;
|
|
@@ -3092,6 +3282,7 @@ export {
|
|
|
3092
3282
|
htlcFactsFromEsploraTxs,
|
|
3093
3283
|
htlcKey,
|
|
3094
3284
|
htlcQueryKey,
|
|
3285
|
+
resolveMinConfirmations,
|
|
3095
3286
|
swapToTracked
|
|
3096
3287
|
};
|
|
3097
3288
|
//# sourceMappingURL=index.js.map
|