@hyperbridge/simplex 0.17.0 → 0.18.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 +85 -151
- package/dist/bin/simplex.js +7769 -1943
- package/dist/bin/simplex.js.map +1 -1
- package/dist/error-reporting.cjs +123 -0
- package/dist/error-reporting.cjs.map +1 -0
- package/dist/error-reporting.d.cts +46 -0
- package/dist/error-reporting.d.ts +46 -0
- package/dist/error-reporting.js +95 -0
- package/dist/error-reporting.js.map +1 -0
- package/dist/index.cjs +1364 -557
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +311 -1896
- package/dist/index.d.ts +311 -1896
- package/dist/index.js +1362 -523
- package/dist/index.js.map +1 -1
- package/dist/sqlite.cjs +1 -0
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +7 -1
- package/dist/sqlite.d.ts +7 -1
- package/dist/sqlite.js +1 -0
- package/dist/sqlite.js.map +1 -1
- package/dist/types-6MEltdkX.d.cts +2297 -0
- package/dist/types-6MEltdkX.d.ts +2297 -0
- package/dist/ui/assets/bridge-BB9cnjYo.png +0 -0
- package/dist/ui/assets/hyperbridge-B8MVl-Oj.png +0 -0
- package/dist/ui/assets/index-DWJcHNgO.css +1 -0
- package/dist/ui/assets/index-Dy5dPCr_.js +2168 -0
- package/dist/ui/assets/usdr-gm4f6KeA.png +0 -0
- package/dist/ui/index.html +2 -2
- package/package.json +19 -4
- package/dist/types-BPyUX8qu.d.cts +0 -663
- package/dist/types-BPyUX8qu.d.ts +0 -663
- package/dist/ui/assets/index-CK8E6aPx.css +0 -1
- package/dist/ui/assets/index-D8DLeEH0.js +0 -106
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import { createRequire } from 'module'; const require = createRequire(import.met
|
|
|
4
4
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
5
5
|
|
|
6
6
|
// src/core/boot.ts
|
|
7
|
-
import {
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { formatUnits as formatUnits7, toHex as toHex5 } from "viem";
|
|
8
9
|
|
|
9
10
|
// src/core/filler.ts
|
|
10
11
|
import { keccakAsU8a } from "@polkadot/util-crypto";
|
|
@@ -563,6 +564,20 @@ var ConfirmationPolicy = class {
|
|
|
563
564
|
// src/core/filler.ts
|
|
564
565
|
import pQueue from "p-queue";
|
|
565
566
|
|
|
567
|
+
// src/config/bundlers.ts
|
|
568
|
+
var HYPERBRIDGE_BUNDLER_URLS = {
|
|
569
|
+
1: "https://bundler.polytope.technology/ethereum",
|
|
570
|
+
56: "https://bundler.polytope.technology/bsc",
|
|
571
|
+
97: "https://bundler.polytope.technology/bsc-chapel",
|
|
572
|
+
137: "https://bundler.polytope.technology/polygon",
|
|
573
|
+
8453: "https://bundler.polytope.technology/base",
|
|
574
|
+
42161: "https://bundler.polytope.technology/arbitrum",
|
|
575
|
+
80002: "https://bundler.polytope.technology/polygon-amoy"
|
|
576
|
+
};
|
|
577
|
+
function hyperbridgeBundlerUrl(chainId) {
|
|
578
|
+
return HYPERBRIDGE_BUNDLER_URLS[chainId];
|
|
579
|
+
}
|
|
580
|
+
|
|
566
581
|
// src/config/asset-registry.ts
|
|
567
582
|
import { isAddress } from "viem";
|
|
568
583
|
var BUILTIN_ASSETS = {
|
|
@@ -693,92 +708,6 @@ var AssetRegistry = class {
|
|
|
693
708
|
}
|
|
694
709
|
};
|
|
695
710
|
|
|
696
|
-
// src/config/pairs.ts
|
|
697
|
-
var RETIRED_PAIR_KEYS = ["bidPriceCurve", "askPriceCurve", "maxOrderSize", "referenceOnly"];
|
|
698
|
-
function retiredPairKeys(pairs) {
|
|
699
|
-
const carried = /* @__PURE__ */ new Set();
|
|
700
|
-
for (const pair of pairs) {
|
|
701
|
-
for (const key of RETIRED_PAIR_KEYS) {
|
|
702
|
-
if (pair[key] !== void 0) carried.add(key);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return RETIRED_PAIR_KEYS.filter((key) => carried.has(key));
|
|
706
|
-
}
|
|
707
|
-
function isKnownSymbol(symbol, userAssets) {
|
|
708
|
-
const normalized = normalizeSymbol(symbol);
|
|
709
|
-
if (isRegistrySymbol(normalized)) return true;
|
|
710
|
-
return Object.keys(userAssets ?? {}).some((key) => normalizeSymbol(key) === normalized);
|
|
711
|
-
}
|
|
712
|
-
function assertPairSymbolsResolve(pairs, registry, chainNames) {
|
|
713
|
-
const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
|
|
714
|
-
for (const pair of pairs) {
|
|
715
|
-
for (const symbol of [pair.token0, pair.token1]) {
|
|
716
|
-
const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
|
|
717
|
-
if (!resolvesSomewhere) {
|
|
718
|
-
throw new Error(
|
|
719
|
-
`pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
for (const chainName2 of chainNames) {
|
|
725
|
-
const addressOwner = /* @__PURE__ */ new Map();
|
|
726
|
-
for (const symbol of pairSymbols) {
|
|
727
|
-
const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
|
|
728
|
-
if (!address) continue;
|
|
729
|
-
const owner = addressOwner.get(address);
|
|
730
|
-
if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
|
|
731
|
-
throw new Error(
|
|
732
|
-
`assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
addressOwner.set(address, symbol);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
function pickAnchorStable(pairs, symbol) {
|
|
740
|
-
const target = normalizeSymbol(symbol);
|
|
741
|
-
for (const stable of USD_STABLE_SYMBOLS) {
|
|
742
|
-
const taken = pairs.some((pair) => {
|
|
743
|
-
const token0 = normalizeSymbol(pair.token0);
|
|
744
|
-
const token1 = normalizeSymbol(pair.token1);
|
|
745
|
-
return token0 === stable && token1 === target || token0 === target && token1 === stable;
|
|
746
|
-
});
|
|
747
|
-
if (!taken) return stable;
|
|
748
|
-
}
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
function validatePairConfigs(pairs, userAssets) {
|
|
752
|
-
if (!Array.isArray(pairs) || pairs.length === 0) {
|
|
753
|
-
throw new Error("pairs: at least one [[pairs]] entry is required");
|
|
754
|
-
}
|
|
755
|
-
const seen = /* @__PURE__ */ new Set();
|
|
756
|
-
for (const pair of pairs) {
|
|
757
|
-
if (!pair.token0 || !pair.token1) {
|
|
758
|
-
throw new Error("pairs: each entry needs 'token0' and 'token1' symbols");
|
|
759
|
-
}
|
|
760
|
-
const token0 = normalizeSymbol(pair.token0);
|
|
761
|
-
const token1 = normalizeSymbol(pair.token1);
|
|
762
|
-
const label = `${token0}/${token1}`;
|
|
763
|
-
if (seen.has(label)) {
|
|
764
|
-
throw new Error(`pairs.${label}: pair is declared twice`);
|
|
765
|
-
}
|
|
766
|
-
if (seen.has(`${token1}/${token0}`)) {
|
|
767
|
-
throw new Error(
|
|
768
|
-
`pairs.${label}: ${token1}/${token0} is already declared \u2014 a market has one orientation, and a limit order on either side trades it`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
seen.add(label);
|
|
772
|
-
for (const symbol of [token0, token1]) {
|
|
773
|
-
if (!isKnownSymbol(symbol, userAssets)) {
|
|
774
|
-
throw new Error(
|
|
775
|
-
`pairs.${label}: unknown symbol '${symbol}' \u2014 the registry ships ${registrySymbols().join(", ")}; anything else needs an [assets.${symbol}] entry`
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
711
|
// src/config/abis/ERC20.ts
|
|
783
712
|
var ERC20_ABI = [
|
|
784
713
|
{
|
|
@@ -1183,7 +1112,7 @@ async function resolveChainConfigs(chains, options = {}) {
|
|
|
1183
1112
|
"RPC endpoint could not report its chainId at startup; keeping it for the quorum to judge per call"
|
|
1184
1113
|
);
|
|
1185
1114
|
}
|
|
1186
|
-
return { chainId: first.chainId, rpcUrls, bundlerUrl:
|
|
1115
|
+
return { chainId: first.chainId, rpcUrls, bundlerUrl: hyperbridgeBundlerUrl(first.chainId) };
|
|
1187
1116
|
})
|
|
1188
1117
|
);
|
|
1189
1118
|
}
|
|
@@ -1252,12 +1181,6 @@ var FillerConfigService = class {
|
|
|
1252
1181
|
}
|
|
1253
1182
|
this.rpcOverrides.set(chainId, validateRpcUrls(rpcUrls));
|
|
1254
1183
|
}
|
|
1255
|
-
setBundlerUrl(chainId, bundlerUrl) {
|
|
1256
|
-
if (!this.rpcOverrides.has(chainId)) {
|
|
1257
|
-
throw new Error(`Chain ${chainId} is not configured`);
|
|
1258
|
-
}
|
|
1259
|
-
this.bundlerUrls.set(chainId, bundlerUrl);
|
|
1260
|
-
}
|
|
1261
1184
|
/** Replaces the rebalancing config at runtime; trigger checks read it live. */
|
|
1262
1185
|
setRebalancing(rebalancing) {
|
|
1263
1186
|
if (this.fillerConfig) this.fillerConfig.rebalancing = rebalancing;
|
|
@@ -1602,7 +1525,7 @@ function validateOrderbookConfig(orderbook) {
|
|
|
1602
1525
|
function validateConfig(config, cliWatchOnly = false) {
|
|
1603
1526
|
if ("strategies" in config) {
|
|
1604
1527
|
throw new Error(
|
|
1605
|
-
"[[strategies]] was removed \u2014
|
|
1528
|
+
"[[strategies]] was removed \u2014 simplex prices every fill from the limit orders you post. Run `simplex init` to migrate this config; per-strategy confirmation policies move to [confirmationPolicies]"
|
|
1606
1529
|
);
|
|
1607
1530
|
}
|
|
1608
1531
|
const allChainsWatchOnly2 = cliWatchOnly || config.simplex?.watchOnly === true;
|
|
@@ -1619,9 +1542,6 @@ function validateConfig(config, cliWatchOnly = false) {
|
|
|
1619
1542
|
if (!Array.isArray(chain.rpcUrls) || chain.rpcUrls.length === 0 || chain.rpcUrls.some((u) => !u)) {
|
|
1620
1543
|
throw new Error("Each chain configuration must have rpcUrls (a non-empty array of strings)");
|
|
1621
1544
|
}
|
|
1622
|
-
if (!chain.bundlerUrl) {
|
|
1623
|
-
throw new Error("Each chain configuration must have bundlerUrl");
|
|
1624
|
-
}
|
|
1625
1545
|
}
|
|
1626
1546
|
const concurrent = config.simplex.maxConcurrentOrders;
|
|
1627
1547
|
if (concurrent !== void 0) {
|
|
@@ -1670,9 +1590,6 @@ function validateConfig(config, cliWatchOnly = false) {
|
|
|
1670
1590
|
if (config.assets) {
|
|
1671
1591
|
validateAssetDefinitions(config.assets);
|
|
1672
1592
|
}
|
|
1673
|
-
if ((config.pairs?.length ?? 0) > 0) {
|
|
1674
|
-
validatePairConfigs(config.pairs, config.assets);
|
|
1675
|
-
}
|
|
1676
1593
|
if (config.confirmationPolicies) {
|
|
1677
1594
|
void new ConfirmationPolicy(config.confirmationPolicies);
|
|
1678
1595
|
}
|
|
@@ -2483,13 +2400,6 @@ var FREE_RPC_URLS = {
|
|
|
2483
2400
|
"https://arb1.arbitrum.io/rpc"
|
|
2484
2401
|
]
|
|
2485
2402
|
};
|
|
2486
|
-
var HYPERBRIDGE_BUNDLER_URLS = {
|
|
2487
|
-
1: "https://bundler.polytope.technology/ethereum",
|
|
2488
|
-
56: "https://bundler.polytope.technology/bsc",
|
|
2489
|
-
137: "https://bundler.polytope.technology/polygon",
|
|
2490
|
-
8453: "https://bundler.polytope.technology/base",
|
|
2491
|
-
42161: "https://bundler.polytope.technology/arbitrum"
|
|
2492
|
-
};
|
|
2493
2403
|
var CHAIN_NATIVE_SYMBOLS = {
|
|
2494
2404
|
1: "ETH",
|
|
2495
2405
|
56: "BNB",
|
|
@@ -2591,7 +2501,8 @@ var INIT_CHAINS = [
|
|
|
2591
2501
|
label: "Polygon Amoy",
|
|
2592
2502
|
network: "testnet",
|
|
2593
2503
|
alchemySubdomain: "polygon-amoy",
|
|
2594
|
-
explorerUrl: external_links_default.chainExplorers.polygonAmoy
|
|
2504
|
+
explorerUrl: external_links_default.chainExplorers.polygonAmoy,
|
|
2505
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[80002]
|
|
2595
2506
|
},
|
|
2596
2507
|
{
|
|
2597
2508
|
chainId: 97,
|
|
@@ -2599,7 +2510,8 @@ var INIT_CHAINS = [
|
|
|
2599
2510
|
label: "BSC Chapel",
|
|
2600
2511
|
network: "testnet",
|
|
2601
2512
|
alchemySubdomain: "bnb-testnet",
|
|
2602
|
-
explorerUrl: external_links_default.chainExplorers.bnbChapel
|
|
2513
|
+
explorerUrl: external_links_default.chainExplorers.bnbChapel,
|
|
2514
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[97]
|
|
2603
2515
|
}
|
|
2604
2516
|
];
|
|
2605
2517
|
function chainByChainId(chainId) {
|
|
@@ -4396,80 +4308,6 @@ async function patchRuntimeState(store, patch) {
|
|
|
4396
4308
|
return next;
|
|
4397
4309
|
}
|
|
4398
4310
|
|
|
4399
|
-
// src/services/bundler-preflight.ts
|
|
4400
|
-
var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
4401
|
-
async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
|
|
4402
|
-
const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
|
|
4403
|
-
const logger = moduleLogger(configService.loggers, "bundler-preflight");
|
|
4404
|
-
const failures = await Promise.all(
|
|
4405
|
-
chains.map(async ({ chainId, bundlerUrl }) => {
|
|
4406
|
-
if (options.watchOnly?.[chainId] === true) return void 0;
|
|
4407
|
-
if (!bundlerUrl?.trim()) return void 0;
|
|
4408
|
-
const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
|
|
4409
|
-
if (!entryPoint) return void 0;
|
|
4410
|
-
return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
|
|
4411
|
-
})
|
|
4412
|
-
);
|
|
4413
|
-
const messages = failures.filter((failure) => failure !== void 0);
|
|
4414
|
-
if (messages.length > 0) throw new Error(messages.join("; "));
|
|
4415
|
-
}
|
|
4416
|
-
function servesEntryPoint(listed, entryPoint) {
|
|
4417
|
-
return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
|
|
4418
|
-
}
|
|
4419
|
-
function describeEntryPoints(listed) {
|
|
4420
|
-
return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
|
|
4421
|
-
}
|
|
4422
|
-
function bundlerHost(url) {
|
|
4423
|
-
try {
|
|
4424
|
-
return new URL(url).host || "an unparseable URL";
|
|
4425
|
-
} catch {
|
|
4426
|
-
return "an unparseable URL";
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
function chainName(chainId) {
|
|
4430
|
-
const label = chainByChainId(chainId)?.label;
|
|
4431
|
-
return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
|
|
4432
|
-
}
|
|
4433
|
-
async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
|
|
4434
|
-
const host = bundlerHost(bundlerUrl);
|
|
4435
|
-
const subject = `Bundler ${host} for ${chainName(chainId)}`;
|
|
4436
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
4437
|
-
let listed;
|
|
4438
|
-
try {
|
|
4439
|
-
listed = await supportedEntryPoints(bundlerUrl, signal);
|
|
4440
|
-
} catch (err) {
|
|
4441
|
-
const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
|
|
4442
|
-
logger.warn(
|
|
4443
|
-
{ chain: formatChainKey(chainId), bundler: host },
|
|
4444
|
-
`${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
|
|
4445
|
-
);
|
|
4446
|
-
return void 0;
|
|
4447
|
-
}
|
|
4448
|
-
if (servesEntryPoint(listed, entryPoint)) return void 0;
|
|
4449
|
-
return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
|
|
4450
|
-
}
|
|
4451
|
-
async function supportedEntryPoints(bundlerUrl, signal) {
|
|
4452
|
-
const response = await fetch(bundlerUrl, {
|
|
4453
|
-
method: "POST",
|
|
4454
|
-
headers: { "Content-Type": "application/json" },
|
|
4455
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
|
|
4456
|
-
signal
|
|
4457
|
-
});
|
|
4458
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
4459
|
-
const json = await response.json();
|
|
4460
|
-
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
4461
|
-
const { result } = json;
|
|
4462
|
-
if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
|
|
4463
|
-
throw new Error("the result is not a list of addresses");
|
|
4464
|
-
}
|
|
4465
|
-
return result;
|
|
4466
|
-
}
|
|
4467
|
-
function errorReason(err) {
|
|
4468
|
-
if (!(err instanceof Error)) return String(err);
|
|
4469
|
-
const { cause } = err;
|
|
4470
|
-
return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
|
|
4471
|
-
}
|
|
4472
|
-
|
|
4473
4311
|
// src/services/wallet/mpcvault.ts
|
|
4474
4312
|
import { concatHex, keccak256 as keccak2562, padHex, toHex } from "viem";
|
|
4475
4313
|
import * as grpc from "@grpc/grpc-js";
|
|
@@ -18511,15 +18349,19 @@ import {
|
|
|
18511
18349
|
formatEther,
|
|
18512
18350
|
maxUint256 as maxUint2562,
|
|
18513
18351
|
erc20Abi as erc20Abi2,
|
|
18514
|
-
BaseError,
|
|
18515
|
-
ContractFunctionRevertedError,
|
|
18516
|
-
ContractFunctionZeroDataError
|
|
18352
|
+
BaseError as BaseError2,
|
|
18353
|
+
ContractFunctionRevertedError as ContractFunctionRevertedError2,
|
|
18354
|
+
ContractFunctionZeroDataError as ContractFunctionZeroDataError2
|
|
18517
18355
|
} from "viem";
|
|
18518
18356
|
|
|
18519
18357
|
// src/services/paymaster/types.ts
|
|
18520
18358
|
import { encodePacked } from "viem";
|
|
18521
18359
|
var RECOMMENDED_AMOUNT_USD = 5n;
|
|
18522
18360
|
var VERIFICATION_GAS_LIMIT_PERMIT2 = 200000n;
|
|
18361
|
+
var PERMIT2_VERIFICATION_GAS_BY_CHAIN = { 137: 300000n };
|
|
18362
|
+
function permit2VerificationGasLimit(chainId) {
|
|
18363
|
+
return PERMIT2_VERIFICATION_GAS_BY_CHAIN[chainId] ?? VERIFICATION_GAS_LIMIT_PERMIT2;
|
|
18364
|
+
}
|
|
18523
18365
|
var VERIFICATION_GAS_LIMIT_PERMIT = 250000n;
|
|
18524
18366
|
var PERMIT2_DEADLINE_SECONDS = 3600n;
|
|
18525
18367
|
var POST_OP_GAS_LIMIT_SIMPLEX = 40000n;
|
|
@@ -18532,7 +18374,15 @@ function packPaymasterAndData(pm) {
|
|
|
18532
18374
|
}
|
|
18533
18375
|
|
|
18534
18376
|
// src/services/paymaster/permit.ts
|
|
18535
|
-
import {
|
|
18377
|
+
import {
|
|
18378
|
+
BaseError,
|
|
18379
|
+
ContractFunctionRevertedError,
|
|
18380
|
+
ContractFunctionZeroDataError,
|
|
18381
|
+
hashDomain,
|
|
18382
|
+
maxUint256,
|
|
18383
|
+
pad,
|
|
18384
|
+
toHex as toHex2
|
|
18385
|
+
} from "viem";
|
|
18536
18386
|
|
|
18537
18387
|
// src/config/abis/EIP2612.ts
|
|
18538
18388
|
import { erc20Abi } from "viem";
|
|
@@ -18551,6 +18401,13 @@ var EIP2612_ABI = [
|
|
|
18551
18401
|
outputs: [{ internalType: "string", name: "", type: "string" }],
|
|
18552
18402
|
stateMutability: "view",
|
|
18553
18403
|
type: "function"
|
|
18404
|
+
},
|
|
18405
|
+
{
|
|
18406
|
+
inputs: [],
|
|
18407
|
+
name: "DOMAIN_SEPARATOR",
|
|
18408
|
+
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
|
|
18409
|
+
stateMutability: "view",
|
|
18410
|
+
type: "function"
|
|
18554
18411
|
}
|
|
18555
18412
|
];
|
|
18556
18413
|
|
|
@@ -18618,25 +18475,73 @@ function normalizeSignature65(signature) {
|
|
|
18618
18475
|
}
|
|
18619
18476
|
|
|
18620
18477
|
// src/services/paymaster/permit.ts
|
|
18478
|
+
var DOMAIN_SHAPES = [
|
|
18479
|
+
{
|
|
18480
|
+
fields: [
|
|
18481
|
+
{ name: "name", type: "string" },
|
|
18482
|
+
{ name: "version", type: "string" },
|
|
18483
|
+
{ name: "chainId", type: "uint256" },
|
|
18484
|
+
{ name: "verifyingContract", type: "address" }
|
|
18485
|
+
],
|
|
18486
|
+
domain: (name, version, chainId, verifyingContract) => ({ name, version, chainId, verifyingContract })
|
|
18487
|
+
},
|
|
18488
|
+
// USDT0 on Polygon: the chain id goes in a salt instead of a chainId field.
|
|
18489
|
+
{
|
|
18490
|
+
fields: [
|
|
18491
|
+
{ name: "name", type: "string" },
|
|
18492
|
+
{ name: "version", type: "string" },
|
|
18493
|
+
{ name: "verifyingContract", type: "address" },
|
|
18494
|
+
{ name: "salt", type: "bytes32" }
|
|
18495
|
+
],
|
|
18496
|
+
domain: (name, version, chainId, verifyingContract) => ({
|
|
18497
|
+
name,
|
|
18498
|
+
version,
|
|
18499
|
+
verifyingContract,
|
|
18500
|
+
salt: pad(toHex2(chainId), { size: 32 })
|
|
18501
|
+
})
|
|
18502
|
+
}
|
|
18503
|
+
];
|
|
18504
|
+
var FALLBACK_VERSIONS = ["1", "2"];
|
|
18505
|
+
async function readIfPresent(read) {
|
|
18506
|
+
try {
|
|
18507
|
+
return await read();
|
|
18508
|
+
} catch (error) {
|
|
18509
|
+
const isRevert = error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError || e instanceof ContractFunctionZeroDataError);
|
|
18510
|
+
if (isRevert) return void 0;
|
|
18511
|
+
throw error;
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
async function resolvePermitDomain(client, tokenAddress, chainId) {
|
|
18515
|
+
const read = (functionName) => readIfPresent(() => client.readContract({ address: tokenAddress, abi: EIP2612_ABI, functionName }));
|
|
18516
|
+
const separator = await read("DOMAIN_SEPARATOR");
|
|
18517
|
+
if (!separator) return null;
|
|
18518
|
+
const name = await read("name");
|
|
18519
|
+
if (name === void 0) return null;
|
|
18520
|
+
const version = await read("version");
|
|
18521
|
+
for (const candidate of version === void 0 ? FALLBACK_VERSIONS : [version]) {
|
|
18522
|
+
for (const shape of DOMAIN_SHAPES) {
|
|
18523
|
+
const domain = shape.domain(name, candidate, chainId, tokenAddress);
|
|
18524
|
+
const hash = hashDomain({ domain, types: { EIP712Domain: shape.fields } });
|
|
18525
|
+
if (hash.toLowerCase() === separator.toLowerCase()) return { domain, fields: shape.fields };
|
|
18526
|
+
}
|
|
18527
|
+
}
|
|
18528
|
+
return null;
|
|
18529
|
+
}
|
|
18621
18530
|
async function signEip2612Permit(client, signer, owner, spender, tokenAddress, value, chainId) {
|
|
18622
|
-
const
|
|
18623
|
-
|
|
18531
|
+
const permitDomain = await resolvePermitDomain(client, tokenAddress, chainId);
|
|
18532
|
+
if (!permitDomain) {
|
|
18533
|
+
throw new Error(`${tokenAddress} on chain ${chainId} has no EIP-2612 permit domain Simplex can sign for`);
|
|
18534
|
+
}
|
|
18535
|
+
const nonce = await client.readContract({
|
|
18624
18536
|
address: tokenAddress,
|
|
18625
|
-
abi: EIP2612_ABI
|
|
18537
|
+
abi: EIP2612_ABI,
|
|
18538
|
+
functionName: "nonces",
|
|
18539
|
+
args: [owner]
|
|
18626
18540
|
});
|
|
18627
|
-
const [name, version, nonce] = await Promise.all([
|
|
18628
|
-
token.read.name(),
|
|
18629
|
-
token.read.version(),
|
|
18630
|
-
token.read.nonces([owner])
|
|
18631
|
-
]);
|
|
18632
18541
|
const typedData = {
|
|
18633
18542
|
types: {
|
|
18634
|
-
|
|
18635
|
-
|
|
18636
|
-
{ name: "version", type: "string" },
|
|
18637
|
-
{ name: "chainId", type: "uint256" },
|
|
18638
|
-
{ name: "verifyingContract", type: "address" }
|
|
18639
|
-
],
|
|
18543
|
+
// Listed explicitly: custody backends hash the domain from this entry, not from `domain`.
|
|
18544
|
+
EIP712Domain: permitDomain.fields,
|
|
18640
18545
|
Permit: [
|
|
18641
18546
|
{ name: "owner", type: "address" },
|
|
18642
18547
|
{ name: "spender", type: "address" },
|
|
@@ -18646,12 +18551,7 @@ async function signEip2612Permit(client, signer, owner, spender, tokenAddress, v
|
|
|
18646
18551
|
]
|
|
18647
18552
|
},
|
|
18648
18553
|
primaryType: "Permit",
|
|
18649
|
-
domain:
|
|
18650
|
-
name,
|
|
18651
|
-
version,
|
|
18652
|
-
chainId,
|
|
18653
|
-
verifyingContract: tokenAddress
|
|
18654
|
-
},
|
|
18554
|
+
domain: permitDomain.domain,
|
|
18655
18555
|
message: {
|
|
18656
18556
|
owner,
|
|
18657
18557
|
spender,
|
|
@@ -18725,7 +18625,7 @@ async function buildSimplexPaymasterData(client, walletClient, signer, solverAcc
|
|
|
18725
18625
|
);
|
|
18726
18626
|
}
|
|
18727
18627
|
if (permit2Allowance < recommended) {
|
|
18728
|
-
if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress)) {
|
|
18628
|
+
if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress, chainId)) {
|
|
18729
18629
|
const pm = await buildPermitMode(
|
|
18730
18630
|
client,
|
|
18731
18631
|
signer,
|
|
@@ -18761,7 +18661,7 @@ async function resolvePendingPermit2Approval(client, solverAccount, paymasterAdd
|
|
|
18761
18661
|
return {
|
|
18762
18662
|
token: selected.address,
|
|
18763
18663
|
spender: permit2,
|
|
18764
|
-
permitCapable: await tokenSupportsPermit(client, selected.address)
|
|
18664
|
+
permitCapable: await tokenSupportsPermit(client, selected.address, configService.getChainId(chain))
|
|
18765
18665
|
};
|
|
18766
18666
|
}
|
|
18767
18667
|
function isConfigured(address) {
|
|
@@ -18781,7 +18681,7 @@ function configuredFeeTokens(chain, configService) {
|
|
|
18781
18681
|
}
|
|
18782
18682
|
async function selectToken(client, solverAccount, tokens, permit2) {
|
|
18783
18683
|
const balances = [];
|
|
18784
|
-
let
|
|
18684
|
+
let fallback3 = null;
|
|
18785
18685
|
for (const token of tokens) {
|
|
18786
18686
|
const balance = await client.readContract({
|
|
18787
18687
|
address: token.address,
|
|
@@ -18795,12 +18695,12 @@ async function selectToken(client, solverAccount, tokens, permit2) {
|
|
|
18795
18695
|
const permit2Allowance = await readAllowance(client, token.address, solverAccount, permit2);
|
|
18796
18696
|
const candidate = { ...token, permit2Allowance };
|
|
18797
18697
|
if (permit2Allowance >= RECOMMENDED_AMOUNT_USD * required) return { selected: candidate, balances };
|
|
18798
|
-
|
|
18698
|
+
fallback3 ?? (fallback3 = candidate);
|
|
18799
18699
|
} else {
|
|
18800
18700
|
balances.push({ symbol: token.symbol, balance, required });
|
|
18801
18701
|
}
|
|
18802
18702
|
}
|
|
18803
|
-
return { selected:
|
|
18703
|
+
return { selected: fallback3, balances };
|
|
18804
18704
|
}
|
|
18805
18705
|
var PERMIT2_SUPPORT_NEGATIVE_TTL_MS = 5 * 6e4;
|
|
18806
18706
|
var permit2Support = /* @__PURE__ */ new Map();
|
|
@@ -18815,7 +18715,7 @@ async function paymasterSupportsPermit2(client, chainId, paymasterAddress) {
|
|
|
18815
18715
|
permit2Support.set(key, { supported: true, checkedAt: Date.now() });
|
|
18816
18716
|
return true;
|
|
18817
18717
|
} catch (error) {
|
|
18818
|
-
const isRevert = error instanceof
|
|
18718
|
+
const isRevert = error instanceof BaseError2 && error.walk((e) => e instanceof ContractFunctionRevertedError2 || e instanceof ContractFunctionZeroDataError2);
|
|
18819
18719
|
if (isRevert) {
|
|
18820
18720
|
permit2Support.set(key, { supported: false, checkedAt: Date.now() });
|
|
18821
18721
|
return false;
|
|
@@ -18853,7 +18753,7 @@ async function buildPermit2Mode(signer, p) {
|
|
|
18853
18753
|
return {
|
|
18854
18754
|
paymaster: p.spender,
|
|
18855
18755
|
paymasterData,
|
|
18856
|
-
paymasterVerificationGasLimit:
|
|
18756
|
+
paymasterVerificationGasLimit: permit2VerificationGasLimit(p.chainId),
|
|
18857
18757
|
paymasterPostOpGasLimit: POST_OP_GAS_LIMIT_SIMPLEX
|
|
18858
18758
|
};
|
|
18859
18759
|
}
|
|
@@ -18923,27 +18823,8 @@ async function sendApproveTx(client, walletClient, tokenAddress, spender, amount
|
|
|
18923
18823
|
});
|
|
18924
18824
|
await client.waitForTransactionReceipt({ hash, confirmations: 2 });
|
|
18925
18825
|
}
|
|
18926
|
-
async function tokenSupportsPermit(client, tokenAddress) {
|
|
18927
|
-
|
|
18928
|
-
await client.readContract({
|
|
18929
|
-
address: tokenAddress,
|
|
18930
|
-
abi: [
|
|
18931
|
-
{
|
|
18932
|
-
inputs: [],
|
|
18933
|
-
name: "version",
|
|
18934
|
-
outputs: [{ type: "string" }],
|
|
18935
|
-
stateMutability: "view",
|
|
18936
|
-
type: "function"
|
|
18937
|
-
}
|
|
18938
|
-
],
|
|
18939
|
-
functionName: "version"
|
|
18940
|
-
});
|
|
18941
|
-
return true;
|
|
18942
|
-
} catch (error) {
|
|
18943
|
-
const isRevert = error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError || e instanceof ContractFunctionZeroDataError);
|
|
18944
|
-
if (isRevert) return false;
|
|
18945
|
-
throw error;
|
|
18946
|
-
}
|
|
18826
|
+
async function tokenSupportsPermit(client, tokenAddress, chainId) {
|
|
18827
|
+
return await resolvePermitDomain(client, tokenAddress, chainId) !== null;
|
|
18947
18828
|
}
|
|
18948
18829
|
|
|
18949
18830
|
// src/services/paymaster/index.ts
|
|
@@ -18959,7 +18840,7 @@ async function buildPaymasterAndData(options) {
|
|
|
18959
18840
|
const shortfall = await depositShortfall(
|
|
18960
18841
|
options,
|
|
18961
18842
|
simplexAddr,
|
|
18962
|
-
(permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT :
|
|
18843
|
+
(permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT : permit2VerificationGasLimit(configService.getChainId(chain))) + POST_OP_GAS_LIMIT_SIMPLEX
|
|
18963
18844
|
);
|
|
18964
18845
|
if (shortfall) {
|
|
18965
18846
|
return { paymasterAndData: "0x", type: "none", reason: shortfall };
|
|
@@ -19751,12 +19632,12 @@ var ContractInteractionService = class {
|
|
|
19751
19632
|
const declared = declaredMs ? declaredMs / 1e3 : void 0;
|
|
19752
19633
|
const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
|
|
19753
19634
|
this.blockTimes.delete(chain);
|
|
19754
|
-
const
|
|
19635
|
+
const fallback3 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
|
|
19755
19636
|
this.logger.warn(
|
|
19756
|
-
{ err, chain, seconds:
|
|
19637
|
+
{ err, chain, seconds: fallback3 },
|
|
19757
19638
|
"Could not measure the block time; using the declared one"
|
|
19758
19639
|
);
|
|
19759
|
-
return
|
|
19640
|
+
return fallback3;
|
|
19760
19641
|
});
|
|
19761
19642
|
this.blockTimes.set(chain, seconds);
|
|
19762
19643
|
return seconds;
|
|
@@ -19949,10 +19830,9 @@ import { encodeERC7821ExecuteBatch as encodeERC7821ExecuteBatch3 } from "@hyperb
|
|
|
19949
19830
|
import {
|
|
19950
19831
|
CryptoUtils as CryptoUtils2,
|
|
19951
19832
|
BundlerMethod,
|
|
19952
|
-
|
|
19953
|
-
fetchRundlerPriorityFee
|
|
19833
|
+
rundlerUserOperationFees
|
|
19954
19834
|
} from "@hyperbridge/sdk";
|
|
19955
|
-
import { toHex as
|
|
19835
|
+
import { toHex as toHex3 } from "viem";
|
|
19956
19836
|
var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
|
|
19957
19837
|
var FALLBACK_CALL_GAS_LIMIT = 1500000n;
|
|
19958
19838
|
var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
|
|
@@ -19984,7 +19864,7 @@ var UserOpSender = class {
|
|
|
19984
19864
|
let auth;
|
|
19985
19865
|
let fees;
|
|
19986
19866
|
try {
|
|
19987
|
-
fees = await this.getGasPrice(bundlerUrl, publicClient
|
|
19867
|
+
fees = await this.getGasPrice(bundlerUrl, publicClient);
|
|
19988
19868
|
const gasForPrefund = gas ?? {
|
|
19989
19869
|
verificationGasLimit: FALLBACK_VERIFICATION_GAS_LIMIT,
|
|
19990
19870
|
callGasLimit: FALLBACK_CALL_GAS_LIMIT,
|
|
@@ -20027,11 +19907,11 @@ var UserOpSender = class {
|
|
|
20027
19907
|
});
|
|
20028
19908
|
const formattedAuth = auth ? {
|
|
20029
19909
|
address: auth.address,
|
|
20030
|
-
chainId:
|
|
20031
|
-
nonce:
|
|
19910
|
+
chainId: toHex3(auth.chainId),
|
|
19911
|
+
nonce: toHex3(auth.nonce),
|
|
20032
19912
|
r: auth.r,
|
|
20033
19913
|
s: auth.s,
|
|
20034
|
-
yParity:
|
|
19914
|
+
yParity: toHex3(auth.yParity)
|
|
20035
19915
|
} : void 0;
|
|
20036
19916
|
const { verificationGasLimit, callGasLimit, preVerificationGas } = gas ?? await this.estimateGas({
|
|
20037
19917
|
bundlerUrl,
|
|
@@ -20139,48 +20019,26 @@ var UserOpSender = class {
|
|
|
20139
20019
|
return userOp;
|
|
20140
20020
|
}
|
|
20141
20021
|
/**
|
|
20142
|
-
*
|
|
20143
|
-
*
|
|
20144
|
-
* rundler bundler requires, when the bundler gives one.
|
|
20022
|
+
* Prices the op for the rundler bundler, as the fill path does: its suggested fees, or the
|
|
20023
|
+
* chain's gas price raised to the priority fee it requires where it suggests none.
|
|
20145
20024
|
*/
|
|
20146
|
-
async getGasPrice(bundlerUrl, publicClient
|
|
20147
|
-
const
|
|
20148
|
-
if (lower.includes("pimlico.io")) {
|
|
20149
|
-
const res = await this.sendBundlerRpc(
|
|
20150
|
-
bundlerUrl,
|
|
20151
|
-
BundlerMethod.PIMLICO_GET_USER_OPERATION_GAS_PRICE,
|
|
20152
|
-
[]
|
|
20153
|
-
);
|
|
20154
|
-
return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
|
|
20155
|
-
}
|
|
20156
|
-
if (lower.includes("alchemy.com")) {
|
|
20157
|
-
const [rundlerPriorityFee2, latestBlock2] = await Promise.all([
|
|
20158
|
-
this.sendBundlerRpc(bundlerUrl, BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
|
|
20159
|
-
publicClient.getBlock({ blockTag: "latest" })
|
|
20160
|
-
]);
|
|
20161
|
-
const baseFeePerGas = latestBlock2.baseFeePerGas ?? await publicClient.getGasPrice();
|
|
20162
|
-
const isArbitrum = BigInt(chainId) === 42161n;
|
|
20163
|
-
const prioBump = isArbitrum ? 0n : 25n;
|
|
20164
|
-
const maxPriorityFeePerGas = BigInt(rundlerPriorityFee2) + BigInt(rundlerPriorityFee2) * prioBump / 100n;
|
|
20165
|
-
const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
|
|
20166
|
-
return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
|
|
20167
|
-
}
|
|
20168
|
-
const [gasPrice, rundlerPriorityFee] = await Promise.all([
|
|
20025
|
+
async getGasPrice(bundlerUrl, publicClient) {
|
|
20026
|
+
const [gasPrice, latestBlock] = await Promise.all([
|
|
20169
20027
|
publicClient.getGasPrice(),
|
|
20170
|
-
|
|
20028
|
+
publicClient.getBlock({ blockTag: "latest" })
|
|
20171
20029
|
]);
|
|
20172
|
-
|
|
20173
|
-
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
|
|
20030
|
+
return rundlerUserOperationFees(
|
|
20031
|
+
bundlerUrl,
|
|
20032
|
+
{
|
|
20033
|
+
maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
|
|
20034
|
+
maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
|
|
20035
|
+
},
|
|
20036
|
+
{
|
|
20037
|
+
baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
|
|
20038
|
+
priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
|
|
20039
|
+
maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
|
|
20040
|
+
}
|
|
20041
|
+
);
|
|
20184
20042
|
}
|
|
20185
20043
|
async sendBundlerRpc(bundlerUrl, method, params) {
|
|
20186
20044
|
const response = await fetch(bundlerUrl, {
|
|
@@ -22983,18 +22841,17 @@ function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
|
|
|
22983
22841
|
if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
|
|
22984
22842
|
return amount / BigInt(10 ** (fromDecimals - toDecimals));
|
|
22985
22843
|
}
|
|
22986
|
-
var FXFiller = class
|
|
22844
|
+
var FXFiller = class {
|
|
22987
22845
|
/**
|
|
22988
22846
|
* @param signer Filler's signing account for UserOp signatures.
|
|
22989
22847
|
* @param configService Network/config provider for addresses and decimals.
|
|
22990
22848
|
* @param clientManager Used to get viem PublicClients for chains.
|
|
22991
22849
|
* @param contractService Shared contract interaction service.
|
|
22992
|
-
* @param
|
|
22993
|
-
* @param registry Asset symbol registry resolving pair symbols per chain.
|
|
22850
|
+
* @param registry Asset symbol registry resolving symbols per chain.
|
|
22994
22851
|
* @param options.confirmationPolicy Optional per-chain confirmation policy for cross-chain orders.
|
|
22995
22852
|
* @param options.fundingVenues Optional funding venues for on-chain liquidity sourcing.
|
|
22996
22853
|
*/
|
|
22997
|
-
constructor(signer, configService, clientManager, contractService,
|
|
22854
|
+
constructor(signer, configService, clientManager, contractService, registry, options) {
|
|
22998
22855
|
this.name = "FXFiller";
|
|
22999
22856
|
/**
|
|
23000
22857
|
* Consecutive orders where the overfill clamp activated. Dormant: the clamp
|
|
@@ -23007,17 +22864,9 @@ var FXFiller = class _FXFiller {
|
|
|
23007
22864
|
this.logger = moduleLogger(configService.loggers, "fx-simplex");
|
|
23008
22865
|
const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
|
|
23009
22866
|
this.limitOrders = limitOrders;
|
|
23010
|
-
if (pairs.length === 0) {
|
|
23011
|
-
throw new Error("FXFiller requires at least one trading pair");
|
|
23012
|
-
}
|
|
23013
|
-
const seenPairs = /* @__PURE__ */ new Set();
|
|
23014
|
-
for (const pair of pairs) {
|
|
23015
|
-
_FXFiller.assertPairValid(pair, seenPairs);
|
|
23016
|
-
}
|
|
23017
22867
|
this.configService = configService;
|
|
23018
22868
|
this.clientManager = clientManager;
|
|
23019
22869
|
this.contractService = contractService;
|
|
23020
|
-
this.pairs = pairs;
|
|
23021
22870
|
this.registry = registry;
|
|
23022
22871
|
this.fundingVenues = fundingVenues;
|
|
23023
22872
|
this.signer = signer;
|
|
@@ -23029,43 +22878,6 @@ var FXFiller = class _FXFiller {
|
|
|
23029
22878
|
};
|
|
23030
22879
|
}
|
|
23031
22880
|
}
|
|
23032
|
-
/** Per-pair invariants, shared by the constructor and addPair. Adds the accepted pair's label to `seenPairs`. */
|
|
23033
|
-
static assertPairValid(pair, seenPairs) {
|
|
23034
|
-
const label = `${normalizeSymbol(pair.token0)}/${normalizeSymbol(pair.token1)}`;
|
|
23035
|
-
const reversed = `${normalizeSymbol(pair.token1)}/${normalizeSymbol(pair.token0)}`;
|
|
23036
|
-
if (seenPairs.has(label) || seenPairs.has(reversed)) {
|
|
23037
|
-
throw new Error(
|
|
23038
|
-
`FXFiller pair ${pair.token0}/${pair.token1}: duplicate market (a pair and its reverse are the same market)`
|
|
23039
|
-
);
|
|
23040
|
-
}
|
|
23041
|
-
seenPairs.add(label);
|
|
23042
|
-
}
|
|
23043
|
-
/**
|
|
23044
|
-
* Adds a market to the running engine. All-or-nothing: the pair passes the
|
|
23045
|
-
* same duplicate and reverse-orientation checks the constructor enforces
|
|
23046
|
-
* before it is pushed.
|
|
23047
|
-
*/
|
|
23048
|
-
addPair(pair) {
|
|
23049
|
-
const seenPairs = new Set(this.pairs.map((p) => `${normalizeSymbol(p.token0)}/${normalizeSymbol(p.token1)}`));
|
|
23050
|
-
_FXFiller.assertPairValid(pair, seenPairs);
|
|
23051
|
-
this.pairs.push(pair);
|
|
23052
|
-
}
|
|
23053
|
-
/**
|
|
23054
|
-
* Removes a market from the running engine. At least one market must remain.
|
|
23055
|
-
* Matching reads the limit orders rather than the pairs, so removing a market
|
|
23056
|
-
* changes what the dashboard lists, not what the filler fills.
|
|
23057
|
-
*/
|
|
23058
|
-
removePair(pair) {
|
|
23059
|
-
const index = this.pairs.indexOf(pair);
|
|
23060
|
-
if (index < 0) {
|
|
23061
|
-
throw new Error(`FXFiller pair ${pair.token0}/${pair.token1}: not a live market`);
|
|
23062
|
-
}
|
|
23063
|
-
const remaining = this.pairs.filter((_, i) => i !== index);
|
|
23064
|
-
if (remaining.length === 0) {
|
|
23065
|
-
throw new Error("FXFiller requires at least one trading pair \u2014 the last market cannot be removed live");
|
|
23066
|
-
}
|
|
23067
|
-
this.pairs.splice(index, 1);
|
|
23068
|
-
}
|
|
23069
22881
|
// =========================================================================
|
|
23070
22882
|
// Lifecycle
|
|
23071
22883
|
// =========================================================================
|
|
@@ -23756,7 +23568,7 @@ function minBigInt(first, ...rest) {
|
|
|
23756
23568
|
|
|
23757
23569
|
// src/services/TokenSender.ts
|
|
23758
23570
|
import { encodeERC7821ExecuteBatch as encodeERC7821ExecuteBatch4 } from "@hyperbridge/sdk";
|
|
23759
|
-
import { encodeFunctionData as encodeFunctionData4, isAddress as isAddress3, parseUnits as parseUnits4 } from "viem";
|
|
23571
|
+
import { encodeFunctionData as encodeFunctionData4, isAddress as isAddress3, maxUint256 as maxUint2565, parseUnits as parseUnits4 } from "viem";
|
|
23760
23572
|
var TokenSender = class {
|
|
23761
23573
|
constructor(clientManager, solver, getVaults, userOpSender, configService) {
|
|
23762
23574
|
this.clientManager = clientManager;
|
|
@@ -23828,7 +23640,7 @@ var TokenSender = class {
|
|
|
23828
23640
|
});
|
|
23829
23641
|
}
|
|
23830
23642
|
}
|
|
23831
|
-
const result = await this.
|
|
23643
|
+
const result = await this.submitCalls(chain, calls);
|
|
23832
23644
|
this.logger.warn({ chain, token, amount, to, redeemed, ...result }, "Operator send executed");
|
|
23833
23645
|
return { ...result, redeemed };
|
|
23834
23646
|
}
|
|
@@ -23884,16 +23696,23 @@ var TokenSender = class {
|
|
|
23884
23696
|
}
|
|
23885
23697
|
return null;
|
|
23886
23698
|
}
|
|
23887
|
-
|
|
23699
|
+
/**
|
|
23700
|
+
* Executes a batch from the filler wallet: sponsored UserOp first, native tx
|
|
23701
|
+
* when no sponsor is available. Shared by every operator-initiated outbound flow.
|
|
23702
|
+
*/
|
|
23703
|
+
async submitCalls(chain, calls) {
|
|
23888
23704
|
if (this.userOpSender?.canSponsor(chain)) {
|
|
23705
|
+
const bootstrap = await this.permit2Bootstrap(chain);
|
|
23706
|
+
const batch = bootstrap ? [bootstrap, ...calls] : calls;
|
|
23889
23707
|
const result = await this.userOpSender.trySendSponsored({
|
|
23890
23708
|
chain,
|
|
23891
|
-
callData: encodeERC7821ExecuteBatch4(
|
|
23709
|
+
callData: encodeERC7821ExecuteBatch4(batch),
|
|
23892
23710
|
gas: {
|
|
23893
23711
|
verificationGasLimit: 60000n,
|
|
23894
|
-
callGasLimit: 450000n * BigInt(
|
|
23712
|
+
callGasLimit: 450000n * BigInt(batch.length),
|
|
23895
23713
|
preVerificationGas: 150000n
|
|
23896
|
-
}
|
|
23714
|
+
},
|
|
23715
|
+
permitBootstrap: !!bootstrap
|
|
23897
23716
|
});
|
|
23898
23717
|
if (result) return { txHash: result.txHash, sponsored: true };
|
|
23899
23718
|
this.logger.warn({ chain }, "Sponsored send unavailable, sending native tx");
|
|
@@ -23915,18 +23734,1118 @@ var TokenSender = class {
|
|
|
23915
23734
|
if (receipt.status !== "success") throw new Error(`Send tx reverted: ${tx}`);
|
|
23916
23735
|
return { txHash: tx, sponsored: false };
|
|
23917
23736
|
}
|
|
23737
|
+
/**
|
|
23738
|
+
* The `approve(Permit2, max)` to run first when the wallet has never approved Permit2 for its fee
|
|
23739
|
+
* token on this chain, and that token has an EIP-2612 permit. The op then pays its gas with a permit
|
|
23740
|
+
* and installs the approval, so a wallet with no native can still send; later ops use Permit2.
|
|
23741
|
+
* Undefined otherwise, or when the check fails, and the send goes ahead as before.
|
|
23742
|
+
*/
|
|
23743
|
+
async permit2Bootstrap(chain) {
|
|
23744
|
+
const paymaster = this.configService?.getSimplexPaymasterAddress(chain);
|
|
23745
|
+
if (!this.configService || !paymaster) return void 0;
|
|
23746
|
+
try {
|
|
23747
|
+
const pending = await resolvePendingPermit2Approval(
|
|
23748
|
+
this.clientManager.getPublicClient(chain),
|
|
23749
|
+
this.solver,
|
|
23750
|
+
paymaster,
|
|
23751
|
+
chain,
|
|
23752
|
+
this.configService
|
|
23753
|
+
);
|
|
23754
|
+
if (!pending?.permitCapable) return void 0;
|
|
23755
|
+
return {
|
|
23756
|
+
target: pending.token,
|
|
23757
|
+
value: 0n,
|
|
23758
|
+
data: encodeFunctionData4({
|
|
23759
|
+
abi: ERC20_ABI,
|
|
23760
|
+
functionName: "approve",
|
|
23761
|
+
args: [pending.spender, maxUint2565]
|
|
23762
|
+
})
|
|
23763
|
+
};
|
|
23764
|
+
} catch (error) {
|
|
23765
|
+
this.logger.warn({ chain, error }, "Could not check whether this send must install the Permit2 approval");
|
|
23766
|
+
return void 0;
|
|
23767
|
+
}
|
|
23768
|
+
}
|
|
23918
23769
|
};
|
|
23919
23770
|
|
|
23920
|
-
// src/
|
|
23921
|
-
|
|
23922
|
-
|
|
23923
|
-
|
|
23924
|
-
|
|
23925
|
-
|
|
23926
|
-
|
|
23927
|
-
|
|
23771
|
+
// src/services/BridgeTopUp.ts
|
|
23772
|
+
import {
|
|
23773
|
+
bytes20ToBytes32 as bytes20ToBytes323,
|
|
23774
|
+
deriveCanonicalPlacedOrder,
|
|
23775
|
+
IntentGatewayABI,
|
|
23776
|
+
createQueryClient,
|
|
23777
|
+
EvmChain as EvmChain2,
|
|
23778
|
+
getChainId as getChainId4,
|
|
23779
|
+
HyperFungibleTokenABI,
|
|
23780
|
+
IntentGateway as IntentGateway2,
|
|
23781
|
+
IsmpClient,
|
|
23782
|
+
SubstrateChain
|
|
23783
|
+
} from "@hyperbridge/sdk";
|
|
23784
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
23785
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
23786
|
+
import {
|
|
23787
|
+
createPublicClient as createPublicClient3,
|
|
23788
|
+
encodeAbiParameters,
|
|
23789
|
+
encodeFunctionData as encodeFunctionData5,
|
|
23790
|
+
fallback as fallback2,
|
|
23791
|
+
formatUnits as formatUnits5,
|
|
23792
|
+
http as http3,
|
|
23793
|
+
parseEventLogs as parseEventLogs2,
|
|
23794
|
+
parseUnits as parseUnits5,
|
|
23795
|
+
stringToHex,
|
|
23796
|
+
toHex as toHex4
|
|
23797
|
+
} from "viem";
|
|
23798
|
+
import { bsc as bscChain } from "viem/chains";
|
|
23799
|
+
var SIMPLEX_REFERRER = stringToHex("Simplex", { size: 32 });
|
|
23800
|
+
var BRIDGE_TOP_UP_CHAIN = "EVM-56";
|
|
23801
|
+
var BRIDGE_TOKEN_BSC = "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB";
|
|
23802
|
+
var BRIDGE_DECIMALS = 18;
|
|
23803
|
+
var NEXUS_STATE_MACHINE = "POLKADOT-3367";
|
|
23804
|
+
var NEXUS_CONSENSUS_STATE = "DOT0";
|
|
23805
|
+
var NEXUS_WS_URL = "wss://nexus.rpc.polytope.technology";
|
|
23806
|
+
var NEXUS_UNIT = 10n ** 6n;
|
|
23807
|
+
var QUOTE_SLIPPAGE_BPS = 100n;
|
|
23808
|
+
var ORDER_WINDOW_SECONDS = 30 * 60;
|
|
23809
|
+
var AUCTION_TIME_MS = 15e3;
|
|
23810
|
+
var REFUND_POLL_MS = 1e4;
|
|
23811
|
+
var RECEIPT_TIMEOUT_MS = 12e4;
|
|
23812
|
+
var MAX_EVENTS = 50;
|
|
23813
|
+
var DELIVERY_POLL_MS = 15e3;
|
|
23814
|
+
var DELIVERY_RETRY_MS = 6e4;
|
|
23815
|
+
var SELF_RELAY_GRACE_MS = 9e4;
|
|
23816
|
+
var RELAYER_DELIVERY_WAIT_MS = 3 * 6e4;
|
|
23817
|
+
var STATUS_BACKFILL_MS = 30 * 6e4;
|
|
23818
|
+
var NETWORK_TIMEOUT_MS = 3e4;
|
|
23819
|
+
var LOG_CHUNK_BLOCKS = 300n;
|
|
23820
|
+
var FILL_SEARCH_BEFORE_MS = 3 * 6e4;
|
|
23821
|
+
var FILL_SEARCH_AFTER_MS = 6e4;
|
|
23822
|
+
var ORDER_FILLED_EVENT = IntentGatewayABI.find(
|
|
23823
|
+
(item) => item.type === "event" && item.name === "OrderFilled"
|
|
23824
|
+
);
|
|
23825
|
+
var DELIVERY_REQUEST_QUERY = `query TopUpDelivery($hash: String!) {
|
|
23826
|
+
requestV2s(
|
|
23827
|
+
filter: {
|
|
23828
|
+
source: { equalTo: "EVM-56" }
|
|
23829
|
+
dest: { equalTo: "POLKADOT-3367" }
|
|
23830
|
+
statusMetadata: { some: { transactionHash: { equalTo: $hash } } }
|
|
23831
|
+
}
|
|
23832
|
+
) {
|
|
23833
|
+
nodes { commitment }
|
|
23834
|
+
}
|
|
23835
|
+
}`;
|
|
23836
|
+
var ORDER_QUERY2 = `query TopUpOrder($commitment: String!) {
|
|
23837
|
+
iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
|
|
23838
|
+
nodes {
|
|
23839
|
+
status
|
|
23840
|
+
statusMetadata { nodes { status timestamp transactionHash filler } }
|
|
23841
|
+
}
|
|
23842
|
+
}
|
|
23843
|
+
}`;
|
|
23844
|
+
var CALL_TUPLE = [
|
|
23845
|
+
{
|
|
23846
|
+
type: "tuple[]",
|
|
23847
|
+
components: [
|
|
23848
|
+
{ name: "to", type: "address" },
|
|
23849
|
+
{ name: "value", type: "uint256" },
|
|
23850
|
+
{ name: "data", type: "bytes" }
|
|
23851
|
+
]
|
|
23928
23852
|
}
|
|
23929
|
-
|
|
23853
|
+
];
|
|
23854
|
+
var PENDING_STAGES = /* @__PURE__ */ new Set([
|
|
23855
|
+
"placing",
|
|
23856
|
+
"awaiting_bids",
|
|
23857
|
+
"filling",
|
|
23858
|
+
"delivering",
|
|
23859
|
+
"unfilled",
|
|
23860
|
+
"reclaiming"
|
|
23861
|
+
]);
|
|
23862
|
+
function isBridgeTopUpPending(stage) {
|
|
23863
|
+
return PENDING_STAGES.has(stage);
|
|
23864
|
+
}
|
|
23865
|
+
function encodeBridgeTopUpCall(bridgeToken, recipient, amount) {
|
|
23866
|
+
const data = encodeFunctionData5({
|
|
23867
|
+
abi: HyperFungibleTokenABI,
|
|
23868
|
+
functionName: "send",
|
|
23869
|
+
args: [
|
|
23870
|
+
{
|
|
23871
|
+
dest: toHex4(NEXUS_STATE_MACHINE),
|
|
23872
|
+
to: recipient,
|
|
23873
|
+
amount,
|
|
23874
|
+
timeout: 0n,
|
|
23875
|
+
relayerFee: 0n,
|
|
23876
|
+
data: "0x"
|
|
23877
|
+
}
|
|
23878
|
+
]
|
|
23879
|
+
});
|
|
23880
|
+
return encodeAbiParameters(CALL_TUPLE, [[{ to: bridgeToken, value: 0n, data }]]);
|
|
23881
|
+
}
|
|
23882
|
+
function bidPreview(bid) {
|
|
23883
|
+
return {
|
|
23884
|
+
solverAddress: bid.solverAddress,
|
|
23885
|
+
outputs: bid.outputs.map((output) => ({ token: output.token, amount: output.amount.toString() }))
|
|
23886
|
+
};
|
|
23887
|
+
}
|
|
23888
|
+
function bridgeTopUpOutput(quotedOut) {
|
|
23889
|
+
const afterSlippage = quotedOut * (10000n - QUOTE_SLIPPAGE_BPS) / 10000n;
|
|
23890
|
+
return afterSlippage / NEXUS_UNIT * NEXUS_UNIT;
|
|
23891
|
+
}
|
|
23892
|
+
function serializeOrder(order) {
|
|
23893
|
+
return JSON.stringify(order, (_key, value) => typeof value === "bigint" ? { $bigint: value.toString() } : value);
|
|
23894
|
+
}
|
|
23895
|
+
function deserializeOrder(json) {
|
|
23896
|
+
return JSON.parse(
|
|
23897
|
+
json,
|
|
23898
|
+
(_key, value) => value && typeof value === "object" && typeof value.$bigint === "string" ? BigInt(value.$bigint) : value
|
|
23899
|
+
);
|
|
23900
|
+
}
|
|
23901
|
+
var BridgeTopUp = class {
|
|
23902
|
+
constructor(deps) {
|
|
23903
|
+
this.deps = deps;
|
|
23904
|
+
this.gateways = /* @__PURE__ */ new Map();
|
|
23905
|
+
/** Serializes state writes so a later stage never lands before an earlier one. */
|
|
23906
|
+
this.writes = Promise.resolve();
|
|
23907
|
+
this.logger = moduleLogger(deps.loggers, "bridge-top-up");
|
|
23908
|
+
}
|
|
23909
|
+
get bridgeToken() {
|
|
23910
|
+
return this.deps.configService.getAssetBySymbol(BRIDGE_TOP_UP_CHAIN, "BRIDGE") ?? BRIDGE_TOKEN_BSC;
|
|
23911
|
+
}
|
|
23912
|
+
/** Loads the stored top-up and resumes driving it when it was left open. Never rejects. */
|
|
23913
|
+
restore() {
|
|
23914
|
+
this.loaded ?? (this.loaded = (async () => {
|
|
23915
|
+
try {
|
|
23916
|
+
this.record = (await this.deps.state.get()).bridgeTopUp;
|
|
23917
|
+
} catch (err) {
|
|
23918
|
+
this.logger.warn({ err }, "Could not read the stored BRIDGE top-up");
|
|
23919
|
+
return;
|
|
23920
|
+
}
|
|
23921
|
+
const record = this.record;
|
|
23922
|
+
if (record?.stage === "failed" && record.placeTxHash && !record.commitment && record.sessionPrivateKey) {
|
|
23923
|
+
void this.recoverPlacement();
|
|
23924
|
+
return;
|
|
23925
|
+
}
|
|
23926
|
+
if (!record || !isBridgeTopUpPending(record.stage)) return;
|
|
23927
|
+
if (record.stage === "placing" && !record.placeTxHash) {
|
|
23928
|
+
await this.update({
|
|
23929
|
+
stage: "failed",
|
|
23930
|
+
error: "Simplex restarted before the order was placed. Nothing was escrowed."
|
|
23931
|
+
});
|
|
23932
|
+
return;
|
|
23933
|
+
}
|
|
23934
|
+
if (record.stage === "placing") {
|
|
23935
|
+
void this.recoverPlacement();
|
|
23936
|
+
return;
|
|
23937
|
+
}
|
|
23938
|
+
if (record.stage === "awaiting_bids" || record.stage === "filling") void this.resumeOrder();
|
|
23939
|
+
if (record.stage === "reclaiming") void this.driveReclaim();
|
|
23940
|
+
if (record.stage === "delivering") void this.resumeDelivery();
|
|
23941
|
+
})());
|
|
23942
|
+
return this.loaded;
|
|
23943
|
+
}
|
|
23944
|
+
async current() {
|
|
23945
|
+
await this.restore();
|
|
23946
|
+
return this.record ? publicState(this.record) : void 0;
|
|
23947
|
+
}
|
|
23948
|
+
/** Networks the filler runs and has delegated its account on. */
|
|
23949
|
+
async sourceChains() {
|
|
23950
|
+
const chainIds = this.deps.configService.getConfiguredChainIds();
|
|
23951
|
+
const delegated = await Promise.all(
|
|
23952
|
+
chainIds.map((chainId) => this.deps.delegation.isDelegated(`EVM-${chainId}`).catch(() => false))
|
|
23953
|
+
);
|
|
23954
|
+
return chainIds.filter((_, index) => delegated[index]);
|
|
23955
|
+
}
|
|
23956
|
+
/** USDC and USDT wallet balances on every network a top-up can be paid from. */
|
|
23957
|
+
async sources() {
|
|
23958
|
+
const rows = (await this.sourceChains()).flatMap(
|
|
23959
|
+
(chainId) => ["USDC", "USDT"].map(async (token) => {
|
|
23960
|
+
const chain = `EVM-${chainId}`;
|
|
23961
|
+
let tokenAddress;
|
|
23962
|
+
let decimals;
|
|
23963
|
+
try {
|
|
23964
|
+
;
|
|
23965
|
+
({ tokenAddress, decimals } = this.payToken(chain, token));
|
|
23966
|
+
} catch {
|
|
23967
|
+
return void 0;
|
|
23968
|
+
}
|
|
23969
|
+
const reserve = this.reserve(chain, tokenAddress);
|
|
23970
|
+
const row = { chain, chainId, token, address: tokenAddress, reserve: formatUnits5(reserve, decimals) };
|
|
23971
|
+
try {
|
|
23972
|
+
const balance = await this.walletBalance(chain, tokenAddress);
|
|
23973
|
+
return {
|
|
23974
|
+
...row,
|
|
23975
|
+
balance: formatUnits5(balance, decimals),
|
|
23976
|
+
spendable: formatUnits5(balance > reserve ? balance - reserve : 0n, decimals)
|
|
23977
|
+
};
|
|
23978
|
+
} catch (err) {
|
|
23979
|
+
this.logger.debug({ err, chain, token }, "Could not read a top-up source balance");
|
|
23980
|
+
return { ...row, balance: null, spendable: null };
|
|
23981
|
+
}
|
|
23982
|
+
})
|
|
23983
|
+
);
|
|
23984
|
+
return (await Promise.all(rows)).filter((row) => row !== void 0);
|
|
23985
|
+
}
|
|
23986
|
+
async quote(params) {
|
|
23987
|
+
const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
|
|
23988
|
+
const amountIn = parseUnits5(params.amount, decimals);
|
|
23989
|
+
if (amountIn <= 0n) throw new Error("Amount must be greater than 0");
|
|
23990
|
+
const gateway = await this.gateway(params.chain);
|
|
23991
|
+
const quote = await gateway.quoteIntent({ tokenIn: tokenAddress, tokenOut: this.bridgeToken, amountIn });
|
|
23992
|
+
const bridgeOut = quote.fillable ? bridgeTopUpOutput(quote.amountOut) : 0n;
|
|
23993
|
+
const rate = quote.amountIn > 0n ? Number(formatUnits5(quote.amountOut, BRIDGE_DECIMALS)) / Number(formatUnits5(quote.amountIn, decimals)) : 0;
|
|
23994
|
+
return {
|
|
23995
|
+
chain: params.chain,
|
|
23996
|
+
token: params.token,
|
|
23997
|
+
tokenAddress,
|
|
23998
|
+
amountIn: formatUnits5(amountIn, decimals),
|
|
23999
|
+
bridgeOut: formatUnits5(bridgeOut, BRIDGE_DECIMALS),
|
|
24000
|
+
rate: rate.toString(),
|
|
24001
|
+
fillable: quote.fillable && bridgeOut > 0n,
|
|
24002
|
+
maxFillableIn: formatUnits5(quote.maxFillableIn, decimals),
|
|
24003
|
+
route: quote.route
|
|
24004
|
+
};
|
|
24005
|
+
}
|
|
24006
|
+
/** Re-quotes, then places and drives the order in the background. Returns its state. */
|
|
24007
|
+
async start(params) {
|
|
24008
|
+
if (!(await this.sourceChains()).includes(getChainId4(params.chain) ?? -1)) {
|
|
24009
|
+
throw new Error(`${params.chain} is not a network the filler fills on`);
|
|
24010
|
+
}
|
|
24011
|
+
await this.restore();
|
|
24012
|
+
if (this.record && isBridgeTopUpPending(this.record.stage)) {
|
|
24013
|
+
throw new Error("A BRIDGE top-up is already pending");
|
|
24014
|
+
}
|
|
24015
|
+
const quote = await this.quote(params);
|
|
24016
|
+
if (!quote.fillable) {
|
|
24017
|
+
throw new Error(`The orderbook cannot fill ${quote.amountIn} ${params.token} (max ${quote.maxFillableIn})`);
|
|
24018
|
+
}
|
|
24019
|
+
const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
|
|
24020
|
+
const amountIn = parseUnits5(quote.amountIn, decimals);
|
|
24021
|
+
const balance = await this.balanceOf(params.chain, tokenAddress);
|
|
24022
|
+
if (balance < amountIn) {
|
|
24023
|
+
const reserve = formatUnits5(this.reserve(params.chain, tokenAddress), decimals);
|
|
24024
|
+
throw new Error(
|
|
24025
|
+
`Not enough ${params.token}: ${formatUnits5(balance, decimals)} spendable after the ${reserve} ${params.token} kept for gas, need ${quote.amountIn}`
|
|
24026
|
+
);
|
|
24027
|
+
}
|
|
24028
|
+
const order = await this.buildOrder(params.chain, tokenAddress, amountIn, parseUnits5(quote.bridgeOut, BRIDGE_DECIMALS));
|
|
24029
|
+
if (this.record && isBridgeTopUpPending(this.record.stage)) {
|
|
24030
|
+
throw new Error("A BRIDGE top-up is already pending");
|
|
24031
|
+
}
|
|
24032
|
+
const now = Date.now();
|
|
24033
|
+
this.record = {
|
|
24034
|
+
id: randomUUID2(),
|
|
24035
|
+
chain: params.chain,
|
|
24036
|
+
token: params.token,
|
|
24037
|
+
amountIn: quote.amountIn,
|
|
24038
|
+
bridgeOut: quote.bridgeOut,
|
|
24039
|
+
stage: "placing",
|
|
24040
|
+
createdAt: now,
|
|
24041
|
+
updatedAt: now,
|
|
24042
|
+
stageTimes: { placing: now },
|
|
24043
|
+
order: serializeOrder(order)
|
|
24044
|
+
};
|
|
24045
|
+
await this.persist();
|
|
24046
|
+
void this.place(order, tokenAddress);
|
|
24047
|
+
return publicState(this.record);
|
|
24048
|
+
}
|
|
24049
|
+
/** Cancels an unfilled order and refunds its escrowed input to the filler wallet. */
|
|
24050
|
+
async reclaim() {
|
|
24051
|
+
await this.restore();
|
|
24052
|
+
const record = this.record;
|
|
24053
|
+
if (!record || record.stage !== "unfilled") throw new Error("There is no unfilled top-up to reclaim");
|
|
24054
|
+
await this.update({ stage: "reclaiming", error: void 0 });
|
|
24055
|
+
void this.driveReclaim();
|
|
24056
|
+
return publicState(this.record);
|
|
24057
|
+
}
|
|
24058
|
+
/**
|
|
24059
|
+
* Places the order, then tracks it. The SDK builds the `placeOrder` calldata and the
|
|
24060
|
+
* session key; the receipt is read back here, through the filler's own multi-endpoint
|
|
24061
|
+
* client, rather than by the SDK from one endpoint, where a node that has not caught up
|
|
24062
|
+
* with the new block fails the read and loses track of an order that did land.
|
|
24063
|
+
*/
|
|
24064
|
+
async place(order, tokenIn) {
|
|
24065
|
+
await this.drive(async (gateway) => {
|
|
24066
|
+
const gen = gateway.executeBest(order, SIMPLEX_REFERRER, { auctionTimeMs: AUCTION_TIME_MS });
|
|
24067
|
+
const first = await gen.next();
|
|
24068
|
+
await gen.return();
|
|
24069
|
+
if (first.done || first.value.status !== "AWAITING_PLACE_ORDER") {
|
|
24070
|
+
throw new Error("The SDK did not produce a placement transaction");
|
|
24071
|
+
}
|
|
24072
|
+
const placement = first.value;
|
|
24073
|
+
await this.update({ sessionPrivateKey: placement.sessionPrivateKey });
|
|
24074
|
+
const calls = this.placementCalls(order, tokenIn, placement);
|
|
24075
|
+
const { txHash } = await this.deps.tokenSender.submitCalls(this.record.chain, calls);
|
|
24076
|
+
await this.update({ placeTxHash: txHash });
|
|
24077
|
+
await this.recordPlacement(order, txHash);
|
|
24078
|
+
await this.trackOrder(gateway);
|
|
24079
|
+
});
|
|
24080
|
+
}
|
|
24081
|
+
/**
|
|
24082
|
+
* Reads the placement receipt and stores the order as the gateway committed it. Throws
|
|
24083
|
+
* when the transaction placed no order, so the top-up is marked failed.
|
|
24084
|
+
*/
|
|
24085
|
+
async recordPlacement(order, txHash) {
|
|
24086
|
+
const receipt = await this.deps.clientManager.getPublicClient(this.record.chain).waitForTransactionReceipt({ hash: txHash, timeout: RECEIPT_TIMEOUT_MS });
|
|
24087
|
+
const [placed] = parseEventLogs2({ abi: IntentGatewayABI, logs: receipt.logs, eventName: "OrderPlaced" });
|
|
24088
|
+
if (!placed) throw new Error(`Transaction ${txHash} placed no order`);
|
|
24089
|
+
const canonical = deriveCanonicalPlacedOrder(order, placed.args);
|
|
24090
|
+
await this.update({
|
|
24091
|
+
stage: "awaiting_bids",
|
|
24092
|
+
commitment: canonical.id,
|
|
24093
|
+
order: serializeOrder(canonical),
|
|
24094
|
+
error: void 0
|
|
24095
|
+
});
|
|
24096
|
+
}
|
|
24097
|
+
/** Continues an order whose placement landed but was not recorded. */
|
|
24098
|
+
async recoverPlacement() {
|
|
24099
|
+
await this.drive(async (gateway) => {
|
|
24100
|
+
const record = this.record;
|
|
24101
|
+
await this.recordPlacement(deserializeOrder(record.order), record.placeTxHash);
|
|
24102
|
+
await this.trackOrder(gateway);
|
|
24103
|
+
});
|
|
24104
|
+
}
|
|
24105
|
+
async resumeDelivery() {
|
|
24106
|
+
await this.drive(() => this.deliver());
|
|
24107
|
+
}
|
|
24108
|
+
async resumeOrder() {
|
|
24109
|
+
await this.drive((gateway) => this.trackOrder(gateway));
|
|
24110
|
+
}
|
|
24111
|
+
/** Follows a placed order through bids to a fill, or to its deadline. */
|
|
24112
|
+
async trackOrder(gateway) {
|
|
24113
|
+
const record = this.record;
|
|
24114
|
+
const order = deserializeOrder(record.order);
|
|
24115
|
+
if (await gateway.isOrderFilled(order)) {
|
|
24116
|
+
await this.update({ stage: "delivering" });
|
|
24117
|
+
await this.deliver();
|
|
24118
|
+
return;
|
|
24119
|
+
}
|
|
24120
|
+
if (!record.sessionPrivateKey) {
|
|
24121
|
+
await this.update({ stage: "unfilled", error: "The order's session key was not saved, so it cannot be resumed" });
|
|
24122
|
+
return;
|
|
24123
|
+
}
|
|
24124
|
+
const gen = gateway.resumeBest(order, {
|
|
24125
|
+
sessionPrivateKey: record.sessionPrivateKey,
|
|
24126
|
+
auctionTimeMs: AUCTION_TIME_MS
|
|
24127
|
+
});
|
|
24128
|
+
for await (const value of gen) {
|
|
24129
|
+
if (await this.applyExecutionUpdate(value)) break;
|
|
24130
|
+
}
|
|
24131
|
+
if (this.record.stage === "delivering") await this.deliver();
|
|
24132
|
+
}
|
|
24133
|
+
/**
|
|
24134
|
+
* Carries the bought BRIDGE to Hyperbridge. The order's output call dispatched it from
|
|
24135
|
+
* BSC with no relayer fee. Relayers may still deliver it; when none has shortly after BSC
|
|
24136
|
+
* is final on Hyperbridge, the filler relays it itself: a request into Hyperbridge goes
|
|
24137
|
+
* in as an unsigned extrinsic and costs nothing. Runs until the request is delivered.
|
|
24138
|
+
*/
|
|
24139
|
+
async deliver() {
|
|
24140
|
+
let relayedAt = 0;
|
|
24141
|
+
while (!await this.syncDelivery()) {
|
|
24142
|
+
const finalized = this.record.deliveryStatuses?.find((status2) => status2.status === "SOURCE_FINALIZED");
|
|
24143
|
+
const due = finalized && Date.now() - finalized.timestamp > SELF_RELAY_GRACE_MS;
|
|
24144
|
+
if (due && Date.now() - relayedAt > DELIVERY_RETRY_MS) {
|
|
24145
|
+
relayedAt = Date.now();
|
|
24146
|
+
await this.selfRelay(this.record.deliveryCommitment).catch((err) => {
|
|
24147
|
+
this.logger.warn({ err }, "Self-relay of the BRIDGE to Hyperbridge failed; retrying");
|
|
24148
|
+
});
|
|
24149
|
+
}
|
|
24150
|
+
await sleep(DELIVERY_POLL_MS);
|
|
24151
|
+
}
|
|
24152
|
+
const id = this.record.id;
|
|
24153
|
+
while (this.record?.id === id && this.needsStatusBackfill()) {
|
|
24154
|
+
await sleep(DELIVERY_POLL_MS);
|
|
24155
|
+
await this.syncDelivery().catch((err) => this.logger.debug({ err }, "BRIDGE delivery backfill failed"));
|
|
24156
|
+
}
|
|
24157
|
+
}
|
|
24158
|
+
/** Delivered, but the indexer has yet to record the delivery, and not for too long. */
|
|
24159
|
+
needsStatusBackfill() {
|
|
24160
|
+
const record = this.record;
|
|
24161
|
+
if (!record || record.stage !== "delivered" || !record.deliveryCommitment) return false;
|
|
24162
|
+
if (record.deliveryStatuses?.some((status2) => status2.status === "DESTINATION")) return false;
|
|
24163
|
+
return Date.now() - (record.stageTimes?.delivered ?? 0) < STATUS_BACKFILL_MS;
|
|
24164
|
+
}
|
|
24165
|
+
/**
|
|
24166
|
+
* Reads where the order and its BRIDGE are, from the indexer and the chains, and records
|
|
24167
|
+
* it. Shared by the driver and {@link sync}, and never runs twice at once. True once the
|
|
24168
|
+
* BRIDGE has reached Hyperbridge.
|
|
24169
|
+
*/
|
|
24170
|
+
syncDelivery() {
|
|
24171
|
+
this.deliverySync ?? (this.deliverySync = this.readDelivery().finally(() => {
|
|
24172
|
+
this.deliverySync = void 0;
|
|
24173
|
+
}));
|
|
24174
|
+
return this.deliverySync;
|
|
24175
|
+
}
|
|
24176
|
+
async readDelivery() {
|
|
24177
|
+
const record = this.record;
|
|
24178
|
+
if (record.stage !== "delivering" && record.stage !== "delivered") return false;
|
|
24179
|
+
if (!record.fillTxHash) {
|
|
24180
|
+
const fillTxHash = await this.findFill();
|
|
24181
|
+
if (!fillTxHash) return false;
|
|
24182
|
+
await this.update({ fillTxHash });
|
|
24183
|
+
}
|
|
24184
|
+
if (!record.deliveryCommitment) {
|
|
24185
|
+
await this.update({ deliveryCommitment: await this.deliveryRequest(record.fillTxHash) });
|
|
24186
|
+
}
|
|
24187
|
+
const hyperbridge = await this.hyperbridge();
|
|
24188
|
+
const request = await withTimeout(
|
|
24189
|
+
this.ismpClient(hyperbridge).queryRequestWithStatus(record.deliveryCommitment),
|
|
24190
|
+
"the BRIDGE request status"
|
|
24191
|
+
);
|
|
24192
|
+
const statuses = (request?.statuses ?? []).filter((status2) => DELIVERY_STATUSES.has(status2.status)).map((status2) => ({
|
|
24193
|
+
status: status2.status,
|
|
24194
|
+
timestamp: toMs(status2.metadata.timestamp),
|
|
24195
|
+
txHash: status2.metadata.transactionHash
|
|
24196
|
+
}));
|
|
24197
|
+
if (statuses.length > 0) await this.update({ deliveryStatuses: statuses });
|
|
24198
|
+
const delivered = statuses.some((status2) => status2.status === "DESTINATION") || !!await withTimeout(hyperbridge.queryRequestReceipt(record.deliveryCommitment), "the request receipt");
|
|
24199
|
+
if (delivered && record.stage !== "delivered") await this.update({ stage: "delivered", error: void 0 });
|
|
24200
|
+
return delivered;
|
|
24201
|
+
}
|
|
24202
|
+
/**
|
|
24203
|
+
* The HyperFungibleToken request the fill dispatched to Hyperbridge. The indexer finds it by
|
|
24204
|
+
* the fill's transaction, as the explorer's search does; the fill's own `Sent` log is the
|
|
24205
|
+
* fallback while the indexer catches up.
|
|
24206
|
+
*/
|
|
24207
|
+
async deliveryRequest(fillTxHash) {
|
|
24208
|
+
const response = await withTimeout(
|
|
24209
|
+
fetch(this.deps.indexerUrl, {
|
|
24210
|
+
method: "POST",
|
|
24211
|
+
headers: { "content-type": "application/json" },
|
|
24212
|
+
body: JSON.stringify({ query: DELIVERY_REQUEST_QUERY, variables: { hash: fillTxHash } })
|
|
24213
|
+
}),
|
|
24214
|
+
"the indexer"
|
|
24215
|
+
);
|
|
24216
|
+
const body = await response.json();
|
|
24217
|
+
const indexed = body.data?.requestV2s?.nodes[0]?.commitment;
|
|
24218
|
+
if (indexed) return indexed;
|
|
24219
|
+
const fill = await withTimeout(this.bsc().getTransactionReceipt({ hash: fillTxHash }), "the fill receipt");
|
|
24220
|
+
const [sent] = parseEventLogs2({ abi: HyperFungibleTokenABI, logs: fill.logs, eventName: "Sent" });
|
|
24221
|
+
if (!sent) throw new Error(`The fill ${fillTxHash} sent no BRIDGE to Hyperbridge`);
|
|
24222
|
+
return sent.args.commitment;
|
|
24223
|
+
}
|
|
24224
|
+
/**
|
|
24225
|
+
* BSC, read through every endpoint there is: the filler need not run it, and a single public
|
|
24226
|
+
* endpoint refuses receipts it considers old.
|
|
24227
|
+
*/
|
|
24228
|
+
bsc() {
|
|
24229
|
+
this.bscClient ?? (this.bscClient = createPublicClient3({
|
|
24230
|
+
chain: bscChain,
|
|
24231
|
+
transport: fallback2(this.bscProofRpcUrls().map((url) => http3(url, { timeout: 1e4, retryCount: 0 })))
|
|
24232
|
+
}));
|
|
24233
|
+
return this.bscClient;
|
|
24234
|
+
}
|
|
24235
|
+
/**
|
|
24236
|
+
* The transaction that filled the order. The executor reports it only for a fill this
|
|
24237
|
+
* process selected, so it comes from the indexer, or, while the indexer has yet to reach
|
|
24238
|
+
* BSC's latest blocks, from the gateway's `OrderFilled` log around when the fill was seen.
|
|
24239
|
+
*/
|
|
24240
|
+
async findFill() {
|
|
24241
|
+
const record = this.record;
|
|
24242
|
+
const indexed = (await this.indexedOrder())?.find((status2) => status2.status === "FILLED");
|
|
24243
|
+
if (indexed) {
|
|
24244
|
+
if (indexed.filler && !record.filler) await this.update({ filler: indexed.filler });
|
|
24245
|
+
return indexed.transactionHash;
|
|
24246
|
+
}
|
|
24247
|
+
const seenAt = record.events?.find((event) => event.status === "FILLED")?.timestamp ?? record.stageTimes?.delivering;
|
|
24248
|
+
if (!seenAt) return void 0;
|
|
24249
|
+
const client = this.bsc();
|
|
24250
|
+
const [latest, blockTime] = await Promise.all([
|
|
24251
|
+
withTimeout(client.getBlock(), "the latest BSC block"),
|
|
24252
|
+
this.blockTimeSeconds()
|
|
24253
|
+
]);
|
|
24254
|
+
const blockAt = (ms) => latest.number - BigInt(Math.ceil((Number(latest.timestamp) - ms / 1e3) / blockTime));
|
|
24255
|
+
const from = blockAt(seenAt - FILL_SEARCH_BEFORE_MS);
|
|
24256
|
+
const to = (() => {
|
|
24257
|
+
const end = blockAt(seenAt + FILL_SEARCH_AFTER_MS);
|
|
24258
|
+
return end < latest.number ? end : latest.number;
|
|
24259
|
+
})();
|
|
24260
|
+
for (let start = from; start <= to; start += LOG_CHUNK_BLOCKS) {
|
|
24261
|
+
const end = start + LOG_CHUNK_BLOCKS - 1n < to ? start + LOG_CHUNK_BLOCKS - 1n : to;
|
|
24262
|
+
const logs = await withTimeout(
|
|
24263
|
+
client.getLogs({
|
|
24264
|
+
address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
|
|
24265
|
+
event: ORDER_FILLED_EVENT,
|
|
24266
|
+
args: { commitment: record.commitment },
|
|
24267
|
+
fromBlock: start,
|
|
24268
|
+
toBlock: end
|
|
24269
|
+
}),
|
|
24270
|
+
"the fill on BSC"
|
|
24271
|
+
);
|
|
24272
|
+
if (logs[0]) return logs[0].transactionHash;
|
|
24273
|
+
}
|
|
24274
|
+
return void 0;
|
|
24275
|
+
}
|
|
24276
|
+
/** The order's statuses as the indexer has them: placed, filled, redeemed or refunded. */
|
|
24277
|
+
async indexedOrder() {
|
|
24278
|
+
const response = await withTimeout(
|
|
24279
|
+
fetch(this.deps.indexerUrl, {
|
|
24280
|
+
method: "POST",
|
|
24281
|
+
headers: { "content-type": "application/json" },
|
|
24282
|
+
body: JSON.stringify({ query: ORDER_QUERY2, variables: { commitment: this.record.commitment } })
|
|
24283
|
+
}),
|
|
24284
|
+
"the indexer"
|
|
24285
|
+
);
|
|
24286
|
+
const body = await response.json();
|
|
24287
|
+
return body.data?.iOrderV3s?.nodes[0]?.statusMetadata.nodes.map((node) => ({
|
|
24288
|
+
status: node.status,
|
|
24289
|
+
timestamp: toMs(node.timestamp),
|
|
24290
|
+
transactionHash: node.transactionHash,
|
|
24291
|
+
filler: node.filler ?? void 0
|
|
24292
|
+
}));
|
|
24293
|
+
}
|
|
24294
|
+
/**
|
|
24295
|
+
* Brings a pending top-up up to date from the chains and the indexer, for a dashboard that
|
|
24296
|
+
* is showing it: someone may have filled, delivered or refunded it while this process was
|
|
24297
|
+
* not looking. One sync at a time; a call made while one runs waits for that one.
|
|
24298
|
+
*/
|
|
24299
|
+
sync() {
|
|
24300
|
+
this.syncing ?? (this.syncing = this.reconcile().catch((err) => this.logger.debug({ err }, "BRIDGE top-up sync failed")).finally(() => {
|
|
24301
|
+
this.syncing = void 0;
|
|
24302
|
+
}));
|
|
24303
|
+
return this.syncing;
|
|
24304
|
+
}
|
|
24305
|
+
async reconcile() {
|
|
24306
|
+
await this.restore();
|
|
24307
|
+
const record = this.record;
|
|
24308
|
+
if (this.needsStatusBackfill()) {
|
|
24309
|
+
await this.syncDelivery();
|
|
24310
|
+
return;
|
|
24311
|
+
}
|
|
24312
|
+
if (!record || !isBridgeTopUpPending(record.stage) || !record.commitment) return;
|
|
24313
|
+
if (record.stage === "awaiting_bids" || record.stage === "filling") {
|
|
24314
|
+
const statuses = await this.indexedOrder();
|
|
24315
|
+
const fill = statuses?.find((status2) => status2.status === "FILLED");
|
|
24316
|
+
if (fill) {
|
|
24317
|
+
await this.update({
|
|
24318
|
+
stage: "delivering",
|
|
24319
|
+
fillTxHash: fill.transactionHash,
|
|
24320
|
+
filler: fill.filler ?? record.filler,
|
|
24321
|
+
stageTimes: { ...record.stageTimes, delivering: fill.timestamp }
|
|
24322
|
+
});
|
|
24323
|
+
void this.resumeDelivery();
|
|
24324
|
+
} else if (statuses?.some((status2) => status2.status === "REFUNDED")) {
|
|
24325
|
+
await this.update({ stage: "reclaimed" });
|
|
24326
|
+
}
|
|
24327
|
+
return;
|
|
24328
|
+
}
|
|
24329
|
+
if (record.stage === "delivering") {
|
|
24330
|
+
await this.syncDelivery();
|
|
24331
|
+
return;
|
|
24332
|
+
}
|
|
24333
|
+
if (record.stage === "unfilled" || record.stage === "reclaiming") {
|
|
24334
|
+
const gateway = await this.gateway(record.chain);
|
|
24335
|
+
await this.refunded(gateway, deserializeOrder(record.order));
|
|
24336
|
+
}
|
|
24337
|
+
}
|
|
24338
|
+
/**
|
|
24339
|
+
* Submits the request to Hyperbridge with a proof from BSC state. Public BSC endpoints
|
|
24340
|
+
* differ in whether they serve `eth_getProof` for recent blocks, so each is tried in turn.
|
|
24341
|
+
*/
|
|
24342
|
+
async selfRelay(commitment) {
|
|
24343
|
+
const hyperbridge = await this.hyperbridge();
|
|
24344
|
+
const failures = [];
|
|
24345
|
+
for (const rpcUrl of this.bscProofRpcUrls()) {
|
|
24346
|
+
if (await withTimeout(hyperbridge.queryRequestReceipt(commitment), "the request receipt")) return;
|
|
24347
|
+
try {
|
|
24348
|
+
const client = this.ismpClient(hyperbridge, rpcUrl);
|
|
24349
|
+
const receipt = await withTimeout(client.aggregateTransactionWithCommitment(commitment), "the relay", 12e4);
|
|
24350
|
+
await this.update({ deliveryTxHash: receipt.transactionHash });
|
|
24351
|
+
return;
|
|
24352
|
+
} catch (err) {
|
|
24353
|
+
const reason = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
24354
|
+
failures.push(`${new URL(rpcUrl).host}: ${reason}`);
|
|
24355
|
+
}
|
|
24356
|
+
}
|
|
24357
|
+
throw new Error(`Could not relay the BRIDGE to Hyperbridge (${failures.join("; ")})`);
|
|
24358
|
+
}
|
|
24359
|
+
/** The operator's BSC endpoints first, then the free public ones. */
|
|
24360
|
+
bscProofRpcUrls() {
|
|
24361
|
+
const urls = [
|
|
24362
|
+
...this.deps.configService.getRpcUrls(BRIDGE_TOP_UP_CHAIN),
|
|
24363
|
+
...chainByChainId(getChainId4(BRIDGE_TOP_UP_CHAIN))?.defaultRpcUrls ?? []
|
|
24364
|
+
];
|
|
24365
|
+
return urls.filter((url, index) => url && urls.indexOf(url) === index);
|
|
24366
|
+
}
|
|
24367
|
+
/** Records an executor update. True when the order reached a final state. */
|
|
24368
|
+
async applyExecutionUpdate(value) {
|
|
24369
|
+
switch (value.status) {
|
|
24370
|
+
case "NEW_BID":
|
|
24371
|
+
case "BIDS_RECEIVED": {
|
|
24372
|
+
const bids = value.status === "NEW_BID" ? [value.bid] : value.bids;
|
|
24373
|
+
const bidCount = value.status === "BIDS_RECEIVED" ? Number(value.bidCount ?? bids.length) : void 0;
|
|
24374
|
+
await this.recordEvent({ status: value.status, bidCount, bids: bids.map(bidPreview) });
|
|
24375
|
+
if (bidCount !== void 0) await this.update({ bidCount: Math.max(this.record.bidCount ?? 0, bidCount) });
|
|
24376
|
+
return false;
|
|
24377
|
+
}
|
|
24378
|
+
case "BID_SELECTED":
|
|
24379
|
+
await this.recordEvent({
|
|
24380
|
+
status: "BID_SELECTED",
|
|
24381
|
+
selectedSolver: value.selectedSolver,
|
|
24382
|
+
userOpHash: value.userOpHash,
|
|
24383
|
+
txHash: value.transactionHash
|
|
24384
|
+
});
|
|
24385
|
+
await this.update({ stage: "filling", fillTxHash: value.transactionHash });
|
|
24386
|
+
return false;
|
|
24387
|
+
case "FILLED":
|
|
24388
|
+
await this.recordEvent({
|
|
24389
|
+
status: "FILLED",
|
|
24390
|
+
selectedSolver: value.selectedSolver,
|
|
24391
|
+
userOpHash: value.userOpHash,
|
|
24392
|
+
txHash: value.transactionHash
|
|
24393
|
+
});
|
|
24394
|
+
await this.update({
|
|
24395
|
+
stage: "delivering",
|
|
24396
|
+
fillTxHash: value.transactionHash ?? this.record.fillTxHash,
|
|
24397
|
+
error: void 0
|
|
24398
|
+
});
|
|
24399
|
+
return true;
|
|
24400
|
+
case "EXPIRED":
|
|
24401
|
+
case "FAILED":
|
|
24402
|
+
await this.update({ stage: "unfilled", error: value.error });
|
|
24403
|
+
return true;
|
|
24404
|
+
case "CANCELLED":
|
|
24405
|
+
await this.update({ stage: "reclaimed" });
|
|
24406
|
+
return true;
|
|
24407
|
+
default:
|
|
24408
|
+
return false;
|
|
24409
|
+
}
|
|
24410
|
+
}
|
|
24411
|
+
/**
|
|
24412
|
+
* Recovers an unfilled order's escrow, following the Hyperbridge app's recovery: the SDK
|
|
24413
|
+
* cancel flow from the paying chain (BSC's expiry height finalized, the cancel sent, the
|
|
24414
|
+
* cancellation finalized on the source, verified and finalized on Hyperbridge), then the
|
|
24415
|
+
* refund. A relayer normally delivers that refund; if none has within a few minutes of
|
|
24416
|
+
* Hyperbridge finalizing, the filler submits Hyperbridge's response itself. Each step is
|
|
24417
|
+
* recorded as it lands, and a restart resumes from the SDK's persisted progress.
|
|
24418
|
+
*/
|
|
24419
|
+
async driveReclaim() {
|
|
24420
|
+
await this.drive(async (gateway) => {
|
|
24421
|
+
const record = this.record;
|
|
24422
|
+
const order = deserializeOrder(record.order);
|
|
24423
|
+
if (await this.refunded(gateway, order)) return;
|
|
24424
|
+
const sameChain = record.chain === BRIDGE_TOP_UP_CHAIN;
|
|
24425
|
+
const ismp = await this.cancelClient(record.chain);
|
|
24426
|
+
const gen = gateway.cancelOrder(order, ismp);
|
|
24427
|
+
let input;
|
|
24428
|
+
while (true) {
|
|
24429
|
+
const { value, done } = await gen.next(input);
|
|
24430
|
+
input = void 0;
|
|
24431
|
+
if (done) break;
|
|
24432
|
+
switch (value.status) {
|
|
24433
|
+
case "DESTINATION_FINALIZED":
|
|
24434
|
+
await this.recordRecoveryStep("DESTINATION_FINALIZED");
|
|
24435
|
+
break;
|
|
24436
|
+
case "AWAITING_CANCEL_TRANSACTION": {
|
|
24437
|
+
if (this.record.reclaimTxHash && !this.hasRecoveryStep("ORDER_CANCELED")) {
|
|
24438
|
+
input = this.record.reclaimTxHash;
|
|
24439
|
+
break;
|
|
24440
|
+
}
|
|
24441
|
+
const calls = [];
|
|
24442
|
+
const quote = await gateway.quoteCancelOrder(order);
|
|
24443
|
+
if (value.value === 0n && quote.relayerFee > 0n) {
|
|
24444
|
+
const feeToken = await this.feeToken(record.chain);
|
|
24445
|
+
calls.push({
|
|
24446
|
+
target: feeToken,
|
|
24447
|
+
value: 0n,
|
|
24448
|
+
data: encodeFunctionData5({
|
|
24449
|
+
abi: ERC20_ABI,
|
|
24450
|
+
functionName: "approve",
|
|
24451
|
+
args: [value.to, quote.relayerFee]
|
|
24452
|
+
})
|
|
24453
|
+
});
|
|
24454
|
+
}
|
|
24455
|
+
calls.push({ target: value.to, value: value.value, data: value.data });
|
|
24456
|
+
const { txHash } = await this.deps.tokenSender.submitCalls(record.chain, calls);
|
|
24457
|
+
await this.update({ reclaimTxHash: txHash });
|
|
24458
|
+
input = txHash;
|
|
24459
|
+
break;
|
|
24460
|
+
}
|
|
24461
|
+
case "CANCEL_STARTED":
|
|
24462
|
+
await this.recordRecoveryStep("ORDER_CANCELED", value.receipt.transactionHash);
|
|
24463
|
+
break;
|
|
24464
|
+
case "SOURCE_FINALIZED":
|
|
24465
|
+
case "HYPERBRIDGE_DELIVERED":
|
|
24466
|
+
case "HYPERBRIDGE_FINALIZED": {
|
|
24467
|
+
const metadata = value.metadata;
|
|
24468
|
+
await this.recordRecoveryStep(
|
|
24469
|
+
value.status,
|
|
24470
|
+
metadata.transactionHash,
|
|
24471
|
+
metadata.timestamp ? toMs(metadata.timestamp) : void 0
|
|
24472
|
+
);
|
|
24473
|
+
if (value.status === "HYPERBRIDGE_FINALIZED" && metadata.calldata) {
|
|
24474
|
+
await this.update({ claimCalldata: metadata.calldata });
|
|
24475
|
+
}
|
|
24476
|
+
break;
|
|
24477
|
+
}
|
|
24478
|
+
case "CANCELLATION_COMPLETE":
|
|
24479
|
+
await this.recordRecoveryStep("ORDER_CANCELED", value.transactionHash);
|
|
24480
|
+
await this.recordRecoveryStep("FUNDS_CLAIMED", value.transactionHash);
|
|
24481
|
+
break;
|
|
24482
|
+
}
|
|
24483
|
+
}
|
|
24484
|
+
if (sameChain && this.hasRecoveryStep("FUNDS_CLAIMED")) {
|
|
24485
|
+
await this.update({ stage: "reclaimed", error: void 0 });
|
|
24486
|
+
return;
|
|
24487
|
+
}
|
|
24488
|
+
while (!await this.refunded(gateway, order)) {
|
|
24489
|
+
const finalizedAt = this.record.recoverySteps?.find((s) => s.step === "HYPERBRIDGE_FINALIZED")?.timestamp;
|
|
24490
|
+
const claimable = this.record.claimCalldata && finalizedAt && Date.now() - finalizedAt > RELAYER_DELIVERY_WAIT_MS;
|
|
24491
|
+
if (claimable && !this.record.claimTxHash) {
|
|
24492
|
+
await this.claimRefund(record.chain, this.record.claimCalldata);
|
|
24493
|
+
continue;
|
|
24494
|
+
}
|
|
24495
|
+
await sleep(REFUND_POLL_MS);
|
|
24496
|
+
}
|
|
24497
|
+
});
|
|
24498
|
+
}
|
|
24499
|
+
/**
|
|
24500
|
+
* Submits Hyperbridge's finalized response to the source chain's ISMP handler, which
|
|
24501
|
+
* refunds the escrow, as the Hyperbridge app's "Claim funds" does.
|
|
24502
|
+
*/
|
|
24503
|
+
async claimRefund(chain, calldata) {
|
|
24504
|
+
const params = await withTimeout(
|
|
24505
|
+
this.deps.clientManager.getPublicClient(chain).readContract({
|
|
24506
|
+
abi: EVM_HOST,
|
|
24507
|
+
address: this.deps.configService.getHostAddress(chain),
|
|
24508
|
+
functionName: "hostParams"
|
|
24509
|
+
}),
|
|
24510
|
+
"the host's handler"
|
|
24511
|
+
);
|
|
24512
|
+
const { txHash } = await this.deps.tokenSender.submitCalls(chain, [
|
|
24513
|
+
{ target: params.handler, value: 0n, data: calldata }
|
|
24514
|
+
]);
|
|
24515
|
+
await this.update({ claimTxHash: txHash });
|
|
24516
|
+
}
|
|
24517
|
+
/**
|
|
24518
|
+
* Whether the escrow is back with the filler, from the source gateway or the indexer's
|
|
24519
|
+
* REFUNDED status; records the refund as the recovery's last step when it is.
|
|
24520
|
+
*/
|
|
24521
|
+
async refunded(gateway, order) {
|
|
24522
|
+
const onChain = await withTimeout(gateway.isOrderRefunded(order), "the refund").catch(() => false);
|
|
24523
|
+
const indexed = (await this.indexedOrder().catch(() => void 0))?.find((s) => s.status === "REFUNDED");
|
|
24524
|
+
if (!onChain && !indexed) return false;
|
|
24525
|
+
if (!this.hasRecoveryStep("FUNDS_CLAIMED")) {
|
|
24526
|
+
await this.recordRecoveryStep(
|
|
24527
|
+
"FUNDS_CLAIMED",
|
|
24528
|
+
this.record.claimTxHash ?? indexed?.transactionHash,
|
|
24529
|
+
indexed?.timestamp
|
|
24530
|
+
);
|
|
24531
|
+
}
|
|
24532
|
+
await this.update({ stage: "reclaimed", error: void 0 });
|
|
24533
|
+
return true;
|
|
24534
|
+
}
|
|
24535
|
+
hasRecoveryStep(step) {
|
|
24536
|
+
return !!this.record?.recoverySteps?.some((entry) => entry.step === step);
|
|
24537
|
+
}
|
|
24538
|
+
/** Records a recovery step once, at the time it landed (or now). */
|
|
24539
|
+
async recordRecoveryStep(step, txHash, timestamp) {
|
|
24540
|
+
if (this.hasRecoveryStep(step)) return;
|
|
24541
|
+
const steps = [...this.record.recoverySteps ?? [], { step, txHash, timestamp: timestamp ?? Date.now() }];
|
|
24542
|
+
await this.update({ recoverySteps: steps });
|
|
24543
|
+
}
|
|
24544
|
+
/** Runs one background flow for the record; a failure is recorded rather than thrown. */
|
|
24545
|
+
async drive(flow) {
|
|
24546
|
+
const record = this.record;
|
|
24547
|
+
if (!record || this.driving === record.id) return;
|
|
24548
|
+
this.driving = record.id;
|
|
24549
|
+
try {
|
|
24550
|
+
await flow(await this.gateway(record.chain));
|
|
24551
|
+
} catch (err) {
|
|
24552
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
24553
|
+
this.logger.warn({ err, id: record.id, stage: record.stage }, "BRIDGE top-up step failed");
|
|
24554
|
+
if (record.stage === "placing" && !record.placeTxHash) {
|
|
24555
|
+
await this.update({ stage: "failed", error });
|
|
24556
|
+
} else if (record.stage === "delivering") {
|
|
24557
|
+
await this.update({ error: `Delivery to Hyperbridge is retrying: ${error}` });
|
|
24558
|
+
setTimeout(() => void this.resumeDelivery(), DELIVERY_RETRY_MS);
|
|
24559
|
+
} else if (record.stage === "reclaiming") {
|
|
24560
|
+
await this.update({ error: `Recovery is retrying: ${error}` });
|
|
24561
|
+
setTimeout(() => void this.driveReclaim(), DELIVERY_RETRY_MS);
|
|
24562
|
+
} else if (record.stage === "placing") {
|
|
24563
|
+
await this.update({ stage: "failed", error: `${error} (placement tx ${record.placeTxHash})` });
|
|
24564
|
+
} else {
|
|
24565
|
+
await this.update({ stage: "unfilled", error });
|
|
24566
|
+
}
|
|
24567
|
+
} finally {
|
|
24568
|
+
if (this.driving === record.id) this.driving = void 0;
|
|
24569
|
+
this.logger.info(publicState(record), "BRIDGE top-up step finished");
|
|
24570
|
+
}
|
|
24571
|
+
}
|
|
24572
|
+
/** Keeps what the executor reported, for the timeline's offers and its links. */
|
|
24573
|
+
async recordEvent(event) {
|
|
24574
|
+
const events = [...this.record.events ?? [], { ...event, timestamp: Date.now() }];
|
|
24575
|
+
await this.update({ events: events.slice(-MAX_EVENTS) });
|
|
24576
|
+
}
|
|
24577
|
+
async update(patch) {
|
|
24578
|
+
if (!this.record) return;
|
|
24579
|
+
const now = Date.now();
|
|
24580
|
+
if (patch.stage && patch.stage !== this.record.stage) {
|
|
24581
|
+
patch = { ...patch, stageTimes: { ...this.record.stageTimes, [patch.stage]: now } };
|
|
24582
|
+
}
|
|
24583
|
+
Object.assign(this.record, patch, { updatedAt: now });
|
|
24584
|
+
await this.persist();
|
|
24585
|
+
}
|
|
24586
|
+
persist() {
|
|
24587
|
+
const snapshot = this.record ? { ...this.record } : void 0;
|
|
24588
|
+
const write = this.writes.then(() => patchRuntimeState(this.deps.state, { bridgeTopUp: snapshot }));
|
|
24589
|
+
this.writes = write.catch((err) => this.logger.warn({ err }, "Could not save the BRIDGE top-up"));
|
|
24590
|
+
return write.then(
|
|
24591
|
+
() => void 0,
|
|
24592
|
+
() => void 0
|
|
24593
|
+
);
|
|
24594
|
+
}
|
|
24595
|
+
async buildOrder(chain, tokenIn, amountIn, bridgeOut) {
|
|
24596
|
+
const dispatcher = await this.callDispatcher();
|
|
24597
|
+
const bsc = this.bsc();
|
|
24598
|
+
const [block, blockTime] = await Promise.all([bsc.getBlockNumber(), this.blockTimeSeconds()]);
|
|
24599
|
+
return {
|
|
24600
|
+
user: bytes20ToBytes323(this.deps.solver),
|
|
24601
|
+
source: chain,
|
|
24602
|
+
destination: BRIDGE_TOP_UP_CHAIN,
|
|
24603
|
+
deadline: block + BigInt(Math.ceil(ORDER_WINDOW_SECONDS / blockTime)),
|
|
24604
|
+
nonce: 0n,
|
|
24605
|
+
fees: 0n,
|
|
24606
|
+
session: "0x0000000000000000000000000000000000000000",
|
|
24607
|
+
predispatch: { assets: [], call: "0x" },
|
|
24608
|
+
inputs: [{ token: bytes20ToBytes323(tokenIn), amount: amountIn }],
|
|
24609
|
+
output: {
|
|
24610
|
+
beneficiary: bytes20ToBytes323(dispatcher),
|
|
24611
|
+
assets: [{ token: bytes20ToBytes323(this.bridgeToken), amount: bridgeOut }],
|
|
24612
|
+
call: encodeBridgeTopUpCall(this.bridgeToken, this.deps.substratePublicKey, bridgeOut)
|
|
24613
|
+
}
|
|
24614
|
+
};
|
|
24615
|
+
}
|
|
24616
|
+
/**
|
|
24617
|
+
* Approvals for the gateway to pull the input and the order fee, then `placeOrder`.
|
|
24618
|
+
* The fee token is often the input token itself; one approval then covers both.
|
|
24619
|
+
*/
|
|
24620
|
+
placementCalls(order, tokenIn, placement) {
|
|
24621
|
+
const amountIn = order.inputs[0].amount;
|
|
24622
|
+
const approvals = /* @__PURE__ */ new Map([[tokenIn.toLowerCase(), amountIn]]);
|
|
24623
|
+
if (placement.feeTokenAmount > 0n) {
|
|
24624
|
+
const fee = placement.feeTokenAddress.toLowerCase();
|
|
24625
|
+
approvals.set(fee, (approvals.get(fee) ?? 0n) + placement.feeTokenAmount);
|
|
24626
|
+
}
|
|
24627
|
+
const calls = [...approvals].map(([token, amount]) => ({
|
|
24628
|
+
target: token,
|
|
24629
|
+
value: 0n,
|
|
24630
|
+
data: encodeFunctionData5({ abi: ERC20_ABI, functionName: "approve", args: [placement.to, amount] })
|
|
24631
|
+
}));
|
|
24632
|
+
calls.push({ target: placement.to, value: placement.value, data: placement.data });
|
|
24633
|
+
return calls;
|
|
24634
|
+
}
|
|
24635
|
+
payToken(chain, token) {
|
|
24636
|
+
const { configService } = this.deps;
|
|
24637
|
+
const tokenAddress = token === "USDC" ? configService.getUsdcAsset(chain) : configService.getUsdtAsset(chain);
|
|
24638
|
+
if (!tokenAddress || /^0x0+$/.test(tokenAddress)) throw new Error(`${token} is not configured on ${chain}`);
|
|
24639
|
+
const decimals = token === "USDC" ? configService.getUsdcDecimals(chain) : configService.getUsdtDecimals(chain);
|
|
24640
|
+
return { tokenAddress, decimals };
|
|
24641
|
+
}
|
|
24642
|
+
async walletBalance(chain, token) {
|
|
24643
|
+
return await this.deps.clientManager.getPublicClient(chain).readContract({
|
|
24644
|
+
abi: ERC20_ABI,
|
|
24645
|
+
address: token,
|
|
24646
|
+
functionName: "balanceOf",
|
|
24647
|
+
args: [this.deps.solver]
|
|
24648
|
+
});
|
|
24649
|
+
}
|
|
24650
|
+
/** What the paymaster pulls for gas on the sponsored path, which a top-up must leave. */
|
|
24651
|
+
reserve(chain, token) {
|
|
24652
|
+
return paymasterReserveForToken(chain, token.toLowerCase(), this.deps.configService);
|
|
24653
|
+
}
|
|
24654
|
+
/** Spendable wallet balance: the paymaster's gas reserve is held back. */
|
|
24655
|
+
async balanceOf(chain, token) {
|
|
24656
|
+
const balance = await this.walletBalance(chain, token);
|
|
24657
|
+
const reserve = this.reserve(chain, token);
|
|
24658
|
+
return balance > reserve ? balance - reserve : 0n;
|
|
24659
|
+
}
|
|
24660
|
+
async feeToken(chain) {
|
|
24661
|
+
return await this.deps.clientManager.getPublicClient(chain).readContract({
|
|
24662
|
+
abi: [{ type: "function", name: "feeToken", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] }],
|
|
24663
|
+
address: this.deps.configService.getHostAddress(chain),
|
|
24664
|
+
functionName: "feeToken"
|
|
24665
|
+
});
|
|
24666
|
+
}
|
|
24667
|
+
/** The CallDispatcher the BSC gateway runs output calls through, read from the gateway itself. */
|
|
24668
|
+
callDispatcher() {
|
|
24669
|
+
this.dispatcher ?? (this.dispatcher = (async () => {
|
|
24670
|
+
const params = await this.bsc().readContract({
|
|
24671
|
+
abi: INTENT_GATEWAY_V2_ABI,
|
|
24672
|
+
address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
|
|
24673
|
+
functionName: "params"
|
|
24674
|
+
});
|
|
24675
|
+
return params.dispatcher;
|
|
24676
|
+
})().catch((err) => {
|
|
24677
|
+
this.dispatcher = void 0;
|
|
24678
|
+
throw err;
|
|
24679
|
+
}));
|
|
24680
|
+
return this.dispatcher;
|
|
24681
|
+
}
|
|
24682
|
+
async blockTimeSeconds() {
|
|
24683
|
+
const client = this.bsc();
|
|
24684
|
+
const latest = await client.getBlock();
|
|
24685
|
+
const earlier = await client.getBlock({ blockNumber: latest.number - 100n });
|
|
24686
|
+
const seconds = Number(latest.timestamp - earlier.timestamp) / 100;
|
|
24687
|
+
return seconds > 0 ? seconds : 1;
|
|
24688
|
+
}
|
|
24689
|
+
evmChain(chain, rpcUrl) {
|
|
24690
|
+
const { configService } = this.deps;
|
|
24691
|
+
const bundlerUrl = chain === BRIDGE_TOP_UP_CHAIN ? configService.getBundlerUrl(chain) ?? hyperbridgeBundlerUrl(getChainId4(chain)) : void 0;
|
|
24692
|
+
return EvmChain2.fromParams({
|
|
24693
|
+
chainId: getChainId4(chain),
|
|
24694
|
+
host: configService.getHostAddress(chain),
|
|
24695
|
+
rpcUrl: rpcUrl ?? configService.getRpcUrl(chain),
|
|
24696
|
+
bundlerUrl
|
|
24697
|
+
});
|
|
24698
|
+
}
|
|
24699
|
+
gateway(source) {
|
|
24700
|
+
let gateway = this.gateways.get(source);
|
|
24701
|
+
if (!gateway) {
|
|
24702
|
+
gateway = (async () => IntentGateway2.create(
|
|
24703
|
+
this.evmChain(source),
|
|
24704
|
+
this.evmChain(BRIDGE_TOP_UP_CHAIN),
|
|
24705
|
+
await this.deps.hyperbridge,
|
|
24706
|
+
{ storagePath: this.deps.storagePath }
|
|
24707
|
+
))().catch((err) => {
|
|
24708
|
+
this.gateways.delete(source);
|
|
24709
|
+
throw err;
|
|
24710
|
+
});
|
|
24711
|
+
this.gateways.set(source, gateway);
|
|
24712
|
+
}
|
|
24713
|
+
return gateway;
|
|
24714
|
+
}
|
|
24715
|
+
hyperbridge() {
|
|
24716
|
+
this.hyperbridgeChain ?? (this.hyperbridgeChain = withTimeout(SubstrateChain.connect({
|
|
24717
|
+
wsUrl: this.deps.configService.getHyperbridgeWsUrl() ?? NEXUS_WS_URL,
|
|
24718
|
+
consensusStateId: NEXUS_CONSENSUS_STATE,
|
|
24719
|
+
hasher: "Keccak",
|
|
24720
|
+
stateMachineId: NEXUS_STATE_MACHINE
|
|
24721
|
+
}), "Hyperbridge").catch((err) => {
|
|
24722
|
+
this.hyperbridgeChain = void 0;
|
|
24723
|
+
throw err;
|
|
24724
|
+
}));
|
|
24725
|
+
return this.hyperbridgeChain;
|
|
24726
|
+
}
|
|
24727
|
+
/** Tracks requests from BSC into Hyperbridge, the route the bought BRIDGE takes. */
|
|
24728
|
+
ismpClient(hyperbridge, rpcUrl) {
|
|
24729
|
+
return new IsmpClient({
|
|
24730
|
+
queryClient: createQueryClient({ url: this.deps.indexerUrl }),
|
|
24731
|
+
source: this.evmChain(BRIDGE_TOP_UP_CHAIN, rpcUrl),
|
|
24732
|
+
dest: hyperbridge,
|
|
24733
|
+
hyperbridge
|
|
24734
|
+
});
|
|
24735
|
+
}
|
|
24736
|
+
/** Tracks the reclaim's cancellation, which runs between the paying chain and BSC. */
|
|
24737
|
+
async cancelClient(source) {
|
|
24738
|
+
return new IsmpClient({
|
|
24739
|
+
queryClient: createQueryClient({ url: this.deps.indexerUrl }),
|
|
24740
|
+
source: this.evmChain(source),
|
|
24741
|
+
dest: this.evmChain(BRIDGE_TOP_UP_CHAIN),
|
|
24742
|
+
hyperbridge: await this.hyperbridge()
|
|
24743
|
+
});
|
|
24744
|
+
}
|
|
24745
|
+
};
|
|
24746
|
+
function publicState(record) {
|
|
24747
|
+
const { order: _order, sessionPrivateKey: _key, ...rest } = record;
|
|
24748
|
+
return { ...rest, pending: isBridgeTopUpPending(record.stage) };
|
|
24749
|
+
}
|
|
24750
|
+
var DELIVERY_STATUSES = /* @__PURE__ */ new Set(["SOURCE", "SOURCE_FINALIZED", "DESTINATION"]);
|
|
24751
|
+
function toMs(timestamp) {
|
|
24752
|
+
const value = Number(timestamp ?? 0);
|
|
24753
|
+
return value > 0 && value < 1e12 ? value * 1e3 : value;
|
|
24754
|
+
}
|
|
24755
|
+
function withTimeout(promise, what, ms = NETWORK_TIMEOUT_MS) {
|
|
24756
|
+
let timer;
|
|
24757
|
+
const timeout = new Promise((_, reject) => {
|
|
24758
|
+
timer = setTimeout(() => reject(new Error(`Timed out reading ${what}`)), ms);
|
|
24759
|
+
});
|
|
24760
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
24761
|
+
}
|
|
24762
|
+
|
|
24763
|
+
// src/services/bundler-preflight.ts
|
|
24764
|
+
var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
24765
|
+
async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
|
|
24766
|
+
const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
|
|
24767
|
+
const logger = moduleLogger(configService.loggers, "bundler-preflight");
|
|
24768
|
+
const failures = await Promise.all(
|
|
24769
|
+
chains.map(async ({ chainId, bundlerUrl }) => {
|
|
24770
|
+
if (options.watchOnly?.[chainId] === true) return void 0;
|
|
24771
|
+
if (!bundlerUrl?.trim()) {
|
|
24772
|
+
return `Hyperbridge runs no bundler for ${chainName(chainId)}, so simplex cannot fill there; set it watch-only`;
|
|
24773
|
+
}
|
|
24774
|
+
const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
|
|
24775
|
+
if (!entryPoint) return void 0;
|
|
24776
|
+
return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
|
|
24777
|
+
})
|
|
24778
|
+
);
|
|
24779
|
+
const messages = failures.filter((failure) => failure !== void 0);
|
|
24780
|
+
if (messages.length > 0) throw new Error(messages.join("; "));
|
|
24781
|
+
}
|
|
24782
|
+
function servesEntryPoint(listed, entryPoint) {
|
|
24783
|
+
return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
|
|
24784
|
+
}
|
|
24785
|
+
function describeEntryPoints(listed) {
|
|
24786
|
+
return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
|
|
24787
|
+
}
|
|
24788
|
+
function bundlerHost(url) {
|
|
24789
|
+
try {
|
|
24790
|
+
return new URL(url).host || "an unparseable URL";
|
|
24791
|
+
} catch {
|
|
24792
|
+
return "an unparseable URL";
|
|
24793
|
+
}
|
|
24794
|
+
}
|
|
24795
|
+
function chainName(chainId) {
|
|
24796
|
+
const label = chainByChainId(chainId)?.label;
|
|
24797
|
+
return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
|
|
24798
|
+
}
|
|
24799
|
+
async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
|
|
24800
|
+
const host = bundlerHost(bundlerUrl);
|
|
24801
|
+
const subject = `Bundler ${host} for ${chainName(chainId)}`;
|
|
24802
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
24803
|
+
let listed;
|
|
24804
|
+
try {
|
|
24805
|
+
listed = await supportedEntryPoints(bundlerUrl, signal);
|
|
24806
|
+
} catch (err) {
|
|
24807
|
+
const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
|
|
24808
|
+
logger.warn(
|
|
24809
|
+
{ chain: formatChainKey(chainId), bundler: host },
|
|
24810
|
+
`${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
|
|
24811
|
+
);
|
|
24812
|
+
return void 0;
|
|
24813
|
+
}
|
|
24814
|
+
if (servesEntryPoint(listed, entryPoint)) return void 0;
|
|
24815
|
+
return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
|
|
24816
|
+
}
|
|
24817
|
+
async function supportedEntryPoints(bundlerUrl, signal) {
|
|
24818
|
+
const response = await fetch(bundlerUrl, {
|
|
24819
|
+
method: "POST",
|
|
24820
|
+
headers: { "Content-Type": "application/json" },
|
|
24821
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
|
|
24822
|
+
signal
|
|
24823
|
+
});
|
|
24824
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
24825
|
+
const json = await response.json();
|
|
24826
|
+
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
24827
|
+
const { result } = json;
|
|
24828
|
+
if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
|
|
24829
|
+
throw new Error("the result is not a list of addresses");
|
|
24830
|
+
}
|
|
24831
|
+
return result;
|
|
24832
|
+
}
|
|
24833
|
+
function errorReason(err) {
|
|
24834
|
+
if (!(err instanceof Error)) return String(err);
|
|
24835
|
+
const { cause } = err;
|
|
24836
|
+
return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
|
|
24837
|
+
}
|
|
24838
|
+
|
|
24839
|
+
// src/orderbook/lifecycle.ts
|
|
24840
|
+
var EXPIRY_SWEEP_MS = 3e4;
|
|
24841
|
+
var LimitOrderLifecycle = class {
|
|
24842
|
+
constructor(service, options, loggers = defaultLoggerContext()) {
|
|
24843
|
+
this.service = service;
|
|
24844
|
+
this.options = options;
|
|
24845
|
+
this.timers = [];
|
|
24846
|
+
this.logger = loggers.get("limit-orders");
|
|
24847
|
+
}
|
|
24848
|
+
/**
|
|
23930
24849
|
* Starts the clocks and kicks off the first reconciliation.
|
|
23931
24850
|
*
|
|
23932
24851
|
* That first pass is not awaited. It is a round trip to the orderbook for
|
|
@@ -23997,7 +24916,30 @@ var LimitOrderLifecycle = class {
|
|
|
23997
24916
|
|
|
23998
24917
|
// src/services/BalanceProvider.ts
|
|
23999
24918
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
24000
|
-
import { formatUnits as
|
|
24919
|
+
import { formatUnits as formatUnits6 } from "viem";
|
|
24920
|
+
|
|
24921
|
+
// src/services/native-setup.ts
|
|
24922
|
+
import { concat, formatEther as formatEther4 } from "viem";
|
|
24923
|
+
async function nativeSetupNeed(client, configService, wallet, chain, nativeBalance) {
|
|
24924
|
+
const paymaster = configService.getSimplexPaymasterAddress(chain);
|
|
24925
|
+
if (!paymaster) return null;
|
|
24926
|
+
const gasPrice = await client.getGasPrice();
|
|
24927
|
+
if (nativeBalance >= (DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice) return null;
|
|
24928
|
+
const pending = await resolvePendingPermit2Approval(client, wallet, paymaster, chain, configService);
|
|
24929
|
+
if (!pending || pending.permitCapable) return null;
|
|
24930
|
+
const solverAccount = configService.getSolverAccountContractAddress(chain);
|
|
24931
|
+
const code = await client.getCode({ address: wallet });
|
|
24932
|
+
const delegated = !!solverAccount && code?.toLowerCase() === concat(["0xef0100", solverAccount]).toLowerCase();
|
|
24933
|
+
const cost = (delegated ? APPROVE_TX_GAS : DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice;
|
|
24934
|
+
if (nativeBalance >= cost) return null;
|
|
24935
|
+
const usdc = configService.getUsdcAsset(chain);
|
|
24936
|
+
return {
|
|
24937
|
+
required: Number(formatEther4(cost * 2n)),
|
|
24938
|
+
feeToken: pending.token.toLowerCase() === usdc.toLowerCase() ? "USDC" : "USDT"
|
|
24939
|
+
};
|
|
24940
|
+
}
|
|
24941
|
+
|
|
24942
|
+
// src/services/BalanceProvider.ts
|
|
24001
24943
|
var BalanceProvider = class extends EventEmitter2 {
|
|
24002
24944
|
constructor(options) {
|
|
24003
24945
|
super();
|
|
@@ -24045,8 +24987,14 @@ var BalanceProvider = class extends EventEmitter2 {
|
|
|
24045
24987
|
})
|
|
24046
24988
|
);
|
|
24047
24989
|
}
|
|
24990
|
+
let tokens = {};
|
|
24991
|
+
try {
|
|
24992
|
+
tokens = await this.options.tokens();
|
|
24993
|
+
} catch (err) {
|
|
24994
|
+
this.logger.warn({ err }, "Could not read the orderbook's books; reading USDC and USDT balances only");
|
|
24995
|
+
}
|
|
24048
24996
|
const fxExoticByChain = /* @__PURE__ */ new Map();
|
|
24049
|
-
for (const [chainKey, addrs] of Object.entries(
|
|
24997
|
+
for (const [chainKey, addrs] of Object.entries(tokens)) {
|
|
24050
24998
|
const id = Number.parseInt(chainKey.replace("EVM-", ""), 10);
|
|
24051
24999
|
if (!isNaN(id)) fxExoticByChain.set(id, addrs);
|
|
24052
25000
|
}
|
|
@@ -24090,7 +25038,14 @@ var BalanceProvider = class extends EventEmitter2 {
|
|
|
24090
25038
|
try {
|
|
24091
25039
|
const native = await client.getBalance({ address: fillerAddr });
|
|
24092
25040
|
const symbol = nativeTokenSymbol(chainId);
|
|
24093
|
-
row.native = { symbol, amount: Number.parseFloat(
|
|
25041
|
+
row.native = { symbol, amount: Number.parseFloat(formatUnits6(native, 18)) };
|
|
25042
|
+
const setup = await nativeSetupNeed(client, this.options.configService, fillerAddr, chain, native).catch(
|
|
25043
|
+
(err) => {
|
|
25044
|
+
this.logger.debug({ err, chainId }, "Could not check whether the chain needs native gas for setup");
|
|
25045
|
+
return null;
|
|
25046
|
+
}
|
|
25047
|
+
);
|
|
25048
|
+
if (setup) row.nativeSetup = setup;
|
|
24094
25049
|
} catch (err) {
|
|
24095
25050
|
issues.push({ chainId, source: "native", message: errorMessage(err) });
|
|
24096
25051
|
}
|
|
@@ -24161,7 +25116,7 @@ var BalanceProvider = class extends EventEmitter2 {
|
|
|
24161
25116
|
functionName: "balanceOf",
|
|
24162
25117
|
args: [fillerAddr]
|
|
24163
25118
|
});
|
|
24164
|
-
wallet = Number.parseFloat(
|
|
25119
|
+
wallet = Number.parseFloat(formatUnits6(balance, definition.decimals));
|
|
24165
25120
|
} catch (err) {
|
|
24166
25121
|
issues.push({
|
|
24167
25122
|
chainId,
|
|
@@ -24189,8 +25144,8 @@ var BalanceProvider = class extends EventEmitter2 {
|
|
|
24189
25144
|
available: wallet !== null && vaultDataAvailable ? Math.max(wallet - walletReserve, 0) + vaultAvailable : null,
|
|
24190
25145
|
vaults: positions.map((position) => ({
|
|
24191
25146
|
address: position.vault,
|
|
24192
|
-
position: Number.parseFloat(
|
|
24193
|
-
available: Number.parseFloat(
|
|
25147
|
+
position: Number.parseFloat(formatUnits6(position.positionAssets, position.decimals)),
|
|
25148
|
+
available: Number.parseFloat(formatUnits6(position.availableAssets, position.decimals)),
|
|
24194
25149
|
acceptsDeposits: position.acceptsDeposits
|
|
24195
25150
|
})),
|
|
24196
25151
|
status: complete ? "fresh" : wallet !== null || positions.length > 0 ? "partial" : "unavailable"
|
|
@@ -24233,8 +25188,8 @@ var BalanceProvider = class extends EventEmitter2 {
|
|
|
24233
25188
|
const api = await coprocessor.queryApi();
|
|
24234
25189
|
const account = await api.query.system.account(address);
|
|
24235
25190
|
const decimals = api.registry.chainDecimals[0] ?? 12;
|
|
24236
|
-
const free = Number.parseFloat(
|
|
24237
|
-
const reserved = Number.parseFloat(
|
|
25191
|
+
const free = Number.parseFloat(formatUnits6(BigInt(account.data.free.toString()), decimals));
|
|
25192
|
+
const reserved = Number.parseFloat(formatUnits6(BigInt(account.data.reserved.toString()), decimals));
|
|
24238
25193
|
this.snapshot = { ...this.snapshot, hyperbridge: { address, free, reserved } };
|
|
24239
25194
|
} catch (err) {
|
|
24240
25195
|
this.logger.warn({ err }, "Failed to fetch Hyperbridge balance");
|
|
@@ -24254,7 +25209,7 @@ function isConfiguredAddress(address) {
|
|
|
24254
25209
|
}
|
|
24255
25210
|
function sumFormatted(positions, field) {
|
|
24256
25211
|
return positions.reduce(
|
|
24257
|
-
(total, position) => total + Number.parseFloat(
|
|
25212
|
+
(total, position) => total + Number.parseFloat(formatUnits6(position[field], position.decimals)),
|
|
24258
25213
|
0
|
|
24259
25214
|
);
|
|
24260
25215
|
}
|
|
@@ -24498,7 +25453,7 @@ var DEFAULT_INDEXER_URLS = {
|
|
|
24498
25453
|
mainnet: "https://nexus.indexer.polytope.technology/",
|
|
24499
25454
|
testnet: "https://gargantua.indexer.polytope.technology/"
|
|
24500
25455
|
};
|
|
24501
|
-
var
|
|
25456
|
+
var ORDER_QUERY3 = `
|
|
24502
25457
|
query OrderSummary($commitment: String!) {
|
|
24503
25458
|
iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
|
|
24504
25459
|
nodes {
|
|
@@ -24591,7 +25546,7 @@ async function fetchOrder(fetchImpl, indexerUrl, commitment) {
|
|
|
24591
25546
|
const response = await fetchImpl(indexerUrl, {
|
|
24592
25547
|
method: "POST",
|
|
24593
25548
|
headers: { "content-type": "application/json" },
|
|
24594
|
-
body: JSON.stringify({ query:
|
|
25549
|
+
body: JSON.stringify({ query: ORDER_QUERY3, variables: { commitment } }),
|
|
24595
25550
|
signal: AbortSignal.timeout(15e3)
|
|
24596
25551
|
});
|
|
24597
25552
|
if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
|
|
@@ -24697,12 +25652,8 @@ async function bootFiller(config, options) {
|
|
|
24697
25652
|
}
|
|
24698
25653
|
}
|
|
24699
25654
|
};
|
|
24700
|
-
|
|
24701
|
-
|
|
24702
|
-
logger.warn(
|
|
24703
|
-
{ keys: retired },
|
|
24704
|
-
"These [[pairs]] keys are no longer read; prices come from limit orders, and the filler fills nothing until one is posted"
|
|
24705
|
-
);
|
|
25655
|
+
if ("pairs" in config) {
|
|
25656
|
+
logger.warn("[[pairs]] is no longer read and can be deleted from the config");
|
|
24706
25657
|
}
|
|
24707
25658
|
logger.info("Resolving chain IDs from RPC endpoints...");
|
|
24708
25659
|
const resolvedChains = await resolveChainConfigs(config.chains, {
|
|
@@ -24807,25 +25758,6 @@ async function bootFiller(config, options) {
|
|
|
24807
25758
|
}
|
|
24808
25759
|
logger.info("Initializing trading engine...");
|
|
24809
25760
|
const assetRegistry = new AssetRegistry(configService, config.assets);
|
|
24810
|
-
const configuredChainNames = resolvedChains.map((chain) => `EVM-${chain.chainId}`);
|
|
24811
|
-
if (config.pairs?.length) {
|
|
24812
|
-
assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
|
|
24813
|
-
}
|
|
24814
|
-
const adminStrategies = [];
|
|
24815
|
-
const tradingPairs = (config.pairs ?? []).map((pair) => ({
|
|
24816
|
-
token0: pair.token0,
|
|
24817
|
-
token1: pair.token1
|
|
24818
|
-
}));
|
|
24819
|
-
tradingPairs.forEach((pair, pairIndex) => {
|
|
24820
|
-
adminStrategies.push({
|
|
24821
|
-
index: pairIndex,
|
|
24822
|
-
pairIndex,
|
|
24823
|
-
exotic: `${pair.token0}/${pair.token1}`,
|
|
24824
|
-
token0: pair.token0,
|
|
24825
|
-
token1: pair.token1,
|
|
24826
|
-
sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
|
|
24827
|
-
});
|
|
24828
|
-
});
|
|
24829
25761
|
const confirmationPolicy = assertConfirmationCoverage(
|
|
24830
25762
|
config.confirmationPolicies,
|
|
24831
25763
|
resolvedChains.map((c) => c.chainId)
|
|
@@ -24836,7 +25768,6 @@ async function bootFiller(config, options) {
|
|
|
24836
25768
|
configService,
|
|
24837
25769
|
chainClientManager,
|
|
24838
25770
|
contractService,
|
|
24839
|
-
tradingPairs,
|
|
24840
25771
|
assetRegistry,
|
|
24841
25772
|
{ confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
|
|
24842
25773
|
);
|
|
@@ -24944,8 +25875,8 @@ async function bootFiller(config, options) {
|
|
|
24944
25875
|
if (vaultVenue) {
|
|
24945
25876
|
vaultVenue.onTx = ({ chain, kind, txHash, sponsored, movements }) => {
|
|
24946
25877
|
const rows = movements.length > 0 ? movements.map((move) => {
|
|
24947
|
-
const underlying = { token: move.symbol, amount:
|
|
24948
|
-
const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount:
|
|
25878
|
+
const underlying = { token: move.symbol, amount: formatUnits7(move.amount, move.decimals) };
|
|
25879
|
+
const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: formatUnits7(move.shares, move.shareDecimals) };
|
|
24949
25880
|
const out = kind === "sweep" ? underlying : shares;
|
|
24950
25881
|
const back = kind === "sweep" ? shares : underlying;
|
|
24951
25882
|
return { token: out.token, amount: out.amount, to: move.vault, tokenIn: back.token, amountIn: back.amount };
|
|
@@ -24955,21 +25886,21 @@ async function bootFiller(config, options) {
|
|
|
24955
25886
|
}
|
|
24956
25887
|
};
|
|
24957
25888
|
}
|
|
24958
|
-
const
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
|
|
24962
|
-
|
|
24963
|
-
|
|
24964
|
-
|
|
24965
|
-
if (!list.includes(address)) list.push(address);
|
|
25889
|
+
const bookTokens = async () => {
|
|
25890
|
+
const { books } = await limitOrderService.books();
|
|
25891
|
+
const symbols = new Set(books.flatMap((book) => [normalizeSymbol(book.base), normalizeSymbol(book.quote)]));
|
|
25892
|
+
const tokens = {};
|
|
25893
|
+
for (const chainId of configService.getConfiguredChainIds()) {
|
|
25894
|
+
const chainName2 = `EVM-${chainId}`;
|
|
25895
|
+
tokens[chainName2] = [...symbols].flatMap((symbol) => assetRegistry.getAddress(symbol, chainName2) ?? []);
|
|
24966
25896
|
}
|
|
24967
|
-
|
|
25897
|
+
return tokens;
|
|
25898
|
+
};
|
|
24968
25899
|
const balanceProvider = new BalanceProvider({
|
|
24969
25900
|
chainClientManager,
|
|
24970
25901
|
configService,
|
|
24971
25902
|
fillerAddress: runtimeSigner.address,
|
|
24972
|
-
|
|
25903
|
+
tokens: bookTokens,
|
|
24973
25904
|
vaultBalances: vaultVenue,
|
|
24974
25905
|
hyperbridge: intentFiller.hyperbridgeConnection,
|
|
24975
25906
|
substratePrivateKey: config.simplex.substratePrivateKey
|
|
@@ -25012,7 +25943,6 @@ async function bootFiller(config, options) {
|
|
|
25012
25943
|
logger.info(
|
|
25013
25944
|
{
|
|
25014
25945
|
chains: resolvedChains.map((c) => c.chainId),
|
|
25015
|
-
pairs: (config.pairs ?? []).map((p) => `${p.token0}/${p.token1}`),
|
|
25016
25946
|
maxConcurrentOrders: config.simplex.maxConcurrentOrders ?? DEFAULT_MAX_CONCURRENT_ORDERS,
|
|
25017
25947
|
watchOnlyChains: watchOnlyChains.length > 0 ? watchOnlyChains : void 0
|
|
25018
25948
|
},
|
|
@@ -25030,12 +25960,32 @@ async function bootFiller(config, options) {
|
|
|
25030
25960
|
activity.detach();
|
|
25031
25961
|
if (options.ownsData) await options.data.close?.();
|
|
25032
25962
|
};
|
|
25963
|
+
const tokenSender = new TokenSender(
|
|
25964
|
+
chainClientManager,
|
|
25965
|
+
runtimeSigner.address,
|
|
25966
|
+
() => config.vault?.vaults ?? [],
|
|
25967
|
+
userOpSender,
|
|
25968
|
+
configService
|
|
25969
|
+
);
|
|
25970
|
+
const bridgeTopUp = config.simplex.substratePrivateKey && network === "mainnet" ? new BridgeTopUp({
|
|
25971
|
+
clientManager: chainClientManager,
|
|
25972
|
+
delegation: new DelegationService(chainClientManager, configService, runtimeSigner),
|
|
25973
|
+
configService,
|
|
25974
|
+
tokenSender,
|
|
25975
|
+
state: options.data.state,
|
|
25976
|
+
storagePath: join(options.data.dataDir ?? process.cwd(), ".hyperbridge-cache"),
|
|
25977
|
+
indexerUrl,
|
|
25978
|
+
solver: runtimeSigner.address,
|
|
25979
|
+
substratePublicKey: toHex5((await deriveSubstrateKeyPair(config.simplex.substratePrivateKey)).publicKey),
|
|
25980
|
+
hyperbridge: intentFiller.hyperbridgeConnection,
|
|
25981
|
+
loggers: options.loggers
|
|
25982
|
+
}) : void 0;
|
|
25983
|
+
void bridgeTopUp?.restore();
|
|
25033
25984
|
return {
|
|
25034
25985
|
intentFiller,
|
|
25035
25986
|
limitOrders: limitOrderService,
|
|
25036
25987
|
balanceProvider,
|
|
25037
25988
|
vaultVenue,
|
|
25038
|
-
adminStrategies,
|
|
25039
25989
|
haltControls,
|
|
25040
25990
|
activity,
|
|
25041
25991
|
data: options.data,
|
|
@@ -25047,9 +25997,6 @@ async function bootFiller(config, options) {
|
|
|
25047
25997
|
confirmationPolicy,
|
|
25048
25998
|
loggers: options.loggers,
|
|
25049
25999
|
assetRegistry,
|
|
25050
|
-
engine,
|
|
25051
|
-
tradingPairs,
|
|
25052
|
-
balanceTokens: token1,
|
|
25053
26000
|
rebalancingService,
|
|
25054
26001
|
resolvedChains,
|
|
25055
26002
|
fillerAddress: runtimeSigner.address,
|
|
@@ -25059,13 +26006,8 @@ async function bootFiller(config, options) {
|
|
|
25059
26006
|
config,
|
|
25060
26007
|
configPath: options.configPath,
|
|
25061
26008
|
startedAt: Date.now(),
|
|
25062
|
-
tokenSender
|
|
25063
|
-
|
|
25064
|
-
runtimeSigner.address,
|
|
25065
|
-
() => config.vault?.vaults ?? [],
|
|
25066
|
-
userOpSender,
|
|
25067
|
-
configService
|
|
25068
|
-
),
|
|
26009
|
+
tokenSender,
|
|
26010
|
+
bridgeTopUp,
|
|
25069
26011
|
vaultPreflight: async (vaults) => {
|
|
25070
26012
|
const byChain = {};
|
|
25071
26013
|
for (const row of vaults) {
|
|
@@ -25993,7 +26935,7 @@ var OrderScanner = class _OrderScanner {
|
|
|
25993
26935
|
async addChainNow(chain) {
|
|
25994
26936
|
if (this.closed) throw new Error("This OrderScanner is closed");
|
|
25995
26937
|
const rpcUrls = validateRpcUrls(chain.rpcUrls);
|
|
25996
|
-
const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls
|
|
26938
|
+
const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls }], {
|
|
25997
26939
|
loggers: this.loggers,
|
|
25998
26940
|
tolerateUnreachable: true
|
|
25999
26941
|
}))[0].chainId;
|
|
@@ -26078,7 +27020,7 @@ var OrderScanner = class _OrderScanner {
|
|
|
26078
27020
|
};
|
|
26079
27021
|
|
|
26080
27022
|
// src/data/indexer-inventory.ts
|
|
26081
|
-
import { formatUnits as
|
|
27023
|
+
import { formatUnits as formatUnits8 } from "viem";
|
|
26082
27024
|
var READ_TIMEOUT_MS = 1e4;
|
|
26083
27025
|
var SETTLED_AFTER_MS = 10 * 6e4;
|
|
26084
27026
|
var BATCH = 40;
|
|
@@ -26155,7 +27097,7 @@ ${queries.join("\n")}
|
|
|
26155
27097
|
if (!token) continue;
|
|
26156
27098
|
let amount;
|
|
26157
27099
|
try {
|
|
26158
|
-
amount = Number(
|
|
27100
|
+
amount = Number(formatUnits8(BigInt(row.balance), token.decimals));
|
|
26159
27101
|
} catch {
|
|
26160
27102
|
continue;
|
|
26161
27103
|
}
|
|
@@ -26270,7 +27212,7 @@ var LimitOrderController = class {
|
|
|
26270
27212
|
}));
|
|
26271
27213
|
return this.indexed;
|
|
26272
27214
|
}
|
|
26273
|
-
/** The
|
|
27215
|
+
/** The books the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
26274
27216
|
books() {
|
|
26275
27217
|
return this.service.books();
|
|
26276
27218
|
}
|
|
@@ -26302,85 +27244,6 @@ var LimitOrderController = class {
|
|
|
26302
27244
|
return cancelled;
|
|
26303
27245
|
}
|
|
26304
27246
|
};
|
|
26305
|
-
var PairController = class {
|
|
26306
|
-
constructor(runtime, persist) {
|
|
26307
|
-
this.runtime = runtime;
|
|
26308
|
-
this.persist = persist;
|
|
26309
|
-
}
|
|
26310
|
-
get pairs() {
|
|
26311
|
-
return this.runtime.config.pairs ?? [];
|
|
26312
|
-
}
|
|
26313
|
-
list() {
|
|
26314
|
-
return this.pairs.map((pair, index) => ({
|
|
26315
|
-
index,
|
|
26316
|
-
token0: pair.token0,
|
|
26317
|
-
token1: pair.token1,
|
|
26318
|
-
sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
|
|
26319
|
-
}));
|
|
26320
|
-
}
|
|
26321
|
-
/**
|
|
26322
|
-
* Opens a new market on the running engine.
|
|
26323
|
-
*
|
|
26324
|
-
* Validated against the same rules boot applies, so a pair accepted here is
|
|
26325
|
-
* one a restart would also accept. New `assets` are registered first, because
|
|
26326
|
-
* the pair's symbols have to resolve before the market can be hydrated.
|
|
26327
|
-
*/
|
|
26328
|
-
async add(pair, options) {
|
|
26329
|
-
const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
|
|
26330
|
-
const assets = options?.assets;
|
|
26331
|
-
const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
|
|
26332
|
-
const nextPairs = [...this.pairs, pair];
|
|
26333
|
-
validatePairConfigs(nextPairs, nextAssets);
|
|
26334
|
-
if (assets && Object.keys(assets).length > 0) assetRegistry.addAssets(assets);
|
|
26335
|
-
assertPairSymbolsResolve(
|
|
26336
|
-
[pair],
|
|
26337
|
-
assetRegistry,
|
|
26338
|
-
this.runtime.resolvedChains.map((chain) => formatChainKey(chain.chainId))
|
|
26339
|
-
);
|
|
26340
|
-
const tradingPair = { token0: pair.token0, token1: pair.token1 };
|
|
26341
|
-
engine.addPair(tradingPair);
|
|
26342
|
-
config.pairs = nextPairs;
|
|
26343
|
-
if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
|
|
26344
|
-
if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
|
|
26345
|
-
for (const chain of this.runtime.resolvedChains) {
|
|
26346
|
-
const chainName2 = formatChainKey(chain.chainId);
|
|
26347
|
-
const address = assetRegistry.getAddress(pair.token1, chainName2);
|
|
26348
|
-
if (!address) continue;
|
|
26349
|
-
const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
|
|
26350
|
-
if (!list.includes(address)) list.push(address);
|
|
26351
|
-
}
|
|
26352
|
-
}
|
|
26353
|
-
const pairIndex = nextPairs.length - 1;
|
|
26354
|
-
adminStrategies.push({
|
|
26355
|
-
index: adminStrategies.reduce((max, s) => Math.max(max, s.index), -1) + 1,
|
|
26356
|
-
pairIndex,
|
|
26357
|
-
exotic: `${pair.token0}/${pair.token1}`,
|
|
26358
|
-
token0: pair.token0,
|
|
26359
|
-
token1: pair.token1,
|
|
26360
|
-
sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
|
|
26361
|
-
});
|
|
26362
|
-
await this.persist();
|
|
26363
|
-
return this.list()[pairIndex];
|
|
26364
|
-
}
|
|
26365
|
-
/** Closes a market. Orders already in flight against it still complete. */
|
|
26366
|
-
async remove(index) {
|
|
26367
|
-
const { engine, tradingPairs, adminStrategies, config } = this.runtime;
|
|
26368
|
-
if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
|
|
26369
|
-
engine.removePair(tradingPairs[index]);
|
|
26370
|
-
config.pairs = this.pairs.filter((_, i) => i !== index);
|
|
26371
|
-
const position = adminStrategies.findIndex((s) => s.pairIndex === index);
|
|
26372
|
-
if (position >= 0) adminStrategies.splice(position, 1);
|
|
26373
|
-
for (const strategy of adminStrategies) {
|
|
26374
|
-
if (strategy.pairIndex > index) strategy.pairIndex -= 1;
|
|
26375
|
-
}
|
|
26376
|
-
await this.persist();
|
|
26377
|
-
}
|
|
26378
|
-
livePair(index) {
|
|
26379
|
-
const pair = this.runtime.tradingPairs[index];
|
|
26380
|
-
if (!pair) throw new Error(`Unknown pair ${index}`);
|
|
26381
|
-
return pair;
|
|
26382
|
-
}
|
|
26383
|
-
};
|
|
26384
27247
|
var ChainController = class {
|
|
26385
27248
|
constructor(runtime, persist, scanner, ownsScanner) {
|
|
26386
27249
|
this.runtime = runtime;
|
|
@@ -26425,10 +27288,7 @@ var ChainController = class {
|
|
|
26425
27288
|
async add(chain) {
|
|
26426
27289
|
return this.serialise(async () => {
|
|
26427
27290
|
validateRpcUrls(chain.rpcUrls);
|
|
26428
|
-
|
|
26429
|
-
throw new Error("A bundler URL is required to submit fill UserOperations");
|
|
26430
|
-
}
|
|
26431
|
-
const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }]);
|
|
27291
|
+
const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls }]);
|
|
26432
27292
|
const { chainId } = resolved;
|
|
26433
27293
|
const { configService, config, intentFiller } = this.runtime;
|
|
26434
27294
|
if (configService.getConfiguredChainIds().includes(chainId)) {
|
|
@@ -26457,7 +27317,7 @@ var ChainController = class {
|
|
|
26457
27317
|
if (watchOnly) intentFiller.setWatchOnly(chainId, true);
|
|
26458
27318
|
try {
|
|
26459
27319
|
if (!inScanner) {
|
|
26460
|
-
await this.scanner.addChain({ rpcUrls: chain.rpcUrls,
|
|
27320
|
+
await this.scanner.addChain({ rpcUrls: chain.rpcUrls, chainId });
|
|
26461
27321
|
}
|
|
26462
27322
|
this.runtime.chainClientManager.invalidate(formatChainKey(chainId));
|
|
26463
27323
|
await intentFiller.addChain(configService.getChainConfig(formatChainKey(chainId)));
|
|
@@ -26474,7 +27334,7 @@ var ChainController = class {
|
|
|
26474
27334
|
this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
|
|
26475
27335
|
}
|
|
26476
27336
|
this.runtime.resolvedChains.push(resolved);
|
|
26477
|
-
config.chains = [...config.chains, { rpcUrls: chain.rpcUrls
|
|
27337
|
+
config.chains = [...config.chains, { rpcUrls: chain.rpcUrls }];
|
|
26478
27338
|
if (Object.keys(confirmationPolicies).length > 0) config.confirmationPolicies = confirmationPolicies;
|
|
26479
27339
|
this.syncWatchOnlyToConfig();
|
|
26480
27340
|
await this.persist();
|
|
@@ -26519,9 +27379,7 @@ var ChainController = class {
|
|
|
26519
27379
|
validateRpcUrls(rpcUrls);
|
|
26520
27380
|
const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
|
|
26521
27381
|
if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
|
|
26522
|
-
const [probed] = await resolveChainConfigs([
|
|
26523
|
-
{ rpcUrls, bundlerUrl: this.runtime.resolvedChains[index].bundlerUrl ?? "" }
|
|
26524
|
-
]);
|
|
27382
|
+
const [probed] = await resolveChainConfigs([{ rpcUrls }]);
|
|
26525
27383
|
if (probed.chainId !== chainId) {
|
|
26526
27384
|
throw new Error(`Those endpoints answer for chain ${probed.chainId}, not ${chainId}`);
|
|
26527
27385
|
}
|
|
@@ -26540,20 +27398,6 @@ var ChainController = class {
|
|
|
26540
27398
|
await this.persist();
|
|
26541
27399
|
});
|
|
26542
27400
|
}
|
|
26543
|
-
async setBundlerUrl(chainId, bundlerUrl) {
|
|
26544
|
-
return this.serialise(async () => {
|
|
26545
|
-
if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
|
|
26546
|
-
const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
|
|
26547
|
-
if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
|
|
26548
|
-
await assertBundlersServeEntryPoint([{ chainId, bundlerUrl }], this.runtime.configService, {
|
|
26549
|
-
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26550
|
-
});
|
|
26551
|
-
this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
|
|
26552
|
-
this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
|
|
26553
|
-
this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
|
|
26554
|
-
await this.persist();
|
|
26555
|
-
});
|
|
26556
|
-
}
|
|
26557
27401
|
/** Monitor without filling. Takes effect on the next order. */
|
|
26558
27402
|
async setWatchOnly(chainId, watchOnly) {
|
|
26559
27403
|
return this.serialise(async () => {
|
|
@@ -26705,7 +27549,6 @@ var Simplex = class _Simplex extends EventEmitter4 {
|
|
|
26705
27549
|
this.stopped = false;
|
|
26706
27550
|
this.logger = runtime.loggers.get("simplex");
|
|
26707
27551
|
const persist = () => this.persistConfig();
|
|
26708
|
-
this.pairs = new PairController(runtime, persist);
|
|
26709
27552
|
this.chains = new ChainController(runtime, persist, runtime.orderScanner, ownedScanners.orders !== void 0);
|
|
26710
27553
|
this.vaults = new VaultController(runtime, persist);
|
|
26711
27554
|
this.assets = new AssetController(runtime, persist);
|
|
@@ -26855,8 +27698,7 @@ var Simplex = class _Simplex extends EventEmitter4 {
|
|
|
26855
27698
|
startedAt: this.runtime.startedAt,
|
|
26856
27699
|
uptimeSec: (Date.now() - this.runtime.startedAt) / 1e3,
|
|
26857
27700
|
chains: this.runtime.resolvedChains.map((chain) => chain.chainId),
|
|
26858
|
-
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26859
|
-
pairs: this.runtime.config.pairs?.length ?? 0
|
|
27701
|
+
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26860
27702
|
};
|
|
26861
27703
|
}
|
|
26862
27704
|
/** Order-activity history, newest first; pass `beforeId` to page backwards. */
|
|
@@ -26933,7 +27775,6 @@ export {
|
|
|
26933
27775
|
LoggerContext,
|
|
26934
27776
|
MemoryDataStore,
|
|
26935
27777
|
OrderScanner,
|
|
26936
|
-
PairController,
|
|
26937
27778
|
RebalanceController,
|
|
26938
27779
|
SignerType,
|
|
26939
27780
|
Simplex,
|
|
@@ -26952,14 +27793,12 @@ export {
|
|
|
26952
27793
|
mpcVaultSigner,
|
|
26953
27794
|
normalizeSymbol,
|
|
26954
27795
|
parseChainKey,
|
|
26955
|
-
pickAnchorStable,
|
|
26956
27796
|
privateKeySigner,
|
|
26957
27797
|
registrySymbols,
|
|
26958
27798
|
secretPhraseSigner,
|
|
26959
27799
|
turnkeySigner,
|
|
26960
27800
|
validateAssetDefinitions,
|
|
26961
27801
|
validateConfig,
|
|
26962
|
-
validatePairConfigs,
|
|
26963
27802
|
validateSecretPhrase,
|
|
26964
27803
|
validateSignerConfig,
|
|
26965
27804
|
validateVaultToml,
|