@hyperbridge/simplex 0.16.6 → 0.16.7

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;
@@ -21275,8 +21342,15 @@ var IntentFiller = class {
21275
21342
  const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
21276
21343
  this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
21277
21344
  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" });
21345
+ const priced = eligibleStrategies.some((s) => s !== null);
21346
+ this.logger.warn(
21347
+ { orderId: order.id },
21348
+ priced ? "No profitable strategy found for order" : "No strategy can fill this order"
21349
+ );
21350
+ this.monitor.emit("orderSkipped", {
21351
+ orderId: order.id,
21352
+ reason: priced ? "No profitable strategy" : "No strategy can fill this order"
21353
+ });
21280
21354
  return null;
21281
21355
  }
21282
21356
  this.logger.info(
@@ -21720,16 +21794,63 @@ function byBestOfferFirst(a, b) {
21720
21794
  if (a.available !== b.available) return a.available > b.available ? -1 : 1;
21721
21795
  return a.order.id < b.order.id ? -1 : 1;
21722
21796
  }
21797
+ function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21798
+ let furthest = -1;
21799
+ for (const order of orders) {
21800
+ const failed = firstFailed(order, incoming, resolve, now);
21801
+ furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
21802
+ }
21803
+ const { source, destination, inputSymbol } = incoming;
21804
+ if (furthest < CHECKS.length) {
21805
+ switch (CHECKS[Math.max(furthest, 0)]) {
21806
+ case "open":
21807
+ return "no limit order is open";
21808
+ case "chain":
21809
+ return `no open limit order fills on ${destination}`;
21810
+ case "input":
21811
+ return `no limit order on ${destination} takes ${inputSymbol} in`;
21812
+ case "output":
21813
+ return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
21814
+ case "source":
21815
+ return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
21816
+ }
21817
+ }
21818
+ const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
21819
+ order,
21820
+ offer: offerFor({
21821
+ side: order.side,
21822
+ inputAmount: incoming.inputNet,
21823
+ price: BigInt(order.price),
21824
+ outputDecimals: incoming.outputDecimals
21825
+ })
21826
+ }));
21827
+ if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
21828
+ return "the limit orders that meet its rate have nothing left to pay out";
21829
+ }
21830
+ const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
21831
+ const symbol = limitOrderLegs(best.order).output;
21832
+ return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
21833
+ }
21834
+ var CHECKS = ["open", "chain", "input", "output", "source"];
21723
21835
  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;
21836
+ return firstFailed(order, incoming, resolve, now) === null;
21837
+ }
21838
+ function firstFailed(order, incoming, resolve, now) {
21839
+ if (order.status !== "open") return "open";
21840
+ if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
21841
+ if (order.fillChain !== incoming.destination) return "chain";
21727
21842
  const legs = limitOrderLegs(order);
21728
- if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return false;
21843
+ if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
21729
21844
  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);
21845
+ if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
21846
+ if (incoming.source === incoming.destination) return null;
21847
+ return order.acceptedSources.includes(incoming.source) ? null : "source";
21848
+ }
21849
+ function readable(amount) {
21850
+ const [whole, fraction] = toHuman(amount).split(".");
21851
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
21852
+ const cut = fraction?.slice(0, 6).replace(/0+$/, "");
21853
+ return cut ? `${grouped}.${cut}` : grouped;
21733
21854
  }
21734
21855
 
21735
21856
  // src/orderbook/usd.ts
@@ -21871,15 +21992,20 @@ var FXFiller = class _FXFiller {
21871
21992
  }
