@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.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((chainName) => registry.getAddress(symbol, chainName) !== null);
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 chainName of chainNames) {
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, chainName)?.toLowerCase();
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 ${chainName} \u2014 symbols must map to distinct contracts`
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.getEntryPointV08Address(chain);
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 scaled = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE : inputAmount * ORDERBOOK_SCALE / price;
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 scaled / unit * unit;
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 scaled = side === "BID" ? divCeil(outputAmount * ORDERBOOK_SCALE, price) : divCeil(outputAmount * price, ORDERBOOK_SCALE);
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(scaled, unit) * unit;
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 scaled = { amountIn: 0n, amountOut: 0n };
3338
+ const scaled2 = { amountIn: 0n, amountOut: 0n };
3248
3339
  for (const name of ["amountIn", "amountOut"]) {
3249
3340
  try {
3250
- scaled[name] = fromHuman(request[name] ?? "");
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 (scaled[name] <= 0n) throw new LimitOrderValidationError(`${name} must be greater than zero`);
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 && scaled.amountOut < BigInt(floor.size)) {
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: scaled.amountIn, amountOut: scaled.amountOut };
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
- if (BigInt(remaining.remaining) < floor) {
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: remaining.remaining, floor: floor.toString() },
3505
+ { id, remaining: order.remaining, floor: floor.toString() },
3412
3506
  "Limit order worked down past the dust floor; closing it"
3413
3507
  );
3414
- if (remaining.commitment) await this.withdraw(remaining.commitment);
3415
- const closed = await this.store.setPosting(id, {
3416
- commitment: null,
3417
- bookExpiresAt: null,
3418
- bookPrice: null,
3419
- orderNonce: remaining.orderNonce,
3420
- status: "filled",
3421
- lastError: null
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(await this.store.setStatus(id, "resizing"));
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())) continue;
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
- return { result: (await this.buildAndSubmit(order, retried)).result, orderNonce: retried };
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 import_decimal2 = require("decimal.js");
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: "solver",
17009
- type: "address",
17010
- internalType: "address"
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
- import_decimal2.Decimal.config({ precision: 28, rounding: 4 });
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
- for (const chainName of chainNames) {
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(chainName);
19152
+ await this.getFeeTokenWithDecimals(chainName2);
18451
19153
  } catch (err) {
18452
- this.logger.warn({ err, chain: chainName }, "Could not prewarm fee token decimals");
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 import_decimal2.Decimal(0);
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 import_decimal2.Decimal((0, import_viem23.formatUnits)(input.amount, decimals));
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 nominal block time; it is deliberately rounded
19038
- * up, since erring long costs a slightly stale quote while erring short silently drops
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
- /** Mirrors the gas-price selection used by the delegation and fill paths. */
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 [rundlerPriorityFee, latestBlock] = await Promise.all([
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 = latestBlock.baseFeePerGas ?? await publicClient.getGasPrice();
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(rundlerPriorityFee) + BigInt(rundlerPriorityFee) * prioBump / 100n;
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 publicClient.getGasPrice();
19432
- return { maxFeePerGas: gasPrice + gasPrice * 10n / 100n, maxPriorityFeePerGas: gasPrice + gasPrice * 8n / 100n };
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 import_decimal3 = require("decimal.js");
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 import_chains = require("@circle-fin/bridge-kit/chains");
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
- import_chains.Ethereum,
20298
- import_chains.EthereumSepolia,
20299
- import_chains.Arbitrum,
20300
- import_chains.ArbitrumSepolia,
20301
- import_chains.Base,
20302
- import_chains.BaseSepolia,
20303
- import_chains.Optimism,
20304
- import_chains.OptimismSepolia,
20305
- import_chains.Polygon,
20306
- import_chains.PolygonAmoy,
20307
- import_chains.Unichain,
20308
- import_chains.UnichainSepolia
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 import_decimal3.Decimal((0, import_viem28.formatUnits)(balanceWei, Number(decimals)));
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 import_decimal4 = require("decimal.js");
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 = import_decimal4.Decimal.max(baseInputUsd, stratValue.inputUsd);
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
- { limitOrderId: plan.limitOrderId, payout: plan.payout, take: plan.fillerInputs[plan.leg]?.amount }
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 import_decimal6 = require("decimal.js");
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 import_decimal6.Decimal(amountUsd))
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 import_decimal6.Decimal(0);
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 import_decimal6.Decimal((0, import_viem29.formatUnits)(spread, outputDecimals)));
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 import_decimal6.Decimal(0);
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 import_decimal6.Decimal(0);
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 import_decimal6.Decimal((0, import_viem29.formatUnits)(input.amount, decimals)));
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 chainName = `EVM-${chain.chainId}`;
23992
- return configService.getChainConfig(chainName);
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: config.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network],
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 chainName of configuredChainNames) {
24220
- const address = assetRegistry.getAddress(pair.token1, chainName);
24957
+ for (const chainName2 of configuredChainNames) {
24958
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
24221
24959
  if (!address) continue;
24222
- const list = token1[chainName] ?? (token1[chainName] = []);
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 chainName = formatChainKey(chain.chainId);
25410
- const address = assetRegistry.getAddress(pair.token1, chainName);
26342
+ const chainName2 = formatChainKey(chain.chainId);
26343
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
25411
26344
  if (!address) continue;
25412
- const list = balanceTokens[chainName] ?? (balanceTokens[chainName] = []);
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();