@hyperbridge/simplex 0.16.11 → 0.16.12

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
@@ -1165,6 +1165,7 @@ var import_viem2 = require("viem");
1165
1165
  var import_sdk2 = require("@hyperbridge/sdk");
1166
1166
  var DEFAULT_BLOCK_SCAN_INTERVAL_SECONDS = 3;
1167
1167
  var MIN_BLOCK_SCAN_INTERVAL_SECONDS = 0.1;
1168
+ var DEFAULT_MIN_ORDER_SIZE_USD = 20;
1168
1169
  function validateRpcUrls(rpcUrls) {
1169
1170
  if (rpcUrls.length === 0) {
1170
1171
  throw new Error("rpcUrls must contain at least one URL");
@@ -1587,6 +1588,13 @@ var FillerConfigService = class {
1587
1588
  getBidValiditySeconds() {
1588
1589
  return this.fillerConfig?.bidValiditySeconds ?? 300;
1589
1590
  }
1591
+ /**
1592
+ * The order size, in USD, below which `order.fees` must cover the execution cost.
1593
+ * Defaults to 20. An order at or above it is filled whatever fees it carries.
1594
+ */
1595
+ getMinOrderSizeUsd() {
1596
+ return this.fillerConfig?.minOrderSizeUsd ?? DEFAULT_MIN_ORDER_SIZE_USD;
1597
+ }
1590
1598
  /** Ceiling bps above user-requested output. Default 500 (5%). */
1591
1599
  getMaxOverfillBps() {
1592
1600
  return BigInt(this.fillerConfig?.overfillProtection?.maxOverfillBps ?? 500);
@@ -1690,6 +1698,12 @@ function validateConfig(config, cliWatchOnly = false) {
1690
1698
  );
1691
1699
  }
1692
1700
  }
1701
+ const minOrderSize = config.simplex.minOrderSizeUsd;
1702
+ if (minOrderSize !== void 0) {
1703
+ if (!Number.isFinite(minOrderSize) || minOrderSize < 0) {
1704
+ throw new Error(`simplex.minOrderSizeUsd must be a number >= 0 (USD); got ${minOrderSize}`);
1705
+ }
1706
+ }
1693
1707
  if (config.allowlist) {
1694
1708
  for (const user of config.allowlist.users ?? []) {
1695
1709
  if (!(0, import_viem2.isAddress)(user)) {
@@ -12861,6 +12875,7 @@ var CacheService = class {
12861
12875
  matchedLimitOrders: {},
12862
12876
  bidPlans: {},
12863
12877
  partialFills: {},
12878
+ feeChecksWaived: {},
12864
12879
  feeTokens: {},
12865
12880
  tokenDecimals: {},
12866
12881
  solverSelection: {}
@@ -12900,6 +12915,10 @@ var CacheService = class {
12900
12915
  stalePartialIds.forEach((orderId) => {
12901
12916
  delete this.cacheData.partialFills[orderId];
12902
12917
  });
12918
+ const staleWaivedIds = Object.entries(this.cacheData.feeChecksWaived).filter(([_, data]) => !this.isCacheValid(data.timestamp)).map(([orderId]) => orderId);
12919
+ staleWaivedIds.forEach((orderId) => {
12920
+ delete this.cacheData.feeChecksWaived[orderId];
12921
+ });
12903
12922
  }
12904
12923
  getGasEstimate(orderId) {
12905
12924
  try {
@@ -13212,6 +13231,24 @@ var CacheService = class {
13212
13231
  clearPartialFill(orderId) {
13213
13232
  delete this.cacheData.partialFills[orderId];
13214
13233
  }
13234
+ /**
13235
+ * Whether the strategy's completed evaluation let a bid through without holding
13236
+ * `order.fees` to the execution cost, because the order is at or above the
13237
+ * operator's minimum size.
13238
+ *
13239
+ * Written and cleared the way {@link isPartialFill} is, for the same reason: the
13240
+ * caller exempts such an order from its profit floor.
13241
+ */
13242
+ isFeeCheckWaived(orderId) {
13243
+ const cache = this.cacheData.feeChecksWaived[orderId];
13244
+ return cache !== void 0 && this.isCacheValid(cache.timestamp) && cache.waived;
13245
+ }
13246
+ setFeeCheckWaived(orderId, waived) {
13247
+ this.cacheData.feeChecksWaived[orderId] = { waived, timestamp: Date.now() };
13248
+ }
13249
+ clearFeeCheckWaived(orderId) {
13250
+ delete this.cacheData.feeChecksWaived[orderId];
13251
+ }
13215
13252
  getFeeTokenWithDecimals(chain) {
13216
13253
  try {
13217
13254
  const cache = this.cacheData.feeTokens[chain];
@@ -18501,7 +18538,10 @@ var ContractInteractionService = class {
18501
18538
  order,
18502
18539
  prependCalls: void 0,
18503
18540
  maxPriorityFeePerGasBumpPercent: gasFeeBumpConfig?.maxPriorityFeePerGasBumpPercent,
18504
- maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent
18541
+ maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent,
18542
+ // No bid without the bundler's own figures: an op signed with the SDK's
18543
+ // fixed fallback limits does not execute, so a failed estimate skips the order.
18544
+ requireBundlerEstimate: true
18505
18545
  });
18506
18546
  this.logger.info({ orderId: order.id }, "Caching gas estimate");
18507
18547
  this.logger.info({ estimate }, "Estimate");
@@ -18524,7 +18564,7 @@ var ContractInteractionService = class {
18524
18564
  callGasLimit: estimate.callGasLimit
18525
18565
  };
18526
18566
  } catch (error) {
18527
- this.logger.error({ err: error }, "Error estimating gas, using generous fallback values");
18567
+ this.logger.error({ err: error, orderId: order.id }, "Error estimating gas, not bidding on this order");
18528
18568
  throw new Error(`Failed to estimate gas: ${error instanceof Error ? error.message : "Unknown error"}`);
18529
18569
  }
18530
18570
  }
@@ -21679,6 +21719,23 @@ var IntentFiller = class {
21679
21719
  this.monitor.emit("orderSkipped", { orderId: order.id, reason: "watch-only" });
21680
21720
  return null;
21681
21721
  }
21722
+ const destinationHead = await this.destinationHead(order);
21723
+ if (destinationHead !== null && order.deadline <= destinationHead) {
21724
+ this.logger.info(
21725
+ {
21726
+ orderId: order.id,
21727
+ destChain: order.destination,
21728
+ deadline: order.deadline.toString(),
21729
+ head: destinationHead.toString()
21730
+ },
21731
+ "Skipping order: its deadline has passed on the destination chain"
21732
+ );
21733
+ this.monitor.emit("orderSkipped", {
21734
+ orderId: order.id,
21735
+ reason: "Order deadline has passed on the destination chain"
21736
+ });
21737
+ return null;
21738
+ }
21682
21739
  const evalStartMs = Date.now();
21683
21740
  const eligibleStrategies = await Promise.all(
21684
21741
  this.strategies.map(async (strategy) => {
@@ -21688,7 +21745,10 @@ var IntentFiller = class {
21688
21745
  })
21689
21746
  );
21690
21747
  const fillsPartially = this.fillsPartially(order);
21691
- const validStrategies = eligibleStrategies.filter((s) => s !== null && (s.profitability > 0 || fillsPartially)).sort((a, b) => b.profitability - a.profitability);
21748
+ const feeCheckWaived = this.feeCheckWaived(order);
21749
+ const validStrategies = eligibleStrategies.filter(
21750
+ (s) => s !== null && (s.profitability > 0 || fillsPartially || feeCheckWaived)
21751
+ ).sort((a, b) => b.profitability - a.profitability);
21692
21752
  const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
21693
21753
  this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
21694
21754
  if (validStrategies.length === 0) {
@@ -21713,6 +21773,23 @@ var IntentFiller = class {
21713
21773
  );
21714
21774
  return validStrategies[0];
21715
21775
  }
21776
+ /**
21777
+ * The destination chain's latest block number, the clock `order.deadline` is read on.
21778
+ *
21779
+ * Null when the read fails. That is not an answer about the order, so evaluation goes
21780
+ * on: the gas estimate simulates the same deadline check and refuses an expired order.
21781
+ */
21782
+ async destinationHead(order) {
21783
+ try {
21784
+ return await this.chainClientManager.getPublicClient(order.destination).getBlockNumber();
21785
+ } catch (err) {
21786
+ this.logger.warn(
21787
+ { orderId: order.id, destChain: order.destination, err },
21788
+ "Could not read the destination head to check the order's deadline"
21789
+ );
21790
+ return null;
21791
+ }
21792
+ }
21716
21793
  /**
21717
21794
  * Whether the strategy's evaluation concluded in a deliberate partial fill.
21718
21795
  *
@@ -21726,6 +21803,15 @@ var IntentFiller = class {
21726
21803
  if (!order.id) return false;
21727
21804
  return this.contractService.cacheService.isPartialFill(order.id);
21728
21805
  }
21806
+ /**
21807
+ * Whether the strategy's evaluation let a bid through without the fee check, because
21808
+ * the order is at or above the operator's minimum size. Read from a flag the strategy
21809
+ * sets only once it has an answer, as {@link fillsPartially} is.
21810
+ */
21811
+ feeCheckWaived(order) {
21812
+ if (!order.id) return false;
21813
+ return this.contractService.cacheService.isFeeCheckWaived(order.id);
21814
+ }
21729
21815
  executeOrder(order, bestStrategy, solverSelectionActive, inputUsdValue, profitUsd) {
21730
21816
  const chainQueue = this.chainQueues.get((0, import_sdk14.getChainId)(order.destination));
21731
21817
  if (!chainQueue) {
@@ -22297,11 +22383,13 @@ var FXFiller = class _FXFiller {
22297
22383
  * - Treat a payout below the ask as a partial fill, which only orders
22298
22384
  * without output calldata and without an earlier partial fill allow.
22299
22385
  * - Cap each payout by the filler's balance plus funding-venue withdrawals,
22300
- * then gate it on fees (full fills) and the same-asset spread.
22386
+ * then gate it on fees (full fills of orders below `minOrderSizeUsd`) and the
22387
+ * same-asset spread.
22301
22388
  * - Cache the resulting bids for later use in `executeOrder`.
22302
22389
  */
22303
22390
  async calculateProfitability(order) {
22304
22391
  if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
22392
+ if (order.id) this.contractService.cacheService.clearFeeCheckWaived(order.id);
22305
22393
  if (this.halted) {
22306
22394
  this.logger.warn({ orderId: order.id }, "FXFiller halted \u2014 rejecting order");
22307
22395
  return 0;
@@ -22317,6 +22405,15 @@ var FXFiller = class _FXFiller {
22317
22405
  const plans = [];
22318
22406
  const plannedOn = /* @__PURE__ */ new Map();
22319
22407
  const spentOn = /* @__PURE__ */ new Map();
22408
+ let atOrAboveMinSize;
22409
+ const feeCheckWaived = async () => {
22410
+ if (atOrAboveMinSize === void 0) {
22411
+ const usd = await this.getOrderUsdValue(order);
22412
+ atOrAboveMinSize = usd !== null && usd.inputUsd.gte(this.configService.getMinOrderSizeUsd());
22413
+ }
22414
+ return atOrAboveMinSize;
22415
+ };
22416
+ let feesWaived = false;
22320
22417
  for (let leg = 0; leg < order.inputs.length; leg++) {
22321
22418
  const input = order.inputs[leg];
22322
22419
  const output = order.output.assets[leg];
@@ -22560,17 +22657,21 @@ var FXFiller = class _FXFiller {
22560
22657
  const executionCost = totalCostInSourceFeeToken + relayerFeeInSourceFeeToken;
22561
22658
  const partialEdgeUsd = sameAssetEdgeUsd.plus(payoutSurplusUsd);
22562
22659
  if (!partialFill && order.fees < executionCost) {
22563
- this.logger.info(
22564
- {
22565
- orderId: order.id,
22566
- orderFees: (0, import_viem29.formatUnits)(order.fees, feeTokenDecimals),
22567
- fillGas: (0, import_viem29.formatUnits)(totalCostInSourceFeeToken, feeTokenDecimals),
22568
- relayerFee: (0, import_viem29.formatUnits)(relayerFeeInSourceFeeToken, feeTokenDecimals),
22569
- executionCost: (0, import_viem29.formatUnits)(executionCost, feeTokenDecimals)
22570
- },
22571
- "Skipping order: attached fees do not cover execution cost (fill gas + relayer fee)"
22572
- );
22573
- continue;
22660
+ if (!await feeCheckWaived()) {
22661
+ this.logger.info(
22662
+ {
22663
+ orderId: order.id,
22664
+ orderFees: (0, import_viem29.formatUnits)(order.fees, feeTokenDecimals),
22665
+ fillGas: (0, import_viem29.formatUnits)(totalCostInSourceFeeToken, feeTokenDecimals),
22666
+ relayerFee: (0, import_viem29.formatUnits)(relayerFeeInSourceFeeToken, feeTokenDecimals),
22667
+ executionCost: (0, import_viem29.formatUnits)(executionCost, feeTokenDecimals),
22668
+ minOrderSizeUsd: this.configService.getMinOrderSizeUsd()
22669
+ },
22670
+ "Skipping order: attached fees do not cover execution cost (fill gas + relayer fee)"
22671
+ );
22672
+ continue;
22673
+ }
22674
+ feesWaived = true;
22574
22675
  }
22575
22676
  if (sameAsset && !sameAssetProfitable) {
22576
22677
  this.logger.info(
@@ -22604,7 +22705,8 @@ var FXFiller = class _FXFiller {
22604
22705
  realizedSpreadProfit: (0, import_viem29.formatUnits)(realizedSpreadProfit, feeTokenDecimals),
22605
22706
  payoutSurplusUsd: payoutSurplusUsd.toString(),
22606
22707
  totalProfit,
22607
- profitable: totalProfit > 0
22708
+ profitable: totalProfit > 0,
22709
+ feeCheckWaived: !partialFill && order.fees < executionCost
22608
22710
  },
22609
22711
  "FX swap profitability evaluation"
22610
22712
  );
@@ -22636,6 +22738,7 @@ var FXFiller = class _FXFiller {
22636
22738
  plans[0].budget
22637
22739
  );
22638
22740
  this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
22741
+ this.contractService.cacheService.setFeeCheckWaived(order.id, feesWaived);
22639
22742
  this.contractService.cacheService.setMatchedLimitOrder(order.id, [
22640
22743
  { limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
22641
22744
  ]);
@@ -23877,6 +23980,7 @@ async function bootFiller(config, options) {
23877
23980
  rebalancing: config.rebalancing,
23878
23981
  targetGasUnits: config.simplex.targetGasUnits,
23879
23982
  bidValiditySeconds: config.simplex.bidValiditySeconds,
23983
+ minOrderSizeUsd: config.simplex.minOrderSizeUsd,
23880
23984
  blockScanIntervalSeconds: config.simplex.blockScanIntervalSeconds,
23881
23985
  gasFeeBump: config.simplex.gasFeeBump,
23882
23986
  overfillProtection: config.simplex.overfillProtection,