@hyperbridge/simplex 0.16.6 → 0.16.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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;
@@ -18563,20 +18630,32 @@ var ContractInteractionService = class {
18563
18630
  } else {
18564
18631
  this.logger.warn({ reason: pmResult.reason }, "No paymaster for bid UserOp; relying on EntryPoint deposit");
18565
18632
  }
18566
- const userOp = await sdkHelper.prepareSubmitBid({
18567
- order,
18568
- fillOptions,
18633
+ const bidGas = {
18569
18634
  solverAccount: solverAccountAddress,
18570
- solverSigner: sdkSigningAccount(this.signer),
18571
18635
  nonce,
18572
18636
  entryPointAddress,
18573
18637
  callGasLimit,
18574
18638
  verificationGasLimit: cachedEstimate.verificationGasLimit,
18575
- preVerificationGas: cachedEstimate.preVerificationGas,
18576
18639
  maxFeePerGas: cachedEstimate.maxFeePerGas,
18577
18640
  maxPriorityFeePerGas: cachedEstimate.maxPriorityFeePerGas,
18578
18641
  callData,
18579
18642
  paymasterAndData
18643
+ };
18644
+ let preVerificationGas = cachedEstimate.preVerificationGas;
18645
+ try {
18646
+ preVerificationGas = await sdkHelper.estimateBidPreVerificationGas(bidGas);
18647
+ } catch (err) {
18648
+ this.logger.warn(
18649
+ { orderId: order.id, err, preVerificationGas: preVerificationGas.toString() },
18650
+ "Could not estimate the bid's preVerificationGas; using the fill estimate's"
18651
+ );
18652
+ }
18653
+ const userOp = await sdkHelper.prepareSubmitBid({
18654
+ ...bidGas,
18655
+ order,
18656
+ fillOptions,
18657
+ solverSigner: sdkSigningAccount(this.signer),
18658
+ preVerificationGas
18580
18659
  });
18581
18660
  const encodedUserOp = encodeUserOpScale(userOp);
18582
18661
  this.logger.info(
@@ -18585,6 +18664,7 @@ var ContractInteractionService = class {
18585
18664
  solverAccount: solverAccountAddress,
18586
18665
  nonce: nonce.toString(),
18587
18666
  callGasLimit: callGasLimit.toString(),
18667
+ preVerificationGas: preVerificationGas.toString(),
18588
18668
  maxFeePerGas: cachedEstimate.maxFeePerGas.toString()
18589
18669
  },
18590
18670
  "Prepared bid UserOp"
@@ -21276,8 +21356,15 @@ var IntentFiller = class {
21276
21356
  const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
21277
21357
  this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
21278
21358
  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" });
21359
+ const priced = eligibleStrategies.some((s) => s !== null);
21360
+ this.logger.warn(
21361
+ { orderId: order.id },
21362
+ priced ? "No profitable strategy found for order" : "No strategy can fill this order"
21363
+ );
21364
+ this.monitor.emit("orderSkipped", {
21365
+ orderId: order.id,
21366
+ reason: priced ? "No profitable strategy" : "No strategy can fill this order"
21367
+ });
21281
21368
  return null;
21282
21369
  }
21283
21370
  this.logger.info(
@@ -21702,10 +21789,6 @@ import { Decimal as Decimal6 } from "decimal.js";
21702
21789
  function limitOrderLegs(order) {
21703
21790
  return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
21704
21791
  }
21705
- function availableOn(order) {
21706
- const available = BigInt(order.remaining) - BigInt(order.reserved);
21707
- return available > 0n ? available : 0n;
21708
- }
21709
21792
  function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21710
21793
  const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
21711
21794
  const offer = offerFor({
@@ -21714,7 +21797,7 @@ function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new D
21714
21797
  price: BigInt(order.price),
21715
21798
  outputDecimals: incoming.outputDecimals
21716
21799
  });
21717
- const available = availableOn(order);
21800
+ const available = BigInt(order.remaining);
21718
21801
  return { order, offer, available, payout: offer < available ? offer : available };
21719
21802
  }).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
21720
21803
  return qualifying;
@@ -21724,16 +21807,63 @@ function byBestOfferFirst(a, b) {
21724
21807
  if (a.available !== b.available) return a.available > b.available ? -1 : 1;
21725
21808
  return a.order.id < b.order.id ? -1 : 1;
21726
21809
  }
21810
+ function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21811
+ let furthest = -1;
21812
+ for (const order of orders) {
21813
+ const failed = firstFailed(order, incoming, resolve, now);
21814
+ furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
21815
+ }
21816
+ const { source, destination, inputSymbol } = incoming;
21817
+ if (furthest < CHECKS.length) {
21818
+ switch (CHECKS[Math.max(furthest, 0)]) {
21819
+ case "open":
21820
+ return "no limit order is open";
21821
+ case "chain":
21822
+ return `no open limit order fills on ${destination}`;
21823
+ case "input":
21824
+ return `no limit order on ${destination} takes ${inputSymbol} in`;
21825
+ case "output":
21826
+ return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
21827
+ case "source":
21828
+ return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
21829
+ }
21830
+ }
21831
+ const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
21832
+ order,
21833
+ offer: offerFor({
21834
+ side: order.side,
21835
+ inputAmount: incoming.inputNet,
21836
+ price: BigInt(order.price),
21837
+ outputDecimals: incoming.outputDecimals
21838
+ })
21839
+ }));
21840
+ if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
21841
+ return "the limit orders that meet its rate have nothing left to pay out";
21842
+ }
21843
+ const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
21844
+ const symbol = limitOrderLegs(best.order).output;
21845
+ return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
21846
+ }
21847
+ var CHECKS = ["open", "chain", "input", "output", "source"];
21727
21848
  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;
