From a26f5cfd375040c1a72e44d829881f84459cea94 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 4 Sep 2026 23:16:21 +0000 Subject: [PATCH] init --- config.env.yaml | 1 + config.example.yaml | 3 ++ src/cli/commands/sweep.ts | 1 + src/config/yaml.test.ts | 3 ++ src/config/yaml.ts | 7 +++++ src/core/modes/router/index.test.ts | 43 ++++++++++++++++++++++++++++- src/core/modes/router/index.ts | 7 +++-- 7 files changed, 62 insertions(+), 3 deletions(-) diff --git a/config.env.yaml b/config.env.yaml index 0ae075a3..28c2d3cc 100644 --- a/config.env.yaml +++ b/config.env.yaml @@ -38,6 +38,7 @@ botMinBalance: $BOT_MIN_BALANCE gasPriceMultiplier: $GAS_PRICE_MULTIPLIER txTimeThreshold: $TX_TIME_THRESHOLD blockTime: $BLOCK_TIME +routerPartialFallback: $ROUTER_PARTIAL_FALLBACK gasBoostProfitThreshold: $GAS_BOOST_PROFIT_THRESHOLD gasBoostMultiplier: $GAS_BOOST_MULTIPLIER gasBoostUsdThreshold: $GAS_BOOST_USD_THRESHOLD diff --git a/config.example.yaml b/config.example.yaml index ce73dead..90a5ca95 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -125,6 +125,9 @@ txTimeThreshold: 15000 # The average block time (in ms) of the operating chain, used as the polling interval of the block number watcher, default is 5000ms (5 seconds) blockTime: 2000 +# Enables the halving backoff retries for router mode partial trades that get rejected onchain, default is true +routerPartialFallback: true + # Time (in minutes) to to check the operating wallet balances, 0 means dont ever check wallet balance, default is 15 mins checkWalletBalanceTime: 15 diff --git a/src/cli/commands/sweep.ts b/src/cli/commands/sweep.ts index 00637397..e4eb1860 100644 --- a/src/cli/commands/sweep.ts +++ b/src/cli/commands/sweep.ts @@ -107,6 +107,7 @@ export async function sweepFunds(opts: SweepOptions) { gasPriceMultiplier: 107, txTimeThreshold: 2_500, blockTime: 5_000, + routerPartialFallback: true, timeout: 15_000, // unused fields but need to be defined diff --git a/src/config/yaml.test.ts b/src/config/yaml.test.ts index b312a841..1a0bc0e1 100644 --- a/src/config/yaml.test.ts +++ b/src/config/yaml.test.ts @@ -39,6 +39,7 @@ botMinBalance: 50.5 gasPriceMultiplier: 150 txTimeThreshold: 4000 blockTime: 3000 +routerPartialFallback: false checkWalletBalanceTime: 30 gasBoostProfitThreshold: 7 gasBoostMultiplier: 3.5 @@ -166,6 +167,7 @@ orderbookTradeTypes: sweepWalletTime: 0, convertToGasTime: 0, rotateMultiWallet: false, + routerPartialFallback: false, checkWalletBalanceTime: 30, gasBoostProfitThreshold: 7, gasBoostMultiplier: 3.5, @@ -358,6 +360,7 @@ orderbookTradeTypes: assert.equal(result.sweepWalletTime, 10); assert.equal(result.convertToGasTime, 2); assert.equal(result.rotateMultiWallet, true); + assert.equal(result.routerPartialFallback, true); // should be default true assert.equal(result.checkWalletBalanceTime, 15); // should be default 15 assert.equal(result.wsRpc, undefined); // no ws rpc when unset assert.equal(result.gasBoostProfitThreshold, undefined); // no boost when unset diff --git a/src/config/yaml.ts b/src/config/yaml.ts index 80e033cb..6f4f6353 100644 --- a/src/config/yaml.ts +++ b/src/config/yaml.ts @@ -123,6 +123,8 @@ export type AppOptions = { txTimeThreshold: number; /** The average block time (in ms) of the operating chain, used as the polling interval of the block number watcher, default is 5000 ms */ blockTime: number; + /** Enables the halving backoff retries for router mode partial trades that get rejected onchain, default is true */ + routerPartialFallback: boolean; /** Time (in minutes) to to check the operating wallet balances, 0 means dont ever check wallet balance, default is 15 mins */ checkWalletBalanceTime: number; /** Optional threshold as the min expected bounty multiple that the estimated profit must exceed to boost the tx gas price, no boost applies if unset */ @@ -379,6 +381,11 @@ export namespace AppOptions { "invalid blockTime value, must be an integer greater than 0", ), ), + routerPartialFallback: Validator.resolveBool( + input.routerPartialFallback, + "expected a boolean value for routerPartialFallback", + true, + ), checkWalletBalanceTime: Validator.resolveNumericValue( input.checkWalletBalanceTime, INT_PATTERN, diff --git a/src/core/modes/router/index.test.ts b/src/core/modes/router/index.test.ts index 28167997..2e26456c 100644 --- a/src/core/modes/router/index.test.ts +++ b/src/core/modes/router/index.test.ts @@ -52,7 +52,7 @@ describe("Test findBestRouterTrade", () => { }; destination = "0xdestination"; mockRainSolver = { - appOptions: {}, + appOptions: { routerPartialFallback: true }, state: { gasPrice: 100n, client: { @@ -560,6 +560,47 @@ describe("Test findBestRouterTrade", () => { expect(result.error.spanAttributes["partialFallback2.error"]).toBeUndefined(); }); + it("should skip backoff when routerPartialFallback is disabled in config", async () => { + mockRainSolver.appOptions.routerPartialFallback = false; + const mockFullTradeError = Result.err({ + type: TradeType.RouteProcessor, + reason: SimulationHaltReason.OrderRatioGreaterThanMarketPrice, + spanAttributes: { error: "ratio too high" }, + noneNodeError: "order ratio issue", + }); + const mockViolationError = Result.err({ + type: TradeType.RouteProcessor, + reason: SimulationHaltReason.NoOpportunity, + spanAttributes: { + error: "execution reverted: MinimalOutputBalanceViolation(0xtoken, 123)", + }, + }); + + (trySimulateTradeSpy as Mock) + .mockResolvedValueOnce(mockFullTradeError) // full size + .mockResolvedValue(mockViolationError); // partial size + (mockRainSolver.state.router.findLargestTradeSize as Mock).mockReturnValue(1000n); + + const result: SimulationResult = await findBestRouterTrade.call( + mockRainSolver, + orderDetails, + signer, + ethPrice, + toToken, + fromToken, + blockNumber, + ); + + assert(result.isErr()); + // only 1 full + 1 partial, no fallback attempts despite the violation error + expect(trySimulateTradeSpy).toHaveBeenCalledTimes(2); + expect(result.error.reason).toBe(SimulationHaltReason.NoOpportunity); + expect(result.error.spanAttributes["partial.error"]).toContain( + "MinimalOutputBalanceViolation", + ); + expect(result.error.spanAttributes["partialFallback1.error"]).toBeUndefined(); + }); + it("should retry with the failing route dexes excluded when full trade dryrun fails", async () => { const sushiQuote = { route: { diff --git a/src/core/modes/router/index.ts b/src/core/modes/router/index.ts index 967f74b3..b03b562b 100644 --- a/src/core/modes/router/index.ts +++ b/src/core/modes/router/index.ts @@ -233,9 +233,12 @@ export async function tryFindBestRouterTrade( // it means the offchain pool data overestimated the output for the found partial trade // size, so backoff by halving the trade size at each step validated against onchain // dryrun and accept the first size that passes, the backoff stops early if a step fails - // with any other error + // with any other error, the backoff only runs when enabled by config const reason = partialTradeSizeSimResult.error.reason; - if (SimulationHaltReason.needsRetry(partialTradeSizeSimResult.error.spanAttributes["error"])) { + if ( + this.appOptions.routerPartialFallback && + SimulationHaltReason.needsRetry(partialTradeSizeSimResult.error.spanAttributes["error"]) + ) { let fallbackTradeSize = partialTradeSize; for (let i = 1; i <= 4; i++) { fallbackTradeSize /= 2n;