@hyperbridge/simplex 0.16.13 → 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,
@@ -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;
@@ -4364,6 +4458,80 @@ async function patchRuntimeState(store, patch) {
4364
4458
  return next;
4365
4459
  }
4366
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
+
4367
4535
  // src/services/wallet/mpcvault.ts
4368
4536
  var import_viem7 = require("viem");
4369
4537
  var grpc = __toESM(require("@grpc/grpc-js"), 1);
@@ -17527,9 +17695,9 @@ var INTENT_GATEWAY_V2_ABI = [
17527
17695
  internalType: "bytes32"
17528
17696
  },
17529
17697
  {
17530
- name: "solver",
17531
- type: "address",
17532
- internalType: "address"
17698
+ name: "userOpHash",
17699
+ type: "bytes32",
17700
+ internalType: "bytes32"
17533
17701
  },
17534
17702
  {
17535
17703
  name: "signature",
@@ -18904,10 +19072,13 @@ var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
18904
19072
  var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
18905
19073
  var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
18906
19074
  var BID_DISCOVERY_PAD_SECONDS = 30;
19075
+ var BLOCK_TIME_SAMPLE_BLOCKS = 1000n;
19076
+ var FALLBACK_BLOCK_TIME_SECONDS = 2;
18907
19077
  var ContractInteractionService = class {
18908
19078
  constructor(clientManager, configService, signer, sharedCacheService) {
18909
19079
  this.clientManager = clientManager;
18910
19080
  this.sdkHelperCache = /* @__PURE__ */ new Map();
19081
+ this.blockTimes = /* @__PURE__ */ new Map();
18911
19082
  this.logger = moduleLogger(configService.loggers, "contract-service");
18912
19083
  this.configService = configService;
18913
19084
  this.cacheService = sharedCacheService || new CacheService();
@@ -18967,11 +19138,20 @@ var ContractInteractionService = class {
18967
19138
  try {
18968
19139
  const chainIds = this.configService.getConfiguredChainIds();
18969
19140
  const chainNames = chainIds.map((id) => `EVM-${id}`);
18970
- 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) {
18971
19151
  try {
18972
- await this.getFeeTokenWithDecimals(chainName);
19152
+ await this.getFeeTokenWithDecimals(chainName2);
18973
19153
  } catch (err) {
18974
- 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");
18975
19155
  }
18976
19156
  }
18977
19157
  for (const destChain of chainNames) {
@@ -19556,18 +19736,59 @@ var ContractInteractionService = class {
19556
19736
  *
19557
19737
  * Operators configure seconds because that is the unit the risk is actually in, but the
19558
19738
  * 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.
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.
19562
19742
  */
19563
19743
  async bidValidUntilBlock(chain) {
19564
19744
  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;
19745
+ const [currentBlock, blockTimeSec] = await Promise.all([client.getBlockNumber(), this.blockTimeSeconds(chain)]);
19568
19746
  const windowSec = this.configService.getBidValiditySeconds() + BID_DISCOVERY_PAD_SECONDS;
19569
19747
  return currentBlock + BigInt(Math.ceil(windowSec / blockTimeSec));
19570
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
+ }
19571
19792
  /**
19572
19793
  * Builds the signed, never-executed UserOperation that carries one limit order
19573
19794
  * to the HyperFX orderbook.
@@ -19747,6 +19968,8 @@ var import_viem24 = require("viem");
19747
19968
  var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
19748
19969
  var FALLBACK_CALL_GAS_LIMIT = 1500000n;
19749
19970
  var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
19971
+ var PRIORITY_FEE_BUMP_PERCENT = 8n;
19972
+ var MAX_FEE_BUMP_PERCENT = 10n;
19750
19973
  var UserOpSender = class {
19751
19974
  constructor(clientManager, configService, signer) {
19752
19975
  this.clientManager = clientManager;
@@ -19927,7 +20150,11 @@ var UserOpSender = class {
19927
20150
  );
19928
20151
  return userOp;
19929
20152
  }
19930
- /** 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
+ */
19931
20158
  async getGasPrice(bundlerUrl, publicClient, chainId) {
19932
20159
  const lower = bundlerUrl.toLowerCase();
19933
20160
  if (lower.includes("pimlico.io")) {
@@ -19939,19 +20166,33 @@ var UserOpSender = class {
19939
20166
  return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
19940
20167
  }
19941
20168
  if (lower.includes("alchemy.com")) {
19942
- const [rundlerPriorityFee, latestBlock] = await Promise.all([
20169
+ const [rundlerPriorityFee2, latestBlock2] = await Promise.all([
19943
20170
  this.sendBundlerRpc(bundlerUrl, import_sdk8.BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
19944
20171
  publicClient.getBlock({ blockTag: "latest" })
19945
20172
  ]);
19946
- const baseFeePerGas = latestBlock.baseFeePerGas ?? await publicClient.getGasPrice();
20173
+ const baseFeePerGas = latestBlock2.baseFeePerGas ?? await publicClient.getGasPrice();
19947
20174
  const isArbitrum = BigInt(chainId) === 42161n;
19948
20175
  const prioBump = isArbitrum ? 0n : 25n;
19949
- const maxPriorityFeePerGas = BigInt(rundlerPriorityFee) + BigInt(rundlerPriorityFee) * prioBump / 100n;
20176
+ const maxPriorityFeePerGas = BigInt(rundlerPriorityFee2) + BigInt(rundlerPriorityFee2) * prioBump / 100n;
19950
20177
  const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
19951
20178
  return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
19952
20179
  }
19953
- const gasPrice = await publicClient.getGasPrice();
19954
- 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
+ });
19955
20196
  }
19956
20197
  async sendBundlerRpc(bundlerUrl, method, params) {
19957
20198
  const response = await fetch(bundlerUrl, {
@@ -20763,7 +21004,7 @@ var BinanceRebalancer = class {
20763
21004
  // src/services/rebalancers/cctp.ts
20764
21005
  var import_bridge_kit = require("@circle-fin/bridge-kit");
20765
21006
  var import_adapter_viem_v2 = require("@circle-fin/adapter-viem-v2");
20766
- var import_chains = require("@circle-fin/bridge-kit/chains");
21007
+ var import_chains2 = require("@circle-fin/bridge-kit/chains");
20767
21008
  var import_sdk11 = require("@hyperbridge/sdk");
20768
21009
  var CHAIN_ID_TO_CCTP = {
20769
21010
  1: "Ethereum",
@@ -20816,18 +21057,18 @@ var CctpRebalancer = class {
20816
21057
  {
20817
21058
  addressContext: "user-controlled",
20818
21059
  supportedChains: [
20819
- import_chains.Ethereum,
20820
- import_chains.EthereumSepolia,
20821
- import_chains.Arbitrum,
20822
- import_chains.ArbitrumSepolia,
20823
- import_chains.Base,
20824
- import_chains.BaseSepolia,
20825
- import_chains.Optimism,
20826
- import_chains.OptimismSepolia,
20827
- import_chains.Polygon,
20828
- import_chains.PolygonAmoy,
20829
- import_chains.Unichain,
20830
- 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
20831
21072
  ]
20832
21073
  }
20833
21074
  );
@@ -24482,8 +24723,8 @@ async function bootFiller(config, options) {
24482
24723
  };
24483
24724
  const configService = new FillerConfigService(resolvedChains, fillerConfigForService, options.loggers);
24484
24725
  const chainConfigs = resolvedChains.map((chain) => {
24485
- const chainName = `EVM-${chain.chainId}`;
24486
- return configService.getChainConfig(chainName);
24726
+ const chainName2 = `EVM-${chain.chainId}`;
24727
+ return configService.getChainConfig(chainName2);
24487
24728
  });
24488
24729
  let watchOnlyConfig;
24489
24730
  const globalWatchOnly = options.watchOnlyOverride === true || config.simplex.watchOnly === true;
@@ -24521,6 +24762,8 @@ async function bootFiller(config, options) {
24521
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]."
24522
24763
  );
24523
24764
  }
24765
+ logger.info("Checking each bundler serves its chain's EntryPoint...");
24766
+ await assertBundlersServeEntryPoint(resolvedChains, configService, { watchOnly: watchOnlyConfig });
24524
24767
  const chainClientManager = new ChainClientManager(configService, options.signer);
24525
24768
  const runtimeSigner = chainClientManager.getSigner();
24526
24769
  if (options.signer) {
@@ -24711,10 +24954,10 @@ async function bootFiller(config, options) {
24711
24954
  const token1 = {};
24712
24955
  for (const pair of config.pairs ?? []) {
24713
24956
  if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
24714
- for (const chainName of configuredChainNames) {
24715
- const address = assetRegistry.getAddress(pair.token1, chainName);
24957
+ for (const chainName2 of configuredChainNames) {
24958
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
24716
24959
  if (!address) continue;
24717
- const list = token1[chainName] ?? (token1[chainName] = []);
24960
+ const list = token1[chainName2] ?? (token1[chainName2] = []);
24718
24961
  if (!list.includes(address)) list.push(address);
24719
24962
  }
24720
24963
  }
@@ -25110,6 +25353,7 @@ var MemoryLimitOrderStore = class {
25110
25353
  status: "open",
25111
25354
  commitment: null,
25112
25355
  orderNonce: order.orderNonce ?? "0",
25356
+ entryPoint: null,
25113
25357
  bookExpiresAt: null,
25114
25358
  bookPrice: null,
25115
25359
  lastError: null,
@@ -26095,10 +26339,10 @@ var PairController = class {
26095
26339
  if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
26096
26340
  if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
26097
26341
  for (const chain of this.runtime.resolvedChains) {
26098
- const chainName = formatChainKey(chain.chainId);
26099
- const address = assetRegistry.getAddress(pair.token1, chainName);
26342
+ const chainName2 = formatChainKey(chain.chainId);
26343
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
26100
26344
  if (!address) continue;
26101
- const list = balanceTokens[chainName] ?? (balanceTokens[chainName] = []);
26345
+ const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
26102
26346
  if (!list.includes(address)) list.push(address);
26103
26347
  }
26104
26348
  }
@@ -26203,8 +26447,9 @@ var ChainController = class {
26203
26447
  `Chain ${chainId} cannot be added as filling: this solver was started without a signer. Restart with \`signer\` to fill.`
26204
26448
  );
26205
26449
  }
26206
- configService.addChain(resolved);
26207
26450
  const watchOnly = chain.watchOnly ?? (this.runtime.signerless || this.runtime.globalWatchOnly);
26451
+ await assertBundlersServeEntryPoint([resolved], configService, { watchOnly: { [chainId]: watchOnly } });
26452
+ configService.addChain(resolved);
26208
26453
  if (watchOnly) intentFiller.setWatchOnly(chainId, true);
26209
26454
  try {
26210
26455
  if (!inScanner) {
@@ -26296,6 +26541,9 @@ var ChainController = class {
26296
26541
  if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
26297
26542
  const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
26298
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
+ });
26299
26547
  this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
26300
26548
  this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
26301
26549
  this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
@@ -26310,6 +26558,10 @@ var ChainController = class {
26310
26558
  "Watch-only cannot be disabled: this solver was started without a signer. Restart with `signer` to fill."
26311
26559
  );
26312
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
+ }
26313
26565
  this.runtime.intentFiller.setWatchOnly(chainId, watchOnly);
26314
26566
  this.syncWatchOnlyToConfig();
26315
26567
  await this.persist();