@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.js CHANGED
@@ -574,7 +574,8 @@ var BUILTIN_ASSETS = {
574
574
  ZARP: { resolve: (r, chain) => r.getAssetBySymbol(chain, "ZARP") },
575
575
  EURC: { resolve: (r, chain) => r.getAssetBySymbol(chain, "EURC") },
576
576
  XSGD: { resolve: (r, chain) => r.getAssetBySymbol(chain, "XSGD") },
577
- TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") }
577
+ TRYB: { resolve: (r, chain) => r.getAssetBySymbol(chain, "TRYB") },
578
+ BRIDGE: { resolve: (r, chain) => r.getAssetBySymbol(chain, "BRIDGE") }
578
579
  };
579
580
  var USD_STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "DAI"]);
580
581
  function normalizeSymbol(symbol) {
@@ -712,7 +713,7 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
712
713
  const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
713
714
  for (const pair of pairs) {
714
715
  for (const symbol of [pair.token0, pair.token1]) {
715
- const resolvesSomewhere = chainNames.some((chainName) => registry.getAddress(symbol, chainName) !== null);
716
+ const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
716
717
  if (!resolvesSomewhere) {
717
718
  throw new Error(
718
719
  `pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
@@ -720,15 +721,15 @@ function assertPairSymbolsResolve(pairs, registry, chainNames) {
720
721
  }
721
722
  }
722
723
  }
723
- for (const chainName of chainNames) {
724
+ for (const chainName2 of chainNames) {
724
725
  const addressOwner = /* @__PURE__ */ new Map();
725
726
  for (const symbol of pairSymbols) {
726
- const address = registry.getAddress(symbol, chainName)?.toLowerCase();
727
+ const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
727
728
  if (!address) continue;
728
729
  const owner = addressOwner.get(address);
729
730
  if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
730
731
  throw new Error(
731
- `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName} \u2014 symbols must map to distinct contracts`
732
+ `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
732
733
  );
733
734
  }
734
735
  addressOwner.set(address, symbol);
@@ -1433,7 +1434,7 @@ var FillerConfigService = class {
1433
1434
  return this.fillerConfig?.substratePrivateKey;
1434
1435
  }
1435
1436
  getEntryPointAddress(chain) {
1436
- return this.chainConfigService.getEntryPointV08Address(chain);
1437
+ return this.chainConfigService.getEntryPointAddress(chain);
1437
1438
  }
1438
1439
  getSolverAccountContractAddress(chain) {
1439
1440
  return this.chainConfigService.getSolverAccountAddress(chain);
@@ -2482,6 +2483,13 @@ var FREE_RPC_URLS = {
2482
2483
  "https://arb1.arbitrum.io/rpc"
2483
2484
  ]
2484
2485
  };
2486
+ var HYPERBRIDGE_BUNDLER_URLS = {
2487
+ 1: "https://bundler.polytope.technology/ethereum",
2488
+ 56: "https://bundler.polytope.technology/bsc",
2489
+ 137: "https://bundler.polytope.technology/polygon",
2490
+ 8453: "https://bundler.polytope.technology/base",
2491
+ 42161: "https://bundler.polytope.technology/arbitrum"
2492
+ };
2485
2493
  var CHAIN_NATIVE_SYMBOLS = {
2486
2494
  1: "ETH",
2487
2495
  56: "BNB",
@@ -2510,7 +2518,8 @@ var INIT_CHAINS = [
2510
2518
  network: "mainnet",
2511
2519
  alchemySubdomain: "eth-mainnet",
2512
2520
  explorerUrl: external_links_default.chainExplorers.ethereum,
2513
- defaultRpcUrls: FREE_RPC_URLS[1]
2521
+ defaultRpcUrls: FREE_RPC_URLS[1],
2522
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[1]
2514
2523
  },
2515
2524
  {
2516
2525
  chainId: 42161,
@@ -2519,7 +2528,8 @@ var INIT_CHAINS = [
2519
2528
  network: "mainnet",
2520
2529
  alchemySubdomain: "arb-mainnet",
2521
2530
  explorerUrl: external_links_default.chainExplorers.arbitrum,
2522
- defaultRpcUrls: FREE_RPC_URLS[42161]
2531
+ defaultRpcUrls: FREE_RPC_URLS[42161],
2532
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[42161]
2523
2533
  },
2524
2534
  {
2525
2535
  chainId: 8453,
@@ -2528,7 +2538,8 @@ var INIT_CHAINS = [
2528
2538
  network: "mainnet",
2529
2539
  alchemySubdomain: "base-mainnet",
2530
2540
  explorerUrl: external_links_default.chainExplorers.base,
2531
- defaultRpcUrls: FREE_RPC_URLS[8453]
2541
+ defaultRpcUrls: FREE_RPC_URLS[8453],
2542
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[8453]
2532
2543
  },
2533
2544
  {
2534
2545
  chainId: 137,
@@ -2537,7 +2548,8 @@ var INIT_CHAINS = [
2537
2548
  network: "mainnet",
2538
2549
  alchemySubdomain: "polygon-mainnet",
2539
2550
  explorerUrl: external_links_default.chainExplorers.polygon,
2540
- defaultRpcUrls: FREE_RPC_URLS[137]
2551
+ defaultRpcUrls: FREE_RPC_URLS[137],
2552
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[137]
2541
2553
  },
2542
2554
  {
2543
2555
  chainId: 56,
@@ -2546,7 +2558,8 @@ var INIT_CHAINS = [
2546
2558
  network: "mainnet",
2547
2559
  alchemySubdomain: "bnb-mainnet",
2548
2560
  explorerUrl: external_links_default.chainExplorers.bnb,
2549
- defaultRpcUrls: FREE_RPC_URLS[56]
2561
+ defaultRpcUrls: FREE_RPC_URLS[56],
2562
+ hyperbridgeBundlerUrl: HYPERBRIDGE_BUNDLER_URLS[56]
2550
2563
  },
2551
2564
  {
2552
2565
  chainId: 11155111,
@@ -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;
@@ -4302,6 +4396,80 @@ async function patchRuntimeState(store, patch) {
4302
4396
  return next;
4303
4397
  }
4304
4398
 
4399
+ // src/services/bundler-preflight.ts
4400
+ var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
4401
+ async function assertBundlersServeEntryPoint(chains, configService, options = {}) {
4402
+ const timeoutMs = options.timeoutMs ?? BUNDLER_PREFLIGHT_TIMEOUT_MS;
4403
+ const logger = moduleLogger(configService.loggers, "bundler-preflight");
4404
+ const failures = await Promise.all(
4405
+ chains.map(async ({ chainId, bundlerUrl }) => {
4406
+ if (options.watchOnly?.[chainId] === true) return void 0;
4407
+ if (!bundlerUrl?.trim()) return void 0;
4408
+ const entryPoint = configService.getEntryPointAddress(formatChainKey(chainId));
4409
+ if (!entryPoint) return void 0;
4410
+ return checkBundler(chainId, bundlerUrl.trim(), entryPoint, timeoutMs, logger);
4411
+ })
4412
+ );
4413
+ const messages = failures.filter((failure) => failure !== void 0);
4414
+ if (messages.length > 0) throw new Error(messages.join("; "));
4415
+ }
4416
+ function servesEntryPoint(listed, entryPoint) {
4417
+ return Array.isArray(listed) && listed.some((address) => String(address).toLowerCase() === entryPoint.toLowerCase());
4418
+ }
4419
+ function describeEntryPoints(listed) {
4420
+ return Array.isArray(listed) && listed.length > 0 ? listed.map(String).join(", ") : "none";
4421
+ }
4422
+ function bundlerHost(url) {
4423
+ try {
4424
+ return new URL(url).host || "an unparseable URL";
4425
+ } catch {
4426
+ return "an unparseable URL";
4427
+ }
4428
+ }
4429
+ function chainName(chainId) {
4430
+ const label = chainByChainId(chainId)?.label;
4431
+ return label ? `${label} (${formatChainKey(chainId)})` : formatChainKey(chainId);
4432
+ }
4433
+ async function checkBundler(chainId, bundlerUrl, entryPoint, timeoutMs, logger) {
4434
+ const host = bundlerHost(bundlerUrl);
4435
+ const subject = `Bundler ${host} for ${chainName(chainId)}`;
4436
+ const signal = AbortSignal.timeout(timeoutMs);
4437
+ let listed;
4438
+ try {
4439
+ listed = await supportedEntryPoints(bundlerUrl, signal);
4440
+ } catch (err) {
4441
+ const reason = signal.aborted ? `timed out after ${timeoutMs}ms` : errorReason(err);
4442
+ logger.warn(
4443
+ { chain: formatChainKey(chainId), bundler: host },
4444
+ `${subject} did not answer eth_supportedEntryPoints: ${reason.split(bundlerUrl).join(host)}. Continuing without confirming it serves EntryPoint ${entryPoint}`
4445
+ );
4446
+ return void 0;
4447
+ }
4448
+ if (servesEntryPoint(listed, entryPoint)) return void 0;
4449
+ return `${subject} does not support EntryPoint ${entryPoint}; it lists ${describeEntryPoints(listed)}`;
4450
+ }
4451
+ async function supportedEntryPoints(bundlerUrl, signal) {
4452
+ const response = await fetch(bundlerUrl, {
4453
+ method: "POST",
4454
+ headers: { "Content-Type": "application/json" },
4455
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_supportedEntryPoints", params: [] }),
4456
+ signal
4457
+ });
4458
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
4459
+ const json = await response.json();
4460
+ if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
4461
+ const { result } = json;
4462
+ if (!Array.isArray(result) || !result.every((address) => typeof address === "string")) {
4463
+ throw new Error("the result is not a list of addresses");
4464
+ }
4465
+ return result;
4466
+ }
4467
+ function errorReason(err) {
4468
+ if (!(err instanceof Error)) return String(err);
4469
+ const { cause } = err;
4470
+ return cause instanceof Error ? `${err.message} (${cause.message})` : err.message;
4471
+ }
4472
+
4305
4473
  // src/services/wallet/mpcvault.ts
4306
4474
  import { concatHex, keccak256 as keccak2562, padHex, toHex } from "viem";
4307
4475
  import * as grpc from "@grpc/grpc-js";
@@ -17496,9 +17664,9 @@ var INTENT_GATEWAY_V2_ABI = [
17496
17664
  internalType: "bytes32"
17497
17665
  },
17498
17666
  {
17499
- name: "solver",
17500
- type: "address",
17501
- internalType: "address"
17667
+ name: "userOpHash",
17668
+ type: "bytes32",
17669
+ internalType: "bytes32"
17502
17670
  },
17503
17671
  {
17504
17672
  name: "signature",
@@ -18887,10 +19055,13 @@ var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
18887
19055
  var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
18888
19056
  var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
18889
19057
  var BID_DISCOVERY_PAD_SECONDS = 30;
19058
+ var BLOCK_TIME_SAMPLE_BLOCKS = 1000n;
19059
+ var FALLBACK_BLOCK_TIME_SECONDS = 2;
18890
19060
  var ContractInteractionService = class {
18891
19061
  constructor(clientManager, configService, signer, sharedCacheService) {
18892
19062
  this.clientManager = clientManager;
18893
19063
  this.sdkHelperCache = /* @__PURE__ */ new Map();
19064
+ this.blockTimes = /* @__PURE__ */ new Map();
18894
19065
  this.logger = moduleLogger(configService.loggers, "contract-service");
18895
19066
  this.configService = configService;
18896
19067
  this.cacheService = sharedCacheService || new CacheService();
@@ -18950,11 +19121,20 @@ var ContractInteractionService = class {
18950
19121
  try {
18951
19122
  const chainIds = this.configService.getConfiguredChainIds();
18952
19123
  const chainNames = chainIds.map((id) => `EVM-${id}`);
18953
- for (const chainName of chainNames) {
19124
+ await Promise.all(
19125
+ chainNames.map(async (chainName2) => {
19126
+ try {
19127
+ await this.blockTimeSeconds(chainName2);
19128
+ } catch (err) {
19129
+ this.logger.warn({ err, chain: chainName2 }, "Could not measure the block time at startup");
19130
+ }
19131
+ })
19132
+ );
19133
+ for (const chainName2 of chainNames) {
18954
19134
  try {
18955
- await this.getFeeTokenWithDecimals(chainName);
19135
+ await this.getFeeTokenWithDecimals(chainName2);
18956
19136
  } catch (err) {
18957
- this.logger.warn({ err, chain: chainName }, "Could not prewarm fee token decimals");
19137
+ this.logger.warn({ err, chain: chainName2 }, "Could not prewarm fee token decimals");
18958
19138
  }
18959
19139
  }
18960
19140
  for (const destChain of chainNames) {
@@ -19539,18 +19719,59 @@ var ContractInteractionService = class {
19539
19719
  *
19540
19720
  * Operators configure seconds because that is the unit the risk is actually in, but the
19541
19721
  * 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.
19722
+ * clock. The conversion uses the chain's block time from {@link blockTimeSeconds}; it is
19723
+ * deliberately rounded up, since erring long costs a slightly stale quote while erring
19724
+ * short silently drops winnable bids.
19545
19725
  */
19546
19726
  async bidValidUntilBlock(chain) {
19547
19727
  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;
19728
+ const [currentBlock, blockTimeSec] = await Promise.all([client.getBlockNumber(), this.blockTimeSeconds(chain)]);
19551
19729
  const windowSec = this.configService.getBidValiditySeconds() + BID_DISCOVERY_PAD_SECONDS;
19552
19730
  return currentBlock + BigInt(Math.ceil(windowSec / blockTimeSec));
19553
19731
  }
19732
+ /**
19733
+ * Seconds per block on `chain`. Measured once, when simplex starts ({@link initCache}), and
19734
+ * kept until it stops. A chain with no measurement yet, because it was added later or its
19735
+ * blocks could not be read at startup, is measured at its first bid.
19736
+ *
19737
+ * The nominal `Chain.blockTime` cannot be relied on alone: it goes stale when a chain
19738
+ * speeds up (BSC makes a block every 0.45s against a declared 0.75s) and some chains
19739
+ * declare none. Converting at a slower block time than the chain's ends a bid early, so
19740
+ * the faster of the measured and the declared figure is used, and a stalled or idle
19741
+ * stretch in the sample never shortens a bid.
19742
+ *
19743
+ * Blocks that cannot be read fall back to the declared figure, then to
19744
+ * {@link FALLBACK_BLOCK_TIME_SECONDS}. That answer is not kept.
19745
+ */
19746
+ blockTimeSeconds(chain) {
19747
+ const known = this.blockTimes.get(chain);
19748
+ if (known) return known;
19749
+ const client = this.clientManager.getPublicClient(chain);
19750
+ const declaredMs = client.chain?.blockTime;
19751
+ const declared = declaredMs ? declaredMs / 1e3 : void 0;
19752
+ const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
19753
+ this.blockTimes.delete(chain);
19754
+ const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19755
+ this.logger.warn(
19756
+ { err, chain, seconds: fallback2 },
19757
+ "Could not measure the block time; using the declared one"
19758
+ );
19759
+ return fallback2;
19760
+ });
19761
+ this.blockTimes.set(chain, seconds);
19762
+ return seconds;
19763
+ }
19764
+ /** The average block time over the chain's last {@link BLOCK_TIME_SAMPLE_BLOCKS} blocks, in seconds. */
19765
+ async measureBlockTimeSeconds(client) {
19766
+ const head = await client.getBlock({ blockTag: "latest" });
19767
+ const span = head.number < BLOCK_TIME_SAMPLE_BLOCKS ? head.number : BLOCK_TIME_SAMPLE_BLOCKS;
19768
+ const tail = await client.getBlock({ blockNumber: head.number - span });
19769
+ const measured = Number(head.timestamp - tail.timestamp) / Number(span);
19770
+ if (!Number.isFinite(measured) || measured <= 0) {
19771
+ throw new Error(`${span} blocks span ${head.timestamp - tail.timestamp}s`);
19772
+ }
19773
+ return measured;
19774
+ }
19554
19775
  /**
19555
19776
  * Builds the signed, never-executed UserOperation that carries one limit order
19556
19777
  * to the HyperFX orderbook.
@@ -19725,11 +19946,18 @@ import { encodeFunctionData as encodeFunctionData3, erc20Abi as erc20Abi3, forma
19725
19946
  import { encodeERC7821ExecuteBatch as encodeERC7821ExecuteBatch3 } from "@hyperbridge/sdk";
19726
19947
 
19727
19948
  // src/services/UserOpSender.ts
19728
- import { CryptoUtils as CryptoUtils2, BundlerMethod } from "@hyperbridge/sdk";
19949
+ import {
19950
+ CryptoUtils as CryptoUtils2,
19951
+ BundlerMethod,
19952
+ applyRundlerPriorityFee,
19953
+ fetchRundlerPriorityFee
19954
+ } from "@hyperbridge/sdk";
19729
19955
  import { toHex as toHex2 } from "viem";
19730
19956
  var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
19731
19957
  var FALLBACK_CALL_GAS_LIMIT = 1500000n;
19732
19958
  var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
19959
+ var PRIORITY_FEE_BUMP_PERCENT = 8n;
19960
+ var MAX_FEE_BUMP_PERCENT = 10n;
19733
19961
  var UserOpSender = class {
19734
19962
  constructor(clientManager, configService, signer) {
19735
19963
  this.clientManager = clientManager;
@@ -19910,7 +20138,11 @@ var UserOpSender = class {
19910
20138
  );
19911
20139
  return userOp;
19912
20140
  }
19913
- /** Mirrors the gas-price selection used by the delegation and fill paths. */
20141
+ /**
20142
+ * Mirrors the gas-price selection used by the delegation and fill paths: Pimlico's and
20143
+ * Alchemy's own pricing, otherwise the chain's gas price raised to the priority fee a
20144
+ * rundler bundler requires, when the bundler gives one.
20145
+ */
19914
20146
  async getGasPrice(bundlerUrl, publicClient, chainId) {
19915
20147
  const lower = bundlerUrl.toLowerCase();
19916
20148
  if (lower.includes("pimlico.io")) {
@@ -19922,19 +20154,33 @@ var UserOpSender = class {
19922
20154
  return { maxFeePerGas: BigInt(res.fast.maxFeePerGas), maxPriorityFeePerGas: BigInt(res.fast.maxPriorityFeePerGas) };
19923
20155
  }
19924
20156
  if (lower.includes("alchemy.com")) {
19925
- const [rundlerPriorityFee, latestBlock] = await Promise.all([
20157
+ const [rundlerPriorityFee2, latestBlock2] = await Promise.all([
19926
20158
  this.sendBundlerRpc(bundlerUrl, BundlerMethod.RUNDLER_MAX_PRIORITY_FEE_PER_GAS, []),
19927
20159
  publicClient.getBlock({ blockTag: "latest" })
19928
20160
  ]);
19929
- const baseFeePerGas = latestBlock.baseFeePerGas ?? await publicClient.getGasPrice();
20161
+ const baseFeePerGas = latestBlock2.baseFeePerGas ?? await publicClient.getGasPrice();
19930
20162
  const isArbitrum = BigInt(chainId) === 42161n;
19931
20163
  const prioBump = isArbitrum ? 0n : 25n;
19932
- const maxPriorityFeePerGas = BigInt(rundlerPriorityFee) + BigInt(rundlerPriorityFee) * prioBump / 100n;
20164
+ const maxPriorityFeePerGas = BigInt(rundlerPriorityFee2) + BigInt(rundlerPriorityFee2) * prioBump / 100n;
19933
20165
  const bufferedBaseFee = baseFeePerGas + baseFeePerGas * 50n / 100n;
19934
20166
  return { maxFeePerGas: bufferedBaseFee + maxPriorityFeePerGas, maxPriorityFeePerGas };
19935
20167
  }
19936
- const gasPrice = await publicClient.getGasPrice();
19937
- return { maxFeePerGas: gasPrice + gasPrice * 10n / 100n, maxPriorityFeePerGas: gasPrice + gasPrice * 8n / 100n };
20168
+ const [gasPrice, rundlerPriorityFee] = await Promise.all([
20169
+ publicClient.getGasPrice(),
20170
+ fetchRundlerPriorityFee(bundlerUrl)
20171
+ ]);
20172
+ const fees = {
20173
+ maxFeePerGas: gasPrice + gasPrice * MAX_FEE_BUMP_PERCENT / 100n,
20174
+ maxPriorityFeePerGas: gasPrice + gasPrice * PRIORITY_FEE_BUMP_PERCENT / 100n
20175
+ };
20176
+ if (rundlerPriorityFee === null) return fees;
20177
+ const latestBlock = await publicClient.getBlock({ blockTag: "latest" });
20178
+ return applyRundlerPriorityFee(fees, {
20179
+ rundlerPriorityFee,
20180
+ baseFeePerGas: latestBlock.baseFeePerGas ?? gasPrice,
20181
+ priorityFeeBumpPercent: PRIORITY_FEE_BUMP_PERCENT,
20182
+ maxFeeBumpPercent: MAX_FEE_BUMP_PERCENT
20183
+ });
19938
20184
  }
19939
20185
  async sendBundlerRpc(bundlerUrl, method, params) {
19940
20186
  const response = await fetch(bundlerUrl, {
@@ -24481,8 +24727,8 @@ async function bootFiller(config, options) {
24481
24727
  };
24482
24728
  const configService = new FillerConfigService(resolvedChains, fillerConfigForService, options.loggers);
24483
24729
  const chainConfigs = resolvedChains.map((chain) => {
24484
- const chainName = `EVM-${chain.chainId}`;
24485
- return configService.getChainConfig(chainName);
24730
+ const chainName2 = `EVM-${chain.chainId}`;
24731
+ return configService.getChainConfig(chainName2);
24486
24732
  });
24487
24733
  let watchOnlyConfig;
24488
24734
  const globalWatchOnly = options.watchOnlyOverride === true || config.simplex.watchOnly === true;
@@ -24520,6 +24766,8 @@ async function bootFiller(config, options) {
24520
24766
  "A signer is required unless every chain is watch-only: pass `signer` to Simplex.start (privateKeySigner, secretPhraseSigner, turnkeySigner, mpcVaultSigner, viemSigner, or your own Signer implementation). Running the binary? Configure it under [simplex.signer]."
24521
24767
  );
24522
24768
  }
24769
+ logger.info("Checking each bundler serves its chain's EntryPoint...");
24770
+ await assertBundlersServeEntryPoint(resolvedChains, configService, { watchOnly: watchOnlyConfig });
24523
24771
  const chainClientManager = new ChainClientManager(configService, options.signer);
24524
24772
  const runtimeSigner = chainClientManager.getSigner();
24525
24773
  if (options.signer) {
@@ -24710,10 +24958,10 @@ async function bootFiller(config, options) {
24710
24958
  const token1 = {};
24711
24959
  for (const pair of config.pairs ?? []) {
24712
24960
  if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
24713
- for (const chainName of configuredChainNames) {
24714
- const address = assetRegistry.getAddress(pair.token1, chainName);
24961
+ for (const chainName2 of configuredChainNames) {
24962
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
24715
24963
  if (!address) continue;
24716
- const list = token1[chainName] ?? (token1[chainName] = []);
24964
+ const list = token1[chainName2] ?? (token1[chainName2] = []);
24717
24965
  if (!list.includes(address)) list.push(address);
24718
24966
  }
24719
24967
  }
@@ -25109,6 +25357,7 @@ var MemoryLimitOrderStore = class {
25109
25357
  status: "open",
25110
25358
  commitment: null,
25111
25359
  orderNonce: order.orderNonce ?? "0",
25360
+ entryPoint: null,
25112
25361
  bookExpiresAt: null,
25113
25362
  bookPrice: null,
25114
25363
  lastError: null,
@@ -26094,10 +26343,10 @@ var PairController = class {
26094
26343
  if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
26095
26344
  if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
26096
26345
  for (const chain of this.runtime.resolvedChains) {
26097
- const chainName = formatChainKey(chain.chainId);
26098
- const address = assetRegistry.getAddress(pair.token1, chainName);
26346
+ const chainName2 = formatChainKey(chain.chainId);
26347
+ const address = assetRegistry.getAddress(pair.token1, chainName2);
26099
26348
  if (!address) continue;
26100
- const list = balanceTokens[chainName] ?? (balanceTokens[chainName] = []);
26349
+ const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
26101
26350
  if (!list.includes(address)) list.push(address);
26102
26351
  }
26103
26352
  }
@@ -26202,8 +26451,9 @@ var ChainController = class {
26202
26451
  `Chain ${chainId} cannot be added as filling: this solver was started without a signer. Restart with \`signer\` to fill.`
26203
26452
  );
26204
26453
  }
26205
- configService.addChain(resolved);
26206
26454
  const watchOnly = chain.watchOnly ?? (this.runtime.signerless || this.runtime.globalWatchOnly);
26455
+ await assertBundlersServeEntryPoint([resolved], configService, { watchOnly: { [chainId]: watchOnly } });
26456
+ configService.addChain(resolved);
26207
26457
  if (watchOnly) intentFiller.setWatchOnly(chainId, true);
26208
26458
  try {
26209
26459
  if (!inScanner) {
@@ -26295,6 +26545,9 @@ var ChainController = class {
26295
26545
  if (!bundlerUrl?.trim()) throw new Error("A bundler URL is required");
26296
26546
  const index = this.runtime.resolvedChains.findIndex((chain) => chain.chainId === chainId);
26297
26547
  if (index < 0) throw new Error(`Chain ${chainId} is not configured`);
26548
+ await assertBundlersServeEntryPoint([{ chainId, bundlerUrl }], this.runtime.configService, {
26549
+ watchOnly: this.runtime.intentFiller.getWatchOnly()
26550
+ });
26298
26551
  this.runtime.configService.setBundlerUrl(chainId, bundlerUrl);
26299
26552
  this.runtime.resolvedChains[index].bundlerUrl = bundlerUrl;
26300
26553
  this.runtime.config.chains[index].bundlerUrl = bundlerUrl;
@@ -26309,6 +26562,10 @@ var ChainController = class {
26309
26562
  "Watch-only cannot be disabled: this solver was started without a signer. Restart with `signer` to fill."
26310
26563
  );
26311
26564
  }
26565
+ if (!watchOnly) {
26566
+ const chain = this.runtime.resolvedChains.find((resolved) => resolved.chainId === chainId);
26567
+ if (chain) await assertBundlersServeEntryPoint([chain], this.runtime.configService);
26568
+ }
26312
26569
  this.runtime.intentFiller.setWatchOnly(chainId, watchOnly);
26313
26570
  this.syncWatchOnlyToConfig();
26314
26571
  await this.persist();