@hyperbridge/simplex 0.16.6 → 0.16.8

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
@@ -2459,6 +2459,7 @@ function closedKey(chain, vault) {
2459
2459
  // src/config/external-links.json
2460
2460
  var external_links_default = {
2461
2461
  hyperfxApp: "https://app.hyperfx.finance",
2462
+ alchemyDashboard: "https://dashboard.alchemy.com/",
2462
2463
  hyperbridgeExplorers: {
2463
2464
  mainnet: "https://nexus.statescan.io",
2464
2465
  testnet: "https://gargantua.statescan.io"
@@ -2478,6 +2479,52 @@ var external_links_default = {
2478
2479
  };
2479
2480
 
2480
2481
  // src/cli/init/chains.ts
2482
+ var FREE_RPC_URLS = {
2483
+ 1: [
2484
+ "https://mainnet.gateway.tenderly.co",
2485
+ "https://rpc.mevblocker.io",
2486
+ "https://ethereum-rpc.publicnode.com",
2487
+ "https://ethereum.public.blockpi.network/v1/rpc/public",
2488
+ "https://eth.blockrazor.xyz",
2489
+ "https://eth.api.pocket.network",
2490
+ "https://ethereum-public.nodies.app",
2491
+ "https://eth-mainnet.public.blastapi.io",
2492
+ "https://public-eth.nownodes.io"
2493
+ ],
2494
+ 56: [
2495
+ "https://bsc-rpc.publicnode.com",
2496
+ "https://public-bsc-mainnet.fastnode.io",
2497
+ "https://public-bsc.nownodes.io",
2498
+ "https://bsc.blockrazor.xyz",
2499
+ "https://rpc-bsc.48.club",
2500
+ "https://bsc.api.pocket.network"
2501
+ ],
2502
+ 137: [
2503
+ "https://polygon-bor-rpc.publicnode.com",
2504
+ "https://polygon.drpc.org",
2505
+ "https://polygon.gateway.tenderly.co",
2506
+ "https://rpc.private.mev-x.com/polygon",
2507
+ "https://poly.api.pocket.network"
2508
+ ],
2509
+ 8453: [
2510
+ "https://base-rpc.publicnode.com",
2511
+ "https://base.gateway.tenderly.co",
2512
+ "https://base.public.blockpi.network/v1/rpc/public",
2513
+ "https://base.rpc.blxrbdn.com",
2514
+ "https://rpc.baseazul.dev",
2515
+ "https://mainnet.base.org",
2516
+ "https://base-public.nodies.app"
2517
+ ],
2518
+ 42161: [
2519
+ "https://arbitrum-one-rpc.publicnode.com",
2520
+ "https://arbitrum.gateway.tenderly.co",
2521
+ "https://arbitrum.drpc.org",
2522
+ "https://public-arb-mainnet.fastnode.io",
2523
+ "https://arb-one.api.pocket.network",
2524
+ "https://arbitrum-one-public.nodies.app",
2525
+ "https://arb1.arbitrum.io/rpc"
2526
+ ]
2527
+ };
2481
2528
  var CHAIN_NATIVE_SYMBOLS = {
2482
2529
  1: "ETH",
2483
2530
  56: "BNB",
@@ -2505,7 +2552,8 @@ var INIT_CHAINS = [
2505
2552
  label: "Ethereum",
2506
2553
  network: "mainnet",
2507
2554
  alchemySubdomain: "eth-mainnet",
2508
- explorerUrl: external_links_default.chainExplorers.ethereum
2555
+ explorerUrl: external_links_default.chainExplorers.ethereum,
2556
+ defaultRpcUrls: FREE_RPC_URLS[1]
2509
2557
  },
2510
2558
  {
2511
2559
  chainId: 42161,
@@ -2513,7 +2561,8 @@ var INIT_CHAINS = [
2513
2561
  label: "Arbitrum",
2514
2562
  network: "mainnet",
2515
2563
  alchemySubdomain: "arb-mainnet",
2516
- explorerUrl: external_links_default.chainExplorers.arbitrum
2564
+ explorerUrl: external_links_default.chainExplorers.arbitrum,
2565
+ defaultRpcUrls: FREE_RPC_URLS[42161]
2517
2566
  },
2518
2567
  {
2519
2568
  chainId: 8453,
@@ -2521,7 +2570,8 @@ var INIT_CHAINS = [
2521
2570
  label: "Base",
2522
2571
  network: "mainnet",
2523
2572
  alchemySubdomain: "base-mainnet",
2524
- explorerUrl: external_links_default.chainExplorers.base
2573
+ explorerUrl: external_links_default.chainExplorers.base,
2574
+ defaultRpcUrls: FREE_RPC_URLS[8453]
2525
2575
  },
2526
2576
  {
2527
2577
  chainId: 137,
@@ -2529,7 +2579,8 @@ var INIT_CHAINS = [
2529
2579
  label: "Polygon",
2530
2580
  network: "mainnet",
2531
2581
  alchemySubdomain: "polygon-mainnet",
2532
- explorerUrl: external_links_default.chainExplorers.polygon
2582
+ explorerUrl: external_links_default.chainExplorers.polygon,
2583
+ defaultRpcUrls: FREE_RPC_URLS[137]
2533
2584
  },
2534
2585
  {
2535
2586
  chainId: 56,
@@ -2537,7 +2588,8 @@ var INIT_CHAINS = [
2537
2588
  label: "BNB Chain",
2538
2589
  network: "mainnet",
2539
2590
  alchemySubdomain: "bnb-mainnet",
2540
- explorerUrl: external_links_default.chainExplorers.bnb
2591
+ explorerUrl: external_links_default.chainExplorers.bnb,
2592
+ defaultRpcUrls: FREE_RPC_URLS[56]
2541
2593
  },
2542
2594
  {
2543
2595
  chainId: 11155111,
@@ -3025,14 +3077,16 @@ var LimitOrderService = class {
3025
3077
  }
3026
3078
  }
3027
3079
  /**
3028
- * The pairs an order can be written against, and the smallest payout each token may carry.
3080
+ * The pairs an order can be written against, the smallest payout each token may carry, and
3081
+ * the tokens the orderbook registers on each chain.
3029
3082
  *
3030
3083
  * The orderbook lists the books; `resolveBook` refuses a request naming anything else, so the
3031
- * operator is offered exactly these and no combination of symbols they could not post.
3084
+ * operator is offered exactly these and no combination of symbols they could not post. The
3085
+ * chains do the same for the source chains an order can accept.
3032
3086
  */
3033
3087
  async books() {
3034
3088
  const limits = await this.limits();
3035
- return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes };
3089
+ return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes, chains: limits.chains ?? [] };
3036
3090
  }
3037
3091
  /**
3038
3092
  * Reads `serverInfo` and `books`, reusing the last read for {@link LIMITS_TTL_MS}.
@@ -3114,6 +3168,18 @@ var LimitOrderService = class {
3114
3168
  );
3115
3169
  }
3116
3170
  }
3171
+ if (book.base !== book.quote) {
3172
+ const input = normalizeSymbol(request.tokenIn);
3173
+ const lacking = sources.filter((source) => {
3174
+ const listed = limits.chains?.find((chain) => chain.id === source);
3175
+ return listed && !listed.tokens.some((token) => normalizeSymbol(token.symbol) === input);
3176
+ });
3177
+ if (lacking.length > 0) {
3178
+ throw new LimitOrderValidationError(
3179
+ `The orderbook has no ${request.tokenIn} on ${lacking.join(", ")}, so swaps from there cannot pay this order`
3180
+ );
3181
+ }
3182
+ }
3117
3183
  if (ttlSecs < limits.serverInfo.minOrderTtlSecs) {
3118
3184
  throw new LimitOrderValidationError(
3119
3185
  `ttlSecs must be at least the orderbook's minimum of ${limits.serverInfo.minOrderTtlSecs}; got ${ttlSecs}`
@@ -12984,14 +13050,10 @@ var RATE_LIMIT_RPC_CODES = /* @__PURE__ */ new Set([-32005, -32097, -32016, 429]
12984
13050
  var RATE_LIMIT_SUSPENSION_MS = 5 * 6e4;
12985
13051
  var SCAN_BUDGET = {
12986
13052
  timeoutMs: 5e3,
12987
- retryCount: 1,
12988
- retryDelayMs: 500,
12989
13053
  deadlineMs: 12e3
12990
13054
  };
12991
13055
  var CONFIRMATION_BUDGET = {
12992
13056
  timeoutMs: 1e4,
12993
- retryCount: 1,
12994
- retryDelayMs: 500,
12995
13057
  deadlineMs: 3e4
12996
13058
  };
12997
13059
  var MALFORMED_RESPONSE_SUSPENSION_MS = 3e4;
@@ -13083,8 +13145,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13083
13145
  chain,
13084
13146
  transport: (0, import_viem14.http)(url, {
13085
13147
  timeout: budget.timeoutMs,
13086
- retryCount: budget.retryCount,
13087
- retryDelay: budget.retryDelayMs
13148
+ // No transport-level retry. viem's retry sleeps for the provider's
13149
+ // `Retry-After` verbatim and uncapped, which reaches the socket
13150
+ // before {@link suspensionMsFor} can apply its clamp — and even
13151
+ // without that header, retrying inside the task is what makes a
13152
+ // task's worst case (timeout, backoff, timeout) approach the
13153
+ // deadline that is supposed to bound it. One attempt per endpoint
13154
+ // per call; `retryPromise` above the quorum owns the retry policy.
13155
+ retryCount: 0
13088
13156
  })
13089
13157
  })
13090
13158
  );
@@ -13098,11 +13166,10 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13098
13166
  return this.rpcUrls.filter((url) => (_QuorumPublicClient.benchedUntil.get(url) ?? 0) > now);
13099
13167
  }
13100
13168
  /**
13101
- * The endpoints this call queries — everyone not benched — and the quorum
13102
- * bar for exactly that set. A benched endpoint is dropped outright: it
13103
- * answers nothing while throttled, so counting it could only fail calls the
13104
- * remaining endpoints agree on. With every endpoint benched, everyone is
13105
- * queried again; there is nobody left to prefer.
13169
+ * The endpoints this call queries: everyone not benched. A benched endpoint is
13170
+ * dropped outright: it answers nothing while throttled, so asking it could only
13171
+ * cost a request. With every endpoint benched, everyone is queried again; there
13172
+ * is nobody left to prefer.
13106
13173
  */
13107
13174
  participants() {
13108
13175
  const now = Date.now();
@@ -13110,12 +13177,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13110
13177
  const active = all.filter(({ idx }) => (_QuorumPublicClient.benchedUntil.get(this.rpcUrls[idx]) ?? 0) <= now);
13111
13178
  const allBenched = active.length === 0;
13112
13179
  const queried = allBenched ? all : active;
13113
- return {
13114
- queried,
13115
- skipped: all.length - queried.length,
13116
- threshold: quorumThreshold(queried.length),
13117
- allBenched
13118
- };
13180
+ return { queried, skipped: all.length - queried.length, allBenched };
13119
13181
  }
13120
13182
  /**
13121
13183
  * Records a task failure against its endpoint. Only the failures
@@ -13160,17 +13222,21 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13160
13222
  * retries would otherwise stall every confirmation poll). Stragglers settle
13161
13223
  * out of band and are ignored. `tryDecide` returns undefined to keep
13162
13224
  * waiting; once every task has settled, `finalize` must decide (it throws
13163
- * the QuorumError when the full response set still has no quorum). Early
13164
- * exit never weakens the trust model — a decision still requires the same
13165
- * quorum, it just doesn't wait for votes it no longer needs.
13225
+ * the QuorumError when the full response set still has no quorum).
13226
+ *
13227
+ * Both are handed the bar: the BFT threshold over the endpoints that have not
13228
+ * failed, answered or still out. A failure lowers it rather than counting
13229
+ * against it. Counting the pending endpoints keeps an early decision to a bar
13230
+ * no lower than the full response set would get, so a vote it did not wait for
13231
+ * could never have reversed it.
13166
13232
  *
13167
13233
  * `deadlineMs` bounds the other direction: when the bar is NOT reachable
13168
13234
  * from the fast responders, the call used to wait out every straggler, and
13169
13235
  * the caller's own retry loop multiplied that into minutes of a chain not
13170
13236
  * being scanned at all, silently. At the deadline the outstanding endpoints
13171
13237
  * are treated as failures — named in the QuorumError, so the operator sees
13172
- * which ones held the call up — and `finalize` decides on the rest. The
13173
- * trust model is untouched: a decision still needs the same quorum.
13238
+ * which ones held the call up — and `finalize` decides over the endpoints
13239
+ * that answered.
13174
13240
  */
13175
13241
  settleUntilQuorum(tasks, tryDecide, finalize) {
13176
13242
  return new Promise((resolve, reject) => {
@@ -13188,13 +13254,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13188
13254
  const evaluate = () => {
13189
13255
  if (done) return;
13190
13256
  try {
13191
- const early = tryDecide(fulfilled);
13257
+ const threshold = quorumThreshold(tasks.length - failures.length);
13258
+ const early = tryDecide(fulfilled, threshold);
13192
13259
  if (early !== void 0) {
13193
13260
  finish(() => resolve(early));
13194
13261
  return;
13195
13262
  }
13196
13263
  if (outstanding === 0) {
13197
- const value = finalize(fulfilled, failures);
13264
+ const value = finalize(fulfilled, failures, threshold);
13198
13265
  finish(() => resolve(value));
13199
13266
  }
13200
13267
  } catch (error) {
@@ -13212,7 +13279,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13212
13279
  error: new Error(`no answer within the ${this.deadlineMs}ms quorum deadline`)
13213
13280
  }));
13214
13281
  try {
13215
- const value = finalize(fulfilled, [...failures, ...stragglers]);
13282
+ const value = finalize(fulfilled, [...failures, ...stragglers], quorumThreshold(fulfilled.length));
13216
13283
  finish(() => resolve(value));
13217
13284
  } catch (error) {
13218
13285
  finish(() => reject(error));
@@ -13239,22 +13306,22 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13239
13306
  });
13240
13307
  }
13241
13308
  /**
13242
- * Highest block head backed by a full quorum. Throws {@link QuorumError}
13243
- * when fewer than `threshold` endpoints respond.
13309
+ * Highest block head backed by a quorum of the endpoints that answer. Throws
13310
+ * {@link QuorumError} when none do.
13244
13311
  */
13245
13312
  async getBlockNumber() {
13246
- const { queried, skipped, threshold, allBenched } = this.participants();
13313
+ const { queried, skipped, allBenched } = this.participants();
13247
13314
  return this.settleUntilQuorum(
13248
13315
  queried.map(({ idx, client }) => ({ idx, task: client.getBlockNumber() })),
13249
13316
  // Early exit as soon as a quorum is satisfiable. Late voters could
13250
13317
  // only raise the reported head; the earlier (lower) head is
13251
13318
  // conservative for every consumer (fewer confirmations counted,
13252
13319
  // smaller scan windows).
13253
- (fulfilled) => this.quorumHead(
13320
+ (fulfilled, threshold) => this.quorumHead(
13254
13321
  fulfilled.map((f) => f.value),
13255
13322
  threshold
13256
13323
  ) ?? void 0,
13257
- (fulfilled, failures) => {
13324
+ (fulfilled, failures, threshold) => {
13258
13325
  const head = this.quorumHead(
13259
13326
  fulfilled.map((f) => f.value),
13260
13327
  threshold
@@ -13295,7 +13362,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13295
13362
  }
13296
13363
  return views;
13297
13364
  };
13298
- const { queried, skipped, threshold, allBenched } = this.participants();
13365
+ const { queried, skipped, allBenched } = this.participants();
13299
13366
  return this.settleUntilQuorum(
13300
13367
  queried.map(({ idx, client }) => ({
13301
13368
  idx,
@@ -13314,8 +13381,8 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13314
13381
  // Early exit only on a POSITIVE quorum: an agreeing inclusion group can
13315
13382
  // only grow, so the first satisfiable quorum is final. Not-found votes
13316
13383
  // never decide early — "not confirmed" needs the full response set.
13317
- (fulfilled) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
13318
- (fulfilled, failures) => {
13384
+ (fulfilled, threshold) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
13385
+ (fulfilled, failures, threshold) => {
13319
13386
  const confirmations = aggregateConfirmations(toViews(fulfilled), threshold);
13320
13387
  if (confirmations === null) {
13321
13388
  throw new QuorumError(
@@ -13344,7 +13411,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13344
13411
  }
13345
13412
  return groups;
13346
13413
  };
13347
- const { queried, skipped, threshold, allBenched } = this.participants();
13414
+ const { queried, skipped, allBenched } = this.participants();
13348
13415
  return this.settleUntilQuorum(
13349
13416
  queried.map(({ idx, client }) => ({
13350
13417
  idx,
@@ -13356,13 +13423,13 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13356
13423
  // two disjoint groups can't both hold one), so the first satisfiable
13357
13424
  // agreement is final — no need to wait out stragglers that could
13358
13425
  // only join or lose.
13359
- (fulfilled) => {
13426
+ (fulfilled, threshold) => {
13360
13427
  for (const group of groupOf(fulfilled).values()) {
13361
13428
  if (group.providerIdxs.length >= threshold) return group.result;
13362
13429
  }
13363
13430
  return void 0;
13364
13431
  },
13365
- (fulfilled, failures) => {
13432
+ (fulfilled, failures, threshold) => {
13366
13433
  const groups = groupOf(fulfilled);
13367
13434
  for (const group of groups.values()) {
13368
13435
  if (group.providerIdxs.length >= threshold) return group.result;
@@ -18575,20 +18642,32 @@ var ContractInteractionService = class {
18575
18642
  } else {
18576
18643
  this.logger.warn({ reason: pmResult.reason }, "No paymaster for bid UserOp; relying on EntryPoint deposit");
18577
18644
  }
18578
- const userOp = await sdkHelper.prepareSubmitBid({
18579
- order,
18580
- fillOptions,
18645
+ const bidGas = {
18581
18646
  solverAccount: solverAccountAddress,
18582
- solverSigner: sdkSigningAccount(this.signer),
18583
18647
  nonce,
18584
18648
  entryPointAddress,
18585
18649
  callGasLimit,
18586
18650
  verificationGasLimit: cachedEstimate.verificationGasLimit,
18587
- preVerificationGas: cachedEstimate.preVerificationGas,
18588
18651
  maxFeePerGas: cachedEstimate.maxFeePerGas,
18589
18652
  maxPriorityFeePerGas: cachedEstimate.maxPriorityFeePerGas,
18590
18653
  callData,
18591
18654
  paymasterAndData
18655
+ };
18656
+ let preVerificationGas = cachedEstimate.preVerificationGas;
18657
+ try {
18658
+ preVerificationGas = await sdkHelper.estimateBidPreVerificationGas(bidGas);
18659
+ } catch (err) {
18660
+ this.logger.warn(
18661
+ { orderId: order.id, err, preVerificationGas: preVerificationGas.toString() },
18662
+ "Could not estimate the bid's preVerificationGas; using the fill estimate's"
18663
+ );
18664
+ }
18665
+ const userOp = await sdkHelper.prepareSubmitBid({
18666
+ ...bidGas,
18667
+ order,
18668
+ fillOptions,
18669
+ solverSigner: sdkSigningAccount(this.signer),
18670
+ preVerificationGas
18592
18671
  });
18593
18672
  const encodedUserOp = (0, import_sdk7.encodeUserOpScale)(userOp);
18594
18673
  this.logger.info(
@@ -18597,6 +18676,7 @@ var ContractInteractionService = class {
18597
18676
  solverAccount: solverAccountAddress,
18598
18677
  nonce: nonce.toString(),
18599
18678
  callGasLimit: callGasLimit.toString(),
18679
+ preVerificationGas: preVerificationGas.toString(),
18600
18680
  maxFeePerGas: cachedEstimate.maxFeePerGas.toString()
18601
18681
  },
18602
18682
  "Prepared bid UserOp"
@@ -21275,8 +21355,15 @@ var IntentFiller = class {
21275
21355
  const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
21276
21356
  this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
21277
21357
  if (validStrategies.length === 0) {
21278
- this.logger.warn({ orderId: order.id }, "No profitable strategy found for order");
21279
- this.monitor.emit("orderSkipped", { orderId: order.id, reason: "No profitable strategy" });
21358
+ const priced = eligibleStrategies.some((s) => s !== null);
21359
+ this.logger.warn(
21360
+ { orderId: order.id },
21361
+ priced ? "No profitable strategy found for order" : "No strategy can fill this order"
21362
+ );
21363
+ this.monitor.emit("orderSkipped", {
21364
+ orderId: order.id,
21365
+ reason: priced ? "No profitable strategy" : "No strategy can fill this order"
21366
+ });
21280
21367
  return null;
21281
21368
  }
21282
21369
  this.logger.info(
@@ -21698,10 +21785,6 @@ var import_decimal6 = require("decimal.js");
21698
21785
  function limitOrderLegs(order) {
21699
21786
  return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
21700
21787
  }
21701
- function availableOn(order) {
21702
- const available = BigInt(order.remaining) - BigInt(order.reserved);
21703
- return available > 0n ? available : 0n;
21704
- }
21705
21788
  function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21706
21789
  const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
21707
21790
  const offer = offerFor({
@@ -21710,7 +21793,7 @@ function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new D
21710
21793
  price: BigInt(order.price),
21711
21794
  outputDecimals: incoming.outputDecimals
21712
21795
  });
21713
- const available = availableOn(order);
21796
+ const available = BigInt(order.remaining);
21714
21797
  return { order, offer, available, payout: offer < available ? offer : available };
21715
21798
  }).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
21716
21799
  return qualifying;
@@ -21720,16 +21803,63 @@ function byBestOfferFirst(a, b) {
21720
21803
  if (a.available !== b.available) return a.available > b.available ? -1 : 1;
21721
21804
  return a.order.id < b.order.id ? -1 : 1;
21722
21805
  }
21806
+ function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21807
+ let furthest = -1;
21808
+ for (const order of orders) {
21809
+ const failed = firstFailed(order, incoming, resolve, now);
21810
+ furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
21811
+ }
21812
+ const { source, destination, inputSymbol } = incoming;
21813
+ if (furthest < CHECKS.length) {
21814
+ switch (CHECKS[Math.max(furthest, 0)]) {
21815
+ case "open":
21816
+ return "no limit order is open";
21817
+ case "chain":
21818
+ return `no open limit order fills on ${destination}`;
21819
+ case "input":
21820
+ return `no limit order on ${destination} takes ${inputSymbol} in`;
21821
+ case "output":
21822
+ return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
21823
+ case "source":
21824
+ return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
21825
+ }
21826
+ }
21827
+ const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
21828
+ order,
21829
+ offer: offerFor({
21830
+ side: order.side,
21831
+ inputAmount: incoming.inputNet,
21832
+ price: BigInt(order.price),
21833
+ outputDecimals: incoming.outputDecimals
21834
+ })
21835
+ }));
21836
+ if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
21837
+ return "the limit orders that meet its rate have nothing left to pay out";
21838
+ }
21839
+ const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
21840
+ const symbol = limitOrderLegs(best.order).output;
21841
+ return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
21842
+ }
21843
+ var CHECKS = ["open", "chain", "input", "output", "source"];
21723
21844
  function serves(order, incoming, resolve, now) {
21724
- if (order.status !== "open") return false;
21725
- if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return false;
21726
- if (order.fillChain !== incoming.destination) return false;
21845
+ return firstFailed(order, incoming, resolve, now) === null;
21846
+ }
21847
+ function firstFailed(order, incoming, resolve, now) {
21848
+ if (order.status !== "open") return "open";
21849
+ if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
21850
+ if (order.fillChain !== incoming.destination) return "chain";
21727
21851
  const legs = limitOrderLegs(order);
21728
- if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return false;
21852
+ if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
21729
21853
  const outputToken = resolve(legs.output, order.fillChain);
21730
- if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return false;
21731
- if (incoming.source === incoming.destination) return true;
21732
- return order.acceptedSources.includes(incoming.source);
21854
+ if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
21855
+ if (incoming.source === incoming.destination) return null;
21856
+ return order.acceptedSources.includes(incoming.source) ? null : "source";
21857
+ }
21858
+ function readable(amount) {
21859
+ const [whole, fraction] = toHuman(amount).split(".");
21860
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
21861
+ const cut = fraction?.slice(0, 6).replace(/0+$/, "");
21862
+ return cut ? `${grouped}.${cut}` : grouped;
21733
21863
  }
21734
21864
 
21735
21865
  // src/orderbook/usd.ts
@@ -21871,15 +22001,20 @@ var FXFiller = class _FXFiller {
21871
22001
  }
21872
22002
  try {
21873
22003
  if (order.inputs.length !== order.output.assets.length) {
21874
- this.logger.debug(
21875
- { inputs: order.inputs.length, outputs: order.output.assets.length },
22004
+ this.logger.info(
22005
+ { orderId: order.id, inputs: order.inputs.length, outputs: order.output.assets.length },
21876
22006
  "Order input/output length mismatch or empty"
21877
22007
  );
21878
22008
  return false;
21879
22009
  }
21880
22010
  if ((await this.matchOrder(order)).length === 0) {
21881
- this.logger.debug(
21882
- { orderId: order.id, sourceChain: order.source, destChain: order.destination },
22011
+ this.logger.info(
22012
+ {
22013
+ orderId: order.id,
22014
+ sourceChain: order.source,
22015
+ destChain: order.destination,
22016
+ reason: await this.explainUnmatched(order)
22017
+ },
21883
22018
  "No limit order matches this order"
21884
22019
  );
21885
22020
  return false;
@@ -22421,30 +22556,53 @@ var FXFiller = class _FXFiller {
22421
22556
  */
22422
22557
  async matchLeg(order, leg) {
22423
22558
  if (!this.limitOrders) return [];
22559
+ const incoming = await this.incomingFor(order, leg);
22560
+ if (!incoming) return [];
22561
+ return matchLimitOrders(
22562
+ await this.limitOrders.open(),
22563
+ incoming,
22564
+ (symbol, chain) => this.registry.getAddress(symbol, chain)
22565
+ );
22566
+ }
22567
+ /** Why no limit order serves the order, leg by leg, for the line that says it was passed over. */
22568
+ async explainUnmatched(order) {
22569
+ if (!this.limitOrders) return "limit orders are not loaded";
22570
+ const open = await this.limitOrders.open();
22571
+ const reasons = [];
22572
+ for (let leg = 0; leg < order.inputs.length; leg++) {
22573
+ const incoming = await this.incomingFor(order, leg);
22574
+ const reason = incoming ? whyUnmatched(open, incoming, (symbol, chain) => this.registry.getAddress(symbol, chain)) : `its input token is not in the asset registry on ${order.source}`;
22575
+ reasons.push(order.inputs.length > 1 ? `leg ${leg}: ${reason}` : reason);
22576
+ }
22577
+ return reasons.join("; ");
22578
+ }
22579
+ /**
22580
+ * One leg as the matcher sees it, or null when its input token is not one the asset
22581
+ * registry knows on the source chain.
22582
+ *
22583
+ * Amounts cross into the matcher at 1e18, the unit limit orders are kept in.
22584
+ */
22585
+ async incomingFor(order, leg) {
22424
22586
  const input = order.inputs[leg];
22425
22587
  const output = order.output.assets[leg];
22426
- if (!input || !output) return [];
22588
+ if (!input || !output) return null;
22427
22589
  const inputToken = (0, import_sdk15.bytes32ToBytes20)(input.token);
22428
22590
  const outputToken = (0, import_sdk15.bytes32ToBytes20)(output.token);
22429
22591
  const inputSymbol = this.registry.symbolFor(inputToken, order.source);
22430
- if (!inputSymbol) return [];
22592
+ if (!inputSymbol) return null;
22431
22593
  const [inputDecimals, outputDecimals] = await Promise.all([
22432
22594
  this.contractService.getTokenDecimals(inputToken, order.source),
22433
22595
  this.contractService.getTokenDecimals(outputToken, order.destination)
22434
22596
  ]);
22435
- return matchLimitOrders(
22436
- await this.limitOrders.open(),
22437
- {
22438
- source: order.source,
22439
- destination: order.destination,
22440
- inputSymbol,
22441
- outputToken,
22442
- inputNet: toScaled(input.amount, inputDecimals),
22443
- requestedOutput: toScaled(output.amount, outputDecimals),
22444
- outputDecimals
22445
- },
22446
- (symbol, chain) => this.registry.getAddress(symbol, chain)
22447
- );
22597
+ return {
22598
+ source: order.source,
22599
+ destination: order.destination,
22600
+ inputSymbol,
22601
+ outputToken,
22602
+ inputNet: toScaled(input.amount, inputDecimals),
22603
+ requestedOutput: toScaled(output.amount, outputDecimals),
22604
+ outputDecimals
22605
+ };
22448
22606
  }
22449
22607
  /**
22450
22608
  * The order's input in **USD**, or null when no limit order connects its input
@@ -23522,48 +23680,42 @@ async function bootFiller(config, options) {
23522
23680
  assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
23523
23681
  }
23524
23682
  const adminStrategies = [];
23525
- const strategies = [];
23526
- let confirmationPolicy;
23527
- let tradingPairs;
23528
- let engine;
23529
- if (config.pairs?.length) {
23530
- tradingPairs = config.pairs.map((pair) => ({ token0: pair.token0, token1: pair.token1 }));
23531
- tradingPairs.forEach((pair, pairIndex) => {
23532
- adminStrategies.push({
23533
- index: pairIndex,
23534
- pairIndex,
23535
- exotic: `${pair.token0}/${pair.token1}`,
23536
- token0: pair.token0,
23537
- token1: pair.token1,
23538
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
23539
- });
23683
+ const tradingPairs = (config.pairs ?? []).map((pair) => ({
23684
+ token0: pair.token0,
23685
+ token1: pair.token1
23686
+ }));
23687
+ tradingPairs.forEach((pair, pairIndex) => {
23688
+ adminStrategies.push({
23689
+ index: pairIndex,
23690
+ pairIndex,
23691
+ exotic: `${pair.token0}/${pair.token1}`,
23692
+ token0: pair.token0,
23693
+ token1: pair.token1,
23694
+ sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
23540
23695
  });
23541
- confirmationPolicy = assertConfirmationCoverage(
23542
- config.confirmationPolicies,
23543
- resolvedChains.map((c) => c.chainId)
23544
- );
23545
- const fundingVenues = vaultVenue ? [vaultVenue] : [];
23546
- engine = new FXFiller(
23547
- runtimeSigner,
23548
- configService,
23549
- chainClientManager,
23550
- contractService,
23551
- tradingPairs,
23552
- assetRegistry,
23553
- { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
23554
- );
23555
- logger.info("Hydrating funding venue state...");
23556
- await engine.initialise();
23557
- strategies.push(engine);
23558
- }
23696
+ });
23697
+ const confirmationPolicy = assertConfirmationCoverage(
23698
+ config.confirmationPolicies,
23699
+ resolvedChains.map((c) => c.chainId)
23700
+ );
23701
+ const fundingVenues = vaultVenue ? [vaultVenue] : [];
23702
+ const engine = new FXFiller(
23703
+ runtimeSigner,
23704
+ configService,
23705
+ chainClientManager,
23706
+ contractService,
23707
+ tradingPairs,
23708
+ assetRegistry,
23709
+ { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
23710
+ );
23711
+ logger.info("Hydrating funding venue state...");
23712
+ await engine.initialise();
23713
+ const strategies = [engine];
23559
23714
  const haltControls = strategies.map((engine2, index) => ({
23560
23715
  index,
23561
23716
  isHalted: () => engine2.isHalted(),
23562
23717
  resetHalt: () => engine2.resetHalt()
23563
23718
  }));
23564
- if (vaultVenue) {
23565
- await vaultVenue.initialise(runtimeSigner.address);
23566
- }
23567
23719
  let rebalancingService;
23568
23720
  const rebalancingConfig = configService.getRebalancingConfig();
23569
23721
  if (rebalancingConfig?.triggerPercentage !== void 0 && rebalancingConfig?.baseBalances) {
@@ -24080,8 +24232,8 @@ var MemoryLimitOrderStore = class {
24080
24232
  async reserve(id, amount) {
24081
24233
  const order = this.orders.get(id);
24082
24234
  if (!order || order.status !== "open") return false;
24235
+ if (BigInt(amount) > BigInt(order.remaining)) return false;
24083
24236
  const reserved = BigInt(order.reserved) + BigInt(amount);
24084
- if (reserved > BigInt(order.remaining)) return false;
24085
24237
  this.patch(id, { reserved: reserved.toString() });
24086
24238
  return true;
24087
24239
  }
@@ -24157,6 +24309,8 @@ var MAX_BLOCK_RANGE = 1000n;
24157
24309
  var PUBLISH_CHUNK = 250;
24158
24310
  var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
24159
24311
  var STOP_DRAIN_TIMEOUT_MS = 5e3;
24312
+ var STALL_THRESHOLD_MS = 45e3;
24313
+ var STALL_REPORT_INTERVAL_MS = 6e4;
24160
24314
  var ChainScanner = class {
24161
24315
  constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
24162
24316
  this.target = target;
@@ -24170,6 +24324,18 @@ var ChainScanner = class {
24170
24324
  this.errorHandler = () => {
24171
24325
  };
24172
24326
  this.stopped = false;
24327
+ /**
24328
+ * How far past the cursor the next pass reads, as `toBlock - fromBlock`:
24329
+ * halved after a failed read, doubled after a good one, between 0 (one block)
24330
+ * and {@link MAX_BLOCK_RANGE}.
24331
+ *
24332
+ * Free endpoints cap `eth_getLogs` ranges far below the full span — on
24333
+ * Arbitrum three of seven refused 100 blocks — and the cursor never moves
24334
+ * past a range it failed to read. At a fixed span a catch-up that outgrew
24335
+ * those caps was stuck for good: the range only grew, so the capped endpoints
24336
+ * never rejoined and the quorum never formed.
24337
+ */
24338
+ this.span = MAX_BLOCK_RANGE;
24173
24339
  this.logger = loggers.get("chain-scanner");
24174
24340
  this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
24175
24341
  if (this.quorumClient.size > 1) {
@@ -24212,8 +24378,9 @@ var ChainScanner = class {
24212
24378
  if (this.timer || this.stopped) return;
24213
24379
  this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
24214
24380
  this.timer = setInterval(() => {
24215
- if (this.mutex.isLocked()) return;
24381
+ if (this.mutex.isLocked()) return this.reportStall();
24216
24382
  void this.mutex.runExclusive(async () => {
24383
+ this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
24217
24384
  try {
24218
24385
  await this.scan();
24219
24386
  } catch (error) {
@@ -24227,10 +24394,66 @@ var ChainScanner = class {
24227
24394
  "Error in block scanner"
24228
24395
  );
24229
24396
  this.errorHandler(error);
24397
+ } finally {
24398
+ this.notePassEnd();
24230
24399
  }
24231
24400
  });
24232
24401
  }, this.scanIntervalMs);
24233
24402
  }
24403
+ /**
24404
+ * Records which await the current pass is sitting on, for the watchdog.
24405
+ *
24406
+ * A no-op when no pass is registered — `scan()` is driven directly by tests
24407
+ * and by nothing else, so there is no watchdog to inform.
24408
+ */
24409
+ enter(phase) {
24410
+ if (this.pass) this.pass.phase = phase;
24411
+ }
24412
+ /**
24413
+ * Says that a pass has outlived the quorum budgets and names the phase it is
24414
+ * stuck in, then repeats every {@link STALL_REPORT_INTERVAL_MS} until it ends.
24415
+ *
24416
+ * Observation only: the pass is left alone. Aborting it would need a cancel
24417
+ * path through the quorum client that does not exist, and the cursor is safe
24418
+ * either way — a pass that never completes never advances it.
24419
+ */
24420
+ reportStall() {
24421
+ const pass = this.pass;
24422
+ if (!pass) return;
24423
+ const now = Date.now();
24424
+ const heldMs = now - pass.startedAt;
24425
+ if (heldMs < STALL_THRESHOLD_MS) return;
24426
+ if (pass.reportedAt !== 0 && now - pass.reportedAt < STALL_REPORT_INTERVAL_MS) return;
24427
+ pass.reportedAt = now;
24428
+ this.logger.warn(
24429
+ {
24430
+ chainId: this.target.chainId,
24431
+ phase: pass.phase,
24432
+ heldMs,
24433
+ cursor: this.cursor?.toString(),
24434
+ rpcUrls: this.target.rpcUrls
24435
+ },
24436
+ "Block scan pass has outlived its budget \u2014 this chain is not being scanned"
24437
+ );
24438
+ }
24439
+ /**
24440
+ * Closes out a pass, and says so if the watchdog complained about it — without
24441
+ * this the log shows a chain going quiet and never shows it coming back.
24442
+ */
24443
+ notePassEnd() {
24444
+ const pass = this.pass;
24445
+ this.pass = void 0;
24446
+ if (!pass || pass.reportedAt === 0) return;
24447
+ this.logger.warn(
24448
+ {
24449
+ chainId: this.target.chainId,
24450
+ phase: pass.phase,
24451
+ heldMs: Date.now() - pass.startedAt,
24452
+ cursor: this.cursor?.toString()
24453
+ },
24454
+ "Block scan pass finished after outliving its budget \u2014 scanning has resumed"
24455
+ );
24456
+ }
24234
24457
  /**
24235
24458
  * Points this loop at different endpoints, keeping the cursor.
24236
24459
  *
@@ -24272,11 +24495,12 @@ var ChainScanner = class {
24272
24495
  }
24273
24496
  if (currentBlock <= this.cursor) return;
24274
24497
  const fromBlock = this.cursor + 1n;
24275
- const toBlock = fromBlock + MAX_BLOCK_RANGE > currentBlock ? currentBlock : fromBlock + MAX_BLOCK_RANGE;
24498
+ const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
24276
24499
  this.logger.debug(
24277
24500
  { chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
24278
24501
  "Scanning blocks"
24279
24502
  );
24503
+ this.enter("logs");
24280
24504
  let logs;
24281
24505
  try {
24282
24506
  logs = await (0, import_sdk17.retryPromise)(
@@ -24298,6 +24522,7 @@ var ChainScanner = class {
24298
24522
  );
24299
24523
  } catch (error) {
24300
24524
  if (isBlockRangeError(error)) return;
24525
+ this.narrow(toBlock - fromBlock);
24301
24526
  throw error;
24302
24527
  }
24303
24528
  if (this.stopped) return;
@@ -24308,6 +24533,7 @@ var ChainScanner = class {
24308
24533
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
24309
24534
  "Found OrderPlaced events in block scan"
24310
24535
  );
24536
+ this.enter("publish-orders");
24311
24537
  await this.publishOrders(placed);
24312
24538
  }
24313
24539
  if (filled.length > 0) {
@@ -24315,10 +24541,36 @@ var ChainScanner = class {
24315
24541
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
24316
24542
  "Found OrderFilled events in block scan"
24317
24543
  );
24544
+ this.enter("publish-fills");
24318
24545
  await this.publishFills(filled);
24319
24546
  }
24320
24547
  if (this.stopped) return;
24321
24548
  this.cursor = toBlock;
24549
+ this.widen();
24550
+ }
24551
+ /**
24552
+ * Halves the span below the range that just failed, so the next pass asks
24553
+ * for something a range-capped endpoint will answer. Halving the attempted
24554
+ * range rather than the span matters near the head, where a pass reads fewer
24555
+ * blocks than the span allows.
24556
+ */
24557
+ narrow(attempted) {
24558
+ const next = attempted / 2n;
24559
+ if (next >= this.span) return;
24560
+ this.span = next;
24561
+ this.logger.warn(
24562
+ { chainId: this.target.chainId, span: Number(next) },
24563
+ "Narrowing the block scan range after a failed read"
24564
+ );
24565
+ }
24566
+ /** Doubles the span back toward {@link MAX_BLOCK_RANGE} after a good read. */
24567
+ widen() {
24568
+ if (this.span >= MAX_BLOCK_RANGE) return;
24569
+ const next = this.span === 0n ? 1n : this.span * 2n;
24570
+ this.span = next > MAX_BLOCK_RANGE ? MAX_BLOCK_RANGE : next;
24571
+ if (this.span === MAX_BLOCK_RANGE) {
24572
+ this.logger.info({ chainId: this.target.chainId }, "Block scan range back to full");
24573
+ }
24322
24574
  }
24323
24575
  async publishOrders(logs) {
24324
24576
  const rebuilt = reconstructOrdersFromLogs(logs, {
@@ -24685,7 +24937,7 @@ var LimitOrderController = class {
24685
24937
  ]);
24686
24938
  return { order, fills, bids };
24687
24939
  }
24688
- /** The pairs the orderbook lists, and the smallest payout each token may carry. */
24940
+ /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
24689
24941
  books() {
24690
24942
  return this.service.books();
24691
24943
  }
@@ -24733,10 +24985,7 @@ var PairController = class {
24733
24985
  * the pair's symbols have to resolve before the market can be hydrated.
24734
24986
  */
24735
24987
  async add(pair, options) {
24736
- const { engine, tradingPairs, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
24737
- if (!engine || !tradingPairs) {
24738
- throw new Error("No trading engine is running \u2014 this filler was started without pairs");
24739
- }
24988
+ const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
24740
24989
  const assets = options?.assets;
24741
24990
  const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
24742
24991
  const nextPairs = [...this.pairs, pair];
@@ -24775,7 +25024,6 @@ var PairController = class {
24775
25024
  /** Closes a market. Orders already in flight against it still complete. */
24776
25025
  async remove(index) {
24777
25026
  const { engine, tradingPairs, adminStrategies, config } = this.runtime;
24778
- if (!engine || !tradingPairs) throw new Error("No trading engine is running");
24779
25027
  if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
24780
25028
  engine.removePair(tradingPairs[index]);
24781
25029
  config.pairs = this.pairs.filter((_, i) => i !== index);
@@ -24787,7 +25035,7 @@ var PairController = class {
24787
25035
  await this.persist();
24788
25036
  }
24789
25037
  livePair(index) {
24790
- const pair = this.runtime.tradingPairs?.[index];
25038
+ const pair = this.runtime.tradingPairs[index];
24791
25039
  if (!pair) throw new Error(`Unknown pair ${index}`);
24792
25040
  return pair;
24793
25041
  }
@@ -24879,9 +25127,9 @@ var ChainController = class {
24879
25127
  throw error;
24880
25128
  }
24881
25129
  const curve = confirmationPolicies[String(chainId)];
24882
- if (curve) this.runtime.confirmationPolicy?.add(chainId, curve);
24883
- else if (!this.runtime.confirmationPolicy?.has(chainId)) {
24884
- this.runtime.confirmationPolicy?.adopt(chainId, resolvedPolicy);
25130
+ if (curve) this.runtime.confirmationPolicy.add(chainId, curve);
25131
+ else if (!this.runtime.confirmationPolicy.has(chainId)) {
25132
+ this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
24885
25133
  }
24886
25134
  this.runtime.resolvedChains.push(resolved);
24887
25135
  config.chains = [...config.chains, { rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }];
@@ -24909,7 +25157,7 @@ var ChainController = class {
24909
25157
  await intentFiller.removeChain(chainId);
24910
25158
  if (this.ownsScanner) await this.scanner.removeChain(chainId);
24911
25159
  configService.removeChain(chainId);
24912
- this.runtime.confirmationPolicy?.remove(chainId);
25160
+ this.runtime.confirmationPolicy.remove(chainId);
24913
25161
  this.runtime.chainClientManager.invalidate(chainKey);
24914
25162
  this.runtime.resolvedChains.splice(index, 1);
24915
25163
  config.chains = config.chains.filter((_, i) => i !== index);