@hyperbridge/simplex 0.16.5 → 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,
@@ -2654,10 +2706,16 @@ var CANCEL_ORDER_MUTATION = `
2654
2706
  `;
2655
2707
  var OrderbookRequestError = class extends Error {
2656
2708
  };
2709
+ function graphqlEndpoint(url) {
2710
+ const trimmed = url.trim();
2711
+ if (trimmed.includes("?")) return trimmed;
2712
+ const withoutTrailingSlash = trimmed.replace(/\/+$/, "");
2713
+ return withoutTrailingSlash.endsWith("/graphql") ? withoutTrailingSlash : `${withoutTrailingSlash}/graphql`;
2714
+ }
2657
2715
  var OrderbookClient = class {
2658
2716
  constructor(url, requestTimeoutMs, loggers = defaultLoggerContext()) {
2659
- this.url = url;
2660
2717
  this.requestTimeoutMs = requestTimeoutMs;
2718
+ this.url = graphqlEndpoint(url);
2661
2719
  this.logger = loggers.get("orderbook");
2662
2720
  }
2663
2721
  /** The limits to validate against before posting, plus the books on offer. */
@@ -2755,7 +2813,9 @@ var OrderbookClient = class {
2755
2813
  clearTimeout(timer);
2756
2814
  }
2757
2815
  if (!response.ok) {
2758
- throw new OrderbookRequestError(`Orderbook returned HTTP ${response.status} ${response.statusText}`);
2816
+ throw new OrderbookRequestError(
2817
+ `Orderbook at ${this.url} returned HTTP ${response.status} ${response.statusText}`
2818
+ );
2759
2819
  }
2760
2820
  const body = await response.json().catch((err) => {
2761
2821
  throw new OrderbookRequestError(`Orderbook returned a body that is not JSON: ${describe(err)}`);
@@ -2870,7 +2930,7 @@ var HEARTBEAT_TYPES = {
2870
2930
  var LimitOrderValidationError = class extends Error {
2871
2931
  };
2872
2932
  var LimitOrderService = class {
2873
- constructor(store, client, contractService, configService, assetRegistry, signer, defaultTtlSecs, delegationService, loggers = defaultLoggerContext(), startingNonce = initialOrderNonce) {
2933
+ constructor(store, client, contractService, configService, assetRegistry, signer, defaultTtlSecs, delegationService, loggers = defaultLoggerContext(), startingNonce = initialOrderNonce, vaultBalances) {
2874
2934
  this.store = store;
2875
2935
  this.client = client;
2876
2936
  this.contractService = contractService;
@@ -2880,6 +2940,7 @@ var LimitOrderService = class {
2880
2940
  this.defaultTtlSecs = defaultTtlSecs;
2881
2941
  this.delegationService = delegationService;
2882
2942
  this.startingNonce = startingNonce;
2943
+ this.vaultBalances = vaultBalances;
2883
2944
  this.logger = loggers.get("limit-orders");
2884
2945
  }
2885
2946
  /**
@@ -3015,6 +3076,18 @@ var LimitOrderService = class {
3015
3076
  throw err;
3016
3077
  }
3017
3078
  }
3079
+ /**
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.
3082
+ *
3083
+ * The orderbook lists the books; `resolveBook` refuses a request naming anything else, so the
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.
3086
+ */
3087
+ async books() {
3088
+ const limits = await this.limits();
3089
+ return { books: limits.books, minOrderSizes: limits.serverInfo.minOrderSizes, chains: limits.chains ?? [] };
3090
+ }
3018
3091
  /**
3019
3092
  * Reads `serverInfo` and `books`, reusing the last read for {@link LIMITS_TTL_MS}.
3020
3093
  *
@@ -3095,6 +3168,18 @@ var LimitOrderService = class {
3095
3168
  );
3096
3169
  }
3097
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
+ }
3098
3183
  if (ttlSecs < limits.serverInfo.minOrderTtlSecs) {
3099
3184
  throw new LimitOrderValidationError(
3100
3185
  `ttlSecs must be at least the orderbook's minimum of ${limits.serverInfo.minOrderTtlSecs}; got ${ttlSecs}`
@@ -3122,28 +3207,61 @@ var LimitOrderService = class {
3122
3207
  * balance on the fill chain, so the operator hears it while they are creating
3123
3208
  * the order.
3124
3209
  *
3210
+ * A fill pays out of the wallet and then withdraws any shortfall from the
3211
+ * configured ERC-4626 vaults in the same batch, so what those vaults hold of
3212
+ * the payout token backs the order too and is counted here. Without it an
3213
+ * operator who sweeps inventory into a vault is refused orders their filler
3214
+ * would have filled.
3215
+ *
3125
3216
  * Each order is checked against the whole balance, not against what is left of
3126
3217
  * it after the others. One balance backs every order resting on it — that is
3127
3218
  * what quoting both sides of a book is — and the orderbook says as much: it
3128
3219
  * advertises each entry at `min(quoted, balance)` rather than dividing the
3129
3220
  * balance between them. Whichever order fills first draws the inventory down,
3130
3221
  * and the rest are cut to what is left. Netting them here would refuse the
3131
- * second side of every book.
3222
+ * second side of every book. Vault positions are counted the same way: the
3223
+ * whole position, not what is left after reservations for fills in flight.
3132
3224
  */
3133
3225
  async assertWalletCanPay(symbol, chain, payout) {
3134
3226
  const token = this.assetRegistry.getAddress(symbol, chain);
3135
3227
  if (!token) return;
3136
3228
  const decimals = await this.decimalsFor(symbol, token, chain);
3137
- const balance = toScaled(
3229
+ const wallet = toScaled(
3138
3230
  await this.contractService.getTokenBalance(chain, token, this.signer.address),
3139
3231
  decimals
3140
3232
  );
3141
- if (balance < payout) {
3233
+ const vaults = await this.vaultHoldings(token, chain);
3234
+ if (wallet + vaults < payout) {
3235
+ const held = vaults > 0n ? `${toHuman(wallet)} in the wallet and ${toHuman(vaults)} in vaults` : toHuman(wallet);
3142
3236
  throw new LimitOrderValidationError(
3143
- `The wallet holds ${toHuman(balance)} ${symbol} on ${chain}, which cannot pay out ${toHuman(payout)}`
3237
+ `The wallet holds ${held} ${symbol} on ${chain}, which cannot pay out ${toHuman(payout)}`
3144
3238
  );
3145
3239
  }
3146
3240
  }
3241
+ /**
3242
+ * What the configured vaults hold of `token` on `chain`, in scaled units.
3243
+ *
3244
+ * Zero when no vault is configured. A snapshot that cannot be read is also
3245
+ * zero: the wallet alone then has to cover the order, which refuses one the
3246
+ * filler might have managed rather than accepting one it could not.
3247
+ */
3248
+ async vaultHoldings(token, chain) {
3249
+ if (!this.vaultBalances) return 0n;
3250
+ let positions;
3251
+ try {
3252
+ positions = await this.vaultBalances.getBalanceSnapshot(chain);
3253
+ } catch (err) {
3254
+ this.logger.warn({ chain, token, err }, "Could not read vault balances; backing the order on the wallet alone");
3255
+ return 0n;
3256
+ }
3257
+ const wanted = token.toLowerCase();
3258
+ let total = 0n;
3259
+ for (const position of positions) {
3260
+ if (position.chain !== chain || position.asset.toLowerCase() !== wanted) continue;
3261
+ total += toScaled(position.positionAssets, position.decimals);
3262
+ }
3263
+ return total;
3264
+ }
3147
3265
  /**
3148
3266
  * Works a limit order down by what a fill delivered, and puts the rest back on
3149
3267
  * the orderbook.
@@ -12932,14 +13050,10 @@ var RATE_LIMIT_RPC_CODES = /* @__PURE__ */ new Set([-32005, -32097, -32016, 429]
12932
13050
  var RATE_LIMIT_SUSPENSION_MS = 5 * 6e4;
12933
13051
  var SCAN_BUDGET = {
12934
13052
  timeoutMs: 5e3,
12935
- retryCount: 1,
12936
- retryDelayMs: 500,
12937
13053
  deadlineMs: 12e3
12938
13054
  };
12939
13055
  var CONFIRMATION_BUDGET = {
12940
13056
  timeoutMs: 1e4,
12941
- retryCount: 1,
12942
- retryDelayMs: 500,
12943
13057
  deadlineMs: 3e4
12944
13058
  };
12945
13059
  var MALFORMED_RESPONSE_SUSPENSION_MS = 3e4;
@@ -13031,8 +13145,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13031
13145
  chain,
13032
13146
  transport: (0, import_viem14.http)(url, {
13033
13147
  timeout: budget.timeoutMs,
13034
- retryCount: budget.retryCount,
13035
- 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
13036
13156
  })
13037
13157
  })
13038
13158
  );
@@ -13046,11 +13166,10 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13046
13166
  return this.rpcUrls.filter((url) => (_QuorumPublicClient.benchedUntil.get(url) ?? 0) > now);
13047
13167
  }
13048
13168
  /**
13049
- * The endpoints this call queries — everyone not benched — and the quorum
13050
- * bar for exactly that set. A benched endpoint is dropped outright: it
13051
- * answers nothing while throttled, so counting it could only fail calls the
13052
- * remaining endpoints agree on. With every endpoint benched, everyone is
13053
- * 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.
13054
13173
  */
13055
13174
  participants() {
13056
13175
  const now = Date.now();
@@ -13058,12 +13177,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13058
13177
  const active = all.filter(({ idx }) => (_QuorumPublicClient.benchedUntil.get(this.rpcUrls[idx]) ?? 0) <= now);
13059
13178
  const allBenched = active.length === 0;
13060
13179
  const queried = allBenched ? all : active;
13061
- return {
13062
- queried,
13063
- skipped: all.length - queried.length,
13064
- threshold: quorumThreshold(queried.length),
13065
- allBenched
13066
- };
13180
+ return { queried, skipped: all.length - queried.length, allBenched };
13067
13181
  }
13068
13182
  /**
13069
13183
  * Records a task failure against its endpoint. Only the failures
@@ -13108,17 +13222,21 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13108
13222
  * retries would otherwise stall every confirmation poll). Stragglers settle
13109
13223
  * out of band and are ignored. `tryDecide` returns undefined to keep
13110
13224
  * waiting; once every task has settled, `finalize` must decide (it throws
13111
- * the QuorumError when the full response set still has no quorum). Early
13112
- * exit never weakens the trust model — a decision still requires the same
13113
- * 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.
13114
13232
  *
13115
13233
  * `deadlineMs` bounds the other direction: when the bar is NOT reachable
13116
13234
  * from the fast responders, the call used to wait out every straggler, and
13117
13235
  * the caller's own retry loop multiplied that into minutes of a chain not
13118
13236
  * being scanned at all, silently. At the deadline the outstanding endpoints
13119
13237
  * are treated as failures — named in the QuorumError, so the operator sees
13120
- * which ones held the call up — and `finalize` decides on the rest. The
13121
- * 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.
13122
13240
  */
13123
13241
  settleUntilQuorum(tasks, tryDecide, finalize) {
13124
13242
  return new Promise((resolve, reject) => {
@@ -13136,13 +13254,14 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13136
13254
  const evaluate = () => {
13137
13255
  if (done) return;
13138
13256
  try {
13139
- const early = tryDecide(fulfilled);
13257
+ const threshold = quorumThreshold(tasks.length - failures.length);
13258
+ const early = tryDecide(fulfilled, threshold);
13140
13259
  if (early !== void 0) {
13141
13260
  finish(() => resolve(early));
13142
13261
  return;
13143
13262
  }
13144
13263
  if (outstanding === 0) {
13145
- const value = finalize(fulfilled, failures);
13264
+ const value = finalize(fulfilled, failures, threshold);
13146
13265
  finish(() => resolve(value));
13147
13266
  }
13148
13267
  } catch (error) {
@@ -13160,7 +13279,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13160
13279
  error: new Error(`no answer within the ${this.deadlineMs}ms quorum deadline`)
13161
13280
  }));
13162
13281
  try {
13163
- const value = finalize(fulfilled, [...failures, ...stragglers]);
13282
+ const value = finalize(fulfilled, [...failures, ...stragglers], quorumThreshold(fulfilled.length));
13164
13283
  finish(() => resolve(value));
13165
13284
  } catch (error) {
13166
13285
  finish(() => reject(error));
@@ -13187,22 +13306,22 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13187
13306
  });
13188
13307
  }
13189
13308
  /**
13190
- * Highest block head backed by a full quorum. Throws {@link QuorumError}
13191
- * 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.
13192
13311
  */
13193
13312
  async getBlockNumber() {
13194
- const { queried, skipped, threshold, allBenched } = this.participants();
13313
+ const { queried, skipped, allBenched } = this.participants();
13195
13314
  return this.settleUntilQuorum(
13196
13315
  queried.map(({ idx, client }) => ({ idx, task: client.getBlockNumber() })),
13197
13316
  // Early exit as soon as a quorum is satisfiable. Late voters could
13198
13317
  // only raise the reported head; the earlier (lower) head is
13199
13318
  // conservative for every consumer (fewer confirmations counted,
13200
13319
  // smaller scan windows).
13201
- (fulfilled) => this.quorumHead(
13320
+ (fulfilled, threshold) => this.quorumHead(
13202
13321
  fulfilled.map((f) => f.value),
13203
13322
  threshold
13204
13323
  ) ?? void 0,
13205
- (fulfilled, failures) => {
13324
+ (fulfilled, failures, threshold) => {
13206
13325
  const head = this.quorumHead(
13207
13326
  fulfilled.map((f) => f.value),
13208
13327
  threshold
@@ -13243,7 +13362,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13243
13362
  }
13244
13363
  return views;
13245
13364
  };
13246
- const { queried, skipped, threshold, allBenched } = this.participants();
13365
+ const { queried, skipped, allBenched } = this.participants();
13247
13366
  return this.settleUntilQuorum(
13248
13367
  queried.map(({ idx, client }) => ({
13249
13368
  idx,
@@ -13262,8 +13381,8 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13262
13381
  // Early exit only on a POSITIVE quorum: an agreeing inclusion group can
13263
13382
  // only grow, so the first satisfiable quorum is final. Not-found votes
13264
13383
  // never decide early — "not confirmed" needs the full response set.
13265
- (fulfilled) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
13266
- (fulfilled, failures) => {
13384
+ (fulfilled, threshold) => aggregateConfirmations(toViews(fulfilled), threshold) ?? void 0,
13385
+ (fulfilled, failures, threshold) => {
13267
13386
  const confirmations = aggregateConfirmations(toViews(fulfilled), threshold);
13268
13387
  if (confirmations === null) {
13269
13388
  throw new QuorumError(
@@ -13292,7 +13411,7 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13292
13411
  }
13293
13412
  return groups;
13294
13413
  };
13295
- const { queried, skipped, threshold, allBenched } = this.participants();
13414
+ const { queried, skipped, allBenched } = this.participants();
13296
13415
  return this.settleUntilQuorum(
13297
13416
  queried.map(({ idx, client }) => ({
13298
13417
  idx,
@@ -13304,13 +13423,13 @@ var _QuorumPublicClient = class _QuorumPublicClient {
13304
13423
  // two disjoint groups can't both hold one), so the first satisfiable
13305
13424
  // agreement is final — no need to wait out stragglers that could
13306
13425
  // only join or lose.
13307
- (fulfilled) => {
13426
+ (fulfilled, threshold) => {
13308
13427
  for (const group of groupOf(fulfilled).values()) {
13309
13428
  if (group.providerIdxs.length >= threshold) return group.result;
13310
13429
  }
13311
13430
  return void 0;
13312
13431
  },
13313
- (fulfilled, failures) => {
13432
+ (fulfilled, failures, threshold) => {
13314
13433
  const groups = groupOf(fulfilled);
13315
13434
  for (const group of groups.values()) {
13316
13435
  if (group.providerIdxs.length >= threshold) return group.result;
@@ -21223,8 +21342,15 @@ var IntentFiller = class {
21223
21342
  const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
21224
21343
  this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
21225
21344
  if (validStrategies.length === 0) {
21226
- this.logger.warn({ orderId: order.id }, "No profitable strategy found for order");
21227
- 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
+ });
21228
21354
  return null;
21229
21355
  }
21230
21356
  this.logger.info(
@@ -21668,16 +21794,63 @@ function byBestOfferFirst(a, b) {
21668
21794
  if (a.available !== b.available) return a.available > b.available ? -1 : 1;
21669
21795
  return a.order.id < b.order.id ? -1 : 1;
21670
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"];
21671
21835
  function serves(order, incoming, resolve, now) {
21672
- if (order.status !== "open") return false;
21673
- if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return false;
21674
- 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";
21675
21842
  const legs = limitOrderLegs(order);
21676
- if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return false;
21843
+ if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
21677
21844
  const outputToken = resolve(legs.output, order.fillChain);
21678
- if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return false;
21679
- if (incoming.source === incoming.destination) return true;
21680
- 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;
21681
21854
  }
21682
21855
 
21683
21856
  // src/orderbook/usd.ts
@@ -21819,15 +21992,20 @@ var FXFiller = class _FXFiller {
21819
21992
  }
21820
21993
  try {
21821
21994
  if (order.inputs.length !== order.output.assets.length) {
21822
- this.logger.debug(
21823
- { 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 },
21824
21997
  "Order input/output length mismatch or empty"
21825
21998
  );
21826
21999
  return false;
21827
22000
  }
21828
22001
  if ((await this.matchOrder(order)).length === 0) {
21829
- this.logger.debug(
21830
- { 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
+ },
21831
22009
  "No limit order matches this order"
21832
22010
  );
21833
22011
  return false;
@@ -22369,30 +22547,53 @@ var FXFiller = class _FXFiller {
22369
22547
  */
22370
22548
  async matchLeg(order, leg) {
22371
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) {
22372
22577
  const input = order.inputs[leg];
22373
22578
  const output = order.output.assets[leg];
22374
- if (!input || !output) return [];
22579
+ if (!input || !output) return null;
22375
22580
  const inputToken = (0, import_sdk15.bytes32ToBytes20)(input.token);
22376
22581
  const outputToken = (0, import_sdk15.bytes32ToBytes20)(output.token);
22377
22582
  const inputSymbol = this.registry.symbolFor(inputToken, order.source);
22378
- if (!inputSymbol) return [];
22583
+ if (!inputSymbol) return null;
22379
22584
  const [inputDecimals, outputDecimals] = await Promise.all([
22380
22585
  this.contractService.getTokenDecimals(inputToken, order.source),
22381
22586
  this.contractService.getTokenDecimals(outputToken, order.destination)
22382
22587
  ]);
22383
- return matchLimitOrders(
22384
- await this.limitOrders.open(),
22385
- {
22386
- source: order.source,
22387
- destination: order.destination,
22388
- inputSymbol,
22389
- outputToken,
22390
- inputNet: toScaled(input.amount, inputDecimals),
22391
- requestedOutput: toScaled(output.amount, outputDecimals),
22392
- outputDecimals
22393
- },
22394
- (symbol, chain) => this.registry.getAddress(symbol, chain)
22395
- );
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
+ };
22396
22597
  }
22397
22598
  /**
22398
22599
  * The order's input in **USD**, or null when no limit order connects its input
@@ -23559,7 +23760,10 @@ async function bootFiller(config, options) {
23559
23760
  runtimeSigner,
23560
23761
  config.orderbook.defaultTtlSecs ?? DEFAULT_LIMIT_ORDER_TTL_SECONDS,
23561
23762
  new DelegationService(chainClientManager, configService, runtimeSigner),
23562
- options.loggers
23763
+ options.loggers,
23764
+ initialOrderNonce,
23765
+ // A fill withdraws a payout shortfall from the vaults, so what they hold backs an order too.
23766
+ vaultVenue
23563
23767
  );
23564
23768
  intentFiller.setLimitOrderService(limitOrderService);
23565
23769
  const lifecycle = new LimitOrderLifecycle(
@@ -24102,6 +24306,8 @@ var MAX_BLOCK_RANGE = 1000n;
24102
24306
  var PUBLISH_CHUNK = 250;
24103
24307
  var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
24104
24308
  var STOP_DRAIN_TIMEOUT_MS = 5e3;
24309
+ var STALL_THRESHOLD_MS = 45e3;
24310
+ var STALL_REPORT_INTERVAL_MS = 6e4;
24105
24311
  var ChainScanner = class {
24106
24312
  constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
24107
24313
  this.target = target;
@@ -24115,6 +24321,18 @@ var ChainScanner = class {
24115
24321
  this.errorHandler = () => {
24116
24322
  };
24117
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;
24118
24336
  this.logger = loggers.get("chain-scanner");
24119
24337
  this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
24120
24338
  if (this.quorumClient.size > 1) {
@@ -24157,8 +24375,9 @@ var ChainScanner = class {
24157
24375
  if (this.timer || this.stopped) return;
24158
24376
  this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
24159
24377
  this.timer = setInterval(() => {
24160
- if (this.mutex.isLocked()) return;
24378
+ if (this.mutex.isLocked()) return this.reportStall();
24161
24379
  void this.mutex.runExclusive(async () => {
24380
+ this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
24162
24381
  try {
24163
24382
  await this.scan();
24164
24383
  } catch (error) {
@@ -24172,10 +24391,66 @@ var ChainScanner = class {
24172
24391
  "Error in block scanner"
24173
24392
  );
24174
24393
  this.errorHandler(error);
24394
+ } finally {
24395
+ this.notePassEnd();
24175
24396
  }
24176
24397
  });
24177
24398
  }, this.scanIntervalMs);
24178
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
+ }
24179
24454
  /**
24180
24455
  * Points this loop at different endpoints, keeping the cursor.
24181
24456
  *
@@ -24217,11 +24492,12 @@ var ChainScanner = class {
24217
24492
  }
24218
24493
  if (currentBlock <= this.cursor) return;
24219
24494
  const fromBlock = this.cursor + 1n;
24220
- const toBlock = fromBlock + MAX_BLOCK_RANGE > currentBlock ? currentBlock : fromBlock + MAX_BLOCK_RANGE;
24495
+ const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
24221
24496
  this.logger.debug(
24222
24497
  { chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
24223
24498
  "Scanning blocks"
24224
24499
  );
24500
+ this.enter("logs");
24225
24501
  let logs;
24226
24502
  try {
24227
24503
  logs = await (0, import_sdk17.retryPromise)(
@@ -24243,6 +24519,7 @@ var ChainScanner = class {
24243
24519
  );
24244
24520
  } catch (error) {
24245
24521
  if (isBlockRangeError(error)) return;
24522
+ this.narrow(toBlock - fromBlock);
24246
24523
  throw error;
24247
24524
  }
24248
24525
  if (this.stopped) return;
@@ -24253,6 +24530,7 @@ var ChainScanner = class {
24253
24530
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
24254
24531
  "Found OrderPlaced events in block scan"
24255
24532
  );
24533
+ this.enter("publish-orders");
24256
24534
  await this.publishOrders(placed);
24257
24535
  }
24258
24536
  if (filled.length > 0) {
@@ -24260,10 +24538,36 @@ var ChainScanner = class {
24260
24538
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
24261
24539
  "Found OrderFilled events in block scan"
24262
24540
  );
24541
+ this.enter("publish-fills");
24263
24542
  await this.publishFills(filled);
24264
24543
  }
24265
24544
  if (this.stopped) return;
24266
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
+ }
24267
24571
  }
24268
24572
  async publishOrders(logs) {
24269
24573
  const rebuilt = reconstructOrdersFromLogs(logs, {
@@ -24630,6 +24934,10 @@ var LimitOrderController = class {
24630
24934
  ]);
24631
24935
  return { order, fills, bids };
24632
24936
  }
24937
+ /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
24938
+ books() {
24939
+ return this.service.books();
24940
+ }
24633
24941
  /** Creates the order, posts it, and reports what the orderbook made of it. */
24634
24942
  async create(request) {
24635
24943
  const posted = await this.service.create(request);