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