@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.js CHANGED
@@ -563,6 +563,20 @@ var ConfirmationPolicy = class {
563
563
  // src/core/filler.ts
564
564
  import pQueue from "p-queue";
565
565
 
566
+ // src/config/bundlers.ts
567
+ var HYPERBRIDGE_BUNDLER_URLS = {
568
+ 1: "https://bundler.polytope.technology/ethereum",
569
+ 56: "https://bundler.polytope.technology/bsc",
570
+ 97: "https://bundler.polytope.technology/bsc-chapel",
571
+ 137: "https://bundler.polytope.technology/polygon",
572
+ 8453: "https://bundler.polytope.technology/base",
573
+ 42161: "https://bundler.polytope.technology/arbitrum",
574
+ 80002: "https://bundler.polytope.technology/polygon-amoy"
575
+ };
576
+ function hyperbridgeBundlerUrl(chainId) {
577
+ return HYPERBRIDGE_BUNDLER_URLS[chainId];
578
+ }
579
+
566
580
  // src/config/asset-registry.ts
567
581
  import { isAddress } from "viem";
568
582
  var BUILTIN_ASSETS = {
@@ -574,7 +588,8 @@ var BUILTIN_ASSETS = {
574
588
  ZARP: { resolve: (r, chain) => r.getAssetBySymbol(chain, "ZARP") },
575
589
  EURC: { resolve: (r, chain) => r.getAssetBySymbol(chain, "EURC") },
576
590
  XSGD: { resolve: (r, chain) => r.getAssetBySymbol(chain, "XSGD") },
577
- TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") }
591
+ TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") },
592
+ BRIDGE: { resolve: (r, chain) => r.getAssetBySymbol(chain, "BRIDGE") }
578
593
  };
579
594
  var USD_STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "DAI"]);
580
595
  function normalizeSymbol(symbol) {
@@ -712,7 +727,7 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
712
727
  const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
713
728
  for (const pair of pairs) {
714
729
  for (const symbol of [pair.token0, pair.token1]) {
715
- const resolvesSomewhere = chainNames.some((chainName) => registry.getAddress(symbol, chainName) !== null);
730
+ const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
716
731
  if (!resolvesSomewhere) {
717
732
  throw new Error(
718
733
  `pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
@@ -720,15 +735,15 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
720
735
  }
721
736
  }
722
737
  }
723
- for (const chainName of chainNames) {
738
+ for (const chainName2 of chainNames) {
724
739
  const addressOwner = /* @__PURE__ */ new Map();
725
740
  for (const symbol of pairSymbols) {
726
- const address = registry.getAddress(symbol, chainName)?.toLowerCase();
741
+ const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
727
742
  if (!address) continue;
728
743
  const owner = addressOwner.get(address);
729
744
  if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
730
745
  throw new Error(
731
- `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName} \u2014 symbols must map to distinct contracts`
746
+ `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
732
747
  );
733
748
  }
734
749
  addressOwner.set(address, symbol);
@@ -1182,7 +1197,7 @@ async function resolveChainConfigs(chains, options = {}) {
1182
1197
  "RPC endpoint could not report its chainId at startup; keeping it for the quorum to judge per call"
1183
1198
  );
1184
1199
  }
1185
- return { chainId: first.chainId, rpcUrls, bundlerUrl: chain.bundlerUrl };
1200
+ return { chainId: first.chainId, rpcUrls, bundlerUrl: hyperbridgeBundlerUrl(first.chainId) };
1186
1201
  })
1187
1202
  );
1188
1203
  }
@@ -1251,12 +1266,6 @@ var FillerConfigService = class {
1251
1266
  }
1252
1267
  this.rpcOverrides.set(chainId, validateRpcUrls(rpcUrls));
1253
1268
  }
1254
- setBundlerUrl(chainId, bundlerUrl) {
1255
- if (!this.rpcOverrides.has(chainId)) {
1256
- throw new Error(`Chain ${chainId} is not configured`);
1257
- }
1258
- this.bundlerUrls.set(chainId, bundlerUrl);
1259
- }
1260
1269
  /** Replaces the rebalancing config at runtime; trigger checks read it live. */
