@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.cjs
CHANGED
|
@@ -37,7 +37,6 @@ __export(src_exports, {
|
|
|
37
37
|
LoggerContext: () => LoggerContext,
|
|
38
38
|
MemoryDataStore: () => MemoryDataStore,
|
|
39
39
|
OrderScanner: () => OrderScanner,
|
|
40
|
-
PairController: () => PairController,
|
|
41
40
|
RebalanceController: () => RebalanceController,
|
|
42
41
|
SignerType: () => SignerType,
|
|
43
42
|
Simplex: () => Simplex,
|
|
@@ -56,14 +55,12 @@ __export(src_exports, {
|
|
|
56
55
|
mpcVaultSigner: () => mpcVaultSigner,
|
|
57
56
|
normalizeSymbol: () => normalizeSymbol,
|
|
58
57
|
parseChainKey: () => parseChainKey,
|
|
59
|
-
pickAnchorStable: () => pickAnchorStable,
|
|
60
58
|
privateKeySigner: () => privateKeySigner,
|
|
61
59
|
registrySymbols: () => registrySymbols,
|
|
62
60
|
secretPhraseSigner: () => secretPhraseSigner,
|
|
63
61
|
turnkeySigner: () => turnkeySigner,
|
|
64
62
|
validateAssetDefinitions: () => validateAssetDefinitions,
|
|
65
63
|
validateConfig: () => validateConfig,
|
|
66
|
-
validatePairConfigs: () => validatePairConfigs,
|
|
67
64
|
validateSecretPhrase: () => validateSecretPhrase,
|
|
68
65
|
validateSignerConfig: () => validateSignerConfig,
|
|
69
66
|
validateVaultToml: () => validateVaultToml,
|
|
@@ -75,7 +72,8 @@ module.exports = __toCommonJS(src_exports);
|
|
|
75
72
|
var import_node_events3 = require("node:events");
|
|
76
73
|
|
|
77
74
|
// src/core/boot.ts
|
|
78
|
-
var
|
|
75
|
+
var import_node_path = require("node:path");
|
|
76
|
+
var import_viem34 = require("viem");
|
|
79
77
|
|
|
80
78
|
// src/core/filler.ts
|
|
81
79
|
var import_util_crypto = require("@polkadot/util-crypto");
|
|
@@ -625,6 +623,20 @@ var ConfirmationPolicy = class {
|
|
|
625
623
|
// src/core/filler.ts
|
|
626
624
|
var import_p_queue = __toESM(require("p-queue"), 1);
|
|
627
625
|
|
|
626
|
+
// src/config/bundlers.ts
|
|
627
|
+
var HYPERBRIDGE_BUNDLER_URLS = {
|
|
628
|
+
1: "https://bundler.polytope.technology/ethereum",
|
|
629
|
+
56: "https://bundler.polytope.technology/bsc",
|
|
630
|
+
97: "https://bundler.polytope.technology/bsc-chapel",
|
|
631
|
+
137: "https://bundler.polytope.technology/polygon",
|
|
632
|
+
8453: "https://bundler.polytope.technology/base",
|
|
633
|
+
42161: "https://bundler.polytope.technology/arbitrum",
|
|
634
|
+
80002: "https://bundler.polytope.technology/polygon-amoy"
|
|
635
|
+
};
|
|
636
|
+
function hyperbridgeBundlerUrl(chainId) {
|
|
637
|
+
return HYPERBRIDGE_BUNDLER_URLS[chainId];
|
|
638
|
+
}
|
|
639
|
+
|
|
628
640
|
// src/config/asset-registry.ts
|
|
629
641
|
var import_viem = require("viem");
|
|
630
642
|
var BUILTIN_ASSETS = {
|
|
@@ -755,92 +767,6 @@ var AssetRegistry = class {
|
|
|
755
767
|
}
|
|
756
768
|
};
|
|
757
769
|
|
|
758
|
-
// src/config/pairs.ts
|
|
759
|
-
var RETIRED_PAIR_KEYS = ["bidPriceCurve", "askPriceCurve", "maxOrderSize", "referenceOnly"];
|
|
760
|
-
function retiredPairKeys(pairs) {
|
|
761
|
-
const carried = /* @__PURE__ */ new Set();
|
|
762
|
-
for (const pair of pairs) {
|
|
763
|
-
for (const key of RETIRED_PAIR_KEYS) {
|
|
764
|
-
if (pair[key] !== void 0) carried.add(key);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return RETIRED_PAIR_KEYS.filter((key) => carried.has(key));
|
|
768
|
-
}
|
|
769
|
-
function isKnownSymbol(symbol, userAssets) {
|
|
770
|
-
const normalized = normalizeSymbol(symbol);
|
|
771
|
-
if (isRegistrySymbol(normalized)) return true;
|
|
772
|
-
return Object.keys(userAssets ?? {}).some((key) => normalizeSymbol(key) === normalized);
|
|
773
|
-
}
|
|
774
|
-
function assertPairSymbolsResolve(pairs, registry, chainNames) {
|
|
775
|
-
const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
|
|
776
|
-
for (const pair of pairs) {
|
|
777
|
-
for (const symbol of [pair.token0, pair.token1]) {
|
|
778
|
-
const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
|
|
779
|
-
if (!resolvesSomewhere) {
|
|
780
|
-
throw new Error(
|
|
781
|
-
`pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
for (const chainName2 of chainNames) {
|
|
787
|
-
const addressOwner = /* @__PURE__ */ new Map();
|
|
788
|
-
for (const symbol of pairSymbols) {
|
|
789
|
-
const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
|
|
790
|
-
if (!address) continue;
|
|
791
|
-
const owner = addressOwner.get(address);
|
|
792
|
-
if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
|
|
793
|
-
throw new Error(
|
|
794
|
-
`assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
addressOwner.set(address, symbol);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
function pickAnchorStable(pairs, symbol) {
|
|
802
|
-
const target = normalizeSymbol(symbol);
|
|
803
|
-
for (const stable of USD_STABLE_SYMBOLS) {
|
|
804
|
-
const taken = pairs.some((pair) => {
|
|
805
|
-
const token0 = normalizeSymbol(pair.token0);
|
|
806
|
-
const token1 = normalizeSymbol(pair.token1);
|
|
807
|
-
return token0 === stable && token1 === target || token0 === target && token1 === stable;
|
|
808
|
-
});
|
|
809
|
-
if (!taken) return stable;
|
|
810
|
-
}
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
function validatePairConfigs(pairs, userAssets) {
|
|
814
|
-
if (!Array.isArray(pairs) || pairs.length === 0) {
|
|
815
|
-
throw new Error("pairs: at least one [[pairs]] entry is required");
|
|
816
|
-
}
|
|
817
|
-
const seen = /* @__PURE__ */ new Set();
|
|
818
|
-
for (const pair of pairs) {
|
|
819
|
-
if (!pair.token0 || !pair.token1) {
|
|
820
|
-
throw new Error("pairs: each entry needs 'token0' and 'token1' symbols");
|
|
821
|
-
}
|
|
822
|
-
const token0 = normalizeSymbol(pair.token0);
|
|
823
|
-
const token1 = normalizeSymbol(pair.token1);
|
|
824
|
-
const label = `${token0}/${token1}`;
|
|
825
|
-
if (seen.has(label)) {
|
|
826
|
-
throw new Error(`pairs.${label}: pair is declared twice`);
|
|
827
|
-
}
|
|
828
|
-
if (seen.has(`${token1}/${token0}`)) {
|
|
829
|
-
throw new Error(
|
|
830
|
-
`pairs.${label}: ${token1}/${token0} is already declared \u2014 a market has one orientation, and a limit order on either side trades it`
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
seen.add(label);
|
|
834
|
-
for (const symbol of [token0, token1]) {
|
|
835
|
-
if (!isKnownSymbol(symbol, userAssets)) {
|
|
836
|
-
throw new Error(
|
|
837
|
-
`pairs.${label}: unknown symbol '${symbol}' \u2014 the registry ships ${registrySymbols().join(", ")}; anything else needs an [assets.${symbol}] entry`
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
770
|
// src/config/abis/ERC20.ts
|
|
845
771
|
var ERC20_ABI = [
|
|
846
772
|
{
|
|
@@ -1245,7 +1171,7 @@ async function resolveChainConfigs(chains, options = {}) {
|
|
|
1245
1171
|
"RPC endpoint could not report its chainId at startup; keeping it for the quorum to judge per call"
|
|
1246
1172
|
);
|
|
1247
1173
|
}
|
|
1248
|
-
return { chainId: first.chainId, rpcUrls, bundlerUrl:
|
|
1174
|
+
return { chainId: first.chainId, rpcUrls, bundlerUrl: hyperbridgeBundlerUrl(first.chainId) };
|
|
1249
1175
|
})
|
|
1250
1176
|
);
|
|
1251
1177
|
}
|
|
@@ -1314,12 +1240,6 @@ var FillerConfigService = class {
|
|
|
1314
1240
|
}
|
|
1315
1241
|
this.rpcOverrides.set(chainId, validateRpcUrls(rpcUrls));
|
|
1316
1242
|
}
|
|
1317
|
-
setBundlerUrl(chainId, bundlerUrl) {
|
|
1318
|
-
if (!this.rpcOverrides.has(chainId)) {
|
|
1319
|
-
throw new Error(`Chain ${chainId} is not configured`);
|
|
1320
|
-
}
|
|
1321
|
-
this.bundlerUrls.set(chainId, bundlerUrl);
|
|
1322
|
-
}
|
|
1323
1243
|
/** Replaces the rebalancing config at runtime; trigger checks read it live. */
|
|
1324
1244
|
setRebalancing(rebalancing) {
|
|
1325
1245
|
if (this.fillerConfig) this.fillerConfig.rebalancing = rebalancing;
|
|
@@ -1664,7 +1584,7 @@ function validateOrderbookConfig(orderbook) {
|
|
|
1664
1584
|
function validateConfig(config, cliWatchOnly = false) {
|
|
1665
1585
|
if ("strategies" in config) {
|
|
1666
1586
|
throw new Error(
|
|
1667
|
-
"[[strategies]] was removed \u2014
|
|
1587
|
+
"[[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]"
|
|
1668
1588
|
);
|
|
1669
1589
|
}
|
|
1670
1590
|
const allChainsWatchOnly2 = cliWatchOnly || config.simplex?.watchOnly === true;
|
|
@@ -1681,9 +1601,6 @@ function validateConfig(config, cliWatchOnly = false) {
|
|
|
1681
1601
|
if (!Array.isArray(chain.rpcUrls) || chain.rpcUrls.length === 0 || chain.rpcUrls.some((u) => !u)) {
|
|
1682
1602
|
throw new Error("Each chain configuration must have rpcUrls (a non-empty array of strings)");
|
|
1683
1603
|
}
|
|
1684
|
-
if (!chain.bundlerUrl) {
|
|
1685
|
-
throw new Error("Each chain configuration must have bundlerUrl");
|
|
1686
|
-
}
|
|
1687
1604
|
}
|
|
1688
1605
|
const concurrent = config.simplex.maxConcurrentOrders;
|
|
1689
1606
|
if (concurrent !== void 0) {
|
|
@@ -1732,9 +1649,6 @@ function validateConfig(config, cliWatchOnly = false) {
|
|
|
1732
1649
|
if (config.assets) {
|
|
1733
1650
|
validateAssetDefinitions(config.assets);
|
|
1734
1651
|
}
|
|
1735
|
-
if ((config.pairs?.length ?? 0) > 0) {
|
|
1736
|
-
validatePairConfigs(config.pairs, config.assets);
|
|
1737
|
-
}
|
|
1738
1652
|
if (config.confirmationPolicies) {
|
|
1739
1653
|
void new ConfirmationPolicy(config.confirmationPolicies);
|
|
1740
1654
|
}
|
|
@@ -2545,13 +2459,6 @@ var FREE_RPC_URLS = {
|
|
|
2545
2459
|
"https://arb1.arbitrum.io/rpc"
|
|
2546
2460
|
]
|
|
2547
2461
|
};
|
|
2548
|
-
var HYPERBRIDGE_BUNDLER_URLS = {
|
|
2549
|
-
1: "https://bundler.polytope.technology/ethereum",
|
|
2550
|
-
56: "https://bundler.polytope.technology/bsc",
|
|
2551
|
-
137: "https://bundler.polytope.technology/polygon",
|
|
2552
|
-
8453: "https://bundler.polytope.technology/base",
|
|
2553
|
-
42161: "https://bundler.polytope.technology/arbitrum"
|
|
2554
|
-
};
|
|
2555
2462
|
var CHAIN_NATIVE_SYMBOLS = {
|
|
2556
2463
|
1: "ETH",
|
|
2557
2464
|
56: "BNB",
|
|
@@ -2653,7 +2560,8 @@ var INIT_CHAINS = [
|
|
|
2653
2560
|
label: "Polygon Amoy",
|
|
2654
2561
|
network: "testnet",
|
|
2655
2562
|
alchemySubdomain: "polygon-amoy",
|
|
2656
|
-
explorerUrl: external_links_default.chainExplorers.polygonAmoy
|
|
2563
|
+
explorerUrl: external_links_default.chainExplorers.polygonAmoy,
|
|
2564
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[80002]
|
|
2657
2565
|
},
|
|
2658
2566
|
{
|
|
2659
2567
|
chainId: 97,
|
|
@@ -2661,7 +2569,8 @@ var INIT_CHAINS = [
|
|
|
2661
2569
|
label: "BSC Chapel",
|
|
2662
2570
|
network: "testnet",
|
|
2663
2571
|
alchemySubdomain: "bnb-testnet",
|
|
2664
|
-
explorerUrl: external_links_default.chainExplorers.bnbChapel
|
|
2572
|
+
explorerUrl: external_links_default.chainExplorers.bnbChapel,
|
|
2573
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[97]
|
|
2665
2574
|
}
|
|
2666
2575
|
];
|
|
2667
2576
|
function chainByChainId(chainId) {
|
|
@@ -4458,80 +4367,6 @@ async function patchRuntimeState(store, patch) {
|
|
|
4458
4367
|
return next;
|
|
4459
4368
|
}
|
|
4460
4369
|
|
|
4461
|
-
// src/services/bundler-preflight.ts
|
|
4462
|
-
var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
4463
|
-
async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
|
|
4464
|
-
const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
|
|
4465
|
-
const logger = moduleLogger(configService.loggers, "bundler-preflight");
|
|
4466
|
-
const failures = await Promise.all(
|
|
4467
|
-
chains.map(async ({ chainId, bundlerUrl }) => {
|
|
4468
|
-
if (options.watchOnly?.[chainId] === true) return void 0;
|
|
4469
|
-
if (!bundlerUrl?.trim()) return void 0;
|
|
4470
|
-
const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
|
|
4471
|
-
if (!entryPoint) return void 0;
|
|
4472
|
-
return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
|
|
4473
|
-
})
|
|
4474
|
-
);
|
|
4475
|
-
const messages = failures.filter((failure) => failure !== void 0);
|
|
4476
|
-
if (messages.length > 0) throw new Error(messages.join("; "));
|
|
4477
|
-
}
|
|
4478
|
-
function servesEntryPoint(listed, entryPoint) {
|
|
4479
|
-
return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
|
|
4480
|
-
}
|
|
4481
|
-
function describeEntryPoints(listed) {
|
|
4482
|
-
return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
|
|
4483
|
-
}
|
|
4484
|
-
function bundlerHost(url) {
|
|
4485
|
-
try {
|
|
4486
|
-
return new URL(url).host || "an unparseable URL";
|
|
4487
|
-
} catch {
|
|
4488
|
-
return "an unparseable URL";
|
|
4489
|
-
}
|
|
4490
|
-
}
|
|
4491
|
-
function chainName(chainId) {
|
|
4492
|
-
const label = chainByChainId(chainId)?.label;
|
|
4493
|
-
return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
|
|
4494
|
-
}
|
|
4495
|
-
async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
|
|
4496
|
-
const host = bundlerHost(bundlerUrl);
|
|
4497
|
-
const subject = `Bundler ${host} for ${chainName(chainId)}`;
|
|
4498
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
4499
|
-
let listed;
|
|
4500
|
-
try {
|
|
4501
|
-
listed = await supportedEntryPoints(bundlerUrl, signal);
|
|
4502
|
-
} catch (err) {
|
|
4503
|
-
const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
|
|
4504
|
-
logger.warn(
|
|
4505
|
-
{ chain: formatChainKey(chainId), bundler: host },
|
|
4506
|
-
`${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
|
|
4507
|
-
);
|
|
4508
|
-
return void 0;
|
|
4509
|
-
}
|
|
4510
|
-
if (servesEntryPoint(listed, entryPoint)) return void 0;
|
|
4511
|
-
return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
|
|
4512
|
-
}
|
|
4513
|
-
async function supportedEntryPoints(bundlerUrl, signal) {
|
|
4514
|
-
const response = await fetch(bundlerUrl, {
|
|
4515
|
-
method: "POST",
|
|
4516
|
-
headers: { "Content-Type": "application/json" },
|
|
4517
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
|
|
4518
|
-
signal
|
|
4519
|
-
});
|
|
4520
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
4521
|
-
const json = await response.json();
|
|
4522
|
-
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
4523
|
-
const { result } = json;
|
|
4524
|
-
if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
|
|
4525
|
-
throw new Error("the result is not a list of addresses");
|
|
4526
|
-
}
|
|
4527
|
-
return result;
|
|
4528
|
-
}
|
|
4529
|
-
function errorReason(err) {
|
|
4530
|
-
if (!(err instanceof Error)) return String(err);
|
|
4531
|
-
const { cause } = err;
|
|
4532
|
-
return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
4370
|
// src/services/wallet/mpcvault.ts
|
|
4536
4371
|
var import_viem7 = require("viem");
|
|
4537
4372
|
var grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
@@ -18543,6 +18378,10 @@ var import_viem22 = require("viem");
|
|
|
18543
18378
|
var import_viem18 = require("viem");
|
|
18544
18379
|
var RECOMMENDED_AMOUNT_USD = 5n;
|
|
18545
18380
|
var VERIFICATION_GAS_LIMIT_PERMIT2 = 200000n;
|
|
18381
|
+
var PERMIT2_VERIFICATION_GAS_BY_CHAIN = { 137: 300000n };
|
|
18382
|
+
function permit2VerificationGasLimit(chainId) {
|
|
18383
|
+
return PERMIT2_VERIFICATION_GAS_BY_CHAIN[chainId] ?? VERIFICATION_GAS_LIMIT_PERMIT2;
|
|
18384
|
+
}
|
|
18546
18385
|
var VERIFICATION_GAS_LIMIT_PERMIT = 250000n;
|
|
18547
18386
|
var PERMIT2_DEADLINE_SECONDS = 3600n;
|
|
18548
18387
|
var POST_OP_GAS_LIMIT_SIMPLEX = 40000n;
|
|
@@ -18574,6 +18413,13 @@ var EIP2612_ABI = [
|
|
|
18574
18413
|
outputs: [{ internalType: "string", name: "", type: "string" }],
|
|
18575
18414
|
stateMutability: "view",
|
|
18576
18415
|
type: "function"
|
|
18416
|
+
},
|
|
18417
|
+
{
|
|
18418
|
+
inputs: [],
|
|
18419
|
+
name: "DOMAIN_SEPARATOR",
|
|
18420
|
+
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
|
|
18421
|
+
stateMutability: "view",
|
|
18422
|
+
type: "function"
|
|
18577
18423
|
}
|
|
18578
18424
|
];
|
|
18579
18425
|
|
|
@@ -18635,25 +18481,73 @@ function normalizeSignature65(signature) {
|
|
|
18635
18481
|
}
|
|
18636
18482
|
|
|
18637
18483
|
// src/services/paymaster/permit.ts
|
|
18484
|
+
var DOMAIN_SHAPES = [
|
|
18485
|
+
{
|
|
18486
|
+
fields: [
|
|
18487
|
+
{ name: "name", type: "string" },
|
|
18488
|
+
{ name: "version", type: "string" },
|
|
18489
|
+
{ name: "chainId", type: "uint256" },
|
|
18490
|
+
{ name: "verifyingContract", type: "address" }
|
|
18491
|
+
],
|
|
18492
|
+
domain: (name, version, chainId, verifyingContract) => ({ name, version, chainId, verifyingContract })
|
|
18493
|
+
},
|
|
18494
|
+
// USDT0 on Polygon: the chain id goes in a salt instead of a chainId field.
|
|
18495
|
+
{
|
|
18496
|
+
fields: [
|
|
18497
|
+
{ name: "name", type: "string" },
|
|
18498
|
+
{ name: "version", type: "string" },
|
|
18499
|
+
{ name: "verifyingContract", type: "address" },
|
|
18500
|
+
{ name: "salt", type: "bytes32" }
|
|
18501
|
+
],
|
|
18502
|
+
domain: (name, version, chainId, verifyingContract) => ({
|
|
18503
|
+
name,
|
|
18504
|
+
version,
|
|
18505
|
+
verifyingContract,
|
|
18506
|
+
salt: (0, import_viem21.pad)((0, import_viem21.toHex)(chainId), { size: 32 })
|
|
18507
|
+
})
|
|
18508
|
+
}
|
|
18509
|
+
];
|
|
18510
|
+
var FALLBACK_VERSIONS = ["1", "2"];
|
|
18511
|
+
async function readIfPresent(read) {
|
|
18512
|
+
try {
|
|
18513
|
+
return await read();
|
|
18514
|
+
} catch (error) {
|
|
18515
|
+
const isRevert = error instanceof import_viem21.BaseError && error.walk((e) => e instanceof import_viem21.ContractFunctionRevertedError || e instanceof import_viem21.ContractFunctionZeroDataError);
|
|
18516
|
+
if (isRevert) return void 0;
|
|
18517
|
+
throw error;
|
|
18518
|
+
}
|
|
18519
|
+
}
|
|
18520
|
+
async function resolvePermitDomain(client, tokenAddress, chainId) {
|
|
18521
|
+
const read = (functionName) => readIfPresent(() => client.readContract({ address: tokenAddress, abi: EIP2612_ABI, functionName }));
|
|
18522
|
+
const separator = await read("DOMAIN_SEPARATOR");
|
|
18523
|
+
if (!separator) return null;
|
|
18524
|
+
const name = await read("name");
|
|
18525
|
+
if (name === void 0) return null;
|
|
18526
|
+
const version = await read("version");
|
|
18527
|
+
for (const candidate of version === void 0 ? FALLBACK_VERSIONS : [version]) {
|
|
18528
|
+
for (const shape of DOMAIN_SHAPES) {
|
|
18529
|
+
const domain = shape.domain(name, candidate, chainId, tokenAddress);
|
|
18530
|
+
const hash = (0, import_viem21.hashDomain)({ domain, types: { EIP712Domain: shape.fields } });
|
|
18531
|
+
if (hash.toLowerCase() === separator.toLowerCase()) return { domain, fields: shape.fields };
|
|
18532
|
+
}
|
|
18533
|
+
}
|
|
18534
|
+
return null;
|
|
18535
|
+
}
|
|
18638
18536
|
async function signEip2612Permit(client, signer, owner, spender, tokenAddress, value, chainId) {
|
|
18639
|
-
const
|
|
18640
|
-
|
|
18537
|
+
const permitDomain = await resolvePermitDomain(client, tokenAddress, chainId);
|
|
18538
|
+
if (!permitDomain) {
|
|
18539
|
+
throw new Error(`${tokenAddress} on chain ${chainId} has no EIP-2612 permit domain Simplex can sign for`);
|
|
18540
|
+
}
|
|
18541
|
+
const nonce = await client.readContract({
|
|
18641
18542
|
address: tokenAddress,
|
|
18642
|
-
abi: EIP2612_ABI
|
|
18543
|
+
abi: EIP2612_ABI,
|
|
18544
|
+
functionName: "nonces",
|
|
18545
|
+
args: [owner]
|
|
18643
18546
|
});
|
|
18644
|
-
const [name, version, nonce] = await Promise.all([
|
|
18645
|
-
token.read.name(),
|
|
18646
|
-
token.read.version(),
|
|
18647
|
-
token.read.nonces([owner])
|
|
18648
|
-
]);
|
|
18649
18547
|
const typedData = {
|
|
18650
18548
|
types: {
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
{ name: "version", type: "string" },
|
|
18654
|
-
{ name: "chainId", type: "uint256" },
|
|
18655
|
-
{ name: "verifyingContract", type: "address" }
|
|
18656
|
-
],
|
|
18549
|
+
// Listed explicitly: custody backends hash the domain from this entry, not from `domain`.
|
|
18550
|
+
EIP712Domain: permitDomain.fields,
|
|
18657
18551
|
Permit: [
|
|
18658
18552
|
{ name: "owner", type: "address" },
|
|
18659
18553
|
{ name: "spender", type: "address" },
|
|
@@ -18663,12 +18557,7 @@ async function signEip2612Permit(client, signer, owner, spender, tokenAddress, v
|
|
|
18663
18557
|
]
|
|
18664
18558
|
},
|
|
18665
18559
|
primaryType: "Permit",
|
|
18666
|
-
domain:
|
|
18667
|
-
name,
|
|
18668
|
-
version,
|
|
18669
|
-
chainId,
|
|
18670
|
-
verifyingContract: tokenAddress
|
|
18671
|
-
},
|
|
18560
|
+
domain: permitDomain.domain,
|
|
18672
18561
|
message: {
|
|
18673
18562
|
owner,
|
|
18674
18563
|
spender,
|
|
@@ -18742,7 +18631,7 @@ async function buildSimplexPaymasterData(client, walletClient, signer, solverAcc
|
|
|
18742
18631
|
);
|
|
18743
18632
|
}
|
|
18744
18633
|
if (permit2Allowance < recommended) {
|
|
18745
|
-
if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress)) {
|
|
18634
|
+
if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress, chainId)) {
|
|
18746
18635
|
const pm = await buildPermitMode(
|
|
18747
18636
|
client,
|
|
18748
18637
|
signer,
|
|
@@ -18778,7 +18667,7 @@ async function resolvePendingPermit2Approval(client, solverAccount, paymasterAdd
|
|
|
18778
18667
|
return {
|
|
18779
18668
|
token: selected.address,
|
|
18780
18669
|
spender: permit2,
|
|
18781
|
-
permitCapable: await tokenSupportsPermit(client, selected.address)
|
|
18670
|
+
permitCapable: await tokenSupportsPermit(client, selected.address, configService.getChainId(chain))
|
|
18782
18671
|
};
|
|
18783
18672
|
}
|
|
18784
18673
|
function isConfigured(address) {
|
|
@@ -18798,7 +18687,7 @@ function configuredFeeTokens(chain, configService) {
|
|
|
18798
18687
|
}
|
|
18799
18688
|
async function selectToken(client, solverAccount, tokens, permit2) {
|
|
18800
18689
|
const balances = [];
|
|
18801
|
-
let
|
|
18690
|
+
let fallback3 = null;
|
|
18802
18691
|
for (const token of tokens) {
|
|
18803
18692
|
const balance = await client.readContract({
|
|
18804
18693
|
address: token.address,
|
|
@@ -18812,12 +18701,12 @@ async function selectToken(client, solverAccount, tokens, permit2) {
|
|
|
18812
18701
|
const permit2Allowance = await readAllowance(client, token.address, solverAccount, permit2);
|
|
18813
18702
|
const candidate = { ...token, permit2Allowance };
|
|
18814
18703
|
if (permit2Allowance >= RECOMMENDED_AMOUNT_USD * required) return { selected: candidate, balances };
|
|
18815
|
-
|
|
18704
|
+
fallback3 ?? (fallback3 = candidate);
|
|
18816
18705
|
} else {
|
|
18817
18706
|
balances.push({ symbol: token.symbol, balance, required });
|
|
18818
18707
|
}
|
|
18819
18708
|
}
|
|
18820
|
-
return { selected:
|
|
18709
|
+
return { selected: fallback3, balances };
|
|
18821
18710
|
}
|
|
18822
18711
|
var PERMIT2_SUPPORT_NEGATIVE_TTL_MS = 5 * 6e4;
|
|
18823
18712
|
var permit2Support = /* @__PURE__ */ new Map();
|
|
@@ -18870,7 +18759,7 @@ async function buildPermit2Mode(signer, p) {
|
|
|
18870
18759
|
return {
|
|
18871
18760
|
paymaster: p.spender,
|
|
18872
18761
|
paymasterData,
|
|
18873
|
-
paymasterVerificationGasLimit:
|
|
18762
|
+
paymasterVerificationGasLimit: permit2VerificationGasLimit(p.chainId),
|
|
18874
18763
|
paymasterPostOpGasLimit: POST_OP_GAS_LIMIT_SIMPLEX
|
|
18875
18764
|
};
|
|
18876
18765
|
}
|
|
@@ -18940,27 +18829,8 @@ async function sendApproveTx(client, walletClient, tokenAddress, spender, amount
|
|
|
18940
18829
|
});
|
|
18941
18830
|
await client.waitForTransactionReceipt({ hash, confirmations: 2 });
|
|
18942
18831
|
}
|
|
18943
|
-
async function tokenSupportsPermit(client, tokenAddress) {
|
|
18944
|
-
|
|
18945
|
-
await client.readContract({
|
|
18946
|
-
address: tokenAddress,
|
|
18947
|
-
abi: [
|
|
18948
|
-
{
|
|
18949
|
-
inputs: [],
|
|
18950
|
-
name: "version",
|
|
18951
|
-
outputs: [{ type: "string" }],
|
|
18952
|
-
stateMutability: "view",
|
|
18953
|
-
type: "function"
|
|
18954
|
-
}
|
|
18955
|
-
],
|
|
18956
|
-
functionName: "version"
|
|
18957
|
-
});
|
|
18958
|
-
return true;
|
|
18959
|
-
} catch (error) {
|
|
18960
|
-
const isRevert = error instanceof import_viem22.BaseError && error.walk((e) => e instanceof import_viem22.ContractFunctionRevertedError || e instanceof import_viem22.ContractFunctionZeroDataError);
|
|
18961
|
-
if (isRevert) return false;
|
|
18962
|
-
throw error;
|
|
18963
|
-
}
|
|
18832
|
+
async function tokenSupportsPermit(client, tokenAddress, chainId) {
|
|
18833
|
+
return await resolvePermitDomain(client, tokenAddress, chainId) !== null;
|
|
18964
18834
|
}
|
|
18965
18835
|
|
|
18966
18836
|
// src/services/paymaster/index.ts
|
|
@@ -18976,7 +18846,7 @@ async function buildPaymasterAndData(options) {
|
|
|
18976
18846
|
const shortfall = await depositShortfall(
|
|
18977
18847
|
options,
|
|
18978
18848
|
simplexAddr,
|
|
18979
|
-
(permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT :
|
|
18849
|
+
(permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT : permit2VerificationGasLimit(configService.getChainId(chain))) + POST_OP_GAS_LIMIT_SIMPLEX
|
|
18980
18850
|
);
|
|
18981
18851
|
if (shortfall) {
|
|
18982
18852
|
return { paymasterAndData: "0x", type: "none", reason: shortfall };
|
|
@@ -19768,12 +19638,12 @@ var ContractInteractionService = class {
|
|
|
19768
19638
|
const declared = declaredMs ? declaredMs / 1e3 : void 0;
|
|
19769
19639
|
const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
|
|
19770
19640
|
this.blockTimes.delete(chain);
|
|
19771
|
-
const
|
|
19641
|
+
const fallback3 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
|
|
19772
19642
|
this.logger.warn(
|
|
19773
|
-
{ err, chain, seconds:
|
|
19643
|
+
{ err, chain, seconds: fallback3 },
|
|
19774
19644
|
"Could not measure the block time; using the declared one"
|
|
19775
19645
|
);
|
|
19776
|
-
return
|
|
19646
|
+
return fallback3;
|
|
19777
19647
|
});
|
|
19778
19648
|
this.blockTimes.set(chain, seconds);
|
|
19779
19649
|
return seconds;
|
|
@@ -19996,7 +19866,7 @@ var UserOpSender = class {
|
|
|
19996
19866
|
let auth;
|
|
19997
19867
|
let fees;
|
|
19998
19868
|
try {
|
|
19999
|
-
fees = await this.getGasPrice(bundlerUrl, publicClient
|
|
19869
|
+
fees = await this.getGasPrice(bundlerUrl, publicClient);
|
|
20000
19870
|
const gasForPrefund = gas ?? {
|
|
20001
19871
|
verificationGasLimit: FALLBACK_VERIFICATION_GAS_LIMIT,
|
|
20002
19872
|
callGasLimit: FALLBACK_CALL_GAS_LIMIT,
|
|
@@ -20151,48 +20021,26 @@ var UserOpSender = class {
|
|
|
20151
20021
|
return userOp;
|
|
20152
20022
|
}
|
|
20153
20023
|
/**
|
|
20154
|
-
*
|
|
20155
|
-
*
|
|
20156
|
-
* rundler bundler requires, when the bundler gives one.
|
|
20024
|
+
* Prices the op for the rundler bundler, as the fill path does: its suggested fees, or the
|
|
20025
|
+
* chain's gas price raised to the priority fee it requires where it suggests none.
|
|
20157
20026
|
*/
|
|
20158
|
-
async getGasPrice(bundlerUrl, publicClient
|
|
20159
|
-
const
|
|
20160
|
-
if (lower.includes("pimlico.io")) {
|
|
20161
|
-
const res = await this.sendBundlerRpc(
|
|
20162
|
-
bundlerUrl,
|
|
20163
|
-
import_sdk8.BundlerMethod.PIMLICO_GET_USER_OPERATION_GAS_PRICE,
|
|
20164
|
-
[]
|
|
20165
|
-
);
|
|
20166
|
-
return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
|
|
20167
|
-
}
|
|
20168
|
-
if (lower.includes("alchemy.com")) {
|
|
20169
|
-
const [rundlerPriorityFee2, latestBlock2] = await Promise.all([
|
|
20170
|
-
this.sendBundlerRpc(bundlerUrl, import_sdk8.BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
|
|
20171
|
-
publicClient.getBlock({ blockTag: "latest" })
|
|
20172
|
-
]);
|
|
20173
|
-
const baseFeePerGas = latestBlock2.baseFeePerGas ?? await publicClient.getGasPrice();
|
|
20174
|
-
const isArbitrum = BigInt(chainId) === 42161n;
|
|
20175
|
-
const prioBump = isArbitrum ? 0n : 25n;
|
|
20176
|
-
const maxPriorityFeePerGas = BigInt(rundlerPriorityFee2) + BigInt(rundlerPriorityFee2) * prioBump / 100n;
|
|
20177
|
-
const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
|
|
20178
|
-
return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
|
|
20179
|
-
}
|
|
20180
|
-
const [gasPrice, rundlerPriorityFee] = await Promise.all([
|
|
20027
|
+
async getGasPrice(bundlerUrl, publicClient) {
|
|
20028
|
+
const [gasPrice, latestBlock] = await Promise.all([
|
|
20181
20029
|
publicClient.getGasPrice(),
|
|
20182
|
-
(
|
|
20030
|
+
publicClient.getBlock({ blockTag: "latest" })
|
|
20183
20031
|
]);
|
|
20184
|
-
|
|
20185
|
-
|
|
20186
|
-
|
|
20187
|
-
|
|
20188
|
-
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20195
|
-
|
|
20032
|
+
return (0, import_sdk8.rundlerUserOperationFees)(
|
|
20033
|
+
bundlerUrl,
|
|
20034
|
+
{
|
|
20035
|
+
maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
|
|
20036
|
+
maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
|
|
20037
|
+
},
|
|
20038
|
+
{
|
|
20039
|
+
baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
|
|
20040
|
+
priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
|
|
20041
|
+
maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
|
|
20042
|
+
}
|
|
20043
|
+
);
|
|
20196
20044
|
}
|
|
20197
20045
|
async sendBundlerRpc(bundlerUrl, method, params) {
|
|
20198
20046
|
const response = await fetch(bundlerUrl, {
|
|
@@ -21004,7 +20852,7 @@ var BinanceRebalancer = class {
|
|
|
21004
20852
|
// src/services/rebalancers/cctp.ts
|
|
21005
20853
|
var import_bridge_kit = require("@circle-fin/bridge-kit");
|
|
21006
20854
|
var import_adapter_viem_v2 = require("@circle-fin/adapter-viem-v2");
|
|
21007
|
-
var
|
|
20855
|
+
var import_chains = require("@circle-fin/bridge-kit/chains");
|
|
21008
20856
|
var import_sdk11 = require("@hyperbridge/sdk");
|
|
21009
20857
|
var CHAIN_ID_TO_CCTP = {
|
|
21010
20858
|
1: "Ethereum",
|
|
@@ -21057,18 +20905,18 @@ var CctpRebalancer = class {
|
|
|
21057
20905
|
{
|
|
21058
20906
|
addressContext: "user-controlled",
|
|
21059
20907
|
supportedChains: [
|
|
21060
|
-
|
|
21061
|
-
|
|
21062
|
-
|
|
21063
|
-
|
|
21064
|
-
|
|
21065
|
-
|
|
21066
|
-
|
|
21067
|
-
|
|
21068
|
-
|
|
21069
|
-
|
|
21070
|
-
|
|
21071
|
-
|
|
20908
|
+
import_chains.Ethereum,
|
|
20909
|
+
import_chains.EthereumSepolia,
|
|
20910
|
+
import_chains.Arbitrum,
|
|
20911
|
+
import_chains.ArbitrumSepolia,
|
|
20912
|
+
import_chains.Base,
|
|
20913
|
+
import_chains.BaseSepolia,
|
|
20914
|
+
import_chains.Optimism,
|
|
20915
|
+
import_chains.OptimismSepolia,
|
|
20916
|
+
import_chains.Polygon,
|
|
20917
|
+
import_chains.PolygonAmoy,
|
|
20918
|
+
import_chains.Unichain,
|
|
20919
|
+
import_chains.UnichainSepolia
|
|
21072
20920
|
]
|
|
21073
20921
|
}
|
|
21074
20922
|
);
|
|
@@ -22979,18 +22827,17 @@ function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
|
|
|
22979
22827
|
if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
|
|
22980
22828
|
return amount / BigInt(10 ** (fromDecimals - toDecimals));
|
|
22981
22829
|
}
|
|
22982
|
-
var FXFiller = class
|
|
22830
|
+
var FXFiller = class {
|
|
22983
22831
|
/**
|
|
22984
22832
|
* @param signer Filler's signing account for UserOp signatures.
|
|
22985
22833
|
* @param configService Network/config provider for addresses and decimals.
|
|
22986
22834
|
* @param clientManager Used to get viem PublicClients for chains.
|
|
22987
22835
|
* @param contractService Shared contract interaction service.
|
|
22988
|
-
* @param
|
|
22989
|
-
* @param registry Asset symbol registry resolving pair symbols per chain.
|
|
22836
|
+
* @param registry Asset symbol registry resolving symbols per chain.
|
|
22990
22837
|
* @param options.confirmationPolicy Optional per-chain confirmation policy for cross-chain orders.
|
|
22991
22838
|
* @param options.fundingVenues Optional funding venues for on-chain liquidity sourcing.
|
|
22992
22839
|
*/
|
|
22993
|
-
constructor(signer, configService, clientManager, contractService,
|
|
22840
|
+
constructor(signer, configService, clientManager, contractService, registry, options) {
|
|
22994
22841
|
this.name = "FXFiller";
|
|
22995
22842
|
/**
|
|
22996
22843
|
* Consecutive orders where the overfill clamp activated. Dormant: the clamp
|
|
@@ -23003,17 +22850,9 @@ var FXFiller = class _FXFiller {
|
|
|
23003
22850
|
this.logger = moduleLogger(configService.loggers, "fx-simplex");
|
|
23004
22851
|
const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
|
|
23005
22852
|
this.limitOrders = limitOrders;
|
|
23006
|
-
if (pairs.length === 0) {
|
|
23007
|
-
throw new Error("FXFiller requires at least one trading pair");
|
|
23008
|
-
}
|
|
23009
|
-
const seenPairs = /* @__PURE__ */ new Set();
|
|
23010
|
-
for (const pair of pairs) {
|
|
23011
|
-
_FXFiller.assertPairValid(pair, seenPairs);
|
|
23012
|
-
}
|
|
23013
22853
|
this.configService = configService;
|
|
23014
22854
|
this.clientManager = clientManager;
|
|
23015
22855
|
this.contractService = contractService;
|
|
23016
|
-
this.pairs = pairs;
|
|
23017
22856
|
this.registry = registry;
|
|
23018
22857
|
this.fundingVenues = fundingVenues;
|
|
23019
22858
|
this.signer = signer;
|
|
@@ -23025,43 +22864,6 @@ var FXFiller = class _FXFiller {
|
|
|
23025
22864
|
};
|
|
23026
22865
|
}
|
|
23027
22866
|
}
|
|
23028
|
-
/** Per-pair invariants, shared by the constructor and addPair. Adds the accepted pair's label to `seenPairs`. */
|
|
23029
|
-
static assertPairValid(pair, seenPairs) {
|
|
23030
|
-
const label = `${normalizeSymbol(pair.token0)}/${normalizeSymbol(pair.token1)}`;
|
|
23031
|
-
const reversed = `${normalizeSymbol(pair.token1)}/${normalizeSymbol(pair.token0)}`;
|
|
23032
|
-
if (seenPairs.has(label) || seenPairs.has(reversed)) {
|
|
23033
|
-
throw new Error(
|
|
23034
|
-
`FXFiller pair ${pair.token0}/${pair.token1}: duplicate market (a pair and its reverse are the same market)`
|
|
23035
|
-
);
|
|
23036
|
-
}
|
|
23037
|
-
seenPairs.add(label);
|
|
23038
|
-
}
|
|
23039
|
-
/**
|
|
23040
|
-
* Adds a market to the running engine. All-or-nothing: the pair passes the
|
|
23041
|
-
* same duplicate and reverse-orientation checks the constructor enforces
|
|
23042
|
-
* before it is pushed.
|
|
23043
|
-
*/
|
|
23044
|
-
addPair(pair) {
|
|
23045
|
-
const seenPairs = new Set(this.pairs.map((p) => `${normalizeSymbol(p.token0)}/${normalizeSymbol(p.token1)}`));
|
|
23046
|
-
_FXFiller.assertPairValid(pair, seenPairs);
|
|
23047
|
-
this.pairs.push(pair);
|
|
23048
|
-
}
|
|
23049
|
-
/**
|
|
23050
|
-
* Removes a market from the running engine. At least one market must remain.
|
|
23051
|
-
* Matching reads the limit orders rather than the pairs, so removing a market
|
|
23052
|
-
* changes what the dashboard lists, not what the filler fills.
|
|
23053
|
-
*/
|
|
23054
|
-
removePair(pair) {
|
|
23055
|
-
const index = this.pairs.indexOf(pair);
|
|
23056
|
-
if (index < 0) {
|
|
23057
|
-
throw new Error(`FXFiller pair ${pair.token0}/${pair.token1}: not a live market`);
|
|
23058
|
-
}
|
|
23059
|
-
const remaining = this.pairs.filter((_, i) => i !== index);
|
|
23060
|
-
if (remaining.length === 0) {
|
|
23061
|
-
throw new Error("FXFiller requires at least one trading pair \u2014 the last market cannot be removed live");
|
|
23062
|
-
}
|
|
23063
|
-
this.pairs.splice(index, 1);
|
|
23064
|
-
}
|
|
23065
22867
|
// =========================================================================
|
|
23066
22868
|
// Lifecycle
|
|
23067
22869
|
// =========================================================================
|
|
@@ -23824,7 +23626,7 @@ var TokenSender = class {
|
|
|
23824
23626
|
});
|
|
23825
23627
|
}
|
|
23826
23628
|
}
|
|
23827
|
-
const result = await this.
|
|
23629
|
+
const result = await this.submitCalls(chain, calls);
|
|
23828
23630
|
this.logger.warn({ chain, token, amount, to, redeemed, ...result }, "Operator send executed");
|
|
23829
23631
|
return { ...result, redeemed };
|
|
23830
23632
|
}
|
|
@@ -23880,16 +23682,23 @@ var TokenSender = class {
|
|
|
23880
23682
|
}
|
|
23881
23683
|
return null;
|
|
23882
23684
|
}
|
|
23883
|
-
|
|
23685
|
+
/**
|
|
23686
|
+
* Executes a batch from the filler wallet: sponsored UserOp first, native tx
|
|
23687
|
+
* when no sponsor is available. Shared by every operator-initiated outbound flow.
|
|
23688
|
+
*/
|
|
23689
|
+
async submitCalls(chain, calls) {
|
|
23884
23690
|
if (this.userOpSender?.canSponsor(chain)) {
|
|
23691
|
+
const bootstrap = await this.permit2Bootstrap(chain);
|
|
23692
|
+
const batch = bootstrap ? [bootstrap, ...calls] : calls;
|
|
23885
23693
|
const result = await this.userOpSender.trySendSponsored({
|
|
23886
23694
|
chain,
|
|
23887
|
-
callData: (0, import_sdk16.encodeERC7821ExecuteBatch)(
|
|
23695
|
+
callData: (0, import_sdk16.encodeERC7821ExecuteBatch)(batch),
|
|
23888
23696
|
gas: {
|
|
23889
23697
|
verificationGasLimit: 60000n,
|
|
23890
|
-
callGasLimit: 450000n * BigInt(
|
|
23698
|
+
callGasLimit: 450000n * BigInt(batch.length),
|
|
23891
23699
|
preVerificationGas: 150000n
|
|
23892
|
-
}
|
|
23700
|
+
},
|
|
23701
|
+
permitBootstrap: !!bootstrap
|
|
23893
23702
|
});
|
|
23894
23703
|
if (result) return { txHash: result.txHash, sponsored: true };
|
|
23895
23704
|
this.logger.warn({ chain }, "Sponsored send unavailable, sending native tx");
|
|
@@ -23911,43 +23720,1121 @@ var TokenSender = class {
|
|
|
23911
23720
|
if (receipt.status !== "success") throw new Error(`Send tx reverted: ${tx}`);
|
|
23912
23721
|
return { txHash: tx, sponsored: false };
|
|
23913
23722
|
}
|
|
23914
|
-
};
|
|
23915
|
-
|
|
23916
|
-
// src/orderbook/lifecycle.ts
|
|
23917
|
-
var EXPIRY_SWEEP_MS = 3e4;
|
|
23918
|
-
var LimitOrderLifecycle = class {
|
|
23919
|
-
constructor(service, options, loggers = defaultLoggerContext()) {
|
|
23920
|
-
this.service = service;
|
|
23921
|
-
this.options = options;
|
|
23922
|
-
this.timers = [];
|
|
23923
|
-
this.logger = loggers.get("limit-orders");
|
|
23924
|
-
}
|
|
23925
23723
|
/**
|
|
23926
|
-
*
|
|
23927
|
-
*
|
|
23928
|
-
*
|
|
23929
|
-
*
|
|
23724
|
+
* The `approve(Permit2, max)` to run first when the wallet has never approved Permit2 for its fee
|
|
23725
|
+
* token on this chain, and that token has an EIP-2612 permit. The op then pays its gas with a permit
|
|
23726
|
+
* and installs the approval, so a wallet with no native can still send; later ops use Permit2.
|
|
23727
|
+
* Undefined otherwise, or when the check fails, and the send goes ahead as before.
|
|
23930
23728
|
*/
|
|
23931
|
-
async
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
|
|
23946
|
-
|
|
23729
|
+
async permit2Bootstrap(chain) {
|
|
23730
|
+
const paymaster = this.configService?.getSimplexPaymasterAddress(chain);
|
|
23731
|
+
if (!this.configService || !paymaster) return void 0;
|
|
23732
|
+
try {
|
|
23733
|
+
const pending = await resolvePendingPermit2Approval(
|
|
23734
|
+
this.clientManager.getPublicClient(chain),
|
|
23735
|
+
this.solver,
|
|
23736
|
+
paymaster,
|
|
23737
|
+
chain,
|
|
23738
|
+
this.configService
|
|
23739
|
+
);
|
|
23740
|
+
if (!pending?.permitCapable) return void 0;
|
|
23741
|
+
return {
|
|
23742
|
+
target: pending.token,
|
|
23743
|
+
value: 0n,
|
|
23744
|
+
data: (0, import_viem30.encodeFunctionData)({
|
|
23745
|
+
abi: ERC20_ABI,
|
|
23746
|
+
functionName: "approve",
|
|
23747
|
+
args: [pending.spender, import_viem30.maxUint256]
|
|
23748
|
+
})
|
|
23749
|
+
};
|
|
23750
|
+
} catch (error) {
|
|
23751
|
+
this.logger.warn({ chain, error }, "Could not check whether this send must install the Permit2 approval");
|
|
23752
|
+
return void 0;
|
|
23753
|
+
}
|
|
23947
23754
|
}
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
|
|
23755
|
+
};
|
|
23756
|
+
|
|
23757
|
+
// src/services/BridgeTopUp.ts
|
|
23758
|
+
var import_sdk17 = require("@hyperbridge/sdk");
|
|
23759
|
+
var import_node_crypto2 = require("node:crypto");
|
|
23760
|
+
var import_promises = require("node:timers/promises");
|
|
23761
|
+
var import_viem31 = require("viem");
|
|
23762
|
+
var import_chains2 = require("viem/chains");
|
|
23763
|
+
var SIMPLEX_REFERRER = (0, import_viem31.stringToHex)("Simplex", { size: 32 });
|
|
23764
|
+
var BRIDGE_TOP_UP_CHAIN = "EVM-56";
|
|
23765
|
+
var BRIDGE_TOKEN_BSC = "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB";
|
|
23766
|
+
var BRIDGE_DECIMALS = 18;
|
|
23767
|
+
var NEXUS_STATE_MACHINE = "POLKADOT-3367";
|
|
23768
|
+
var NEXUS_CONSENSUS_STATE = "DOT0";
|
|
23769
|
+
var NEXUS_WS_URL = "wss://nexus.rpc.polytope.technology";
|
|
23770
|
+
var NEXUS_UNIT = 10n ** 6n;
|
|
23771
|
+
var QUOTE_SLIPPAGE_BPS = 100n;
|
|
23772
|
+
var ORDER_WINDOW_SECONDS = 30 * 60;
|
|
23773
|
+
var AUCTION_TIME_MS = 15e3;
|
|
23774
|
+
var REFUND_POLL_MS = 1e4;
|
|
23775
|
+
var RECEIPT_TIMEOUT_MS = 12e4;
|
|
23776
|
+
var MAX_EVENTS = 50;
|
|
23777
|
+
var DELIVERY_POLL_MS = 15e3;
|
|
23778
|
+
var DELIVERY_RETRY_MS = 6e4;
|
|
23779
|
+
var SELF_RELAY_GRACE_MS = 9e4;
|
|
23780
|
+
var RELAYER_DELIVERY_WAIT_MS = 3 * 6e4;
|
|
23781
|
+
var STATUS_BACKFILL_MS = 30 * 6e4;
|
|
23782
|
+
var NETWORK_TIMEOUT_MS = 3e4;
|
|
23783
|
+
var LOG_CHUNK_BLOCKS = 300n;
|
|
23784
|
+
var FILL_SEARCH_BEFORE_MS = 3 * 6e4;
|
|
23785
|
+
var FILL_SEARCH_AFTER_MS = 6e4;
|
|
23786
|
+
var ORDER_FILLED_EVENT = import_sdk17.IntentGatewayABI.find(
|
|
23787
|
+
(item) => item.type === "event" && item.name === "OrderFilled"
|
|
23788
|
+
);
|
|
23789
|
+
var DELIVERY_REQUEST_QUERY = `query TopUpDelivery($hash: String!) {
|
|
23790
|
+
requestV2s(
|
|
23791
|
+
filter: {
|
|
23792
|
+
source: { equalTo: "EVM-56" }
|
|
23793
|
+
dest: { equalTo: "POLKADOT-3367" }
|
|
23794
|
+
statusMetadata: { some: { transactionHash: { equalTo: $hash } } }
|
|
23795
|
+
}
|
|
23796
|
+
) {
|
|
23797
|
+
nodes { commitment }
|
|
23798
|
+
}
|
|
23799
|
+
}`;
|
|
23800
|
+
var ORDER_QUERY2 = `query TopUpOrder($commitment: String!) {
|
|
23801
|
+
iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
|
|
23802
|
+
nodes {
|
|
23803
|
+
status
|
|
23804
|
+
statusMetadata { nodes { status timestamp transactionHash filler } }
|
|
23805
|
+
}
|
|
23806
|
+
}
|
|
23807
|
+
}`;
|
|
23808
|
+
var CALL_TUPLE = [
|
|
23809
|
+
{
|
|
23810
|
+
type: "tuple[]",
|
|
23811
|
+
components: [
|
|
23812
|
+
{ name: "to", type: "address" },
|
|
23813
|
+
{ name: "value", type: "uint256" },
|
|
23814
|
+
{ name: "data", type: "bytes" }
|
|
23815
|
+
]
|
|
23816
|
+
}
|
|
23817
|
+
];
|
|
23818
|
+
var PENDING_STAGES = /* @__PURE__ */ new Set([
|
|
23819
|
+
"placing",
|
|
23820
|
+
"awaiting_bids",
|
|
23821
|
+
"filling",
|
|
23822
|
+
"delivering",
|
|
23823
|
+
"unfilled",
|
|
23824
|
+
"reclaiming"
|
|
23825
|
+
]);
|
|
23826
|
+
function isBridgeTopUpPending(stage) {
|
|
23827
|
+
return PENDING_STAGES.has(stage);
|
|
23828
|
+
}
|
|
23829
|
+
function encodeBridgeTopUpCall(bridgeToken, recipient, amount) {
|
|
23830
|
+
const data = (0, import_viem31.encodeFunctionData)({
|
|
23831
|
+
abi: import_sdk17.HyperFungibleTokenABI,
|
|
23832
|
+
functionName: "send",
|
|
23833
|
+
args: [
|
|
23834
|
+
{
|
|
23835
|
+
dest: (0, import_viem31.toHex)(NEXUS_STATE_MACHINE),
|
|
23836
|
+
to: recipient,
|
|
23837
|
+
amount,
|
|
23838
|
+
timeout: 0n,
|
|
23839
|
+
relayerFee: 0n,
|
|
23840
|
+
data: "0x"
|
|
23841
|
+
}
|
|
23842
|
+
]
|
|
23843
|
+
});
|
|
23844
|
+
return (0, import_viem31.encodeAbiParameters)(CALL_TUPLE, [[{ to: bridgeToken, value: 0n, data }]]);
|
|
23845
|
+
}
|
|
23846
|
+
function bidPreview(bid) {
|
|
23847
|
+
return {
|
|
23848
|
+
solverAddress: bid.solverAddress,
|
|
23849
|
+
outputs: bid.outputs.map((output) => ({ token: output.token, amount: output.amount.toString() }))
|
|
23850
|
+
};
|
|
23851
|
+
}
|
|
23852
|
+
function bridgeTopUpOutput(quotedOut) {
|
|
23853
|
+
const afterSlippage = quotedOut * (10000n - QUOTE_SLIPPAGE_BPS) / 10000n;
|
|
23854
|
+
return afterSlippage / NEXUS_UNIT * NEXUS_UNIT;
|
|
23855
|
+
}
|
|
23856
|
+
function serializeOrder(order) {
|
|
23857
|
+
return JSON.stringify(order, (_key, value) => typeof value === "bigint" ? { $bigint: value.toString() } : value);
|
|
23858
|
+
}
|
|
23859
|
+
function deserializeOrder(json) {
|
|
23860
|
+
return JSON.parse(
|
|
23861
|
+
json,
|
|
23862
|
+
(_key, value) => value && typeof value === "object" && typeof value.$bigint === "string" ? BigInt(value.$bigint) : value
|
|
23863
|
+
);
|
|
23864
|
+
}
|
|
23865
|
+
var BridgeTopUp = class {
|
|
23866
|
+
constructor(deps) {
|
|
23867
|
+
this.deps = deps;
|
|
23868
|
+
this.gateways = /* @__PURE__ */ new Map();
|
|
23869
|
+
/** Serializes state writes so a later stage never lands before an earlier one. */
|
|
23870
|
+
this.writes = Promise.resolve();
|
|
23871
|
+
this.logger = moduleLogger(deps.loggers, "bridge-top-up");
|
|
23872
|
+
}
|
|
23873
|
+
get bridgeToken() {
|
|
23874
|
+
return this.deps.configService.getAssetBySymbol(BRIDGE_TOP_UP_CHAIN, "BRIDGE") ?? BRIDGE_TOKEN_BSC;
|
|
23875
|
+
}
|
|
23876
|
+
/** Loads the stored top-up and resumes driving it when it was left open. Never rejects. */
|
|
23877
|
+
restore() {
|
|
23878
|
+
this.loaded ?? (this.loaded = (async () => {
|
|
23879
|
+
try {
|
|
23880
|
+
this.record = (await this.deps.state.get()).bridgeTopUp;
|
|
23881
|
+
} catch (err) {
|
|
23882
|
+
this.logger.warn({ err }, "Could not read the stored BRIDGE top-up");
|
|
23883
|
+
return;
|
|
23884
|
+
}
|
|
23885
|
+
const record = this.record;
|
|
23886
|
+
if (record?.stage === "failed" && record.placeTxHash && !record.commitment && record.sessionPrivateKey) {
|
|
23887
|
+
void this.recoverPlacement();
|
|
23888
|
+
return;
|
|
23889
|
+
}
|
|
23890
|
+
if (!record || !isBridgeTopUpPending(record.stage)) return;
|
|
23891
|
+
if (record.stage === "placing" && !record.placeTxHash) {
|
|
23892
|
+
await this.update({
|
|
23893
|
+
stage: "failed",
|
|
23894
|
+
error: "Simplex restarted before the order was placed. Nothing was escrowed."
|
|
23895
|
+
});
|
|
23896
|
+
return;
|
|
23897
|
+
}
|
|
23898
|
+
if (record.stage === "placing") {
|
|
23899
|
+
void this.recoverPlacement();
|
|
23900
|
+
return;
|
|
23901
|
+
}
|
|
23902
|
+
if (record.stage === "awaiting_bids" || record.stage === "filling") void this.resumeOrder();
|
|
23903
|
+
if (record.stage === "reclaiming") void this.driveReclaim();
|
|
23904
|
+
if (record.stage === "delivering") void this.resumeDelivery();
|
|
23905
|
+
})());
|
|
23906
|
+
return this.loaded;
|
|
23907
|
+
}
|
|
23908
|
+
async current() {
|
|
23909
|
+
await this.restore();
|
|
23910
|
+
return this.record ? publicState(this.record) : void 0;
|
|
23911
|
+
}
|
|
23912
|
+
/** Networks the filler runs and has delegated its account on. */
|
|
23913
|
+
async sourceChains() {
|
|
23914
|
+
const chainIds = this.deps.configService.getConfiguredChainIds();
|
|
23915
|
+
const delegated = await Promise.all(
|
|
23916
|
+
chainIds.map((chainId) => this.deps.delegation.isDelegated(`EVM-${chainId}`).catch(() => false))
|
|
23917
|
+
);
|
|
23918
|
+
return chainIds.filter((_, index) => delegated[index]);
|
|
23919
|
+
}
|
|
23920
|
+
/** USDC and USDT wallet balances on every network a top-up can be paid from. */
|
|
23921
|
+
async sources() {
|
|
23922
|
+
const rows = (await this.sourceChains()).flatMap(
|
|
23923
|
+
(chainId) => ["USDC", "USDT"].map(async (token) => {
|
|
23924
|
+
const chain = `EVM-${chainId}`;
|
|
23925
|
+
let tokenAddress;
|
|
23926
|
+
let decimals;
|
|
23927
|
+
try {
|
|
23928
|
+
;
|
|
23929
|
+
({ tokenAddress, decimals } = this.payToken(chain, token));
|
|
23930
|
+
} catch {
|
|
23931
|
+
return void 0;
|
|
23932
|
+
}
|
|
23933
|
+
const reserve = this.reserve(chain, tokenAddress);
|
|
23934
|
+
const row = { chain, chainId, token, address: tokenAddress, reserve: (0, import_viem31.formatUnits)(reserve, decimals) };
|
|
23935
|
+
try {
|
|
23936
|
+
const balance = await this.walletBalance(chain, tokenAddress);
|
|
23937
|
+
return {
|
|
23938
|
+
...row,
|
|
23939
|
+
balance: (0, import_viem31.formatUnits)(balance, decimals),
|
|
23940
|
+
spendable: (0, import_viem31.formatUnits)(balance > reserve ? balance - reserve : 0n, decimals)
|
|
23941
|
+
};
|
|
23942
|
+
} catch (err) {
|
|
23943
|
+
this.logger.debug({ err, chain, token }, "Could not read a top-up source balance");
|
|
23944
|
+
return { ...row, balance: null, spendable: null };
|
|
23945
|
+
}
|
|
23946
|
+
})
|
|
23947
|
+
);
|
|
23948
|
+
return (await Promise.all(rows)).filter((row) => row !== void 0);
|
|
23949
|
+
}
|
|
23950
|
+
async quote(params) {
|
|
23951
|
+
const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
|
|
23952
|
+
const amountIn = (0, import_viem31.parseUnits)(params.amount, decimals);
|
|
23953
|
+
if (amountIn <= 0n) throw new Error("Amount must be greater than 0");
|
|
23954
|
+
const gateway = await this.gateway(params.chain);
|
|
23955
|
+
const quote = await gateway.quoteIntent({ tokenIn: tokenAddress, tokenOut: this.bridgeToken, amountIn });
|
|
23956
|
+
const bridgeOut = quote.fillable ? bridgeTopUpOutput(quote.amountOut) : 0n;
|
|
23957
|
+
const rate = quote.amountIn > 0n ? Number((0, import_viem31.formatUnits)(quote.amountOut, BRIDGE_DECIMALS)) / Number((0, import_viem31.formatUnits)(quote.amountIn, decimals)) : 0;
|
|
23958
|
+
return {
|
|
23959
|
+
chain: params.chain,
|
|
23960
|
+
token: params.token,
|
|
23961
|
+
tokenAddress,
|
|
23962
|
+
amountIn: (0, import_viem31.formatUnits)(amountIn, decimals),
|
|
23963
|
+
bridgeOut: (0, import_viem31.formatUnits)(bridgeOut, BRIDGE_DECIMALS),
|
|
23964
|
+
rate: rate.toString(),
|
|
23965
|
+
fillable: quote.fillable && bridgeOut > 0n,
|
|
23966
|
+
maxFillableIn: (0, import_viem31.formatUnits)(quote.maxFillableIn, decimals),
|
|
23967
|
+
route: quote.route
|
|
23968
|
+
};
|
|
23969
|
+
}
|
|
23970
|
+
/** Re-quotes, then places and drives the order in the background. Returns its state. */
|
|
23971
|
+
async start(params) {
|
|
23972
|
+
if (!(await this.sourceChains()).includes((0, import_sdk17.getChainId)(params.chain) ?? -1)) {
|
|
23973
|
+
throw new Error(`${params.chain} is not a network the filler fills on`);
|
|
23974
|
+
}
|
|
23975
|
+
await this.restore();
|
|
23976
|
+
if (this.record && isBridgeTopUpPending(this.record.stage)) {
|
|
23977
|
+
throw new Error("A BRIDGE top-up is already pending");
|
|
23978
|
+
}
|
|
23979
|
+
const quote = await this.quote(params);
|
|
23980
|
+
if (!quote.fillable) {
|
|
23981
|
+
throw new Error(`The orderbook cannot fill ${quote.amountIn} ${params.token} (max ${quote.maxFillableIn})`);
|
|
23982
|
+
}
|
|
23983
|
+
const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
|
|
23984
|
+
const amountIn = (0, import_viem31.parseUnits)(quote.amountIn, decimals);
|
|
23985
|
+
const balance = await this.balanceOf(params.chain, tokenAddress);
|
|
23986
|
+
if (balance < amountIn) {
|
|
23987
|
+
const reserve = (0, import_viem31.formatUnits)(this.reserve(params.chain, tokenAddress), decimals);
|
|
23988
|
+
throw new Error(
|
|
23989
|
+
`Not enough ${params.token}: ${(0, import_viem31.formatUnits)(balance, decimals)} spendable after the ${reserve} ${params.token} kept for gas, need ${quote.amountIn}`
|
|
23990
|
+
);
|
|
23991
|
+
}
|
|
23992
|
+
const order = await this.buildOrder(params.chain, tokenAddress, amountIn, (0, import_viem31.parseUnits)(quote.bridgeOut, BRIDGE_DECIMALS));
|
|
23993
|
+
if (this.record && isBridgeTopUpPending(this.record.stage)) {
|
|
23994
|
+
throw new Error("A BRIDGE top-up is already pending");
|
|
23995
|
+
}
|
|
23996
|
+
const now = Date.now();
|
|
23997
|
+
this.record = {
|
|
23998
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
23999
|
+
chain: params.chain,
|
|
24000
|
+
token: params.token,
|
|
24001
|
+
amountIn: quote.amountIn,
|
|
24002
|
+
bridgeOut: quote.bridgeOut,
|
|
24003
|
+
stage: "placing",
|
|
24004
|
+
createdAt: now,
|
|
24005
|
+
updatedAt: now,
|
|
24006
|
+
stageTimes: { placing: now },
|
|
24007
|
+
order: serializeOrder(order)
|
|
24008
|
+
};
|
|
24009
|
+
await this.persist();
|
|
24010
|
+
void this.place(order, tokenAddress);
|
|
24011
|
+
return publicState(this.record);
|
|
24012
|
+
}
|
|
24013
|
+
/** Cancels an unfilled order and refunds its escrowed input to the filler wallet. */
|
|
24014
|
+
async reclaim() {
|
|
24015
|
+
await this.restore();
|
|
24016
|
+
const record = this.record;
|
|
24017
|
+
if (!record || record.stage !== "unfilled") throw new Error("There is no unfilled top-up to reclaim");
|
|
24018
|
+
await this.update({ stage: "reclaiming", error: void 0 });
|
|
24019
|
+
void this.driveReclaim();
|
|
24020
|
+
return publicState(this.record);
|
|
24021
|
+
}
|
|
24022
|
+
/**
|
|
24023
|
+
* Places the order, then tracks it. The SDK builds the `placeOrder` calldata and the
|
|
24024
|
+
* session key; the receipt is read back here, through the filler's own multi-endpoint
|
|
24025
|
+
* client, rather than by the SDK from one endpoint, where a node that has not caught up
|
|
24026
|
+
* with the new block fails the read and loses track of an order that did land.
|
|
24027
|
+
*/
|
|
24028
|
+
async place(order, tokenIn) {
|
|
24029
|
+
await this.drive(async (gateway) => {
|
|
24030
|
+
const gen = gateway.executeBest(order, SIMPLEX_REFERRER, { auctionTimeMs: AUCTION_TIME_MS });
|
|
24031
|
+
const first = await gen.next();
|
|
24032
|
+
await gen.return();
|
|
24033
|
+
if (first.done || first.value.status !== "AWAITING_PLACE_ORDER") {
|
|
24034
|
+
throw new Error("The SDK did not produce a placement transaction");
|
|
24035
|
+
}
|
|
24036
|
+
const placement = first.value;
|
|
24037
|
+
await this.update({ sessionPrivateKey: placement.sessionPrivateKey });
|
|
24038
|
+
const calls = this.placementCalls(order, tokenIn, placement);
|
|
24039
|
+
const { txHash } = await this.deps.tokenSender.submitCalls(this.record.chain, calls);
|
|
24040
|
+
await this.update({ placeTxHash: txHash });
|
|
24041
|
+
await this.recordPlacement(order, txHash);
|
|
24042
|
+
await this.trackOrder(gateway);
|
|
24043
|
+
});
|
|
24044
|
+
}
|
|
24045
|
+
/**
|
|
24046
|
+
* Reads the placement receipt and stores the order as the gateway committed it. Throws
|
|
24047
|
+
* when the transaction placed no order, so the top-up is marked failed.
|
|
24048
|
+
*/
|
|
24049
|
+
async recordPlacement(order, txHash) {
|
|
24050
|
+
const receipt = await this.deps.clientManager.getPublicClient(this.record.chain).waitForTransactionReceipt({ hash: txHash, timeout: RECEIPT_TIMEOUT_MS });
|
|
24051
|
+
const [placed] = (0, import_viem31.parseEventLogs)({ abi: import_sdk17.IntentGatewayABI, logs: receipt.logs, eventName: "OrderPlaced" });
|
|
24052
|
+
if (!placed) throw new Error(`Transaction ${txHash} placed no order`);
|
|
24053
|
+
const canonical = (0, import_sdk17.deriveCanonicalPlacedOrder)(order, placed.args);
|
|
24054
|
+
await this.update({
|
|
24055
|
+
stage: "awaiting_bids",
|
|
24056
|
+
commitment: canonical.id,
|
|
24057
|
+
order: serializeOrder(canonical),
|
|
24058
|
+
error: void 0
|
|
24059
|
+
});
|
|
24060
|
+
}
|
|
24061
|
+
/** Continues an order whose placement landed but was not recorded. */
|
|
24062
|
+
async recoverPlacement() {
|
|
24063
|
+
await this.drive(async (gateway) => {
|
|
24064
|
+
const record = this.record;
|
|
24065
|
+
await this.recordPlacement(deserializeOrder(record.order), record.placeTxHash);
|
|
24066
|
+
await this.trackOrder(gateway);
|
|
24067
|
+
});
|
|
24068
|
+
}
|
|
24069
|
+
async resumeDelivery() {
|
|
24070
|
+
await this.drive(() => this.deliver());
|
|
24071
|
+
}
|
|
24072
|
+
async resumeOrder() {
|
|
24073
|
+
await this.drive((gateway) => this.trackOrder(gateway));
|
|
24074
|
+
}
|
|
24075
|
+
/** Follows a placed order through bids to a fill, or to its deadline. */
|
|
24076
|
+
async trackOrder(gateway) {
|
|
24077
|
+
const record = this.record;
|
|
24078
|
+
const order = deserializeOrder(record.order);
|
|
24079
|
+
if (await gateway.isOrderFilled(order)) {
|
|
24080
|
+
await this.update({ stage: "delivering" });
|
|
24081
|
+
await this.deliver();
|
|
24082
|
+
return;
|
|
24083
|
+
}
|
|
24084
|
+
if (!record.sessionPrivateKey) {
|
|
24085
|
+
await this.update({ stage: "unfilled", error: "The order's session key was not saved, so it cannot be resumed" });
|
|
24086
|
+
return;
|
|
24087
|
+
}
|
|
24088
|
+
const gen = gateway.resumeBest(order, {
|
|
24089
|
+
sessionPrivateKey: record.sessionPrivateKey,
|
|
24090
|
+
auctionTimeMs: AUCTION_TIME_MS
|
|
24091
|
+
});
|
|
24092
|
+
for await (const value of gen) {
|
|
24093
|
+
if (await this.applyExecutionUpdate(value)) break;
|
|
24094
|
+
}
|
|
24095
|
+
if (this.record.stage === "delivering") await this.deliver();
|
|
24096
|
+
}
|
|
24097
|
+
/**
|
|
24098
|
+
* Carries the bought BRIDGE to Hyperbridge. The order's output call dispatched it from
|
|
24099
|
+
* BSC with no relayer fee. Relayers may still deliver it; when none has shortly after BSC
|
|
24100
|
+
* is final on Hyperbridge, the filler relays it itself: a request into Hyperbridge goes
|
|
24101
|
+
* in as an unsigned extrinsic and costs nothing. Runs until the request is delivered.
|
|
24102
|
+
*/
|
|
24103
|
+
async deliver() {
|
|
24104
|
+
let relayedAt = 0;
|
|
24105
|
+
while (!await this.syncDelivery()) {
|
|
24106
|
+
const finalized = this.record.deliveryStatuses?.find((status2) => status2.status === "SOURCE_FINALIZED");
|
|
24107
|
+
const due = finalized && Date.now() - finalized.timestamp > SELF_RELAY_GRACE_MS;
|
|
24108
|
+
if (due && Date.now() - relayedAt > DELIVERY_RETRY_MS) {
|
|
24109
|
+
relayedAt = Date.now();
|
|
24110
|
+
await this.selfRelay(this.record.deliveryCommitment).catch((err) => {
|
|
24111
|
+
this.logger.warn({ err }, "Self-relay of the BRIDGE to Hyperbridge failed; retrying");
|
|
24112
|
+
});
|
|
24113
|
+
}
|
|
24114
|
+
await (0, import_promises.setTimeout)(DELIVERY_POLL_MS);
|
|
24115
|
+
}
|
|
24116
|
+
const id = this.record.id;
|
|
24117
|
+
while (this.record?.id === id && this.needsStatusBackfill()) {
|
|
24118
|
+
await (0, import_promises.setTimeout)(DELIVERY_POLL_MS);
|
|
24119
|
+
await this.syncDelivery().catch((err) => this.logger.debug({ err }, "BRIDGE delivery backfill failed"));
|
|
24120
|
+
}
|
|
24121
|
+
}
|
|
24122
|
+
/** Delivered, but the indexer has yet to record the delivery, and not for too long. */
|
|
24123
|
+
needsStatusBackfill() {
|
|
24124
|
+
const record = this.record;
|
|
24125
|
+
if (!record || record.stage !== "delivered" || !record.deliveryCommitment) return false;
|
|
24126
|
+
if (record.deliveryStatuses?.some((status2) => status2.status === "DESTINATION")) return false;
|
|
24127
|
+
return Date.now() - (record.stageTimes?.delivered ?? 0) < STATUS_BACKFILL_MS;
|
|
24128
|
+
}
|
|
24129
|
+
/**
|
|
24130
|
+
* Reads where the order and its BRIDGE are, from the indexer and the chains, and records
|
|
24131
|
+
* it. Shared by the driver and {@link sync}, and never runs twice at once. True once the
|
|
24132
|
+
* BRIDGE has reached Hyperbridge.
|
|
24133
|
+
*/
|
|
24134
|
+
syncDelivery() {
|
|
24135
|
+
this.deliverySync ?? (this.deliverySync = this.readDelivery().finally(() => {
|
|
24136
|
+
this.deliverySync = void 0;
|
|
24137
|
+
}));
|
|
24138
|
+
return this.deliverySync;
|
|
24139
|
+
}
|
|
24140
|
+
async readDelivery() {
|
|
24141
|
+
const record = this.record;
|
|
24142
|
+
if (record.stage !== "delivering" && record.stage !== "delivered") return false;
|
|
24143
|
+
if (!record.fillTxHash) {
|
|
24144
|
+
const fillTxHash = await this.findFill();
|
|
24145
|
+
if (!fillTxHash) return false;
|
|
24146
|
+
await this.update({ fillTxHash });
|
|
24147
|
+
}
|
|
24148
|
+
if (!record.deliveryCommitment) {
|
|
24149
|
+
await this.update({ deliveryCommitment: await this.deliveryRequest(record.fillTxHash) });
|
|
24150
|
+
}
|
|
24151
|
+
const hyperbridge = await this.hyperbridge();
|
|
24152
|
+
const request = await withTimeout(
|
|
24153
|
+
this.ismpClient(hyperbridge).queryRequestWithStatus(record.deliveryCommitment),
|
|
24154
|
+
"the BRIDGE request status"
|
|
24155
|
+
);
|
|
24156
|
+
const statuses = (request?.statuses ?? []).filter((status2) => DELIVERY_STATUSES.has(status2.status)).map((status2) => ({
|
|
24157
|
+
status: status2.status,
|
|
24158
|
+
timestamp: toMs(status2.metadata.timestamp),
|
|
24159
|
+
txHash: status2.metadata.transactionHash
|
|
24160
|
+
}));
|
|
24161
|
+
if (statuses.length > 0) await this.update({ deliveryStatuses: statuses });
|
|
24162
|
+
const delivered = statuses.some((status2) => status2.status === "DESTINATION") || !!await withTimeout(hyperbridge.queryRequestReceipt(record.deliveryCommitment), "the request receipt");
|
|
24163
|
+
if (delivered && record.stage !== "delivered") await this.update({ stage: "delivered", error: void 0 });
|
|
24164
|
+
return delivered;
|
|
24165
|
+
}
|
|
24166
|
+
/**
|
|
24167
|
+
* The HyperFungibleToken request the fill dispatched to Hyperbridge. The indexer finds it by
|
|
24168
|
+
* the fill's transaction, as the explorer's search does; the fill's own `Sent` log is the
|
|
24169
|
+
* fallback while the indexer catches up.
|
|
24170
|
+
*/
|
|
24171
|
+
async deliveryRequest(fillTxHash) {
|
|
24172
|
+
const response = await withTimeout(
|
|
24173
|
+
fetch(this.deps.indexerUrl, {
|
|
24174
|
+
method: "POST",
|
|
24175
|
+
headers: { "content-type": "application/json" },
|
|
24176
|
+
body: JSON.stringify({ query: DELIVERY_REQUEST_QUERY, variables: { hash: fillTxHash } })
|
|
24177
|
+
}),
|
|
24178
|
+
"the indexer"
|
|
24179
|
+
);
|
|
24180
|
+
const body = await response.json();
|
|
24181
|
+
const indexed = body.data?.requestV2s?.nodes[0]?.commitment;
|
|
24182
|
+
if (indexed) return indexed;
|
|
24183
|
+
const fill = await withTimeout(this.bsc().getTransactionReceipt({ hash: fillTxHash }), "the fill receipt");
|
|
24184
|
+
const [sent] = (0, import_viem31.parseEventLogs)({ abi: import_sdk17.HyperFungibleTokenABI, logs: fill.logs, eventName: "Sent" });
|
|
24185
|
+
if (!sent) throw new Error(`The fill ${fillTxHash} sent no BRIDGE to Hyperbridge`);
|
|
24186
|
+
return sent.args.commitment;
|
|
24187
|
+
}
|
|
24188
|
+
/**
|
|
24189
|
+
* BSC, read through every endpoint there is: the filler need not run it, and a single public
|
|
24190
|
+
* endpoint refuses receipts it considers old.
|
|
24191
|
+
*/
|
|
24192
|
+
bsc() {
|
|
24193
|
+
this.bscClient ?? (this.bscClient = (0, import_viem31.createPublicClient)({
|
|
24194
|
+
chain: import_chains2.bsc,
|
|
24195
|
+
transport: (0, import_viem31.fallback)(this.bscProofRpcUrls().map((url) => (0, import_viem31.http)(url, { timeout: 1e4, retryCount: 0 })))
|
|
24196
|
+
}));
|
|
24197
|
+
return this.bscClient;
|
|
24198
|
+
}
|
|
24199
|
+
/**
|
|
24200
|
+
* The transaction that filled the order. The executor reports it only for a fill this
|
|
24201
|
+
* process selected, so it comes from the indexer, or, while the indexer has yet to reach
|
|
24202
|
+
* BSC's latest blocks, from the gateway's `OrderFilled` log around when the fill was seen.
|
|
24203
|
+
*/
|
|
24204
|
+
async findFill() {
|
|
24205
|
+
const record = this.record;
|
|
24206
|
+
const indexed = (await this.indexedOrder())?.find((status2) => status2.status === "FILLED");
|
|
24207
|
+
if (indexed) {
|
|
24208
|
+
if (indexed.filler && !record.filler) await this.update({ filler: indexed.filler });
|
|
24209
|
+
return indexed.transactionHash;
|
|
24210
|
+
}
|
|
24211
|
+
const seenAt = record.events?.find((event) => event.status === "FILLED")?.timestamp ?? record.stageTimes?.delivering;
|
|
24212
|
+
if (!seenAt) return void 0;
|
|
24213
|
+
const client = this.bsc();
|
|
24214
|
+
const [latest, blockTime] = await Promise.all([
|
|
24215
|
+
withTimeout(client.getBlock(), "the latest BSC block"),
|
|
24216
|
+
this.blockTimeSeconds()
|
|
24217
|
+
]);
|
|
24218
|
+
const blockAt = (ms) => latest.number - BigInt(Math.ceil((Number(latest.timestamp) - ms / 1e3) / blockTime));
|
|
24219
|
+
const from = blockAt(seenAt - FILL_SEARCH_BEFORE_MS);
|
|
24220
|
+
const to = (() => {
|
|
24221
|
+
const end = blockAt(seenAt + FILL_SEARCH_AFTER_MS);
|
|
24222
|
+
return end < latest.number ? end : latest.number;
|
|
24223
|
+
})();
|
|
24224
|
+
for (let start = from; start <= to; start += LOG_CHUNK_BLOCKS) {
|
|
24225
|
+
const end = start + LOG_CHUNK_BLOCKS - 1n < to ? start + LOG_CHUNK_BLOCKS - 1n : to;
|
|
24226
|
+
const logs = await withTimeout(
|
|
24227
|
+
client.getLogs({
|
|
24228
|
+
address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
|
|
24229
|
+
event: ORDER_FILLED_EVENT,
|
|
24230
|
+
args: { commitment: record.commitment },
|
|
24231
|
+
fromBlock: start,
|
|
24232
|
+
toBlock: end
|
|
24233
|
+
}),
|
|
24234
|
+
"the fill on BSC"
|
|
24235
|
+
);
|
|
24236
|
+
if (logs[0]) return logs[0].transactionHash;
|
|
24237
|
+
}
|
|
24238
|
+
return void 0;
|
|
24239
|
+
}
|
|
24240
|
+
/** The order's statuses as the indexer has them: placed, filled, redeemed or refunded. */
|
|
24241
|
+
async indexedOrder() {
|
|
24242
|
+
const response = await withTimeout(
|
|
24243
|
+
fetch(this.deps.indexerUrl, {
|
|
24244
|
+
method: "POST",
|
|
24245
|
+
headers: { "content-type": "application/json" },
|
|
24246
|
+
body: JSON.stringify({ query: ORDER_QUERY2, variables: { commitment: this.record.commitment } })
|
|
24247
|
+
}),
|
|
24248
|
+
"the indexer"
|
|
24249
|
+
);
|
|
24250
|
+
const body = await response.json();
|
|
24251
|
+
return body.data?.iOrderV3s?.nodes[0]?.statusMetadata.nodes.map((node) => ({
|
|
24252
|
+
status: node.status,
|
|
24253
|
+
timestamp: toMs(node.timestamp),
|
|
24254
|
+
transactionHash: node.transactionHash,
|
|
24255
|
+
filler: node.filler ?? void 0
|
|
24256
|
+
}));
|
|
24257
|
+
}
|
|
24258
|
+
/**
|
|
24259
|
+
* Brings a pending top-up up to date from the chains and the indexer, for a dashboard that
|
|
24260
|
+
* is showing it: someone may have filled, delivered or refunded it while this process was
|
|
24261
|
+
* not looking. One sync at a time; a call made while one runs waits for that one.
|
|
24262
|
+
*/
|
|
24263
|
+
sync() {
|
|
24264
|
+
this.syncing ?? (this.syncing = this.reconcile().catch((err) => this.logger.debug({ err }, "BRIDGE top-up sync failed")).finally(() => {
|
|
24265
|
+
this.syncing = void 0;
|
|
24266
|
+
}));
|
|
24267
|
+
return this.syncing;
|
|
24268
|
+
}
|
|
24269
|
+
async reconcile() {
|
|
24270
|
+
await this.restore();
|
|
24271
|
+
const record = this.record;
|
|
24272
|
+
if (this.needsStatusBackfill()) {
|
|
24273
|
+
await this.syncDelivery();
|
|
24274
|
+
return;
|
|
24275
|
+
}
|
|
24276
|
+
if (!record || !isBridgeTopUpPending(record.stage) || !record.commitment) return;
|
|
24277
|
+
if (record.stage === "awaiting_bids" || record.stage === "filling") {
|
|
24278
|
+
const statuses = await this.indexedOrder();
|
|
24279
|
+
const fill = statuses?.find((status2) => status2.status === "FILLED");
|
|
24280
|
+
if (fill) {
|
|
24281
|
+
await this.update({
|
|
24282
|
+
stage: "delivering",
|
|
24283
|
+
fillTxHash: fill.transactionHash,
|
|
24284
|
+
filler: fill.filler ?? record.filler,
|
|
24285
|
+
stageTimes: { ...record.stageTimes, delivering: fill.timestamp }
|
|
24286
|
+
});
|
|
24287
|
+
void this.resumeDelivery();
|
|
24288
|
+
} else if (statuses?.some((status2) => status2.status === "REFUNDED")) {
|
|
24289
|
+
await this.update({ stage: "reclaimed" });
|
|
24290
|
+
}
|
|
24291
|
+
return;
|
|
24292
|
+
}
|
|
24293
|
+
if (record.stage === "delivering") {
|
|
24294
|
+
await this.syncDelivery();
|
|
24295
|
+
return;
|
|
24296
|
+
}
|
|
24297
|
+
if (record.stage === "unfilled" || record.stage === "reclaiming") {
|
|
24298
|
+
const gateway = await this.gateway(record.chain);
|
|
24299
|
+
await this.refunded(gateway, deserializeOrder(record.order));
|
|
24300
|
+
}
|
|
24301
|
+
}
|
|
24302
|
+
/**
|
|
24303
|
+
* Submits the request to Hyperbridge with a proof from BSC state. Public BSC endpoints
|
|
24304
|
+
* differ in whether they serve `eth_getProof` for recent blocks, so each is tried in turn.
|
|
24305
|
+
*/
|
|
24306
|
+
async selfRelay(commitment) {
|
|
24307
|
+
const hyperbridge = await this.hyperbridge();
|
|
24308
|
+
const failures = [];
|
|
24309
|
+
for (const rpcUrl of this.bscProofRpcUrls()) {
|
|
24310
|
+
if (await withTimeout(hyperbridge.queryRequestReceipt(commitment), "the request receipt")) return;
|
|
24311
|
+
try {
|
|
24312
|
+
const client = this.ismpClient(hyperbridge, rpcUrl);
|
|
24313
|
+
const receipt = await withTimeout(client.aggregateTransactionWithCommitment(commitment), "the relay", 12e4);
|
|
24314
|
+
await this.update({ deliveryTxHash: receipt.transactionHash });
|
|
24315
|
+
return;
|
|
24316
|
+
} catch (err) {
|
|
24317
|
+
const reason = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
24318
|
+
failures.push(`${new URL(rpcUrl).host}: ${reason}`);
|
|
24319
|
+
}
|
|
24320
|
+
}
|
|
24321
|
+
throw new Error(`Could not relay the BRIDGE to Hyperbridge (${failures.join("; ")})`);
|
|
24322
|
+
}
|
|
24323
|
+
/** The operator's BSC endpoints first, then the free public ones. */
|
|
24324
|
+
bscProofRpcUrls() {
|
|
24325
|
+
const urls = [
|
|
24326
|
+
...this.deps.configService.getRpcUrls(BRIDGE_TOP_UP_CHAIN),
|
|
24327
|
+
...chainByChainId((0, import_sdk17.getChainId)(BRIDGE_TOP_UP_CHAIN))?.defaultRpcUrls ?? []
|
|
24328
|
+
];
|
|
24329
|
+
return urls.filter((url, index) => url && urls.indexOf(url) === index);
|
|
24330
|
+
}
|
|
24331
|
+
/** Records an executor update. True when the order reached a final state. */
|
|
24332
|
+
async applyExecutionUpdate(value) {
|
|
24333
|
+
switch (value.status) {
|
|
24334
|
+
case "NEW_BID":
|
|
24335
|
+
case "BIDS_RECEIVED": {
|
|
24336
|
+
const bids = value.status === "NEW_BID" ? [value.bid] : value.bids;
|
|
24337
|
+
const bidCount = value.status === "BIDS_RECEIVED" ? Number(value.bidCount ?? bids.length) : void 0;
|
|
24338
|
+
await this.recordEvent({ status: value.status, bidCount, bids: bids.map(bidPreview) });
|
|
24339
|
+
if (bidCount !== void 0) await this.update({ bidCount: Math.max(this.record.bidCount ?? 0, bidCount) });
|
|
24340
|
+
return false;
|
|
24341
|
+
}
|
|
24342
|
+
case "BID_SELECTED":
|
|
24343
|
+
await this.recordEvent({
|
|
24344
|
+
status: "BID_SELECTED",
|
|
24345
|
+
selectedSolver: value.selectedSolver,
|
|
24346
|
+
userOpHash: value.userOpHash,
|
|
24347
|
+
txHash: value.transactionHash
|
|
24348
|
+
});
|
|
24349
|
+
await this.update({ stage: "filling", fillTxHash: value.transactionHash });
|
|
24350
|
+
return false;
|
|
24351
|
+
case "FILLED":
|
|
24352
|
+
await this.recordEvent({
|
|
24353
|
+
status: "FILLED",
|
|
24354
|
+
selectedSolver: value.selectedSolver,
|
|
24355
|
+
userOpHash: value.userOpHash,
|
|
24356
|
+
txHash: value.transactionHash
|
|
24357
|
+
});
|
|
24358
|
+
await this.update({
|
|
24359
|
+
stage: "delivering",
|
|
24360
|
+
fillTxHash: value.transactionHash ?? this.record.fillTxHash,
|
|
24361
|
+
error: void 0
|
|
24362
|
+
});
|
|
24363
|
+
return true;
|
|
24364
|
+
case "EXPIRED":
|
|
24365
|
+
case "FAILED":
|
|
24366
|
+
await this.update({ stage: "unfilled", error: value.error });
|
|
24367
|
+
return true;
|
|
24368
|
+
case "CANCELLED":
|
|
24369
|
+
await this.update({ stage: "reclaimed" });
|
|
24370
|
+
return true;
|
|
24371
|
+
default:
|
|
24372
|
+
return false;
|
|
24373
|
+
}
|
|
24374
|
+
}
|
|
24375
|
+
/**
|
|
24376
|
+
* Recovers an unfilled order's escrow, following the Hyperbridge app's recovery: the SDK
|
|
24377
|
+
* cancel flow from the paying chain (BSC's expiry height finalized, the cancel sent, the
|
|
24378
|
+
* cancellation finalized on the source, verified and finalized on Hyperbridge), then the
|
|
24379
|
+
* refund. A relayer normally delivers that refund; if none has within a few minutes of
|
|
24380
|
+
* Hyperbridge finalizing, the filler submits Hyperbridge's response itself. Each step is
|
|
24381
|
+
* recorded as it lands, and a restart resumes from the SDK's persisted progress.
|
|
24382
|
+
*/
|
|
24383
|
+
async driveReclaim() {
|
|
24384
|
+
await this.drive(async (gateway) => {
|
|
24385
|
+
const record = this.record;
|
|
24386
|
+
const order = deserializeOrder(record.order);
|
|
24387
|
+
if (await this.refunded(gateway, order)) return;
|
|
24388
|
+
const sameChain = record.chain === BRIDGE_TOP_UP_CHAIN;
|
|
24389
|
+
const ismp = await this.cancelClient(record.chain);
|
|
24390
|
+
const gen = gateway.cancelOrder(order, ismp);
|
|
24391
|
+
let input;
|
|
24392
|
+
while (true) {
|
|
24393
|
+
const { value, done } = await gen.next(input);
|
|
24394
|
+
input = void 0;
|
|
24395
|
+
if (done) break;
|
|
24396
|
+
switch (value.status) {
|
|
24397
|
+
case "DESTINATION_FINALIZED":
|
|
24398
|
+
await this.recordRecoveryStep("DESTINATION_FINALIZED");
|
|
24399
|
+
break;
|
|
24400
|
+
case "AWAITING_CANCEL_TRANSACTION": {
|
|
24401
|
+
if (this.record.reclaimTxHash && !this.hasRecoveryStep("ORDER_CANCELED")) {
|
|
24402
|
+
input = this.record.reclaimTxHash;
|
|
24403
|
+
break;
|
|
24404
|
+
}
|
|
24405
|
+
const calls = [];
|
|
24406
|
+
const quote = await gateway.quoteCancelOrder(order);
|
|
24407
|
+
if (value.value === 0n && quote.relayerFee > 0n) {
|
|
24408
|
+
const feeToken = await this.feeToken(record.chain);
|
|
24409
|
+
calls.push({
|
|
24410
|
+
target: feeToken,
|
|
24411
|
+
value: 0n,
|
|
24412
|
+
data: (0, import_viem31.encodeFunctionData)({
|
|
24413
|
+
abi: ERC20_ABI,
|
|
24414
|
+
functionName: "approve",
|
|
24415
|
+
args: [value.to, quote.relayerFee]
|
|
24416
|
+
})
|
|
24417
|
+
});
|
|
24418
|
+
}
|
|
24419
|
+
calls.push({ target: value.to, value: value.value, data: value.data });
|
|
24420
|
+
const { txHash } = await this.deps.tokenSender.submitCalls(record.chain, calls);
|
|
24421
|
+
await this.update({ reclaimTxHash: txHash });
|
|
24422
|
+
input = txHash;
|
|
24423
|
+
break;
|
|
24424
|
+
}
|
|
24425
|
+
case "CANCEL_STARTED":
|
|
24426
|
+
await this.recordRecoveryStep("ORDER_CANCELED", value.receipt.transactionHash);
|
|
24427
|
+
break;
|
|
24428
|
+
case "SOURCE_FINALIZED":
|
|
24429
|
+
case "HYPERBRIDGE_DELIVERED":
|
|
24430
|
+
case "HYPERBRIDGE_FINALIZED": {
|
|
24431
|
+
const metadata = value.metadata;
|
|
24432
|
+
await this.recordRecoveryStep(
|
|
24433
|
+
value.status,
|
|
24434
|
+
metadata.transactionHash,
|
|
24435
|
+
metadata.timestamp ? toMs(metadata.timestamp) : void 0
|
|
24436
|
+
);
|
|
24437
|
+
if (value.status === "HYPERBRIDGE_FINALIZED" && metadata.calldata) {
|
|
24438
|
+
await this.update({ claimCalldata: metadata.calldata });
|
|
24439
|
+
}
|
|
24440
|
+
break;
|
|
24441
|
+
}
|
|
24442
|
+
case "CANCELLATION_COMPLETE":
|
|
24443
|
+
await this.recordRecoveryStep("ORDER_CANCELED", value.transactionHash);
|
|
24444
|
+
await this.recordRecoveryStep("FUNDS_CLAIMED", value.transactionHash);
|
|
24445
|
+
break;
|
|
24446
|
+
}
|
|
24447
|
+
}
|
|
24448
|
+
if (sameChain && this.hasRecoveryStep("FUNDS_CLAIMED")) {
|
|
24449
|
+
await this.update({ stage: "reclaimed", error: void 0 });
|
|
24450
|
+
return;
|
|
24451
|
+
}
|
|
24452
|
+
while (!await this.refunded(gateway, order)) {
|
|
24453
|
+
const finalizedAt = this.record.recoverySteps?.find((s) => s.step === "HYPERBRIDGE_FINALIZED")?.timestamp;
|
|
24454
|
+
const claimable = this.record.claimCalldata && finalizedAt && Date.now() - finalizedAt > RELAYER_DELIVERY_WAIT_MS;
|
|
24455
|
+
if (claimable && !this.record.claimTxHash) {
|
|
24456
|
+
await this.claimRefund(record.chain, this.record.claimCalldata);
|
|
24457
|
+
continue;
|
|
24458
|
+
}
|
|
24459
|
+
await (0, import_promises.setTimeout)(REFUND_POLL_MS);
|
|
24460
|
+
}
|
|
24461
|
+
});
|
|
24462
|
+
}
|
|
24463
|
+
/**
|
|
24464
|
+
* Submits Hyperbridge's finalized response to the source chain's ISMP handler, which
|
|
24465
|
+
* refunds the escrow, as the Hyperbridge app's "Claim funds" does.
|
|
24466
|
+
*/
|
|
24467
|
+
async claimRefund(chain, calldata) {
|
|
24468
|
+
const params = await withTimeout(
|
|
24469
|
+
this.deps.clientManager.getPublicClient(chain).readContract({
|
|
24470
|
+
abi: EVM_HOST,
|
|
24471
|
+
address: this.deps.configService.getHostAddress(chain),
|
|
24472
|
+
functionName: "hostParams"
|
|
24473
|
+
}),
|
|
24474
|
+
"the host's handler"
|
|
24475
|
+
);
|
|
24476
|
+
const { txHash } = await this.deps.tokenSender.submitCalls(chain, [
|
|
24477
|
+
{ target: params.handler, value: 0n, data: calldata }
|
|
24478
|
+
]);
|
|
24479
|
+
await this.update({ claimTxHash: txHash });
|
|
24480
|
+
}
|
|
24481
|
+
/**
|
|
24482
|
+
* Whether the escrow is back with the filler, from the source gateway or the indexer's
|
|
24483
|
+
* REFUNDED status; records the refund as the recovery's last step when it is.
|
|
24484
|
+
*/
|
|
24485
|
+
async refunded(gateway, order) {
|
|
24486
|
+
const onChain = await withTimeout(gateway.isOrderRefunded(order), "the refund").catch(() => false);
|
|
24487
|
+
const indexed = (await this.indexedOrder().catch(() => void 0))?.find((s) => s.status === "REFUNDED");
|
|
24488
|
+
if (!onChain && !indexed) return false;
|
|
24489
|
+
if (!this.hasRecoveryStep("FUNDS_CLAIMED")) {
|
|
24490
|
+
await this.recordRecoveryStep(
|
|
24491
|
+
"FUNDS_CLAIMED",
|
|
24492
|
+
this.record.claimTxHash ?? indexed?.transactionHash,
|
|
24493
|
+
indexed?.timestamp
|
|
24494
|
+
);
|
|
24495
|
+
}
|
|
24496
|
+
await this.update({ stage: "reclaimed", error: void 0 });
|
|
24497
|
+
return true;
|
|
24498
|
+
}
|
|
24499
|
+
hasRecoveryStep(step) {
|
|
24500
|
+
return !!this.record?.recoverySteps?.some((entry) => entry.step === step);
|
|
24501
|
+
}
|
|
24502
|
+
/** Records a recovery step once, at the time it landed (or now). */
|
|
24503
|
+
async recordRecoveryStep(step, txHash, timestamp) {
|
|
24504
|
+
if (this.hasRecoveryStep(step)) return;
|
|
24505
|
+
const steps = [...this.record.recoverySteps ?? [], { step, txHash, timestamp: timestamp ?? Date.now() }];
|
|
24506
|
+
await this.update({ recoverySteps: steps });
|
|
24507
|
+
}
|
|
24508
|
+
/** Runs one background flow for the record; a failure is recorded rather than thrown. */
|
|
24509
|
+
async drive(flow) {
|
|
24510
|
+
const record = this.record;
|
|
24511
|
+
if (!record || this.driving === record.id) return;
|
|
24512
|
+
this.driving = record.id;
|
|
24513
|
+
try {
|
|
24514
|
+
await flow(await this.gateway(record.chain));
|
|
24515
|
+
} catch (err) {
|
|
24516
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
24517
|
+
this.logger.warn({ err, id: record.id, stage: record.stage }, "BRIDGE top-up step failed");
|
|
24518
|
+
if (record.stage === "placing" && !record.placeTxHash) {
|
|
24519
|
+
await this.update({ stage: "failed", error });
|
|
24520
|
+
} else if (record.stage === "delivering") {
|
|
24521
|
+
await this.update({ error: `Delivery to Hyperbridge is retrying: ${error}` });
|
|
24522
|
+
setTimeout(() => void this.resumeDelivery(), DELIVERY_RETRY_MS);
|
|
24523
|
+
} else if (record.stage === "reclaiming") {
|
|
24524
|
+
await this.update({ error: `Recovery is retrying: ${error}` });
|
|
24525
|
+
setTimeout(() => void this.driveReclaim(), DELIVERY_RETRY_MS);
|
|
24526
|
+
} else if (record.stage === "placing") {
|
|
24527
|
+
await this.update({ stage: "failed", error: `${error} (placement tx ${record.placeTxHash})` });
|
|
24528
|
+
} else {
|
|
24529
|
+
await this.update({ stage: "unfilled", error });
|
|
24530
|
+
}
|
|
24531
|
+
} finally {
|
|
24532
|
+
if (this.driving === record.id) this.driving = void 0;
|
|
24533
|
+
this.logger.info(publicState(record), "BRIDGE top-up step finished");
|
|
24534
|
+
}
|
|
24535
|
+
}
|
|
24536
|
+
/** Keeps what the executor reported, for the timeline's offers and its links. */
|
|
24537
|
+
async recordEvent(event) {
|
|
24538
|
+
const events = [...this.record.events ?? [], { ...event, timestamp: Date.now() }];
|
|
24539
|
+
await this.update({ events: events.slice(-MAX_EVENTS) });
|
|
24540
|
+
}
|
|
24541
|
+
async update(patch) {
|
|
24542
|
+
if (!this.record) return;
|
|
24543
|
+
const now = Date.now();
|
|
24544
|
+
if (patch.stage && patch.stage !== this.record.stage) {
|
|
24545
|
+
patch = { ...patch, stageTimes: { ...this.record.stageTimes, [patch.stage]: now } };
|
|
24546
|
+
}
|
|
24547
|
+
Object.assign(this.record, patch, { updatedAt: now });
|
|
24548
|
+
await this.persist();
|
|
24549
|
+
}
|
|
24550
|
+
persist() {
|
|
24551
|
+
const snapshot = this.record ? { ...this.record } : void 0;
|
|
24552
|
+
const write = this.writes.then(() => patchRuntimeState(this.deps.state, { bridgeTopUp: snapshot }));
|
|
24553
|
+
this.writes = write.catch((err) => this.logger.warn({ err }, "Could not save the BRIDGE top-up"));
|
|
24554
|
+
return write.then(
|
|
24555
|
+
() => void 0,
|
|
24556
|
+
() => void 0
|
|
24557
|
+
);
|
|
24558
|
+
}
|
|
24559
|
+
async buildOrder(chain, tokenIn, amountIn, bridgeOut) {
|
|
24560
|
+
const dispatcher = await this.callDispatcher();
|
|
24561
|
+
const bsc = this.bsc();
|
|
24562
|
+
const [block, blockTime] = await Promise.all([bsc.getBlockNumber(), this.blockTimeSeconds()]);
|
|
24563
|
+
return {
|
|
24564
|
+
user: (0, import_sdk17.bytes20ToBytes32)(this.deps.solver),
|
|
24565
|
+
source: chain,
|
|
24566
|
+
destination: BRIDGE_TOP_UP_CHAIN,
|
|
24567
|
+
deadline: block + BigInt(Math.ceil(ORDER_WINDOW_SECONDS / blockTime)),
|
|
24568
|
+
nonce: 0n,
|
|
24569
|
+
fees: 0n,
|
|
24570
|
+
session: "0x0000000000000000000000000000000000000000",
|
|
24571
|
+
predispatch: { assets: [], call: "0x" },
|
|
24572
|
+
inputs: [{ token: (0, import_sdk17.bytes20ToBytes32)(tokenIn), amount: amountIn }],
|
|
24573
|
+
output: {
|
|
24574
|
+
beneficiary: (0, import_sdk17.bytes20ToBytes32)(dispatcher),
|
|
24575
|
+
assets: [{ token: (0, import_sdk17.bytes20ToBytes32)(this.bridgeToken), amount: bridgeOut }],
|
|
24576
|
+
call: encodeBridgeTopUpCall(this.bridgeToken, this.deps.substratePublicKey, bridgeOut)
|
|
24577
|
+
}
|
|
24578
|
+
};
|
|
24579
|
+
}
|
|
24580
|
+
/**
|
|
24581
|
+
* Approvals for the gateway to pull the input and the order fee, then `placeOrder`.
|
|
24582
|
+
* The fee token is often the input token itself; one approval then covers both.
|
|
24583
|
+
*/
|
|
24584
|
+
placementCalls(order, tokenIn, placement) {
|
|
24585
|
+
const amountIn = order.inputs[0].amount;
|
|
24586
|
+
const approvals = /* @__PURE__ */ new Map([[tokenIn.toLowerCase(), amountIn]]);
|
|
24587
|
+
if (placement.feeTokenAmount > 0n) {
|
|
24588
|
+
const fee = placement.feeTokenAddress.toLowerCase();
|
|
24589
|
+
approvals.set(fee, (approvals.get(fee) ?? 0n) + placement.feeTokenAmount);
|
|
24590
|
+
}
|
|
24591
|
+
const calls = [...approvals].map(([token, amount]) => ({
|
|
24592
|
+
target: token,
|
|
24593
|
+
value: 0n,
|
|
24594
|
+
data: (0, import_viem31.encodeFunctionData)({ abi: ERC20_ABI, functionName: "approve", args: [placement.to, amount] })
|
|
24595
|
+
}));
|
|
24596
|
+
calls.push({ target: placement.to, value: placement.value, data: placement.data });
|
|
24597
|
+
return calls;
|
|
24598
|
+
}
|
|
24599
|
+
payToken(chain, token) {
|
|
24600
|
+
const { configService } = this.deps;
|
|
24601
|
+
const tokenAddress = token === "USDC" ? configService.getUsdcAsset(chain) : configService.getUsdtAsset(chain);
|
|
24602
|
+
if (!tokenAddress || /^0x0+$/.test(tokenAddress)) throw new Error(`${token} is not configured on ${chain}`);
|
|
24603
|
+
const decimals = token === "USDC" ? configService.getUsdcDecimals(chain) : configService.getUsdtDecimals(chain);
|
|
24604
|
+
return { tokenAddress, decimals };
|
|
24605
|
+
}
|
|
24606
|
+
async walletBalance(chain, token) {
|
|
24607
|
+
return await this.deps.clientManager.getPublicClient(chain).readContract({
|
|
24608
|
+
abi: ERC20_ABI,
|
|
24609
|
+
address: token,
|
|
24610
|
+
functionName: "balanceOf",
|
|
24611
|
+
args: [this.deps.solver]
|
|
24612
|
+
});
|
|
24613
|
+
}
|
|
24614
|
+
/** What the paymaster pulls for gas on the sponsored path, which a top-up must leave. */
|
|
24615
|
+
reserve(chain, token) {
|
|
24616
|
+
return paymasterReserveForToken(chain, token.toLowerCase(), this.deps.configService);
|
|
24617
|
+
}
|
|
24618
|
+
/** Spendable wallet balance: the paymaster's gas reserve is held back. */
|
|
24619
|
+
async balanceOf(chain, token) {
|
|
24620
|
+
const balance = await this.walletBalance(chain, token);
|
|
24621
|
+
const reserve = this.reserve(chain, token);
|
|
24622
|
+
return balance > reserve ? balance - reserve : 0n;
|
|
24623
|
+
}
|
|
24624
|
+
async feeToken(chain) {
|
|
24625
|
+
return await this.deps.clientManager.getPublicClient(chain).readContract({
|
|
24626
|
+
abi: [{ type: "function", name: "feeToken", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] }],
|
|
24627
|
+
address: this.deps.configService.getHostAddress(chain),
|
|
24628
|
+
functionName: "feeToken"
|
|
24629
|
+
});
|
|
24630
|
+
}
|
|
24631
|
+
/** The CallDispatcher the BSC gateway runs output calls through, read from the gateway itself. */
|
|
24632
|
+
callDispatcher() {
|
|
24633
|
+
this.dispatcher ?? (this.dispatcher = (async () => {
|
|
24634
|
+
const params = await this.bsc().readContract({
|
|
24635
|
+
abi: INTENT_GATEWAY_V2_ABI,
|
|
24636
|
+
address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
|
|
24637
|
+
functionName: "params"
|
|
24638
|
+
});
|
|
24639
|
+
return params.dispatcher;
|
|
24640
|
+
})().catch((err) => {
|
|
24641
|
+
this.dispatcher = void 0;
|
|
24642
|
+
throw err;
|
|
24643
|
+
}));
|
|
24644
|
+
return this.dispatcher;
|
|
24645
|
+
}
|
|
24646
|
+
async blockTimeSeconds() {
|
|
24647
|
+
const client = this.bsc();
|
|
24648
|
+
const latest = await client.getBlock();
|
|
24649
|
+
const earlier = await client.getBlock({ blockNumber: latest.number - 100n });
|
|
24650
|
+
const seconds = Number(latest.timestamp - earlier.timestamp) / 100;
|
|
24651
|
+
return seconds > 0 ? seconds : 1;
|
|
24652
|
+
}
|
|
24653
|
+
evmChain(chain, rpcUrl) {
|
|
24654
|
+
const { configService } = this.deps;
|
|
24655
|
+
const bundlerUrl = chain === BRIDGE_TOP_UP_CHAIN ? configService.getBundlerUrl(chain) ?? hyperbridgeBundlerUrl((0, import_sdk17.getChainId)(chain)) : void 0;
|
|
24656
|
+
return import_sdk17.EvmChain.fromParams({
|
|
24657
|
+
chainId: (0, import_sdk17.getChainId)(chain),
|
|
24658
|
+
host: configService.getHostAddress(chain),
|
|
24659
|
+
rpcUrl: rpcUrl ?? configService.getRpcUrl(chain),
|
|
24660
|
+
bundlerUrl
|
|
24661
|
+
});
|
|
24662
|
+
}
|
|
24663
|
+
gateway(source) {
|
|
24664
|
+
let gateway = this.gateways.get(source);
|
|
24665
|
+
if (!gateway) {
|
|
24666
|
+
gateway = (async () => import_sdk17.IntentGateway.create(
|
|
24667
|
+
this.evmChain(source),
|
|
24668
|
+
this.evmChain(BRIDGE_TOP_UP_CHAIN),
|
|
24669
|
+
await this.deps.hyperbridge,
|
|
24670
|
+
{ storagePath: this.deps.storagePath }
|
|
24671
|
+
))().catch((err) => {
|
|
24672
|
+
this.gateways.delete(source);
|
|
24673
|
+
throw err;
|
|
24674
|
+
});
|
|
24675
|
+
this.gateways.set(source, gateway);
|
|
24676
|
+
}
|
|
24677
|
+
return gateway;
|
|
24678
|
+
}
|
|
24679
|
+
hyperbridge() {
|
|
24680
|
+
this.hyperbridgeChain ?? (this.hyperbridgeChain = withTimeout(import_sdk17.SubstrateChain.connect({
|
|
24681
|
+
wsUrl: this.deps.configService.getHyperbridgeWsUrl() ?? NEXUS_WS_URL,
|
|
24682
|
+
consensusStateId: NEXUS_CONSENSUS_STATE,
|
|
24683
|
+
hasher: "Keccak",
|
|
24684
|
+
stateMachineId: NEXUS_STATE_MACHINE
|
|
24685
|
+
}), "Hyperbridge").catch((err) => {
|
|
24686
|
+
this.hyperbridgeChain = void 0;
|
|
24687
|
+
throw err;
|
|
24688
|
+
}));
|
|
24689
|
+
return this.hyperbridgeChain;
|
|
24690
|
+
}
|
|
24691
|
+
/** Tracks requests from BSC into Hyperbridge, the route the bought BRIDGE takes. */
|
|
24692
|
+
ismpClient(hyperbridge, rpcUrl) {
|
|
24693
|
+
return new import_sdk17.IsmpClient({
|
|
24694
|
+
queryClient: (0, import_sdk17.createQueryClient)({ url: this.deps.indexerUrl }),
|
|
24695
|
+
source: this.evmChain(BRIDGE_TOP_UP_CHAIN, rpcUrl),
|
|
24696
|
+
dest: hyperbridge,
|
|
24697
|
+
hyperbridge
|
|
24698
|
+
});
|
|
24699
|
+
}
|
|
24700
|
+
/** Tracks the reclaim's cancellation, which runs between the paying chain and BSC. */
|
|
24701
|
+
async cancelClient(source) {
|
|
24702
|
+
return new import_sdk17.IsmpClient({
|
|
24703
|
+
queryClient: (0, import_sdk17.createQueryClient)({ url: this.deps.indexerUrl }),
|
|
24704
|
+
source: this.evmChain(source),
|
|
24705
|
+
dest: this.evmChain(BRIDGE_TOP_UP_CHAIN),
|
|
24706
|
+
hyperbridge: await this.hyperbridge()
|
|
24707
|
+
});
|
|
24708
|
+
}
|
|
24709
|
+
};
|
|
24710
|
+
function publicState(record) {
|
|
24711
|
+
const { order: _order, sessionPrivateKey: _key, ...rest } = record;
|
|
24712
|
+
return { ...rest, pending: isBridgeTopUpPending(record.stage) };
|
|
24713
|
+
}
|
|
24714
|
+
var DELIVERY_STATUSES = /* @__PURE__ */ new Set(["SOURCE", "SOURCE_FINALIZED", "DESTINATION"]);
|
|
24715
|
+
function toMs(timestamp) {
|
|
24716
|
+
const value = Number(timestamp ?? 0);
|
|
24717
|
+
return value > 0 && value < 1e12 ? value * 1e3 : value;
|
|
24718
|
+
}
|
|
24719
|
+
function withTimeout(promise, what, ms = NETWORK_TIMEOUT_MS) {
|
|
24720
|
+
let timer;
|
|
24721
|
+
const timeout = new Promise((_, reject) => {
|
|
24722
|
+
timer = setTimeout(() => reject(new Error(`Timed out reading ${what}`)), ms);
|
|
24723
|
+
});
|
|
24724
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
24725
|
+
}
|
|
24726
|
+
|
|
24727
|
+
// src/services/bundler-preflight.ts
|
|
24728
|
+
var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
24729
|
+
async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
|
|
24730
|
+
const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
|
|
24731
|
+
const logger = moduleLogger(configService.loggers, "bundler-preflight");
|
|
24732
|
+
const failures = await Promise.all(
|
|
24733
|
+
chains.map(async ({ chainId, bundlerUrl }) => {
|
|
24734
|
+
if (options.watchOnly?.[chainId] === true) return void 0;
|
|
24735
|
+
if (!bundlerUrl?.trim()) {
|
|
24736
|
+
return `Hyperbridge runs no bundler for ${chainName(chainId)}, so simplex cannot fill there; set it watch-only`;
|
|
24737
|
+
}
|
|
24738
|
+
const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
|
|
24739
|
+
if (!entryPoint) return void 0;
|
|
24740
|
+
return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
|
|
24741
|
+
})
|
|
24742
|
+
);
|
|
24743
|
+
const messages = failures.filter((failure) => failure !== void 0);
|
|
24744
|
+
if (messages.length > 0) throw new Error(messages.join("; "));
|
|
24745
|
+
}
|
|
24746
|
+
function servesEntryPoint(listed, entryPoint) {
|
|
24747
|
+
return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
|
|
24748
|
+
}
|
|
24749
|
+
function describeEntryPoints(listed) {
|
|
24750
|
+
return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
|
|
24751
|
+
}
|
|
24752
|
+
function bundlerHost(url) {
|
|
24753
|
+
try {
|
|
24754
|
+
return new URL(url).host || "an unparseable URL";
|
|
24755
|
+
} catch {
|
|
24756
|
+
return "an unparseable URL";
|
|
24757
|
+
}
|
|
24758
|
+
}
|
|
24759
|
+
function chainName(chainId) {
|
|
24760
|
+
const label = chainByChainId(chainId)?.label;
|
|
24761
|
+
return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
|
|
24762
|
+
}
|
|
24763
|
+
async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
|
|
24764
|
+
const host = bundlerHost(bundlerUrl);
|
|
24765
|
+
const subject = `Bundler ${host} for ${chainName(chainId)}`;
|
|
24766
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
24767
|
+
let listed;
|
|
24768
|
+
try {
|
|
24769
|
+
listed = await supportedEntryPoints(bundlerUrl, signal);
|
|
24770
|
+
} catch (err) {
|
|
24771
|
+
const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
|
|
24772
|
+
logger.warn(
|
|
24773
|
+
{ chain: formatChainKey(chainId), bundler: host },
|
|
24774
|
+
`${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
|
|
24775
|
+
);
|
|
24776
|
+
return void 0;
|
|
24777
|
+
}
|
|
24778
|
+
if (servesEntryPoint(listed, entryPoint)) return void 0;
|
|
24779
|
+
return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
|
|
24780
|
+
}
|
|
24781
|
+
async function supportedEntryPoints(bundlerUrl, signal) {
|
|
24782
|
+
const response = await fetch(bundlerUrl, {
|
|
24783
|
+
method: "POST",
|
|
24784
|
+
headers: { "Content-Type": "application/json" },
|
|
24785
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
|
|
24786
|
+
signal
|
|
24787
|
+
});
|
|
24788
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
24789
|
+
const json = await response.json();
|
|
24790
|
+
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
24791
|
+
const { result } = json;
|
|
24792
|
+
if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
|
|
24793
|
+
throw new Error("the result is not a list of addresses");
|
|
24794
|
+
}
|
|
24795
|
+
return result;
|
|
24796
|
+
}
|
|
24797
|
+
function errorReason(err) {
|
|
24798
|
+
if (!(err instanceof Error)) return String(err);
|
|
24799
|
+
const { cause } = err;
|
|
24800
|
+
return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
|
|
24801
|
+
}
|
|
24802
|
+
|
|
24803
|
+
// src/orderbook/lifecycle.ts
|
|
24804
|
+
var EXPIRY_SWEEP_MS = 3e4;
|
|
24805
|
+
var LimitOrderLifecycle = class {
|
|
24806
|
+
constructor(service, options, loggers = defaultLoggerContext()) {
|
|
24807
|
+
this.service = service;
|
|
24808
|
+
this.options = options;
|
|
24809
|
+
this.timers = [];
|
|
24810
|
+
this.logger = loggers.get("limit-orders");
|
|
24811
|
+
}
|
|
24812
|
+
/**
|
|
24813
|
+
* Starts the clocks and kicks off the first reconciliation.
|
|
24814
|
+
*
|
|
24815
|
+
* That first pass is not awaited. It is a round trip to the orderbook for
|
|
24816
|
+
* every page of the solver's orders, and boot has no reason to wait on it.
|
|
24817
|
+
*/
|
|
24818
|
+
async start() {
|
|
24819
|
+
if (this.timers.length > 0) return;
|
|
24820
|
+
const heartbeatMs = await this.service.heartbeatIntervalMs().catch((err) => {
|
|
24821
|
+
this.logger.warn({ err }, "Could not read the orderbook's heartbeat interval; using the fallback");
|
|
24822
|
+
return FALLBACK_HEARTBEAT_INTERVAL_MS;
|
|
24823
|
+
});
|
|
24824
|
+
const reconcileMs = this.options.reconcileIntervalSecs * 1e3;
|
|
24825
|
+
this.every(heartbeatMs, "heartbeat", () => this.service.heartbeat());
|
|
24826
|
+
this.every(EXPIRY_SWEEP_MS, "expiry", () => this.expire());
|
|
24827
|
+
this.every(reconcileMs, "reconciliation", () => this.upkeep());
|
|
24828
|
+
this.logger.info({ heartbeatMs, expiryMs: EXPIRY_SWEEP_MS, reconcileMs }, "Orderbook lifecycle started");
|
|
24829
|
+
void this.upkeep();
|
|
24830
|
+
}
|
|
24831
|
+
stop() {
|
|
24832
|
+
this.timers.forEach((timer) => clearInterval(timer));
|
|
24833
|
+
this.timers = [];
|
|
24834
|
+
}
|
|
24835
|
+
/** Takes orders that have outlived their TTL off the book. */
|
|
24836
|
+
async expire() {
|
|
24837
|
+
const expired = await this.service.expireStale();
|
|
23951
24838
|
if (expired > 0) this.logger.info({ expired }, "Withdrew limit orders that had outlived their expiry");
|
|
23952
24839
|
}
|
|
23953
24840
|
/**
|
|
@@ -23993,7 +24880,30 @@ var LimitOrderLifecycle = class {
|
|
|
23993
24880
|
|
|
23994
24881
|
// src/services/BalanceProvider.ts
|
|
23995
24882
|
var import_node_events = require("node:events");
|
|
23996
|
-
var
|
|
24883
|
+
var import_viem33 = require("viem");
|
|
24884
|
+
|
|
24885
|
+
// src/services/native-setup.ts
|
|
24886
|
+
var import_viem32 = require("viem");
|
|
24887
|
+
async function nativeSetupNeed(client, configService, wallet, chain, nativeBalance) {
|
|
24888
|
+
const paymaster = configService.getSimplexPaymasterAddress(chain);
|
|
24889
|
+
if (!paymaster) return null;
|
|
24890
|
+
const gasPrice = await client.getGasPrice();
|
|
24891
|
+
if (nativeBalance >= (DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice) return null;
|
|
24892
|
+
const pending = await resolvePendingPermit2Approval(client, wallet, paymaster, chain, configService);
|
|
24893
|
+
if (!pending || pending.permitCapable) return null;
|
|
24894
|
+
const solverAccount = configService.getSolverAccountContractAddress(chain);
|
|
24895
|
+
const code = await client.getCode({ address: wallet });
|
|
24896
|
+
const delegated = !!solverAccount && code?.toLowerCase() === (0, import_viem32.concat)(["0xef0100", solverAccount]).toLowerCase();
|
|
24897
|
+
const cost = (delegated ? APPROVE_TX_GAS : DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice;
|
|
24898
|
+
if (nativeBalance >= cost) return null;
|
|
24899
|
+
const usdc = configService.getUsdcAsset(chain);
|
|
24900
|
+
return {
|
|
24901
|
+
required: Number((0, import_viem32.formatEther)(cost * 2n)),
|
|
24902
|
+
feeToken: pending.token.toLowerCase() === usdc.toLowerCase() ? "USDC" : "USDT"
|
|
24903
|
+
};
|
|
24904
|
+
}
|
|
24905
|
+
|
|
24906
|
+
// src/services/BalanceProvider.ts
|
|
23997
24907
|
var BalanceProvider = class extends import_node_events.EventEmitter {
|
|
23998
24908
|
constructor(options) {
|
|
23999
24909
|
super();
|
|
@@ -24041,8 +24951,14 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
|
|
|
24041
24951
|
})
|
|
24042
24952
|
);
|
|
24043
24953
|
}
|
|
24954
|
+
let tokens = {};
|
|
24955
|
+
try {
|
|
24956
|
+
tokens = await this.options.tokens();
|
|
24957
|
+
} catch (err) {
|
|
24958
|
+
this.logger.warn({ err }, "Could not read the orderbook's books; reading USDC and USDT balances only");
|
|
24959
|
+
}
|
|
24044
24960
|
const fxExoticByChain = /* @__PURE__ */ new Map();
|
|
24045
|
-
for (const [chainKey, addrs] of Object.entries(
|
|
24961
|
+
for (const [chainKey, addrs] of Object.entries(tokens)) {
|
|
24046
24962
|
const id = Number.parseInt(chainKey.replace("EVM-", ""), 10);
|
|
24047
24963
|
if (!isNaN(id)) fxExoticByChain.set(id, addrs);
|
|
24048
24964
|
}
|
|
@@ -24086,7 +25002,14 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
|
|
|
24086
25002
|
try {
|
|
24087
25003
|
const native = await client.getBalance({ address: fillerAddr });
|
|
24088
25004
|
const symbol = nativeTokenSymbol(chainId);
|
|
24089
|
-
row.native = { symbol, amount: Number.parseFloat((0,
|
|
25005
|
+
row.native = { symbol, amount: Number.parseFloat((0, import_viem33.formatUnits)(native, 18)) };
|
|
25006
|
+
const setup = await nativeSetupNeed(client, this.options.configService, fillerAddr, chain, native).catch(
|
|
25007
|
+
(err) => {
|
|
25008
|
+
this.logger.debug({ err, chainId }, "Could not check whether the chain needs native gas for setup");
|
|
25009
|
+
return null;
|
|
25010
|
+
}
|
|
25011
|
+
);
|
|
25012
|
+
if (setup) row.nativeSetup = setup;
|
|
24090
25013
|
} catch (err) {
|
|
24091
25014
|
issues.push({ chainId, source: "native", message: errorMessage(err) });
|
|
24092
25015
|
}
|
|
@@ -24157,7 +25080,7 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
|
|
|
24157
25080
|
functionName: "balanceOf",
|
|
24158
25081
|
args: [fillerAddr]
|
|
24159
25082
|
});
|
|
24160
|
-
wallet = Number.parseFloat((0,
|
|
25083
|
+
wallet = Number.parseFloat((0, import_viem33.formatUnits)(balance, definition.decimals));
|
|
24161
25084
|
} catch (err) {
|
|
24162
25085
|
issues.push({
|
|
24163
25086
|
chainId,
|
|
@@ -24185,8 +25108,8 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
|
|
|
24185
25108
|
available: wallet !== null && vaultDataAvailable ? Math.max(wallet - walletReserve, 0) + vaultAvailable : null,
|
|
24186
25109
|
vaults: positions.map((position) => ({
|
|
24187
25110
|
address: position.vault,
|
|
24188
|
-
position: Number.parseFloat((0,
|
|
24189
|
-
available: Number.parseFloat((0,
|
|
25111
|
+
position: Number.parseFloat((0, import_viem33.formatUnits)(position.positionAssets, position.decimals)),
|
|
25112
|
+
available: Number.parseFloat((0, import_viem33.formatUnits)(position.availableAssets, position.decimals)),
|
|
24190
25113
|
acceptsDeposits: position.acceptsDeposits
|
|
24191
25114
|
})),
|
|
24192
25115
|
status: complete ? "fresh" : wallet !== null || positions.length > 0 ? "partial" : "unavailable"
|
|
@@ -24229,8 +25152,8 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
|
|
|
24229
25152
|
const api = await coprocessor.queryApi();
|
|
24230
25153
|
const account = await api.query.system.account(address);
|
|
24231
25154
|
const decimals = api.registry.chainDecimals[0] ?? 12;
|
|
24232
|
-
const free = Number.parseFloat((0,
|
|
24233
|
-
const reserved = Number.parseFloat((0,
|
|
25155
|
+
const free = Number.parseFloat((0, import_viem33.formatUnits)(BigInt(account.data.free.toString()), decimals));
|
|
25156
|
+
const reserved = Number.parseFloat((0, import_viem33.formatUnits)(BigInt(account.data.reserved.toString()), decimals));
|
|
24234
25157
|
this.snapshot = { ...this.snapshot, hyperbridge: { address, free, reserved } };
|
|
24235
25158
|
} catch (err) {
|
|
24236
25159
|
this.logger.warn({ err }, "Failed to fetch Hyperbridge balance");
|
|
@@ -24250,7 +25173,7 @@ function isConfiguredAddress(address) {
|
|
|
24250
25173
|
}
|
|
24251
25174
|
function sumFormatted(positions, field) {
|
|
24252
25175
|
return positions.reduce(
|
|
24253
|
-
(total, position) => total + Number.parseFloat((0,
|
|
25176
|
+
(total, position) => total + Number.parseFloat((0, import_viem33.formatUnits)(position[field], position.decimals)),
|
|
24254
25177
|
0
|
|
24255
25178
|
);
|
|
24256
25179
|
}
|
|
@@ -24494,7 +25417,7 @@ var DEFAULT_INDEXER_URLS = {
|
|
|
24494
25417
|
mainnet: "https://nexus.indexer.polytope.technology/",
|
|
24495
25418
|
testnet: "https://gargantua.indexer.polytope.technology/"
|
|
24496
25419
|
};
|
|
24497
|
-
var
|
|
25420
|
+
var ORDER_QUERY3 = `
|
|
24498
25421
|
query OrderSummary($commitment: String!) {
|
|
24499
25422
|
iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
|
|
24500
25423
|
nodes {
|
|
@@ -24587,7 +25510,7 @@ async function fetchOrder(fetchImpl, indexerUrl, commitment) {
|
|
|
24587
25510
|
const response = await fetchImpl(indexerUrl, {
|
|
24588
25511
|
method: "POST",
|
|
24589
25512
|
headers: { "content-type": "application/json" },
|
|
24590
|
-
body: JSON.stringify({ query:
|
|
25513
|
+
body: JSON.stringify({ query: ORDER_QUERY3, variables: { commitment } }),
|
|
24591
25514
|
signal: AbortSignal.timeout(15e3)
|
|
24592
25515
|
});
|
|
24593
25516
|
if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
|
|
@@ -24693,12 +25616,8 @@ async function bootFiller(config, options) {
|
|
|
24693
25616
|
}
|
|
24694
25617
|
}
|
|
24695
25618
|
};
|
|
24696
|
-
|
|
24697
|
-
|
|
24698
|
-
logger.warn(
|
|
24699
|
-
{ keys: retired },
|
|
24700
|
-
"These [[pairs]] keys are no longer read; prices come from limit orders, and the filler fills nothing until one is posted"
|
|
24701
|
-
);
|
|
25619
|
+
if ("pairs" in config) {
|
|
25620
|
+
logger.warn("[[pairs]] is no longer read and can be deleted from the config");
|
|
24702
25621
|
}
|
|
24703
25622
|
logger.info("Resolving chain IDs from RPC endpoints...");
|
|
24704
25623
|
const resolvedChains = await resolveChainConfigs(config.chains, {
|
|
@@ -24803,25 +25722,6 @@ async function bootFiller(config, options) {
|
|
|
24803
25722
|
}
|
|
24804
25723
|
logger.info("Initializing trading engine...");
|
|
24805
25724
|
const assetRegistry = new AssetRegistry(configService, config.assets);
|
|
24806
|
-
const configuredChainNames = resolvedChains.map((chain) => `EVM-${chain.chainId}`);
|
|
24807
|
-
if (config.pairs?.length) {
|
|
24808
|
-
assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
|
|
24809
|
-
}
|
|
24810
|
-
const adminStrategies = [];
|
|
24811
|
-
const tradingPairs = (config.pairs ?? []).map((pair) => ({
|
|
24812
|
-
token0: pair.token0,
|
|
24813
|
-
token1: pair.token1
|
|
24814
|
-
}));
|
|
24815
|
-
tradingPairs.forEach((pair, pairIndex) => {
|
|
24816
|
-
adminStrategies.push({
|
|
24817
|
-
index: pairIndex,
|
|
24818
|
-
pairIndex,
|
|
24819
|
-
exotic: `${pair.token0}/${pair.token1}`,
|
|
24820
|
-
token0: pair.token0,
|
|
24821
|
-
token1: pair.token1,
|
|
24822
|
-
sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
|
|
24823
|
-
});
|
|
24824
|
-
});
|
|
24825
25725
|
const confirmationPolicy = assertConfirmationCoverage(
|
|
24826
25726
|
config.confirmationPolicies,
|
|
24827
25727
|
resolvedChains.map((c) => c.chainId)
|
|
@@ -24832,7 +25732,6 @@ async function bootFiller(config, options) {
|
|
|
24832
25732
|
configService,
|
|
24833
25733
|
chainClientManager,
|
|
24834
25734
|
contractService,
|
|
24835
|
-
tradingPairs,
|
|
24836
25735
|
assetRegistry,
|
|
24837
25736
|
{ confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
|
|
24838
25737
|
);
|
|
@@ -24940,8 +25839,8 @@ async function bootFiller(config, options) {
|
|
|
24940
25839
|
if (vaultVenue) {
|
|
24941
25840
|
vaultVenue.onTx = ({ chain, kind, txHash, sponsored, movements }) => {
|
|
24942
25841
|
const rows = movements.length > 0 ? movements.map((move) => {
|
|
24943
|
-
const underlying = { token: move.symbol, amount: (0,
|
|
24944
|
-
const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: (0,
|
|
25842
|
+
const underlying = { token: move.symbol, amount: (0, import_viem34.formatUnits)(move.amount, move.decimals) };
|
|
25843
|
+
const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: (0, import_viem34.formatUnits)(move.shares, move.shareDecimals) };
|
|
24945
25844
|
const out = kind === "sweep" ? underlying : shares;
|
|
24946
25845
|
const back = kind === "sweep" ? shares : underlying;
|
|
24947
25846
|
return { token: out.token, amount: out.amount, to: move.vault, tokenIn: back.token, amountIn: back.amount };
|
|
@@ -24951,21 +25850,21 @@ async function bootFiller(config, options) {
|
|
|
24951
25850
|
}
|
|
24952
25851
|
};
|
|
24953
25852
|
}
|
|
24954
|
-
const
|
|
24955
|
-
|
|
24956
|
-
|
|
24957
|
-
|
|
24958
|
-
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
if (!list.includes(address)) list.push(address);
|
|
25853
|
+
const bookTokens = async () => {
|
|
25854
|
+
const { books } = await limitOrderService.books();
|
|
25855
|
+
const symbols = new Set(books.flatMap((book) => [normalizeSymbol(book.base), normalizeSymbol(book.quote)]));
|
|
25856
|
+
const tokens = {};
|
|
25857
|
+
for (const chainId of configService.getConfiguredChainIds()) {
|
|
25858
|
+
const chainName2 = `EVM-${chainId}`;
|
|
25859
|
+
tokens[chainName2] = [...symbols].flatMap((symbol) => assetRegistry.getAddress(symbol, chainName2) ?? []);
|
|
24962
25860
|
}
|
|
24963
|
-
|
|
25861
|
+
return tokens;
|
|
25862
|
+
};
|
|
24964
25863
|
const balanceProvider = new BalanceProvider({
|
|
24965
25864
|
chainClientManager,
|
|
24966
25865
|
configService,
|
|
24967
25866
|
fillerAddress: runtimeSigner.address,
|
|
24968
|
-
|
|
25867
|
+
tokens: bookTokens,
|
|
24969
25868
|
vaultBalances: vaultVenue,
|
|
24970
25869
|
hyperbridge: intentFiller.hyperbridgeConnection,
|
|
24971
25870
|
substratePrivateKey: config.simplex.substratePrivateKey
|
|
@@ -25008,7 +25907,6 @@ async function bootFiller(config, options) {
|
|
|
25008
25907
|
logger.info(
|
|
25009
25908
|
{
|
|
25010
25909
|
chains: resolvedChains.map((c) => c.chainId),
|
|
25011
|
-
pairs: (config.pairs ?? []).map((p) => `${p.token0}/${p.token1}`),
|
|
25012
25910
|
maxConcurrentOrders: config.simplex.maxConcurrentOrders ?? DEFAULT_MAX_CONCURRENT_ORDERS,
|
|
25013
25911
|
watchOnlyChains: watchOnlyChains.length > 0 ? watchOnlyChains : void 0
|
|
25014
25912
|
},
|
|
@@ -25026,12 +25924,32 @@ async function bootFiller(config, options) {
|
|
|
25026
25924
|
activity.detach();
|
|
25027
25925
|
if (options.ownsData) await options.data.close?.();
|
|
25028
25926
|
};
|
|
25927
|
+
const tokenSender = new TokenSender(
|
|
25928
|
+
chainClientManager,
|
|
25929
|
+
runtimeSigner.address,
|
|
25930
|
+
() => config.vault?.vaults ?? [],
|
|
25931
|
+
userOpSender,
|
|
25932
|
+
configService
|
|
25933
|
+
);
|
|
25934
|
+
const bridgeTopUp = config.simplex.substratePrivateKey && network === "mainnet" ? new BridgeTopUp({
|
|
25935
|
+
clientManager: chainClientManager,
|
|
25936
|
+
delegation: new DelegationService(chainClientManager, configService, runtimeSigner),
|
|
25937
|
+
configService,
|
|
25938
|
+
tokenSender,
|
|
25939
|
+
state: options.data.state,
|
|
25940
|
+
storagePath: (0, import_node_path.join)(options.data.dataDir ?? process.cwd(), ".hyperbridge-cache"),
|
|
25941
|
+
indexerUrl,
|
|
25942
|
+
solver: runtimeSigner.address,
|
|
25943
|
+
substratePublicKey: (0, import_viem34.toHex)((await deriveSubstrateKeyPair(config.simplex.substratePrivateKey)).publicKey),
|
|
25944
|
+
hyperbridge: intentFiller.hyperbridgeConnection,
|
|
25945
|
+
loggers: options.loggers
|
|
25946
|
+
}) : void 0;
|
|
25947
|
+
void bridgeTopUp?.restore();
|
|
25029
25948
|
return {
|
|
25030
25949
|
intentFiller,
|
|
25031
25950
|
limitOrders: limitOrderService,
|
|
25032
25951
|
balanceProvider,
|
|
25033
25952
|
vaultVenue,
|
|
25034
|
-
adminStrategies,
|
|
25035
25953
|
haltControls,
|
|
25036
25954
|
activity,
|
|
25037
25955
|
data: options.data,
|
|
@@ -25043,9 +25961,6 @@ async function bootFiller(config, options) {
|
|
|
25043
25961
|
confirmationPolicy,
|
|
25044
25962
|
loggers: options.loggers,
|
|
25045
25963
|
assetRegistry,
|
|
25046
|
-
engine,
|
|
25047
|
-
tradingPairs,
|
|
25048
|
-
balanceTokens: token1,
|
|
25049
25964
|
rebalancingService,
|
|
25050
25965
|
resolvedChains,
|
|
25051
25966
|
fillerAddress: runtimeSigner.address,
|
|
@@ -25055,13 +25970,8 @@ async function bootFiller(config, options) {
|
|
|
25055
25970
|
config,
|
|
25056
25971
|
configPath: options.configPath,
|
|
25057
25972
|
startedAt: Date.now(),
|
|
25058
|
-
tokenSender
|
|
25059
|
-
|
|
25060
|
-
runtimeSigner.address,
|
|
25061
|
-
() => config.vault?.vaults ?? [],
|
|
25062
|
-
userOpSender,
|
|
25063
|
-
configService
|
|
25064
|
-
),
|
|
25973
|
+
tokenSender,
|
|
25974
|
+
bridgeTopUp,
|
|
25065
25975
|
vaultPreflight: async (vaults) => {
|
|
25066
25976
|
const byChain = {};
|
|
25067
25977
|
for (const row of vaults) {
|
|
@@ -25473,11 +26383,11 @@ var MemoryDataStore = class {
|
|
|
25473
26383
|
};
|
|
25474
26384
|
|
|
25475
26385
|
// src/scanner/order-scanner.ts
|
|
25476
|
-
var
|
|
26386
|
+
var import_sdk19 = require("@hyperbridge/sdk");
|
|
25477
26387
|
|
|
25478
26388
|
// src/scanner/chain-scanner.ts
|
|
25479
26389
|
var import_async_mutex2 = require("async-mutex");
|
|
25480
|
-
var
|
|
26390
|
+
var import_sdk18 = require("@hyperbridge/sdk");
|
|
25481
26391
|
var GATEWAY_EVENTS = INTENT_GATEWAY_V2_ABI.filter(
|
|
25482
26392
|
(item) => item.type === "event" && (item.name === "OrderPlaced" || item.name === "OrderFilled" || item.name === "PartialFill")
|
|
25483
26393
|
);
|
|
@@ -25659,7 +26569,7 @@ var ChainScanner = class {
|
|
|
25659
26569
|
return { trace: (message) => this.logger.warn({ chainId: this.target.chainId }, message) };
|
|
25660
26570
|
}
|
|
25661
26571
|
async scan() {
|
|
25662
|
-
const currentBlock = await (0,
|
|
26572
|
+
const currentBlock = await (0, import_sdk18.retryPromise)(() => this.quorumClient.getBlockNumber(), {
|
|
25663
26573
|
maxRetries: 1,
|
|
25664
26574
|
backoffMs: 250,
|
|
25665
26575
|
logMessage: `Failed to get current block number on chain ${this.target.chainId}`,
|
|
@@ -25680,7 +26590,7 @@ var ChainScanner = class {
|
|
|
25680
26590
|
this.enter("logs");
|
|
25681
26591
|
let logs;
|
|
25682
26592
|
try {
|
|
25683
|
-
logs = await (0,
|
|
26593
|
+
logs = await (0, import_sdk18.retryPromise)(
|
|
25684
26594
|
() => this.quorumClient.getLogs({
|
|
25685
26595
|
address: this.target.gateway,
|
|
25686
26596
|
events: GATEWAY_EVENTS,
|
|
@@ -25989,7 +26899,7 @@ var OrderScanner = class _OrderScanner {
|
|
|
25989
26899
|
async addChainNow(chain) {
|
|
25990
26900
|
if (this.closed) throw new Error("This OrderScanner is closed");
|
|
25991
26901
|
const rpcUrls = validateRpcUrls(chain.rpcUrls);
|
|
25992
|
-
const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls
|
|
26902
|
+
const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls }], {
|
|
25993
26903
|
loggers: this.loggers,
|
|
25994
26904
|
tolerateUnreachable: true
|
|
25995
26905
|
}))[0].chainId;
|
|
@@ -25998,7 +26908,7 @@ var OrderScanner = class _OrderScanner {
|
|
|
25998
26908
|
throw new Error(`Chain ${chainId} is already in this scanner`);
|
|
25999
26909
|
}
|
|
26000
26910
|
const name = `EVM-${chainId}`;
|
|
26001
|
-
const gateway = chain.gateway ?? new
|
|
26911
|
+
const gateway = chain.gateway ?? new import_sdk19.ChainConfigService({}).getIntentGatewayAddress(name);
|
|
26002
26912
|
const scanner = new ChainScanner(
|
|
26003
26913
|
{ chain: name, chainId, gateway, rpcUrls },
|
|
26004
26914
|
this.loggers,
|
|
@@ -26074,7 +26984,7 @@ var OrderScanner = class _OrderScanner {
|
|
|
26074
26984
|
};
|
|
26075
26985
|
|
|
26076
26986
|
// src/data/indexer-inventory.ts
|
|
26077
|
-
var
|
|
26987
|
+
var import_viem35 = require("viem");
|
|
26078
26988
|
var READ_TIMEOUT_MS = 1e4;
|
|
26079
26989
|
var SETTLED_AFTER_MS = 10 * 6e4;
|
|
26080
26990
|
var BATCH = 40;
|
|
@@ -26151,7 +27061,7 @@ ${queries.join("\n")}
|
|
|
26151
27061
|
if (!token) continue;
|
|
26152
27062
|
let amount;
|
|
26153
27063
|
try {
|
|
26154
|
-
amount = Number((0,
|
|
27064
|
+
amount = Number((0, import_viem35.formatUnits)(BigInt(row.balance), token.decimals));
|
|
26155
27065
|
} catch {
|
|
26156
27066
|
continue;
|
|
26157
27067
|
}
|
|
@@ -26266,7 +27176,7 @@ var LimitOrderController = class {
|
|
|
26266
27176
|
}));
|
|
26267
27177
|
return this.indexed;
|
|
26268
27178
|
}
|
|
26269
|
-
/** The
|
|
27179
|
+
/** The books the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
26270
27180
|
books() {
|
|
26271
27181
|
return this.service.books();
|
|
26272
27182
|
}
|
|
@@ -26298,85 +27208,6 @@ var LimitOrderController = class {
|
|
|
26298
27208
|
return cancelled;
|
|
26299
27209
|
}
|
|
26300
27210
|
};
|
|
26301
|
-
var PairController = class {
|
|
26302
|
-
constructor(runtime, persist) {
|
|
26303
|
-
this.runtime = runtime;
|
|
26304
|
-
this.persist = persist;
|
|
26305
|
-
}
|
|
26306
|
-
get pairs() {
|
|
26307
|
-
return this.runtime.config.pairs ?? [];
|
|
26308
|
-
}
|
|
26309
|
-
list() {
|
|
26310
|
-
return this.pairs.map((pair, index) => ({
|
|
26311
|
-
index,
|
|
26312
|
-
token0: pair.token0,
|
|
26313
|
-
token1: pair.token1,
|
|
26314
|
-
sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
|
|
26315
|
-
}));
|
|
26316
|
-
}
|
|
26317
|
-
/**
|
|
26318
|
-
* Opens a new market on the running engine.
|
|
26319
|
-
*
|
|
26320
|
-
* Validated against the same rules boot applies, so a pair accepted here is
|
|
26321
|
-
* one a restart would also accept. New `assets` are registered first, because
|
|
26322
|
-
* the pair's symbols have to resolve before the market can be hydrated.
|
|
26323
|
-
*/
|
|
26324
|
-
async add(pair, options) {
|
|
26325
|
-
const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
|
|
26326
|
-
const assets = options?.assets;
|
|
26327
|
-
const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
|
|
26328
|
-
const nextPairs = [...this.pairs, pair];
|
|
26329
|
-
validatePairConfigs(nextPairs, nextAssets);
|
|
26330
|
-
if (assets && Object.keys(assets).length > 0) assetRegistry.addAssets(assets);
|
|
26331
|
-
assertPairSymbolsResolve(
|
|
26332
|
-
[pair],
|
|
26333
|
-
assetRegistry,
|
|
26334
|
-
this.runtime.resolvedChains.map((chain) => formatChainKey(chain.chainId))
|
|
26335
|
-
);
|
|
26336
|
-
const tradingPair = { token0: pair.token0, token1: pair.token1 };
|
|
26337
|
-
engine.addPair(tradingPair);
|
|
26338
|
-
config.pairs = nextPairs;
|
|
26339
|
-
if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
|
|
26340
|
-
if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
|
|
26341
|
-
for (const chain of this.runtime.resolvedChains) {
|
|
26342
|
-
const chainName2 = formatChainKey(chain.chainId);
|
|
26343
|
-
const address = assetRegistry.getAddress(pair.token1, chainName2);
|
|
26344
|
-
if (!address) continue;
|
|
26345
|
-
const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
|
|
26346
|
-
if (!list.includes(address)) list.push(address);
|
|
26347
|
-
}
|
|
26348
|
-
}
|
|
26349
|
-
const pairIndex = nextPairs.length - 1;
|
|
26350
|
-
adminStrategies.push({
|
|
26351
|
-
index: adminStrategies.reduce((max, s) => Math.max(max, s.index), -1) + 1,
|
|
26352
|
-
pairIndex,
|
|
26353
|
-
exotic: `${pair.token0}/${pair.token1}`,
|
|
26354
|
-
token0: pair.token0,
|
|
26355
|
-
token1: pair.token1,
|
|
26356
|
-
sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
|
|
26357
|
-
});
|
|
26358
|
-
await this.persist();
|
|
26359
|
-
return this.list()[pairIndex];
|
|
26360
|
-
}
|
|
26361
|
-
/** Closes a market. Orders already in flight against it still complete. */
|
|
26362
|
-
async remove(index) {
|
|
26363
|
-
const { engine, tradingPairs, adminStrategies, config } = this.runtime;
|
|
26364
|
-
if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
|
|
26365
|
-
engine.removePair(tradingPairs[index]);
|
|
26366
|
-
config.pairs = this.pairs.filter((_, i) => i !== index);
|
|
26367
|
-
const position = adminStrategies.findIndex((s) => s.pairIndex === index);
|
|
26368
|
-
if (position >= 0) adminStrategies.splice(position, 1);
|
|
26369
|
-
for (const strategy of adminStrategies) {
|
|
26370
|
-
if (strategy.pairIndex > index) strategy.pairIndex -= 1;
|
|
26371
|
-
}
|
|
26372
|
-
await this.persist();
|
|
26373
|
-
}
|
|
26374
|
-
livePair(index) {
|
|
26375
|
-
const pair = this.runtime.tradingPairs[index];
|
|
26376
|
-
if (!pair) throw new Error(`Unknown pair ${index}`);
|
|
26377
|
-
return pair;
|
|
26378
|
-
}
|
|
26379
|
-
};
|
|
26380
27211
|
var ChainController = class {
|
|
26381
27212
|
constructor(runtime, persist, scanner, ownsScanner) {
|
|
26382
27213
|
this.runtime = runtime;
|
|
@@ -26421,10 +27252,7 @@ var ChainController = class {
|
|
|
26421
27252
|
async add(chain) {
|
|
26422
27253
|
return this.serialise(async () => {
|
|
26423
27254
|
validateRpcUrls(chain.rpcUrls);
|
|
26424
|
-
|
|
26425
|
-
throw new Error("A bundler URL is required to submit fill UserOperations");
|
|
26426
|
-
}
|
|
26427
|
-
const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }]);
|
|
27255
|
+
const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls }]);
|
|
26428
27256
|
const { chainId } = resolved;
|
|
26429
27257
|
const { configService, config, intentFiller } = this.runtime;
|
|
26430
27258
|
if (configService.getConfiguredChainIds().includes(chainId)) {
|
|
@@ -26453,7 +27281,7 @@ var ChainController = class {
|
|
|
26453
27281
|
if (watchOnly) intentFiller.setWatchOnly(chainId, true);
|
|
26454
27282
|
try {
|
|
26455
27283
|
if (!inScanner) {
|
|
26456
|
-
await this.scanner.addChain({ rpcUrls: chain.rpcUrls,
|
|
27284
|
+
await this.scanner.addChain({ rpcUrls: chain.rpcUrls, chainId });
|
|
26457
27285
|
}
|
|
26458
27286
|
this.runtime.chainClientManager.invalidate(formatChainKey(chainId));
|
|
26459
27287
|
await intentFiller.addChain(configService.getChainConfig(formatChainKey(chainId)));
|
|
@@ -26470,7 +27298,7 @@ var ChainController = class {
|
|
|
26470
27298
|
this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
|
|
26471
27299
|
}
|
|
26472
27300
|
this.runtime.resolvedChains.push(resolved);
|
|
26473
|
-
config.chains = [...config.chains, { rpcUrls: chain.rpcUrls
|
|
27301
|
+
config.chains = [...config.chains, { rpcUrls: chain.rpcUrls }];
|
|
26474
27302
|
if (Object.keys(confirmationPolicies).length > 0) config.confirmationPolicies = confirmationPolicies;
|
|
26475
27303
|
this.syncWatchOnlyToConfig();
|
|
26476
27304
|
await this.persist();
|
|
@@ -26515,9 +27343,7 @@ var ChainController = class {
|
|
|
26515
27343
|
validateRpcUrls(rpcUrls);
|
|
26516
27344
|
const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
|
|
26517
27345
|
if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
|
|
26518
|
-
const [probed] = await resolveChainConfigs([
|
|
26519
|
-
{ rpcUrls, bundlerUrl: this.runtime.resolvedChains[index].bundlerUrl ?? "" }
|
|
26520
|
-
]);
|
|
27346
|
+
const [probed] = await resolveChainConfigs([{ rpcUrls }]);
|
|
26521
27347
|
if (probed.chainId !== chainId) {
|
|
26522
27348
|
throw new Error(`Those endpoints answer for chain ${probed.chainId}, not ${chainId}`);
|
|
26523
27349
|
}
|
|
@@ -26536,20 +27362,6 @@ var ChainController = class {
|
|
|
26536
27362
|
await this.persist();
|
|
26537
27363
|
});
|
|
26538
27364
|
}
|
|
26539
|
-
async setBundlerUrl(chainId, bundlerUrl) {
|
|
26540
|
-
return this.serialise(async () => {
|
|
26541
|
-
if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
|
|
26542
|
-
const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
|
|
26543
|
-
if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
|
|
26544
|
-
await assertBundlersServeEntryPoint([{ chainId, bundlerUrl }], this.runtime.configService, {
|
|
26545
|
-
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26546
|
-
});
|
|
26547
|
-
this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
|
|
26548
|
-
this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
|
|
26549
|
-
this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
|
|
26550
|
-
await this.persist();
|
|
26551
|
-
});
|
|
26552
|
-
}
|
|
26553
27365
|
/** Monitor without filling. Takes effect on the next order. */
|
|
26554
27366
|
async setWatchOnly(chainId, watchOnly) {
|
|
26555
27367
|
return this.serialise(async () => {
|
|
@@ -26701,7 +27513,6 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
|
|
|
26701
27513
|
this.stopped = false;
|
|
26702
27514
|
this.logger = runtime.loggers.get("simplex");
|
|
26703
27515
|
const persist = () => this.persistConfig();
|
|
26704
|
-
this.pairs = new PairController(runtime, persist);
|
|
26705
27516
|
this.chains = new ChainController(runtime, persist, runtime.orderScanner, ownedScanners.orders !== void 0);
|
|
26706
27517
|
this.vaults = new VaultController(runtime, persist);
|
|
26707
27518
|
this.assets = new AssetController(runtime, persist);
|
|
@@ -26851,8 +27662,7 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
|
|
|
26851
27662
|
startedAt: this.runtime.startedAt,
|
|
26852
27663
|
uptimeSec: (Date.now() - this.runtime.startedAt) / 1e3,
|
|
26853
27664
|
chains: this.runtime.resolvedChains.map((chain) => chain.chainId),
|
|
26854
|
-
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26855
|
-
pairs: this.runtime.config.pairs?.length ?? 0
|
|
27665
|
+
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26856
27666
|
};
|
|
26857
27667
|
}
|
|
26858
27668
|
/** Order-activity history, newest first; pass `beforeId` to page backwards. */
|
|
@@ -26930,7 +27740,6 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
|
|
|
26930
27740
|
LoggerContext,
|
|
26931
27741
|
MemoryDataStore,
|
|
26932
27742
|
OrderScanner,
|
|
26933
|
-
PairController,
|
|
26934
27743
|
RebalanceController,
|
|
26935
27744
|
SignerType,
|
|
26936
27745
|
Simplex,
|
|
@@ -26949,14 +27758,12 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
|
|
|
26949
27758
|
mpcVaultSigner,
|
|
26950
27759
|
normalizeSymbol,
|
|
26951
27760
|
parseChainKey,
|
|
26952
|
-
pickAnchorStable,
|
|
26953
27761
|
privateKeySigner,
|
|
26954
27762
|
registrySymbols,
|
|
26955
27763
|
secretPhraseSigner,
|
|
26956
27764
|
turnkeySigner,
|
|
26957
27765
|
validateAssetDefinitions,
|
|
26958
27766
|
validateConfig,
|
|
26959
|
-
validatePairConfigs,
|
|
26960
27767
|
validateSecretPhrase,
|
|
26961
27768
|
validateSignerConfig,
|
|
26962
27769
|
validateVaultToml,
|