@hyperbridge/simplex 0.16.12 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/simplex.js +1728 -409
- package/dist/bin/simplex.js.map +1 -1
- package/dist/index.cjs +1086 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -23
- package/dist/index.d.ts +224 -23
- package/dist/index.js +1080 -134
- package/dist/index.js.map +1 -1
- package/dist/sqlite.cjs +67 -4
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +21 -2
- package/dist/sqlite.d.ts +21 -2
- package/dist/sqlite.js +66 -4
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-B5zkFhxR.d.cts → types-BPyUX8qu.d.cts} +64 -1
- package/dist/{types-B5zkFhxR.d.ts → types-BPyUX8qu.d.ts} +64 -1
- package/dist/ui/assets/Novatica-Medium-CjNgGPEL.ttf +0 -0
- package/dist/ui/assets/index-CK8E6aPx.css +1 -0
- package/dist/ui/assets/index-D8DLeEH0.js +106 -0
- package/dist/ui/index.html +2 -2
- package/package.json +5 -5
- package/dist/ui/assets/index-QU1Ik7IX.js +0 -106
- package/dist/ui/assets/index-XKHMOpY2.css +0 -1
package/dist/index.js
CHANGED
|
@@ -574,7 +574,8 @@ var BUILTIN_ASSETS = {
|
|
|
574
574
|
ZARP: { resolve: (r, chain) => r.getAssetBySymbol(chain, "ZARP") },
|
|
575
575
|
EURC: { resolve: (r, chain) => r.getAssetBySymbol(chain, "EURC") },
|
|
576
576
|
XSGD: { resolve: (r, chain) => r.getAssetBySymbol(chain, "XSGD") },
|
|
577
|
-
TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") }
|
|
577
|
+
TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") },
|
|
578
|
+
BRIDGE: { resolve: (r, chain) => r.getAssetBySymbol(chain, "BRIDGE") }
|
|
578
579
|
};
|
|
579
580
|
var USD_STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "DAI"]);
|
|
580
581
|
function normalizeSymbol(symbol) {
|
|
@@ -712,7 +713,7 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
|
|
|
712
713
|
const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
|
|
713
714
|
for (const pair of pairs) {
|
|
714
715
|
for (const symbol of [pair.token0, pair.token1]) {
|
|
715
|
-
const resolvesSomewhere = chainNames.some((
|
|
716
|
+
const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
|
|
716
717
|
if (!resolvesSomewhere) {
|
|
717
718
|
throw new Error(
|
|
718
719
|
`pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
|
|
@@ -720,15 +721,15 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
|
|
|
720
721
|
}
|
|
721
722
|
}
|
|
722
723
|
}
|
|
723
|
-
for (const
|
|
724
|
+
for (const chainName2 of chainNames) {
|
|
724
725
|
const addressOwner = /* @__PURE__ */ new Map();
|
|
725
726
|
for (const symbol of pairSymbols) {
|
|
726
|
-
const address = registry.getAddress(symbol,
|
|
727
|
+
const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
|
|
727
728
|
if (!address) continue;
|
|
728
729
|
const owner = addressOwner.get(address);
|
|
729
730
|
if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
|
|
730
731
|
throw new Error(
|
|
731
|
-
`assets: '${symbol}' and '${owner}' both resolve to ${address} on ${
|
|
732
|
+
`assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
|
|
732
733
|
);
|
|
733
734
|
}
|
|
734
735
|
addressOwner.set(address, symbol);
|
|
@@ -1433,7 +1434,7 @@ var FillerConfigService = class {
|
|
|
1433
1434
|
return this.fillerConfig?.substratePrivateKey;
|
|
1434
1435
|
}
|
|
1435
1436
|
getEntryPointAddress(chain) {
|
|
1436
|
-
return this.chainConfigService.
|
|
1437
|
+
return this.chainConfigService.getEntryPointAddress(chain);
|
|
1437
1438
|
}
|
|
1438
1439
|
getSolverAccountContractAddress(chain) {
|
|
1439
1440
|
return this.chainConfigService.getSolverAccountAddress(chain);
|
|
@@ -2482,6 +2483,13 @@ var FREE_RPC_URLS = {
|
|
|
2482
2483
|
"https://arb1.arbitrum.io/rpc"
|
|
2483
2484
|
]
|
|
2484
2485
|
};
|
|
2486
|
+
var HYPERBRIDGE_BUNDLER_URLS = {
|
|
2487
|
+
1: "https://bundler.polytope.technology/ethereum",
|
|
2488
|
+
56: "https://bundler.polytope.technology/bsc",
|
|
2489
|
+
137: "https://bundler.polytope.technology/polygon",
|
|
2490
|
+
8453: "https://bundler.polytope.technology/base",
|
|
2491
|
+
42161: "https://bundler.polytope.technology/arbitrum"
|
|
2492
|
+
};
|
|
2485
2493
|
var CHAIN_NATIVE_SYMBOLS = {
|
|
2486
2494
|
1: "ETH",
|
|
2487
2495
|
56: "BNB",
|
|
@@ -2510,7 +2518,8 @@ var INIT_CHAINS = [
|
|
|
2510
2518
|
network: "mainnet",
|
|
2511
2519
|
alchemySubdomain: "eth-mainnet",
|
|
2512
2520
|
explorerUrl: external_links_default.chainExplorers.ethereum,
|
|
2513
|
-
defaultRpcUrls: FREE_RPC_URLS[1]
|
|
2521
|
+
defaultRpcUrls: FREE_RPC_URLS[1],
|
|
2522
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[1]
|
|
2514
2523
|
},
|
|
2515
2524
|
{
|
|
2516
2525
|
chainId: 42161,
|
|
@@ -2519,7 +2528,8 @@ var INIT_CHAINS = [
|
|
|
2519
2528
|
network: "mainnet",
|
|
2520
2529
|
alchemySubdomain: "arb-mainnet",
|
|
2521
2530
|
explorerUrl: external_links_default.chainExplorers.arbitrum,
|
|
2522
|
-
defaultRpcUrls: FREE_RPC_URLS[42161]
|
|
2531
|
+
defaultRpcUrls: FREE_RPC_URLS[42161],
|
|
2532
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[42161]
|
|
2523
2533
|
},
|
|
2524
2534
|
{
|
|
2525
2535
|
chainId: 8453,
|
|
@@ -2528,7 +2538,8 @@ var INIT_CHAINS = [
|
|
|
2528
2538
|
network: "mainnet",
|
|
2529
2539
|
alchemySubdomain: "base-mainnet",
|
|
2530
2540
|
explorerUrl: external_links_default.chainExplorers.base,
|
|
2531
|
-
defaultRpcUrls: FREE_RPC_URLS[8453]
|
|
2541
|
+
defaultRpcUrls: FREE_RPC_URLS[8453],
|
|
2542
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[8453]
|
|
2532
2543
|
},
|
|
2533
2544
|
{
|
|
2534
2545
|
chainId: 137,
|
|
@@ -2537,7 +2548,8 @@ var INIT_CHAINS = [
|
|
|
2537
2548
|
network: "mainnet",
|
|
2538
2549
|
alchemySubdomain: "polygon-mainnet",
|
|
2539
2550
|
explorerUrl: external_links_default.chainExplorers.polygon,
|
|
2540
|
-
defaultRpcUrls: FREE_RPC_URLS[137]
|
|
2551
|
+
defaultRpcUrls: FREE_RPC_URLS[137],
|
|
2552
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[137]
|
|
2541
2553
|
},
|
|
2542
2554
|
{
|
|
2543
2555
|
chainId: 56,
|
|
@@ -2546,7 +2558,8 @@ var INIT_CHAINS = [
|
|
|
2546
2558
|
network: "mainnet",
|
|
2547
2559
|
alchemySubdomain: "bnb-mainnet",
|
|
2548
2560
|
explorerUrl: external_links_default.chainExplorers.bnb,
|
|
2549
|
-
defaultRpcUrls: FREE_RPC_URLS[56]
|
|
2561
|
+
defaultRpcUrls: FREE_RPC_URLS[56],
|
|
2562
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[56]
|
|
2550
2563
|
},
|
|
2551
2564
|
{
|
|
2552
2565
|
chainId: 11155111,
|
|
@@ -2615,6 +2628,35 @@ var LIMITS_QUERY = `
|
|
|
2615
2628
|
chains { id name tokens { symbol decimals } }
|
|
2616
2629
|
}
|
|
2617
2630
|
`;
|
|
2631
|
+
var LEVEL_FIELDS = "fillChain priceBucket price worstPrice baseSize quoteSize orderCount solverCount";
|
|
2632
|
+
var SNAPSHOT_QUERY = `
|
|
2633
|
+
query OrderbookSnapshot($id: ID!, $sourceChain: String, $fillChain: String) {
|
|
2634
|
+
book(id: $id) {
|
|
2635
|
+
id base quote
|
|
2636
|
+
bids: levels(side: BID, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2637
|
+
asks: levels(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2638
|
+
bidLiquidity: availableLiquidity(side: BID, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2639
|
+
askLiquidity: availableLiquidity(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2640
|
+
}
|
|
2641
|
+
serverInfo { priceGranularities { book granularity } }
|
|
2642
|
+
}
|
|
2643
|
+
`;
|
|
2644
|
+
var LEVEL_ORDERS_QUERY = `
|
|
2645
|
+
query OrderbookLevelOrders($id: ID!, $side: Side!, $fillChain: String, $sourceChain: String, $after: String) {
|
|
2646
|
+
book(id: $id) {
|
|
2647
|
+
orders(side: $side, fillChain: $fillChain, sourceChain: $sourceChain, first: 50, after: $after) {
|
|
2648
|
+
edges {
|
|
2649
|
+
node {
|
|
2650
|
+
solver { address }
|
|
2651
|
+
commitment fillChain price priceBucket advertisedSize quotedAmount resized expiresAt acceptedSources
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
pageInfo { hasNextPage endCursor }
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
`;
|
|
2659
|
+
var LEVEL_ORDER_PAGES = 20;
|
|
2618
2660
|
var POSTED_ORDER_FIELDS = "commitment side status price quotedAmount advertisedSize expiresAt acceptedSources";
|
|
2619
2661
|
var SUBMIT_ORDER_MUTATION = `
|
|
2620
2662
|
mutation SubmitOrder($userOp: Bytes!) {
|
|
@@ -2679,6 +2721,44 @@ var OrderbookClient = class {
|
|
|
2679
2721
|
async limits() {
|
|
2680
2722
|
return this.request(LIMITS_QUERY, {});
|
|
2681
2723
|
}
|
|
2724
|
+
/** The public book across all solvers, restricted to the selected route. */
|
|
2725
|
+
async snapshot(id, filters = {}) {
|
|
2726
|
+
const { book, serverInfo } = await this.request(SNAPSHOT_QUERY, {
|
|
2727
|
+
id,
|
|
2728
|
+
sourceChain: filters.sourceChain || null,
|
|
2729
|
+
fillChain: filters.fillChain || null
|
|
2730
|
+
});
|
|
2731
|
+
if (!book) return null;
|
|
2732
|
+
const granularity = serverInfo.priceGranularities.find((entry) => entry.book === book.id)?.granularity ?? null;
|
|
2733
|
+
return { ...book, granularity };
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* The orders behind one price level. The orderbook filters orders by side and route but not by
|
|
2737
|
+
* price bucket, so this walks the side's orders on the level's fill chain and keeps the bucket.
|
|
2738
|
+
* Null when the book does not exist.
|
|
2739
|
+
*/
|
|
2740
|
+
async levelOrders(id, level) {
|
|
2741
|
+
const found = [];
|
|
2742
|
+
let after = null;
|
|
2743
|
+
for (let page = 0; page < LEVEL_ORDER_PAGES; page++) {
|
|
2744
|
+
const { book } = await this.request(LEVEL_ORDERS_QUERY, {
|
|
2745
|
+
id,
|
|
2746
|
+
side: level.side,
|
|
2747
|
+
fillChain: level.fillChain,
|
|
2748
|
+
sourceChain: level.sourceChain || null,
|
|
2749
|
+
after
|
|
2750
|
+
});
|
|
2751
|
+
if (!book) return null;
|
|
2752
|
+
for (const { node } of book.orders.edges) {
|
|
2753
|
+
if (node.priceBucket !== level.priceBucket) continue;
|
|
2754
|
+
const { solver, priceBucket: _bucket, ...order } = node;
|
|
2755
|
+
found.push({ ...order, solver: solver.address });
|
|
2756
|
+
}
|
|
2757
|
+
if (!book.orders.pageInfo.hasNextPage || !book.orders.pageInfo.endCursor) break;
|
|
2758
|
+
after = book.orders.pageInfo.endCursor;
|
|
2759
|
+
}
|
|
2760
|
+
return found;
|
|
2761
|
+
}
|
|
2682
2762
|
async submitOrder(userOp) {
|
|
2683
2763
|
const { submitOrder } = await this.request(SUBMIT_ORDER_MUTATION, { userOp });
|
|
2684
2764
|
switch (submitOrder.__typename) {
|
|
@@ -2792,7 +2872,7 @@ function describe(err) {
|
|
|
2792
2872
|
|
|
2793
2873
|
// src/orderbook/limit-orders.ts
|
|
2794
2874
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2795
|
-
import { getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
|
|
2875
|
+
import { ENTRY_POINT_V08, getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
|
|
2796
2876
|
|
|
2797
2877
|
// src/orderbook/amounts.ts
|
|
2798
2878
|
import { keccak256, stringToBytes } from "viem";
|
|
@@ -2863,16 +2943,16 @@ function signedAmounts(params) {
|
|
|
2863
2943
|
function offerFor(params) {
|
|
2864
2944
|
const { side, inputAmount, price, outputDecimals } = params;
|
|
2865
2945
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2866
|
-
const
|
|
2946
|
+
const scaled2 = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE : inputAmount * ORDERBOOK_SCALE / price;
|
|
2867
2947
|
const unit = outputDecimals >= 18 ? 1n : 10n ** BigInt(18 - outputDecimals);
|
|
2868
|
-
return
|
|
2948
|
+
return scaled2 / unit * unit;
|
|
2869
2949
|
}
|
|
2870
2950
|
function inputFor(params) {
|
|
2871
2951
|
const { side, outputAmount, price, inputDecimals } = params;
|
|
2872
2952
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2873
|
-
const
|
|
2953
|
+
const scaled2 = side === "BID" ? divCeil(outputAmount * ORDERBOOK_SCALE, price) : divCeil(outputAmount * price, ORDERBOOK_SCALE);
|
|
2874
2954
|
const unit = inputDecimals >= 18 ? 1n : 10n ** BigInt(18 - inputDecimals);
|
|
2875
|
-
return divCeil(
|
|
2955
|
+
return divCeil(scaled2, unit) * unit;
|
|
2876
2956
|
}
|
|
2877
2957
|
|
|
2878
2958
|
// src/orderbook/matching.ts
|
|
@@ -2993,6 +3073,8 @@ var LimitOrderService = class {
|
|
|
2993
3073
|
this.delegationService = delegationService;
|
|
2994
3074
|
this.startingNonce = startingNonce;
|
|
2995
3075
|
this.vaultBalances = vaultBalances;
|
|
3076
|
+
/** Re-signs in flight, by order id. A fill's resize waits one out rather than reposting alongside it. */
|
|
3077
|
+
this.resigning = /* @__PURE__ */ new Map();
|
|
2996
3078
|
this.logger = loggers.get("limit-orders");
|
|
2997
3079
|
}
|
|
2998
3080
|
/**
|
|
@@ -3086,6 +3168,7 @@ var LimitOrderService = class {
|
|
|
3086
3168
|
bookExpiresAt: null,
|
|
3087
3169
|
bookPrice: null,
|
|
3088
3170
|
orderNonce: order.orderNonce,
|
|
3171
|
+
entryPoint: null,
|
|
3089
3172
|
status: "cancelled",
|
|
3090
3173
|
lastError: null
|
|
3091
3174
|
}),
|
|
@@ -3128,6 +3211,14 @@ var LimitOrderService = class {
|
|
|
3128
3211
|
throw err;
|
|
3129
3212
|
}
|
|
3130
3213
|
}
|
|
3214
|
+
/** Public market levels read through the configured orderbook client. */
|
|
3215
|
+
orderbookSnapshot(book, filters = {}) {
|
|
3216
|
+
return this.client.snapshot(book, filters);
|
|
3217
|
+
}
|
|
3218
|
+
/** Every solver's order behind one public price level. */
|
|
3219
|
+
orderbookLevelOrders(book, level) {
|
|
3220
|
+
return this.client.levelOrders(book, level);
|
|
3221
|
+
}
|
|
3131
3222
|
/**
|
|
3132
3223
|
* The pairs an order can be written against, the smallest payout each token may carry, and
|
|
3133
3224
|
* the tokens the orderbook registers on each chain.
|
|
@@ -3182,16 +3273,16 @@ var LimitOrderService = class {
|
|
|
3182
3273
|
if (!this.configService.getConfiguredChainIds().includes(getChainId(request.fillChain) ?? -1)) {
|
|
3183
3274
|
throw new LimitOrderValidationError(`'${request.fillChain}' is not a chain this filler is configured for`);
|
|
3184
3275
|
}
|
|
3185
|
-
const
|
|
3276
|
+
const scaled2 = { amountIn: 0n, amountOut: 0n };
|
|
3186
3277
|
for (const name of ["amountIn", "amountOut"]) {
|
|
3187
3278
|
try {
|
|
3188
|
-
|
|
3279
|
+
scaled2[name] = fromHuman(request[name] ?? "");
|
|
3189
3280
|
} catch (err) {
|
|
3190
3281
|
throw new LimitOrderValidationError(
|
|
3191
3282
|
`${name} must be an amount in whole tokens, like "1000" or "1500.25"; ${err.message}`
|
|
3192
3283
|
);
|
|
3193
3284
|
}
|
|
3194
|
-
if (
|
|
3285
|
+
if (scaled2[name] <= 0n) throw new LimitOrderValidationError(`${name} must be greater than zero`);
|
|
3195
3286
|
}
|
|
3196
3287
|
const sources = request.acceptedSources ?? [];
|
|
3197
3288
|
if (sources.length === 0) {
|
|
@@ -3238,7 +3329,7 @@ var LimitOrderService = class {
|
|
|
3238
3329
|
);
|
|
3239
3330
|
}
|
|
3240
3331
|
const floor = limits.serverInfo.minOrderSizes.find((entry) => entry.symbol === request.tokenOut);
|
|
3241
|
-
if (floor &&
|
|
3332
|
+
if (floor && scaled2.amountOut < BigInt(floor.size)) {
|
|
3242
3333
|
throw new LimitOrderValidationError(
|
|
3243
3334
|
`amountOut is below the orderbook's dust floor for ${request.tokenOut} (${toHuman(BigInt(floor.size))})`
|
|
3244
3335
|
);
|
|
@@ -3248,7 +3339,7 @@ var LimitOrderService = class {
|
|
|
3248
3339
|
throw new LimitOrderValidationError(`'${symbol}' does not resolve to a token address on ${request.fillChain}`);
|
|
3249
3340
|
}
|
|
3250
3341
|
}
|
|
3251
|
-
return { amountIn:
|
|
3342
|
+
return { amountIn: scaled2.amountIn, amountOut: scaled2.amountOut };
|
|
3252
3343
|
}
|
|
3253
3344
|
/**
|
|
3254
3345
|
* Refuses an order the wallet cannot pay out.
|
|
@@ -3344,24 +3435,32 @@ var LimitOrderService = class {
|
|
|
3344
3435
|
async resize(remaining, delivered) {
|
|
3345
3436
|
const id = remaining.id;
|
|
3346
3437
|
const floor = await this.dustFloor(remaining);
|
|
3347
|
-
|
|
3438
|
+
for (let resigning = this.resigning.get(id); resigning; resigning = this.resigning.get(id)) await resigning;
|
|
3439
|
+
const order = await this.store.setStatus(id, "resizing", null, POSTABLE);
|
|
3440
|
+
if (!order) return null;
|
|
3441
|
+
if (BigInt(order.remaining) < floor) {
|
|
3348
3442
|
this.logger.info(
|
|
3349
|
-
{ id, remaining:
|
|
3443
|
+
{ id, remaining: order.remaining, floor: floor.toString() },
|
|
3350
3444
|
"Limit order worked down past the dust floor; closing it"
|
|
3351
3445
|
);
|
|
3352
|
-
if (
|
|
3353
|
-
const closed = await this.store.setPosting(
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3446
|
+
if (order.commitment) await this.withdraw(order.commitment);
|
|
3447
|
+
const closed = await this.store.setPosting(
|
|
3448
|
+
id,
|
|
3449
|
+
{
|
|
3450
|
+
commitment: null,
|
|
3451
|
+
bookExpiresAt: null,
|
|
3452
|
+
bookPrice: null,
|
|
3453
|
+
orderNonce: order.orderNonce,
|
|
3454
|
+
entryPoint: null,
|
|
3455
|
+
status: "filled",
|
|
3456
|
+
lastError: null
|
|
3457
|
+
},
|
|
3458
|
+
POSTABLE
|
|
3459
|
+
);
|
|
3361
3460
|
if (closed) this.report({ kind: "filled", order: closed });
|
|
3362
3461
|
return closed;
|
|
3363
3462
|
}
|
|
3364
|
-
const resized = await this.repost(
|
|
3463
|
+
const resized = await this.repost(order);
|
|
3365
3464
|
if (resized) this.report({ kind: "resized", order: resized, delivered });
|
|
3366
3465
|
return resized;
|
|
3367
3466
|
}
|
|
@@ -3374,7 +3473,7 @@ var LimitOrderService = class {
|
|
|
3374
3473
|
*/
|
|
3375
3474
|
async repost(order) {
|
|
3376
3475
|
if (!order) return null;
|
|
3377
|
-
if (isLocal(order)) return this.store.setStatus(order.id, "open");
|
|
3476
|
+
if (isLocal(order)) return this.store.setStatus(order.id, "open", null, POSTABLE);
|
|
3378
3477
|
if (hasExpired(order.expiresAt, /* @__PURE__ */ new Date())) return this.retire(order);
|
|
3379
3478
|
if (order.commitment) {
|
|
3380
3479
|
const withdrawn = await this.withdraw(order.commitment);
|
|
@@ -3385,10 +3484,13 @@ var LimitOrderService = class {
|
|
|
3385
3484
|
{ id: order.id, commitment: order.commitment, err: message },
|
|
3386
3485
|
"Could not clear the old entry; leaving the posting alone rather than adding a second"
|
|
3387
3486
|
);
|
|
3388
|
-
return this.store.setStatus(order.id, "open", message);
|
|
3487
|
+
return this.store.setStatus(order.id, "open", message, POSTABLE);
|
|
3389
3488
|
}
|
|
3390
3489
|
}
|
|
3391
|
-
await this.store.setStatus(order.id, "resizing")
|
|
3490
|
+
if (!await this.store.setStatus(order.id, "resizing", null, POSTABLE)) {
|
|
3491
|
+
this.logger.info({ id: order.id }, "Limit order closed while it was being reposted; leaving it closed");
|
|
3492
|
+
return null;
|
|
3493
|
+
}
|
|
3392
3494
|
const nonce = (BigInt(order.orderNonce) + 1n).toString();
|
|
3393
3495
|
return (await this.post({ ...order, commitment: null, orderNonce: nonce })).order;
|
|
3394
3496
|
}
|
|
@@ -3431,7 +3533,9 @@ var LimitOrderService = class {
|
|
|
3431
3533
|
* They drift apart when a request never got an answer or the process died
|
|
3432
3534
|
* between two of them: an entry the orderbook still lists that nothing here
|
|
3433
3535
|
* owns, an order here whose entry has gone, or an entry the orderbook has cut
|
|
3434
|
-
* down because the solver cannot cover what it quoted.
|
|
3536
|
+
* down because the solver cannot cover what it quoted. A posted op is also
|
|
3537
|
+
* signed over one EntryPoint, so when the fill chain moves to another, every
|
|
3538
|
+
* entry signed before the move is withdrawn and posted again.
|
|
3435
3539
|
*/
|
|
3436
3540
|
async reconcile(now = /* @__PURE__ */ new Date()) {
|
|
3437
3541
|
const [entries, live] = await Promise.all([this.postedOrders(), this.live()]);
|
|
@@ -3458,7 +3562,17 @@ var LimitOrderService = class {
|
|
|
3458
3562
|
}
|
|
3459
3563
|
for (const order of live) {
|
|
3460
3564
|
if (isLocal(order)) continue;
|
|
3461
|
-
if (order.commitment && found.has(order.commitment.toLowerCase()))
|
|
3565
|
+
if (order.commitment && found.has(order.commitment.toLowerCase())) {
|
|
3566
|
+
try {
|
|
3567
|
+
if (await this.resign(order)) report.reposted++;
|
|
3568
|
+
} catch (err) {
|
|
3569
|
+
this.logger.error(
|
|
3570
|
+
{ id: order.id, err },
|
|
3571
|
+
"Could not sign the limit order again for its chain's EntryPoint"
|
|
3572
|
+
);
|
|
3573
|
+
}
|
|
3574
|
+
continue;
|
|
3575
|
+
}
|
|
3462
3576
|
if (sinceMs(order.updatedAt, now) < POST_GRACE_MS) continue;
|
|
3463
3577
|
this.logger.warn({ id: order.id }, "Limit order has no entry on the orderbook; posting it again");
|
|
3464
3578
|
try {
|
|
@@ -3470,6 +3584,45 @@ var LimitOrderService = class {
|
|
|
3470
3584
|
}
|
|
3471
3585
|
return report;
|
|
3472
3586
|
}
|
|
3587
|
+
/**
|
|
3588
|
+
* Withdraws a posting signed for an EntryPoint its fill chain no longer uses,
|
|
3589
|
+
* and posts it again signed for the one it uses now. Answers whether a new
|
|
3590
|
+
* posting landed.
|
|
3591
|
+
*
|
|
3592
|
+
* The row is read again and claimed out of `open` first: a fill can have worked
|
|
3593
|
+
* it down or started its own repost since reconciliation listed it, and that
|
|
3594
|
+
* write is the one to follow. A fill that lands once the claim is taken waits
|
|
3595
|
+
* in `resize` for this to finish. The EntryPoint is only written with a posting
|
|
3596
|
+
* that landed, so a pass cut short anywhere leaves a row the next pass still
|
|
3597
|
+
* reads as stale, or as having no entry, and finishes the job.
|
|
3598
|
+
*/
|
|
3599
|
+
async resign(listed) {
|
|
3600
|
+
const entryPoint = this.configService.getEntryPointAddress(listed.fillChain);
|
|
3601
|
+
if (!entryPoint || sameAddress(signedFor(listed), entryPoint)) return false;
|
|
3602
|
+
const order = await this.store.get(listed.id);
|
|
3603
|
+
if (!order || order.status !== "open" || order.commitment !== listed.commitment) return false;
|
|
3604
|
+
if (sameAddress(signedFor(order), entryPoint) || this.resigning.has(order.id)) return false;
|
|
3605
|
+
const resigned = this.signAgain(order, entryPoint);
|
|
3606
|
+
this.resigning.set(
|
|
3607
|
+
order.id,
|
|
3608
|
+
resigned.catch(() => false)
|
|
3609
|
+
);
|
|
3610
|
+
try {
|
|
3611
|
+
return await resigned;
|
|
3612
|
+
} finally {
|
|
3613
|
+
this.resigning.delete(order.id);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
async signAgain(order, entryPoint) {
|
|
3617
|
+
const claimed = await this.store.setStatus(order.id, "resizing", null, ["open"]);
|
|
3618
|
+
if (!claimed) return false;
|
|
3619
|
+
this.logger.warn(
|
|
3620
|
+
{ id: order.id, commitment: order.commitment, signedFor: signedFor(order), entryPoint },
|
|
3621
|
+
"Limit order was signed for an EntryPoint its chain no longer uses; signing it again"
|
|
3622
|
+
);
|
|
3623
|
+
const posted = await this.repost(claimed);
|
|
3624
|
+
return !!posted?.commitment && sameAddress(signedFor(posted), entryPoint);
|
|
3625
|
+
}
|
|
3473
3626
|
/**
|
|
3474
3627
|
* Lowers every open order's `remaining` to what the solver's account will still
|
|
3475
3628
|
* pay for it, and puts the order back on the book at that size.
|
|
@@ -3551,6 +3704,7 @@ var LimitOrderService = class {
|
|
|
3551
3704
|
bookExpiresAt: null,
|
|
3552
3705
|
bookPrice: null,
|
|
3553
3706
|
orderNonce: order.orderNonce,
|
|
3707
|
+
entryPoint: null,
|
|
3554
3708
|
status: "expired",
|
|
3555
3709
|
lastError: null
|
|
3556
3710
|
});
|
|
@@ -3607,11 +3761,11 @@ var LimitOrderService = class {
|
|
|
3607
3761
|
}
|
|
3608
3762
|
/** Builds and submits the posting, then writes the orderbook's answer onto the row. */
|
|
3609
3763
|
async post(order) {
|
|
3610
|
-
const { result, orderNonce } = await this.submit(order);
|
|
3764
|
+
const { result, orderNonce, entryPoint } = await this.submit(order);
|
|
3611
3765
|
if (result.kind === "accepted" || result.kind === "unchanged") {
|
|
3612
3766
|
const posted = result.order;
|
|
3613
3767
|
this.logger.info(
|
|
3614
|
-
{ id: order.id, commitment: posted.commitment, price: posted.price },
|
|
3768
|
+
{ id: order.id, commitment: posted.commitment, price: posted.price, entryPoint },
|
|
3615
3769
|
"Limit order posted to the orderbook"
|
|
3616
3770
|
);
|
|
3617
3771
|
const stored = await this.store.setPosting(
|
|
@@ -3621,6 +3775,10 @@ var LimitOrderService = class {
|
|
|
3621
3775
|
bookExpiresAt: posted.expiresAt,
|
|
3622
3776
|
bookPrice: posted.price,
|
|
3623
3777
|
orderNonce: orderNonce.toString(),
|
|
3778
|
+
// An entry the orderbook already held was signed by an earlier attempt,
|
|
3779
|
+
// for whichever EntryPoint was current then. Recorded as unknown, which
|
|
3780
|
+
// reconciliation reads as v0.8 and signs again once the chain is past it.
|
|
3781
|
+
entryPoint: result.kind === "accepted" ? entryPoint : null,
|
|
3624
3782
|
status: "open",
|
|
3625
3783
|
lastError: null
|
|
3626
3784
|
},
|
|
@@ -3650,6 +3808,7 @@ var LimitOrderService = class {
|
|
|
3650
3808
|
bookExpiresAt: null,
|
|
3651
3809
|
bookPrice: null,
|
|
3652
3810
|
orderNonce: orderNonce.toString(),
|
|
3811
|
+
entryPoint: null,
|
|
3653
3812
|
status: "open",
|
|
3654
3813
|
lastError: message
|
|
3655
3814
|
}),
|
|
@@ -3672,22 +3831,30 @@ var LimitOrderService = class {
|
|
|
3672
3831
|
* only the second one.
|
|
3673
3832
|
*/
|
|
3674
3833
|
async submit(order) {
|
|
3834
|
+
const entryPoint = this.entryPointFor(order.fillChain);
|
|
3675
3835
|
const orderNonce = BigInt(order.orderNonce);
|
|
3676
|
-
const attempt = await this.buildAndSubmit(order, orderNonce);
|
|
3836
|
+
const attempt = await this.buildAndSubmit(order, orderNonce, entryPoint);
|
|
3677
3837
|
const first = attempt.result;
|
|
3678
3838
|
if (first.kind !== "rejected" || first.code !== "REPLAYED" && first.code !== "ORDER_EXISTS") {
|
|
3679
|
-
return { result: first, orderNonce };
|
|
3839
|
+
return { result: first, orderNonce, entryPoint };
|
|
3680
3840
|
}
|
|
3681
3841
|
if (first.code === "ORDER_EXISTS" && attempt.commitment) {
|
|
3682
3842
|
const live = await this.entryAt(order, attempt.commitment);
|
|
3683
3843
|
if (live) {
|
|
3684
3844
|
this.logger.info({ id: order.id, commitment: live.commitment }, "This posting is already on the orderbook");
|
|
3685
|
-
return { result: { kind: "unchanged", order: live }, orderNonce };
|
|
3845
|
+
return { result: { kind: "unchanged", order: live }, orderNonce, entryPoint };
|
|
3686
3846
|
}
|
|
3687
3847
|
}
|
|
3688
3848
|
this.logger.warn({ id: order.id, code: first.code }, "Orderbook has seen this op before; reposting on a new nonce");
|
|
3689
3849
|
const retried = orderNonce + 1n;
|
|
3690
|
-
|
|
3850
|
+
const { result } = await this.buildAndSubmit(order, retried, entryPoint);
|
|
3851
|
+
return { result, orderNonce: retried, entryPoint };
|
|
3852
|
+
}
|
|
3853
|
+
/** The EntryPoint a posting on `chain` is signed for now. */
|
|
3854
|
+
entryPointFor(chain) {
|
|
3855
|
+
const entryPoint = this.configService.getEntryPointAddress(chain);
|
|
3856
|
+
if (!entryPoint) throw new LimitOrderValidationError(`No EntryPoint is configured for ${chain}`);
|
|
3857
|
+
return entryPoint;
|
|
3691
3858
|
}
|
|
3692
3859
|
/**
|
|
3693
3860
|
* The entry the orderbook holds at `commitment`, if it holds one. A lookup that
|
|
@@ -3703,8 +3870,8 @@ var LimitOrderService = class {
|
|
|
3703
3870
|
}
|
|
3704
3871
|
}
|
|
3705
3872
|
/** The orderbook's answer, and the commitment the op we sent hashes to. */
|
|
3706
|
-
async buildAndSubmit(order, orderNonce) {
|
|
3707
|
-
const { commitment, userOp } = await this.buildUserOp(order, orderNonce);
|
|
3873
|
+
async buildAndSubmit(order, orderNonce, entryPoint) {
|
|
3874
|
+
const { commitment, userOp } = await this.buildUserOp(order, orderNonce, entryPoint);
|
|
3708
3875
|
try {
|
|
3709
3876
|
return { result: await this.client.submitOrder(userOp), commitment };
|
|
3710
3877
|
} catch (err) {
|
|
@@ -3714,7 +3881,7 @@ var LimitOrderService = class {
|
|
|
3714
3881
|
throw err;
|
|
3715
3882
|
}
|
|
3716
3883
|
}
|
|
3717
|
-
async buildUserOp(order, orderNonce) {
|
|
3884
|
+
async buildUserOp(order, orderNonce, entryPointAddress) {
|
|
3718
3885
|
const baseToken = this.assetRegistry.getAddress(order.base, order.fillChain);
|
|
3719
3886
|
const quoteToken = this.assetRegistry.getAddress(order.quote, order.fillChain);
|
|
3720
3887
|
const [baseDecimals, quoteDecimals] = await Promise.all([
|
|
@@ -3729,10 +3896,6 @@ var LimitOrderService = class {
|
|
|
3729
3896
|
baseDecimals,
|
|
3730
3897
|
quoteDecimals
|
|
3731
3898
|
});
|
|
3732
|
-
const entryPointAddress = this.configService.getEntryPointAddress(order.fillChain);
|
|
3733
|
-
if (!entryPointAddress) {
|
|
3734
|
-
throw new LimitOrderValidationError(`No EntryPoint is configured for ${order.fillChain}`);
|
|
3735
|
-
}
|
|
3736
3899
|
return this.contractService.prepareLimitOrderUserOp({
|
|
3737
3900
|
fillChain: order.fillChain,
|
|
3738
3901
|
entryPointAddress,
|
|
@@ -3763,6 +3926,12 @@ function hasExpired(expiresAt, now) {
|
|
|
3763
3926
|
const at = Date.parse(expiresAt);
|
|
3764
3927
|
return !Number.isNaN(at) && at <= now.getTime();
|
|
3765
3928
|
}
|
|
3929
|
+
function signedFor(order) {
|
|
3930
|
+
return order.entryPoint ?? ENTRY_POINT_V08;
|
|
3931
|
+
}
|
|
3932
|
+
function sameAddress(a, b) {
|
|
3933
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
3934
|
+
}
|
|
3766
3935
|
function sinceMs(updatedAt, now) {
|
|
3767
3936
|
const written = Date.parse(`${updatedAt.replace(" ", "T")}Z`);
|
|
3768
3937
|
return Number.isNaN(written) ? Number.POSITIVE_INFINITY : now.getTime() - written;
|
|
@@ -3774,6 +3943,451 @@ function underFunded(entry) {
|
|
|
3774
3943
|
return entry.resized ? `UNDER_FUNDED: the orderbook is advertising ${entry.advertisedSize} of the ${entry.quotedAmount} quoted` : "UNDER_FUNDED: the solver's balance does not cover this posting";
|
|
3775
3944
|
}
|
|
3776
3945
|
|
|
3946
|
+
// src/orderbook/profitability.ts
|
|
3947
|
+
import { Decimal as BaseDecimal } from "decimal.js";
|
|
3948
|
+
|
|
3949
|
+
// src/data/inventory.ts
|
|
3950
|
+
var INVENTORY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3951
|
+
function inventoryOf(snapshot) {
|
|
3952
|
+
if (snapshot.status !== "fresh") return null;
|
|
3953
|
+
const held = {};
|
|
3954
|
+
for (const chain of snapshot.chains) {
|
|
3955
|
+
for (const asset of chain.assets) {
|
|
3956
|
+
if (asset.total === null) return null;
|
|
3957
|
+
held[asset.symbol] = (held[asset.symbol] ?? 0) + asset.total;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
return Object.keys(held).length > 0 ? held : null;
|
|
3961
|
+
}
|
|
3962
|
+
function parseUtc(value) {
|
|
3963
|
+
return Date.parse(`${value.replace(" ", "T")}Z`);
|
|
3964
|
+
}
|
|
3965
|
+
var InventoryRecorder = class {
|
|
3966
|
+
constructor(store, balances, logger, now = Date.now) {
|
|
3967
|
+
this.store = store;
|
|
3968
|
+
this.balances = balances;
|
|
3969
|
+
this.logger = logger;
|
|
3970
|
+
this.now = now;
|
|
3971
|
+
this.recording = false;
|
|
3972
|
+
this.onSnapshot = (snapshot) => void this.consider(snapshot);
|
|
3973
|
+
}
|
|
3974
|
+
start() {
|
|
3975
|
+
this.balances.on("snapshot", this.onSnapshot);
|
|
3976
|
+
void this.consider(this.balances.getSnapshot());
|
|
3977
|
+
}
|
|
3978
|
+
stop() {
|
|
3979
|
+
this.balances.off("snapshot", this.onSnapshot);
|
|
3980
|
+
}
|
|
3981
|
+
/** Takes a snapshot from `snapshot` if one is due and the read is complete. Never rejects. */
|
|
3982
|
+
async consider(snapshot) {
|
|
3983
|
+
if (this.recording) return;
|
|
3984
|
+
const held = inventoryOf(snapshot);
|
|
3985
|
+
if (!held) return;
|
|
3986
|
+
this.recording = true;
|
|
3987
|
+
try {
|
|
3988
|
+
if (this.lastTakenAt === void 0) {
|
|
3989
|
+
const latest = await this.store.latest();
|
|
3990
|
+
this.lastTakenAt = latest ? parseUtc(latest.takenAt) : 0;
|
|
3991
|
+
}
|
|
3992
|
+
if (this.now() - this.lastTakenAt < INVENTORY_INTERVAL_MS) return;
|
|
3993
|
+
await this.store.record(held);
|
|
3994
|
+
this.lastTakenAt = this.now();
|
|
3995
|
+
this.logger.info({ tokens: Object.keys(held).length }, "Recorded an inventory snapshot");
|
|
3996
|
+
} catch (err) {
|
|
3997
|
+
this.logger.warn({ err }, "Could not record an inventory snapshot");
|
|
3998
|
+
} finally {
|
|
3999
|
+
this.recording = false;
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
|
|
4004
|
+
// src/orderbook/usd.ts
|
|
4005
|
+
import { Decimal as Decimal2 } from "decimal.js";
|
|
4006
|
+
function limitOrderUsdEdges(orders) {
|
|
4007
|
+
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
4008
|
+
base: normalizeSymbol(order.base),
|
|
4009
|
+
quote: normalizeSymbol(order.quote),
|
|
4010
|
+
rate: new Decimal2(order.price).div(ORDERBOOK_SCALE.toString())
|
|
4011
|
+
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
4012
|
+
}
|
|
4013
|
+
function usdFactorsFrom(edges) {
|
|
4014
|
+
const factors = /* @__PURE__ */ new Map();
|
|
4015
|
+
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new Decimal2(1));
|
|
4016
|
+
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
4017
|
+
let grew = true;
|
|
4018
|
+
while (grew) {
|
|
4019
|
+
grew = false;
|
|
4020
|
+
for (const edge of ordered) {
|
|
4021
|
+
const usdBase = factors.get(edge.base);
|
|
4022
|
+
const usdQuote = factors.get(edge.quote);
|
|
4023
|
+
if (usdBase && !usdQuote) {
|
|
4024
|
+
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
4025
|
+
grew = true;
|
|
4026
|
+
} else if (usdQuote && !usdBase) {
|
|
4027
|
+
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
4028
|
+
grew = true;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
return factors;
|
|
4033
|
+
}
|
|
4034
|
+
function usdValueOf(factors, symbol, amount) {
|
|
4035
|
+
const factor = factors.get(normalizeSymbol(symbol));
|
|
4036
|
+
return factor ? amount.mul(factor) : null;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
// src/orderbook/profitability.ts
|
|
4040
|
+
var Decimal3 = BaseDecimal.clone({ precision: 40 });
|
|
4041
|
+
var PROFIT_PERIODS = {
|
|
4042
|
+
"7d": { bucket: "day", count: 7 },
|
|
4043
|
+
"30d": { bucket: "day", count: 30 },
|
|
4044
|
+
"12w": { bucket: "week", count: 12 },
|
|
4045
|
+
"12m": { bucket: "month", count: 12 },
|
|
4046
|
+
all: { bucket: "year", count: null }
|
|
4047
|
+
};
|
|
4048
|
+
var MINUTE = 6e4;
|
|
4049
|
+
var DAY = 864e5;
|
|
4050
|
+
var BALANCES_REACH_MS = 7 * DAY;
|
|
4051
|
+
function bucketStart(local, bucket) {
|
|
4052
|
+
const date = new Date(local);
|
|
4053
|
+
const year = date.getUTCFullYear();
|
|
4054
|
+
const month = date.getUTCMonth();
|
|
4055
|
+
if (bucket === "year") return Date.UTC(year, 0, 1);
|
|
4056
|
+
if (bucket === "month") return Date.UTC(year, month, 1);
|
|
4057
|
+
const day = Date.UTC(year, month, date.getUTCDate());
|
|
4058
|
+
return bucket === "day" ? day : day - (date.getUTCDay() + 6) % 7 * DAY;
|
|
4059
|
+
}
|
|
4060
|
+
function shiftBucket(start, bucket, steps) {
|
|
4061
|
+
if (bucket === "day") return start + steps * DAY;
|
|
4062
|
+
if (bucket === "week") return start + steps * 7 * DAY;
|
|
4063
|
+
const date = new Date(start);
|
|
4064
|
+
return bucket === "month" ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + steps, 1) : Date.UTC(date.getUTCFullYear() + steps, 0, 1);
|
|
4065
|
+
}
|
|
4066
|
+
function scaled(value) {
|
|
4067
|
+
return new Decimal3(value).div(ORDERBOOK_SCALE.toString());
|
|
4068
|
+
}
|
|
4069
|
+
function tradeOf(fill, at) {
|
|
4070
|
+
try {
|
|
4071
|
+
const paid = scaled(fill.amount);
|
|
4072
|
+
const taken = fill.amountIn === null ? null : scaled(fill.amountIn);
|
|
4073
|
+
const price = scaled(fill.price);
|
|
4074
|
+
if (!paid.gt(0) || !price.gt(0) || taken !== null && !taken.gt(0)) return null;
|
|
4075
|
+
const buying = fill.side === "BID";
|
|
4076
|
+
const [baseAmount, quoteAmount] = buying ? [taken ?? paid.div(price), paid] : [paid, taken ?? paid.mul(price)];
|
|
4077
|
+
return {
|
|
4078
|
+
at,
|
|
4079
|
+
book: fill.book,
|
|
4080
|
+
base: normalizeSymbol(fill.base),
|
|
4081
|
+
quote: normalizeSymbol(fill.quote),
|
|
4082
|
+
buying,
|
|
4083
|
+
baseAmount,
|
|
4084
|
+
quoteAmount,
|
|
4085
|
+
estimated: taken === null
|
|
4086
|
+
};
|
|
4087
|
+
} catch {
|
|
4088
|
+
return null;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
function add(holdings, symbol, amount) {
|
|
4092
|
+
holdings.set(symbol, (holdings.get(symbol) ?? new Decimal3(0)).add(amount));
|
|
4093
|
+
}
|
|
4094
|
+
function apply(holdings, trade, sign) {
|
|
4095
|
+
const base = trade.buying ? trade.baseAmount : trade.baseAmount.neg();
|
|
4096
|
+
const quote = trade.buying ? trade.quoteAmount.neg() : trade.quoteAmount;
|
|
4097
|
+
add(holdings, trade.base, base.mul(sign));
|
|
4098
|
+
add(holdings, trade.quote, quote.mul(sign));
|
|
4099
|
+
}
|
|
4100
|
+
var Sums = class {
|
|
4101
|
+
constructor() {
|
|
4102
|
+
this.change = /* @__PURE__ */ new Map();
|
|
4103
|
+
this.boughtUsd = new Decimal3(0);
|
|
4104
|
+
this.soldUsd = new Decimal3(0);
|
|
4105
|
+
this.buys = 0;
|
|
4106
|
+
this.sells = 0;
|
|
4107
|
+
}
|
|
4108
|
+
};
|
|
4109
|
+
var BookLedger = class {
|
|
4110
|
+
constructor(id, base, quote) {
|
|
4111
|
+
this.id = id;
|
|
4112
|
+
this.base = base;
|
|
4113
|
+
this.quote = quote;
|
|
4114
|
+
this.bought = new Decimal3(0);
|
|
4115
|
+
this.boughtQuote = new Decimal3(0);
|
|
4116
|
+
this.sold = new Decimal3(0);
|
|
4117
|
+
this.soldQuote = new Decimal3(0);
|
|
4118
|
+
/** The rates of the book's latest buy and latest sell, over all of its history. */
|
|
4119
|
+
this.latestBuy = null;
|
|
4120
|
+
this.latestSell = null;
|
|
4121
|
+
this.inPeriod = false;
|
|
4122
|
+
}
|
|
4123
|
+
/**
|
|
4124
|
+
* The rate the book is valued at. Midway between the latest buy and the latest sell, because
|
|
4125
|
+
* either alone sits half a spread to one side and would price open volume against whichever
|
|
4126
|
+
* way the book happened to trade last.
|
|
4127
|
+
*/
|
|
4128
|
+
rate() {
|
|
4129
|
+
if (this.latestBuy && this.latestSell) return this.latestBuy.add(this.latestSell).div(2);
|
|
4130
|
+
return this.latestBuy ?? this.latestSell;
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
function readingOf(reading) {
|
|
4134
|
+
const held = /* @__PURE__ */ new Map();
|
|
4135
|
+
const cover = /* @__PURE__ */ new Map();
|
|
4136
|
+
for (const [symbol, amount] of Object.entries(reading.balances)) {
|
|
4137
|
+
const chains = reading.chains[symbol];
|
|
4138
|
+
if (!Number.isFinite(amount) || !chains?.length) continue;
|
|
4139
|
+
add(held, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4140
|
+
cover.set(normalizeSymbol(symbol), [...chains].sort().join(","));
|
|
4141
|
+
}
|
|
4142
|
+
return { held, cover };
|
|
4143
|
+
}
|
|
4144
|
+
function movedBetween(earlier, later) {
|
|
4145
|
+
if (!earlier || !later) return null;
|
|
4146
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4147
|
+
for (const [symbol, cover] of later.cover) {
|
|
4148
|
+
if (earlier.cover.get(symbol) !== cover) continue;
|
|
4149
|
+
const before = earlier.held.get(symbol) ?? new Decimal3(0);
|
|
4150
|
+
moved.set(symbol, (later.held.get(symbol) ?? new Decimal3(0)).sub(before));
|
|
4151
|
+
}
|
|
4152
|
+
return moved;
|
|
4153
|
+
}
|
|
4154
|
+
function nearestRecord(instant, records) {
|
|
4155
|
+
let record;
|
|
4156
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
4157
|
+
for (const candidate of records) {
|
|
4158
|
+
const distance = Math.abs(candidate.at - instant);
|
|
4159
|
+
if (candidate.live && distance > BALANCES_REACH_MS) continue;
|
|
4160
|
+
if (distance < gap || distance === gap && record?.live && !candidate.live) {
|
|
4161
|
+
record = candidate;
|
|
4162
|
+
gap = distance;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
return record;
|
|
4166
|
+
}
|
|
4167
|
+
function inventoryAt(instant, records, trades, outflows, readings) {
|
|
4168
|
+
const record = nearestRecord(instant, records);
|
|
4169
|
+
if (!record) return null;
|
|
4170
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
4171
|
+
for (const [symbol, amount] of Object.entries(record.balances)) {
|
|
4172
|
+
if (Number.isFinite(amount)) add(recorded, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4173
|
+
}
|
|
4174
|
+
const held = new Map(recorded);
|
|
4175
|
+
const after = record.at >= instant;
|
|
4176
|
+
const [from, to] = after ? [instant, record.at] : [record.at, instant];
|
|
4177
|
+
const sign = after ? -1 : 1;
|
|
4178
|
+
for (const trade of trades) {
|
|
4179
|
+
if (trade.at >= from && trade.at < to) apply(held, trade, sign);
|
|
4180
|
+
}
|
|
4181
|
+
for (const outflow of outflows) {
|
|
4182
|
+
if (outflow.at >= from && outflow.at < to) {
|
|
4183
|
+
add(held, normalizeSymbol(outflow.symbol), new Decimal3(outflow.amount).mul(-sign));
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
const moved = movedBetween(readings.get(from), readings.get(to));
|
|
4187
|
+
if (moved) {
|
|
4188
|
+
for (const [symbol, amount] of moved) {
|
|
4189
|
+
held.set(symbol, (recorded.get(symbol) ?? new Decimal3(0)).add(amount.mul(sign)));
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
for (const [symbol, amount] of held) if (amount.isNegative()) held.set(symbol, new Decimal3(0));
|
|
4193
|
+
return { held, record };
|
|
4194
|
+
}
|
|
4195
|
+
function tradesOf(fills, now) {
|
|
4196
|
+
const trades = [];
|
|
4197
|
+
for (const fill of fills) {
|
|
4198
|
+
const at = parseUtc(fill.filledAt);
|
|
4199
|
+
if (!Number.isFinite(at) || at > now) continue;
|
|
4200
|
+
const trade = tradeOf(fill, at);
|
|
4201
|
+
if (trade) trades.push(trade);
|
|
4202
|
+
}
|
|
4203
|
+
return trades.sort((a, b) => a.at - b.at);
|
|
4204
|
+
}
|
|
4205
|
+
function bucketStarts(period, now, offset, firstAt) {
|
|
4206
|
+
const { bucket, count } = PROFIT_PERIODS[period];
|
|
4207
|
+
const last = bucketStart(now - offset, bucket);
|
|
4208
|
+
const first = count === null ? bucketStart((firstAt ?? now) - offset, bucket) : shiftBucket(last, bucket, 1 - count);
|
|
4209
|
+
const starts = [];
|
|
4210
|
+
for (let start = first; start <= last; start = shiftBucket(start, bucket, 1)) starts.push(start);
|
|
4211
|
+
return starts;
|
|
4212
|
+
}
|
|
4213
|
+
function inventoryInstants(fills, options) {
|
|
4214
|
+
const { period, now } = options;
|
|
4215
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4216
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4217
|
+
const instants = /* @__PURE__ */ new Set([now]);
|
|
4218
|
+
for (const start of bucketStarts(period, now, offset, tradesOf(fills, now)[0]?.at)) {
|
|
4219
|
+
instants.add(start + offset);
|
|
4220
|
+
const record = nearestRecord(start + offset, records);
|
|
4221
|
+
if (record) instants.add(record.at);
|
|
4222
|
+
}
|
|
4223
|
+
return [...instants].sort((a, b) => a - b);
|
|
4224
|
+
}
|
|
4225
|
+
function summarizeProfit(fills, options) {
|
|
4226
|
+
const { period, now } = options;
|
|
4227
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4228
|
+
const { bucket } = PROFIT_PERIODS[period];
|
|
4229
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4230
|
+
const outflows = options.outflows ?? [];
|
|
4231
|
+
const readings = new Map((options.readings ?? []).map((reading) => [reading.at, readingOf(reading)]));
|
|
4232
|
+
const trades = tradesOf(fills, now);
|
|
4233
|
+
const starts = bucketStarts(period, now, offset, trades[0]?.at);
|
|
4234
|
+
const first = starts[0];
|
|
4235
|
+
const books = /* @__PURE__ */ new Map();
|
|
4236
|
+
const spelling = /* @__PURE__ */ new Map();
|
|
4237
|
+
for (const record of records) {
|
|
4238
|
+
for (const symbol of Object.keys(record.balances)) {
|
|
4239
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
for (const fill of fills) {
|
|
4243
|
+
for (const symbol of [fill.base, fill.quote]) {
|
|
4244
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
for (const trade of trades) {
|
|
4248
|
+
let book = books.get(trade.book);
|
|
4249
|
+
if (!book) {
|
|
4250
|
+
book = new BookLedger(trade.book, trade.base, trade.quote);
|
|
4251
|
+
books.set(trade.book, book);
|
|
4252
|
+
}
|
|
4253
|
+
const rate = trade.quoteAmount.div(trade.baseAmount);
|
|
4254
|
+
if (trade.buying) book.latestBuy = rate;
|
|
4255
|
+
else book.latestSell = rate;
|
|
4256
|
+
}
|
|
4257
|
+
const edges = [];
|
|
4258
|
+
for (const book of books.values()) {
|
|
4259
|
+
const rate = book.rate();
|
|
4260
|
+
if (rate && book.base !== book.quote) {
|
|
4261
|
+
edges.push({ base: book.base, quote: book.quote, rate: new BaseDecimal(rate.toString()) });
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
const factors = usdFactorsFrom(edges);
|
|
4265
|
+
const priceOf = (symbol) => {
|
|
4266
|
+
const factor = factors.get(symbol);
|
|
4267
|
+
return factor ? new Decimal3(factor.toString()) : null;
|
|
4268
|
+
};
|
|
4269
|
+
const unpriced = /* @__PURE__ */ new Set();
|
|
4270
|
+
const valueOf = (holdings) => {
|
|
4271
|
+
let total = new Decimal3(0);
|
|
4272
|
+
for (const [symbol, amount] of holdings) {
|
|
4273
|
+
if (amount.isZero()) continue;
|
|
4274
|
+
const price = priceOf(symbol);
|
|
4275
|
+
if (price) total = total.add(amount.mul(price));
|
|
4276
|
+
else unpriced.add(symbol);
|
|
4277
|
+
}
|
|
4278
|
+
return total;
|
|
4279
|
+
};
|
|
4280
|
+
const totals = new Sums();
|
|
4281
|
+
const buckets = new Map(starts.map((start) => [start, new Sums()]));
|
|
4282
|
+
let estimatedFills = 0;
|
|
4283
|
+
for (const trade of trades) {
|
|
4284
|
+
const sums = buckets.get(bucketStart(trade.at - offset, bucket));
|
|
4285
|
+
if (!sums) continue;
|
|
4286
|
+
const book = books.get(trade.book);
|
|
4287
|
+
if (!book) continue;
|
|
4288
|
+
book.inPeriod = true;
|
|
4289
|
+
if (trade.estimated) estimatedFills += 1;
|
|
4290
|
+
if (trade.buying) {
|
|
4291
|
+
book.bought = book.bought.add(trade.baseAmount);
|
|
4292
|
+
book.boughtQuote = book.boughtQuote.add(trade.quoteAmount);
|
|
4293
|
+
} else {
|
|
4294
|
+
book.sold = book.sold.add(trade.baseAmount);
|
|
4295
|
+
book.soldQuote = book.soldQuote.add(trade.quoteAmount);
|
|
4296
|
+
}
|
|
4297
|
+
const basePrice = priceOf(trade.base);
|
|
4298
|
+
const quotePrice = priceOf(trade.quote);
|
|
4299
|
+
const volume = basePrice ? trade.baseAmount.mul(basePrice) : quotePrice ? trade.quoteAmount.mul(quotePrice) : new Decimal3(0);
|
|
4300
|
+
for (const sum of [totals, sums]) {
|
|
4301
|
+
apply(sum.change, trade, 1);
|
|
4302
|
+
if (trade.buying) {
|
|
4303
|
+
sum.buys += 1;
|
|
4304
|
+
sum.boughtUsd = sum.boughtUsd.add(volume);
|
|
4305
|
+
} else {
|
|
4306
|
+
sum.sells += 1;
|
|
4307
|
+
sum.soldUsd = sum.soldUsd.add(volume);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
const figures = (sums, startsAt, endsAt) => {
|
|
4312
|
+
const profit = valueOf(sums.change);
|
|
4313
|
+
const began2 = inventoryAt(startsAt, records, trades, outflows, readings);
|
|
4314
|
+
const inventory = began2 ? valueOf(began2.held) : null;
|
|
4315
|
+
const moved = movedBetween(readings.get(startsAt), readings.get(endsAt));
|
|
4316
|
+
if (moved) {
|
|
4317
|
+
for (const [symbol, amount] of sums.change) {
|
|
4318
|
+
if (moved.has(symbol)) moved.set(symbol, (moved.get(symbol) ?? new Decimal3(0)).sub(amount));
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
return {
|
|
4322
|
+
profitUsd: profit.toNumber(),
|
|
4323
|
+
startInventoryUsd: inventory ? inventory.toNumber() : null,
|
|
4324
|
+
returnPct: inventory?.gt(0) ? profit.div(inventory).mul(100).toNumber() : null,
|
|
4325
|
+
transfersUsd: moved && moved.size > 0 ? valueOf(moved).toNumber() : null,
|
|
4326
|
+
boughtUsd: sums.boughtUsd.toNumber(),
|
|
4327
|
+
soldUsd: sums.soldUsd.toNumber(),
|
|
4328
|
+
buys: sums.buys,
|
|
4329
|
+
sells: sums.sells
|
|
4330
|
+
};
|
|
4331
|
+
};
|
|
4332
|
+
const from = first + offset;
|
|
4333
|
+
const began = inventoryAt(from, records, trades, outflows, readings);
|
|
4334
|
+
const startInventory = began && {
|
|
4335
|
+
at: from,
|
|
4336
|
+
source: began.record.live ? "balances" : "snapshot",
|
|
4337
|
+
recordedAt: began.record.at,
|
|
4338
|
+
usd: valueOf(began.held).toNumber(),
|
|
4339
|
+
tokens: [...began.held].filter(([, amount]) => amount.gt(0)).map(([symbol, amount]) => {
|
|
4340
|
+
const price = priceOf(symbol);
|
|
4341
|
+
return {
|
|
4342
|
+
symbol: spelling.get(symbol) ?? symbol,
|
|
4343
|
+
amount: amount.toNumber(),
|
|
4344
|
+
usd: price ? amount.mul(price).toNumber() : null
|
|
4345
|
+
};
|
|
4346
|
+
}).sort((a, b) => (b.usd ?? 0) - (a.usd ?? 0) || a.symbol.localeCompare(b.symbol))
|
|
4347
|
+
};
|
|
4348
|
+
const rows = [];
|
|
4349
|
+
for (const book of books.values()) {
|
|
4350
|
+
if (!book.inPeriod) continue;
|
|
4351
|
+
const averageBuy = book.bought.gt(0) ? book.boughtQuote.div(book.bought) : null;
|
|
4352
|
+
const averageSell = book.sold.gt(0) ? book.soldQuote.div(book.sold) : null;
|
|
4353
|
+
const baseChange = book.bought.sub(book.sold);
|
|
4354
|
+
const quoteChange = book.soldQuote.sub(book.boughtQuote);
|
|
4355
|
+
const basePrice = priceOf(book.base);
|
|
4356
|
+
const quotePrice = priceOf(book.quote);
|
|
4357
|
+
rows.push({
|
|
4358
|
+
book: book.id,
|
|
4359
|
+
base: spelling.get(book.base) ?? book.base,
|
|
4360
|
+
quote: spelling.get(book.quote) ?? book.quote,
|
|
4361
|
+
bought: book.bought.toNumber(),
|
|
4362
|
+
sold: book.sold.toNumber(),
|
|
4363
|
+
averageBuy: averageBuy ? averageBuy.toNumber() : null,
|
|
4364
|
+
averageSell: averageSell ? averageSell.toNumber() : null,
|
|
4365
|
+
spreadPct: averageBuy && averageSell ? averageSell.div(averageBuy).sub(1).mul(100).toNumber() : null,
|
|
4366
|
+
baseChange: baseChange.toNumber(),
|
|
4367
|
+
quoteChange: quoteChange.toNumber(),
|
|
4368
|
+
rate: book.rate()?.toNumber() ?? null,
|
|
4369
|
+
profitUsd: basePrice && quotePrice ? baseChange.mul(basePrice).add(quoteChange.mul(quotePrice)).toNumber() : null
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
rows.sort((a, b) => (b.profitUsd ?? 0) - (a.profitUsd ?? 0) || a.book.localeCompare(b.book));
|
|
4373
|
+
return {
|
|
4374
|
+
period,
|
|
4375
|
+
bucket,
|
|
4376
|
+
from,
|
|
4377
|
+
to: now,
|
|
4378
|
+
totals: figures(totals, from, now),
|
|
4379
|
+
series: starts.map((start, index) => {
|
|
4380
|
+
const sums = buckets.get(start) ?? new Sums();
|
|
4381
|
+
const endsAt = index + 1 < starts.length ? starts[index + 1] + offset : now;
|
|
4382
|
+
return { start: start + offset, ...figures(sums, start + offset, endsAt) };
|
|
4383
|
+
}),
|
|
4384
|
+
books: rows,
|
|
4385
|
+
startInventory,
|
|
4386
|
+
estimatedFills,
|
|
4387
|
+
unpricedTokens: [...unpriced].map((symbol) => spelling.get(symbol) ?? symbol).sort()
|
|
4388
|
+
};
|
|
4389
|
+
}
|
|
4390
|
+
|
|
3777
4391
|
// src/data/state.ts
|
|
3778
4392
|
async function patchRuntimeState(store, patch) {
|
|
3779
4393
|
if (store.patch) return store.patch(patch);
|
|
@@ -3782,6 +4396,80 @@ async function patchRuntimeState(store, patch) {
|
|
|
3782
4396
|
return next;
|
|
3783
4397
|
}
|
|
3784
4398
|
|
|
4399
|
+
// src/services/bundler-preflight.ts
|
|
4400
|
+
var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
4401
|
+
async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
|
|
4402
|
+
const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
|
|
4403
|
+
const logger = moduleLogger(configService.loggers, "bundler-preflight");
|
|
4404
|
+
const failures = await Promise.all(
|
|
4405
|
+
chains.map(async ({ chainId, bundlerUrl }) => {
|
|
4406
|
+
if (options.watchOnly?.[chainId] === true) return void 0;
|
|
4407
|
+
if (!bundlerUrl?.trim()) return void 0;
|
|
4408
|
+
const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
|
|
4409
|
+
if (!entryPoint) return void 0;
|
|
4410
|
+
return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
|
|
4411
|
+
})
|
|
4412
|
+
);
|
|
4413
|
+
const messages = failures.filter((failure) => failure !== void 0);
|
|
4414
|
+
if (messages.length > 0) throw new Error(messages.join("; "));
|
|
4415
|
+
}
|
|
4416
|
+
function servesEntryPoint(listed, entryPoint) {
|
|
4417
|
+
return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
|
|
4418
|
+
}
|
|
4419
|
+
function describeEntryPoints(listed) {
|
|
4420
|
+
return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
|
|
4421
|
+
}
|
|
4422
|
+
function bundlerHost(url) {
|
|
4423
|
+
try {
|
|
4424
|
+
return new URL(url).host || "an unparseable URL";
|
|
4425
|
+
} catch {
|
|
4426
|
+
return "an unparseable URL";
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
function chainName(chainId) {
|
|
4430
|
+
const label = chainByChainId(chainId)?.label;
|
|
4431
|
+
return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
|
|
4432
|
+
}
|
|
4433
|
+
async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
|
|
4434
|
+
const host = bundlerHost(bundlerUrl);
|
|
4435
|
+
const subject = `Bundler ${host} for ${chainName(chainId)}`;
|
|
4436
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
4437
|
+
let listed;
|
|
4438
|
+
try {
|
|
4439
|
+
listed = await supportedEntryPoints(bundlerUrl, signal);
|
|
4440
|
+
} catch (err) {
|
|
4441
|
+
const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
|
|
4442
|
+
logger.warn(
|
|
4443
|
+
{ chain: formatChainKey(chainId), bundler: host },
|
|
4444
|
+
`${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
|
|
4445
|
+
);
|
|
4446
|
+
return void 0;
|
|
4447
|
+
}
|
|
4448
|
+
if (servesEntryPoint(listed, entryPoint)) return void 0;
|
|
4449
|
+
return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
|
|
4450
|
+
}
|
|
4451
|
+
async function supportedEntryPoints(bundlerUrl, signal) {
|
|
4452
|
+
const response = await fetch(bundlerUrl, {
|
|
4453
|
+
method: "POST",
|
|
4454
|
+
headers: { "Content-Type": "application/json" },
|
|
4455
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
|
|
4456
|
+
signal
|
|
4457
|
+
});
|
|
4458
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
4459
|
+
const json = await response.json();
|
|
4460
|
+
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
4461
|
+
const { result } = json;
|
|
4462
|
+
if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
|
|
4463
|
+
throw new Error("the result is not a list of addresses");
|
|
4464
|
+
}
|
|
4465
|
+
return result;
|
|
4466
|
+
}
|
|
4467
|
+
function errorReason(err) {
|
|
4468
|
+
if (!(err instanceof Error)) return String(err);
|
|
4469
|
+
const { cause } = err;
|
|
4470
|
+
return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
|
|
4471
|
+
}
|
|
4472
|
+
|
|
3785
4473
|
// src/services/wallet/mpcvault.ts
|
|
3786
4474
|
import { concatHex, keccak256 as keccak2562, padHex, toHex } from "viem";
|
|
3787
4475
|
import * as grpc from "@grpc/grpc-js";
|
|
@@ -13045,6 +13733,7 @@ var CacheService = class {
|
|
|
13045
13733
|
})),
|
|
13046
13734
|
partialFill: plan.partialFill,
|
|
13047
13735
|
profit: plan.profit,
|
|
13736
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13048
13737
|
...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
|
|
13049
13738
|
}));
|
|
13050
13739
|
} catch (error) {
|
|
@@ -13075,6 +13764,7 @@ var CacheService = class {
|
|
|
13075
13764
|
})),
|
|
13076
13765
|
partialFill: plan.partialFill,
|
|
13077
13766
|
profit: plan.profit,
|
|
13767
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13078
13768
|
...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
|
|
13079
13769
|
})),
|
|
13080
13770
|
timestamp: Date.now()
|
|
@@ -15713,7 +16403,7 @@ var EVM_HOST = [
|
|
|
15713
16403
|
];
|
|
15714
16404
|
|
|
15715
16405
|
// src/services/ContractInteractionService.ts
|
|
15716
|
-
import { Decimal as
|
|
16406
|
+
import { Decimal as Decimal4 } from "decimal.js";
|
|
15717
16407
|
|
|
15718
16408
|
// src/config/abis/IntentGatewayV2.ts
|
|
15719
16409
|
var INTENT_GATEWAY_V2_ABI = [
|
|
@@ -16974,9 +17664,9 @@ var INTENT_GATEWAY_V2_ABI = [
|
|
|
16974
17664
|
internalType: "bytes32"
|
|
16975
17665
|
},
|
|
16976
17666
|
{
|
|
16977
|
-
name: "
|
|
16978
|
-
type: "
|
|
16979
|
-
internalType: "
|
|
17667
|
+
name: "userOpHash",
|
|
17668
|
+
type: "bytes32",
|
|
17669
|
+
internalType: "bytes32"
|
|
16980
17670
|
},
|
|
16981
17671
|
{
|
|
16982
17672
|
name: "signature",
|
|
@@ -18355,7 +19045,7 @@ function describeShortfall(balances) {
|
|
|
18355
19045
|
}
|
|
18356
19046
|
|
|
18357
19047
|
// src/services/ContractInteractionService.ts
|
|
18358
|
-
|
|
19048
|
+
Decimal4.config({ precision: 28, rounding: 4 });
|
|
18359
19049
|
var FUNDING_GAS_PER_CALL = 400000n;
|
|
18360
19050
|
var DEBIT_ORDER_GAS = 60000n;
|
|
18361
19051
|
function settles(budget) {
|
|
@@ -18365,10 +19055,13 @@ var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
|
|
|
18365
19055
|
var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
|
|
18366
19056
|
var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
|
|
18367
19057
|
var BID_DISCOVERY_PAD_SECONDS = 30;
|
|
19058
|
+
var BLOCK_TIME_SAMPLE_BLOCKS = 1000n;
|
|
19059
|
+
var FALLBACK_BLOCK_TIME_SECONDS = 2;
|
|
18368
19060
|
var ContractInteractionService = class {
|
|
18369
19061
|
constructor(clientManager, configService, signer, sharedCacheService) {
|
|
18370
19062
|
this.clientManager = clientManager;
|
|
18371
19063
|
this.sdkHelperCache = /* @__PURE__ */ new Map();
|
|
19064
|
+
this.blockTimes = /* @__PURE__ */ new Map();
|
|
18372
19065
|
this.logger = moduleLogger(configService.loggers, "contract-service");
|
|
18373
19066
|
this.configService = configService;
|
|
18374
19067
|
this.cacheService = sharedCacheService || new CacheService();
|
|
@@ -18428,11 +19121,20 @@ var ContractInteractionService = class {
|
|
|
18428
19121
|
try {
|
|
18429
19122
|
const chainIds = this.configService.getConfiguredChainIds();
|
|
18430
19123
|
const chainNames = chainIds.map((id) => `EVM-${id}`);
|
|
18431
|
-
|
|
19124
|
+
await Promise.all(
|
|
19125
|
+
chainNames.map(async (chainName2) => {
|
|
19126
|
+
try {
|
|
19127
|
+
await this.blockTimeSeconds(chainName2);
|
|
19128
|
+
} catch (err) {
|
|
19129
|
+
this.logger.warn({ err, chain: chainName2 }, "Could not measure the block time at startup");
|
|
19130
|
+
}
|
|
19131
|
+
})
|
|
19132
|
+
);
|
|
19133
|
+
for (const chainName2 of chainNames) {
|
|
18432
19134
|
try {
|
|
18433
|
-
await this.getFeeTokenWithDecimals(
|
|
19135
|
+
await this.getFeeTokenWithDecimals(chainName2);
|
|
18434
19136
|
} catch (err) {
|
|
18435
|
-
this.logger.warn({ err, chain:
|
|
19137
|
+
this.logger.warn({ err, chain: chainName2 }, "Could not prewarm fee token decimals");
|
|
18436
19138
|
}
|
|
18437
19139
|
}
|
|
18438
19140
|
for (const destChain of chainNames) {
|
|
@@ -18696,7 +19398,7 @@ var ContractInteractionService = class {
|
|
|
18696
19398
|
* @returns The total USD value of inputs (sum of normalized stable amounts, or 0 if none)
|
|
18697
19399
|
*/
|
|
18698
19400
|
async getInputUsdValue(order) {
|
|
18699
|
-
let inputUsdValue = new
|
|
19401
|
+
let inputUsdValue = new Decimal4(0);
|
|
18700
19402
|
const inputs = order.inputs;
|
|
18701
19403
|
const sourceUsdc = this.configService.getUsdcAsset(order.source).toLowerCase();
|
|
18702
19404
|
const sourceUsdt = this.configService.getUsdtAsset(order.source).toLowerCase();
|
|
@@ -18705,7 +19407,7 @@ var ContractInteractionService = class {
|
|
|
18705
19407
|
const addr = tokenAddress.toLowerCase();
|
|
18706
19408
|
if (addr !== sourceUsdc && addr !== sourceUsdt) continue;
|
|
18707
19409
|
const decimals = await this.getTokenDecimals(tokenAddress, order.source);
|
|
18708
|
-
const tokenAmount = new
|
|
19410
|
+
const tokenAmount = new Decimal4(formatUnits2(input.amount, decimals));
|
|
18709
19411
|
inputUsdValue = inputUsdValue.plus(tokenAmount);
|
|
18710
19412
|
}
|
|
18711
19413
|
return inputUsdValue;
|
|
@@ -19017,18 +19719,59 @@ var ContractInteractionService = class {
|
|
|
19017
19719
|
*
|
|
19018
19720
|
* Operators configure seconds because that is the unit the risk is actually in, but the
|
|
19019
19721
|
* contract compares against block numbers so `order.deadline` and this read the same
|
|
19020
|
-
* clock. The conversion uses the chain's
|
|
19021
|
-
* up, since erring long costs a slightly stale quote while erring
|
|
19022
|
-
* winnable bids.
|
|
19722
|
+
* clock. The conversion uses the chain's block time from {@link blockTimeSeconds}; it is
|
|
19723
|
+
* deliberately rounded up, since erring long costs a slightly stale quote while erring
|
|
19724
|
+
* short silently drops winnable bids.
|
|
19023
19725
|
*/
|
|
19024
19726
|
async bidValidUntilBlock(chain) {
|
|
19025
19727
|
const client = this.clientManager.getPublicClient(chain);
|
|
19026
|
-
const currentBlock = await client.getBlockNumber();
|
|
19027
|
-
const blockTimeMs = client.chain?.blockTime;
|
|
19028
|
-
const blockTimeSec = blockTimeMs ? blockTimeMs / 1e3 : 2;
|
|
19728
|
+
const [currentBlock, blockTimeSec] = await Promise.all([client.getBlockNumber(), this.blockTimeSeconds(chain)]);
|
|
19029
19729
|
const windowSec = this.configService.getBidValiditySeconds() + BID_DISCOVERY_PAD_SECONDS;
|
|
19030
19730
|
return currentBlock + BigInt(Math.ceil(windowSec / blockTimeSec));
|
|
19031
19731
|
}
|
|
19732
|
+
/**
|
|
19733
|
+
* Seconds per block on `chain`. Measured once, when simplex starts ({@link initCache}), and
|
|
19734
|
+
* kept until it stops. A chain with no measurement yet, because it was added later or its
|
|
19735
|
+
* blocks could not be read at startup, is measured at its first bid.
|
|
19736
|
+
*
|
|
19737
|
+
* The nominal `Chain.blockTime` cannot be relied on alone: it goes stale when a chain
|
|
19738
|
+
* speeds up (BSC makes a block every 0.45s against a declared 0.75s) and some chains
|
|
19739
|
+
* declare none. Converting at a slower block time than the chain's ends a bid early, so
|
|
19740
|
+
* the faster of the measured and the declared figure is used, and a stalled or idle
|
|
19741
|
+
* stretch in the sample never shortens a bid.
|
|
19742
|
+
*
|
|
19743
|
+
* Blocks that cannot be read fall back to the declared figure, then to
|
|
19744
|
+
* {@link FALLBACK_BLOCK_TIME_SECONDS}. That answer is not kept.
|
|
19745
|
+
*/
|
|
19746
|
+
blockTimeSeconds(chain) {
|
|
19747
|
+
const known = this.blockTimes.get(chain);
|
|
19748
|
+
if (known) return known;
|
|
19749
|
+
const client = this.clientManager.getPublicClient(chain);
|
|
19750
|
+
const declaredMs = client.chain?.blockTime;
|
|
19751
|
+
const declared = declaredMs ? declaredMs / 1e3 : void 0;
|
|
19752
|
+
const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
|
|
19753
|
+
this.blockTimes.delete(chain);
|
|
19754
|
+
const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
|
|
19755
|
+
this.logger.warn(
|
|
19756
|
+
{ err, chain, seconds: fallback2 },
|
|
19757
|
+
"Could not measure the block time; using the declared one"
|
|
19758
|
+
);
|
|
19759
|
+
return fallback2;
|
|
19760
|
+
});
|
|
19761
|
+
this.blockTimes.set(chain, seconds);
|
|
19762
|
+
return seconds;
|
|
19763
|
+
}
|
|
19764
|
+
/** The average block time over the chain's last {@link BLOCK_TIME_SAMPLE_BLOCKS} blocks, in seconds. */
|
|
19765
|
+
async measureBlockTimeSeconds(client) {
|
|
19766
|
+
const head = await client.getBlock({ blockTag: "latest" });
|
|
19767
|
+
const span = head.number < BLOCK_TIME_SAMPLE_BLOCKS ? head.number : BLOCK_TIME_SAMPLE_BLOCKS;
|
|
19768
|
+
const tail = await client.getBlock({ blockNumber: head.number - span });
|
|
19769
|
+
const measured = Number(head.timestamp - tail.timestamp) / Number(span);
|
|
19770
|
+
if (!Number.isFinite(measured) || measured <= 0) {
|
|
19771
|
+
throw new Error(`${span} blocks span ${head.timestamp - tail.timestamp}s`);
|
|
19772
|
+
}
|
|
19773
|
+
return measured;
|
|
19774
|
+
}
|
|
19032
19775
|
/**
|
|
19033
19776
|
* Builds the signed, never-executed UserOperation that carries one limit order
|
|
19034
19777
|
* to the HyperFX orderbook.
|
|
@@ -19203,11 +19946,18 @@ import { encodeFunctionData as encodeFunctionData3, erc20Abi as erc20Abi3, forma
|
|
|
19203
19946
|
import { encodeERC7821ExecuteBatch as encodeERC7821ExecuteBatch3 } from "@hyperbridge/sdk";
|
|
19204
19947
|
|
|
19205
19948
|
// src/services/UserOpSender.ts
|
|
19206
|
-
import {
|
|
19949
|
+
import {
|
|
19950
|
+
CryptoUtils as CryptoUtils2,
|
|
19951
|
+
BundlerMethod,
|
|
19952
|
+
applyRundlerPriorityFee,
|
|
19953
|
+
fetchRundlerPriorityFee
|
|
19954
|
+
} from "@hyperbridge/sdk";
|
|
19207
19955
|
import { toHex as toHex2 } from "viem";
|
|
19208
19956
|
var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
|
|
19209
19957
|
var FALLBACK_CALL_GAS_LIMIT = 1500000n;
|
|
19210
19958
|
var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
|
|
19959
|
+
var PRIORITY_FEE_BUMP_PERCENT = 8n;
|
|
19960
|
+
var MAX_FEE_BUMP_PERCENT = 10n;
|
|
19211
19961
|
var UserOpSender = class {
|
|
19212
19962
|
constructor(clientManager, configService, signer) {
|
|
19213
19963
|
this.clientManager = clientManager;
|
|
@@ -19388,7 +20138,11 @@ var UserOpSender = class {
|
|
|
19388
20138
|
);
|
|
19389
20139
|
return userOp;
|
|
19390
20140
|
}
|
|
19391
|
-
/**
|
|
20141
|
+
/**
|
|
20142
|
+
* Mirrors the gas-price selection used by the delegation and fill paths: Pimlico's and
|
|
20143
|
+
* Alchemy's own pricing, otherwise the chain's gas price raised to the priority fee a
|
|
20144
|
+
* rundler bundler requires, when the bundler gives one.
|
|
20145
|
+
*/
|
|
19392
20146
|
async getGasPrice(bundlerUrl, publicClient, chainId) {
|
|
19393
20147
|
const lower = bundlerUrl.toLowerCase();
|
|
19394
20148
|
if (lower.includes("pimlico.io")) {
|
|
@@ -19400,19 +20154,33 @@ var UserOpSender = class {
|
|
|
19400
20154
|
return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
|
|
19401
20155
|
}
|
|
19402
20156
|
if (lower.includes("alchemy.com")) {
|
|
19403
|
-
const [
|
|
20157
|
+
const [rundlerPriorityFee2, latestBlock2] = await Promise.all([
|
|
19404
20158
|
this.sendBundlerRpc(bundlerUrl, BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
|
|
19405
20159
|
publicClient.getBlock({ blockTag: "latest" })
|
|
19406
20160
|
]);
|
|
19407
|
-
const baseFeePerGas =
|
|
20161
|
+
const baseFeePerGas = latestBlock2.baseFeePerGas ?? await publicClient.getGasPrice();
|
|
19408
20162
|
const isArbitrum = BigInt(chainId) === 42161n;
|
|
19409
20163
|
const prioBump = isArbitrum ? 0n : 25n;
|
|
19410
|
-
const maxPriorityFeePerGas = BigInt(
|
|
20164
|
+
const maxPriorityFeePerGas = BigInt(rundlerPriorityFee2) + BigInt(rundlerPriorityFee2) * prioBump / 100n;
|
|
19411
20165
|
const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
|
|
19412
20166
|
return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
|
|
19413
20167
|
}
|
|
19414
|
-
const gasPrice = await
|
|
19415
|
-
|
|
20168
|
+
const [gasPrice, rundlerPriorityFee] = await Promise.all([
|
|
20169
|
+
publicClient.getGasPrice(),
|
|
20170
|
+
fetchRundlerPriorityFee(bundlerUrl)
|
|
20171
|
+
]);
|
|
20172
|
+
const fees = {
|
|
20173
|
+
maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
|
|
20174
|
+
maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
|
|
20175
|
+
};
|
|
20176
|
+
if (rundlerPriorityFee === null) return fees;
|
|
20177
|
+
const latestBlock = await publicClient.getBlock({ blockTag: "latest" });
|
|
20178
|
+
return applyRundlerPriorityFee(fees, {
|
|
20179
|
+
rundlerPriorityFee,
|
|
20180
|
+
baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
|
|
20181
|
+
priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
|
|
20182
|
+
maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
|
|
20183
|
+
});
|
|
19416
20184
|
}
|
|
19417
20185
|
async sendBundlerRpc(bundlerUrl, method, params) {
|
|
19418
20186
|
const response = await fetch(bundlerUrl, {
|
|
@@ -19834,7 +20602,7 @@ var DelegationService = class {
|
|
|
19834
20602
|
// src/services/RebalancingService.ts
|
|
19835
20603
|
import { formatUnits as formatUnits3 } from "viem";
|
|
19836
20604
|
import { parseStateMachineId as parseStateMachineId4 } from "@hyperbridge/sdk";
|
|
19837
|
-
import { Decimal as
|
|
20605
|
+
import { Decimal as Decimal5 } from "decimal.js";
|
|
19838
20606
|
|
|
19839
20607
|
// src/services/rebalancers/binance.ts
|
|
19840
20608
|
import { Wallet as Wallet2 } from "@binance/wallet";
|
|
@@ -20998,7 +21766,7 @@ var RebalancingService = class {
|
|
|
20998
21766
|
functionName: "balanceOf",
|
|
20999
21767
|
args: [this.walletAddress]
|
|
21000
21768
|
});
|
|
21001
|
-
const balanceDecimal = new
|
|
21769
|
+
const balanceDecimal = new Decimal5(formatUnits3(balanceWei, Number(decimals)));
|
|
21002
21770
|
return balanceDecimal.toNumber();
|
|
21003
21771
|
} catch (error) {
|
|
21004
21772
|
this.logger.error({ error, chain, asset }, "Failed to get current balance");
|
|
@@ -21031,7 +21799,7 @@ var RebalancingService = class {
|
|
|
21031
21799
|
};
|
|
21032
21800
|
|
|
21033
21801
|
// src/core/filler.ts
|
|
21034
|
-
import { Decimal as
|
|
21802
|
+
import { Decimal as Decimal6 } from "decimal.js";
|
|
21035
21803
|
var HYPERBRIDGE_CONNECT_TIMEOUT_MS = 3e4;
|
|
21036
21804
|
function byAmount(a, b) {
|
|
21037
21805
|
return BigInt(a.amount) < BigInt(b.amount) ? -1 : BigInt(a.amount) > BigInt(b.amount) ? 1 : 0;
|
|
@@ -21587,7 +22355,7 @@ var IntentFiller = class {
|
|
|
21587
22355
|
try {
|
|
21588
22356
|
const stratValue = await strategy.getOrderUsdValue(order);
|
|
21589
22357
|
if (stratValue != null) {
|
|
21590
|
-
inputUsdValue =
|
|
22358
|
+
inputUsdValue = Decimal6.max(baseInputUsd, stratValue.inputUsd);
|
|
21591
22359
|
valued = true;
|
|
21592
22360
|
break;
|
|
21593
22361
|
}
|
|
@@ -21835,7 +22603,12 @@ var IntentFiller = class {
|
|
|
21835
22603
|
else cache.clearFundingPrepends(order.id);
|
|
21836
22604
|
}
|
|
21837
22605
|
reservation = await this.holdAll([
|
|
21838
|
-
{
|
|
22606
|
+
{
|
|
22607
|
+
limitOrderId: plan.limitOrderId,
|
|
22608
|
+
payout: plan.payout,
|
|
22609
|
+
take: plan.fillerInputs[plan.leg]?.amount,
|
|
22610
|
+
takeDecimals: plan.inputDecimals
|
|
22611
|
+
}
|
|
21839
22612
|
]);
|
|
21840
22613
|
if (reservation.length === 0) {
|
|
21841
22614
|
this.logger.info(
|
|
@@ -22005,7 +22778,8 @@ var IntentFiller = class {
|
|
|
22005
22778
|
taken.push({
|
|
22006
22779
|
limitOrderId: hold.limitOrderId,
|
|
22007
22780
|
amount,
|
|
22008
|
-
...hold.take !== void 0 ? { take: hold.take.toString() } : {}
|
|
22781
|
+
...hold.take !== void 0 ? { take: hold.take.toString() } : {},
|
|
22782
|
+
...hold.takeDecimals !== void 0 ? { takeDecimals: hold.takeDecimals } : {}
|
|
22009
22783
|
});
|
|
22010
22784
|
}
|
|
22011
22785
|
return taken;
|
|
@@ -22032,6 +22806,7 @@ var IntentFiller = class {
|
|
|
22032
22806
|
}
|
|
22033
22807
|
const settled = executedHold(claimed, delivered.scaled, released);
|
|
22034
22808
|
const charged = chargedFor(settled, delivered, released);
|
|
22809
|
+
const takenIn = released > 0n && settled.takeDecimals !== void 0 ? toScaled(released, settled.takeDecimals) : null;
|
|
22035
22810
|
const settledBid = rows.find(
|
|
22036
22811
|
(row) => row.reservations.some(
|
|
22037
22812
|
(hold) => hold.limitOrderId === settled.limitOrderId && hold.amount === settled.amount && hold.take === settled.take
|
|
@@ -22050,6 +22825,7 @@ var IntentFiller = class {
|
|
|
22050
22825
|
commitment,
|
|
22051
22826
|
bid: settledBid,
|
|
22052
22827
|
amount: share.toString(),
|
|
22828
|
+
amountIn: takenIn?.toString() ?? null,
|
|
22053
22829
|
transactionHash: transactionHash ?? null
|
|
22054
22830
|
});
|
|
22055
22831
|
}
|
|
@@ -22201,44 +22977,7 @@ import {
|
|
|
22201
22977
|
ADDRESS_ZERO as ADDRESS_ZERO2
|
|
22202
22978
|
} from "@hyperbridge/sdk";
|
|
22203
22979
|
import { formatUnits as formatUnits4 } from "viem";
|
|
22204
|
-
import { Decimal as
|
|
22205
|
-
|
|
22206
|
-
// src/orderbook/usd.ts
|
|
22207
|
-
import { Decimal as Decimal5 } from "decimal.js";
|
|
22208
|
-
function limitOrderUsdEdges(orders) {
|
|
22209
|
-
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
22210
|
-
base: normalizeSymbol(order.base),
|
|
22211
|
-
quote: normalizeSymbol(order.quote),
|
|
22212
|
-
rate: new Decimal5(order.price).div(ORDERBOOK_SCALE.toString())
|
|
22213
|
-
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
22214
|
-
}
|
|
22215
|
-
function usdFactorsFrom(edges) {
|
|
22216
|
-
const factors = /* @__PURE__ */ new Map();
|
|
22217
|
-
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new Decimal5(1));
|
|
22218
|
-
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
22219
|
-
let grew = true;
|
|
22220
|
-
while (grew) {
|
|
22221
|
-
grew = false;
|
|
22222
|
-
for (const edge of ordered) {
|
|
22223
|
-
const usdBase = factors.get(edge.base);
|
|
22224
|
-
const usdQuote = factors.get(edge.quote);
|
|
22225
|
-
if (usdBase && !usdQuote) {
|
|
22226
|
-
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
22227
|
-
grew = true;
|
|
22228
|
-
} else if (usdQuote && !usdBase) {
|
|
22229
|
-
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
22230
|
-
grew = true;
|
|
22231
|
-
}
|
|
22232
|
-
}
|
|
22233
|
-
}
|
|
22234
|
-
return factors;
|
|
22235
|
-
}
|
|
22236
|
-
function usdValueOf(factors, symbol, amount) {
|
|
22237
|
-
const factor = factors.get(normalizeSymbol(symbol));
|
|
22238
|
-
return factor ? amount.mul(factor) : null;
|
|
22239
|
-
}
|
|
22240
|
-
|
|
22241
|
-
// src/strategies/fx.ts
|
|
22980
|
+
import { Decimal as Decimal7 } from "decimal.js";
|
|
22242
22981
|
function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
|
|
22243
22982
|
if (fromDecimals === toDecimals) return amount;
|
|
22244
22983
|
if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
|
|
@@ -22286,7 +23025,7 @@ var FXFiller = class _FXFiller {
|
|
|
22286
23025
|
this.maxConsecutiveClamps = configService.getMaxConsecutiveClamps();
|
|
22287
23026
|
if (confirmationPolicy) {
|
|
22288
23027
|
this.confirmationPolicy = {
|
|
22289
|
-
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new
|
|
23028
|
+
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new Decimal7(amountUsd))
|
|
22290
23029
|
};
|
|
22291
23030
|
}
|
|
22292
23031
|
}
|
|
@@ -22622,17 +23361,17 @@ var FXFiller = class _FXFiller {
|
|
|
22622
23361
|
const outputSymbol = this.registry.symbolFor(outputToken, destChain);
|
|
22623
23362
|
const sameAsset = outputSymbol !== null && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.base) && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.quote);
|
|
22624
23363
|
let realizedSpreadProfit = 0n;
|
|
22625
|
-
let sameAssetEdgeUsd = new
|
|
23364
|
+
let sameAssetEdgeUsd = new Decimal7(0);
|
|
22626
23365
|
let sameAssetProfitable = true;
|
|
22627
23366
|
if (sameAsset) {
|
|
22628
23367
|
const convertedInput = adjustDecimalsFloor(releasedInput, inputDecimals, outputDecimals);
|
|
22629
23368
|
const spread = convertedInput - finalOutputAmount;
|
|
22630
23369
|
if (spread <= 0n) sameAssetProfitable = false;
|
|
22631
23370
|
realizedSpreadProfit = adjustDecimalsFloor(spread, outputDecimals, feeTokenDecimals);
|
|
22632
|
-
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new
|
|
23371
|
+
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new Decimal7(formatUnits4(spread, outputDecimals)));
|
|
22633
23372
|
if (spreadUsd) sameAssetEdgeUsd = spreadUsd;
|
|
22634
23373
|
}
|
|
22635
|
-
const payoutSurplusUsd = new
|
|
23374
|
+
const payoutSurplusUsd = new Decimal7(0);
|
|
22636
23375
|
const { totalCostInSourceFeeToken, relayerFeeInSourceFeeToken, dispatchFee } = await this.contractService.estimateGasFillPost(order);
|
|
22637
23376
|
if (sourceChain !== destChain && dispatchFee > 0n) {
|
|
22638
23377
|
const feeToken = await this.contractService.getFeeTokenWithDecimals(destChain);
|
|
@@ -22719,6 +23458,7 @@ var FXFiller = class _FXFiller {
|
|
|
22719
23458
|
payout: toScaled(finalOutputAmount, outputDecimals),
|
|
22720
23459
|
fillerOutputs,
|
|
22721
23460
|
fillerInputs,
|
|
23461
|
+
inputDecimals,
|
|
22722
23462
|
fundingCalls: [...fundingCalls],
|
|
22723
23463
|
partialFill,
|
|
22724
23464
|
profit: totalProfit,
|
|
@@ -22997,13 +23737,13 @@ var FXFiller = class _FXFiller {
|
|
|
22997
23737
|
async getOrderUsdValue(order) {
|
|
22998
23738
|
if (!this.limitOrders || order.inputs.length === 0) return null;
|
|
22999
23739
|
const factors = usdFactorsFrom(limitOrderUsdEdges(await this.limitOrders.open()));
|
|
23000
|
-
let inputUsd = new
|
|
23740
|
+
let inputUsd = new Decimal7(0);
|
|
23001
23741
|
for (const input of order.inputs) {
|
|
23002
23742
|
const inputToken = bytes32ToBytes204(input.token);
|
|
23003
23743
|
const symbol = this.registry.symbolFor(inputToken, order.source);
|
|
23004
23744
|
if (!symbol) return null;
|
|
23005
23745
|
const decimals = await this.contractService.getTokenDecimals(inputToken, order.source);
|
|
23006
|
-
const legUsd = usdValueOf(factors, symbol, new
|
|
23746
|
+
const legUsd = usdValueOf(factors, symbol, new Decimal7(formatUnits4(input.amount, decimals)));
|
|
23007
23747
|
if (!legUsd) return null;
|
|
23008
23748
|
inputUsd = inputUsd.plus(legUsd);
|
|
23009
23749
|
}
|
|
@@ -23987,8 +24727,8 @@ async function bootFiller(config, options) {
|
|
|
23987
24727
|
};
|
|
23988
24728
|
const configService = new FillerConfigService(resolvedChains, fillerConfigForService, options.loggers);
|
|
23989
24729
|
const chainConfigs = resolvedChains.map((chain) => {
|
|
23990
|
-
const
|
|
23991
|
-
return configService.getChainConfig(
|
|
24730
|
+
const chainName2 = `EVM-${chain.chainId}`;
|
|
24731
|
+
return configService.getChainConfig(chainName2);
|
|
23992
24732
|
});
|
|
23993
24733
|
let watchOnlyConfig;
|
|
23994
24734
|
const globalWatchOnly = options.watchOnlyOverride === true || config.simplex.watchOnly === true;
|
|
@@ -24026,6 +24766,8 @@ async function bootFiller(config, options) {
|
|
|
24026
24766
|
"A signer is required unless every chain is watch-only: pass `signer` to Simplex.start (privateKeySigner, secretPhraseSigner, turnkeySigner, mpcVaultSigner, viemSigner, or your own Signer implementation). Running the binary? Configure it under [simplex.signer]."
|
|
24027
24767
|
);
|
|
24028
24768
|
}
|
|
24769
|
+
logger.info("Checking each bundler serves its chain's EntryPoint...");
|
|
24770
|
+
await assertBundlersServeEntryPoint(resolvedChains, configService, { watchOnly: watchOnlyConfig });
|
|
24029
24771
|
const chainClientManager = new ChainClientManager(configService, options.signer);
|
|
24030
24772
|
const runtimeSigner = chainClientManager.getSigner();
|
|
24031
24773
|
if (options.signer) {
|
|
@@ -24188,9 +24930,10 @@ async function bootFiller(config, options) {
|
|
|
24188
24930
|
const activity = new ActivityRecorder(options.data.activity, options.loggers, { describeToken });
|
|
24189
24931
|
activity.attach(intentFiller.monitor);
|
|
24190
24932
|
const network = resolvedChains.some((chain) => chainByChainId(chain.chainId)?.network === "testnet") ? "testnet" : "mainnet";
|
|
24933
|
+
const indexerUrl = config.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network];
|
|
24191
24934
|
void backfillOrderSummaries({
|
|
24192
24935
|
store: options.data.activity,
|
|
24193
|
-
indexerUrl
|
|
24936
|
+
indexerUrl,
|
|
24194
24937
|
fillerAddress: runtimeSigner.address,
|
|
24195
24938
|
describeToken,
|
|
24196
24939
|
onUpdated: (rows) => {
|
|
@@ -24215,10 +24958,10 @@ async function bootFiller(config, options) {
|
|
|
24215
24958
|
const token1 = {};
|
|
24216
24959
|
for (const pair of config.pairs ?? []) {
|
|
24217
24960
|
if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
|
|
24218
|
-
for (const
|
|
24219
|
-
const address = assetRegistry.getAddress(pair.token1,
|
|
24961
|
+
for (const chainName2 of configuredChainNames) {
|
|
24962
|
+
const address = assetRegistry.getAddress(pair.token1, chainName2);
|
|
24220
24963
|
if (!address) continue;
|
|
24221
|
-
const list = token1[
|
|
24964
|
+
const list = token1[chainName2] ?? (token1[chainName2] = []);
|
|
24222
24965
|
if (!list.includes(address)) list.push(address);
|
|
24223
24966
|
}
|
|
24224
24967
|
}
|
|
@@ -24256,6 +24999,15 @@ async function bootFiller(config, options) {
|
|
|
24256
24999
|
vaultVenue?.startSweeping();
|
|
24257
25000
|
started.push(() => balanceProvider.stop());
|
|
24258
25001
|
await balanceProvider.start();
|
|
25002
|
+
if (options.signer) {
|
|
25003
|
+
const inventoryRecorder = new InventoryRecorder(
|
|
25004
|
+
options.data.inventory,
|
|
25005
|
+
balanceProvider,
|
|
25006
|
+
moduleLogger(options.loggers, "inventory")
|
|
25007
|
+
);
|
|
25008
|
+
started.push(() => inventoryRecorder.stop());
|
|
25009
|
+
inventoryRecorder.start();
|
|
25010
|
+
}
|
|
24259
25011
|
const watchOnlyChains = watchOnlyConfig ? Object.entries(watchOnlyConfig).filter(([, value]) => value === true).map(([chainId]) => Number.parseInt(chainId, 10)) : [];
|
|
24260
25012
|
logger.info(
|
|
24261
25013
|
{
|
|
@@ -24301,6 +25053,8 @@ async function bootFiller(config, options) {
|
|
|
24301
25053
|
rebalancingService,
|
|
24302
25054
|
resolvedChains,
|
|
24303
25055
|
fillerAddress: runtimeSigner.address,
|
|
25056
|
+
indexerUrl,
|
|
25057
|
+
describeToken,
|
|
24304
25058
|
watchOnly: watchOnlyConfig,
|
|
24305
25059
|
config,
|
|
24306
25060
|
configPath: options.configPath,
|
|
@@ -24575,10 +25329,20 @@ var MemoryLimitOrderStore = class {
|
|
|
24575
25329
|
commitment: fill.commitment,
|
|
24576
25330
|
bid: fill.bid ?? null,
|
|
24577
25331
|
amount: fill.amount,
|
|
25332
|
+
amountIn: fill.amountIn ?? null,
|
|
24578
25333
|
transactionHash: fill.transactionHash ?? null,
|
|
24579
25334
|
filledAt: sqliteDatetime(/* @__PURE__ */ new Date())
|
|
24580
25335
|
});
|
|
24581
25336
|
}
|
|
25337
|
+
async fillHistory() {
|
|
25338
|
+
return this.fillRows.flatMap((fill) => {
|
|
25339
|
+
const order = this.orders.get(fill.limitOrderId);
|
|
25340
|
+
if (!order) return [];
|
|
25341
|
+
const { id, limitOrderId, amount, amountIn, filledAt } = fill;
|
|
25342
|
+
const { book, base, quote, side, price } = order;
|
|
25343
|
+
return [{ id, limitOrderId, book, base, quote, side, price, amount, amountIn, filledAt }];
|
|
25344
|
+
});
|
|
25345
|
+
}
|
|
24582
25346
|
async fills(limitOrderId, limit = 100) {
|
|
24583
25347
|
return this.fillRows.filter((fill) => fill.limitOrderId === limitOrderId).slice(-capLimit(limit)).reverse().map((fill) => ({ ...fill }));
|
|
24584
25348
|
}
|
|
@@ -24593,6 +25357,7 @@ var MemoryLimitOrderStore = class {
|
|
|
24593
25357
|
status: "open",
|
|
24594
25358
|
commitment: null,
|
|
24595
25359
|
orderNonce: order.orderNonce ?? "0",
|
|
25360
|
+
entryPoint: null,
|
|
24596
25361
|
bookExpiresAt: null,
|
|
24597
25362
|
bookPrice: null,
|
|
24598
25363
|
lastError: null,
|
|
@@ -24685,12 +25450,29 @@ var MemoryStateStore = class {
|
|
|
24685
25450
|
this.state = { ...state };
|
|
24686
25451
|
}
|
|
24687
25452
|
};
|
|
25453
|
+
var MemoryInventoryStore = class {
|
|
25454
|
+
constructor() {
|
|
25455
|
+
this.rows = [];
|
|
25456
|
+
}
|
|
25457
|
+
async record(balances) {
|
|
25458
|
+
this.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(/* @__PURE__ */ new Date()), balances: { ...balances } });
|
|
25459
|
+
}
|
|
25460
|
+
async latest() {
|
|
25461
|
+
const last = this.rows[this.rows.length - 1];
|
|
25462
|
+
return last ? { ...last, balances: { ...last.balances } } : null;
|
|
25463
|
+
}
|
|
25464
|
+
async since(from) {
|
|
25465
|
+
const cutoff = sqliteDatetime(from);
|
|
25466
|
+
return this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }));
|
|
25467
|
+
}
|
|
25468
|
+
};
|
|
24688
25469
|
var MemoryDataStore = class {
|
|
24689
25470
|
constructor() {
|
|
24690
25471
|
this.bids = new MemoryBidStore();
|
|
24691
25472
|
this.activity = new MemoryActivityStore();
|
|
24692
25473
|
this.state = new MemoryStateStore();
|
|
24693
25474
|
this.limitOrders = new MemoryLimitOrderStore();
|
|
25475
|
+
this.inventory = new MemoryInventoryStore();
|
|
24694
25476
|
}
|
|
24695
25477
|
};
|
|
24696
25478
|
|
|
@@ -25295,6 +26077,106 @@ var OrderScanner = class _OrderScanner {
|
|
|
25295
26077
|
}
|
|
25296
26078
|
};
|
|
25297
26079
|
|
|
26080
|
+
// src/data/indexer-inventory.ts
|
|
26081
|
+
import { formatUnits as formatUnits7 } from "viem";
|
|
26082
|
+
var READ_TIMEOUT_MS = 1e4;
|
|
26083
|
+
var SETTLED_AFTER_MS = 10 * 6e4;
|
|
26084
|
+
var BATCH = 40;
|
|
26085
|
+
var MAX_KEPT = 2e3;
|
|
26086
|
+
var IndexerInventory = class {
|
|
26087
|
+
constructor(options) {
|
|
26088
|
+
this.options = options;
|
|
26089
|
+
this.kept = /* @__PURE__ */ new Map();
|
|
26090
|
+
this.described = /* @__PURE__ */ new Map();
|
|
26091
|
+
this.fetch = options.fetch ?? fetch;
|
|
26092
|
+
}
|
|
26093
|
+
/**
|
|
26094
|
+
* A reading for each of `instants` the indexer could answer for. Never rejects: an instant it
|
|
26095
|
+
* could not be asked about is simply missing from the answer.
|
|
26096
|
+
*/
|
|
26097
|
+
async at(instants, now) {
|
|
26098
|
+
const readings = [];
|
|
26099
|
+
const wanted = [];
|
|
26100
|
+
for (const instant of new Set(instants)) {
|
|
26101
|
+
const kept = this.kept.get(instant);
|
|
26102
|
+
if (kept) readings.push(kept);
|
|
26103
|
+
else wanted.push(instant);
|
|
26104
|
+
}
|
|
26105
|
+
try {
|
|
26106
|
+
for (let index = 0; index < wanted.length; index += BATCH) {
|
|
26107
|
+
for (const reading of await this.read(wanted.slice(index, index + BATCH))) {
|
|
26108
|
+
readings.push(reading);
|
|
26109
|
+
if (reading.at <= now - SETTLED_AFTER_MS) this.keep(reading);
|
|
26110
|
+
}
|
|
26111
|
+
}
|
|
26112
|
+
} catch (err) {
|
|
26113
|
+
this.options.logger.warn(
|
|
26114
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
26115
|
+
"Could not read inventory from the indexer; carrying records by fills and sends alone"
|
|
26116
|
+
);
|
|
26117
|
+
}
|
|
26118
|
+
return readings;
|
|
26119
|
+
}
|
|
26120
|
+
keep(reading) {
|
|
26121
|
+
if (this.kept.size >= MAX_KEPT) this.kept.clear();
|
|
26122
|
+
this.kept.set(reading.at, reading);
|
|
26123
|
+
}
|
|
26124
|
+
/** One request for several instants, each its own aliased query at that moment in history. */
|
|
26125
|
+
async read(instants) {
|
|
26126
|
+
const queries = instants.map(
|
|
26127
|
+
(instant, index) => `r${index}: solverInventories(filter: { solver: { equalTo: $solver } }, blockHeight: "${Math.floor(instant)}", first: 1000) { nodes { chain tokenAddress balance } }`
|
|
26128
|
+
);
|
|
26129
|
+
const response = await this.fetch(this.options.indexerUrl, {
|
|
26130
|
+
method: "POST",
|
|
26131
|
+
headers: { "content-type": "application/json" },
|
|
26132
|
+
body: JSON.stringify({
|
|
26133
|
+
query: `query SolverInventory($solver: String!) {
|
|
26134
|
+
${queries.join("\n")}
|
|
26135
|
+
}`,
|
|
26136
|
+
variables: { solver: this.options.solver.toLowerCase() }
|
|
26137
|
+
}),
|
|
26138
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
26139
|
+
});
|
|
26140
|
+
if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
|
|
26141
|
+
const body = await response.json();
|
|
26142
|
+
if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
|
|
26143
|
+
const readings = [];
|
|
26144
|
+
for (const [index, instant] of instants.entries()) {
|
|
26145
|
+
const rows = body.data?.[`r${index}`]?.nodes;
|
|
26146
|
+
if (rows) readings.push(await this.readingOf(instant, rows));
|
|
26147
|
+
}
|
|
26148
|
+
return readings;
|
|
26149
|
+
}
|
|
26150
|
+
async readingOf(at, rows) {
|
|
26151
|
+
const balances = {};
|
|
26152
|
+
const chains = {};
|
|
26153
|
+
for (const row of rows) {
|
|
26154
|
+
const token = await this.describe(row.chain, row.tokenAddress);
|
|
26155
|
+
if (!token) continue;
|
|
26156
|
+
let amount;
|
|
26157
|
+
try {
|
|
26158
|
+
amount = Number(formatUnits7(BigInt(row.balance), token.decimals));
|
|
26159
|
+
} catch {
|
|
26160
|
+
continue;
|
|
26161
|
+
}
|
|
26162
|
+
balances[token.symbol] = (balances[token.symbol] ?? 0) + amount;
|
|
26163
|
+
chains[token.symbol] = [...chains[token.symbol] ?? [], row.chain];
|
|
26164
|
+
}
|
|
26165
|
+
return { at, balances, chains };
|
|
26166
|
+
}
|
|
26167
|
+
/** A token's symbol and decimals, remembered once both are known. */
|
|
26168
|
+
async describe(chain, token) {
|
|
26169
|
+
const key = `${chain}:${token.toLowerCase()}`;
|
|
26170
|
+
const known = this.described.get(key);
|
|
26171
|
+
if (known) return known;
|
|
26172
|
+
const { symbol, decimals } = await this.options.describeToken(chain, token);
|
|
26173
|
+
if (!symbol || decimals === null) return null;
|
|
26174
|
+
const description = { symbol, decimals };
|
|
26175
|
+
this.described.set(key, description);
|
|
26176
|
+
return description;
|
|
26177
|
+
}
|
|
26178
|
+
};
|
|
26179
|
+
|
|
25298
26180
|
// src/simplex.ts
|
|
25299
26181
|
var EVENT_MAP = {
|
|
25300
26182
|
newOrder: "order:detected",
|
|
@@ -25340,10 +26222,66 @@ var LimitOrderController = class {
|
|
|
25340
26222
|
]);
|
|
25341
26223
|
return { order, fills, bids };
|
|
25342
26224
|
}
|
|
26225
|
+
/**
|
|
26226
|
+
* What the operator's buys and sells earned over `period`, bucketed on a clock
|
|
26227
|
+
* `tzOffsetMinutes` behind UTC.
|
|
26228
|
+
*
|
|
26229
|
+
* Profit is the change the period's fills made to what the solver holds, and
|
|
26230
|
+
* it is compared with the inventory the period began with. That inventory
|
|
26231
|
+
* comes from the stored snapshots and, when the balances have been read in
|
|
26232
|
+
* full, from what is held now, each carried to the period's start by what
|
|
26233
|
+
* moved between. The indexer says what moved, deposits and withdrawals
|
|
26234
|
+
* included, wherever it has followed the solver; elsewhere the fills and
|
|
26235
|
+
* sends on record stand in. It answers whether or not an orderbook is
|
|
26236
|
+
* configured, and whether or not the indexer does.
|
|
26237
|
+
*/
|
|
26238
|
+
async profitability(period, tzOffsetMinutes = 0) {
|
|
26239
|
+
const now = Date.now();
|
|
26240
|
+
const { data, balanceProvider } = this.runtime;
|
|
26241
|
+
const [fills, snapshots, ledger] = await Promise.all([
|
|
26242
|
+
data.limitOrders.fillHistory(),
|
|
26243
|
+
data.inventory.since(/* @__PURE__ */ new Date(0)),
|
|
26244
|
+
data.activity.walletTxs(500)
|
|
26245
|
+
]);
|
|
26246
|
+
const inventory = snapshots.map((snapshot) => ({
|
|
26247
|
+
at: parseUtc(snapshot.takenAt),
|
|
26248
|
+
balances: snapshot.balances
|
|
26249
|
+
}));
|
|
26250
|
+
const held = inventoryOf(balanceProvider.getSnapshot());
|
|
26251
|
+
if (held) inventory.push({ at: now, balances: held, live: true });
|
|
26252
|
+
const outflows = [];
|
|
26253
|
+
for (const tx of ledger) {
|
|
26254
|
+
const amount = Number(tx.amount);
|
|
26255
|
+
if (tx.kind === "send" && tx.token && Number.isFinite(amount) && amount > 0) {
|
|
26256
|
+
outflows.push({ at: tx.ts, symbol: tx.token, amount });
|
|
26257
|
+
}
|
|
26258
|
+
}
|
|
26259
|
+
const instants = inventoryInstants(fills, { period, now, tzOffsetMinutes, inventory });
|
|
26260
|
+
const readings = await this.indexerInventory().at(instants, now);
|
|
26261
|
+
return summarizeProfit(fills, { period, now, tzOffsetMinutes, inventory, outflows, readings });
|
|
26262
|
+
}
|
|
26263
|
+
/** One reader for the life of the filler, so the readings it has already fetched are kept. */
|
|
26264
|
+
indexerInventory() {
|
|
26265
|
+
this.indexed ?? (this.indexed = new IndexerInventory({
|
|
26266
|
+
indexerUrl: this.runtime.indexerUrl,
|
|
26267
|
+
solver: this.runtime.fillerAddress,
|
|
26268
|
+
describeToken: this.runtime.describeToken,
|
|
26269
|
+
logger: this.runtime.loggers.get("inventory")
|
|
26270
|
+
}));
|
|
26271
|
+
return this.indexed;
|
|
26272
|
+
}
|
|
25343
26273
|
/** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
25344
26274
|
books() {
|
|
25345
26275
|
return this.service.books();
|
|
25346
26276
|
}
|
|
26277
|
+
/** Public bids and asks across solvers, optionally restricted to a route. */
|
|
26278
|
+
orderbookSnapshot(book, filters = {}) {
|
|
26279
|
+
return this.service.orderbookSnapshot(book, filters);
|
|
26280
|
+
}
|
|
26281
|
+
/** Every solver's order behind one public price level. */
|
|
26282
|
+
orderbookLevelOrders(book, level) {
|
|
26283
|
+
return this.service.orderbookLevelOrders(book, level);
|
|
26284
|
+
}
|
|
25347
26285
|
/** Creates the order, posts it, and reports what the orderbook made of it. */
|
|
25348
26286
|
async create(request) {
|
|
25349
26287
|
const posted = await this.service.create(request);
|
|
@@ -25405,10 +26343,10 @@ var PairController = class {
|
|
|
25405
26343
|
if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
|
|
25406
26344
|
if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
|
|
25407
26345
|
for (const chain of this.runtime.resolvedChains) {
|
|
25408
|
-
const
|
|
25409
|
-
const address = assetRegistry.getAddress(pair.token1,
|
|
26346
|
+
const chainName2 = formatChainKey(chain.chainId);
|
|
26347
|
+
const address = assetRegistry.getAddress(pair.token1, chainName2);
|
|
25410
26348
|
if (!address) continue;
|
|
25411
|
-
const list = balanceTokens[
|
|
26349
|
+
const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
|
|
25412
26350
|
if (!list.includes(address)) list.push(address);
|
|
25413
26351
|
}
|
|
25414
26352
|
}
|
|
@@ -25513,8 +26451,9 @@ var ChainController = class {
|
|
|
25513
26451
|
`Chain ${chainId} cannot be added as filling: this solver was started without a signer. Restart with \`signer\` to fill.`
|
|
25514
26452
|
);
|
|
25515
26453
|
}
|
|
25516
|
-
configService.addChain(resolved);
|
|
25517
26454
|
const watchOnly = chain.watchOnly ?? (this.runtime.signerless || this.runtime.globalWatchOnly);
|
|
26455
|
+
await assertBundlersServeEntryPoint([resolved], configService, { watchOnly: { [chainId]: watchOnly } });
|
|
26456
|
+
configService.addChain(resolved);
|
|
25518
26457
|
if (watchOnly) intentFiller.setWatchOnly(chainId, true);
|
|
25519
26458
|
try {
|
|
25520
26459
|
if (!inScanner) {
|
|
@@ -25606,6 +26545,9 @@ var ChainController = class {
|
|
|
25606
26545
|
if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
|
|
25607
26546
|
const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
|
|
25608
26547
|
if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
|
|
26548
|
+
await assertBundlersServeEntryPoint([{ chainId, bundlerUrl }], this.runtime.configService, {
|
|
26549
|
+
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26550
|
+
});
|
|
25609
26551
|
this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
|
|
25610
26552
|
this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
|
|
25611
26553
|
this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
|
|
@@ -25620,6 +26562,10 @@ var ChainController = class {
|
|
|
25620
26562
|
"Watch-only cannot be disabled: this solver was started without a signer. Restart with `signer` to fill."
|
|
25621
26563
|
);
|
|
25622
26564
|
}
|
|
26565
|
+
if (!watchOnly) {
|
|
26566
|
+
const chain = this.runtime.resolvedChains.find((resolved) => resolved.chainId === chainId);
|
|
26567
|
+
if (chain) await assertBundlersServeEntryPoint([chain], this.runtime.configService);
|
|
26568
|
+
}
|
|
25623
26569
|
this.runtime.intentFiller.setWatchOnly(chainId, watchOnly);
|
|
25624
26570
|
this.syncWatchOnlyToConfig();
|
|
25625
26571
|
await this.persist();
|