@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.js CHANGED
@@ -2402,6 +2402,7 @@ function closedKey(chain, vault) {
2402
2402
  // src/config/external-links.json
2403
2403
  var external_links_default = {
2404
2404
  hyperfxApp: "https://app.hyperfx.finance",
2405
+ alchemyDashboard: "https://dashboard.alchemy.com/",
2405
2406
  hyperbridgeExplorers: {
2406
2407
  mainnet: "https://nexus.statescan.io",
2407
2408
  testnet: "https://gargantua.statescan.io"
@@ -2421,6 +2422,52 @@ var external_links_default = {
2421
2422
  };
2422
2423
 
2423
2424
  // src/cli/init/chains.ts
2425
+ var FREE_RPC_URLS = {
2426
+ 1: [
2427
+ "https://mainnet.gateway.tenderly.co",
2428
+ "https://rpc.mevblocker.io",
2429
+ "https://ethereum-rpc.publicnode.com",
2430
+ "https://ethereum.public.blockpi.network/v1/rpc/public",
2431
+ "https://eth.blockrazor.xyz",
2432
+ "https://eth.api.pocket.network",
2433
+ "https://ethereum-public.nodies.app",
2434
+ "https://eth-mainnet.public.blastapi.io",
2435
+ "https://public-eth.nownodes.io"
2436
+ ],
2437
+ 56: [
2438
+ "https://bsc-rpc.publicnode.com",
2439
+ "https://public-bsc-mainnet.fastnode.io",
2440
+ "https://public-bsc.nownodes.io",
2441
+ "https://bsc.blockrazor.xyz",
2442
+ "https://rpc-bsc.48.club",
2443
+ "https://bsc.api.pocket.network"
2444
+ ],
2445
+ 137: [
2446
+ "https://polygon-bor-rpc.publicnode.com",
2447
+ "https://polygon.drpc.org",
2448
+ "https://polygon.gateway.tenderly.co",
2449
+ "https://rpc.private.mev-x.com/polygon",
2450
+ "https://poly.api.pocket.network"
2451
+ ],
2452
+ 8453: [
2453
+ "https://base-rpc.publicnode.com",
2454
+ "https://base.gateway.tenderly.co",
2455
+ "https://base.public.blockpi.network/v1/rpc/public",
2456
+ "https://base.rpc.blxrbdn.com",
2457
+ "https://rpc.baseazul.dev",
2458
+ "https://mainnet.base.org",
2459
+ "https://base-public.nodies.app"
2460
+ ],
2461
+ 42161: [
2462
+ "https://arbitrum-one-rpc.publicnode.com",
2463
+ "https://arbitrum.gateway.tenderly.co",
2464
+ "https://arbitrum.drpc.org",
2465
+ "https://public-arb-mainnet.fastnode.io",
2466
+ "https://arb-one.api.pocket.network",
2467
+ "https://arbitrum-one-public.nodies.app",
2468
+ "https://arb1.arbitrum.io/rpc"
2469
+ ]
2470
+ };
2424
2471
  var CHAIN_NATIVE_SYMBOLS = {
2425
2472
  1: "ETH",
2426
2473
  56: "BNB",
@@ -2448,7 +2495,8 @@ var INIT_CHAINS = [
2448
2495
  label: "Ethereum",
2449
2496
  network: "mainnet",
2450
2497
  alchemySubdomain: "eth-mainnet",
2451
- explorerUrl: external_links_default.chainExplorers.ethereum
2498
+ explorerUrl: external_links_default.chainExplorers.ethereum,
2499
+ defaultRpcUrls: FREE_RPC_URLS[1]
2452
2500
  },
2453
2501
  {
2454
2502
  chainId: 42161,
@@ -2456,7 +2504,8 @@ var INIT_CHAINS = [
2456
2504
  label: "Arbitrum",
2457
2505
  network: "mainnet",
2458
2506
  alchemySubdomain: "arb-mainnet",
2459
- explorerUrl: external_links_default.chainExplorers.arbitrum
2507
+ explorerUrl: external_links_default.chainExplorers.arbitrum,
2508
+ defaultRpcUrls: FREE_RPC_URLS[42161]
2460
2509
  },
2461
2510
  {
2462
2511
  chainId: 8453,
@@ -2464,7 +2513,8 @@ var INIT_CHAINS = [
2464
2513
  label: "Base",
2465
2514
  network: "mainnet",
2466
2515
  alchemySubdomain: "base-mainnet",
2467
- explorerUrl: external_links_default.chainExplorers.base
2516
+ explorerUrl: external_links_default.chainExplorers.base,
2517
+ defaultRpcUrls: FREE_RPC_URLS[8453]
2468
2518
  },
2469
2519
  {
2470
2520
  chainId: 137,
@@ -2472,7 +2522,8 @@ var INIT_CHAINS = [
2472
2522
  label: "Polygon",
2473
2523
  network: "mainnet",
2474
2524
  alchemySubdomain: "polygon-mainnet",
2475
- explorerUrl: external_links_default.chainExplorers.polygon
2525
+ explorerUrl: external_links_default.chainExplorers.polygon,
2526
+ defaultRpcUrls: FREE_RPC_URLS[137]
2476
2527
  },
2477
2528
  {
2478
2529
  chainId: 56,
@@ -2480,7 +2531,8 @@ var INIT_CHAINS = [
2480
2531
  label: "BNB Chain",
2481
2532
  network: "mainnet",
2482
2533
  alchemySubdomain: "bnb-mainnet",
2483
- explorerUrl: external_links_default.chainExplorers.bnb
2534
+ explorerUrl: external_links_default.chainExplorers.bnb,
2535
+ defaultRpcUrls: FREE_RPC_URLS[56]
2484
2536
  },
2485
2537
  {
2486
2538
  chainId: 11155111,
@@ -2968,14 +3020,16 @@ var LimitOrderService = class {
2968
3020
  }
2969
3021
  }
2970
3022
  /**
2971
- * The pairs an order can be written against, and the smallest payout each token may carry.
3023
+ * The pairs an order can be written against, the smallest payout each token may carry, and
3024
+ * the tokens the orderbook registers on each chain.
2972
3025
  *
2973
3026
  * The orderbook lists the books; `resolveBook` refuses a request naming anything else, so the
2974
- * operator is offered exactly these and no combination of symbols they could not post.
3027
+ * operator is offered exactly these and no combination of symbols they could not post. The
3028
+ * chains do the same for the source chains an order can accept.
2975
3029
  */
2976
3030
  async books() {
2977
3031
  const limits = await this.limits();
2978
- return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes };
3032
+ return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes, chains: limits.chains ?? [] };
2979
3033
  }
2980
3034
  /**
2981
3035
  * Reads `serverInfo` and `books`, reusing the last read for {@link LIMITS_TTL_MS}.
@@ -3057,6 +3111,18 @@ var LimitOrderService = class {
3057
3111
  );
3058
3112
  }
3059
3113
  }
3114
+ if (book.base !== book.quote) {
3115
+ const input = normalizeSymbol(request.tokenIn);
3116
+ const lacking = sources.filter((source) => {
3117
+ const listed = limits.chains?.find((chain) => chain.id === source);
3118
+ return listed && !listed.tokens.some((token) => normalizeSymbol(token.symbol) === input);
3119
+ });
3120
+ if (lacking.length > 0) {
3121
+ throw new LimitOrderValidationError(
3122
+ `The orderbook has no ${request.tokenIn} on ${lacking.join(", ")}, so swaps from there cannot pay this order`
3123
+ );
3124
+ }
3125
+ }
3060
3126
  if (ttlSecs < limits.serverInfo.minOrderTtlSecs) {
3061
3127
  throw new LimitOrderValidationError(
3062
3128
  `ttlSecs must be at least the orderbook's minimum of ${limits.serverInfo.minOrderTtlSecs}; got ${ttlSecs}`
@@ -12942,14 +13008,10 @@ var RATE_LIMIT_RPC_CODES = /* @__PURE__ */ new Set([-32005, -32097, -32016, 429]
12942
13008
  var RATE_LIMIT_SUSPENSION_MS = 5 * 6e4;
12943
13009
  var SCAN_BUDGET = {
12944
13010
  timeoutMs: 5e3,
12945
- retryCount: 1,
12946
- retryDelayMs: 500,
12947
13011
  deadlineMs: 12e3
12948
13012
  };
12949
13013
  var CONFIRMATION_BUDGET = {
12950
13014
  timeoutMs: 1e4,
12951
- retryCount: 1,
12952
- retryDelayMs: 500,
12953
13015
  deadlineMs: 3e4
12954
13016
  };
12955
13017
  var MALFORMED_RESPONSE_SUSPENSION_MS = 3e4;
@@ -13041,8 +13103,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13041
13103
  chain,
13042
13104
  transport: http(url, {
13043
13105
  timeout: budget.timeoutMs,
13044
- retryCount: budget.retryCount,
13045
- retryDelay: budget.retryDelayMs
13106
+ // No transport-level retry. viem's retry sleeps for the provider's
13107
+ // `Retry-After` verbatim and uncapped, which reaches the socket
13108
+ // before {@link suspensionMsFor} can apply its clamp — and even
13109
+ // without that header, retrying inside the task is what makes a
13110
+ // task's worst case (timeout, backoff, timeout) approach the
13111
+ // deadline that is supposed to bound it. One attempt per endpoint
13112
+ // per call; `retryPromise` above the quorum owns the retry policy.
13113
+ retryCount: 0
13046
13114
  })
13047
13115
  })
13048
13116
  );
@@ -13056,11 +13124,10 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13056
13124
  return this.rpcUrls.filter((url) => (_QuorumPublicClient.benchedUntil.get(url) ?? 0) > now);
13057
13125
  }
13058
13126
  /**
13059
- * The endpoints this call queries — everyone not benched — and the quorum
13060
- * bar for exactly that set. A benched endpoint is dropped outright: it
13061
- * answers nothing while throttled, so counting it could only fail calls the
13062
- * remaining endpoints agree on. With every endpoint benched, everyone is
13063
- * queried again; there is nobody left to prefer.
13127
+ * The endpoints this call queries: everyone not benched. A benched endpoint is
13128
+ * dropped outright: it answers nothing while throttled, so asking it could only
13129
+ * cost a request. With every endpoint benched, everyone is queried again; there
13130
+ * is nobody left to prefer.
13064
13131
  */
13065
13132
  participants() {
13066
13133
  const now = Date.now();
@@ -13068,12 +13135,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13068
13135
  const active = all.filter(({ idx }) => (_QuorumPublicClient.benchedUntil.get(this.rpcUrls[idx]) ?? 0) <= now);
13069
13136
  const allBenched = active.length === 0;
13070
13137
  const queried = allBenched ? all : active;
13071
- return {
13072
- queried,
13073
- skipped: all.length - queried.length,
13074
- threshold: quorumThreshold(queried.length),
13075
- allBenched
13076
- };
13138
+ return { queried, skipped: all.length - queried.length, allBenched };
13077
13139
  }
13078
13140
  /**
13079
13141
  * Records a task failure against its endpoint. Only the failures
@@ -13118,17 +13180,21 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13118
13180
  * retries would otherwise stall every confirmation poll). Stragglers settle
13119
13181
  * out of band and are ignored. `tryDecide` returns undefined to keep
13120
13182
  * waiting; once every task has settled, `finalize` must decide (it throws
13121
- * the QuorumError when the full response set still has no quorum). Early
13122
- * exit never weakens the trust model — a decision still requires the same
13123
- * quorum, it just doesn't wait for votes it no longer needs.
13183
+ * the QuorumError when the full response set still has no quorum).
13184
+ *
13185
+ * Both are handed the bar: the BFT threshold over the endpoints that have not
13186
+ * failed, answered or still out. A failure lowers it rather than counting
13187
+ * against it. Counting the pending endpoints keeps an early decision to a bar
13188
+ * no lower than the full response set would get, so a vote it did not wait for
13189
+ * could never have reversed it.
13124
13190
  *
13125
13191
  * `deadlineMs` bounds the other direction: when the bar is NOT reachable
13126
13192
  * from the fast responders, the call used to wait out every straggler, and
13127
13193
  * the caller's own retry loop multiplied that into minutes of a chain not
13128
13194
  * being scanned at all, silently. At the deadline the outstanding endpoints
13129
13195
  * are treated as failures — named in the QuorumError, so the operator sees
13130
- * which ones held the call up — and `finalize` decides on the rest. The
13131
- * trust model is untouched: a decision still needs the same quorum.
13196
+ * which ones held the call up — and `finalize` decides over the endpoints
13197
+ * that answered.
13132
13198
  */
13133
13199
  settleUntilQuorum(tasks, tryDecide, finalize) {
13134
13200
  return new Promise((resolve, reject) => {
@@ -13146,13 +13212,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13146
13212
  const evaluate = () => {
13147
13213
  if (done) return;
13148
13214
  try {
13149
- const early = tryDecide(fulfilled);
13215
+ const threshold = quorumThreshold(tasks.length - failures.length);
13216
+ const early = tryDecide(fulfilled, threshold);
13150
13217
  if (early !== void 0) {
13151
13218
  finish(() => resolve(early));
13152
13219
  return;
13153
13220
  }
13154
13221
  if (outstanding === 0) {
13155
- const value = finalize(fulfilled, failures);
13222
+ const value = finalize(fulfilled, failures, threshold);
13156
13223
  finish(() => resolve(value));
13157
13224
  }
13158
13225
  } catch (error) {
@@ -13170,7 +13237,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13170
13237
  error: new Error(`no answer within the ${this.deadlineMs}ms quorum deadline`)
13171
13238
  }));
13172
13239
  try {
13173
- const value = finalize(fulfilled, [...failures, ...stragglers]);
13240
+ const value = finalize(fulfilled, [...failures, ...stragglers], quorumThreshold(fulfilled.length));
13174
13241
  finish(() => resolve(value));
13175
13242
  } catch (error) {
13176
13243
  finish(() => reject(error));
@@ -13197,22 +13264,22 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13197
13264
  });
13198
13265
  }
13199
13266
  /**
13200
- * Highest block head backed by a full quorum. Throws {@link QuorumError}
13201
- * when fewer than `threshold` endpoints respond.
13267
+ * Highest block head backed by a quorum of the endpoints that answer. Throws
13268
+ * {@link QuorumError} when none do.
13202
13269
  */
13203
13270
  async getBlockNumber() {
13204
- const { queried, skipped, threshold, allBenched } = this.participants();
13271
+ const { queried, skipped, allBenched } = this.participants();
13205
13272
  return this.settleUntilQuorum(
13206
13273
  queried.map(({ idx, client }) => ({ idx, task: client.getBlockNumber() })),
13207
13274
  // Early exit as soon as a quorum is satisfiable. Late voters could
13208
13275
  // only raise the reported head; the earlier (lower) head is
13209
13276
  // conservative for every consumer (fewer confirmations counted,
13210
13277
  // smaller scan windows).
13211
- (fulfilled) => this.quorumHead(
13278
+ (fulfilled, threshold) => this.quorumHead(
13212
13279
  fulfilled.map((f) => f.value),
13213
13280
  threshold
13214
13281
  ) ?? void 0,
13215
- (fulfilled, failures) => {
13282
+ (fulfilled, failures, threshold) => {
13216
13283
  const head = this.quorumHead(
13217
13284
  fulfilled.map((f) => f.value),
13218
13285
  threshold
@@ -13253,7 +13320,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13253
13320
  }
13254
13321
  return views;
13255
13322
  };
13256
- const { queried, skipped, threshold, allBenched } = this.participants();
13323
+ const { queried, skipped, allBenched } = this.participants();
13257
13324
  return this.settleUntilQuorum(
13258
13325
  queried.map(({ idx, client }) => ({
13259
13326
  idx,
@@ -13272,8 +13339,8 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13272
13339
  // Early exit only on a POSITIVE quorum: an agreeing inclusion group can
13273
13340
  // only grow, so the first satisfiable quorum is final. Not-found votes
13274
13341
  // never decide early — "not confirmed" needs the full response set.
13275
- (fulfilled) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
13276
- (fulfilled, failures) => {
13342
+ (fulfilled, threshold) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
13343
+ (fulfilled, failures, threshold) => {
13277
13344
  const confirmations = aggregateConfirmations(toViews(fulfilled), threshold);
13278
13345
  if (confirmations === null) {
13279
13346
  throw new QuorumError(
@@ -13302,7 +13369,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13302
13369
  }
13303
13370
  return groups;
13304
13371
  };
13305
- const { queried, skipped, threshold, allBenched } = this.participants();
13372
+ const { queried, skipped, allBenched } = this.participants();
13306
13373
  return this.settleUntilQuorum(
13307
13374
  queried.map(({ idx, client }) => ({
13308
13375
  idx,
@@ -13314,13 +13381,13 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13314
13381
  // two disjoint groups can't both hold one), so the first satisfiable
13315
13382
  // agreement is final — no need to wait out stragglers that could
13316
13383
  // only join or lose.
13317
- (fulfilled) => {
13384
+ (fulfilled, threshold) => {
13318
13385
  for (const group of groupOf(fulfilled).values()) {
13319
13386
  if (group.providerIdxs.length >= threshold) return group.result;
13320
13387
  }
13321
13388
  return void 0;
13322
13389
  },
13323
- (fulfilled, failures) => {
13390
+ (fulfilled, failures, threshold) => {
13324
13391
  const groups = groupOf(fulfilled);
13325
13392
  for (const group of groups.values()) {
13326
13393
  if (group.providerIdxs.length >= threshold) return group.result;
@@ -21276,8 +21343,15 @@ var IntentFiller = class {
21276
21343
  const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
21277
21344
  this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
21278
21345
  if (validStrategies.length === 0) {
21279
- this.logger.warn({ orderId: order.id }, "No profitable strategy found for order");
21280
- this.monitor.emit("orderSkipped", { orderId: order.id, reason: "No profitable strategy" });
21346
+ const priced = eligibleStrategies.some((s) => s !== null);
21347
+ this.logger.warn(
21348
+ { orderId: order.id },
21349
+ priced ? "No profitable strategy found for order" : "No strategy can fill this order"
21350
+ );
21351
+ this.monitor.emit("orderSkipped", {
21352
+ orderId: order.id,
21353
+ reason: priced ? "No profitable strategy" : "No strategy can fill this order"
21354
+ });
21281
21355
  return null;
21282
21356
  }
21283
21357
  this.logger.info(
@@ -21724,16 +21798,63 @@ function byBestOfferFirst(a, b) {
21724
21798
  if (a.available !== b.available) return a.available > b.available ? -1 : 1;
21725
21799
  return a.order.id < b.order.id ? -1 : 1;
21726
21800
  }
21801
+ function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21802
+ let furthest = -1;
21803
+ for (const order of orders) {
21804
+ const failed = firstFailed(order, incoming, resolve, now);
21805
+ furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
21806
+ }
21807
+ const { source, destination, inputSymbol } = incoming;
21808
+ if (furthest < CHECKS.length) {
21809
+ switch (CHECKS[Math.max(furthest, 0)]) {
21810
+ case "open":
21811
+ return "no limit order is open";
21812
+ case "chain":
21813
+ return `no open limit order fills on ${destination}`;
21814
+ case "input":
21815
+ return `no limit order on ${destination} takes ${inputSymbol} in`;
21816
+ case "output":
21817
+ return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
21818
+ case "source":
21819
+ return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
21820
+ }
21821
+ }
21822
+ const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
21823
+ order,
21824
+ offer: offerFor({
21825
+ side: order.side,
21826
+ inputAmount: incoming.inputNet,
21827
+ price: BigInt(order.price),
21828
+ outputDecimals: incoming.outputDecimals
21829
+ })
21830
+ }));
21831
+ if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
21832
+ return "the limit orders that meet its rate have nothing left to pay out";
21833
+ }
21834
+ const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
21835
+ const symbol = limitOrderLegs(best.order).output;
21836
+ return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
21837
+ }
21838
+ var CHECKS = ["open", "chain", "input", "output", "source"];
21727
21839
  function serves(order, incoming, resolve, now) {
21728
- if (order.status !== "open") return false;
21729
- if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return false;
21730
- if (order.fillChain !== incoming.destination) return false;
21840
+ return firstFailed(order, incoming, resolve, now) === null;
21841
+ }
21842
+ function firstFailed(order, incoming, resolve, now) {
21843
+ if (order.status !== "open") return "open";
21844
+ if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
21845
+ if (order.fillChain !== incoming.destination) return "chain";
21731
21846
  const legs = limitOrderLegs(order);
21732
- if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return false;
21847
+ if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
21733
21848
  const outputToken = resolve(legs.output, order.fillChain);
21734
- if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return false;
21735
- if (incoming.source === incoming.destination) return true;
21736
- return order.acceptedSources.includes(incoming.source);
21849
+ if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
21850
+ if (incoming.source === incoming.destination) return null;
21851
+ return order.acceptedSources.includes(incoming.source) ? null : "source";
21852
+ }
21853
+ function readable(amount) {
21854
+ const [whole, fraction] = toHuman(amount).split(".");
21855
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
21856
+ const cut = fraction?.slice(0, 6).replace(/0+$/, "");
21857
+ return cut ? `${grouped}.${cut}` : grouped;
21737
21858
  }
21738
21859
 
21739
21860
  // src/orderbook/usd.ts
@@ -21875,15 +21996,20 @@ var FXFiller = class _FXFiller {
21875
21996
  }
21876
21997
  try {
21877
21998
  if (order.inputs.length !== order.output.assets.length) {
21878
- this.logger.debug(
21879
- { inputs: order.inputs.length, outputs: order.output.assets.length },
21999
+ this.logger.info(
22000
+ { orderId: order.id, inputs: order.inputs.length, outputs: order.output.assets.length },
21880
22001
  "Order input/output length mismatch or empty"
21881
22002
  );
21882
22003
  return false;
21883
22004
  }
21884
22005
  if ((await this.matchOrder(order)).length === 0) {
21885
- this.logger.debug(
21886
- { orderId: order.id, sourceChain: order.source, destChain: order.destination },
22006
+ this.logger.info(
22007
+ {
22008
+ orderId: order.id,
22009
+ sourceChain: order.source,
22010
+ destChain: order.destination,
22011
+ reason: await this.explainUnmatched(order)
22012
+ },
21887
22013
  "No limit order matches this order"
21888
22014
  );
21889
22015
  return false;
@@ -22425,30 +22551,53 @@ var FXFiller = class _FXFiller {
22425
22551
  */
22426
22552
  async matchLeg(order, leg) {
22427
22553
  if (!this.limitOrders) return [];
22554
+ const incoming = await this.incomingFor(order, leg);
22555
+ if (!incoming) return [];
22556
+ return matchLimitOrders(
22557
+ await this.limitOrders.open(),
22558
+ incoming,
22559
+ (symbol, chain) => this.registry.getAddress(symbol, chain)
22560
+ );
22561
+ }
22562
+ /** Why no limit order serves the order, leg by leg, for the line that says it was passed over. */
22563
+ async explainUnmatched(order) {
22564
+ if (!this.limitOrders) return "limit orders are not loaded";
22565
+ const open = await this.limitOrders.open();
22566
+ const reasons = [];
22567
+ for (let leg = 0; leg < order.inputs.length; leg++) {
22568
+ const incoming = await this.incomingFor(order, leg);
22569
+ 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}`;
22570
+ reasons.push(order.inputs.length > 1 ? `leg ${leg}: ${reason}` : reason);
22571
+ }
22572
+ return reasons.join("; ");
22573
+ }
22574
+ /**
22575
+ * One leg as the matcher sees it, or null when its input token is not one the asset
22576
+ * registry knows on the source chain.
22577
+ *
22578
+ * Amounts cross into the matcher at 1e18, the unit limit orders are kept in.
22579
+ */
22580
+ async incomingFor(order, leg) {
22428
22581
  const input = order.inputs[leg];
22429
22582
  const output = order.output.assets[leg];
22430
- if (!input || !output) return [];
22583
+ if (!input || !output) return null;
22431
22584
  const inputToken = bytes32ToBytes204(input.token);
22432
22585
  const outputToken = bytes32ToBytes204(output.token);
22433
22586
  const inputSymbol = this.registry.symbolFor(inputToken, order.source);
22434
- if (!inputSymbol) return [];
22587
+ if (!inputSymbol) return null;
22435
22588
  const [inputDecimals, outputDecimals] = await Promise.all([
22436
22589
  this.contractService.getTokenDecimals(inputToken, order.source),
22437
22590
  this.contractService.getTokenDecimals(outputToken, order.destination)
22438
22591
  ]);
22439
- return matchLimitOrders(
22440
- await this.limitOrders.open(),
22441
- {
22442
- source: order.source,
22443
- destination: order.destination,
22444
- inputSymbol,
22445
- outputToken,
22446
- inputNet: toScaled(input.amount, inputDecimals),
22447
- requestedOutput: toScaled(output.amount, outputDecimals),
22448
- outputDecimals
22449
- },
22450
- (symbol, chain) => this.registry.getAddress(symbol, chain)
22451
- );
22592
+ return {
22593
+ source: order.source,
22594
+ destination: order.destination,
22595
+ inputSymbol,
22596
+ outputToken,
22597
+ inputNet: toScaled(input.amount, inputDecimals),
22598
+ requestedOutput: toScaled(output.amount, outputDecimals),
22599
+ outputDecimals
22600
+ };
22452
22601
  }
22453
22602
  /**
22454
22603
  * The order's input in **USD**, or null when no limit order connects its input
@@ -24161,6 +24310,8 @@ var MAX_BLOCK_RANGE = 1000n;
24161
24310
  var PUBLISH_CHUNK = 250;
24162
24311
  var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
24163
24312
  var STOP_DRAIN_TIMEOUT_MS = 5e3;
24313
+ var STALL_THRESHOLD_MS = 45e3;
24314
+ var STALL_REPORT_INTERVAL_MS = 6e4;
24164
24315
  var ChainScanner = class {
24165
24316
  constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
24166
24317
  this.target = target;
@@ -24174,6 +24325,18 @@ var ChainScanner = class {
24174
24325
  this.errorHandler = () => {
24175
24326
  };
24176
24327
  this.stopped = false;
24328
+ /**
24329
+ * How far past the cursor the next pass reads, as `toBlock - fromBlock`:
24330
+ * halved after a failed read, doubled after a good one, between 0 (one block)
24331
+ * and {@link MAX_BLOCK_RANGE}.
24332
+ *
24333
+ * Free endpoints cap `eth_getLogs` ranges far below the full span — on
24334
+ * Arbitrum three of seven refused 100 blocks — and the cursor never moves
24335
+ * past a range it failed to read. At a fixed span a catch-up that outgrew
24336
+ * those caps was stuck for good: the range only grew, so the capped endpoints
24337
+ * never rejoined and the quorum never formed.
24338
+ */
24339
+ this.span = MAX_BLOCK_RANGE;
24177
24340
  this.logger = loggers.get("chain-scanner");
24178
24341
  this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
24179
24342
  if (this.quorumClient.size > 1) {
@@ -24216,8 +24379,9 @@ var ChainScanner = class {
24216
24379
  if (this.timer || this.stopped) return;
24217
24380
  this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
24218
24381
  this.timer = setInterval(() => {
24219
- if (this.mutex.isLocked()) return;
24382
+ if (this.mutex.isLocked()) return this.reportStall();
24220
24383
  void this.mutex.runExclusive(async () => {
24384
+ this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
24221
24385
  try {
24222
24386
  await this.scan();
24223
24387
  } catch (error) {
@@ -24231,10 +24395,66 @@ var ChainScanner = class {
24231
24395
  "Error in block scanner"
24232
24396
  );
24233
24397
  this.errorHandler(error);
24398
+ } finally {
24399
+ this.notePassEnd();
24234
24400
  }
24235
24401
  });
24236
24402
  }, this.scanIntervalMs);
24237
24403
  }
24404
+ /**
24405
+ * Records which await the current pass is sitting on, for the watchdog.
24406
+ *
24407
+ * A no-op when no pass is registered — `scan()` is driven directly by tests
24408
+ * and by nothing else, so there is no watchdog to inform.
24409
+ */
24410
+ enter(phase) {
24411
+ if (this.pass) this.pass.phase = phase;
24412
+ }
24413
+ /**
24414
+ * Says that a pass has outlived the quorum budgets and names the phase it is
24415
+ * stuck in, then repeats every {@link STALL_REPORT_INTERVAL_MS} until it ends.
24416
+ *
24417
+ * Observation only: the pass is left alone. Aborting it would need a cancel
24418
+ * path through the quorum client that does not exist, and the cursor is safe
24419
+ * either way — a pass that never completes never advances it.
24420
+ */
24421
+ reportStall() {
24422
+ const pass = this.pass;
24423
+ if (!pass) return;
24424
+ const now = Date.now();
24425
+ const heldMs = now - pass.startedAt;
24426
+ if (heldMs < STALL_THRESHOLD_MS) return;
24427
+ if (pass.reportedAt !== 0 && now - pass.reportedAt < STALL_REPORT_INTERVAL_MS) return;
24428
+ pass.reportedAt = now;
24429
+ this.logger.warn(
24430
+ {
24431
+ chainId: this.target.chainId,
24432
+ phase: pass.phase,
24433
+ heldMs,
24434
+ cursor: this.cursor?.toString(),
24435
+ rpcUrls: this.target.rpcUrls
24436
+ },
24437
+ "Block scan pass has outlived its budget \u2014 this chain is not being scanned"
24438
+ );
24439
+ }
24440
+ /**
24441
+ * Closes out a pass, and says so if the watchdog complained about it — without
24442
+ * this the log shows a chain going quiet and never shows it coming back.
24443
+ */
24444
+ notePassEnd() {
24445
+ const pass = this.pass;
24446
+ this.pass = void 0;
24447
+ if (!pass || pass.reportedAt === 0) return;
24448
+ this.logger.warn(
24449
+ {
24450
+ chainId: this.target.chainId,
24451
+ phase: pass.phase,
24452
+ heldMs: Date.now() - pass.startedAt,
24453
+ cursor: this.cursor?.toString()
24454
+ },
24455
+ "Block scan pass finished after outliving its budget \u2014 scanning has resumed"
24456
+ );
24457
+ }
24238
24458
  /**
24239
24459
  * Points this loop at different endpoints, keeping the cursor.
24240
24460
  *
@@ -24276,11 +24496,12 @@ var ChainScanner = class {
24276
24496
  }
24277
24497
  if (currentBlock <= this.cursor) return;
24278
24498
  const fromBlock = this.cursor + 1n;
24279
- const toBlock = fromBlock + MAX_BLOCK_RANGE > currentBlock ? currentBlock : fromBlock + MAX_BLOCK_RANGE;
24499
+ const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
24280
24500
  this.logger.debug(
24281
24501
  { chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
24282
24502
  "Scanning blocks"
24283
24503
  );
24504
+ this.enter("logs");
24284
24505
  let logs;
24285
24506
  try {
24286
24507
  logs = await retryPromise3(
@@ -24302,6 +24523,7 @@ var ChainScanner = class {
24302
24523
  );
24303
24524
  } catch (error) {
24304
24525
  if (isBlockRangeError(error)) return;
24526
+ this.narrow(toBlock - fromBlock);
24305
24527
  throw error;
24306
24528
  }
24307
24529
  if (this.stopped) return;
@@ -24312,6 +24534,7 @@ var ChainScanner = class {
24312
24534
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
24313
24535
  "Found OrderPlaced events in block scan"
24314
24536
  );
24537
+ this.enter("publish-orders");
24315
24538
  await this.publishOrders(placed);
24316
24539
  }
24317
24540
  if (filled.length > 0) {
@@ -24319,10 +24542,36 @@ var ChainScanner = class {
24319
24542
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
24320
24543
  "Found OrderFilled events in block scan"
24321
24544
  );
24545
+ this.enter("publish-fills");
24322
24546
  await this.publishFills(filled);
24323
24547
  }
24324
24548
  if (this.stopped) return;
24325
24549
  this.cursor = toBlock;
24550
+ this.widen();
24551
+ }
24552
+ /**
24553
+ * Halves the span below the range that just failed, so the next pass asks
24554
+ * for something a range-capped endpoint will answer. Halving the attempted
24555
+ * range rather than the span matters near the head, where a pass reads fewer
24556
+ * blocks than the span allows.
24557
+ */
24558
+ narrow(attempted) {
24559
+ const next = attempted / 2n;
24560
+ if (next >= this.span) return;
24561
+ this.span = next;
24562
+ this.logger.warn(
24563
+ { chainId: this.target.chainId, span: Number(next) },
24564
+ "Narrowing the block scan range after a failed read"
24565
+ );
24566
+ }
24567
+ /** Doubles the span back toward {@link MAX_BLOCK_RANGE} after a good read. */
24568
+ widen() {
24569
+ if (this.span >= MAX_BLOCK_RANGE) return;
24570
+ const next = this.span === 0n ? 1n : this.span * 2n;
24571
+ this.span = next > MAX_BLOCK_RANGE ? MAX_BLOCK_RANGE : next;
24572
+ if (this.span === MAX_BLOCK_RANGE) {
24573
+ this.logger.info({ chainId: this.target.chainId }, "Block scan range back to full");
24574
+ }
24326
24575
  }
24327
24576
  async publishOrders(logs) {
24328
24577
  const rebuilt = reconstructOrdersFromLogs(logs, {
@@ -24689,7 +24938,7 @@ var LimitOrderController = class {
24689
24938
  ]);
24690
24939
  return { order, fills, bids };
24691
24940
  }
24692
- /** The pairs the orderbook lists, and the smallest payout each token may carry. */
24941
+ /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
24693
24942
  books() {
24694
24943
  return this.service.books();
24695
24944
  }