21872
21993
  try {
21873
21994
  if (order.inputs.length !== order.output.assets.length) {
21874
- this.logger.debug(
21875
- { inputs: order.inputs.length, outputs: order.output.assets.length },
21995
+ this.logger.info(
21996
+ { orderId: order.id, inputs: order.inputs.length, outputs: order.output.assets.length },
21876
21997
  "Order input/output length mismatch or empty"
21877
21998
  );
21878
21999
  return false;
21879
22000
  }
21880
22001
  if ((await this.matchOrder(order)).length === 0) {
21881
- this.logger.debug(
21882
- { orderId: order.id, sourceChain: order.source, destChain: order.destination },
22002
+ this.logger.info(
22003
+ {
22004
+ orderId: order.id,
22005
+ sourceChain: order.source,
22006
+ destChain: order.destination,
22007
+ reason: await this.explainUnmatched(order)
22008
+ },
21883
22009
  "No limit order matches this order"
21884
22010
  );
21885
22011
  return false;
@@ -22421,30 +22547,53 @@ var FXFiller = class _FXFiller {
22421
22547
  */
22422
22548
  async matchLeg(order, leg) {
22423
22549
  if (!this.limitOrders) return [];
22550
+ const incoming = await this.incomingFor(order, leg);
22551
+ if (!incoming) return [];
22552
+ return matchLimitOrders(
22553
+ await this.limitOrders.open(),
22554
+ incoming,
22555
+ (symbol, chain) => this.registry.getAddress(symbol, chain)
22556
+ );
22557
+ }
22558
+ /** Why no limit order serves the order, leg by leg, for the line that says it was passed over. */
22559
+ async explainUnmatched(order) {
22560
+ if (!this.limitOrders) return "limit orders are not loaded";
22561
+ const open = await this.limitOrders.open();
22562
+ const reasons = [];
22563
+ for (let leg = 0; leg < order.inputs.length; leg++) {
22564
+ const incoming = await this.incomingFor(order, leg);
22565
+ 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}`;
22566
+ reasons.push(order.inputs.length > 1 ? `leg ${leg}: ${reason}` : reason);
22567
+ }
22568
+ return reasons.join("; ");
22569
+ }
22570
+ /**
22571
+ * One leg as the matcher sees it, or null when its input token is not one the asset
22572
+ * registry knows on the source chain.
22573
+ *
22574
+ * Amounts cross into the matcher at 1e18, the unit limit orders are kept in.
22575
+ */
22576
+ async incomingFor(order, leg) {
22424
22577
  const input = order.inputs[leg];
22425
22578
  const output = order.output.assets[leg];
22426
- if (!input || !output) return [];
22579
+ if (!input || !output) return null;
22427
22580
  const inputToken = (0, import_sdk15.bytes32ToBytes20)(input.token);
22428
22581
  const outputToken = (0, import_sdk15.bytes32ToBytes20)(output.token);
22429
22582
  const inputSymbol = this.registry.symbolFor(inputToken, order.source);
22430
- if (!inputSymbol) return [];
22583
+ if (!inputSymbol) return null;
22431
22584
  const [inputDecimals, outputDecimals] = await Promise.all([
22432
22585
  this.contractService.getTokenDecimals(inputToken, order.source),
22433
22586
  this.contractService.getTokenDecimals(outputToken, order.destination)
22434
22587
  ]);
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
- );
22588
+ return {
22589
+ source: order.source,
22590
+ destination: order.destination,
22591
+ inputSymbol,
22592
+ outputToken,
22593
+ inputNet: toScaled(input.amount, inputDecimals),
22594
+ requestedOutput: toScaled(output.amount, outputDecimals),
22595
+ outputDecimals
22596
+ };
22448
22597
  }
22449
22598
  /**
22450
22599
  * The order's input in **USD**, or null when no limit order connects its input
@@ -24157,6 +24306,8 @@ var MAX_BLOCK_RANGE = 1000n;
24157
24306
  var PUBLISH_CHUNK = 250;
24158
24307
  var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
24159
24308
  var STOP_DRAIN_TIMEOUT_MS = 5e3;
24309
+ var STALL_THRESHOLD_MS = 45e3;
24310
+ var STALL_REPORT_INTERVAL_MS = 6e4;
24160
24311
  var ChainScanner = class {
24161
24312
  constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
24162
24313
  this.target = target;
@@ -24170,6 +24321,18 @@ var ChainScanner = class {
24170
24321
  this.errorHandler = () => {
24171
24322
  };
24172
24323
  this.stopped = false;
24324
+ /**
24325
+ * How far past the cursor the next pass reads, as `toBlock - fromBlock`:
24326
+ * halved after a failed read, doubled after a good one, between 0 (one block)
24327
+ * and {@link MAX_BLOCK_RANGE}.
24328
+ *
24329
+ * Free endpoints cap `eth_getLogs` ranges far below the full span — on
24330
+ * Arbitrum three of seven refused 100 blocks — and the cursor never moves
24331
+ * past a range it failed to read. At a fixed span a catch-up that outgrew
24332
+ * those caps was stuck for good: the range only grew, so the capped endpoints
24333
+ * never rejoined and the quorum never formed.
24334
+ */
24335
+ this.span = MAX_BLOCK_RANGE;
24173
24336
  this.logger = loggers.get("chain-scanner");
24174
24337
  this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
24175
24338
  if (this.quorumClient.size > 1) {
@@ -24212,8 +24375,9 @@ var ChainScanner = class {
24212
24375
  if (this.timer || this.stopped) return;
24213
24376
  this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
24214
24377
  this.timer = setInterval(() => {
24215
- if (this.mutex.isLocked()) return;
24378
+ if (this.mutex.isLocked()) return this.reportStall();
24216
24379
  void this.mutex.runExclusive(async () => {
24380
+ this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
24217
24381
  try {
24218
24382
  await this.scan();
24219
24383
  } catch (error) {
@@ -24227,10 +24391,66 @@ var ChainScanner = class {
24227
24391
  "Error in block scanner"
24228
24392
  );
24229
24393
  this.errorHandler(error);
24394
+ } finally {
24395
+ this.notePassEnd();
24230
24396
  }
24231
24397
  });
24232
24398
  }, this.scanIntervalMs);
24233
24399
  }
24400
+ /**
24401
+ * Records which await the current pass is sitting on, for the watchdog.
24402
+ *
24403
+ * A no-op when no pass is registered — `scan()` is driven directly by tests
24404
+ * and by nothing else, so there is no watchdog to inform.
24405
+ */
24406
+ enter(phase) {
24407
+ if (this.pass) this.pass.phase = phase;
24408
+ }
24409
+ /**
24410
+ * Says that a pass has outlived the quorum budgets and names the phase it is
24411
+ * stuck in, then repeats every {@link STALL_REPORT_INTERVAL_MS} until it ends.
24412
+ *
24413
+ * Observation only: the pass is left alone. Aborting it would need a cancel
24414
+ * path through the quorum client that does not exist, and the cursor is safe
24415
+ * either way — a pass that never completes never advances it.
24416
+ */
24417
+ reportStall() {
24418
+ const pass = this.pass;
24419
+ if (!pass) return;
24420
+ const now = Date.now();
24421
+ const heldMs = now - pass.startedAt;
24422
+ if (heldMs < STALL_THRESHOLD_MS) return;
24423
+ if (pass.reportedAt !== 0 && now - pass.reportedAt < STALL_REPORT_INTERVAL_MS) return;
24424
+ pass.reportedAt = now;
24425
+ this.logger.warn(
24426
+ {
24427
+ chainId: this.target.chainId,
24428
+ phase: pass.phase,
24429
+ heldMs,
24430
+ cursor: this.cursor?.toString(),
24431
+ rpcUrls: this.target.rpcUrls
24432
+ },
24433
+ "Block scan pass has outlived its budget \u2014 this chain is not being scanned"
24434
+ );
24435
+ }
24436
+ /**
24437
+ * Closes out a pass, and says so if the watchdog complained about it — without
24438
+ * this the log shows a chain going quiet and never shows it coming back.
24439
+ */
24440
+ notePassEnd() {
24441
+ const pass = this.pass;
24442
+ this.pass = void 0;
24443
+ if (!pass || pass.reportedAt === 0) return;
24444
+ this.logger.warn(
24445
+ {
24446
+ chainId: this.target.chainId,
24447
+ phase: pass.phase,
24448
+ heldMs: Date.now() - pass.startedAt,
24449
+ cursor: this.cursor?.toString()
24450
+ },
24451
+ "Block scan pass finished after outliving its budget \u2014 scanning has resumed"
24452
+ );
24453
+ }
24234
24454
  /**
24235
24455
  * Points this loop at different endpoints, keeping the cursor.
24236
24456
  *
@@ -24272,11 +24492,12 @@ var ChainScanner = class {
24272
24492
  }
24273
24493
  if (currentBlock <= this.cursor) return;
24274
24494
  const fromBlock = this.cursor + 1n;
24275
- const toBlock = fromBlock + MAX_BLOCK_RANGE > currentBlock ? currentBlock : fromBlock + MAX_BLOCK_RANGE;
24495
+ const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
24276
24496
  this.logger.debug(
24277
24497
  { chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
24278
24498
  "Scanning blocks"
24279
24499
  );
24500
+ this.enter("logs");
24280
24501
  let logs;
24281
24502
  try {
24282
24503
  logs = await (0, import_sdk17.retryPromise)(
@@ -24298,6 +24519,7 @@ var ChainScanner = class {
24298
24519
  );
24299
24520
  } catch (error) {
24300
24521
  if (isBlockRangeError(error)) return;
24522
+ this.narrow(toBlock - fromBlock);
24301
24523
  throw error;
24302
24524
  }
24303
24525
  if (this.stopped) return;
@@ -24308,6 +24530,7 @@ var ChainScanner = class {
24308
24530
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
24309
24531
  "Found OrderPlaced events in block scan"
24310
24532
  );
24533
+ this.enter("publish-orders");
24311
24534
  await this.publishOrders(placed);
24312
24535
  }
24313
24536
  if (filled.length > 0) {
@@ -24315,10 +24538,36 @@ var ChainScanner = class {
24315
24538
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
24316
24539
  "Found OrderFilled events in block scan"
24317
24540
  );
24541
+ this.enter("publish-fills");
24318
24542
  await this.publishFills(filled);
24319
24543
  }
24320
24544
  if (this.stopped) return;
24321
24545
  this.cursor = toBlock;
24546
+ this.widen();
24547
+ }
24548
+ /**
24549
+ * Halves the span below the range that just failed, so the next pass asks
24550
+ * for something a range-capped endpoint will answer. Halving the attempted
24551
+ * range rather than the span matters near the head, where a pass reads fewer
24552
+ * blocks than the span allows.
24553
+ */
24554
+ narrow(attempted) {
24555
+ const next = attempted / 2n;
24556
+ if (next >= this.span) return;
24557
+ this.span = next;
24558
+ this.logger.warn(
24559
+ { chainId: this.target.chainId, span: Number(next) },
24560
+ "Narrowing the block scan range after a failed read"
24561
+ );
24562
+ }
24563
+ /** Doubles the span back toward {@link MAX_BLOCK_RANGE} after a good read. */
24564
+ widen() {
24565
+ if (this.span >= MAX_BLOCK_RANGE) return;
24566
+ const next = this.span === 0n ? 1n : this.span * 2n;
24567
+ this.span = next > MAX_BLOCK_RANGE ? MAX_BLOCK_RANGE : next;
24568
+ if (this.span === MAX_BLOCK_RANGE) {
24569
+ this.logger.info({ chainId: this.target.chainId }, "Block scan range back to full");
24570
+ }
24322
24571
  }
24323
24572
  async publishOrders(logs) {
24324
24573
  const rebuilt = reconstructOrdersFromLogs(logs, {
@@ -24685,7 +24934,7 @@ var LimitOrderController = class {
24685
24934
  ]);
24686
24935
  return { order, fills, bids };
24687
24936
  }
24688
- /** The pairs the orderbook lists, and the smallest payout each token may carry. */
24937
+ /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
24689
24938
  books() {
24690
24939
  return this.service.books();
24691
24940
  }