@hyperbridge/simplex 0.16.13 → 0.17.1

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
@@ -625,6 +625,20 @@ var ConfirmationPolicy = class {
625
625
  // src/core/filler.ts
626
626
  var import_p_queue = __toESM(require("p-queue"), 1);
627
627
 
628
+ // src/config/bundlers.ts
629
+ var HYPERBRIDGE_BUNDLER_URLS = {
630
+ 1: "https://bundler.polytope.technology/ethereum",
631
+ 56: "https://bundler.polytope.technology/bsc",
632
+ 97: "https://bundler.polytope.technology/bsc-chapel",
633
+ 137: "https://bundler.polytope.technology/polygon",
634
+ 8453: "https://bundler.polytope.technology/base",
635
+ 42161: "https://bundler.polytope.technology/arbitrum",
636
+ 80002: "https://bundler.polytope.technology/polygon-amoy"
637
+ };
638
+ function hyperbridgeBundlerUrl(chainId) {
639
+ return HYPERBRIDGE_BUNDLER_URLS[chainId];
640
+ }
641
+
628
642
  // src/config/asset-registry.ts
629
643
  var import_viem = require("viem");
630
644
  var BUILTIN_ASSETS = {
@@ -636,7 +650,8 @@ var BUILTIN_ASSETS = {
636
650
  ZARP: { resolve: (r, chain) => r.getAssetBySymbol(chain, "ZARP") },
637
651
  EURC: { resolve: (r, chain) => r.getAssetBySymbol(chain, "EURC") },
638
652
  XSGD: { resolve: (r, chain) => r.getAssetBySymbol(chain, "XSGD") },
639
- TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") }
653
+ TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") },
654
+ BRIDGE: { resolve: (r, chain) => r.getAssetBySymbol(chain, "BRIDGE") }
640
655
  };
641
656
  var USD_STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "DAI"]);
642
657
  function normalizeSymbol(symbol) {
@@ -774,7 +789,7 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
774
789
  const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
775
790
  for (const pair of pairs) {
776
791
  for (const symbol of [pair.token0, pair.token1]) {
777
- const resolvesSomewhere = chainNames.some((chainName) => registry.getAddress(symbol, chainName) !== null);
792
+ const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
778
793
  if (!resolvesSomewhere) {
779
794
  throw new Error(
780
795
  `pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
@@ -782,15 +797,15 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
782
797
  }
783
798
  }
784
799
  }
785
- for (const chainName of chainNames) {
800
+ for (const chainName2 of chainNames) {
786
801
  const addressOwner = /* @__PURE__ */ new Map();
787
802
  for (const symbol of pairSymbols) {
788
- const address = registry.getAddress(symbol, chainName)?.toLowerCase();
803
+ const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
789
804
  if (!address) continue;
790
805
  const owner = addressOwner.get(address);
791
806
  if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
792
807
  throw new Error(
793
- `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName} \u2014 symbols must map to distinct contracts`
808
+ `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
794
809
  );
795
810
  }
796
811
  addressOwner.set(address, symbol);
@@ -1244,7 +1259,7 @@ async function resolveChainConfigs(chains, options = {}) {
1244
1259
  "RPC endpoint could not report its chainId at startup; keeping it for the quorum to judge per call"
1245
1260
  );
1246
1261
  }
1247
- return { chainId: first.chainId, rpcUrls, bundlerUrl: chain.bundlerUrl };
1262
+ return { chainId: first.chainId, rpcUrls, bundlerUrl: hyperbridgeBundlerUrl(first.chainId) };
1248
1263
  })
1249
1264
  );
1250
1265
  }
@@ -1313,12 +1328,6 @@ var FillerConfigService = class {
1313
1328
  }
1314
1329
  this.rpcOverrides.set(chainId, validateRpcUrls(rpcUrls));
1315
1330
  }
1316
- setBundlerUrl(chainId, bundlerUrl) {
1317
- if (!this.rpcOverrides.has(chainId)) {
1318
- throw new Error(`Chain ${chainId} is not configured`);
1319
- }
1320
- this.bundlerUrls.set(chainId, bundlerUrl);
1321
- }
1322
1331
  /** Replaces the rebalancing config at runtime; trigger checks read it live. */
