diff --git a/config.env.yaml b/config.env.yaml index a5966911..f9901df1 100644 --- a/config.env.yaml +++ b/config.env.yaml @@ -35,6 +35,8 @@ txGas: $TX_GAS quoteGas: $QUOTE_GAS botMinBalance: $BOT_MIN_BALANCE gasPriceMultiplier: $GAS_PRICE_MULTIPLIER +txTimeThreshold: $TX_TIME_THRESHOLD +checkWalletBalanceTime: $CHECK_WALLET_BALANCE_TIME gasLimitMultiplier: $GAS_LIMIT_MULTIPLIER timeout: $TIMEOUT maxRatio: $MAX_RATIO diff --git a/config.example.yaml b/config.example.yaml index 22b3fd19..32fe6857 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -114,6 +114,12 @@ botMinBalance: 0.5 # Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7% gasPriceMultiplier: 107 +# Time threshold (in ms) for a transaction mine time before it counts as a trigger to increase gas price multiplier for future transactions, default is 15000ms (15 seconds) +txTimeThreshold: 15000 + +# Time (in minutes) to to check the operating wallet balances, 0 means dont ever check wallet balance, default is 15 mins +checkWalletBalanceTime: 15 + # Option to multiply the gas limit estimation from the rpc as percentage, default is 100, ie no change gasLimitMultiplier: 100 diff --git a/src/cli/commands/sweep.ts b/src/cli/commands/sweep.ts index 9f16eeb1..1295229e 100644 --- a/src/cli/commands/sweep.ts +++ b/src/cli/commands/sweep.ts @@ -105,6 +105,7 @@ export async function sweepFunds(opts: SweepOptions) { subgraph, gasLimitMultiplier: 100, gasPriceMultiplier: 107, + txTimeThreshold: 2_500, timeout: 15_000, // unused fields but need to be defined @@ -124,6 +125,7 @@ export async function sweepFunds(opts: SweepOptions) { sweepWalletTime: 0, convertToGasTime: 0, rotateMultiWallet: false, + checkWalletBalanceTime: 0, }; // prepare state config fields @@ -172,6 +174,7 @@ export async function sweepFunds(opts: SweepOptions) { client, chainConfig, baseGasPriceMultiplier: options.gasPriceMultiplier, + txTimeThreshold: options.txTimeThreshold, }), }; const state = new SharedState(stateConfig); diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index f1c0f567..a0b55868 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -6,7 +6,7 @@ import { RainSolverLogger } from "../logger"; import { WalletManager, WalletType } from "../wallet"; import { SharedState, SharedStateConfig } from "../state"; import { SubgraphConfig, SubgraphManager } from "../subgraph"; -import { Result, sleep, withBigintSerializer } from "../common"; +import { Result, sleep } from "../common"; import { SpanStatusCode, trace, context } from "@opentelemetry/api"; import { describe, it, expect, vi, beforeEach, Mock } from "vitest"; @@ -471,21 +471,9 @@ describe("Test RainSolverCli", () => { "meta.liquidityProviders": ["lp1", "lp2"], }); - expect(mockRoundSpan.setAttribute).toHaveBeenCalledWith( - "circulatingAccounts", - JSON.stringify( - { - "0xworker1": 1000000000000000000n, - "0xworker2": 2000000000000000000n, - }, - withBigintSerializer, - ), - ); - expect(mockRoundSpan.setAttribute).toHaveBeenCalledWith("lastAccountIndex", 5); expect(mockRoundSpan.setAttribute).toHaveBeenCalledWith("avgGasCost", "1"); expect(mockSubgraphManager.getOrderbooks).toHaveBeenCalledTimes(1); - expect(mockWalletManager.getWorkerWalletsBalance).toHaveBeenCalledTimes(1); }); it("should handle single wallet mode (no worker balances)", async () => { @@ -835,6 +823,7 @@ describe("Test RainSolverCli", () => { (trace.setSpan as Mock).mockReturnValue({ test: "context" }); (context.active as Mock).mockReturnValue({ test: "active" }); + (rainSolverCli as any).nextCheckWalletBalanceTime = Date.now() - 1; (mockWalletManager.checkMainWalletBalance as Mock).mockResolvedValue({ name: "check-balance", }); @@ -892,9 +881,13 @@ describe("Test RainSolverCli", () => { (trace.setSpan as Mock).mockReturnValue({ test: "context" }); (context.active as Mock).mockReturnValue({ test: "active" }); + (rainSolverCli as any).nextCheckWalletBalanceTime = Date.now() - 1; (mockWalletManager.checkMainWalletBalance as Mock).mockResolvedValue({ name: "check-balance", }); + (mockWalletManager.getWorkerWalletsBalance as Mock).mockResolvedValue({ + name: "check-multi-balance", + }); (mockWalletManager.fundOwnedVaults as Mock).mockRejectedValue( new Error("Fund vault failed"), ); diff --git a/src/cli/index.ts b/src/cli/index.ts index 24e25730..044776c6 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -72,6 +72,8 @@ export class RainSolverCli { private nextGasConversionTime: number; /** Time for next worker wallet assessment */ private nextAssesWorkerWalletTime = Date.now() + 15 * MINUTE; + /** Time for next wallet balance check */ + private nextCheckWalletBalanceTime: number; private constructor( state: SharedState, @@ -83,6 +85,7 @@ export class RainSolverCli { logger: RainSolverLogger, nextDatafetcherReset: number, ) { + const now = Date.now(); this.state = state; this.appOptions = appOptions; this.orderManager = orderManager; @@ -92,9 +95,13 @@ export class RainSolverCli { this.logger = logger; this.nextDatafetcherReset = nextDatafetcherReset; this.nextSweepTime = - appOptions.sweepWalletTime === 0 ? 0 : Date.now() + appOptions.sweepWalletTime * DAY; + appOptions.sweepWalletTime === 0 ? 0 : now + appOptions.sweepWalletTime * DAY; this.nextGasConversionTime = - appOptions.convertToGasTime === 0 ? 0 : Date.now() + appOptions.convertToGasTime * DAY; + appOptions.convertToGasTime === 0 ? 0 : now + appOptions.convertToGasTime * DAY; + this.nextCheckWalletBalanceTime = + appOptions.checkWalletBalanceTime === 0 + ? 0 + : now + appOptions.checkWalletBalanceTime * MINUTE; } /** @@ -202,6 +209,8 @@ export class RainSolverCli { * reports and executes wallet ops. */ async run() { + let prevMainWalletBalanceReport: PreAssembledSpan | undefined; + let prevMultiWalletBalanceReports; // eslint-disable-next-line no-constant-condition while (true) { // start round span and get round ctx @@ -209,11 +218,51 @@ export class RainSolverCli { const roundCtx = trace.setSpan(context.active(), roundSpan); // report meta info - await this.reportMetaInfoForRound(roundSpan); + this.reportMetaInfoForRound(roundSpan).catch(() => {}); + + const now = Date.now(); + + // check wallet balances upon configured intervals + let checkMainWalletBalancePromise = undefined; + let getWorkerWalletsBalancePromise = undefined; + if ( + this.roundCount === 1 || + (this.nextCheckWalletBalanceTime !== 0 && this.nextCheckWalletBalanceTime <= now) + ) { + // update interval + if (this.roundCount > 1) { + this.nextCheckWalletBalanceTime = + now + this.appOptions.checkWalletBalanceTime * MINUTE; + } - // check main wallet balance - const checkBalanceReport = await this.walletManager.checkMainWalletBalance(); - this.logger.exportPreAssembledSpan(checkBalanceReport, roundCtx); + // check main wallet balance + checkMainWalletBalancePromise = this.walletManager.checkMainWalletBalance(); + + // report worker wallet balances + if (this.walletManager.config.type === WalletType.Mnemonic) { + getWorkerWalletsBalancePromise = this.walletManager.getWorkerWalletsBalance(); + roundSpan.setAttribute( + "lastAccountIndex", + this.walletManager.workers.lastUsedDerivationIndex, + ); + } + } else { + if (prevMainWalletBalanceReport) { + prevMainWalletBalanceReport.startTime = now; + prevMainWalletBalanceReport.endTime = performance.now(); + this.logger.exportPreAssembledSpan(prevMainWalletBalanceReport, roundCtx); + } + if (prevMultiWalletBalanceReports) { + roundSpan.setAttribute( + "circulatingAccounts", + JSON.stringify(prevMultiWalletBalanceReports, withBigintSerializer), + ); + roundSpan.setAttribute( + "lastAccountIndex", + this.walletManager.workers.lastUsedDerivationIndex, + ); + } + } try { // try funding owned vaults and report @@ -255,6 +304,27 @@ export class RainSolverCli { // report rpcs performance for round await this.reportRpcMetricsForRound(roundCtx); + if (checkMainWalletBalancePromise !== undefined) { + await checkMainWalletBalancePromise + .then((checkBalanceReport) => { + this.logger.exportPreAssembledSpan(checkBalanceReport, roundCtx); + prevMainWalletBalanceReport = checkBalanceReport; + }) + .catch(() => {}); + } + + if (getWorkerWalletsBalancePromise !== undefined) { + await getWorkerWalletsBalancePromise + .then((v) => { + roundSpan.setAttribute( + "circulatingAccounts", + JSON.stringify(v, withBigintSerializer), + ); + prevMultiWalletBalanceReports = v; + }) + .catch(() => {}); + } + // eslint-disable-next-line no-console console.log(`Starting next round in ${this.appOptions.sleep / 1000} seconds...`, "\n"); roundSpan.end(); @@ -476,21 +546,6 @@ export class RainSolverCli { "meta.liquidityProviders": this.state.router.getLiquidityProvidersList(), }); - // report worker wallet balances - if (this.walletManager.config.type === WalletType.Mnemonic) { - roundSpan.setAttribute( - "circulatingAccounts", - JSON.stringify( - await this.walletManager.getWorkerWalletsBalance(), - withBigintSerializer, - ), - ); - roundSpan.setAttribute( - "lastAccountIndex", - this.walletManager.workers.lastUsedDerivationIndex, - ); - } - // report avg gas cost if (this.avgGasCost) { roundSpan.setAttribute("avgGasCost", formatUnits(this.avgGasCost, 18)); diff --git a/src/config/yaml.test.ts b/src/config/yaml.test.ts index 921b81fb..dbc5fb18 100644 --- a/src/config/yaml.test.ts +++ b/src/config/yaml.test.ts @@ -36,6 +36,8 @@ txGas: 15000 quoteGas: 2000000 botMinBalance: 50.5 gasPriceMultiplier: 150 +txTimeThreshold: 4000 +checkWalletBalanceTime: 30 gasLimitMultiplier: 90 timeout: 20000 maxRatio: true @@ -110,6 +112,7 @@ orderbookTradeTypes: quoteGas: BigInt(2000000), botMinBalance: "50.5", gasPriceMultiplier: 150, + txTimeThreshold: 4000, gasLimitMultiplier: 90, timeout: 20000, maxRatio: true, @@ -156,6 +159,7 @@ orderbookTradeTypes: sweepWalletTime: 0, convertToGasTime: 0, rotateMultiWallet: false, + checkWalletBalanceTime: 30, }; // AppOptions returned from fromYaml() should match expected @@ -191,6 +195,7 @@ orderbookTradeTypes: quoteGas: "2000000", botMinBalance: "50.5", gasPriceMultiplier: "150", + txTimeThreshold: "4000", gasLimitMultiplier: "90", timeout: "20000", maxRatio: true, @@ -282,6 +287,7 @@ orderbookTradeTypes: // botMinBalance is resolved as string ("50.5") assert.deepEqual(result.botMinBalance, "50.5"); assert.deepEqual(result.gasPriceMultiplier, 150); + assert.deepEqual(result.txTimeThreshold, 4000); assert.deepEqual(result.gasLimitMultiplier, 90); assert.deepEqual(result.timeout, 20000); assert.equal(result.maxRatio, true); @@ -341,5 +347,6 @@ orderbookTradeTypes: assert.equal(result.sweepWalletTime, 10); assert.equal(result.convertToGasTime, 2); assert.equal(result.rotateMultiWallet, true); + assert.equal(result.checkWalletBalanceTime, 15); // should be default 15 }); }); diff --git a/src/config/yaml.ts b/src/config/yaml.ts index ebc37217..5ac229ea 100644 --- a/src/config/yaml.ts +++ b/src/config/yaml.ts @@ -116,6 +116,10 @@ export type AppOptions = { convertToGasTime: number; /** Determines if multi wallets should be rotated at runtime, meaning new ones to replace older ones once they runs out of gas, default is false */ rotateMultiWallet: boolean; + /** Time threshold (in ms) for a transaction mine time before it counts as a trigger to increase gas price multiplier for future transactions, default is 15 seconds */ + txTimeThreshold: number; + /** Time (in minutes) to to check the operating wallet balances, 0 means dont ever check wallet balance, default is 15 mins */ + checkWalletBalanceTime: number; }; /** Provides methods to instantiate and validate AppOptions */ @@ -339,6 +343,30 @@ export namespace AppOptions { "expected a boolean value for rotateMultiWallet", false, ), + txTimeThreshold: Validator.resolveNumericValue( + input.txTimeThreshold, + INT_PATTERN, + "invalid txTimeThreshold value, must be an integer greater than 0", + "15000", + undefined, + (txTimeThreshold) => + assert( + txTimeThreshold > 0, + "invalid txTimeThreshold value, must be an integer greater than 0", + ), + ), + checkWalletBalanceTime: Validator.resolveNumericValue( + input.checkWalletBalanceTime, + INT_PATTERN, + "invalid checkWalletBalanceTime, must be an integer greater than equal to 0", + "15", + undefined, + (checkWalletBalanceTime) => + assert( + checkWalletBalanceTime >= 0, + "invalid checkWalletBalanceTime, must be an integer greater than equal to 0", + ), + ), } as AppOptions); } catch (error: any) { if (error instanceof AppOptionsError) { diff --git a/src/core/process/order.test.ts b/src/core/process/order.test.ts index d4103c8c..027e9f1d 100644 --- a/src/core/process/order.test.ts +++ b/src/core/process/order.test.ts @@ -79,6 +79,7 @@ describe("Test processOrder", () => { }, }, signer: {}, + blockNumber: 123n, } as any; mockRainSolver = { state: mockState, diff --git a/src/core/process/order.ts b/src/core/process/order.ts index e811a934..8fdc7e7f 100644 --- a/src/core/process/order.ts +++ b/src/core/process/order.ts @@ -64,6 +64,7 @@ export async function processOrder( spanAttributes["details.pair"] = tokenPair; spanAttributes["details.orderbook"] = orderDetails.orderbook; spanAttributes["details.owner"] = orderDetails.takeOrder.struct.order.owner.toLowerCase(); + spanAttributes["details.startBlockNumber"] = dataFetcherBlockNumber.toString(); if (orderDetails.oracleUrl) { spanAttributes["details.oracle"] = orderDetails.oracleUrl; } diff --git a/src/gas/index.test.ts b/src/gas/index.test.ts index 8618a174..9ad57330 100644 --- a/src/gas/index.test.ts +++ b/src/gas/index.test.ts @@ -167,10 +167,9 @@ describe("Test GasManager", () => { // class field defaults expect(manager.gasIncreasePointsPerStep).toBe(3); expect(manager.gasIncreaseStepTime).toBe(60 * 60 * 1000); // 3_600_000 ms - expect(manager.txTimeThreshold).toBe(15_000); - // maxGasPriceMultiplier defaults to base + 50 when not provided - expect(manager.maxGasPriceMultiplier).toBe(150); + // maxGasPriceMultiplier defaults to base + 100 when not provided + expect(manager.maxGasPriceMultiplier).toBe(200); // multiplier starts at the base value expect(manager.gasPriceMultiplier).toBe(100); diff --git a/src/gas/index.ts b/src/gas/index.ts index 5bfe8cf8..bc0df952 100644 --- a/src/gas/index.ts +++ b/src/gas/index.ts @@ -17,7 +17,7 @@ export type GasManagerConfig = { /** The time to stay in increased gas price multiplier before resetting to base value */ gasIncreaseStepTime?: number; /** The time threshold (in ms) for transaction mine time before considering it as a trigger for gas price multiplier increase */ - txTimeThreshold?: number; + txTimeThreshold: number; }; /** Transaction mining record */ @@ -67,7 +67,7 @@ export class GasManager { /** The time to stay in increased the gas price multiplier before reseting to base */ readonly gasIncreaseStepTime: number = 60 * 60 * 1000; // default 60 minutes in milliseconds /** The threshold for transaction time before considering it as a trigger for gas price multiplierincrease */ - readonly txTimeThreshold: number = 15_000; // default 15 seconds threshold + readonly txTimeThreshold: number; // default 15 seconds threshold /** Current gas price of the operating chain */ gasPrice = 0n; @@ -84,9 +84,7 @@ export class GasManager { this.client = config.client; this.chainConfig = config.chainConfig; this.baseGasPriceMultiplier = config.baseGasPriceMultiplier; - if (config.txTimeThreshold !== undefined) { - this.txTimeThreshold = config.txTimeThreshold; - } + this.txTimeThreshold = config.txTimeThreshold; if (config.gasIncreasePointsPerStep !== undefined) { this.gasIncreasePointsPerStep = config.gasIncreasePointsPerStep; } @@ -96,7 +94,7 @@ export class GasManager { if (config.maxGasPriceMultiplier !== undefined) { this.maxGasPriceMultiplier = config.maxGasPriceMultiplier; } else { - this.maxGasPriceMultiplier = this.baseGasPriceMultiplier + 50; + this.maxGasPriceMultiplier = this.baseGasPriceMultiplier + 100; // default +1x ceiling } this.gasPriceMultiplier = config.baseGasPriceMultiplier; } diff --git a/src/state/index.ts b/src/state/index.ts index 08e69dc8..6c1b92a0 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -177,6 +177,7 @@ export namespace SharedStateConfig { client, chainConfig, baseGasPriceMultiplier: options.gasPriceMultiplier, + txTimeThreshold: options.txTimeThreshold, }), };