1261
1270
  setRebalancing(rebalancing) {
1262
1271
  if (this.fillerConfig) this.fillerConfig.rebalancing = rebalancing;
@@ -1433,7 +1442,7 @@ var FillerConfigService = class {
1433
1442
  return this.fillerConfig?.substratePrivateKey;
1434
1443
  }
1435
1444
  getEntryPointAddress(chain) {
1436
- return this.chainConfigService.getEntryPointV08Address(chain);
1445
+ return this.chainConfigService.getEntryPointAddress(chain);
1437
1446
  }
1438
1447
  getSolverAccountContractAddress(chain) {
1439
1448
  return this.chainConfigService.getSolverAccountAddress(chain);
@@ -1618,9 +1627,6 @@ function validateConfig(config, cliWatchOnly = false) {
1618
1627
  if (!Array.isArray(chain.rpcUrls) || chain.rpcUrls.length === 0 || chain.rpcUrls.some((u) => !u)) {
1619
1628
  throw new Error("Each chain configuration must have rpcUrls (a non-empty array of strings)");
1620
1629
  }
1621
- if (!chain.bundlerUrl) {
1622
- throw new Error("Each chain configuration must have bundlerUrl");
1623
- }
1624
1630
  }
1625
1631
  const concurrent = config.simplex.maxConcurrentOrders;
1626
1632
  if (concurrent !== void 0) {
@@ -2510,7 +2516,8 @@ var INIT_CHAINS = [
2510
2516
  network: "mainnet",
2511
2517
  alchemySubdomain: "eth-mainnet",
2512
2518
  explorerUrl: external_links_default.chainExplorers.ethereum,
2513
- defaultRpcUrls: FREE_RPC_URLS[1]
2519
+ defaultRpcUrls: FREE_RPC_URLS[1],
2520
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[1]
2514
2521
  },
2515
2522
  {
2516
2523
  chainId: 42161,
@@ -2519,7 +2526,8 @@ var INIT_CHAINS = [
2519
2526
  network: "mainnet",
2520
2527
  alchemySubdomain: "arb-mainnet",
2521
2528
  explorerUrl: external_links_default.chainExplorers.arbitrum,
2522
- defaultRpcUrls: FREE_RPC_URLS[42161]
2529
+ defaultRpcUrls: FREE_RPC_URLS[42161],
2530
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[42161]
2523
2531
  },
2524
2532
  {
2525
2533
  chainId: 8453,
@@ -2528,7 +2536,8 @@ var INIT_CHAINS = [
2528
2536
  network: "mainnet",
2529
2537
  alchemySubdomain: "base-mainnet",
2530
2538
  explorerUrl: external_links_default.chainExplorers.base,
2531
- defaultRpcUrls: FREE_RPC_URLS[8453]
2539
+ defaultRpcUrls: FREE_RPC_URLS[8453],
2540
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[8453]
2532
2541
  },
2533
2542
  {
2534
2543
  chainId: 137,
@@ -2537,7 +2546,8 @@ var INIT_CHAINS = [
2537
2546
  network: "mainnet",
2538
2547
  alchemySubdomain: "polygon-mainnet",
2539
2548
  explorerUrl: external_links_default.chainExplorers.polygon,
2540
- defaultRpcUrls: FREE_RPC_URLS[137]
2549
+ defaultRpcUrls: FREE_RPC_URLS[137],
2550
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[137]
2541
2551
  },
2542
2552
  {
2543
2553
  chainId: 56,
@@ -2546,7 +2556,8 @@ var INIT_CHAINS = [
2546
2556
  network: "mainnet",
2547
2557
  alchemySubdomain: "bnb-mainnet",
2548
2558
  explorerUrl: external_links_default.chainExplorers.bnb,
2549
- defaultRpcUrls: FREE_RPC_URLS[56]
2559
+ defaultRpcUrls: FREE_RPC_URLS[56],
2560
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[56]
2550
2561
  },
2551
2562
  {
2552
2563
  chainId: 11155111,
@@ -2578,7 +2589,8 @@ var INIT_CHAINS = [
2578
2589
  label: "Polygon Amoy",
2579
2590
  network: "testnet",
2580
2591
  alchemySubdomain: "polygon-amoy",
2581
- explorerUrl: external_links_default.chainExplorers.polygonAmoy
2592
+ explorerUrl: external_links_default.chainExplorers.polygonAmoy,
2593
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[80002]
2582
2594
  },
2583
2595
  {
2584
2596
  chainId: 97,
@@ -2586,7 +2598,8 @@ var INIT_CHAINS = [
2586
2598
  label: "BSC Chapel",
2587
2599
  network: "testnet",
2588
2600
  alchemySubdomain: "bnb-testnet",
2589
- explorerUrl: external_links_default.chainExplorers.bnbChapel
2601
+ explorerUrl: external_links_default.chainExplorers.bnbChapel,
2602
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[97]
2590
2603
  }
2591
2604
  ];
2592
2605
  function chainByChainId(chainId) {
@@ -2859,7 +2872,7 @@ function describe(err) {
2859
2872
 
2860
2873
  // src/orderbook/limit-orders.ts
2861
2874
  import { randomBytes, randomUUID } from "node:crypto";
2862
- import { getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
2875
+ import { ENTRY_POINT_V08, getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
2863
2876
 
2864
2877
  // src/orderbook/amounts.ts
2865
2878
  import { keccak256, stringToBytes } from "viem";
@@ -3060,6 +3073,8 @@ var LimitOrderService = class {
3060
3073
  this.delegationService = delegationService;
3061
3074
  this.startingNonce = startingNonce;
3062
3075
  this.vaultBalances = vaultBalances;
3076
+ /** Re-signs in flight, by order id. A fill's resize waits one out rather than reposting alongside it. */
3077
+ this.resigning = /* @__PURE__ */ new Map();
3063
3078
  this.logger = loggers.get("limit-orders");
3064
3079
  }
3065
3080
  /**
@@ -3153,6 +3168,7 @@ var LimitOrderService = class {
3153
3168
  bookExpiresAt: null,
3154
3169
  bookPrice: null,
3155
3170
  orderNonce: order.orderNonce,
3171
+ entryPoint: null,
3156
3172
  status: "cancelled",
3157
3173
  lastError: null
3158
3174
  }),
@@ -3419,24 +3435,32 @@ var LimitOrderService = class {
3419
3435
  async resize(remaining, delivered) {
3420
3436
  const id = remaining.id;
3421
3437
  const floor = await this.dustFloor(remaining);
3422
- if (BigInt(remaining.remaining) < floor) {
3438
+ for (let resigning = this.resigning.get(id); resigning; resigning = this.resigning.get(id)) await resigning;
3439
+ const order = await this.store.setStatus(id, "resizing", null, POSTABLE);
3440
+ if (!order) return null;
3441
+ if (BigInt(order.remaining) < floor) {
3423
3442
  this.logger.info(
3424
- { id, remaining: remaining.remaining, floor: floor.toString() },
3443
+ { id, remaining: order.remaining, floor: floor.toString() },
3425
3444
  "Limit order worked down past the dust floor; closing it"
3426
3445
  );
3427
- if (remaining.commitment) await this.withdraw(remaining.commitment);
3428
- const closed = await this.store.setPosting(id, {
3429
- commitment: null,
3430
- bookExpiresAt: null,
3431
- bookPrice: null,
3432
- orderNonce: remaining.orderNonce,
3433
- status: "filled",
3434
- lastError: null
3435
- });
3446
+ if (order.commitment) await this.withdraw(order.commitment);
3447
+ const closed = await this.store.setPosting(
3448
+ id,
3449
+ {
3450
+ commitment: null,
3451
+ bookExpiresAt: null,
3452
+ bookPrice: null,
3453
+ orderNonce: order.orderNonce,
3454
+ entryPoint: null,
3455
+ status: "filled",
3456
+ lastError: null
3457
+ },
3458
+ POSTABLE
3459
+ );
3436
3460
  if (closed) this.report({ kind: "filled", order: closed });
3437
3461
  return closed;
3438
3462
  }
3439
- const resized = await this.repost(await this.store.setStatus(id, "resizing"));
3463
+ const resized = await this.repost(order);
3440
3464
  if (resized) this.report({ kind: "resized", order: resized, delivered });
3441
3465
  return resized;
3442
3466
  }
@@ -3449,7 +3473,7 @@ var LimitOrderService = class {
3449
3473
  */
3450
3474
  async repost(order) {
3451
3475
  if (!order) return null;
3452
- if (isLocal(order)) return this.store.setStatus(order.id, "open");
3476
+ if (isLocal(order)) return this.store.setStatus(order.id, "open", null, POSTABLE);
3453
3477
  if (hasExpired(order.expiresAt, /* @__PURE__ */ new Date())) return this.retire(order);
3454
3478
  if (order.commitment) {
3455
3479
  const withdrawn = await this.withdraw(order.commitment);
@@ -3460,10 +3484,13 @@ var LimitOrderService = class {
3460
3484
  { id: order.id, commitment: order.commitment, err: message },
3461
3485
  "Could not clear the old entry; leaving the posting alone rather than adding a second"
3462
3486
  );
3463
- return this.store.setStatus(order.id, "open", message);
3487
+ return this.store.setStatus(order.id, "open", message, POSTABLE);
3464
3488
  }
3465
3489
  }
3466
- await this.store.setStatus(order.id, "resizing");
3490
+ if (!await this.store.setStatus(order.id, "resizing", null, POSTABLE)) {
3491
+ this.logger.info({ id: order.id }, "Limit order closed while it was being reposted; leaving it closed");
3492
+ return null;
3493
+ }
3467
3494
  const nonce = (BigInt(order.orderNonce) + 1n).toString();
3468
3495
  return (await this.post({ ...order, commitment: null, orderNonce: nonce })).order;
3469
3496
  }
@@ -3506,7 +3533,9 @@ var LimitOrderService = class {
3506
3533
  * They drift apart when a request never got an answer or the process died
3507
3534
  * between two of them: an entry the orderbook still lists that nothing here
3508
3535
  * owns, an order here whose entry has gone, or an entry the orderbook has cut
3509
- * down because the solver cannot cover what it quoted.
3536
+ * down because the solver cannot cover what it quoted. A posted op is also
3537
+ * signed over one EntryPoint, so when the fill chain moves to another, every
3538
+ * entry signed before the move is withdrawn and posted again.
3510
3539
  */
3511
3540
  async reconcile(now = /* @__PURE__ */ new Date()) {
3512
3541
  const [entries, live] = await Promise.all([this.postedOrders(), this.live()]);
@@ -3533,7 +3562,17 @@ var LimitOrderService = class {
3533
3562
  }
3534
3563
  for (const order of live) {
3535
3564
  if (isLocal(order)) continue;
3536
- if (order.commitment && found.has(order.commitment.toLowerCase())) continue;
3565
+ if (order.commitment && found.has(order.commitment.toLowerCase())) {
3566
+ try {
3567
+ if (await this.resign(order)) report.reposted++;
3568
+ } catch (err) {
3569
+ this.logger.error(
3570
+ { id: order.id, err },
3571
+ "Could not sign the limit order again for its chain's EntryPoint"
3572
+ );
3573
+ }
3574
+ continue;
3575
+ }
3537
3576
  if (sinceMs(order.updatedAt, now) < POST_GRACE_MS) continue;
3538
3577
  this.logger.warn({ id: order.id }, "Limit order has no entry on the orderbook; posting it again");
3539
3578
  try {
@@ -3545,6 +3584,45 @@ var LimitOrderService = class {
3545
3584
  }
3546
3585
  return report;
3547
3586
  }
3587
+ /**
3588
+ * Withdraws a posting signed for an EntryPoint its fill chain no longer uses,
3589
+ * and posts it again signed for the one it uses now. Answers whether a new
3590
+ * posting landed.
3591
+ *
3592
+ * The row is read again and claimed out of `open` first: a fill can have worked
3593
+ * it down or started its own repost since reconciliation listed it, and that
3594
+ * write is the one to follow. A fill that lands once the claim is taken waits
3595
+ * in `resize` for this to finish. The EntryPoint is only written with a posting
3596
+ * that landed, so a pass cut short anywhere leaves a row the next pass still
3597
+ * reads as stale, or as having no entry, and finishes the job.
3598
+ */
3599
+ async resign(listed) {
3600
+ const entryPoint = this.configService.getEntryPointAddress(listed.fillChain);
3601
+ if (!entryPoint || sameAddress(signedFor(listed), entryPoint)) return false;
3602
+ const order = await this.store.get(listed.id);
3603
+ if (!order || order.status !== "open" || order.commitment !== listed.commitment) return false;
3604
+ if (sameAddress(signedFor(order), entryPoint) || this.resigning.has(order.id)) return false;
3605
+ const resigned = this.signAgain(order, entryPoint);
3606
+ this.resigning.set(
3607
+ order.id,
3608
+ resigned.catch(() => false)
3609
+ );
3610
+ try {
3611
+ return await resigned;
3612
+ } finally {
3613
+ this.resigning.delete(order.id);
3614
+ }
3615
+ }
3616
+ async signAgain(order, entryPoint) {
3617
+ const claimed = await this.store.setStatus(order.id, "resizing", null, ["open"]);
3618
+ if (!claimed) return false;
3619
+ this.logger.warn(
3620
+ { id: order.id, commitment: order.commitment, signedFor: signedFor(order), entryPoint },
3621
+ "Limit order was signed for an EntryPoint its chain no longer uses; signing it again"
3622
+ );
3623
+ const posted = await this.repost(claimed);
3624
+ return !!posted?.commitment && sameAddress(signedFor(posted), entryPoint);
3625
+ }
3548
3626
  /**
3549
3627
  * Lowers every open order's `remaining` to what the solver's account will still
3550
3628
  * pay for it, and puts the order back on the book at that size.
@@ -3626,6 +3704,7 @@ var LimitOrderService = class {
3626
3704
  bookExpiresAt: null,
3627
3705
  bookPrice: null,
3628
3706
  orderNonce: order.orderNonce,
3707
+ entryPoint: null,
3629
3708
  status: "expired",
3630
3709
  lastError: null
3631
3710
  });
@@ -3682,11 +3761,11 @@ var LimitOrderService = class {
3682
3761
  }
3683
3762
  /** Builds and submits the posting, then writes the orderbook's answer onto the row. */
3684
3763
  async post(order) {
3685
- const { result, orderNonce } = await this.submit(order);
3764
+ const { result, orderNonce, entryPoint } = await this.submit(order);
3686
3765
  if (result.kind === "accepted" || result.kind === "unchanged") {
3687
3766
  const posted = result.order;
3688
3767
  this.logger.info(
3689
- { id: order.id, commitment: posted.commitment, price: posted.price },
3768
+ { id: order.id, commitment: posted.commitment, price: posted.price, entryPoint },
3690
3769
  "Limit order posted to the orderbook"
3691
3770
  );
3692
3771
  const stored = await this.store.setPosting(
@@ -3696,6 +3775,10 @@ var LimitOrderService = class {
3696
3775
  bookExpiresAt: posted.expiresAt,
3697
3776
  bookPrice: posted.price,
3698
3777
  orderNonce: orderNonce.toString(),
3778
+ // An entry the orderbook already held was signed by an earlier attempt,
3779
+ // for whichever EntryPoint was current then. Recorded as unknown, which
3780
+ // reconciliation reads as v0.8 and signs again once the chain is past it.
3781
+ entryPoint: result.kind === "accepted" ? entryPoint : null,
3699
3782
  status: "open",
3700
3783
  lastError: null
3701
3784
  },
@@ -3725,6 +3808,7 @@ var LimitOrderService = class {
3725
3808
  bookExpiresAt: null,
3726
3809
  bookPrice: null,
3727
3810
  orderNonce: orderNonce.toString(),
3811
+ entryPoint: null,
3728
3812
  status: "open",
3729
3813
  lastError: message
3730
3814
  }),
@@ -3747,22 +3831,30 @@ var LimitOrderService = class {
3747
3831
  * only the second one.
3748
3832
  */
3749
3833
  async submit(order) {
3834
+ const entryPoint = this.entryPointFor(order.fillChain);
3750
3835
  const orderNonce = BigInt(order.orderNonce);
3751
- const attempt = await this.buildAndSubmit(order, orderNonce);
3836
+ const attempt = await this.buildAndSubmit(order, orderNonce, entryPoint);
3752
3837
  const first = attempt.result;
3753
3838
  if (first.kind !== "rejected" || first.code !== "REPLAYED" && first.code !== "ORDER_EXISTS") {
3754
- return { result: first, orderNonce };
3839
+ return { result: first, orderNonce, entryPoint };
3755
3840
  }
3756
3841
  if (first.code === "ORDER_EXISTS" && attempt.commitment) {
3757
3842
  const live = await this.entryAt(order, attempt.commitment);
3758
3843
  if (live) {
3759
3844
  this.logger.info({ id: order.id, commitment: live.commitment }, "This posting is already on the orderbook");
3760
- return { result: { kind: "unchanged", order: live }, orderNonce };
3845
+ return { result: { kind: "unchanged", order: live }, orderNonce, entryPoint };
3761
3846
  }
3762
3847
  }
3763
3848
  this.logger.warn({ id: order.id, code: first.code }, "Orderbook has seen this op before; reposting on a new nonce");
3764
3849
  const retried = orderNonce + 1n;
3765
- return { result: (await this.buildAndSubmit(order, retried)).result, orderNonce: retried };
3850
+ const { result } = await this.buildAndSubmit(order, retried, entryPoint);
3851
+ return { result, orderNonce: retried, entryPoint };
3852
+ }
3853
+ /** The EntryPoint a posting on `chain` is signed for now. */
3854
+ entryPointFor(chain) {
3855
+ const entryPoint = this.configService.getEntryPointAddress(chain);
3856
+ if (!entryPoint) throw new LimitOrderValidationError(`No EntryPoint is configured for ${chain}`);
3857
+ return entryPoint;
3766
3858
  }
3767
3859
  /**
3768
3860
  * The entry the orderbook holds at `commitment`, if it holds one. A lookup that
@@ -3778,8 +3870,8 @@ var LimitOrderService = class {
3778
3870
  }
3779
3871
  }
3780
3872
  /** The orderbook's answer, and the commitment the op we sent hashes to. */
3781
- async buildAndSubmit(order, orderNonce) {
3782
- const { commitment, userOp } = await this.buildUserOp(order, orderNonce);
3873
+ async buildAndSubmit(order, orderNonce, entryPoint) {
3874
+ const { commitment, userOp } = await this.buildUserOp(order, orderNonce, entryPoint);
3783
3875
  try {
3784
3876
  return { result: await this.client.submitOrder(userOp), commitment };
3785
3877
  } catch (err) {
@@ -3789,7 +3881,7 @@ var LimitOrderService = class {
3789
3881
  throw err;
3790
3882
  }
3791
3883
  }
3792
- async buildUserOp(order, orderNonce) {
3884
+ async buildUserOp(order, orderNonce, entryPointAddress) {
3793
3885
  const baseToken = this.assetRegistry.getAddress(order.base, order.fillChain);
3794
3886
  const quoteToken = this.assetRegistry.getAddress(order.quote, order.fillChain);
3795
3887
  const [baseDecimals, quoteDecimals] = await Promise.all([
@@ -3804,10 +3896,6 @@ var LimitOrderService = class {
3804
3896
  baseDecimals,
3805
3897
  quoteDecimals
3806
3898
  });
3807
- const entryPointAddress = this.configService.getEntryPointAddress(order.fillChain);
3808
- if (!entryPointAddress) {
3809
- throw new LimitOrderValidationError(`No EntryPoint is configured for ${order.fillChain}`);
3810
- }
3811
3899
  return this.contractService.prepareLimitOrderUserOp({
3812
3900
  fillChain: order.fillChain,
3813
3901
  entryPointAddress,
@@ -3838,6 +3926,12 @@ function hasExpired(expiresAt, now) {
3838
3926
  const at = Date.parse(expiresAt);
3839
3927
  return !Number.isNaN(at) && at <= now.getTime();
3840
3928
  }
3929
+ function signedFor(order) {
3930
+ return order.entryPoint ?? ENTRY_POINT_V08;
3931
+ }
3932
+ function sameAddress(a, b) {
3933
+ return a.toLowerCase() === b.toLowerCase();
3934
+ }
3841
3935
  function sinceMs(updatedAt, now) {
3842
3936
  const written = Date.parse(`${updatedAt.replace(" ", "T")}Z`);
3843
3937
  return Number.isNaN(written) ? Number.POSITIVE_INFINITY : now.getTime() - written;
@@ -17496,9 +17590,9 @@ var INTENT_GATEWAY_V2_ABI = [
17496
17590
  internalType: "bytes32"
17497
17591
  },
17498
17592
  {
17499
- name: "solver",
17500
- type: "address",
17501
- internalType: "address"
17593
+ name: "userOpHash",
17594
+ type: "bytes32",
17595
+ internalType: "bytes32"
17502
17596
  },
17503
17597
  {
17504
17598
  name: "signature",
@@ -18887,10 +18981,13 @@ var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
18887
18981
  var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
18888
18982
  var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
18889
18983
  var BID_DISCOVERY_PAD_SECONDS = 30;
18984
+ var BLOCK_TIME_SAMPLE_BLOCKS = 1000n;
18985
+ var FALLBACK_BLOCK_TIME_SECONDS = 2;
18890
18986
  var ContractInteractionService = class {
18891
18987
  constructor(clientManager, configService, signer, sharedCacheService) {
18892
18988
  this.clientManager = clientManager;
18893
18989
  this.sdkHelperCache = /* @__PURE__ */ new Map();
18990
+ this.blockTimes = /* @__PURE__ */ new Map();
18894
18991
  this.logger = moduleLogger(configService.loggers, "contract-service");
18895
18992
  this.configService = configService;
18896
18993
  this.cacheService = sharedCacheService || new CacheService();
@@ -18950,11 +19047,20 @@ var ContractInteractionService = class {
18950
19047
  try {
18951
19048
  const chainIds = this.configService.getConfiguredChainIds();
18952
19049
  const chainNames = chainIds.map((id) => `EVM-${id}`);
18953
- for (const chainName of chainNames) {
19050
+ await Promise.all(
19051
+ chainNames.map(async (chainName2) => {
19052
+ try {
19053
+ await this.blockTimeSeconds(chainName2);
19054
+ } catch (err) {
19055
+ this.logger.warn({ err, chain: chainName2 }, "Could not measure the block time at startup");
19056
+ }
19057
+ })
19058
+ );
19059
+ for (const chainName2 of chainNames) {
18954
19060
  try {
18955
- await this.getFeeTokenWithDecimals(chainName);
19061
+ await this.getFeeTokenWithDecimals(chainName2);
18956
19062
  } catch (err) {
18957
- this.logger.warn({ err, chain: chainName }, "Could not prewarm fee token decimals");
19063
+ this.logger.warn({ err, chain: chainName2 }, "Could not prewarm fee token decimals");
18958
19064
  }
18959
19065
  }
18960
19066
  for (const destChain of chainNames) {
@@ -19539,18 +19645,59 @@ var ContractInteractionService = class {
19539
19645
  *
19540
19646
  * Operators configure seconds because that is the unit the risk is actually in, but the
19541
19647
  * contract compares against block numbers so `order.deadline` and this read the same
19542
- * clock. The conversion uses the chain's nominal block time; it is deliberately rounded
19543
- * up, since erring long costs a slightly stale quote while erring short silently drops
19544
- * winnable bids.
19648
+ * clock. The conversion uses the chain's block time from {@link blockTimeSeconds}; it is
19649
+ * deliberately rounded up, since erring long costs a slightly stale quote while erring
19650
+ * short silently drops winnable bids.
19545
19651
  */
19546
19652
  async bidValidUntilBlock(chain) {
19547
19653
  const client = this.clientManager.getPublicClient(chain);
19548
- const currentBlock = await client.getBlockNumber();
19549
- const blockTimeMs = client.chain?.blockTime;
19550
- const blockTimeSec = blockTimeMs ? blockTimeMs / 1e3 : 2;
19654
+ const [currentBlock, blockTimeSec] = await Promise.all([client.getBlockNumber(), this.blockTimeSeconds(chain)]);
19551
19655
  const windowSec = this.configService.getBidValiditySeconds() + BID_DISCOVERY_PAD_SECONDS;
19552
19656
  return currentBlock + BigInt(Math.ceil(windowSec / blockTimeSec));
19553
19657
  }
19658
+ /**
19659
+ * Seconds per block on `chain`. Measured once, when simplex starts ({@link initCache}), and
19660
+ * kept until it stops. A chain with no measurement yet, because it was added later or its
19661
+ * blocks could not be read at startup, is measured at its first bid.
19662
+ *
19663
+ * The nominal `Chain.blockTime` cannot be relied on alone: it goes stale when a chain
19664
+ * speeds up (BSC makes a block every 0.45s against a declared 0.75s) and some chains
19665
+ * declare none. Converting at a slower block time than the chain's ends a bid early, so
19666
+ * the faster of the measured and the declared figure is used, and a stalled or idle
19667
+ * stretch in the sample never shortens a bid.
19668
+ *
19669
+ * Blocks that cannot be read fall back to the declared figure, then to
19670
+ * {@link FALLBACK_BLOCK_TIME_SECONDS}. That answer is not kept.
19671
+ */
19672
+ blockTimeSeconds(chain) {
19673
+ const known = this.blockTimes.get(chain);
19674
+ if (known) return known;
19675
+ const client = this.clientManager.getPublicClient(chain);
19676
+ const declaredMs = client.chain?.blockTime;
19677
+ const declared = declaredMs ? declaredMs / 1e3 : void 0;
19678
+ const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
19679
+ this.blockTimes.delete(chain);
19680
+ const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19681
+ this.logger.warn(
19682
+ { err, chain, seconds: fallback2 },
19683
+ "Could not measure the block time; using the declared one"
19684
+ );
19685
+ return fallback2;
19686
+ });
19687
+ this.blockTimes.set(chain, seconds);
19688
+ return seconds;
19689
+ }
19690
+ /** The average block time over the chain's last {@link BLOCK_TIME_SAMPLE_BLOCKS} blocks, in seconds. */
19691
+ async measureBlockTimeSeconds(client) {
19692
+ const head = await client.getBlock({ blockTag: "latest" });
19693
+ const span = head.number < BLOCK_TIME_SAMPLE_BLOCKS ? head.number : BLOCK_TIME_SAMPLE_BLOCKS;
19694
+ const tail = await client.getBlock({ blockNumber: head.number - span });
19695
+ const measured = Number(head.timestamp - tail.timestamp) / Number(span);
19696
+ if (!Number.isFinite(measured) || measured <= 0) {
19697
+ throw new Error(`${span} blocks span ${head.timestamp - tail.timestamp}s`);
19698
+ }
19699
+ return measured;
19700
+ }
19554
19701
  /**
19555
19702
  * Builds the signed, never-executed UserOperation that carries one limit order
19556
19703
  * to the HyperFX orderbook.
@@ -19725,11 +19872,17 @@ import { encodeFunctionData as encodeFunctionData3, erc20Abi as erc20Abi3, forma
19725
19872
  import { encodeERC7821ExecuteBatch as encodeERC7821ExecuteBatch3 } from "@hyperbridge/sdk";
19726
19873
 
19727
19874
  // src/services/UserOpSender.ts
19728
- import { CryptoUtils as CryptoUtils2, BundlerMethod } from "@hyperbridge/sdk";
19875
+ import {
19876
+ CryptoUtils as CryptoUtils2,
19877
+ BundlerMethod,
19878
+ rundlerUserOperationFees
19879
+ } from "@hyperbridge/sdk";
19729
19880
  import { toHex as toHex2 } from "viem";
19730
19881
  var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
19731
19882
  var FALLBACK_CALL_GAS_LIMIT = 1500000n;
19732
19883
  var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
19884
+ var PRIORITY_FEE_BUMP_PERCENT = 8n;
19885
+ var MAX_FEE_BUMP_PERCENT = 10n;
19733
19886
  var UserOpSender = class {
19734
19887
  constructor(clientManager, configService, signer) {
19735
19888
  this.clientManager = clientManager;
@@ -19756,7 +19909,7 @@ var UserOpSender = class {
19756
19909
  let auth;
19757
19910
  let fees;
19758
19911
  try {
19759
- fees = await this.getGasPrice(bundlerUrl, publicClient, chainId);
19912
+ fees = await this.getGasPrice(bundlerUrl, publicClient);
19760
19913
  const gasForPrefund = gas ?? {
19761
19914
  verificationGasLimit: FALLBACK_VERIFICATION_GAS_LIMIT,
19762
19915
  callGasLimit: FALLBACK_CALL_GAS_LIMIT,
@@ -19910,31 +20063,27 @@ var UserOpSender = class {
19910
20063
  );
19911
20064
  return userOp;
19912
20065
  }
19913
- /** Mirrors the gas-price selection used by the delegation and fill paths. */
19914
- async getGasPrice(bundlerUrl, publicClient, chainId) {
19915
- const lower = bundlerUrl.toLowerCase();
19916
- if (lower.includes("pimlico.io")) {
19917
- const res = await this.sendBundlerRpc(
19918
- bundlerUrl,
19919
- BundlerMethod.PIMLICO_GET_USER_OPERATION_GAS_PRICE,
19920
- []
19921
- );
19922
- return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
19923
- }
19924
- if (lower.includes("alchemy.com")) {
19925
- const [rundlerPriorityFee, latestBlock] = await Promise.all([
19926
- this.sendBundlerRpc(bundlerUrl, BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
19927
- publicClient.getBlock({ blockTag: "latest" })
19928
- ]);
19929
- const baseFeePerGas = latestBlock.baseFeePerGas ?? await publicClient.getGasPrice();
19930
- const isArbitrum = BigInt(chainId) === 42161n;
19931
- const prioBump = isArbitrum ? 0n : 25n;
19932
- const maxPriorityFeePerGas = BigInt(rundlerPriorityFee) + BigInt(rundlerPriorityFee) * prioBump / 100n;
19933
- const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
19934
- return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
19935
- }
19936
- const gasPrice = await publicClient.getGasPrice();
19937
- return { maxFeePerGas: gasPrice + gasPrice * 10n / 100n, maxPriorityFeePerGas: gasPrice + gasPrice * 8n / 100n };
20066
+ /**
20067
+ * Prices the op for the rundler bundler, as the fill path does: its suggested fees, or the
20068
+ * chain's gas price raised to the priority fee it requires where it suggests none.
20069
+ */
20070
+ async getGasPrice(bundlerUrl, publicClient) {
20071
+ const [gasPrice, latestBlock] = await Promise.all([
20072
+ publicClient.getGasPrice(),
20073
+ publicClient.getBlock({ blockTag: "latest" })
20074
+ ]);
20075
+ return rundlerUserOperationFees(
20076
+ bundlerUrl,
20077
+ {
20078
+ maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
20079
+ maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
20080
+ },
20081
+ {
20082
+ baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
20083
+ priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
20084
+ maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
20085
+ }
20086
+ );
19938
20087
  }
19939
20088
  async sendBundlerRpc(bundlerUrl, method, params) {
19940
20089
  const response = await fetch(bundlerUrl, {
@@ -23671,6 +23820,82 @@ var TokenSender = class {
23671
23820
  }
23672
23821
  };
23673
23822
 
23823
+ // src/services/bundler-preflight.ts
23824
+ var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
23825
+ async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
23826
+ const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
23827
+ const logger = moduleLogger(configService.loggers, "bundler-preflight");
23828
+ const failures = await Promise.all(
23829
+ chains.map(async ({ chainId, bundlerUrl }) => {
23830
+ if (options.watchOnly?.[chainId] === true) return void 0;
23831
+ if (!bundlerUrl?.trim()) {
23832
+ return `Hyperbridge runs no bundler for ${chainName(chainId)}, so simplex cannot fill there; set it watch-only`;
23833
+ }
23834
+ const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
23835
+ if (!entryPoint) return void 0;
23836
+ return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
23837
+ })
23838
+ );
23839
+ const messages = failures.filter((failure) => failure !== void 0);
23840
+ if (messages.length > 0) throw new Error(messages.join("; "));
23841
+ }
23842
+ function servesEntryPoint(listed, entryPoint) {
23843
+ return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
23844
+ }
23845
+ function describeEntryPoints(listed) {
23846
+ return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
23847
+ }
23848
+ function bundlerHost(url) {
23849
+ try {
23850
+ return new URL(url).host || "an unparseable URL";
23851
+ } catch {
23852
+ return "an unparseable URL";
23853
+ }
23854
+ }
23855
+ function chainName(chainId) {
23856
+ const label = chainByChainId(chainId)?.label;
23857
+ return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
23858
+ }
23859
+ async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
23860
+ const host = bundlerHost(bundlerUrl);
23861
+ const subject = `Bundler ${host} for ${chainName(chainId)}`;
23862
+ const signal = AbortSignal.timeout(timeoutMs);
23863
+ let listed;
23864
+ try {
23865
+ listed = await supportedEntryPoints(bundlerUrl, signal);
23866
+ } catch (err) {
23867
+ const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
23868
+ logger.warn(
23869
+ { chain: formatChainKey(chainId), bundler: host },
23870
+ `${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
23871
+ );
23872
+ return void 0;
23873
+ }
23874
+ if (servesEntryPoint(listed, entryPoint)) return void 0;
23875
+ return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
23876
+ }
23877
+ async function supportedEntryPoints(bundlerUrl, signal) {
23878
+ const response = await fetch(bundlerUrl, {
23879
+ method: "POST",
23880
+ headers: { "Content-Type": "application/json" },
23881
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
23882
+ signal
23883
+ });
23884
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
23885
+ const json = await response.json();
23886
+ if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
23887
+ const { result } = json;
23888
+ if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
23889
+ throw new Error("the result is not a list of addresses");
23890
+ }
23891
+ return result;
23892
+ }
23893
+ function errorReason(err) {
23894
+ if (!(err instanceof Error)) return String(err);
23895
+ const { cause } = err;
23896
+ return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
23897
+ }
23898
+
23674
23899
  // src/orderbook/lifecycle.ts
23675
23900
  var EXPIRY_SWEEP_MS = 3e4;
23676
23901
  var LimitOrderLifecycle = class {
@@ -24481,8 +24706,8 @@ async function bootFiller(config, options) {
24481
24706
  };
24482
24707
  const configService = new FillerConfigService(resolvedChains, fillerConfigForService, options.loggers);
24483
24708
  const chainConfigs = resolvedChains.map((chain) => {
24484
- const chainName = `EVM-${chain.chainId}`;
24485
- return configService.getChainConfig(chainName);
24709
+ const chainName2 = `EVM-${chain.chainId}`;
24710
+ return configService.getChainConfig(chainName2);
24486
24711
  });
24487
24712
  let watchOnlyConfig;
24488
24713
  const globalWatchOnly = options.watchOnlyOverride === true || config.simplex.watchOnly === true;
@@ -24520,6 +24745,8 @@ async function bootFiller(config, options) {
24520
24745
  "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]."
24521
24746
  );
24522
24747
  }
24748
+ logger.info("Checking each bundler serves its chain's EntryPoint...");
24749
+ await assertBundlersServeEntryPoint(resolvedChains, configService, { watchOnly: watchOnlyConfig });
24523
24750
  const chainClientManager = new ChainClientManager(configService, options.signer);
24524
24751
  const runtimeSigner = chainClientManager.getSigner();
24525
24752
  if (options.signer) {
@@ -24710,10 +24937,10 @@ async function bootFiller(config, options) {
24710
24937
  const token1 = {};
24711
24938
  for (const pair of config.pairs ?? []) {
24712
24939
  if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
24713
- for (const chainName of configuredChainNames) {
24714
- const address = assetRegistry.getAddress(pair.token1, chainName);
24940
+ for (const chainName2 of configuredChainNames) {
24941
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
24715
24942
  if (!address) continue;
24716
- const list = token1[chainName] ?? (token1[chainName] = []);
24943
+ const list = token1[chainName2] ?? (token1[chainName2] = []);
24717
24944
  if (!list.includes(address)) list.push(address);
24718
24945
  }
24719
24946
  }
@@ -25109,6 +25336,7 @@ var MemoryLimitOrderStore = class {
25109
25336
  status: "open",
25110
25337
  commitment: null,
25111
25338
  orderNonce: order.orderNonce ?? "0",
25339
+ entryPoint: null,
25112
25340
  bookExpiresAt: null,
25113
25341
  bookPrice: null,
25114
25342
  lastError: null,
@@ -25744,7 +25972,7 @@ var OrderScanner = class _OrderScanner {
25744
25972
  async addChainNow(chain) {
25745
25973
  if (this.closed) throw new Error("This OrderScanner is closed");
25746
25974
  const rpcUrls = validateRpcUrls(chain.rpcUrls);
25747
- const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls, bundlerUrl: chain.bundlerUrl ?? "" }], {
25975
+ const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls }], {
25748
25976
  loggers: this.loggers,
25749
25977
  tolerateUnreachable: true
25750
25978
  }))[0].chainId;
@@ -26094,10 +26322,10 @@ var PairController = class {
26094
26322
  if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
26095
26323
  if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
26096
26324
  for (const chain of this.runtime.resolvedChains) {
26097
- const chainName = formatChainKey(chain.chainId);
26098
- const address = assetRegistry.getAddress(pair.token1, chainName);
26325
+ const chainName2 = formatChainKey(chain.chainId);
26326
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
26099
26327
  if (!address) continue;
26100
- const list = balanceTokens[chainName] ?? (balanceTokens[chainName] = []);
26328
+ const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
26101
26329
  if (!list.includes(address)) list.push(address);
26102
26330
  }
26103
26331
  }
@@ -26176,10 +26404,7 @@ var ChainController = class {
26176
26404
  async add(chain) {
26177
26405
  return this.serialise(async () => {
26178
26406
  validateRpcUrls(chain.rpcUrls);
26179
- if (!chain.bundlerUrl?.trim()) {
26180
- throw new Error("A bundler URL is required to submit fill UserOperations");
26181
- }
26182
- const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }]);
26407
+ const [resolved] = await resolveChainConfigs([{ rpcUrls: chain.rpcUrls }]);
26183
26408
  const { chainId } = resolved;
26184
26409
  const { configService, config, intentFiller } = this.runtime;
26185
26410
  if (configService.getConfiguredChainIds().includes(chainId)) {
@@ -26202,12 +26427,13 @@ var ChainController = class {
26202
26427
  `Chain ${chainId} cannot be added as filling: this solver was started without a signer. Restart with \`signer\` to fill.`
26203
26428
  );
26204
26429
  }
26205
- configService.addChain(resolved);
26206
26430
  const watchOnly = chain.watchOnly ?? (this.runtime.signerless || this.runtime.globalWatchOnly);
26431
+ await assertBundlersServeEntryPoint([resolved], configService, { watchOnly: { [chainId]: watchOnly } });
26432
+ configService.addChain(resolved);
26207
26433
  if (watchOnly) intentFiller.setWatchOnly(chainId, true);
26208
26434
  try {
26209
26435
  if (!inScanner) {
26210
- await this.scanner.addChain({ rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl, chainId });
26436
+ await this.scanner.addChain({ rpcUrls: chain.rpcUrls, chainId });
26211
26437
  }
26212
26438
  this.runtime.chainClientManager.invalidate(formatChainKey(chainId));
26213
26439
  await intentFiller.addChain(configService.getChainConfig(formatChainKey(chainId)));
@@ -26224,7 +26450,7 @@ var ChainController = class {
26224
26450
  this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
26225
26451
  }
26226
26452
  this.runtime.resolvedChains.push(resolved);
26227
- config.chains = [...config.chains, { rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }];
26453
+ config.chains = [...config.chains, { rpcUrls: chain.rpcUrls }];
26228
26454
  if (Object.keys(confirmationPolicies).length > 0) config.confirmationPolicies = confirmationPolicies;
26229
26455
  this.syncWatchOnlyToConfig();
26230
26456
  await this.persist();
@@ -26269,9 +26495,7 @@ var ChainController = class {
26269
26495
  validateRpcUrls(rpcUrls);
26270
26496
  const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
26271
26497
  if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
26272
- const [probed] = await resolveChainConfigs([
26273
- { rpcUrls, bundlerUrl: this.runtime.resolvedChains[index].bundlerUrl ?? "" }
26274
- ]);
26498
+ const [probed] = await resolveChainConfigs([{ rpcUrls }]);
26275
26499
  if (probed.chainId !== chainId) {
26276
26500
  throw new Error(`Those endpoints answer for chain ${probed.chainId}, not ${chainId}`);
26277
26501
  }
@@ -26290,17 +26514,6 @@ var ChainController = class {
26290
26514
  await this.persist();
26291
26515
  });
26292
26516
  }
26293
- async setBundlerUrl(chainId, bundlerUrl) {
26294
- return this.serialise(async () => {
26295
- if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
26296
- const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
26297
- if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
26298
- this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
26299
- this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
26300
- this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
26301
- await this.persist();
26302
- });
26303
- }
26304
26517
  /** Monitor without filling. Takes effect on the next order. */
26305
26518
  async setWatchOnly(chainId, watchOnly) {
26306
26519
  return this.serialise(async () => {
@@ -26309,6 +26522,10 @@ var ChainController = class {
26309
26522
  "Watch-only cannot be disabled: this solver was started without a signer. Restart with `signer` to fill."
26310
26523
  );
26311
26524
  }
26525
+ if (!watchOnly) {
26526
+ const chain = this.runtime.resolvedChains.find((resolved) => resolved.chainId === chainId);
26527
+ if (chain) await assertBundlersServeEntryPoint([chain], this.runtime.configService);
26528
+ }
26312
26529
  this.runtime.intentFiller.setWatchOnly(chainId, watchOnly);
26313
26530
  this.syncWatchOnlyToConfig();
26314
26531
  await this.persist();