1323
1332
  setRebalancing(rebalancing) {
1324
1333
  if (this.fillerConfig) this.fillerConfig.rebalancing = rebalancing;
@@ -1495,7 +1504,7 @@ var FillerConfigService = class {
1495
1504
  return this.fillerConfig?.substratePrivateKey;
1496
1505
  }
1497
1506
  getEntryPointAddress(chain) {
1498
- return this.chainConfigService.getEntryPointV08Address(chain);
1507
+ return this.chainConfigService.getEntryPointAddress(chain);
1499
1508
  }
1500
1509
  getSolverAccountContractAddress(chain) {
1501
1510
  return this.chainConfigService.getSolverAccountAddress(chain);
@@ -1680,9 +1689,6 @@ function validateConfig(config, cliWatchOnly = false) {
1680
1689
  if (!Array.isArray(chain.rpcUrls) || chain.rpcUrls.length === 0 || chain.rpcUrls.some((u) => !u)) {
1681
1690
  throw new Error("Each chain configuration must have rpcUrls (a non-empty array of strings)");
1682
1691
  }
1683
- if (!chain.bundlerUrl) {
1684
- throw new Error("Each chain configuration must have bundlerUrl");
1685
- }
1686
1692
  }
1687
1693
  const concurrent = config.simplex.maxConcurrentOrders;
1688
1694
  if (concurrent !== void 0) {
@@ -2572,7 +2578,8 @@ var INIT_CHAINS = [
2572
2578
  network: "mainnet",
2573
2579
  alchemySubdomain: "eth-mainnet",
2574
2580
  explorerUrl: external_links_default.chainExplorers.ethereum,
2575
- defaultRpcUrls: FREE_RPC_URLS[1]
2581
+ defaultRpcUrls: FREE_RPC_URLS[1],
2582
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[1]
2576
2583
  },
2577
2584
  {
2578
2585
  chainId: 42161,
@@ -2581,7 +2588,8 @@ var INIT_CHAINS = [
2581
2588
  network: "mainnet",
2582
2589
  alchemySubdomain: "arb-mainnet",
2583
2590
  explorerUrl: external_links_default.chainExplorers.arbitrum,
2584
- defaultRpcUrls: FREE_RPC_URLS[42161]
2591
+ defaultRpcUrls: FREE_RPC_URLS[42161],
2592
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[42161]
2585
2593
  },
2586
2594
  {
2587
2595
  chainId: 8453,
@@ -2590,7 +2598,8 @@ var INIT_CHAINS = [
2590
2598
  network: "mainnet",
2591
2599
  alchemySubdomain: "base-mainnet",
2592
2600
  explorerUrl: external_links_default.chainExplorers.base,
2593
- defaultRpcUrls: FREE_RPC_URLS[8453]
2601
+ defaultRpcUrls: FREE_RPC_URLS[8453],
2602
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[8453]
2594
2603
  },
2595
2604
  {
2596
2605
  chainId: 137,
@@ -2599,7 +2608,8 @@ var INIT_CHAINS = [
2599
2608
  network: "mainnet",
2600
2609
  alchemySubdomain: "polygon-mainnet",
2601
2610
  explorerUrl: external_links_default.chainExplorers.polygon,
2602
- defaultRpcUrls: FREE_RPC_URLS[137]
2611
+ defaultRpcUrls: FREE_RPC_URLS[137],
2612
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[137]
2603
2613
  },
2604
2614
  {
2605
2615
  chainId: 56,
@@ -2608,7 +2618,8 @@ var INIT_CHAINS = [
2608
2618
  network: "mainnet",
2609
2619
  alchemySubdomain: "bnb-mainnet",
2610
2620
  explorerUrl: external_links_default.chainExplorers.bnb,
2611
- defaultRpcUrls: FREE_RPC_URLS[56]
2621
+ defaultRpcUrls: FREE_RPC_URLS[56],
2622
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[56]
2612
2623
  },
2613
2624
  {
2614
2625
  chainId: 11155111,
@@ -2640,7 +2651,8 @@ var INIT_CHAINS = [
2640
2651
  label: "Polygon Amoy",
2641
2652
  network: "testnet",
2642
2653
  alchemySubdomain: "polygon-amoy",
2643
- explorerUrl: external_links_default.chainExplorers.polygonAmoy
2654
+ explorerUrl: external_links_default.chainExplorers.polygonAmoy,
2655
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[80002]
2644
2656
  },
2645
2657
  {
2646
2658
  chainId: 97,
@@ -2648,7 +2660,8 @@ var INIT_CHAINS = [
2648
2660
  label: "BSC Chapel",
2649
2661
  network: "testnet",
2650
2662
  alchemySubdomain: "bnb-testnet",
2651
- explorerUrl: external_links_default.chainExplorers.bnbChapel
2663
+ explorerUrl: external_links_default.chainExplorers.bnbChapel,
2664
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[97]
2652
2665
  }
2653
2666
  ];
2654
2667
  function chainByChainId(chainId) {
@@ -3122,6 +3135,8 @@ var LimitOrderService = class {
3122
3135
  this.delegationService = delegationService;
3123
3136
  this.startingNonce = startingNonce;
3124
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();
3125
3140
  this.logger = loggers.get("limit-orders");
3126
3141
  }
3127
3142
  /**
@@ -3215,6 +3230,7 @@ var LimitOrderService = class {
3215
3230
  bookExpiresAt: null,
3216
3231
  bookPrice: null,
3217
3232
  orderNonce: order.orderNonce,
3233
+ entryPoint: null,
3218
3234
  status: "cancelled",
3219
3235
  lastError: null
3220
3236
  }),
@@ -3481,24 +3497,32 @@ var LimitOrderService = class {
3481
3497
  async resize(remaining, delivered) {
3482
3498
  const id = remaining.id;
3483
3499
  const floor = await this.dustFloor(remaining);
3484
- 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) {
3485
3504
  this.logger.info(
3486
- { id, remaining: remaining.remaining, floor: floor.toString() },
3505
+ { id, remaining: order.remaining, floor: floor.toString() },
3487
3506
  "Limit order worked down past the dust floor; closing it"
3488
3507
  );
3489
- if (remaining.commitment) await this.withdraw(remaining.commitment);
3490
- const closed = await this.store.setPosting(id, {
3491
- commitment: null,
3492
- bookExpiresAt: null,
3493
- bookPrice: null,
3494
- orderNonce: remaining.orderNonce,
3495
- status: "filled",
3496
- lastError: null
3497
- });
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
+ );
3498
3522
  if (closed) this.report({ kind: "filled", order: closed });
3499
3523
  return closed;
3500
3524
  }
3501
- const resized = await this.repost(await this.store.setStatus(id, "resizing"));
3525
+ const resized = await this.repost(order);
3502
3526
  if (resized) this.report({ kind: "resized", order: resized, delivered });
3503
3527
  return resized;
3504
3528
  }
@@ -3511,7 +3535,7 @@ var LimitOrderService = class {
3511
3535
  */
3512
3536
  async repost(order) {
3513
3537
  if (!order) return null;
3514
- if (isLocal(order)) return this.store.setStatus(order.id, "open");
3538
+ if (isLocal(order)) return this.store.setStatus(order.id, "open", null, POSTABLE);
3515
3539
  if (hasExpired(order.expiresAt, /* @__PURE__ */ new Date())) return this.retire(order);
3516
3540
  if (order.commitment) {
3517
3541
  const withdrawn = await this.withdraw(order.commitment);
@@ -3522,10 +3546,13 @@ var LimitOrderService = class {
3522
3546
  { id: order.id, commitment: order.commitment, err: message },
3523
3547
  "Could not clear the old entry; leaving the posting alone rather than adding a second"
3524
3548
  );
3525
- return this.store.setStatus(order.id, "open", message);
3549
+ return this.store.setStatus(order.id, "open", message, POSTABLE);
3526
3550
  }
3527
3551
  }
3528
- 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
+ }
3529
3556
  const nonce = (BigInt(order.orderNonce) + 1n).toString();
3530
3557
  return (await this.post({ ...order, commitment: null, orderNonce: nonce })).order;
3531
3558
  }
@@ -3568,7 +3595,9 @@ var LimitOrderService = class {
3568
3595
  * They drift apart when a request never got an answer or the process died
3569
3596
  * between two of them: an entry the orderbook still lists that nothing here
3570
3597
  * owns, an order here whose entry has gone, or an entry the orderbook has cut
3571
- * 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.
3572
3601
  */
3573
3602
  async reconcile(now = /* @__PURE__ */ new Date()) {
3574
3603
  const [entries, live] = await Promise.all([this.postedOrders(), this.live()]);
@@ -3595,7 +3624,17 @@ var LimitOrderService = class {
3595
3624
  }
3596
3625
  for (const order of live) {
3597
3626
  if (isLocal(order)) continue;
3598
- 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
+ }
3599
3638
  if (sinceMs(order.updatedAt, now) < POST_GRACE_MS) continue;
3600
3639
  this.logger.warn({ id: order.id }, "Limit order has no entry on the orderbook; posting it again");
3601
3640
  try {
@@ -3607,6 +3646,45 @@ var LimitOrderService = class {
3607
3646
  }
3608
3647
  return report;
3609
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
+ }
3610
3688
  /**
3611
3689
  * Lowers every open order's `remaining` to what the solver's account will still
3612
3690
  * pay for it, and puts the order back on the book at that size.
@@ -3688,6 +3766,7 @@ var LimitOrderService = class {
3688
3766
  bookExpiresAt: null,
3689
3767
  bookPrice: null,
3690
3768
  orderNonce: order.orderNonce,
3769
+ entryPoint: null,
3691
3770
  status: "expired",
3692
3771
  lastError: null
3693
3772
  });
@@ -3744,11 +3823,11 @@ var LimitOrderService = class {
3744
3823
  }
3745
3824
  /** Builds and submits the posting, then writes the orderbook's answer onto the row. */
3746
3825
  async post(order) {
3747
- const { result, orderNonce } = await this.submit(order);
3826
+ const { result, orderNonce, entryPoint } = await this.submit(order);
3748
3827
  if (result.kind === "accepted" || result.kind === "unchanged") {
3749
3828
  const posted = result.order;
3750
3829
  this.logger.info(
3751
- { id: order.id, commitment: posted.commitment, price: posted.price },
3830
+ { id: order.id, commitment: posted.commitment, price: posted.price, entryPoint },
3752
3831
  "Limit order posted to the orderbook"
3753
3832
  );
3754
3833
  const stored = await this.store.setPosting(
@@ -3758,6 +3837,10 @@ var LimitOrderService = class {
3758
3837
  bookExpiresAt: posted.expiresAt,
3759
3838
  bookPrice: posted.price,
3760
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,
3761
3844
  status: "open",
3762
3845
  lastError: null
3763
3846
  },
@@ -3787,6 +3870,7 @@ var LimitOrderService = class {
3787
3870
  bookExpiresAt: null,
3788
3871
  bookPrice: null,
3789
3872
  orderNonce: orderNonce.toString(),
3873
+ entryPoint: null,
3790
3874
  status: "open",
3791
3875
  lastError: message
3792
3876
  }),
@@ -3809,22 +3893,30 @@ var LimitOrderService = class {
3809
3893
  * only the second one.
3810
3894
  */
3811
3895
  async submit(order) {
3896
+ const entryPoint = this.entryPointFor(order.fillChain);
3812
3897
  const orderNonce = BigInt(order.orderNonce);
3813
- const attempt = await this.buildAndSubmit(order, orderNonce);
3898
+ const attempt = await this.buildAndSubmit(order, orderNonce, entryPoint);
3814
3899
  const first = attempt.result;
3815
3900
  if (first.kind !== "rejected" || first.code !== "REPLAYED" && first.code !== "ORDER_EXISTS") {
3816
- return { result: first, orderNonce };
3901
+ return { result: first, orderNonce, entryPoint };
3817
3902
  }
3818
3903
  if (first.code === "ORDER_EXISTS" && attempt.commitment) {
3819
3904
  const live = await this.entryAt(order, attempt.commitment);
3820
3905
  if (live) {
3821
3906
  this.logger.info({ id: order.id, commitment: live.commitment }, "This posting is already on the orderbook");
3822
- return { result: { kind: "unchanged", order: live }, orderNonce };
3907
+ return { result: { kind: "unchanged", order: live }, orderNonce, entryPoint };
3823
3908
  }
3824
3909
  }
3825
3910
  this.logger.warn({ id: order.id, code: first.code }, "Orderbook has seen this op before; reposting on a new nonce");
3826
3911
  const retried = orderNonce + 1n;
3827
- 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;
3828
3920
  }
3829
3921
  /**
3830
3922
  * The entry the orderbook holds at `commitment`, if it holds one. A lookup that
@@ -3840,8 +3932,8 @@ var LimitOrderService = class {
3840
3932
  }
3841
3933
  }
3842
3934
  /** The orderbook's answer, and the commitment the op we sent hashes to. */
3843
- async buildAndSubmit(order, orderNonce) {
3844
- const { commitment, userOp } = await this.buildUserOp(order, orderNonce);
3935
+ async buildAndSubmit(order, orderNonce, entryPoint) {
3936
+ const { commitment, userOp } = await this.buildUserOp(order, orderNonce, entryPoint);
3845
3937
  try {
3846
3938
  return { result: await this.client.submitOrder(userOp), commitment };
3847
3939
  } catch (err) {
@@ -3851,7 +3943,7 @@ var LimitOrderService = class {
3851
3943
  throw err;
3852
3944
  }
3853
3945
  }
3854
- async buildUserOp(order, orderNonce) {
3946
+ async buildUserOp(order, orderNonce, entryPointAddress) {
3855
3947
  const baseToken = this.assetRegistry.getAddress(order.base, order.fillChain);
3856
3948
  const quoteToken = this.assetRegistry.getAddress(order.quote, order.fillChain);
3857
3949
  const [baseDecimals, quoteDecimals] = await Promise.all([
@@ -3866,10 +3958,6 @@ var LimitOrderService = class {
3866
3958
  baseDecimals,
3867
3959
  quoteDecimals
3868
3960
  });
3869
- const entryPointAddress = this.configService.getEntryPointAddress(order.fillChain);
3870
- if (!entryPointAddress) {
3871
- throw new LimitOrderValidationError(`No EntryPoint is configured for ${order.fillChain}`);
3872
- }
3873
3961
  return this.contractService.prepareLimitOrderUserOp({
3874
3962
  fillChain: order.fillChain,
3875
3963
  entryPointAddress,
@@ -3900,6 +3988,12 @@ function hasExpired(expiresAt, now) {
3900
3988
  const at = Date.parse(expiresAt);
3901
3989
  return !Number.isNaN(at) && at <= now.getTime();
3902
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
+ }
3903
3997
  function sinceMs(updatedAt, now) {
3904
3998
  const written = Date.parse(`${updatedAt.replace(" ", "T")}Z`);
3905
3999
  return Number.isNaN(written) ? Number.POSITIVE_INFINITY : now.getTime() - written;
@@ -17527,9 +17621,9 @@ var INTENT_GATEWAY_V2_ABI = [
17527
17621
  internalType: "bytes32"
17528
17622
  },
17529
17623
  {
17530
- name: "solver",
17531
- type: "address",
17532
- internalType: "address"
17624
+ name: "userOpHash",
17625
+ type: "bytes32",
17626
+ internalType: "bytes32"
17533
17627
  },
17534
17628
  {
17535
17629
  name: "signature",
@@ -18904,10 +18998,13 @@ var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
18904
18998
  var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
18905
18999
  var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
18906
19000
  var BID_DISCOVERY_PAD_SECONDS = 30;
19001
+ var BLOCK_TIME_SAMPLE_BLOCKS = 1000n;
19002
+ var FALLBACK_BLOCK_TIME_SECONDS = 2;
18907
19003
  var ContractInteractionService = class {
18908
19004
  constructor(clientManager, configService, signer, sharedCacheService) {
18909
19005
  this.clientManager = clientManager;
18910
19006
  this.sdkHelperCache = /* @__PURE__ */ new Map();
19007
+ this.blockTimes = /* @__PURE__ */ new Map();
18911
19008
  this.logger = moduleLogger(configService.loggers, "contract-service");
18912
19009
  this.configService = configService;
18913
19010
  this.cacheService = sharedCacheService || new CacheService();
@@ -18967,11 +19064,20 @@ var ContractInteractionService = class {
18967
19064
  try {
18968
19065
  const chainIds = this.configService.getConfiguredChainIds();
18969
19066
  const chainNames = chainIds.map((id) => `EVM-${id}`);
18970
- for (const chainName of chainNames) {
19067
+ await Promise.all(
19068
+ chainNames.map(async (chainName2) => {
19069
+ try {
19070
+ await this.blockTimeSeconds(chainName2);
19071
+ } catch (err) {
19072
+ this.logger.warn({ err, chain: chainName2 }, "Could not measure the block time at startup");
19073
+ }
19074
+ })
19075
+ );
19076
+ for (const chainName2 of chainNames) {
18971
19077
  try {
18972
- await this.getFeeTokenWithDecimals(chainName);
19078
+ await this.getFeeTokenWithDecimals(chainName2);
18973
19079
  } catch (err) {
18974
- this.logger.warn({ err, chain: chainName }, "Could not prewarm fee token decimals");
19080
+ this.logger.warn({ err, chain: chainName2 }, "Could not prewarm fee token decimals");
18975
19081
  }
18976
19082
  }
18977
19083
  for (const destChain of chainNames) {
@@ -19556,18 +19662,59 @@ var ContractInteractionService = class {
19556
19662
  *
19557
19663
  * Operators configure seconds because that is the unit the risk is actually in, but the
19558
19664
  * contract compares against block numbers so `order.deadline` and this read the same
19559
- * clock. The conversion uses the chain's nominal block time; it is deliberately rounded
19560
- * up, since erring long costs a slightly stale quote while erring short silently drops
19561
- * winnable bids.
19665
+ * clock. The conversion uses the chain's block time from {@link blockTimeSeconds}; it is
19666
+ * deliberately rounded up, since erring long costs a slightly stale quote while erring
19667
+ * short silently drops winnable bids.
19562
19668
  */
19563
19669
  async bidValidUntilBlock(chain) {
19564
19670
  const client = this.clientManager.getPublicClient(chain);
19565
- const currentBlock = await client.getBlockNumber();
19566
- const blockTimeMs = client.chain?.blockTime;
19567
- const blockTimeSec = blockTimeMs ? blockTimeMs / 1e3 : 2;
19671
+ const [currentBlock, blockTimeSec] = await Promise.all([client.getBlockNumber(), this.blockTimeSeconds(chain)]);
19568
19672
  const windowSec = this.configService.getBidValiditySeconds() + BID_DISCOVERY_PAD_SECONDS;
19569
19673
  return currentBlock + BigInt(Math.ceil(windowSec / blockTimeSec));
19570
19674
  }
19675
+ /**
19676
+ * Seconds per block on `chain`. Measured once, when simplex starts ({@link initCache}), and
19677
+ * kept until it stops. A chain with no measurement yet, because it was added later or its
19678
+ * blocks could not be read at startup, is measured at its first bid.
19679
+ *
19680
+ * The nominal `Chain.blockTime` cannot be relied on alone: it goes stale when a chain
19681
+ * speeds up (BSC makes a block every 0.45s against a declared 0.75s) and some chains
19682
+ * declare none. Converting at a slower block time than the chain's ends a bid early, so
19683
+ * the faster of the measured and the declared figure is used, and a stalled or idle
19684
+ * stretch in the sample never shortens a bid.
19685
+ *
19686
+ * Blocks that cannot be read fall back to the declared figure, then to
19687
+ * {@link FALLBACK_BLOCK_TIME_SECONDS}. That answer is not kept.
19688
+ */
19689
+ blockTimeSeconds(chain) {
19690
+ const known = this.blockTimes.get(chain);
19691
+ if (known) return known;
19692
+ const client = this.clientManager.getPublicClient(chain);
19693
+ const declaredMs = client.chain?.blockTime;
19694
+ const declared = declaredMs ? declaredMs / 1e3 : void 0;
19695
+ const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
19696
+ this.blockTimes.delete(chain);
19697
+ const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19698
+ this.logger.warn(
19699
+ { err, chain, seconds: fallback2 },
19700
+ "Could not measure the block time; using the declared one"
19701
+ );
19702
+ return fallback2;
19703
+ });
19704
+ this.blockTimes.set(chain, seconds);
19705
+ return seconds;
19706
+ }
19707
+ /** The average block time over the chain's last {@link BLOCK_TIME_SAMPLE_BLOCKS} blocks, in seconds. */
19708
+ async measureBlockTimeSeconds(client) {
19709
+ const head = await client.getBlock({ blockTag: "latest" });
19710
+ const span = head.number < BLOCK_TIME_SAMPLE_BLOCKS ? head.number : BLOCK_TIME_SAMPLE_BLOCKS;
19711
+ const tail = await client.getBlock({ blockNumber: head.number - span });
19712
+ const measured = Number(head.timestamp - tail.timestamp) / Number(span);
19713
+ if (!Number.isFinite(measured) || measured <= 0) {
19714
+ throw new Error(`${span} blocks span ${head.timestamp - tail.timestamp}s`);
19715
+ }
19716
+ return measured;
19717
+ }
19571
19718
  /**
19572
19719
  * Builds the signed, never-executed UserOperation that carries one limit order
19573
19720
  * to the HyperFX orderbook.
@@ -19747,6 +19894,8 @@ var import_viem24 = require("viem");
19747
19894
  var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
19748
19895
  var FALLBACK_CALL_GAS_LIMIT = 1500000n;
19749
19896
  var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
19897
+ var PRIORITY_FEE_BUMP_PERCENT = 8n;
19898
+ var MAX_FEE_BUMP_PERCENT = 10n;
19750
19899
  var UserOpSender = class {
19751
19900
  constructor(clientManager, configService, signer) {
19752
19901
  this.clientManager = clientManager;
@@ -19773,7 +19922,7 @@ var UserOpSender = class {
19773
19922
  let auth;
19774
19923
  let fees;
19775
19924
  try {
19776
- fees = await this.getGasPrice(bundlerUrl, publicClient, chainId);
19925
+ fees = await this.getGasPrice(bundlerUrl, publicClient);
19777
19926
  const gasForPrefund = gas ?? {
19778
19927
  verificationGasLimit: FALLBACK_VERIFICATION_GAS_LIMIT,
19779
19928
  callGasLimit: FALLBACK_CALL_GAS_LIMIT,
@@ -19927,31 +20076,27 @@ var UserOpSender = class {
19927
20076
  );
19928
20077
  return userOp;
19929
20078
  }
19930
- /** Mirrors the gas-price selection used by the delegation and fill paths. */
19931
- async getGasPrice(bundlerUrl, publicClient, chainId) {
19932
- const lower = bundlerUrl.toLowerCase();
19933
- if (lower.includes("pimlico.io")) {
19934
- const res = await this.sendBundlerRpc(
19935
- bundlerUrl,
19936
- import_sdk8.BundlerMethod.PIMLICO_GET_USER_OPERATION_GAS_PRICE,
19937
- []
19938
- );
19939
- return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
19940
- }
19941
- if (lower.includes("alchemy.com")) {
19942
- const [rundlerPriorityFee, latestBlock] = await Promise.all([
19943
- this.sendBundlerRpc(bundlerUrl, import_sdk8.BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
19944
- publicClient.getBlock({ blockTag: "latest" })
19945
- ]);
19946
- const baseFeePerGas = latestBlock.baseFeePerGas ?? await publicClient.getGasPrice();
19947
- const isArbitrum = BigInt(chainId) === 42161n;
19948
- const prioBump = isArbitrum ? 0n : 25n;
19949
- const maxPriorityFeePerGas = BigInt(rundlerPriorityFee) + BigInt(rundlerPriorityFee) * prioBump / 100n;
19950
- const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
19951
- return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
19952
- }
19953
- const gasPrice = await publicClient.getGasPrice();
19954
- return { maxFeePerGas: gasPrice + gasPrice * 10n / 100n, maxPriorityFeePerGas: gasPrice + gasPrice * 8n / 100n };
20079
+ /**
20080
+ * Prices the op for the rundler bundler, as the fill path does: its suggested fees, or the
20081
+ * chain's gas price raised to the priority fee it requires where it suggests none.
20082
+ */
20083
+ async getGasPrice(bundlerUrl, publicClient) {
20084
+ const [gasPrice, latestBlock] = await Promise.all([
20085
+ publicClient.getGasPrice(),
20086
+ publicClient.getBlock({ blockTag: "latest" })
20087
+ ]);
20088
+ return (0, import_sdk8.rundlerUserOperationFees)(
20089
+ bundlerUrl,
20090
+ {
20091
+ maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
20092
+ maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
20093
+ },
20094
+ {
20095
+ baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
20096
+ priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
20097
+ maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
20098
+ }
20099
+ );
19955
20100
  }
19956
20101
  async sendBundlerRpc(bundlerUrl, method, params) {
19957
20102
  const response = await fetch(bundlerUrl, {
@@ -23672,6 +23817,82 @@ var TokenSender = class {
23672
23817
  }
23673
23818
  };
23674
23819
 
23820
+ // src/services/bundler-preflight.ts
23821
+ var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
23822
+ async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
23823
+ const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
23824
+ const logger = moduleLogger(configService.loggers, "bundler-preflight");
23825
+ const failures = await Promise.all(
23826
+ chains.map(async ({ chainId, bundlerUrl }) => {
23827
+ if (options.watchOnly?.[chainId] === true) return void 0;
23828
+ if (!bundlerUrl?.trim()) {
23829
+ return `Hyperbridge runs no bundler for ${chainName(chainId)}, so simplex cannot fill there; set it watch-only`;
23830
+ }
23831
+ const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
23832
+ if (!entryPoint) return void 0;
23833
+ return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
23834
+ })
23835
+ );
23836
+ const messages = failures.filter((failure) => failure !== void 0);
23837
+ if (messages.length > 0) throw new Error(messages.join("; "));
23838
+ }
23839
+ function servesEntryPoint(listed, entryPoint) {
23840
+ return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
23841
+ }
23842
+ function describeEntryPoints(listed) {
23843
+ return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
23844
+ }
23845
+ function bundlerHost(url) {
23846
+ try {
23847
+ return new URL(url).host || "an unparseable URL";
23848
+ } catch {
23849
+ return "an unparseable URL";
23850
+ }
23851
+ }
23852
+ function chainName(chainId) {
23853
+ const label = chainByChainId(chainId)?.label;
23854
+ return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
23855
+ }
23856
+ async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
23857
+ const host = bundlerHost(bundlerUrl);
23858
+ const subject = `Bundler ${host} for ${chainName(chainId)}`;
23859
+ const signal = AbortSignal.timeout(timeoutMs);
23860
+ let listed;
23861
+ try {
23862
+ listed = await supportedEntryPoints(bundlerUrl, signal);
23863
+ } catch (err) {
23864
+ const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
23865
+ logger.warn(
23866
+ { chain: formatChainKey(chainId), bundler: host },
23867
+ `${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
23868
+ );
23869
+ return void 0;
23870
+ }
23871
+ if (servesEntryPoint(listed, entryPoint)) return void 0;
23872
+ return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
23873
+ }
23874
+ async function supportedEntryPoints(bundlerUrl, signal) {
23875
+ const response = await fetch(bundlerUrl, {
23876
+ method: "POST",
23877
+ headers: { "Content-Type": "application/json" },
23878
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
23879
+ signal
23880
+ });
23881
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
23882
+ const json = await response.json();
23883
+ if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
23884
+ const { result } = json;
23885
+ if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
23886
+ throw new Error("the result is not a list of addresses");
23887
+ }
23888
+ return result;
23889
+ }
23890
+ function errorReason(err) {
23891
+ if (!(err instanceof Error)) return String(err);
23892
+ const { cause } = err;
23893
+ return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
23894
+ }
23895
+
23675
23896
  // src/orderbook/lifecycle.ts
23676
23897
  var EXPIRY_SWEEP_MS = 3e4;
23677
23898
  var LimitOrderLifecycle = class {
@@ -24482,8 +24703,8 @@ async function bootFiller(config, options) {
24482
24703
  };
24483
24704
  const configService = new FillerConfigService(resolvedChains, fillerConfigForService, options.loggers);
24484
24705
  const chainConfigs = resolvedChains.map((chain) => {
24485
- const chainName = `EVM-${chain.chainId}`;
24486
- return configService.getChainConfig(chainName);
24706
+ const chainName2 = `EVM-${chain.chainId}`;
24707
+ return configService.getChainConfig(chainName2);
24487
24708
  });
24488
24709
  let watchOnlyConfig;
24489
24710
  const globalWatchOnly = options.watchOnlyOverride === true || config.simplex.watchOnly === true;
@@ -24521,6 +24742,8 @@ async function bootFiller(config, options) {
24521
24742
  "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]."
24522
24743
  );
24523
24744
  }
24745
+ logger.info("Checking each bundler serves its chain's EntryPoint...");
24746
+ await assertBundlersServeEntryPoint(resolvedChains, configService, { watchOnly: watchOnlyConfig });
24524
24747
  const chainClientManager = new ChainClientManager(configService, options.signer);
24525
24748
  const runtimeSigner = chainClientManager.getSigner();
24526
24749
  if (options.signer) {
@@ -24711,10 +24934,10 @@ async function bootFiller(config, options) {
24711
24934
  const token1 = {};
24712
24935
  for (const pair of config.pairs ?? []) {
24713
24936
  if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
24714
- for (const chainName of configuredChainNames) {
24715
- const address = assetRegistry.getAddress(pair.token1, chainName);
24937
+ for (const chainName2 of configuredChainNames) {
24938
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
24716
24939
  if (!address) continue;
24717
- const list = token1[chainName] ?? (token1[chainName] = []);
24940
+ const list = token1[chainName2] ?? (token1[chainName2] = []);
24718
24941
  if (!list.includes(address)) list.push(address);
24719
24942
  }
24720
24943
  }
@@ -25110,6 +25333,7 @@ var MemoryLimitOrderStore = class {
25110
25333
  status: "open",
25111
25334
  commitment: null,
25112
25335
  orderNonce: order.orderNonce ?? "0",
25336
+ entryPoint: null,
25113
25337
  bookExpiresAt: null,
25114
25338
  bookPrice: null,
25115
25339
  lastError: null,
@@ -25745,7 +25969,7 @@ var OrderScanner = class _OrderScanner {
25745
25969
  async addChainNow(chain) {
25746
25970
  if (this.closed) throw new Error("This OrderScanner is closed");
25747
25971
  const rpcUrls = validateRpcUrls(chain.rpcUrls);
25748
- const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls, bundlerUrl: chain.bundlerUrl ?? "" }], {
25972
+ const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls }], {
25749
25973
  loggers: this.loggers,
25750
25974
  tolerateUnreachable: true
25751
25975
  }))[0].chainId;
@@ -26095,10 +26319,10 @@ var PairController = class {
26095
26319
  if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
26096
26320
  if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
26097
26321
  for (const chain of this.runtime.resolvedChains) {
26098
- const chainName = formatChainKey(chain.chainId);
26099
- const address = assetRegistry.getAddress(pair.token1, chainName);
26322
+ const chainName2 = formatChainKey(chain.chainId);
26323
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
26100
26324
  if (!address) continue;
26101
- const list = balanceTokens[chainName] ?? (balanceTokens[chainName] = []);
26325
+ const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
26102
26326
  if (!list.includes(address)) list.push(address);
26103
26327
  }
26104
26328
  }
@@ -26177,10 +26401,7 @@ var ChainController = class {
26177
26401
  async add(chain) {
26178
26402
  return this.serialise(async () => {
26179
26403
  validateRpcUrls(chain.rpcUrls);
26180
- if (!chain.bundlerUrl?.trim()) {
26181
- throw new Error("A bundler URL is required to submit fill UserOperations");
26182
- }
26183
- const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }]);
26404
+ const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls }]);
26184
26405
  const { chainId } = resolved;
26185
26406
  const { configService, config, intentFiller } = this.runtime;
26186
26407
  if (configService.getConfiguredChainIds().includes(chainId)) {
@@ -26203,12 +26424,13 @@ var ChainController = class {
26203
26424
  `Chain ${chainId} cannot be added as filling: this solver was started without a signer. Restart with \`signer\` to fill.`
26204
26425
  );
26205
26426
  }
26206
- configService.addChain(resolved);
26207
26427
  const watchOnly = chain.watchOnly ?? (this.runtime.signerless || this.runtime.globalWatchOnly);
26428
+ await assertBundlersServeEntryPoint([resolved], configService, { watchOnly: { [chainId]: watchOnly } });
26429
+ configService.addChain(resolved);
26208
26430
  if (watchOnly) intentFiller.setWatchOnly(chainId, true);
26209
26431
  try {
26210
26432
  if (!inScanner) {
26211
- await this.scanner.addChain({ rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl, chainId });
26433
+ await this.scanner.addChain({ rpcUrls: chain.rpcUrls, chainId });
26212
26434
  }
26213
26435
  this.runtime.chainClientManager.invalidate(formatChainKey(chainId));
26214
26436
  await intentFiller.addChain(configService.getChainConfig(formatChainKey(chainId)));
@@ -26225,7 +26447,7 @@ var ChainController = class {
26225
26447
  this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
26226
26448
  }
26227
26449
  this.runtime.resolvedChains.push(resolved);
26228
- config.chains = [...config.chains, { rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }];
26450
+ config.chains = [...config.chains, { rpcUrls: chain.rpcUrls }];
26229
26451
  if (Object.keys(confirmationPolicies).length > 0) config.confirmationPolicies = confirmationPolicies;
26230
26452
  this.syncWatchOnlyToConfig();
26231
26453
  await this.persist();
@@ -26270,9 +26492,7 @@ var ChainController = class {
26270
26492
  validateRpcUrls(rpcUrls);
26271
26493
  const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
26272
26494
  if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
26273
- const [probed] = await resolveChainConfigs([
26274
- { rpcUrls, bundlerUrl: this.runtime.resolvedChains[index].bundlerUrl ?? "" }
26275
- ]);
26495
+ const [probed] = await resolveChainConfigs([{ rpcUrls }]);
26276
26496
  if (probed.chainId !== chainId) {
26277
26497
  throw new Error(`Those endpoints answer for chain ${probed.chainId}, not ${chainId}`);
26278
26498
  }
@@ -26291,17 +26511,6 @@ var ChainController = class {
26291
26511
  await this.persist();
26292
26512
  });
26293
26513
  }
26294
- async setBundlerUrl(chainId, bundlerUrl) {
26295
- return this.serialise(async () => {
26296
- if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
26297
- const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
26298
- if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
26299
- this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
26300
- this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
26301
- this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
26302
- await this.persist();
26303
- });
26304
- }
26305
26514
  /** Monitor without filling. Takes effect on the next order. */
26306
26515
  async setWatchOnly(chainId, watchOnly) {
26307
26516
  return this.serialise(async () => {
@@ -26310,6 +26519,10 @@ var ChainController = class {
26310
26519
  "Watch-only cannot be disabled: this solver was started without a signer. Restart with `signer` to fill."
26311
26520
  );
26312
26521
  }
26522
+ if (!watchOnly) {
26523
+ const chain = this.runtime.resolvedChains.find((resolved) => resolved.chainId === chainId);
26524
+ if (chain) await assertBundlersServeEntryPoint([chain], this.runtime.configService);
26525
+ }
26313
26526
  this.runtime.intentFiller.setWatchOnly(chainId, watchOnly);
26314
26527
  this.syncWatchOnlyToConfig();
26315
26528
  await this.persist();