@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.cjs
CHANGED
|
@@ -636,7 +636,8 @@ var BUILTIN_ASSETS = {
|
|
|
636
636
|
ZARP: { resolve: (r, chain) => r.getAssetBySymbol(chain, "ZARP") },
|
|
637
637
|
EURC: { resolve: (r, chain) => r.getAssetBySymbol(chain, "EURC") },
|
|
638
638
|
XSGD: { resolve: (r, chain) => r.getAssetBySymbol(chain, "XSGD") },
|
|
639
|
-
TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") }
|
|
639
|
+
TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") },
|
|
640
|
+
BRIDGE: { resolve: (r, chain) => r.getAssetBySymbol(chain, "BRIDGE") }
|
|
640
641
|
};
|
|
641
642
|
var USD_STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "DAI"]);
|
|
642
643
|
function normalizeSymbol(symbol) {
|
|
@@ -774,7 +775,7 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
|
|
|
774
775
|
const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
|
|
775
776
|
for (const pair of pairs) {
|
|
776
777
|
for (const symbol of [pair.token0, pair.token1]) {
|
|
777
|
-
const resolvesSomewhere = chainNames.some((
|
|
778
|
+
const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
|
|
778
779
|
if (!resolvesSomewhere) {
|
|
779
780
|
throw new Error(
|
|
780
781
|
`pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
|
|
@@ -782,15 +783,15 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
|
|
|
782
783
|
}
|
|
783
784
|
}
|
|
784
785
|
}
|
|
785
|
-
for (const
|
|
786
|
+
for (const chainName2 of chainNames) {
|
|
786
787
|
const addressOwner = /* @__PURE__ */ new Map();
|
|
787
788
|
for (const symbol of pairSymbols) {
|
|
788
|
-
const address = registry.getAddress(symbol,
|
|
789
|
+
const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
|
|
789
790
|
if (!address) continue;
|
|
790
791
|
const owner = addressOwner.get(address);
|
|
791
792
|
if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
|
|
792
793
|
throw new Error(
|
|
793
|
-
`assets: '${symbol}' and '${owner}' both resolve to ${address} on ${
|
|
794
|
+
`assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
|
|
794
795
|
);
|
|
795
796
|
}
|
|
796
797
|
addressOwner.set(address, symbol);
|
|
@@ -1495,7 +1496,7 @@ var FillerConfigService = class {
|
|
|
1495
1496
|
return this.fillerConfig?.substratePrivateKey;
|
|
1496
1497
|
}
|
|
1497
1498
|
getEntryPointAddress(chain) {
|
|
1498
|
-
return this.chainConfigService.
|
|
1499
|
+
return this.chainConfigService.getEntryPointAddress(chain);
|
|
1499
1500
|
}
|
|
1500
1501
|
getSolverAccountContractAddress(chain) {
|
|
1501
1502
|
return this.chainConfigService.getSolverAccountAddress(chain);
|
|
@@ -2544,6 +2545,13 @@ var FREE_RPC_URLS = {
|
|
|
2544
2545
|
"https://arb1.arbitrum.io/rpc"
|
|
2545
2546
|
]
|
|
2546
2547
|
};
|
|
2548
|
+
var HYPERBRIDGE_BUNDLER_URLS = {
|
|
2549
|
+
1: "https://bundler.polytope.technology/ethereum",
|
|
2550
|
+
56: "https://bundler.polytope.technology/bsc",
|
|
2551
|
+
137: "https://bundler.polytope.technology/polygon",
|
|
2552
|
+
8453: "https://bundler.polytope.technology/base",
|
|
2553
|
+
42161: "https://bundler.polytope.technology/arbitrum"
|
|
2554
|
+
};
|
|
2547
2555
|
var CHAIN_NATIVE_SYMBOLS = {
|
|
2548
2556
|
1: "ETH",
|
|
2549
2557
|
56: "BNB",
|
|
@@ -2572,7 +2580,8 @@ var INIT_CHAINS = [
|
|
|
2572
2580
|
network: "mainnet",
|
|
2573
2581
|
alchemySubdomain: "eth-mainnet",
|
|
2574
2582
|
explorerUrl: external_links_default.chainExplorers.ethereum,
|
|
2575
|
-
defaultRpcUrls: FREE_RPC_URLS[1]
|
|
2583
|
+
defaultRpcUrls: FREE_RPC_URLS[1],
|
|
2584
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[1]
|
|
2576
2585
|
},
|
|
2577
2586
|
{
|
|
2578
2587
|
chainId: 42161,
|
|
@@ -2581,7 +2590,8 @@ var INIT_CHAINS = [
|
|
|
2581
2590
|
network: "mainnet",
|
|
2582
2591
|
alchemySubdomain: "arb-mainnet",
|
|
2583
2592
|
explorerUrl: external_links_default.chainExplorers.arbitrum,
|
|
2584
|
-
defaultRpcUrls: FREE_RPC_URLS[42161]
|
|
2593
|
+
defaultRpcUrls: FREE_RPC_URLS[42161],
|
|
2594
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[42161]
|
|
2585
2595
|
},
|
|
2586
2596
|
{
|
|
2587
2597
|
chainId: 8453,
|
|
@@ -2590,7 +2600,8 @@ var INIT_CHAINS = [
|
|
|
2590
2600
|
network: "mainnet",
|
|
2591
2601
|
alchemySubdomain: "base-mainnet",
|
|
2592
2602
|
explorerUrl: external_links_default.chainExplorers.base,
|
|
2593
|
-
defaultRpcUrls: FREE_RPC_URLS[8453]
|
|
2603
|
+
defaultRpcUrls: FREE_RPC_URLS[8453],
|
|
2604
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[8453]
|
|
2594
2605
|
},
|
|
2595
2606
|
{
|
|
2596
2607
|
chainId: 137,
|
|
@@ -2599,7 +2610,8 @@ var INIT_CHAINS = [
|
|
|
2599
2610
|
network: "mainnet",
|
|
2600
2611
|
alchemySubdomain: "polygon-mainnet",
|
|
2601
2612
|
explorerUrl: external_links_default.chainExplorers.polygon,
|
|
2602
|
-
defaultRpcUrls: FREE_RPC_URLS[137]
|
|
2613
|
+
defaultRpcUrls: FREE_RPC_URLS[137],
|
|
2614
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[137]
|
|
2603
2615
|
},
|
|
2604
2616
|
{
|
|
2605
2617
|
chainId: 56,
|
|
@@ -2608,7 +2620,8 @@ var INIT_CHAINS = [
|
|
|
2608
2620
|
network: "mainnet",
|
|
2609
2621
|
alchemySubdomain: "bnb-mainnet",
|
|
2610
2622
|
explorerUrl: external_links_default.chainExplorers.bnb,
|
|
2611
|
-
defaultRpcUrls: FREE_RPC_URLS[56]
|
|
2623
|
+
defaultRpcUrls: FREE_RPC_URLS[56],
|
|
2624
|
+
hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[56]
|
|
2612
2625
|
},
|
|
2613
2626
|
{
|
|
2614
2627
|
chainId: 11155111,
|
|
@@ -2677,6 +2690,35 @@ var LIMITS_QUERY = `
|
|
|
2677
2690
|
chains { id name tokens { symbol decimals } }
|
|
2678
2691
|
}
|
|
2679
2692
|
`;
|
|
2693
|
+
var LEVEL_FIELDS = "fillChain priceBucket price worstPrice baseSize quoteSize orderCount solverCount";
|
|
2694
|
+
var SNAPSHOT_QUERY = `
|
|
2695
|
+
query OrderbookSnapshot($id: ID!, $sourceChain: String, $fillChain: String) {
|
|
2696
|
+
book(id: $id) {
|
|
2697
|
+
id base quote
|
|
2698
|
+
bids: levels(side: BID, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2699
|
+
asks: levels(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2700
|
+
bidLiquidity: availableLiquidity(side: BID, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2701
|
+
askLiquidity: availableLiquidity(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2702
|
+
}
|
|
2703
|
+
serverInfo { priceGranularities { book granularity } }
|
|
2704
|
+
}
|
|
2705
|
+
`;
|
|
2706
|
+
var LEVEL_ORDERS_QUERY = `
|
|
2707
|
+
query OrderbookLevelOrders($id: ID!, $side: Side!, $fillChain: String, $sourceChain: String, $after: String) {
|
|
2708
|
+
book(id: $id) {
|
|
2709
|
+
orders(side: $side, fillChain: $fillChain, sourceChain: $sourceChain, first: 50, after: $after) {
|
|
2710
|
+
edges {
|
|
2711
|
+
node {
|
|
2712
|
+
solver { address }
|
|
2713
|
+
commitment fillChain price priceBucket advertisedSize quotedAmount resized expiresAt acceptedSources
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
pageInfo { hasNextPage endCursor }
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
`;
|
|
2721
|
+
var LEVEL_ORDER_PAGES = 20;
|
|
2680
2722
|
var POSTED_ORDER_FIELDS = "commitment side status price quotedAmount advertisedSize expiresAt acceptedSources";
|
|
2681
2723
|
var SUBMIT_ORDER_MUTATION = `
|
|
2682
2724
|
mutation SubmitOrder($userOp: Bytes!) {
|
|
@@ -2741,6 +2783,44 @@ var OrderbookClient = class {
|
|
|
2741
2783
|
async limits() {
|
|
2742
2784
|
return this.request(LIMITS_QUERY, {});
|
|
2743
2785
|
}
|
|
2786
|
+
/** The public book across all solvers, restricted to the selected route. */
|
|
2787
|
+
async snapshot(id, filters = {}) {
|
|
2788
|
+
const { book, serverInfo } = await this.request(SNAPSHOT_QUERY, {
|
|
2789
|
+
id,
|
|
2790
|
+
sourceChain: filters.sourceChain || null,
|
|
2791
|
+
fillChain: filters.fillChain || null
|
|
2792
|
+
});
|
|
2793
|
+
if (!book) return null;
|
|
2794
|
+
const granularity = serverInfo.priceGranularities.find((entry) => entry.book === book.id)?.granularity ?? null;
|
|
2795
|
+
return { ...book, granularity };
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* The orders behind one price level. The orderbook filters orders by side and route but not by
|
|
2799
|
+
* price bucket, so this walks the side's orders on the level's fill chain and keeps the bucket.
|
|
2800
|
+
* Null when the book does not exist.
|
|
2801
|
+
*/
|
|
2802
|
+
async levelOrders(id, level) {
|
|
2803
|
+
const found = [];
|
|
2804
|
+
let after = null;
|
|
2805
|
+
for (let page = 0; page < LEVEL_ORDER_PAGES; page++) {
|
|
2806
|
+
const { book } = await this.request(LEVEL_ORDERS_QUERY, {
|
|
2807
|
+
id,
|
|
2808
|
+
side: level.side,
|
|
2809
|
+
fillChain: level.fillChain,
|
|
2810
|
+
sourceChain: level.sourceChain || null,
|
|
2811
|
+
after
|
|
2812
|
+
});
|
|
2813
|
+
if (!book) return null;
|
|
2814
|
+
for (const { node } of book.orders.edges) {
|
|
2815
|
+
if (node.priceBucket !== level.priceBucket) continue;
|
|
2816
|
+
const { solver, priceBucket: _bucket, ...order } = node;
|
|
2817
|
+
found.push({ ...order, solver: solver.address });
|
|
2818
|
+
}
|
|
2819
|
+
if (!book.orders.pageInfo.hasNextPage || !book.orders.pageInfo.endCursor) break;
|
|
2820
|
+
after = book.orders.pageInfo.endCursor;
|
|
2821
|
+
}
|
|
2822
|
+
return found;
|
|
2823
|
+
}
|
|
2744
2824
|
async submitOrder(userOp) {
|
|
2745
2825
|
const { submitOrder } = await this.request(SUBMIT_ORDER_MUTATION, { userOp });
|
|
2746
2826
|
switch (submitOrder.__typename) {
|
|
@@ -2925,16 +3005,16 @@ function signedAmounts(params) {
|
|
|
2925
3005
|
function offerFor(params) {
|
|
2926
3006
|
const { side, inputAmount, price, outputDecimals } = params;
|
|
2927
3007
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2928
|
-
const
|
|
3008
|
+
const scaled2 = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE : inputAmount * ORDERBOOK_SCALE / price;
|
|
2929
3009
|
const unit = outputDecimals >= 18 ? 1n : 10n ** BigInt(18 - outputDecimals);
|
|
2930
|
-
return
|
|
3010
|
+
return scaled2 / unit * unit;
|
|
2931
3011
|
}
|
|
2932
3012
|
function inputFor(params) {
|
|
2933
3013
|
const { side, outputAmount, price, inputDecimals } = params;
|
|
2934
3014
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2935
|
-
const
|
|
3015
|
+
const scaled2 = side === "BID" ? divCeil(outputAmount * ORDERBOOK_SCALE, price) : divCeil(outputAmount * price, ORDERBOOK_SCALE);
|
|
2936
3016
|
const unit = inputDecimals >= 18 ? 1n : 10n ** BigInt(18 - inputDecimals);
|
|
2937
|
-
return divCeil(
|
|
3017
|
+
return divCeil(scaled2, unit) * unit;
|
|
2938
3018
|
}
|
|
2939
3019
|
|
|
2940
3020
|
// src/orderbook/matching.ts
|
|
@@ -3055,6 +3135,8 @@ var LimitOrderService = class {
|
|
|
3055
3135
|
this.delegationService = delegationService;
|
|
3056
3136
|
this.startingNonce = startingNonce;
|
|
3057
3137
|
this.vaultBalances = vaultBalances;
|
|
3138
|
+
/** Re-signs in flight, by order id. A fill's resize waits one out rather than reposting alongside it. */
|
|
3139
|
+
this.resigning = /* @__PURE__ */ new Map();
|
|
3058
3140
|
this.logger = loggers.get("limit-orders");
|
|
3059
3141
|
}
|
|
3060
3142
|
/**
|
|
@@ -3148,6 +3230,7 @@ var LimitOrderService = class {
|
|
|
3148
3230
|
bookExpiresAt: null,
|
|
3149
3231
|
bookPrice: null,
|
|
3150
3232
|
orderNonce: order.orderNonce,
|
|
3233
|
+
entryPoint: null,
|
|
3151
3234
|
status: "cancelled",
|
|
3152
3235
|
lastError: null
|
|
3153
3236
|
}),
|
|
@@ -3190,6 +3273,14 @@ var LimitOrderService = class {
|
|
|
3190
3273
|
throw err;
|
|
3191
3274
|
}
|
|
3192
3275
|
}
|
|
3276
|
+
/** Public market levels read through the configured orderbook client. */
|
|
3277
|
+
orderbookSnapshot(book, filters = {}) {
|
|
3278
|
+
return this.client.snapshot(book, filters);
|
|
3279
|
+
}
|
|
3280
|
+
/** Every solver's order behind one public price level. */
|
|
3281
|
+
orderbookLevelOrders(book, level) {
|
|
3282
|
+
return this.client.levelOrders(book, level);
|
|
3283
|
+
}
|
|
3193
3284
|
/**
|
|
3194
3285
|
* The pairs an order can be written against, the smallest payout each token may carry, and
|
|
3195
3286
|
* the tokens the orderbook registers on each chain.
|
|
@@ -3244,16 +3335,16 @@ var LimitOrderService = class {
|
|
|
3244
3335
|
if (!this.configService.getConfiguredChainIds().includes((0, import_sdk4.getChainId)(request.fillChain) ?? -1)) {
|
|
3245
3336
|
throw new LimitOrderValidationError(`'${request.fillChain}' is not a chain this filler is configured for`);
|
|
3246
3337
|
}
|
|
3247
|
-
const
|
|
3338
|
+
const scaled2 = { amountIn: 0n, amountOut: 0n };
|
|
3248
3339
|
for (const name of ["amountIn", "amountOut"]) {
|
|
3249
3340
|
try {
|
|
3250
|
-
|
|
3341
|
+
scaled2[name] = fromHuman(request[name] ?? "");
|
|
3251
3342
|
} catch (err) {
|
|
3252
3343
|
throw new LimitOrderValidationError(
|
|
3253
3344
|
`${name} must be an amount in whole tokens, like "1000" or "1500.25"; ${err.message}`
|
|
3254
3345
|
);
|
|
3255
3346
|
}
|
|
3256
|
-
if (
|
|
3347
|
+
if (scaled2[name] <= 0n) throw new LimitOrderValidationError(`${name} must be greater than zero`);
|
|
3257
3348
|
}
|
|
3258
3349
|
const sources = request.acceptedSources ?? [];
|
|
3259
3350
|
if (sources.length === 0) {
|
|
@@ -3300,7 +3391,7 @@ var LimitOrderService = class {
|
|
|
3300
3391
|
);
|
|
3301
3392
|
}
|
|
3302
3393
|
const floor = limits.serverInfo.minOrderSizes.find((entry) => entry.symbol === request.tokenOut);
|
|
3303
|
-
if (floor &&
|
|
3394
|
+
if (floor && scaled2.amountOut < BigInt(floor.size)) {
|
|
3304
3395
|
throw new LimitOrderValidationError(
|
|
3305
3396
|
`amountOut is below the orderbook's dust floor for ${request.tokenOut} (${toHuman(BigInt(floor.size))})`
|
|
3306
3397
|
);
|
|
@@ -3310,7 +3401,7 @@ var LimitOrderService = class {
|
|
|
3310
3401
|
throw new LimitOrderValidationError(`'${symbol}' does not resolve to a token address on ${request.fillChain}`);
|
|
3311
3402
|
}
|
|
3312
3403
|
}
|
|
3313
|
-
return { amountIn:
|
|
3404
|
+
return { amountIn: scaled2.amountIn, amountOut: scaled2.amountOut };
|
|
3314
3405
|
}
|
|
3315
3406
|
/**
|
|
3316
3407
|
* Refuses an order the wallet cannot pay out.
|
|
@@ -3406,24 +3497,32 @@ var LimitOrderService = class {
|
|
|
3406
3497
|
async resize(remaining, delivered) {
|
|
3407
3498
|
const id = remaining.id;
|
|
3408
3499
|
const floor = await this.dustFloor(remaining);
|
|
3409
|
-
|
|
3500
|
+
for (let resigning = this.resigning.get(id); resigning; resigning = this.resigning.get(id)) await resigning;
|
|
3501
|
+
const order = await this.store.setStatus(id, "resizing", null, POSTABLE);
|
|
3502
|
+
if (!order) return null;
|
|
3503
|
+
if (BigInt(order.remaining) < floor) {
|
|
3410
3504
|
this.logger.info(
|
|
3411
|
-
{ id, remaining:
|
|
3505
|
+
{ id, remaining: order.remaining, floor: floor.toString() },
|
|
3412
3506
|
"Limit order worked down past the dust floor; closing it"
|
|
3413
3507
|
);
|
|
3414
|
-
if (
|
|
3415
|
-
const closed = await this.store.setPosting(
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3508
|
+
if (order.commitment) await this.withdraw(order.commitment);
|
|
3509
|
+
const closed = await this.store.setPosting(
|
|
3510
|
+
id,
|
|
3511
|
+
{
|
|
3512
|
+
commitment: null,
|
|
3513
|
+
bookExpiresAt: null,
|
|
3514
|
+
bookPrice: null,
|
|
3515
|
+
orderNonce: order.orderNonce,
|
|
3516
|
+
entryPoint: null,
|
|
3517
|
+
status: "filled",
|
|
3518
|
+
lastError: null
|
|
3519
|
+
},
|
|
3520
|
+
POSTABLE
|
|
3521
|
+
);
|
|
3423
3522
|
if (closed) this.report({ kind: "filled", order: closed });
|
|
3424
3523
|
return closed;
|
|
3425
3524
|
}
|
|
3426
|
-
const resized = await this.repost(
|
|
3525
|
+
const resized = await this.repost(order);
|
|
3427
3526
|
if (resized) this.report({ kind: "resized", order: resized, delivered });
|
|
3428
3527
|
return resized;
|
|
3429
3528
|
}
|
|
@@ -3436,7 +3535,7 @@ var LimitOrderService = class {
|
|
|
3436
3535
|
*/
|
|
3437
3536
|
async repost(order) {
|
|
3438
3537
|
if (!order) return null;
|
|
3439
|
-
if (isLocal(order)) return this.store.setStatus(order.id, "open");
|
|
3538
|
+
if (isLocal(order)) return this.store.setStatus(order.id, "open", null, POSTABLE);
|
|
3440
3539
|
if (hasExpired(order.expiresAt, /* @__PURE__ */ new Date())) return this.retire(order);
|
|
3441
3540
|
if (order.commitment) {
|
|
3442
3541
|
const withdrawn = await this.withdraw(order.commitment);
|
|
@@ -3447,10 +3546,13 @@ var LimitOrderService = class {
|
|
|
3447
3546
|
{ id: order.id, commitment: order.commitment, err: message },
|
|
3448
3547
|
"Could not clear the old entry; leaving the posting alone rather than adding a second"
|
|
3449
3548
|
);
|
|
3450
|
-
return this.store.setStatus(order.id, "open", message);
|
|
3549
|
+
return this.store.setStatus(order.id, "open", message, POSTABLE);
|
|
3451
3550
|
}
|
|
3452
3551
|
}
|
|
3453
|
-
await this.store.setStatus(order.id, "resizing")
|
|
3552
|
+
if (!await this.store.setStatus(order.id, "resizing", null, POSTABLE)) {
|
|
3553
|
+
this.logger.info({ id: order.id }, "Limit order closed while it was being reposted; leaving it closed");
|
|
3554
|
+
return null;
|
|
3555
|
+
}
|
|
3454
3556
|
const nonce = (BigInt(order.orderNonce) + 1n).toString();
|
|
3455
3557
|
return (await this.post({ ...order, commitment: null, orderNonce: nonce })).order;
|
|
3456
3558
|
}
|
|
@@ -3493,7 +3595,9 @@ var LimitOrderService = class {
|
|
|
3493
3595
|
* They drift apart when a request never got an answer or the process died
|
|
3494
3596
|
* between two of them: an entry the orderbook still lists that nothing here
|
|
3495
3597
|
* owns, an order here whose entry has gone, or an entry the orderbook has cut
|
|
3496
|
-
* down because the solver cannot cover what it quoted.
|
|
3598
|
+
* down because the solver cannot cover what it quoted. A posted op is also
|
|
3599
|
+
* signed over one EntryPoint, so when the fill chain moves to another, every
|
|
3600
|
+
* entry signed before the move is withdrawn and posted again.
|
|
3497
3601
|
*/
|
|
3498
3602
|
async reconcile(now = /* @__PURE__ */ new Date()) {
|
|
3499
3603
|
const [entries, live] = await Promise.all([this.postedOrders(), this.live()]);
|
|
@@ -3520,7 +3624,17 @@ var LimitOrderService = class {
|
|
|
3520
3624
|
}
|
|
3521
3625
|
for (const order of live) {
|
|
3522
3626
|
if (isLocal(order)) continue;
|
|
3523
|
-
if (order.commitment && found.has(order.commitment.toLowerCase()))
|
|
3627
|
+
if (order.commitment && found.has(order.commitment.toLowerCase())) {
|
|
3628
|
+
try {
|
|
3629
|
+
if (await this.resign(order)) report.reposted++;
|
|
3630
|
+
} catch (err) {
|
|
3631
|
+
this.logger.error(
|
|
3632
|
+
{ id: order.id, err },
|
|
3633
|
+
"Could not sign the limit order again for its chain's EntryPoint"
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3524
3638
|
if (sinceMs(order.updatedAt, now) < POST_GRACE_MS) continue;
|
|
3525
3639
|
this.logger.warn({ id: order.id }, "Limit order has no entry on the orderbook; posting it again");
|
|
3526
3640
|
try {
|
|
@@ -3532,6 +3646,45 @@ var LimitOrderService = class {
|
|
|
3532
3646
|
}
|
|
3533
3647
|
return report;
|
|
3534
3648
|
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Withdraws a posting signed for an EntryPoint its fill chain no longer uses,
|
|
3651
|
+
* and posts it again signed for the one it uses now. Answers whether a new
|
|
3652
|
+
* posting landed.
|
|
3653
|
+
*
|
|
3654
|
+
* The row is read again and claimed out of `open` first: a fill can have worked
|
|
3655
|
+
* it down or started its own repost since reconciliation listed it, and that
|
|
3656
|
+
* write is the one to follow. A fill that lands once the claim is taken waits
|
|
3657
|
+
* in `resize` for this to finish. The EntryPoint is only written with a posting
|
|
3658
|
+
* that landed, so a pass cut short anywhere leaves a row the next pass still
|
|
3659
|
+
* reads as stale, or as having no entry, and finishes the job.
|
|
3660
|
+
*/
|
|
3661
|
+
async resign(listed) {
|
|
3662
|
+
const entryPoint = this.configService.getEntryPointAddress(listed.fillChain);
|
|
3663
|
+
if (!entryPoint || sameAddress(signedFor(listed), entryPoint)) return false;
|
|
3664
|
+
const order = await this.store.get(listed.id);
|
|
3665
|
+
if (!order || order.status !== "open" || order.commitment !== listed.commitment) return false;
|
|
3666
|
+
if (sameAddress(signedFor(order), entryPoint) || this.resigning.has(order.id)) return false;
|
|
3667
|
+
const resigned = this.signAgain(order, entryPoint);
|
|
3668
|
+
this.resigning.set(
|
|
3669
|
+
order.id,
|
|
3670
|
+
resigned.catch(() => false)
|
|
3671
|
+
);
|
|
3672
|
+
try {
|
|
3673
|
+
return await resigned;
|
|
3674
|
+
} finally {
|
|
3675
|
+
this.resigning.delete(order.id);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
async signAgain(order, entryPoint) {
|
|
3679
|
+
const claimed = await this.store.setStatus(order.id, "resizing", null, ["open"]);
|
|
3680
|
+
if (!claimed) return false;
|
|
3681
|
+
this.logger.warn(
|
|
3682
|
+
{ id: order.id, commitment: order.commitment, signedFor: signedFor(order), entryPoint },
|
|
3683
|
+
"Limit order was signed for an EntryPoint its chain no longer uses; signing it again"
|
|
3684
|
+
);
|
|
3685
|
+
const posted = await this.repost(claimed);
|
|
3686
|
+
return !!posted?.commitment && sameAddress(signedFor(posted), entryPoint);
|
|
3687
|
+
}
|
|
3535
3688
|
/**
|
|
3536
3689
|
* Lowers every open order's `remaining` to what the solver's account will still
|
|
3537
3690
|
* pay for it, and puts the order back on the book at that size.
|
|
@@ -3613,6 +3766,7 @@ var LimitOrderService = class {
|
|
|
3613
3766
|
bookExpiresAt: null,
|
|
3614
3767
|
bookPrice: null,
|
|
3615
3768
|
orderNonce: order.orderNonce,
|
|
3769
|
+
entryPoint: null,
|
|
3616
3770
|
status: "expired",
|
|
3617
3771
|
lastError: null
|
|
3618
3772
|
});
|
|
@@ -3669,11 +3823,11 @@ var LimitOrderService = class {
|
|
|
3669
3823
|
}
|
|
3670
3824
|
/** Builds and submits the posting, then writes the orderbook's answer onto the row. */
|
|
3671
3825
|
async post(order) {
|
|
3672
|
-
const { result, orderNonce } = await this.submit(order);
|
|
3826
|
+
const { result, orderNonce, entryPoint } = await this.submit(order);
|
|
3673
3827
|
if (result.kind === "accepted" || result.kind === "unchanged") {
|
|
3674
3828
|
const posted = result.order;
|
|
3675
3829
|
this.logger.info(
|
|
3676
|
-
{ id: order.id, commitment: posted.commitment, price: posted.price },
|
|
3830
|
+
{ id: order.id, commitment: posted.commitment, price: posted.price, entryPoint },
|
|
3677
3831
|
"Limit order posted to the orderbook"
|
|
3678
3832
|
);
|
|
3679
3833
|
const stored = await this.store.setPosting(
|
|
@@ -3683,6 +3837,10 @@ var LimitOrderService = class {
|
|
|
3683
3837
|
bookExpiresAt: posted.expiresAt,
|
|
3684
3838
|
bookPrice: posted.price,
|
|
3685
3839
|
orderNonce: orderNonce.toString(),
|
|
3840
|
+
// An entry the orderbook already held was signed by an earlier attempt,
|
|
3841
|
+
// for whichever EntryPoint was current then. Recorded as unknown, which
|
|
3842
|
+
// reconciliation reads as v0.8 and signs again once the chain is past it.
|
|
3843
|
+
entryPoint: result.kind === "accepted" ? entryPoint : null,
|
|
3686
3844
|
status: "open",
|
|
3687
3845
|
lastError: null
|
|
3688
3846
|
},
|
|
@@ -3712,6 +3870,7 @@ var LimitOrderService = class {
|
|
|
3712
3870
|
bookExpiresAt: null,
|
|
3713
3871
|
bookPrice: null,
|
|
3714
3872
|
orderNonce: orderNonce.toString(),
|
|
3873
|
+
entryPoint: null,
|
|
3715
3874
|
status: "open",
|
|
3716
3875
|
lastError: message
|
|
3717
3876
|
}),
|
|
@@ -3734,22 +3893,30 @@ var LimitOrderService = class {
|
|
|
3734
3893
|
* only the second one.
|
|
3735
3894
|
*/
|
|
3736
3895
|
async submit(order) {
|
|
3896
|
+
const entryPoint = this.entryPointFor(order.fillChain);
|
|
3737
3897
|
const orderNonce = BigInt(order.orderNonce);
|
|
3738
|
-
const attempt = await this.buildAndSubmit(order, orderNonce);
|
|
3898
|
+
const attempt = await this.buildAndSubmit(order, orderNonce, entryPoint);
|
|
3739
3899
|
const first = attempt.result;
|
|
3740
3900
|
if (first.kind !== "rejected" || first.code !== "REPLAYED" && first.code !== "ORDER_EXISTS") {
|
|
3741
|
-
return { result: first, orderNonce };
|
|
3901
|
+
return { result: first, orderNonce, entryPoint };
|
|
3742
3902
|
}
|
|
3743
3903
|
if (first.code === "ORDER_EXISTS" && attempt.commitment) {
|
|
3744
3904
|
const live = await this.entryAt(order, attempt.commitment);
|
|
3745
3905
|
if (live) {
|
|
3746
3906
|
this.logger.info({ id: order.id, commitment: live.commitment }, "This posting is already on the orderbook");
|
|
3747
|
-
return { result: { kind: "unchanged", order: live }, orderNonce };
|
|
3907
|
+
return { result: { kind: "unchanged", order: live }, orderNonce, entryPoint };
|
|
3748
3908
|
}
|
|
3749
3909
|
}
|
|
3750
3910
|
this.logger.warn({ id: order.id, code: first.code }, "Orderbook has seen this op before; reposting on a new nonce");
|
|
3751
3911
|
const retried = orderNonce + 1n;
|
|
3752
|
-
|
|
3912
|
+
const { result } = await this.buildAndSubmit(order, retried, entryPoint);
|
|
3913
|
+
return { result, orderNonce: retried, entryPoint };
|
|
3914
|
+
}
|
|
3915
|
+
/** The EntryPoint a posting on `chain` is signed for now. */
|
|
3916
|
+
entryPointFor(chain) {
|
|
3917
|
+
const entryPoint = this.configService.getEntryPointAddress(chain);
|
|
3918
|
+
if (!entryPoint) throw new LimitOrderValidationError(`No EntryPoint is configured for ${chain}`);
|
|
3919
|
+
return entryPoint;
|
|
3753
3920
|
}
|
|
3754
3921
|
/**
|
|
3755
3922
|
* The entry the orderbook holds at `commitment`, if it holds one. A lookup that
|
|
@@ -3765,8 +3932,8 @@ var LimitOrderService = class {
|
|
|
3765
3932
|
}
|
|
3766
3933
|
}
|
|
3767
3934
|
/** The orderbook's answer, and the commitment the op we sent hashes to. */
|
|
3768
|
-
async buildAndSubmit(order, orderNonce) {
|
|
3769
|
-
const { commitment, userOp } = await this.buildUserOp(order, orderNonce);
|
|
3935
|
+
async buildAndSubmit(order, orderNonce, entryPoint) {
|
|
3936
|
+
const { commitment, userOp } = await this.buildUserOp(order, orderNonce, entryPoint);
|
|
3770
3937
|
try {
|
|
3771
3938
|
return { result: await this.client.submitOrder(userOp), commitment };
|
|
3772
3939
|
} catch (err) {
|
|
@@ -3776,7 +3943,7 @@ var LimitOrderService = class {
|
|
|
3776
3943
|
throw err;
|
|
3777
3944
|
}
|
|
3778
3945
|
}
|
|
3779
|
-
async buildUserOp(order, orderNonce) {
|
|
3946
|
+
async buildUserOp(order, orderNonce, entryPointAddress) {
|
|
3780
3947
|
const baseToken = this.assetRegistry.getAddress(order.base, order.fillChain);
|
|
3781
3948
|
const quoteToken = this.assetRegistry.getAddress(order.quote, order.fillChain);
|
|
3782
3949
|
const [baseDecimals, quoteDecimals] = await Promise.all([
|
|
@@ -3791,10 +3958,6 @@ var LimitOrderService = class {
|
|
|
3791
3958
|
baseDecimals,
|
|
3792
3959
|
quoteDecimals
|
|
3793
3960
|
});
|
|
3794
|
-
const entryPointAddress = this.configService.getEntryPointAddress(order.fillChain);
|
|
3795
|
-
if (!entryPointAddress) {
|
|
3796
|
-
throw new LimitOrderValidationError(`No EntryPoint is configured for ${order.fillChain}`);
|
|
3797
|
-
}
|
|
3798
3961
|
return this.contractService.prepareLimitOrderUserOp({
|
|
3799
3962
|
fillChain: order.fillChain,
|
|
3800
3963
|
entryPointAddress,
|
|
@@ -3825,6 +3988,12 @@ function hasExpired(expiresAt, now) {
|
|
|
3825
3988
|
const at = Date.parse(expiresAt);
|
|
3826
3989
|
return !Number.isNaN(at) && at <= now.getTime();
|
|
3827
3990
|
}
|
|
3991
|
+
function signedFor(order) {
|
|
3992
|
+
return order.entryPoint ?? import_sdk4.ENTRY_POINT_V08;
|
|
3993
|
+
}
|
|
3994
|
+
function sameAddress(a, b) {
|
|
3995
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
3996
|
+
}
|
|
3828
3997
|
function sinceMs(updatedAt, now) {
|
|
3829
3998
|
const written = Date.parse(`${updatedAt.replace(" ", "T")}Z`);
|
|
3830
3999
|
return Number.isNaN(written) ? Number.POSITIVE_INFINITY : now.getTime() - written;
|
|
@@ -3836,6 +4005,451 @@ function underFunded(entry) {
|
|
|
3836
4005
|
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";
|
|
3837
4006
|
}
|
|
3838
4007
|
|
|
4008
|
+
// src/orderbook/profitability.ts
|
|
4009
|
+
var import_decimal3 = require("decimal.js");
|
|
4010
|
+
|
|
4011
|
+
// src/data/inventory.ts
|
|
4012
|
+
var INVENTORY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4013
|
+
function inventoryOf(snapshot) {
|
|
4014
|
+
if (snapshot.status !== "fresh") return null;
|
|
4015
|
+
const held = {};
|
|
4016
|
+
for (const chain of snapshot.chains) {
|
|
4017
|
+
for (const asset of chain.assets) {
|
|
4018
|
+
if (asset.total === null) return null;
|
|
4019
|
+
held[asset.symbol] = (held[asset.symbol] ?? 0) + asset.total;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
return Object.keys(held).length > 0 ? held : null;
|
|
4023
|
+
}
|
|
4024
|
+
function parseUtc(value) {
|
|
4025
|
+
return Date.parse(`${value.replace(" ", "T")}Z`);
|
|
4026
|
+
}
|
|
4027
|
+
var InventoryRecorder = class {
|
|
4028
|
+
constructor(store, balances, logger, now = Date.now) {
|
|
4029
|
+
this.store = store;
|
|
4030
|
+
this.balances = balances;
|
|
4031
|
+
this.logger = logger;
|
|
4032
|
+
this.now = now;
|
|
4033
|
+
this.recording = false;
|
|
4034
|
+
this.onSnapshot = (snapshot) => void this.consider(snapshot);
|
|
4035
|
+
}
|
|
4036
|
+
start() {
|
|
4037
|
+
this.balances.on("snapshot", this.onSnapshot);
|
|
4038
|
+
void this.consider(this.balances.getSnapshot());
|
|
4039
|
+
}
|
|
4040
|
+
stop() {
|
|
4041
|
+
this.balances.off("snapshot", this.onSnapshot);
|
|
4042
|
+
}
|
|
4043
|
+
/** Takes a snapshot from `snapshot` if one is due and the read is complete. Never rejects. */
|
|
4044
|
+
async consider(snapshot) {
|
|
4045
|
+
if (this.recording) return;
|
|
4046
|
+
const held = inventoryOf(snapshot);
|
|
4047
|
+
if (!held) return;
|
|
4048
|
+
this.recording = true;
|
|
4049
|
+
try {
|
|
4050
|
+
if (this.lastTakenAt === void 0) {
|
|
4051
|
+
const latest = await this.store.latest();
|
|
4052
|
+
this.lastTakenAt = latest ? parseUtc(latest.takenAt) : 0;
|
|
4053
|
+
}
|
|
4054
|
+
if (this.now() - this.lastTakenAt < INVENTORY_INTERVAL_MS) return;
|
|
4055
|
+
await this.store.record(held);
|
|
4056
|
+
this.lastTakenAt = this.now();
|
|
4057
|
+
this.logger.info({ tokens: Object.keys(held).length }, "Recorded an inventory snapshot");
|
|
4058
|
+
} catch (err) {
|
|
4059
|
+
this.logger.warn({ err }, "Could not record an inventory snapshot");
|
|
4060
|
+
} finally {
|
|
4061
|
+
this.recording = false;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
4065
|
+
|
|
4066
|
+
// src/orderbook/usd.ts
|
|
4067
|
+
var import_decimal2 = require("decimal.js");
|
|
4068
|
+
function limitOrderUsdEdges(orders) {
|
|
4069
|
+
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
4070
|
+
base: normalizeSymbol(order.base),
|
|
4071
|
+
quote: normalizeSymbol(order.quote),
|
|
4072
|
+
rate: new import_decimal2.Decimal(order.price).div(ORDERBOOK_SCALE.toString())
|
|
4073
|
+
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
4074
|
+
}
|
|
4075
|
+
function usdFactorsFrom(edges) {
|
|
4076
|
+
const factors = /* @__PURE__ */ new Map();
|
|
4077
|
+
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new import_decimal2.Decimal(1));
|
|
4078
|
+
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
4079
|
+
let grew = true;
|
|
4080
|
+
while (grew) {
|
|
4081
|
+
grew = false;
|
|
4082
|
+
for (const edge of ordered) {
|
|
4083
|
+
const usdBase = factors.get(edge.base);
|
|
4084
|
+
const usdQuote = factors.get(edge.quote);
|
|
4085
|
+
if (usdBase && !usdQuote) {
|
|
4086
|
+
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
4087
|
+
grew = true;
|
|
4088
|
+
} else if (usdQuote && !usdBase) {
|
|
4089
|
+
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
4090
|
+
grew = true;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
return factors;
|
|
4095
|
+
}
|
|
4096
|
+
function usdValueOf(factors, symbol, amount) {
|
|
4097
|
+
const factor = factors.get(normalizeSymbol(symbol));
|
|
4098
|
+
return factor ? amount.mul(factor) : null;
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
// src/orderbook/profitability.ts
|
|
4102
|
+
var Decimal3 = import_decimal3.Decimal.clone({ precision: 40 });
|
|
4103
|
+
var PROFIT_PERIODS = {
|
|
4104
|
+
"7d": { bucket: "day", count: 7 },
|
|
4105
|
+
"30d": { bucket: "day", count: 30 },
|
|
4106
|
+
"12w": { bucket: "week", count: 12 },
|
|
4107
|
+
"12m": { bucket: "month", count: 12 },
|
|
4108
|
+
all: { bucket: "year", count: null }
|
|
4109
|
+
};
|
|
4110
|
+
var MINUTE = 6e4;
|
|
4111
|
+
var DAY = 864e5;
|
|
4112
|
+
var BALANCES_REACH_MS = 7 * DAY;
|
|
4113
|
+
function bucketStart(local, bucket) {
|
|
4114
|
+
const date = new Date(local);
|
|
4115
|
+
const year = date.getUTCFullYear();
|
|
4116
|
+
const month = date.getUTCMonth();
|
|
4117
|
+
if (bucket === "year") return Date.UTC(year, 0, 1);
|
|
4118
|
+
if (bucket === "month") return Date.UTC(year, month, 1);
|
|
4119
|
+
const day = Date.UTC(year, month, date.getUTCDate());
|
|
4120
|
+
return bucket === "day" ? day : day - (date.getUTCDay() + 6) % 7 * DAY;
|
|
4121
|
+
}
|
|
4122
|
+
function shiftBucket(start, bucket, steps) {
|
|
4123
|
+
if (bucket === "day") return start + steps * DAY;
|
|
4124
|
+
if (bucket === "week") return start + steps * 7 * DAY;
|
|
4125
|
+
const date = new Date(start);
|
|
4126
|
+
return bucket === "month" ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + steps, 1) : Date.UTC(date.getUTCFullYear() + steps, 0, 1);
|
|
4127
|
+
}
|
|
4128
|
+
function scaled(value) {
|
|
4129
|
+
return new Decimal3(value).div(ORDERBOOK_SCALE.toString());
|
|
4130
|
+
}
|
|
4131
|
+
function tradeOf(fill, at) {
|
|
4132
|
+
try {
|
|
4133
|
+
const paid = scaled(fill.amount);
|
|
4134
|
+
const taken = fill.amountIn === null ? null : scaled(fill.amountIn);
|
|
4135
|
+
const price = scaled(fill.price);
|
|
4136
|
+
if (!paid.gt(0) || !price.gt(0) || taken !== null && !taken.gt(0)) return null;
|
|
4137
|
+
const buying = fill.side === "BID";
|
|
4138
|
+
const [baseAmount, quoteAmount] = buying ? [taken ?? paid.div(price), paid] : [paid, taken ?? paid.mul(price)];
|
|
4139
|
+
return {
|
|
4140
|
+
at,
|
|
4141
|
+
book: fill.book,
|
|
4142
|
+
base: normalizeSymbol(fill.base),
|
|
4143
|
+
quote: normalizeSymbol(fill.quote),
|
|
4144
|
+
buying,
|
|
4145
|
+
baseAmount,
|
|
4146
|
+
quoteAmount,
|
|
4147
|
+
estimated: taken === null
|
|
4148
|
+
};
|
|
4149
|
+
} catch {
|
|
4150
|
+
return null;
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
function add(holdings, symbol, amount) {
|
|
4154
|
+
holdings.set(symbol, (holdings.get(symbol) ?? new Decimal3(0)).add(amount));
|
|
4155
|
+
}
|
|
4156
|
+
function apply(holdings, trade, sign) {
|
|
4157
|
+
const base = trade.buying ? trade.baseAmount : trade.baseAmount.neg();
|
|
4158
|
+
const quote = trade.buying ? trade.quoteAmount.neg() : trade.quoteAmount;
|
|
4159
|
+
add(holdings, trade.base, base.mul(sign));
|
|
4160
|
+
add(holdings, trade.quote, quote.mul(sign));
|
|
4161
|
+
}
|
|
4162
|
+
var Sums = class {
|
|
4163
|
+
constructor() {
|
|
4164
|
+
this.change = /* @__PURE__ */ new Map();
|
|
4165
|
+
this.boughtUsd = new Decimal3(0);
|
|
4166
|
+
this.soldUsd = new Decimal3(0);
|
|
4167
|
+
this.buys = 0;
|
|
4168
|
+
this.sells = 0;
|
|
4169
|
+
}
|
|
4170
|
+
};
|
|
4171
|
+
var BookLedger = class {
|
|
4172
|
+
constructor(id, base, quote) {
|
|
4173
|
+
this.id = id;
|
|
4174
|
+
this.base = base;
|
|
4175
|
+
this.quote = quote;
|
|
4176
|
+
this.bought = new Decimal3(0);
|
|
4177
|
+
this.boughtQuote = new Decimal3(0);
|
|
4178
|
+
this.sold = new Decimal3(0);
|
|
4179
|
+
this.soldQuote = new Decimal3(0);
|
|
4180
|
+
/** The rates of the book's latest buy and latest sell, over all of its history. */
|
|
4181
|
+
this.latestBuy = null;
|
|
4182
|
+
this.latestSell = null;
|
|
4183
|
+
this.inPeriod = false;
|
|
4184
|
+
}
|
|
4185
|
+
/**
|
|
4186
|
+
* The rate the book is valued at. Midway between the latest buy and the latest sell, because
|
|
4187
|
+
* either alone sits half a spread to one side and would price open volume against whichever
|
|
4188
|
+
* way the book happened to trade last.
|
|
4189
|
+
*/
|
|
4190
|
+
rate() {
|
|
4191
|
+
if (this.latestBuy && this.latestSell) return this.latestBuy.add(this.latestSell).div(2);
|
|
4192
|
+
return this.latestBuy ?? this.latestSell;
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
function readingOf(reading) {
|
|
4196
|
+
const held = /* @__PURE__ */ new Map();
|
|
4197
|
+
const cover = /* @__PURE__ */ new Map();
|
|
4198
|
+
for (const [symbol, amount] of Object.entries(reading.balances)) {
|
|
4199
|
+
const chains = reading.chains[symbol];
|
|
4200
|
+
if (!Number.isFinite(amount) || !chains?.length) continue;
|
|
4201
|
+
add(held, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4202
|
+
cover.set(normalizeSymbol(symbol), [...chains].sort().join(","));
|
|
4203
|
+
}
|
|
4204
|
+
return { held, cover };
|
|
4205
|
+
}
|
|
4206
|
+
function movedBetween(earlier, later) {
|
|
4207
|
+
if (!earlier || !later) return null;
|
|
4208
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4209
|
+
for (const [symbol, cover] of later.cover) {
|
|
4210
|
+
if (earlier.cover.get(symbol) !== cover) continue;
|
|
4211
|
+
const before = earlier.held.get(symbol) ?? new Decimal3(0);
|
|
4212
|
+
moved.set(symbol, (later.held.get(symbol) ?? new Decimal3(0)).sub(before));
|
|
4213
|
+
}
|
|
4214
|
+
return moved;
|
|
4215
|
+
}
|
|
4216
|
+
function nearestRecord(instant, records) {
|
|
4217
|
+
let record;
|
|
4218
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
4219
|
+
for (const candidate of records) {
|
|
4220
|
+
const distance = Math.abs(candidate.at - instant);
|
|
4221
|
+
if (candidate.live && distance > BALANCES_REACH_MS) continue;
|
|
4222
|
+
if (distance < gap || distance === gap && record?.live && !candidate.live) {
|
|
4223
|
+
record = candidate;
|
|
4224
|
+
gap = distance;
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
return record;
|
|
4228
|
+
}
|
|
4229
|
+
function inventoryAt(instant, records, trades, outflows, readings) {
|
|
4230
|
+
const record = nearestRecord(instant, records);
|
|
4231
|
+
if (!record) return null;
|
|
4232
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
4233
|
+
for (const [symbol, amount] of Object.entries(record.balances)) {
|
|
4234
|
+
if (Number.isFinite(amount)) add(recorded, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4235
|
+
}
|
|
4236
|
+
const held = new Map(recorded);
|
|
4237
|
+
const after = record.at >= instant;
|
|
4238
|
+
const [from, to] = after ? [instant, record.at] : [record.at, instant];
|
|
4239
|
+
const sign = after ? -1 : 1;
|
|
4240
|
+
for (const trade of trades) {
|
|
4241
|
+
if (trade.at >= from && trade.at < to) apply(held, trade, sign);
|
|
4242
|
+
}
|
|
4243
|
+
for (const outflow of outflows) {
|
|
4244
|
+
if (outflow.at >= from && outflow.at < to) {
|
|
4245
|
+
add(held, normalizeSymbol(outflow.symbol), new Decimal3(outflow.amount).mul(-sign));
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
const moved = movedBetween(readings.get(from), readings.get(to));
|
|
4249
|
+
if (moved) {
|
|
4250
|
+
for (const [symbol, amount] of moved) {
|
|
4251
|
+
held.set(symbol, (recorded.get(symbol) ?? new Decimal3(0)).add(amount.mul(sign)));
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
for (const [symbol, amount] of held) if (amount.isNegative()) held.set(symbol, new Decimal3(0));
|
|
4255
|
+
return { held, record };
|
|
4256
|
+
}
|
|
4257
|
+
function tradesOf(fills, now) {
|
|
4258
|
+
const trades = [];
|
|
4259
|
+
for (const fill of fills) {
|
|
4260
|
+
const at = parseUtc(fill.filledAt);
|
|
4261
|
+
if (!Number.isFinite(at) || at > now) continue;
|
|
4262
|
+
const trade = tradeOf(fill, at);
|
|
4263
|
+
if (trade) trades.push(trade);
|
|
4264
|
+
}
|
|
4265
|
+
return trades.sort((a, b) => a.at - b.at);
|
|
4266
|
+
}
|
|
4267
|
+
function bucketStarts(period, now, offset, firstAt) {
|
|
4268
|
+
const { bucket, count } = PROFIT_PERIODS[period];
|
|
4269
|
+
const last = bucketStart(now - offset, bucket);
|
|
4270
|
+
const first = count === null ? bucketStart((firstAt ?? now) - offset, bucket) : shiftBucket(last, bucket, 1 - count);
|
|
4271
|
+
const starts = [];
|
|
4272
|
+
for (let start = first; start <= last; start = shiftBucket(start, bucket, 1)) starts.push(start);
|
|
4273
|
+
return starts;
|
|
4274
|
+
}
|
|
4275
|
+
function inventoryInstants(fills, options) {
|
|
4276
|
+
const { period, now } = options;
|
|
4277
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4278
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4279
|
+
const instants = /* @__PURE__ */ new Set([now]);
|
|
4280
|
+
for (const start of bucketStarts(period, now, offset, tradesOf(fills, now)[0]?.at)) {
|
|
4281
|
+
instants.add(start + offset);
|
|
4282
|
+
const record = nearestRecord(start + offset, records);
|
|
4283
|
+
if (record) instants.add(record.at);
|
|
4284
|
+
}
|
|
4285
|
+
return [...instants].sort((a, b) => a - b);
|
|
4286
|
+
}
|
|
4287
|
+
function summarizeProfit(fills, options) {
|
|
4288
|
+
const { period, now } = options;
|
|
4289
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4290
|
+
const { bucket } = PROFIT_PERIODS[period];
|
|
4291
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4292
|
+
const outflows = options.outflows ?? [];
|
|
4293
|
+
const readings = new Map((options.readings ?? []).map((reading) => [reading.at, readingOf(reading)]));
|
|
4294
|
+
const trades = tradesOf(fills, now);
|
|
4295
|
+
const starts = bucketStarts(period, now, offset, trades[0]?.at);
|
|
4296
|
+
const first = starts[0];
|
|
4297
|
+
const books = /* @__PURE__ */ new Map();
|
|
4298
|
+
const spelling = /* @__PURE__ */ new Map();
|
|
4299
|
+
for (const record of records) {
|
|
4300
|
+
for (const symbol of Object.keys(record.balances)) {
|
|
4301
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
for (const fill of fills) {
|
|
4305
|
+
for (const symbol of [fill.base, fill.quote]) {
|
|
4306
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
for (const trade of trades) {
|
|
4310
|
+
let book = books.get(trade.book);
|
|
4311
|
+
if (!book) {
|
|
4312
|
+
book = new BookLedger(trade.book, trade.base, trade.quote);
|
|
4313
|
+
books.set(trade.book, book);
|
|
4314
|
+
}
|
|
4315
|
+
const rate = trade.quoteAmount.div(trade.baseAmount);
|
|
4316
|
+
if (trade.buying) book.latestBuy = rate;
|
|
4317
|
+
else book.latestSell = rate;
|
|
4318
|
+
}
|
|
4319
|
+
const edges = [];
|
|
4320
|
+
for (const book of books.values()) {
|
|
4321
|
+
const rate = book.rate();
|
|
4322
|
+
if (rate && book.base !== book.quote) {
|
|
4323
|
+
edges.push({ base: book.base, quote: book.quote, rate: new import_decimal3.Decimal(rate.toString()) });
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
const factors = usdFactorsFrom(edges);
|
|
4327
|
+
const priceOf = (symbol) => {
|
|
4328
|
+
const factor = factors.get(symbol);
|
|
4329
|
+
return factor ? new Decimal3(factor.toString()) : null;
|
|
4330
|
+
};
|
|
4331
|
+
const unpriced = /* @__PURE__ */ new Set();
|
|
4332
|
+
const valueOf = (holdings) => {
|
|
4333
|
+
let total = new Decimal3(0);
|
|
4334
|
+
for (const [symbol, amount] of holdings) {
|
|
4335
|
+
if (amount.isZero()) continue;
|
|
4336
|
+
const price = priceOf(symbol);
|
|
4337
|
+
if (price) total = total.add(amount.mul(price));
|
|
4338
|
+
else unpriced.add(symbol);
|
|
4339
|
+
}
|
|
4340
|
+
return total;
|
|
4341
|
+
};
|
|
4342
|
+
const totals = new Sums();
|
|
4343
|
+
const buckets = new Map(starts.map((start) => [start, new Sums()]));
|
|
4344
|
+
let estimatedFills = 0;
|
|
4345
|
+
for (const trade of trades) {
|
|
4346
|
+
const sums = buckets.get(bucketStart(trade.at - offset, bucket));
|
|
4347
|
+
if (!sums) continue;
|
|
4348
|
+
const book = books.get(trade.book);
|
|
4349
|
+
if (!book) continue;
|
|
4350
|
+
book.inPeriod = true;
|
|
4351
|
+
if (trade.estimated) estimatedFills += 1;
|
|
4352
|
+
if (trade.buying) {
|
|
4353
|
+
book.bought = book.bought.add(trade.baseAmount);
|
|
4354
|
+
book.boughtQuote = book.boughtQuote.add(trade.quoteAmount);
|
|
4355
|
+
} else {
|
|
4356
|
+
book.sold = book.sold.add(trade.baseAmount);
|
|
4357
|
+
book.soldQuote = book.soldQuote.add(trade.quoteAmount);
|
|
4358
|
+
}
|
|
4359
|
+
const basePrice = priceOf(trade.base);
|
|
4360
|
+
const quotePrice = priceOf(trade.quote);
|
|
4361
|
+
const volume = basePrice ? trade.baseAmount.mul(basePrice) : quotePrice ? trade.quoteAmount.mul(quotePrice) : new Decimal3(0);
|
|
4362
|
+
for (const sum of [totals, sums]) {
|
|
4363
|
+
apply(sum.change, trade, 1);
|
|
4364
|
+
if (trade.buying) {
|
|
4365
|
+
sum.buys += 1;
|
|
4366
|
+
sum.boughtUsd = sum.boughtUsd.add(volume);
|
|
4367
|
+
} else {
|
|
4368
|
+
sum.sells += 1;
|
|
4369
|
+
sum.soldUsd = sum.soldUsd.add(volume);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
const figures = (sums, startsAt, endsAt) => {
|
|
4374
|
+
const profit = valueOf(sums.change);
|
|
4375
|
+
const began2 = inventoryAt(startsAt, records, trades, outflows, readings);
|
|
4376
|
+
const inventory = began2 ? valueOf(began2.held) : null;
|
|
4377
|
+
const moved = movedBetween(readings.get(startsAt), readings.get(endsAt));
|
|
4378
|
+
if (moved) {
|
|
4379
|
+
for (const [symbol, amount] of sums.change) {
|
|
4380
|
+
if (moved.has(symbol)) moved.set(symbol, (moved.get(symbol) ?? new Decimal3(0)).sub(amount));
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
return {
|
|
4384
|
+
profitUsd: profit.toNumber(),
|
|
4385
|
+
startInventoryUsd: inventory ? inventory.toNumber() : null,
|
|
4386
|
+
returnPct: inventory?.gt(0) ? profit.div(inventory).mul(100).toNumber() : null,
|
|
4387
|
+
transfersUsd: moved && moved.size > 0 ? valueOf(moved).toNumber() : null,
|
|
4388
|
+
boughtUsd: sums.boughtUsd.toNumber(),
|
|
4389
|
+
soldUsd: sums.soldUsd.toNumber(),
|
|
4390
|
+
buys: sums.buys,
|
|
4391
|
+
sells: sums.sells
|
|
4392
|
+
};
|
|
4393
|
+
};
|
|
4394
|
+
const from = first + offset;
|
|
4395
|
+
const began = inventoryAt(from, records, trades, outflows, readings);
|
|
4396
|
+
const startInventory = began && {
|
|
4397
|
+
at: from,
|
|
4398
|
+
source: began.record.live ? "balances" : "snapshot",
|
|
4399
|
+
recordedAt: began.record.at,
|
|
4400
|
+
usd: valueOf(began.held).toNumber(),
|
|
4401
|
+
tokens: [...began.held].filter(([, amount]) => amount.gt(0)).map(([symbol, amount]) => {
|
|
4402
|
+
const price = priceOf(symbol);
|
|
4403
|
+
return {
|
|
4404
|
+
symbol: spelling.get(symbol) ?? symbol,
|
|
4405
|
+
amount: amount.toNumber(),
|
|
4406
|
+
usd: price ? amount.mul(price).toNumber() : null
|
|
4407
|
+
};
|
|
4408
|
+
}).sort((a, b) => (b.usd ?? 0) - (a.usd ?? 0) || a.symbol.localeCompare(b.symbol))
|
|
4409
|
+
};
|
|
4410
|
+
const rows = [];
|
|
4411
|
+
for (const book of books.values()) {
|
|
4412
|
+
if (!book.inPeriod) continue;
|
|
4413
|
+
const averageBuy = book.bought.gt(0) ? book.boughtQuote.div(book.bought) : null;
|
|
4414
|
+
const averageSell = book.sold.gt(0) ? book.soldQuote.div(book.sold) : null;
|
|
4415
|
+
const baseChange = book.bought.sub(book.sold);
|
|
4416
|
+
const quoteChange = book.soldQuote.sub(book.boughtQuote);
|
|
4417
|
+
const basePrice = priceOf(book.base);
|
|
4418
|
+
const quotePrice = priceOf(book.quote);
|
|
4419
|
+
rows.push({
|
|
4420
|
+
book: book.id,
|
|
4421
|
+
base: spelling.get(book.base) ?? book.base,
|
|
4422
|
+
quote: spelling.get(book.quote) ?? book.quote,
|
|
4423
|
+
bought: book.bought.toNumber(),
|
|
4424
|
+
sold: book.sold.toNumber(),
|
|
4425
|
+
averageBuy: averageBuy ? averageBuy.toNumber() : null,
|
|
4426
|
+
averageSell: averageSell ? averageSell.toNumber() : null,
|
|
4427
|
+
spreadPct: averageBuy && averageSell ? averageSell.div(averageBuy).sub(1).mul(100).toNumber() : null,
|
|
4428
|
+
baseChange: baseChange.toNumber(),
|
|
4429
|
+
quoteChange: quoteChange.toNumber(),
|
|
4430
|
+
rate: book.rate()?.toNumber() ?? null,
|
|
4431
|
+
profitUsd: basePrice && quotePrice ? baseChange.mul(basePrice).add(quoteChange.mul(quotePrice)).toNumber() : null
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
rows.sort((a, b) => (b.profitUsd ?? 0) - (a.profitUsd ?? 0) || a.book.localeCompare(b.book));
|
|
4435
|
+
return {
|
|
4436
|
+
period,
|
|
4437
|
+
bucket,
|
|
4438
|
+
from,
|
|
4439
|
+
to: now,
|
|
4440
|
+
totals: figures(totals, from, now),
|
|
4441
|
+
series: starts.map((start, index) => {
|
|
4442
|
+
const sums = buckets.get(start) ?? new Sums();
|
|
4443
|
+
const endsAt = index + 1 < starts.length ? starts[index + 1] + offset : now;
|
|
4444
|
+
return { start: start + offset, ...figures(sums, start + offset, endsAt) };
|
|
4445
|
+
}),
|
|
4446
|
+
books: rows,
|
|
4447
|
+
startInventory,
|
|
4448
|
+
estimatedFills,
|
|
4449
|
+
unpricedTokens: [...unpriced].map((symbol) => spelling.get(symbol) ?? symbol).sort()
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
|
|
3839
4453
|
// src/data/state.ts
|
|
3840
4454
|
async function patchRuntimeState(store, patch) {
|
|
3841
4455
|
if (store.patch) return store.patch(patch);
|
|
@@ -3844,6 +4458,80 @@ async function patchRuntimeState(store, patch) {
|
|
|
3844
4458
|
return next;
|
|
3845
4459
|
}
|
|
3846
4460
|
|
|
4461
|
+
// src/services/bundler-preflight.ts
|
|
4462
|
+
var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
4463
|
+
async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
|
|
4464
|
+
const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
|
|
4465
|
+
const logger = moduleLogger(configService.loggers, "bundler-preflight");
|
|
4466
|
+
const failures = await Promise.all(
|
|
4467
|
+
chains.map(async ({ chainId, bundlerUrl }) => {
|
|
4468
|
+
if (options.watchOnly?.[chainId] === true) return void 0;
|
|
4469
|
+
if (!bundlerUrl?.trim()) return void 0;
|
|
4470
|
+
const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
|
|
4471
|
+
if (!entryPoint) return void 0;
|
|
4472
|
+
return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
|
|
4473
|
+
})
|
|
4474
|
+
);
|
|
4475
|
+
const messages = failures.filter((failure) => failure !== void 0);
|
|
4476
|
+
if (messages.length > 0) throw new Error(messages.join("; "));
|
|
4477
|
+
}
|
|
4478
|
+
function servesEntryPoint(listed, entryPoint) {
|
|
4479
|
+
return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
|
|
4480
|
+
}
|
|
4481
|
+
function describeEntryPoints(listed) {
|
|
4482
|
+
return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
|
|
4483
|
+
}
|
|
4484
|
+
function bundlerHost(url) {
|
|
4485
|
+
try {
|
|
4486
|
+
return new URL(url).host || "an unparseable URL";
|
|
4487
|
+
} catch {
|
|
4488
|
+
return "an unparseable URL";
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
function chainName(chainId) {
|
|
4492
|
+
const label = chainByChainId(chainId)?.label;
|
|
4493
|
+
return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
|
|
4494
|
+
}
|
|
4495
|
+
async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
|
|
4496
|
+
const host = bundlerHost(bundlerUrl);
|
|
4497
|
+
const subject = `Bundler ${host} for ${chainName(chainId)}`;
|
|
4498
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
4499
|
+
let listed;
|
|
4500
|
+
try {
|
|
4501
|
+
listed = await supportedEntryPoints(bundlerUrl, signal);
|
|
4502
|
+
} catch (err) {
|
|
4503
|
+
const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
|
|
4504
|
+
logger.warn(
|
|
4505
|
+
{ chain: formatChainKey(chainId), bundler: host },
|
|
4506
|
+
`${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
|
|
4507
|
+
);
|
|
4508
|
+
return void 0;
|
|
4509
|
+
}
|
|
4510
|
+
if (servesEntryPoint(listed, entryPoint)) return void 0;
|
|
4511
|
+
return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
|
|
4512
|
+
}
|
|
4513
|
+
async function supportedEntryPoints(bundlerUrl, signal) {
|
|
4514
|
+
const response = await fetch(bundlerUrl, {
|
|
4515
|
+
method: "POST",
|
|
4516
|
+
headers: { "Content-Type": "application/json" },
|
|
4517
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
|
|
4518
|
+
signal
|
|
4519
|
+
});
|
|
4520
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
4521
|
+
const json = await response.json();
|
|
4522
|
+
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
4523
|
+
const { result } = json;
|
|
4524
|
+
if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
|
|
4525
|
+
throw new Error("the result is not a list of addresses");
|
|
4526
|
+
}
|
|
4527
|
+
return result;
|
|
4528
|
+
}
|
|
4529
|
+
function errorReason(err) {
|
|
4530
|
+
if (!(err instanceof Error)) return String(err);
|
|
4531
|
+
const { cause } = err;
|
|
4532
|
+
return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
|
|
4533
|
+
}
|
|
4534
|
+
|
|
3847
4535
|
// src/services/wallet/mpcvault.ts
|
|
3848
4536
|
var import_viem7 = require("viem");
|
|
3849
4537
|
var grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
@@ -13100,6 +13788,7 @@ var CacheService = class {
|
|
|
13100
13788
|
})),
|
|
13101
13789
|
partialFill: plan.partialFill,
|
|
13102
13790
|
profit: plan.profit,
|
|
13791
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13103
13792
|
...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
|
|
13104
13793
|
}));
|
|
13105
13794
|
} catch (error) {
|
|
@@ -13130,6 +13819,7 @@ var CacheService = class {
|
|
|
13130
13819
|
})),
|
|
13131
13820
|
partialFill: plan.partialFill,
|
|
13132
13821
|
profit: plan.profit,
|
|
13822
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13133
13823
|
...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
|
|
13134
13824
|
})),
|
|
13135
13825
|
timestamp: Date.now()
|
|
@@ -15744,7 +16434,7 @@ var EVM_HOST = [
|
|
|
15744
16434
|
];
|
|
15745
16435
|
|
|
15746
16436
|
// src/services/ContractInteractionService.ts
|
|
15747
|
-
var
|
|
16437
|
+
var import_decimal4 = require("decimal.js");
|
|
15748
16438
|
|
|
15749
16439
|
// src/config/abis/IntentGatewayV2.ts
|
|
15750
16440
|
var INTENT_GATEWAY_V2_ABI = [
|
|
@@ -17005,9 +17695,9 @@ var INTENT_GATEWAY_V2_ABI = [
|
|
|
17005
17695
|
internalType: "bytes32"
|
|
17006
17696
|
},
|
|
17007
17697
|
{
|
|
17008
|
-
name: "
|
|
17009
|
-
type: "
|
|
17010
|
-
internalType: "
|
|
17698
|
+
name: "userOpHash",
|
|
17699
|
+
type: "bytes32",
|
|
17700
|
+
internalType: "bytes32"
|
|
17011
17701
|
},
|
|
17012
17702
|
{
|
|
17013
17703
|
name: "signature",
|
|
@@ -18372,7 +19062,7 @@ function describeShortfall(balances) {
|
|
|
18372
19062
|
}
|
|
18373
19063
|
|
|
18374
19064
|
// src/services/ContractInteractionService.ts
|
|
18375
|
-
|
|
19065
|
+
import_decimal4.Decimal.config({ precision: 28, rounding: 4 });
|
|
18376
19066
|
var FUNDING_GAS_PER_CALL = 400000n;
|
|
18377
19067
|
var DEBIT_ORDER_GAS = 60000n;
|
|
18378
19068
|
function settles(budget) {
|
|
@@ -18382,10 +19072,13 @@ var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
|
|
|
18382
19072
|
var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
|
|
18383
19073
|
var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
|
|
18384
19074
|
var BID_DISCOVERY_PAD_SECONDS = 30;
|
|
19075
|
+
var BLOCK_TIME_SAMPLE_BLOCKS = 1000n;
|
|
19076
|
+
var FALLBACK_BLOCK_TIME_SECONDS = 2;
|
|
18385
19077
|
var ContractInteractionService = class {
|
|
18386
19078
|
constructor(clientManager, configService, signer, sharedCacheService) {
|
|
18387
19079
|
this.clientManager = clientManager;
|
|
18388
19080
|
this.sdkHelperCache = /* @__PURE__ */ new Map();
|
|
19081
|
+
this.blockTimes = /* @__PURE__ */ new Map();
|
|
18389
19082
|
this.logger = moduleLogger(configService.loggers, "contract-service");
|
|
18390
19083
|
this.configService = configService;
|
|
18391
19084
|
this.cacheService = sharedCacheService || new CacheService();
|
|
@@ -18445,11 +19138,20 @@ var ContractInteractionService = class {
|
|
|
18445
19138
|
try {
|
|
18446
19139
|
const chainIds = this.configService.getConfiguredChainIds();
|
|
18447
19140
|
const chainNames = chainIds.map((id) => `EVM-${id}`);
|
|
18448
|
-
|
|
19141
|
+
await Promise.all(
|
|
19142
|
+
chainNames.map(async (chainName2) => {
|
|
19143
|
+
try {
|
|
19144
|
+
await this.blockTimeSeconds(chainName2);
|
|
19145
|
+
} catch (err) {
|
|
19146
|
+
this.logger.warn({ err, chain: chainName2 }, "Could not measure the block time at startup");
|
|
19147
|
+
}
|
|
19148
|
+
})
|
|
19149
|
+
);
|
|
19150
|
+
for (const chainName2 of chainNames) {
|
|
18449
19151
|
try {
|
|
18450
|
-
await this.getFeeTokenWithDecimals(
|
|
19152
|
+
await this.getFeeTokenWithDecimals(chainName2);
|
|
18451
19153
|
} catch (err) {
|
|
18452
|
-
this.logger.warn({ err, chain:
|
|
19154
|
+
this.logger.warn({ err, chain: chainName2 }, "Could not prewarm fee token decimals");
|
|
18453
19155
|
}
|
|
18454
19156
|
}
|
|
18455
19157
|
for (const destChain of chainNames) {
|
|
@@ -18713,7 +19415,7 @@ var ContractInteractionService = class {
|
|
|
18713
19415
|
* @returns The total USD value of inputs (sum of normalized stable amounts, or 0 if none)
|
|
18714
19416
|
*/
|
|
18715
19417
|
async getInputUsdValue(order) {
|
|
18716
|
-
let inputUsdValue = new
|
|
19418
|
+
let inputUsdValue = new import_decimal4.Decimal(0);
|
|
18717
19419
|
const inputs = order.inputs;
|
|
18718
19420
|
const sourceUsdc = this.configService.getUsdcAsset(order.source).toLowerCase();
|
|
18719
19421
|
const sourceUsdt = this.configService.getUsdtAsset(order.source).toLowerCase();
|
|
@@ -18722,7 +19424,7 @@ var ContractInteractionService = class {
|
|
|
18722
19424
|
const addr = tokenAddress.toLowerCase();
|
|
18723
19425
|
if (addr !== sourceUsdc && addr !== sourceUsdt) continue;
|
|
18724
19426
|
const decimals = await this.getTokenDecimals(tokenAddress, order.source);
|
|
18725
|
-
const tokenAmount = new
|
|
19427
|
+
const tokenAmount = new import_decimal4.Decimal((0, import_viem23.formatUnits)(input.amount, decimals));
|
|
18726
19428
|
inputUsdValue = inputUsdValue.plus(tokenAmount);
|
|
18727
19429
|
}
|
|
18728
19430
|
return inputUsdValue;
|
|
@@ -19034,18 +19736,59 @@ var ContractInteractionService = class {
|
|
|
19034
19736
|
*
|
|
19035
19737
|
* Operators configure seconds because that is the unit the risk is actually in, but the
|
|
19036
19738
|
* contract compares against block numbers so `order.deadline` and this read the same
|
|
19037
|
-
* clock. The conversion uses the chain's
|
|
19038
|
-
* up, since erring long costs a slightly stale quote while erring
|
|
19039
|
-
* winnable bids.
|
|
19739
|
+
* clock. The conversion uses the chain's block time from {@link blockTimeSeconds}; it is
|
|
19740
|
+
* deliberately rounded up, since erring long costs a slightly stale quote while erring
|
|
19741
|
+
* short silently drops winnable bids.
|
|
19040
19742
|
*/
|
|
19041
19743
|
async bidValidUntilBlock(chain) {
|
|
19042
19744
|
const client = this.clientManager.getPublicClient(chain);
|
|
19043
|
-
const currentBlock = await client.getBlockNumber();
|
|
19044
|
-
const blockTimeMs = client.chain?.blockTime;
|
|
19045
|
-
const blockTimeSec = blockTimeMs ? blockTimeMs / 1e3 : 2;
|
|
19745
|
+
const [currentBlock, blockTimeSec] = await Promise.all([client.getBlockNumber(), this.blockTimeSeconds(chain)]);
|
|
19046
19746
|
const windowSec = this.configService.getBidValiditySeconds() + BID_DISCOVERY_PAD_SECONDS;
|
|
19047
19747
|
return currentBlock + BigInt(Math.ceil(windowSec / blockTimeSec));
|
|
19048
19748
|
}
|
|
19749
|
+
/**
|
|
19750
|
+
* Seconds per block on `chain`. Measured once, when simplex starts ({@link initCache}), and
|
|
19751
|
+
* kept until it stops. A chain with no measurement yet, because it was added later or its
|
|
19752
|
+
* blocks could not be read at startup, is measured at its first bid.
|
|
19753
|
+
*
|
|
19754
|
+
* The nominal `Chain.blockTime` cannot be relied on alone: it goes stale when a chain
|
|
19755
|
+
* speeds up (BSC makes a block every 0.45s against a declared 0.75s) and some chains
|
|
19756
|
+
* declare none. Converting at a slower block time than the chain's ends a bid early, so
|
|
19757
|
+
* the faster of the measured and the declared figure is used, and a stalled or idle
|
|
19758
|
+
* stretch in the sample never shortens a bid.
|
|
19759
|
+
*
|
|
19760
|
+
* Blocks that cannot be read fall back to the declared figure, then to
|
|
19761
|
+
* {@link FALLBACK_BLOCK_TIME_SECONDS}. That answer is not kept.
|
|
19762
|
+
*/
|
|
19763
|
+
blockTimeSeconds(chain) {
|
|
19764
|
+
const known = this.blockTimes.get(chain);
|
|
19765
|
+
if (known) return known;
|
|
19766
|
+
const client = this.clientManager.getPublicClient(chain);
|
|
19767
|
+
const declaredMs = client.chain?.blockTime;
|
|
19768
|
+
const declared = declaredMs ? declaredMs / 1e3 : void 0;
|
|
19769
|
+
const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
|
|
19770
|
+
this.blockTimes.delete(chain);
|
|
19771
|
+
const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
|
|
19772
|
+
this.logger.warn(
|
|
19773
|
+
{ err, chain, seconds: fallback2 },
|
|
19774
|
+
"Could not measure the block time; using the declared one"
|
|
19775
|
+
);
|
|
19776
|
+
return fallback2;
|
|
19777
|
+
});
|
|
19778
|
+
this.blockTimes.set(chain, seconds);
|
|
19779
|
+
return seconds;
|
|
19780
|
+
}
|
|
19781
|
+
/** The average block time over the chain's last {@link BLOCK_TIME_SAMPLE_BLOCKS} blocks, in seconds. */
|
|
19782
|
+
async measureBlockTimeSeconds(client) {
|
|
19783
|
+
const head = await client.getBlock({ blockTag: "latest" });
|
|
19784
|
+
const span = head.number < BLOCK_TIME_SAMPLE_BLOCKS ? head.number : BLOCK_TIME_SAMPLE_BLOCKS;
|
|
19785
|
+
const tail = await client.getBlock({ blockNumber: head.number - span });
|
|
19786
|
+
const measured = Number(head.timestamp - tail.timestamp) / Number(span);
|
|
19787
|
+
if (!Number.isFinite(measured) || measured <= 0) {
|
|
19788
|
+
throw new Error(`${span} blocks span ${head.timestamp - tail.timestamp}s`);
|
|
19789
|
+
}
|
|
19790
|
+
return measured;
|
|
19791
|
+
}
|
|
19049
19792
|
/**
|
|
19050
19793
|
* Builds the signed, never-executed UserOperation that carries one limit order
|
|
19051
19794
|
* to the HyperFX orderbook.
|
|
@@ -19225,6 +19968,8 @@ var import_viem24 = require("viem");
|
|
|
19225
19968
|
var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
|
|
19226
19969
|
var FALLBACK_CALL_GAS_LIMIT = 1500000n;
|
|
19227
19970
|
var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
|
|
19971
|
+
var PRIORITY_FEE_BUMP_PERCENT = 8n;
|
|
19972
|
+
var MAX_FEE_BUMP_PERCENT = 10n;
|
|
19228
19973
|
var UserOpSender = class {
|
|
19229
19974
|
constructor(clientManager, configService, signer) {
|
|
19230
19975
|
this.clientManager = clientManager;
|
|
@@ -19405,7 +20150,11 @@ var UserOpSender = class {
|
|
|
19405
20150
|
);
|
|
19406
20151
|
return userOp;
|
|
19407
20152
|
}
|
|
19408
|
-
/**
|
|
20153
|
+
/**
|
|
20154
|
+
* Mirrors the gas-price selection used by the delegation and fill paths: Pimlico's and
|
|
20155
|
+
* Alchemy's own pricing, otherwise the chain's gas price raised to the priority fee a
|
|
20156
|
+
* rundler bundler requires, when the bundler gives one.
|
|
20157
|
+
*/
|
|
19409
20158
|
async getGasPrice(bundlerUrl, publicClient, chainId) {
|
|
19410
20159
|
const lower = bundlerUrl.toLowerCase();
|
|
19411
20160
|
if (lower.includes("pimlico.io")) {
|
|
@@ -19417,19 +20166,33 @@ var UserOpSender = class {
|
|
|
19417
20166
|
return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
|
|
19418
20167
|
}
|
|
19419
20168
|
if (lower.includes("alchemy.com")) {
|
|
19420
|
-
const [
|
|
20169
|
+
const [rundlerPriorityFee2, latestBlock2] = await Promise.all([
|
|
19421
20170
|
this.sendBundlerRpc(bundlerUrl, import_sdk8.BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
|
|
19422
20171
|
publicClient.getBlock({ blockTag: "latest" })
|
|
19423
20172
|
]);
|
|
19424
|
-
const baseFeePerGas =
|
|
20173
|
+
const baseFeePerGas = latestBlock2.baseFeePerGas ?? await publicClient.getGasPrice();
|
|
19425
20174
|
const isArbitrum = BigInt(chainId) === 42161n;
|
|
19426
20175
|
const prioBump = isArbitrum ? 0n : 25n;
|
|
19427
|
-
const maxPriorityFeePerGas = BigInt(
|
|
20176
|
+
const maxPriorityFeePerGas = BigInt(rundlerPriorityFee2) + BigInt(rundlerPriorityFee2) * prioBump / 100n;
|
|
19428
20177
|
const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
|
|
19429
20178
|
return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
|
|
19430
20179
|
}
|
|
19431
|
-
const gasPrice = await
|
|
19432
|
-
|
|
20180
|
+
const [gasPrice, rundlerPriorityFee] = await Promise.all([
|
|
20181
|
+
publicClient.getGasPrice(),
|
|
20182
|
+
(0, import_sdk8.fetchRundlerPriorityFee)(bundlerUrl)
|
|
20183
|
+
]);
|
|
20184
|
+
const fees = {
|
|
20185
|
+
maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
|
|
20186
|
+
maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
|
|
20187
|
+
};
|
|
20188
|
+
if (rundlerPriorityFee === null) return fees;
|
|
20189
|
+
const latestBlock = await publicClient.getBlock({ blockTag: "latest" });
|
|
20190
|
+
return (0, import_sdk8.applyRundlerPriorityFee)(fees, {
|
|
20191
|
+
rundlerPriorityFee,
|
|
20192
|
+
baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
|
|
20193
|
+
priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
|
|
20194
|
+
maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
|
|
20195
|
+
});
|
|
19433
20196
|
}
|
|
19434
20197
|
async sendBundlerRpc(bundlerUrl, method, params) {
|
|
19435
20198
|
const response = await fetch(bundlerUrl, {
|
|
@@ -19851,7 +20614,7 @@ var DelegationService = class {
|
|
|
19851
20614
|
// src/services/RebalancingService.ts
|
|
19852
20615
|
var import_viem28 = require("viem");
|
|
19853
20616
|
var import_sdk13 = require("@hyperbridge/sdk");
|
|
19854
|
-
var
|
|
20617
|
+
var import_decimal5 = require("decimal.js");
|
|
19855
20618
|
|
|
19856
20619
|
// src/services/rebalancers/binance.ts
|
|
19857
20620
|
var import_wallet2 = require("@binance/wallet");
|
|
@@ -20241,7 +21004,7 @@ var BinanceRebalancer = class {
|
|
|
20241
21004
|
// src/services/rebalancers/cctp.ts
|
|
20242
21005
|
var import_bridge_kit = require("@circle-fin/bridge-kit");
|
|
20243
21006
|
var import_adapter_viem_v2 = require("@circle-fin/adapter-viem-v2");
|
|
20244
|
-
var
|
|
21007
|
+
var import_chains2 = require("@circle-fin/bridge-kit/chains");
|
|
20245
21008
|
var import_sdk11 = require("@hyperbridge/sdk");
|
|
20246
21009
|
var CHAIN_ID_TO_CCTP = {
|
|
20247
21010
|
1: "Ethereum",
|
|
@@ -20294,18 +21057,18 @@ var CctpRebalancer = class {
|
|
|
20294
21057
|
{
|
|
20295
21058
|
addressContext: "user-controlled",
|
|
20296
21059
|
supportedChains: [
|
|
20297
|
-
|
|
20298
|
-
|
|
20299
|
-
|
|
20300
|
-
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
|
|
20304
|
-
|
|
20305
|
-
|
|
20306
|
-
|
|
20307
|
-
|
|
20308
|
-
|
|
21060
|
+
import_chains2.Ethereum,
|
|
21061
|
+
import_chains2.EthereumSepolia,
|
|
21062
|
+
import_chains2.Arbitrum,
|
|
21063
|
+
import_chains2.ArbitrumSepolia,
|
|
21064
|
+
import_chains2.Base,
|
|
21065
|
+
import_chains2.BaseSepolia,
|
|
21066
|
+
import_chains2.Optimism,
|
|
21067
|
+
import_chains2.OptimismSepolia,
|
|
21068
|
+
import_chains2.Polygon,
|
|
21069
|
+
import_chains2.PolygonAmoy,
|
|
21070
|
+
import_chains2.Unichain,
|
|
21071
|
+
import_chains2.UnichainSepolia
|
|
20309
21072
|
]
|
|
20310
21073
|
}
|
|
20311
21074
|
);
|
|
@@ -21002,7 +21765,7 @@ var RebalancingService = class {
|
|
|
21002
21765
|
functionName: "balanceOf",
|
|
21003
21766
|
args: [this.walletAddress]
|
|
21004
21767
|
});
|
|
21005
|
-
const balanceDecimal = new
|
|
21768
|
+
const balanceDecimal = new import_decimal5.Decimal((0, import_viem28.formatUnits)(balanceWei, Number(decimals)));
|
|
21006
21769
|
return balanceDecimal.toNumber();
|
|
21007
21770
|
} catch (error) {
|
|
21008
21771
|
this.logger.error({ error, chain, asset }, "Failed to get current balance");
|
|
@@ -21035,7 +21798,7 @@ var RebalancingService = class {
|
|
|
21035
21798
|
};
|
|
21036
21799
|
|
|
21037
21800
|
// src/core/filler.ts
|
|
21038
|
-
var
|
|
21801
|
+
var import_decimal6 = require("decimal.js");
|
|
21039
21802
|
var HYPERBRIDGE_CONNECT_TIMEOUT_MS = 3e4;
|
|
21040
21803
|
function byAmount(a, b) {
|
|
21041
21804
|
return BigInt(a.amount) < BigInt(b.amount) ? -1 : BigInt(a.amount) > BigInt(b.amount) ? 1 : 0;
|
|
@@ -21591,7 +22354,7 @@ var IntentFiller = class {
|
|
|
21591
22354
|
try {
|
|
21592
22355
|
const stratValue = await strategy.getOrderUsdValue(order);
|
|
21593
22356
|
if (stratValue != null) {
|
|
21594
|
-
inputUsdValue =
|
|
22357
|
+
inputUsdValue = import_decimal6.Decimal.max(baseInputUsd, stratValue.inputUsd);
|
|
21595
22358
|
valued = true;
|
|
21596
22359
|
break;
|
|
21597
22360
|
}
|
|
@@ -21839,7 +22602,12 @@ var IntentFiller = class {
|
|
|
21839
22602
|
else cache.clearFundingPrepends(order.id);
|
|
21840
22603
|
}
|
|
21841
22604
|
reservation = await this.holdAll([
|
|
21842
|
-
{
|
|
22605
|
+
{
|
|
22606
|
+
limitOrderId: plan.limitOrderId,
|
|
22607
|
+
payout: plan.payout,
|
|
22608
|
+
take: plan.fillerInputs[plan.leg]?.amount,
|
|
22609
|
+
takeDecimals: plan.inputDecimals
|
|
22610
|
+
}
|
|
21843
22611
|
]);
|
|
21844
22612
|
if (reservation.length === 0) {
|
|
21845
22613
|
this.logger.info(
|
|
@@ -22009,7 +22777,8 @@ var IntentFiller = class {
|
|
|
22009
22777
|
taken.push({
|
|
22010
22778
|
limitOrderId: hold.limitOrderId,
|
|
22011
22779
|
amount,
|
|
22012
|
-
...hold.take !== void 0 ? { take: hold.take.toString() } : {}
|
|
22780
|
+
...hold.take !== void 0 ? { take: hold.take.toString() } : {},
|
|
22781
|
+
...hold.takeDecimals !== void 0 ? { takeDecimals: hold.takeDecimals } : {}
|
|
22013
22782
|
});
|
|
22014
22783
|
}
|
|
22015
22784
|
return taken;
|
|
@@ -22036,6 +22805,7 @@ var IntentFiller = class {
|
|
|
22036
22805
|
}
|
|
22037
22806
|
const settled = executedHold(claimed, delivered.scaled, released);
|
|
22038
22807
|
const charged = chargedFor(settled, delivered, released);
|
|
22808
|
+
const takenIn = released > 0n && settled.takeDecimals !== void 0 ? toScaled(released, settled.takeDecimals) : null;
|
|
22039
22809
|
const settledBid = rows.find(
|
|
22040
22810
|
(row) => row.reservations.some(
|
|
22041
22811
|
(hold) => hold.limitOrderId === settled.limitOrderId && hold.amount === settled.amount && hold.take === settled.take
|
|
@@ -22054,6 +22824,7 @@ var IntentFiller = class {
|
|
|
22054
22824
|
commitment,
|
|
22055
22825
|
bid: settledBid,
|
|
22056
22826
|
amount: share.toString(),
|
|
22827
|
+
amountIn: takenIn?.toString() ?? null,
|
|
22057
22828
|
transactionHash: transactionHash ?? null
|
|
22058
22829
|
});
|
|
22059
22830
|
}
|
|
@@ -22202,44 +22973,7 @@ var IntentFiller = class {
|
|
|
22202
22973
|
// src/strategies/fx.ts
|
|
22203
22974
|
var import_sdk15 = require("@hyperbridge/sdk");
|
|
22204
22975
|
var import_viem29 = require("viem");
|
|
22205
|
-
var
|
|
22206
|
-
|
|
22207
|
-
// src/orderbook/usd.ts
|
|
22208
|
-
var import_decimal5 = require("decimal.js");
|
|
22209
|
-
function limitOrderUsdEdges(orders) {
|
|
22210
|
-
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
22211
|
-
base: normalizeSymbol(order.base),
|
|
22212
|
-
quote: normalizeSymbol(order.quote),
|
|
22213
|
-
rate: new import_decimal5.Decimal(order.price).div(ORDERBOOK_SCALE.toString())
|
|
22214
|
-
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
22215
|
-
}
|
|
22216
|
-
function usdFactorsFrom(edges) {
|
|
22217
|
-
const factors = /* @__PURE__ */ new Map();
|
|
22218
|
-
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new import_decimal5.Decimal(1));
|
|
22219
|
-
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
22220
|
-
let grew = true;
|
|
22221
|
-
while (grew) {
|
|
22222
|
-
grew = false;
|
|
22223
|
-
for (const edge of ordered) {
|
|
22224
|
-
const usdBase = factors.get(edge.base);
|
|
22225
|
-
const usdQuote = factors.get(edge.quote);
|
|
22226
|
-
if (usdBase && !usdQuote) {
|
|
22227
|
-
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
22228
|
-
grew = true;
|
|
22229
|
-
} else if (usdQuote && !usdBase) {
|
|
22230
|
-
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
22231
|
-
grew = true;
|
|
22232
|
-
}
|
|
22233
|
-
}
|
|
22234
|
-
}
|
|
22235
|
-
return factors;
|
|
22236
|
-
}
|
|
22237
|
-
function usdValueOf(factors, symbol, amount) {
|
|
22238
|
-
const factor = factors.get(normalizeSymbol(symbol));
|
|
22239
|
-
return factor ? amount.mul(factor) : null;
|
|
22240
|
-
}
|
|
22241
|
-
|
|
22242
|
-
// src/strategies/fx.ts
|
|
22976
|
+
var import_decimal7 = require("decimal.js");
|
|
22243
22977
|
function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
|
|
22244
22978
|
if (fromDecimals === toDecimals) return amount;
|
|
22245
22979
|
if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
|
|
@@ -22287,7 +23021,7 @@ var FXFiller = class _FXFiller {
|
|
|
22287
23021
|
this.maxConsecutiveClamps = configService.getMaxConsecutiveClamps();
|
|
22288
23022
|
if (confirmationPolicy) {
|
|
22289
23023
|
this.confirmationPolicy = {
|
|
22290
|
-
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new
|
|
23024
|
+
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new import_decimal7.Decimal(amountUsd))
|
|
22291
23025
|
};
|
|
22292
23026
|
}
|
|
22293
23027
|
}
|
|
@@ -22623,17 +23357,17 @@ var FXFiller = class _FXFiller {
|
|
|
22623
23357
|
const outputSymbol = this.registry.symbolFor(outputToken, destChain);
|
|
22624
23358
|
const sameAsset = outputSymbol !== null && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.base) && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.quote);
|
|
22625
23359
|
let realizedSpreadProfit = 0n;
|
|
22626
|
-
let sameAssetEdgeUsd = new
|
|
23360
|
+
let sameAssetEdgeUsd = new import_decimal7.Decimal(0);
|
|
22627
23361
|
let sameAssetProfitable = true;
|
|
22628
23362
|
if (sameAsset) {
|
|
22629
23363
|
const convertedInput = adjustDecimalsFloor(releasedInput, inputDecimals, outputDecimals);
|
|
22630
23364
|
const spread = convertedInput - finalOutputAmount;
|
|
22631
23365
|
if (spread <= 0n) sameAssetProfitable = false;
|
|
22632
23366
|
realizedSpreadProfit = adjustDecimalsFloor(spread, outputDecimals, feeTokenDecimals);
|
|
22633
|
-
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new
|
|
23367
|
+
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new import_decimal7.Decimal((0, import_viem29.formatUnits)(spread, outputDecimals)));
|
|
22634
23368
|
if (spreadUsd) sameAssetEdgeUsd = spreadUsd;
|
|
22635
23369
|
}
|
|
22636
|
-
const payoutSurplusUsd = new
|
|
23370
|
+
const payoutSurplusUsd = new import_decimal7.Decimal(0);
|
|
22637
23371
|
const { totalCostInSourceFeeToken, relayerFeeInSourceFeeToken, dispatchFee } = await this.contractService.estimateGasFillPost(order);
|
|
22638
23372
|
if (sourceChain !== destChain && dispatchFee > 0n) {
|
|
22639
23373
|
const feeToken = await this.contractService.getFeeTokenWithDecimals(destChain);
|
|
@@ -22720,6 +23454,7 @@ var FXFiller = class _FXFiller {
|
|
|
22720
23454
|
payout: toScaled(finalOutputAmount, outputDecimals),
|
|
22721
23455
|
fillerOutputs,
|
|
22722
23456
|
fillerInputs,
|
|
23457
|
+
inputDecimals,
|
|
22723
23458
|
fundingCalls: [...fundingCalls],
|
|
22724
23459
|
partialFill,
|
|
22725
23460
|
profit: totalProfit,
|
|
@@ -22998,13 +23733,13 @@ var FXFiller = class _FXFiller {
|
|
|
22998
23733
|
async getOrderUsdValue(order) {
|
|
22999
23734
|
if (!this.limitOrders || order.inputs.length === 0) return null;
|
|
23000
23735
|
const factors = usdFactorsFrom(limitOrderUsdEdges(await this.limitOrders.open()));
|
|
23001
|
-
let inputUsd = new
|
|
23736
|
+
let inputUsd = new import_decimal7.Decimal(0);
|
|
23002
23737
|
for (const input of order.inputs) {
|
|
23003
23738
|
const inputToken = (0, import_sdk15.bytes32ToBytes20)(input.token);
|
|
23004
23739
|
const symbol = this.registry.symbolFor(inputToken, order.source);
|
|
23005
23740
|
if (!symbol) return null;
|
|
23006
23741
|
const decimals = await this.contractService.getTokenDecimals(inputToken, order.source);
|
|
23007
|
-
const legUsd = usdValueOf(factors, symbol, new
|
|
23742
|
+
const legUsd = usdValueOf(factors, symbol, new import_decimal7.Decimal((0, import_viem29.formatUnits)(input.amount, decimals)));
|
|
23008
23743
|
if (!legUsd) return null;
|
|
23009
23744
|
inputUsd = inputUsd.plus(legUsd);
|
|
23010
23745
|
}
|
|
@@ -23988,8 +24723,8 @@ async function bootFiller(config, options) {
|
|
|
23988
24723
|
};
|
|
23989
24724
|
const configService = new FillerConfigService(resolvedChains, fillerConfigForService, options.loggers);
|
|
23990
24725
|
const chainConfigs = resolvedChains.map((chain) => {
|
|
23991
|
-
const
|
|
23992
|
-
return configService.getChainConfig(
|
|
24726
|
+
const chainName2 = `EVM-${chain.chainId}`;
|
|
24727
|
+
return configService.getChainConfig(chainName2);
|
|
23993
24728
|
});
|
|
23994
24729
|
let watchOnlyConfig;
|
|
23995
24730
|
const globalWatchOnly = options.watchOnlyOverride === true || config.simplex.watchOnly === true;
|
|
@@ -24027,6 +24762,8 @@ async function bootFiller(config, options) {
|
|
|
24027
24762
|
"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]."
|
|
24028
24763
|
);
|
|
24029
24764
|
}
|
|
24765
|
+
logger.info("Checking each bundler serves its chain's EntryPoint...");
|
|
24766
|
+
await assertBundlersServeEntryPoint(resolvedChains, configService, { watchOnly: watchOnlyConfig });
|
|
24030
24767
|
const chainClientManager = new ChainClientManager(configService, options.signer);
|
|
24031
24768
|
const runtimeSigner = chainClientManager.getSigner();
|
|
24032
24769
|
if (options.signer) {
|
|
@@ -24189,9 +24926,10 @@ async function bootFiller(config, options) {
|
|
|
24189
24926
|
const activity = new ActivityRecorder(options.data.activity, options.loggers, { describeToken });
|
|
24190
24927
|
activity.attach(intentFiller.monitor);
|
|
24191
24928
|
const network = resolvedChains.some((chain) => chainByChainId(chain.chainId)?.network === "testnet") ? "testnet" : "mainnet";
|
|
24929
|
+
const indexerUrl = config.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network];
|
|
24192
24930
|
void backfillOrderSummaries({
|
|
24193
24931
|
store: options.data.activity,
|
|
24194
|
-
indexerUrl
|
|
24932
|
+
indexerUrl,
|
|
24195
24933
|
fillerAddress: runtimeSigner.address,
|
|
24196
24934
|
describeToken,
|
|
24197
24935
|
onUpdated: (rows) => {
|
|
@@ -24216,10 +24954,10 @@ async function bootFiller(config, options) {
|
|
|
24216
24954
|
const token1 = {};
|
|
24217
24955
|
for (const pair of config.pairs ?? []) {
|
|
24218
24956
|
if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
|
|
24219
|
-
for (const
|
|
24220
|
-
const address = assetRegistry.getAddress(pair.token1,
|
|
24957
|
+
for (const chainName2 of configuredChainNames) {
|
|
24958
|
+
const address = assetRegistry.getAddress(pair.token1, chainName2);
|
|
24221
24959
|
if (!address) continue;
|
|
24222
|
-
const list = token1[
|
|
24960
|
+
const list = token1[chainName2] ?? (token1[chainName2] = []);
|
|
24223
24961
|
if (!list.includes(address)) list.push(address);
|
|
24224
24962
|
}
|
|
24225
24963
|
}
|
|
@@ -24257,6 +24995,15 @@ async function bootFiller(config, options) {
|
|
|
24257
24995
|
vaultVenue?.startSweeping();
|
|
24258
24996
|
started.push(() => balanceProvider.stop());
|
|
24259
24997
|
await balanceProvider.start();
|
|
24998
|
+
if (options.signer) {
|
|
24999
|
+
const inventoryRecorder = new InventoryRecorder(
|
|
25000
|
+
options.data.inventory,
|
|
25001
|
+
balanceProvider,
|
|
25002
|
+
moduleLogger(options.loggers, "inventory")
|
|
25003
|
+
);
|
|
25004
|
+
started.push(() => inventoryRecorder.stop());
|
|
25005
|
+
inventoryRecorder.start();
|
|
25006
|
+
}
|
|
24260
25007
|
const watchOnlyChains = watchOnlyConfig ? Object.entries(watchOnlyConfig).filter(([, value]) => value === true).map(([chainId]) => Number.parseInt(chainId, 10)) : [];
|
|
24261
25008
|
logger.info(
|
|
24262
25009
|
{
|
|
@@ -24302,6 +25049,8 @@ async function bootFiller(config, options) {
|
|
|
24302
25049
|
rebalancingService,
|
|
24303
25050
|
resolvedChains,
|
|
24304
25051
|
fillerAddress: runtimeSigner.address,
|
|
25052
|
+
indexerUrl,
|
|
25053
|
+
describeToken,
|
|
24305
25054
|
watchOnly: watchOnlyConfig,
|
|
24306
25055
|
config,
|
|
24307
25056
|
configPath: options.configPath,
|
|
@@ -24576,10 +25325,20 @@ var MemoryLimitOrderStore = class {
|
|
|
24576
25325
|
commitment: fill.commitment,
|
|
24577
25326
|
bid: fill.bid ?? null,
|
|
24578
25327
|
amount: fill.amount,
|
|
25328
|
+
amountIn: fill.amountIn ?? null,
|
|
24579
25329
|
transactionHash: fill.transactionHash ?? null,
|
|
24580
25330
|
filledAt: sqliteDatetime(/* @__PURE__ */ new Date())
|
|
24581
25331
|
});
|
|
24582
25332
|
}
|
|
25333
|
+
async fillHistory() {
|
|
25334
|
+
return this.fillRows.flatMap((fill) => {
|
|
25335
|
+
const order = this.orders.get(fill.limitOrderId);
|
|
25336
|
+
if (!order) return [];
|
|
25337
|
+
const { id, limitOrderId, amount, amountIn, filledAt } = fill;
|
|
25338
|
+
const { book, base, quote, side, price } = order;
|
|
25339
|
+
return [{ id, limitOrderId, book, base, quote, side, price, amount, amountIn, filledAt }];
|
|
25340
|
+
});
|
|
25341
|
+
}
|
|
24583
25342
|
async fills(limitOrderId, limit = 100) {
|
|
24584
25343
|
return this.fillRows.filter((fill) => fill.limitOrderId === limitOrderId).slice(-capLimit(limit)).reverse().map((fill) => ({ ...fill }));
|
|
24585
25344
|
}
|
|
@@ -24594,6 +25353,7 @@ var MemoryLimitOrderStore = class {
|
|
|
24594
25353
|
status: "open",
|
|
24595
25354
|
commitment: null,
|
|
24596
25355
|
orderNonce: order.orderNonce ?? "0",
|
|
25356
|
+
entryPoint: null,
|
|
24597
25357
|
bookExpiresAt: null,
|
|
24598
25358
|
bookPrice: null,
|
|
24599
25359
|
lastError: null,
|
|
@@ -24686,12 +25446,29 @@ var MemoryStateStore = class {
|
|
|
24686
25446
|
this.state = { ...state };
|
|
24687
25447
|
}
|
|
24688
25448
|
};
|
|
25449
|
+
var MemoryInventoryStore = class {
|
|
25450
|
+
constructor() {
|
|
25451
|
+
this.rows = [];
|
|
25452
|
+
}
|
|
25453
|
+
async record(balances) {
|
|
25454
|
+
this.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(/* @__PURE__ */ new Date()), balances: { ...balances } });
|
|
25455
|
+
}
|
|
25456
|
+
async latest() {
|
|
25457
|
+
const last = this.rows[this.rows.length - 1];
|
|
25458
|
+
return last ? { ...last, balances: { ...last.balances } } : null;
|
|
25459
|
+
}
|
|
25460
|
+
async since(from) {
|
|
25461
|
+
const cutoff = sqliteDatetime(from);
|
|
25462
|
+
return this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }));
|
|
25463
|
+
}
|
|
25464
|
+
};
|
|
24689
25465
|
var MemoryDataStore = class {
|
|
24690
25466
|
constructor() {
|
|
24691
25467
|
this.bids = new MemoryBidStore();
|
|
24692
25468
|
this.activity = new MemoryActivityStore();
|
|
24693
25469
|
this.state = new MemoryStateStore();
|
|
24694
25470
|
this.limitOrders = new MemoryLimitOrderStore();
|
|
25471
|
+
this.inventory = new MemoryInventoryStore();
|
|
24695
25472
|
}
|
|
24696
25473
|
};
|
|
24697
25474
|
|
|
@@ -25296,6 +26073,106 @@ var OrderScanner = class _OrderScanner {
|
|
|
25296
26073
|
}
|
|
25297
26074
|
};
|
|
25298
26075
|
|
|
26076
|
+
// src/data/indexer-inventory.ts
|
|
26077
|
+
var import_viem33 = require("viem");
|
|
26078
|
+
var READ_TIMEOUT_MS = 1e4;
|
|
26079
|
+
var SETTLED_AFTER_MS = 10 * 6e4;
|
|
26080
|
+
var BATCH = 40;
|
|
26081
|
+
var MAX_KEPT = 2e3;
|
|
26082
|
+
var IndexerInventory = class {
|
|
26083
|
+
constructor(options) {
|
|
26084
|
+
this.options = options;
|
|
26085
|
+
this.kept = /* @__PURE__ */ new Map();
|
|
26086
|
+
this.described = /* @__PURE__ */ new Map();
|
|
26087
|
+
this.fetch = options.fetch ?? fetch;
|
|
26088
|
+
}
|
|
26089
|
+
/**
|
|
26090
|
+
* A reading for each of `instants` the indexer could answer for. Never rejects: an instant it
|
|
26091
|
+
* could not be asked about is simply missing from the answer.
|
|
26092
|
+
*/
|
|
26093
|
+
async at(instants, now) {
|
|
26094
|
+
const readings = [];
|
|
26095
|
+
const wanted = [];
|
|
26096
|
+
for (const instant of new Set(instants)) {
|
|
26097
|
+
const kept = this.kept.get(instant);
|
|
26098
|
+
if (kept) readings.push(kept);
|
|
26099
|
+
else wanted.push(instant);
|
|
26100
|
+
}
|
|
26101
|
+
try {
|
|
26102
|
+
for (let index = 0; index < wanted.length; index += BATCH) {
|
|
26103
|
+
for (const reading of await this.read(wanted.slice(index, index + BATCH))) {
|
|
26104
|
+
readings.push(reading);
|
|
26105
|
+
if (reading.at <= now - SETTLED_AFTER_MS) this.keep(reading);
|
|
26106
|
+
}
|
|
26107
|
+
}
|
|
26108
|
+
} catch (err) {
|
|
26109
|
+
this.options.logger.warn(
|
|
26110
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
26111
|
+
"Could not read inventory from the indexer; carrying records by fills and sends alone"
|
|
26112
|
+
);
|
|
26113
|
+
}
|
|
26114
|
+
return readings;
|
|
26115
|
+
}
|
|
26116
|
+
keep(reading) {
|
|
26117
|
+
if (this.kept.size >= MAX_KEPT) this.kept.clear();
|
|
26118
|
+
this.kept.set(reading.at, reading);
|
|
26119
|
+
}
|
|
26120
|
+
/** One request for several instants, each its own aliased query at that moment in history. */
|
|
26121
|
+
async read(instants) {
|
|
26122
|
+
const queries = instants.map(
|
|
26123
|
+
(instant, index) => `r${index}: solverInventories(filter: { solver: { equalTo: $solver } }, blockHeight: "${Math.floor(instant)}", first: 1000) { nodes { chain tokenAddress balance } }`
|
|
26124
|
+
);
|
|
26125
|
+
const response = await this.fetch(this.options.indexerUrl, {
|
|
26126
|
+
method: "POST",
|
|
26127
|
+
headers: { "content-type": "application/json" },
|
|
26128
|
+
body: JSON.stringify({
|
|
26129
|
+
query: `query SolverInventory($solver: String!) {
|
|
26130
|
+
${queries.join("\n")}
|
|
26131
|
+
}`,
|
|
26132
|
+
variables: { solver: this.options.solver.toLowerCase() }
|
|
26133
|
+
}),
|
|
26134
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
26135
|
+
});
|
|
26136
|
+
if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
|
|
26137
|
+
const body = await response.json();
|
|
26138
|
+
if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
|
|
26139
|
+
const readings = [];
|
|
26140
|
+
for (const [index, instant] of instants.entries()) {
|
|
26141
|
+
const rows = body.data?.[`r${index}`]?.nodes;
|
|
26142
|
+
if (rows) readings.push(await this.readingOf(instant, rows));
|
|
26143
|
+
}
|
|
26144
|
+
return readings;
|
|
26145
|
+
}
|
|
26146
|
+
async readingOf(at, rows) {
|
|
26147
|
+
const balances = {};
|
|
26148
|
+
const chains = {};
|
|
26149
|
+
for (const row of rows) {
|
|
26150
|
+
const token = await this.describe(row.chain, row.tokenAddress);
|
|
26151
|
+
if (!token) continue;
|
|
26152
|
+
let amount;
|
|
26153
|
+
try {
|
|
26154
|
+
amount = Number((0, import_viem33.formatUnits)(BigInt(row.balance), token.decimals));
|
|
26155
|
+
} catch {
|
|
26156
|
+
continue;
|
|
26157
|
+
}
|
|
26158
|
+
balances[token.symbol] = (balances[token.symbol] ?? 0) + amount;
|
|
26159
|
+
chains[token.symbol] = [...chains[token.symbol] ?? [], row.chain];
|
|
26160
|
+
}
|
|
26161
|
+
return { at, balances, chains };
|
|
26162
|
+
}
|
|
26163
|
+
/** A token's symbol and decimals, remembered once both are known. */
|
|
26164
|
+
async describe(chain, token) {
|
|
26165
|
+
const key = `${chain}:${token.toLowerCase()}`;
|
|
26166
|
+
const known = this.described.get(key);
|
|
26167
|
+
if (known) return known;
|
|
26168
|
+
const { symbol, decimals } = await this.options.describeToken(chain, token);
|
|
26169
|
+
if (!symbol || decimals === null) return null;
|
|
26170
|
+
const description = { symbol, decimals };
|
|
26171
|
+
this.described.set(key, description);
|
|
26172
|
+
return description;
|
|
26173
|
+
}
|
|
26174
|
+
};
|
|
26175
|
+
|
|
25299
26176
|
// src/simplex.ts
|
|
25300
26177
|
var EVENT_MAP = {
|
|
25301
26178
|
newOrder: "order:detected",
|
|
@@ -25341,10 +26218,66 @@ var LimitOrderController = class {
|
|
|
25341
26218
|
]);
|
|
25342
26219
|
return { order, fills, bids };
|
|
25343
26220
|
}
|
|
26221
|
+
/**
|
|
26222
|
+
* What the operator's buys and sells earned over `period`, bucketed on a clock
|
|
26223
|
+
* `tzOffsetMinutes` behind UTC.
|
|
26224
|
+
*
|
|
26225
|
+
* Profit is the change the period's fills made to what the solver holds, and
|
|
26226
|
+
* it is compared with the inventory the period began with. That inventory
|
|
26227
|
+
* comes from the stored snapshots and, when the balances have been read in
|
|
26228
|
+
* full, from what is held now, each carried to the period's start by what
|
|
26229
|
+
* moved between. The indexer says what moved, deposits and withdrawals
|
|
26230
|
+
* included, wherever it has followed the solver; elsewhere the fills and
|
|
26231
|
+
* sends on record stand in. It answers whether or not an orderbook is
|
|
26232
|
+
* configured, and whether or not the indexer does.
|
|
26233
|
+
*/
|
|
26234
|
+
async profitability(period, tzOffsetMinutes = 0) {
|
|
26235
|
+
const now = Date.now();
|
|
26236
|
+
const { data, balanceProvider } = this.runtime;
|
|
26237
|
+
const [fills, snapshots, ledger] = await Promise.all([
|
|
26238
|
+
data.limitOrders.fillHistory(),
|
|
26239
|
+
data.inventory.since(/* @__PURE__ */ new Date(0)),
|
|
26240
|
+
data.activity.walletTxs(500)
|
|
26241
|
+
]);
|
|
26242
|
+
const inventory = snapshots.map((snapshot) => ({
|
|
26243
|
+
at: parseUtc(snapshot.takenAt),
|
|
26244
|
+
balances: snapshot.balances
|
|
26245
|
+
}));
|
|
26246
|
+
const held = inventoryOf(balanceProvider.getSnapshot());
|
|
26247
|
+
if (held) inventory.push({ at: now, balances: held, live: true });
|
|
26248
|
+
const outflows = [];
|
|
26249
|
+
for (const tx of ledger) {
|
|
26250
|
+
const amount = Number(tx.amount);
|
|
26251
|
+
if (tx.kind === "send" && tx.token && Number.isFinite(amount) && amount > 0) {
|
|
26252
|
+
outflows.push({ at: tx.ts, symbol: tx.token, amount });
|
|
26253
|
+
}
|
|
26254
|
+
}
|
|
26255
|
+
const instants = inventoryInstants(fills, { period, now, tzOffsetMinutes, inventory });
|
|
26256
|
+
const readings = await this.indexerInventory().at(instants, now);
|
|
26257
|
+
return summarizeProfit(fills, { period, now, tzOffsetMinutes, inventory, outflows, readings });
|
|
26258
|
+
}
|
|
26259
|
+
/** One reader for the life of the filler, so the readings it has already fetched are kept. */
|
|
26260
|
+
indexerInventory() {
|
|
26261
|
+
this.indexed ?? (this.indexed = new IndexerInventory({
|
|
26262
|
+
indexerUrl: this.runtime.indexerUrl,
|
|
26263
|
+
solver: this.runtime.fillerAddress,
|
|
26264
|
+
describeToken: this.runtime.describeToken,
|
|
26265
|
+
logger: this.runtime.loggers.get("inventory")
|
|
26266
|
+
}));
|
|
26267
|
+
return this.indexed;
|
|
26268
|
+
}
|
|
25344
26269
|
/** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
25345
26270
|
books() {
|
|
25346
26271
|
return this.service.books();
|
|
25347
26272
|
}
|
|
26273
|
+
/** Public bids and asks across solvers, optionally restricted to a route. */
|
|
26274
|
+
orderbookSnapshot(book, filters = {}) {
|
|
26275
|
+
return this.service.orderbookSnapshot(book, filters);
|
|
26276
|
+
}
|
|
26277
|
+
/** Every solver's order behind one public price level. */
|
|
26278
|
+
orderbookLevelOrders(book, level) {
|
|
26279
|
+
return this.service.orderbookLevelOrders(book, level);
|
|
26280
|
+
}
|
|
25348
26281
|
/** Creates the order, posts it, and reports what the orderbook made of it. */
|
|
25349
26282
|
async create(request) {
|
|
25350
26283
|
const posted = await this.service.create(request);
|
|
@@ -25406,10 +26339,10 @@ var PairController = class {
|
|
|
25406
26339
|
if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
|
|
25407
26340
|
if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
|
|
25408
26341
|
for (const chain of this.runtime.resolvedChains) {
|
|
25409
|
-
const
|
|
25410
|
-
const address = assetRegistry.getAddress(pair.token1,
|
|
26342
|
+
const chainName2 = formatChainKey(chain.chainId);
|
|
26343
|
+
const address = assetRegistry.getAddress(pair.token1, chainName2);
|
|
25411
26344
|
if (!address) continue;
|
|
25412
|
-
const list = balanceTokens[
|
|
26345
|
+
const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
|
|
25413
26346
|
if (!list.includes(address)) list.push(address);
|
|
25414
26347
|
}
|
|
25415
26348
|
}
|
|
@@ -25514,8 +26447,9 @@ var ChainController = class {
|
|
|
25514
26447
|
`Chain ${chainId} cannot be added as filling: this solver was started without a signer. Restart with \`signer\` to fill.`
|
|
25515
26448
|
);
|
|
25516
26449
|
}
|
|
25517
|
-
configService.addChain(resolved);
|
|
25518
26450
|
const watchOnly = chain.watchOnly ?? (this.runtime.signerless || this.runtime.globalWatchOnly);
|
|
26451
|
+
await assertBundlersServeEntryPoint([resolved], configService, { watchOnly: { [chainId]: watchOnly } });
|
|
26452
|
+
configService.addChain(resolved);
|
|
25519
26453
|
if (watchOnly) intentFiller.setWatchOnly(chainId, true);
|
|
25520
26454
|
try {
|
|
25521
26455
|
if (!inScanner) {
|
|
@@ -25607,6 +26541,9 @@ var ChainController = class {
|
|
|
25607
26541
|
if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
|
|
25608
26542
|
const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
|
|
25609
26543
|
if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
|
|
26544
|
+
await assertBundlersServeEntryPoint([{ chainId, bundlerUrl }], this.runtime.configService, {
|
|
26545
|
+
watchOnly: this.runtime.intentFiller.getWatchOnly()
|
|
26546
|
+
});
|
|
25610
26547
|
this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
|
|
25611
26548
|
this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
|
|
25612
26549
|
this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
|
|
@@ -25621,6 +26558,10 @@ var ChainController = class {
|
|
|
25621
26558
|
"Watch-only cannot be disabled: this solver was started without a signer. Restart with `signer` to fill."
|
|
25622
26559
|
);
|
|
25623
26560
|
}
|
|
26561
|
+
if (!watchOnly) {
|
|
26562
|
+
const chain = this.runtime.resolvedChains.find((resolved) => resolved.chainId === chainId);
|
|
26563
|
+
if (chain) await assertBundlersServeEntryPoint([chain], this.runtime.configService);
|
|
26564
|
+
}
|
|
25624
26565
|
this.runtime.intentFiller.setWatchOnly(chainId, watchOnly);
|
|
25625
26566
|
this.syncWatchOnlyToConfig();
|
|
25626
26567
|
await this.persist();
|