21849
+ return firstFailed(order, incoming, resolve, now) === null;
21850
+ }
21851
+ function firstFailed(order, incoming, resolve, now) {
21852
+ if (order.status !== "open") return "open";
21853
+ if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
21854
+ if (order.fillChain !== incoming.destination) return "chain";
21731
21855
  const legs = limitOrderLegs(order);
21732
- if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return false;
21856
+ if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
21733
21857
  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);
21858
+ if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
21859
+ if (incoming.source === incoming.destination) return null;
21860
+ return order.acceptedSources.includes(incoming.source) ? null : "source";
21861
+ }
21862
+ function readable(amount) {
21863
+ const [whole, fraction] = toHuman(amount).split(".");
21864
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
21865
+ const cut = fraction?.slice(0, 6).replace(/0+$/, "");
21866
+ return cut ? `${grouped}.${cut}` : grouped;
21737
21867
  }
21738
21868
 
21739
21869
  // src/orderbook/usd.ts
@@ -21875,15 +22005,20 @@ var FXFiller = class _FXFiller {
21875
22005
  }
21876
22006
  try {
21877
22007
  if (order.inputs.length !== order.output.assets.length) {
21878
- this.logger.debug(
21879
- { inputs: order.inputs.length, outputs: order.output.assets.length },
22008
+ this.logger.info(
22009
+ { orderId: order.id, inputs: order.inputs.length, outputs: order.output.assets.length },
21880
22010
  "Order input/output length mismatch or empty"
21881
22011
  );
21882
22012
  return false;
21883
22013
  }
21884
22014
  if ((await this.matchOrder(order)).length === 0) {
21885
- this.logger.debug(
21886
- { orderId: order.id, sourceChain: order.source, destChain: order.destination },
22015
+ this.logger.info(
22016
+ {
22017
+ orderId: order.id,
22018
+ sourceChain: order.source,
22019
+ destChain: order.destination,
22020
+ reason: await this.explainUnmatched(order)
22021
+ },
21887
22022
  "No limit order matches this order"
21888
22023
  );
21889
22024
  return false;
@@ -22425,30 +22560,53 @@ var FXFiller = class _FXFiller {
22425
22560
  */
22426
22561
  async matchLeg(order, leg) {
22427
22562
  if (!this.limitOrders) return [];
22563
+ const incoming = await this.incomingFor(order, leg);
22564
+ if (!incoming) return [];
22565
+ return matchLimitOrders(
22566
+ await this.limitOrders.open(),
22567
+ incoming,
22568
+ (symbol, chain) => this.registry.getAddress(symbol, chain)
22569
+ );
22570
+ }
22571
+ /** Why no limit order serves the order, leg by leg, for the line that says it was passed over. */
22572
+ async explainUnmatched(order) {
22573
+ if (!this.limitOrders) return "limit orders are not loaded";
22574
+ const open = await this.limitOrders.open();
22575
+ const reasons = [];
22576
+ for (let leg = 0; leg < order.inputs.length; leg++) {
22577
+ const incoming = await this.incomingFor(order, leg);
22578
+ 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}`;
22579
+ reasons.push(order.inputs.length > 1 ? `leg ${leg}: ${reason}` : reason);
22580
+ }
22581
+ return reasons.join("; ");
22582
+ }
22583
+ /**
22584
+ * One leg as the matcher sees it, or null when its input token is not one the asset
22585
+ * registry knows on the source chain.
22586
+ *
22587
+ * Amounts cross into the matcher at 1e18, the unit limit orders are kept in.
22588
+ */
22589
+ async incomingFor(order, leg) {
22428
22590
  const input = order.inputs[leg];
22429
22591
  const output = order.output.assets[leg];
22430
- if (!input || !output) return [];
22592
+ if (!input || !output) return null;
22431
22593
  const inputToken = bytes32ToBytes204(input.token);
22432
22594
  const outputToken = bytes32ToBytes204(output.token);
22433
22595
  const inputSymbol = this.registry.symbolFor(inputToken, order.source);
22434
- if (!inputSymbol) return [];
22596
+ if (!inputSymbol) return null;
22435
22597
  const [inputDecimals, outputDecimals] = await Promise.all([
22436
22598
  this.contractService.getTokenDecimals(inputToken, order.source),
22437
22599
  this.contractService.getTokenDecimals(outputToken, order.destination)
22438
22600
  ]);
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
- );
22601
+ return {
22602
+ source: order.source,
22603
+ destination: order.destination,
22604
+ inputSymbol,
22605
+ outputToken,
22606
+ inputNet: toScaled(input.amount, inputDecimals),
22607
+ requestedOutput: toScaled(output.amount, outputDecimals),
22608
+ outputDecimals
22609
+ };
22452
22610
  }
22453
22611
  /**
22454
22612
  * The order's input in **USD**, or null when no limit order connects its input
@@ -23526,48 +23684,42 @@ async function bootFiller(config, options) {
23526
23684
  assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
23527
23685
  }
23528
23686
  const adminStrategies = [];
23529
- const strategies = [];
23530
- let confirmationPolicy;
23531
- let tradingPairs;
23532
- let engine;
23533
- if (config.pairs?.length) {
23534
- tradingPairs = config.pairs.map((pair) => ({ token0: pair.token0, token1: pair.token1 }));
23535
- tradingPairs.forEach((pair, pairIndex) => {
23536
- adminStrategies.push({
23537
- index: pairIndex,
23538
- pairIndex,
23539
- exotic: `${pair.token0}/${pair.token1}`,
23540
- token0: pair.token0,
23541
- token1: pair.token1,
23542
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
23543
- });
23687
+ const tradingPairs = (config.pairs ?? []).map((pair) => ({
23688
+ token0: pair.token0,
23689
+ token1: pair.token1
23690
+ }));
23691
+ tradingPairs.forEach((pair, pairIndex) => {
23692
+ adminStrategies.push({
23693
+ index: pairIndex,
23694
+ pairIndex,
23695
+ exotic: `${pair.token0}/${pair.token1}`,
23696
+ token0: pair.token0,
23697
+ token1: pair.token1,
23698
+ sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
23544
23699
  });
23545
- confirmationPolicy = assertConfirmationCoverage(
23546
- config.confirmationPolicies,
23547
- resolvedChains.map((c) => c.chainId)
23548
- );
23549
- const fundingVenues = vaultVenue ? [vaultVenue] : [];
23550
- engine = new FXFiller(
23551
- runtimeSigner,
23552
- configService,
23553
- chainClientManager,
23554
- contractService,
23555
- tradingPairs,
23556
- assetRegistry,
23557
- { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
23558
- );
23559
- logger.info("Hydrating funding venue state...");
23560
- await engine.initialise();
23561
- strategies.push(engine);
23562
- }
23700
+ });
23701
+ const confirmationPolicy = assertConfirmationCoverage(
23702
+ config.confirmationPolicies,
23703
+ resolvedChains.map((c) => c.chainId)
23704
+ );
23705
+ const fundingVenues = vaultVenue ? [vaultVenue] : [];
23706
+ const engine = new FXFiller(
23707
+ runtimeSigner,
23708
+ configService,
23709
+ chainClientManager,
23710
+ contractService,
23711
+ tradingPairs,
23712
+ assetRegistry,
23713
+ { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
23714
+ );
23715
+ logger.info("Hydrating funding venue state...");
23716
+ await engine.initialise();
23717
+ const strategies = [engine];
23563
23718
  const haltControls = strategies.map((engine2, index) => ({
23564
23719
  index,
23565
23720
  isHalted: () => engine2.isHalted(),
23566
23721
  resetHalt: () => engine2.resetHalt()
23567
23722
  }));
23568
- if (vaultVenue) {
23569
- await vaultVenue.initialise(runtimeSigner.address);
23570
- }
23571
23723
  let rebalancingService;
23572
23724
  const rebalancingConfig = configService.getRebalancingConfig();
23573
23725
  if (rebalancingConfig?.triggerPercentage !== void 0 && rebalancingConfig?.baseBalances) {
@@ -24084,8 +24236,8 @@ var MemoryLimitOrderStore = class {
24084
24236
  async reserve(id, amount) {
24085
24237
  const order = this.orders.get(id);
24086
24238
  if (!order || order.status !== "open") return false;
24239
+ if (BigInt(amount) > BigInt(order.remaining)) return false;
24087
24240
  const reserved = BigInt(order.reserved) + BigInt(amount);
24088
- if (reserved > BigInt(order.remaining)) return false;
24089
24241
  this.patch(id, { reserved: reserved.toString() });
24090
24242
  return true;
24091
24243
  }
@@ -24161,6 +24313,8 @@ var MAX_BLOCK_RANGE = 1000n;
24161
24313
  var PUBLISH_CHUNK = 250;
24162
24314
  var yieldToConsumers = () => new Promise((resolve) => setImmediate(resolve));
24163
24315
  var STOP_DRAIN_TIMEOUT_MS = 5e3;
24316
+ var STALL_THRESHOLD_MS = 45e3;
24317
+ var STALL_REPORT_INTERVAL_MS = 6e4;
24164
24318
  var ChainScanner = class {
24165
24319
  constructor(target, loggers = defaultLoggerContext(), scanIntervalMs = DEFAULT_SCAN_INTERVAL_MS) {
24166
24320
  this.target = target;
@@ -24174,6 +24328,18 @@ var ChainScanner = class {
24174
24328
  this.errorHandler = () => {
24175
24329
  };
24176
24330
  this.stopped = false;
24331
+ /**
24332
+ * How far past the cursor the next pass reads, as `toBlock - fromBlock`:
24333
+ * halved after a failed read, doubled after a good one, between 0 (one block)
24334
+ * and {@link MAX_BLOCK_RANGE}.
24335
+ *
24336
+ * Free endpoints cap `eth_getLogs` ranges far below the full span — on
24337
+ * Arbitrum three of seven refused 100 blocks — and the cursor never moves
24338
+ * past a range it failed to read. At a fixed span a catch-up that outgrew
24339
+ * those caps was stuck for good: the range only grew, so the capped endpoints
24340
+ * never rejoined and the quorum never formed.
24341
+ */
24342
+ this.span = MAX_BLOCK_RANGE;
24177
24343
  this.logger = loggers.get("chain-scanner");
24178
24344
  this.quorumClient = new QuorumPublicClient(target.chainId, target.rpcUrls, loggers);
24179
24345
  if (this.quorumClient.size > 1) {
@@ -24216,8 +24382,9 @@ var ChainScanner = class {
24216
24382
  if (this.timer || this.stopped) return;
24217
24383
  this.logger.info({ chainId: this.target.chainId }, "Block scanner started");
24218
24384
  this.timer = setInterval(() => {
24219
- if (this.mutex.isLocked()) return;
24385
+ if (this.mutex.isLocked()) return this.reportStall();
24220
24386
  void this.mutex.runExclusive(async () => {
24387
+ this.pass = { phase: "head", startedAt: Date.now(), reportedAt: 0 };
24221
24388
  try {
24222
24389
  await this.scan();
24223
24390
  } catch (error) {
@@ -24231,10 +24398,66 @@ var ChainScanner = class {
24231
24398
  "Error in block scanner"
24232
24399
  );
24233
24400
  this.errorHandler(error);
24401
+ } finally {
24402
+ this.notePassEnd();
24234
24403
  }
24235
24404
  });
24236
24405
  }, this.scanIntervalMs);
24237
24406
  }
24407
+ /**
24408
+ * Records which await the current pass is sitting on, for the watchdog.
24409
+ *
24410
+ * A no-op when no pass is registered — `scan()` is driven directly by tests
24411
+ * and by nothing else, so there is no watchdog to inform.
24412
+ */
24413
+ enter(phase) {
24414
+ if (this.pass) this.pass.phase = phase;
24415
+ }
24416
+ /**
24417
+ * Says that a pass has outlived the quorum budgets and names the phase it is
24418
+ * stuck in, then repeats every {@link STALL_REPORT_INTERVAL_MS} until it ends.
24419
+ *
24420
+ * Observation only: the pass is left alone. Aborting it would need a cancel
24421
+ * path through the quorum client that does not exist, and the cursor is safe
24422
+ * either way — a pass that never completes never advances it.
24423
+ */
24424
+ reportStall() {
24425
+ const pass = this.pass;
24426
+ if (!pass) return;
24427
+ const now = Date.now();
24428
+ const heldMs = now - pass.startedAt;
24429
+ if (heldMs < STALL_THRESHOLD_MS) return;
24430
+ if (pass.reportedAt !== 0 && now - pass.reportedAt < STALL_REPORT_INTERVAL_MS) return;
24431
+ pass.reportedAt = now;
24432
+ this.logger.warn(
24433
+ {
24434
+ chainId: this.target.chainId,
24435
+ phase: pass.phase,
24436
+ heldMs,
24437
+ cursor: this.cursor?.toString(),
24438
+ rpcUrls: this.target.rpcUrls
24439
+ },
24440
+ "Block scan pass has outlived its budget \u2014 this chain is not being scanned"
24441
+ );
24442
+ }
24443
+ /**
24444
+ * Closes out a pass, and says so if the watchdog complained about it — without
24445
+ * this the log shows a chain going quiet and never shows it coming back.
24446
+ */
24447
+ notePassEnd() {
24448
+ const pass = this.pass;
24449
+ this.pass = void 0;
24450
+ if (!pass || pass.reportedAt === 0) return;
24451
+ this.logger.warn(
24452
+ {
24453
+ chainId: this.target.chainId,
24454
+ phase: pass.phase,
24455
+ heldMs: Date.now() - pass.startedAt,
24456
+ cursor: this.cursor?.toString()
24457
+ },
24458
+ "Block scan pass finished after outliving its budget \u2014 scanning has resumed"
24459
+ );
24460
+ }
24238
24461
  /**
24239
24462
  * Points this loop at different endpoints, keeping the cursor.
24240
24463
  *
@@ -24276,11 +24499,12 @@ var ChainScanner = class {
24276
24499
  }
24277
24500
  if (currentBlock <= this.cursor) return;
24278
24501
  const fromBlock = this.cursor + 1n;
24279
- const toBlock = fromBlock + MAX_BLOCK_RANGE > currentBlock ? currentBlock : fromBlock + MAX_BLOCK_RANGE;
24502
+ const toBlock = fromBlock + this.span > currentBlock ? currentBlock : fromBlock + this.span;
24280
24503
  this.logger.debug(
24281
24504
  { chainId: this.target.chainId, fromBlock, toBlock, gap: Number(toBlock - fromBlock) },
24282
24505
  "Scanning blocks"
24283
24506
  );
24507
+ this.enter("logs");
24284
24508
  let logs;
24285
24509
  try {
24286
24510
  logs = await retryPromise3(
@@ -24302,6 +24526,7 @@ var ChainScanner = class {
24302
24526
  );
24303
24527
  } catch (error) {
24304
24528
  if (isBlockRangeError(error)) return;
24529
+ this.narrow(toBlock - fromBlock);
24305
24530
  throw error;
24306
24531
  }
24307
24532
  if (this.stopped) return;
@@ -24312,6 +24537,7 @@ var ChainScanner = class {
24312
24537
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: placed.length },
24313
24538
  "Found OrderPlaced events in block scan"
24314
24539
  );
24540
+ this.enter("publish-orders");
24315
24541
  await this.publishOrders(placed);
24316
24542
  }
24317
24543
  if (filled.length > 0) {
@@ -24319,10 +24545,36 @@ var ChainScanner = class {
24319
24545
  { chainId: this.target.chainId, fromBlock, toBlock, eventCount: filled.length },
24320
24546
  "Found OrderFilled events in block scan"
24321
24547
  );
24548
+ this.enter("publish-fills");
24322
24549
  await this.publishFills(filled);
24323
24550
  }
24324
24551
  if (this.stopped) return;
24325
24552
  this.cursor = toBlock;
24553
+ this.widen();
24554
+ }
24555
+ /**
24556
+ * Halves the span below the range that just failed, so the next pass asks
24557
+ * for something a range-capped endpoint will answer. Halving the attempted
24558
+ * range rather than the span matters near the head, where a pass reads fewer
24559
+ * blocks than the span allows.
24560
+ */
24561
+ narrow(attempted) {
24562
+ const next = attempted / 2n;
24563
+ if (next >= this.span) return;
24564
+ this.span = next;
24565
+ this.logger.warn(
24566
+ { chainId: this.target.chainId, span: Number(next) },
24567
+ "Narrowing the block scan range after a failed read"
24568
+ );
24569
+ }
24570
+ /** Doubles the span back toward {@link MAX_BLOCK_RANGE} after a good read. */
24571
+ widen() {
24572
+ if (this.span >= MAX_BLOCK_RANGE) return;
24573
+ const next = this.span === 0n ? 1n : this.span * 2n;
24574
+ this.span = next > MAX_BLOCK_RANGE ? MAX_BLOCK_RANGE : next;
24575
+ if (this.span === MAX_BLOCK_RANGE) {
24576
+ this.logger.info({ chainId: this.target.chainId }, "Block scan range back to full");
24577
+ }
24326
24578
  }
24327
24579
  async publishOrders(logs) {
24328
24580
  const rebuilt = reconstructOrdersFromLogs(logs, {
@@ -24689,7 +24941,7 @@ var LimitOrderController = class {
24689
24941
  ]);
24690
24942
  return { order, fills, bids };
24691
24943
  }
24692
- /** The pairs the orderbook lists, and the smallest payout each token may carry. */
24944
+ /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
24693
24945
  books() {
24694
24946
  return this.service.books();
24695
24947
  }
@@ -24737,10 +24989,7 @@ var PairController = class {
24737
24989
  * the pair's symbols have to resolve before the market can be hydrated.
24738
24990
  */
24739
24991
  async add(pair, options) {
24740
- const { engine, tradingPairs, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
24741
- if (!engine || !tradingPairs) {
24742
- throw new Error("No trading engine is running \u2014 this filler was started without pairs");
24743
- }
24992
+ const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
24744
24993
  const assets = options?.assets;
24745
24994
  const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
24746
24995
  const nextPairs = [...this.pairs, pair];
@@ -24779,7 +25028,6 @@ var PairController = class {
24779
25028
  /** Closes a market. Orders already in flight against it still complete. */
24780
25029
  async remove(index) {
24781
25030
  const { engine, tradingPairs, adminStrategies, config } = this.runtime;
24782
- if (!engine || !tradingPairs) throw new Error("No trading engine is running");
24783
25031
  if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
24784
25032
  engine.removePair(tradingPairs[index]);
24785
25033
  config.pairs = this.pairs.filter((_, i) => i !== index);
@@ -24791,7 +25039,7 @@ var PairController = class {
24791
25039
  await this.persist();
24792
25040
  }
24793
25041
  livePair(index) {
24794
- const pair = this.runtime.tradingPairs?.[index];
25042
+ const pair = this.runtime.tradingPairs[index];
24795
25043
  if (!pair) throw new Error(`Unknown pair ${index}`);
24796
25044
  return pair;
24797
25045
  }
@@ -24883,9 +25131,9 @@ var ChainController = class {
24883
25131
  throw error;
24884
25132
  }
24885
25133
  const curve = confirmationPolicies[String(chainId)];
24886
- if (curve) this.runtime.confirmationPolicy?.add(chainId, curve);
24887
- else if (!this.runtime.confirmationPolicy?.has(chainId)) {
24888
- this.runtime.confirmationPolicy?.adopt(chainId, resolvedPolicy);
25134
+ if (curve) this.runtime.confirmationPolicy.add(chainId, curve);
25135
+ else if (!this.runtime.confirmationPolicy.has(chainId)) {
25136
+ this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
24889
25137
  }
24890
25138
  this.runtime.resolvedChains.push(resolved);
24891
25139
  config.chains = [...config.chains, { rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }];
@@ -24913,7 +25161,7 @@ var ChainController = class {
24913
25161
  await intentFiller.removeChain(chainId);
24914
25162
  if (this.ownsScanner) await this.scanner.removeChain(chainId);
24915
25163
  configService.removeChain(chainId);
24916
- this.runtime.confirmationPolicy?.remove(chainId);
25164
+ this.runtime.confirmationPolicy.remove(chainId);
24917
25165
  this.runtime.chainClientManager.invalidate(chainKey);
24918
25166
  this.runtime.resolvedChains.splice(index, 1);
24919
25167
  config.chains = config.chains.filter((_, i) => i !== index);