diff --git a/config.env.yaml b/config.env.yaml index 7e288461..6a8091fd 100644 --- a/config.env.yaml +++ b/config.env.yaml @@ -30,6 +30,7 @@ route: $ROUTE sleep: $SLEEP poolUpdateInterval: $POOL_UPDATE_INTERVAL gasCoveragePercentage: $GAS_COVER +headroom: $HEADROOM txGas: $TX_GAS quoteGas: $QUOTE_GAS botMinBalance: $BOT_MIN_BALANCE diff --git a/config.example.yaml b/config.example.yaml index 68fed8db..e211ef77 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -98,6 +98,10 @@ poolUpdateInterval: 10 # Gas coverage percentage for each transaction to be considered profitable to be submitted, default is 100 gasCoveragePercentage: 250 +# Percentage (number) that accumulates on top of actual profitablity index (gasCoveragePercentage) that assures trade success during +# simulation with that higher profit so to give leeway for price fluctuation between simulation time mining time, default: %2.5 +headroom: 2.5 + # Option to set a gas limit for all submitting txs optionally with appended percentage sign to apply as percentage to original gas txGas: 110% diff --git a/src/cli/commands/sweep.ts b/src/cli/commands/sweep.ts index 2dfa5948..2cdba89a 100644 --- a/src/cli/commands/sweep.ts +++ b/src/cli/commands/sweep.ts @@ -120,6 +120,7 @@ export async function sweepFunds(opts: SweepOptions) { orderbookTradeTypes: {} as any, maxConcurrency: 0, skipSweep: new Set(), + headroom: 0, }; // prepare state config fields diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 68f93c8e..68326392 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -1,7 +1,7 @@ import { AppOptions } from "../config"; import { RainSolver } from "../core"; import { OrderManager } from "../order"; -import { RainSolverCli } from "./index"; +import { DAY, RainSolverCli } from "./index"; import { RainSolverLogger } from "../logger"; import { WalletManager, WalletType } from "../wallet"; import { SharedState, SharedStateConfig } from "../state"; @@ -182,6 +182,7 @@ describe("Test RainSolverCli", () => { sync: vi.fn(), downscaleProtection: vi.fn(), getCurrentMetadata: vi.fn().mockReturnValue({ key: "value" }), + metadata: { key: "value" }, } as any; mockWalletManager = { @@ -198,7 +199,9 @@ describe("Test RainSolverCli", () => { retryPendingRemoveWorkers: vi.fn(), convertHoldingsToGas: vi.fn(), getWorkerWalletsBalance: vi.fn(), + sweepWallet: vi.fn(), workers: { + signers: new Map([["signer", { name: "worker1" }]]), lastUsedDerivationIndex: 5, }, } as any; @@ -655,11 +658,24 @@ describe("Test RainSolverCli", () => { ); }); - it("should call additional operations on round 250", async () => { + it("should call downscale operation on round 250", async () => { rainSolverCli.roundCount = 250; const mockRoundCtx = { test: "context" }; + + (mockWalletManager.retryPendingAddWorkers as Mock).mockResolvedValue([]); + (mockWalletManager.assessWorkers as Mock).mockResolvedValue([]); + (mockOrderManager.downscaleProtection as Mock).mockResolvedValue(undefined); + + await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any); + + expect(mockOrderManager.downscaleProtection).toHaveBeenCalledOnce(); + }); + + it("should sweep every 5 days and convert to gas evey day", async () => { + const mockRoundCtx = { test: "context" }; const mockPendingRemoveReports = [{ name: "pending-remove-1" }]; const mockConvertHoldingsReport = { name: "convert-holdings" }; + const mockSweepReport = { name: "sweep" }; (mockWalletManager.retryPendingAddWorkers as Mock).mockResolvedValue([]); (mockWalletManager.assessWorkers as Mock).mockResolvedValue([]); @@ -669,13 +685,15 @@ describe("Test RainSolverCli", () => { (mockWalletManager.convertHoldingsToGas as Mock).mockResolvedValue( mockConvertHoldingsReport, ); - (mockOrderManager.downscaleProtection as Mock).mockResolvedValue(undefined); + (mockWalletManager.sweepWallet as Mock).mockResolvedValue(mockSweepReport); + (rainSolverCli as any).nextSweepTime = Date.now() - 6 * DAY; + (rainSolverCli as any).nextGasConversionTime = Date.now() - 2 * DAY; await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any); expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(1); expect(mockWalletManager.convertHoldingsToGas).toHaveBeenCalledTimes(1); - expect(mockOrderManager.downscaleProtection).toHaveBeenCalledOnce(); + expect(mockWalletManager.sweepWallet).toHaveBeenCalledTimes(1); expect(mockLogger.exportPreAssembledSpan).toHaveBeenCalledWith( { name: "pending-remove-1" }, @@ -685,6 +703,25 @@ describe("Test RainSolverCli", () => { mockConvertHoldingsReport, mockRoundCtx, ); + expect(mockLogger.exportPreAssembledSpan).toHaveBeenCalledWith( + mockSweepReport, + mockRoundCtx, + ); + + // next time should not call these when time has not reached + await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any); + expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(1); + expect(mockWalletManager.convertHoldingsToGas).toHaveBeenCalledTimes(1); + expect(mockWalletManager.sweepWallet).toHaveBeenCalledTimes(1); + + // set timers to simulate some days passed by + (rainSolverCli as any).nextSweepTime = Date.now() - 6 * DAY; + (rainSolverCli as any).nextGasConversionTime = Date.now() - 2 * DAY; + + await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any); + expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(2); + expect(mockWalletManager.convertHoldingsToGas).toHaveBeenCalledTimes(2); + expect(mockWalletManager.sweepWallet).toHaveBeenCalledTimes(2); }); }); @@ -724,7 +761,6 @@ describe("Test RainSolverCli", () => { expect(mockRoundSpan.setAttribute).toHaveBeenCalledWith("txUrls.failed", [ "https://etherscan.io/tx/0x789", ]); - expect(mockOrderManager.getCurrentMetadata).toHaveBeenCalledTimes(1); expect(mockRoundSpan.setAttribute).toHaveBeenCalledWith("ordersMetadata.key", "value"); }); diff --git a/src/cli/index.ts b/src/cli/index.ts index 1e88d309..22401d53 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -63,7 +63,9 @@ export class RainSolverCli { private nextGasReset = Date.now() + DAY; private nextDatafetcherReset: number; /** Wallet sweeper timer (once every 5 days) */ - private lastSweepTime = Date.now() + 5 * DAY; + private nextSweepTime = Date.now() + 5 * DAY; + /** Convert bounties to gas timer (once every day) */ + private nextGasConversionTime = Date.now() + DAY; private constructor( state: SharedState, @@ -294,7 +296,7 @@ export class RainSolverCli { */ async processOrdersForRound(roundSpan: Span, roundCtx: Context) { // process round and export the reports - const { results, checkpointReports } = await this.rainSolver.processNextRound({ + const { results, totalLength } = await this.rainSolver.processNextRound({ span: roundSpan, context: roundCtx, }); @@ -312,15 +314,11 @@ export class RainSolverCli { } roundSpan.setAttribute("txUrls.success", successTxs); roundSpan.setAttribute("txUrls.failed", failedTxs); - roundSpan.setAttribute( - "ordersMetadata.roundProcessedOrderPairsCount", - checkpointReports.length, - ); - const ordersMetadata = this.orderManager.getCurrentMetadata(); - for (const key in ordersMetadata) { + roundSpan.setAttribute("ordersMetadata.roundProcessedOrderPairsCount", totalLength); + for (const key in this.orderManager.metadata) { roundSpan.setAttribute( `ordersMetadata.${key}`, - ordersMetadata[key as keyof typeof ordersMetadata], + this.orderManager.metadata[key as keyof typeof this.orderManager.metadata], ); } } @@ -345,8 +343,17 @@ export class RainSolverCli { this.logger.exportPreAssembledSpan(report.addWorkerReport, roundCtx); }); - // retry wallet removals and sweep funds once every 250 rounds + // rescale once every 250 rounds if (this.roundCount % 250 === 0) { + // re-evaluate owner limits + await this.orderManager.downscaleProtection(); + } + + const now = Date.now(); + + if (this.nextGasConversionTime <= now) { + this.nextGasConversionTime = now + DAY; + // retry pending remove workers const pendingRemoveReports = await this.walletManager.retryPendingRemoveWorkers(); pendingRemoveReports.forEach((report) => { @@ -354,16 +361,13 @@ export class RainSolverCli { }); // try to sweep main wallet's tokens back to gas - const convertHoldingsToGasReport = await this.walletManager.convertHoldingsToGas(); - this.logger.exportPreAssembledSpan(convertHoldingsToGasReport, roundCtx); - - // re-evaluate owner limits - await this.orderManager.downscaleProtection(); + await this.walletManager.convertHoldingsToGas().then((convertHoldingsToGasReport) => { + this.logger.exportPreAssembledSpan(convertHoldingsToGasReport, roundCtx); + }); } - const now = Date.now(); - if (this.lastSweepTime <= now) { - this.lastSweepTime = now + 5 * DAY; + if (this.nextSweepTime <= now) { + this.nextSweepTime = now + 5 * DAY; // sweep worker wallet bounties for (const [, worker] of this.walletManager.workers.signers) { await this.walletManager.sweepWallet(worker, false).then((report) => { diff --git a/src/config/yaml.test.ts b/src/config/yaml.test.ts index 9ae3e9f8..31ef623f 100644 --- a/src/config/yaml.test.ts +++ b/src/config/yaml.test.ts @@ -31,6 +31,7 @@ route: multi sleep: 20 poolUpdateInterval: 30 gasCoveragePercentage: 110 +headroom: 2.5 txGas: 15000 quoteGas: 2000000 botMinBalance: 50.5 @@ -151,6 +152,7 @@ orderbookTradeTypes: }, maxConcurrency: 15, skipSweep: new Set([`0x${"8".repeat(40)}`, `0x${"9".repeat(40)}`]), + headroom: 102.5, }; // AppOptions returned from fromYaml() should match expected @@ -278,6 +280,7 @@ orderbookTradeTypes: assert.deepEqual(result.timeout, 20000); assert.equal(result.maxRatio, true); assert.equal(result.maxConcurrency, 1); + assert.equal(result.headroom, 102.5); // ownerProfile const expectedOwnerProfile = { diff --git a/src/config/yaml.ts b/src/config/yaml.ts index f54132cb..ecc07968 100644 --- a/src/config/yaml.ts +++ b/src/config/yaml.ts @@ -108,6 +108,8 @@ export type AppOptions = { maxConcurrency: number; /** List of tokens to skip when sweeping bounty tokens */ skipSweep: Set; + /** Trade simulation profitablity headroom, default: 2.5 */ + headroom: number; }; /** Provides methods to instantiate and validate AppOptions */ @@ -294,6 +296,13 @@ export namespace AppOptions { input.skipSweep, "invalid skip sweep list, expected an array of token addresses", ), + headroom: + Validator.resolveNumericValue( + input.headroom, + FLOAT_PATTERN, + "invalid headroom value, must be a number greater than equal to 0", + "2.5", + ) + 100, } as AppOptions); } catch (error: any) { if (error instanceof AppOptionsError) { diff --git a/src/core/index.ts b/src/core/index.ts index fce029c1..cb20aba8 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -58,7 +58,7 @@ export class RainSolver { * @returns An object containing results and reports of the processed round */ async processNextRound(roundSpanCtx?: SpanWithContext, shuffle = true) { - const { settlements, checkpointReports } = await initializeRound.call( + const { settlements, checkpointReports, totalLength } = await initializeRound.call( this, roundSpanCtx, shuffle, @@ -68,6 +68,7 @@ export class RainSolver { results, reports, checkpointReports, + totalLength, }; } diff --git a/src/core/modes/simulator.test.ts b/src/core/modes/simulator.test.ts index 5a97c917..f90e502f 100644 --- a/src/core/modes/simulator.test.ts +++ b/src/core/modes/simulator.test.ts @@ -49,6 +49,7 @@ describe("Test TradeSimulatorBase", () => { appOptions: { gasLimitMultiplier: 1.5, gasCoveragePercentage: "100", + headroom: 3.5, }, } as any as RainSolver; mockSigner = { name: "signer" } as RainSolverSigner; @@ -257,7 +258,10 @@ describe("Test TradeSimulatorBase", () => { Result.err(setTransactionDataError), ); const headroom = BigInt( - (Number(mockSolver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + ( + Number(mockSolver.appOptions.gasCoveragePercentage) * + mockSolver.appOptions.headroom + ).toFixed(), ); const result = await mockSimulator.trySimulateTrade(); @@ -326,7 +330,10 @@ describe("Test TradeSimulatorBase", () => { }; (dryrun as Mock).mockResolvedValueOnce(Result.err(dryrunError)); const headroom = BigInt( - (Number(mockSolver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + ( + Number(mockSolver.appOptions.gasCoveragePercentage) * + mockSolver.appOptions.headroom + ).toFixed(), ); const result = await mockSimulator.trySimulateTrade(); @@ -410,7 +417,10 @@ describe("Test TradeSimulatorBase", () => { .mockResolvedValueOnce(Result.ok(dryrunResult)) .mockResolvedValueOnce(Result.ok(dryrunResult2)); const headroom = BigInt( - (Number(mockSolver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + ( + Number(mockSolver.appOptions.gasCoveragePercentage) * + mockSolver.appOptions.headroom + ).toFixed(), ); // last call to setTransactionData fails const setTransactionDataError = { @@ -525,7 +535,10 @@ describe("Test TradeSimulatorBase", () => { .mockResolvedValueOnce(Result.ok(dryrunResult)) .mockResolvedValueOnce(Result.ok(dryrunResult2)); const headroom = BigInt( - (Number(mockSolver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + ( + Number(mockSolver.appOptions.gasCoveragePercentage) * + mockSolver.appOptions.headroom + ).toFixed(), ); const profitEstimate = 1234n; (mockSimulator.estimateProfit as Mock).mockReturnValueOnce(profitEstimate); diff --git a/src/core/modes/simulator.ts b/src/core/modes/simulator.ts index 94c2b5e0..0a3914f1 100644 --- a/src/core/modes/simulator.ts +++ b/src/core/modes/simulator.ts @@ -153,9 +153,12 @@ export abstract class TradeSimulatorBase { // delete gas to let signer estimate gas again with updated tx data delete prepareParamsResult.value.rawtx.gas; - // determine the success of the trade with 0.25% headroom + // examine the success of the trade with 1.5% headroom const headroom = BigInt( - (Number(this.tradeArgs.solver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + ( + Number(this.tradeArgs.solver.appOptions.gasCoveragePercentage) * + this.tradeArgs.solver.appOptions.headroom + ).toFixed(), ); let minimumExpected = (estimatedGasCost * headroom) / 10000n; this.spanAttributes["gasEst.initial.minBountyExpected"] = minimumExpected.toString(); diff --git a/src/core/process/round.test.ts b/src/core/process/round.test.ts index 8d19fb5f..ddb6a505 100644 --- a/src/core/process/round.test.ts +++ b/src/core/process/round.test.ts @@ -112,7 +112,10 @@ describe("Test initializeRound", () => { ]; const mockSettleFn = vi.fn(); - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); (mockSolver.processOrder as Mock).mockResolvedValue(mockSettleFn); @@ -166,7 +169,10 @@ describe("Test initializeRound", () => { }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue({ account: { address: "0xSigner" }, }); @@ -208,7 +214,10 @@ describe("Test initializeRound", () => { describe("empty orders handling", () => { it("should return empty settlements and checkpointReports for empty orders", async () => { - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue([]); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: [], + zeroOutput: [], + }); const result: initializeRoundType = await initializeRound.call(mockSolver); @@ -221,7 +230,10 @@ describe("Test initializeRound", () => { describe("method call verification", () => { it("should call getNextRoundOrders with correct parameter", async () => { - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue([]); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: [], + zeroOutput: [], + }); await initializeRound.call(mockSolver, undefined, false); @@ -248,7 +260,10 @@ describe("Test initializeRound", () => { }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); await initializeRound.call(mockSolver); @@ -270,7 +285,10 @@ describe("Test initializeRound", () => { }; const mockOrders = [orderDetails]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); await initializeRound.call(mockSolver); @@ -297,7 +315,10 @@ describe("Test initializeRound", () => { }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue({ account: { address: "0xSignerDEF" }, }); @@ -341,7 +362,10 @@ describe("Test initializeRound", () => { }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); const result: initializeRoundType = await initializeRound.call( @@ -383,7 +407,10 @@ describe("Test initializeRound", () => { }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); const result: initializeRoundType = await initializeRound.call(mockSolver); @@ -406,7 +433,10 @@ describe("Test initializeRound", () => { takeOrder: { id: "0xOrder1", struct: { order: { owner: "0xOwner1" } } }, }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: mockOrders, + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); (mockSolver as any).logger = { exportPreAssembledSpan: vi.fn(), @@ -414,7 +444,7 @@ describe("Test initializeRound", () => { const mockCtx = { fields: {} } as any; await initializeRound.call(mockSolver, { span: {} as any, context: mockCtx }); - expect(mockSolver.logger?.exportPreAssembledSpan).toHaveBeenCalledTimes(1); + expect(mockSolver.logger?.exportPreAssembledSpan).toHaveBeenCalledTimes(2); expect(mockSolver.logger?.exportPreAssembledSpan).toHaveBeenCalledWith( expect.anything(), mockCtx, @@ -434,7 +464,10 @@ describe("Test initializeRound", () => { takeOrder: { id: "0xOrder1", struct: { order: { owner: "0xOwner1" } } }, }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); const loggerExportReport = vi.spyOn( RainSolverLogger.prototype, @@ -449,13 +482,17 @@ describe("Test initializeRound", () => { describe("return value structure", () => { it("should always return object with settlements and checkpointReports arrays", async () => { - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue([]); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: [], + zeroOutput: [], + }); const result: initializeRoundType = await initializeRound.call(mockSolver); expect(result).toEqual({ settlements: expect.any(Array), checkpointReports: expect.any(Array), + totalLength: 0, }); expect(Array.isArray(result.settlements)).toBe(true); expect(Array.isArray(result.checkpointReports)).toBe(true); @@ -473,7 +510,10 @@ describe("Test initializeRound", () => { }, ]; - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue({ account: { address: "0xSigner" }, }); @@ -506,7 +546,10 @@ describe("Test initializeRound", () => { }, ]; const mockSettleFn = vi.fn(); - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); (mockSolver.processOrder as Mock).mockResolvedValue(mockSettleFn); @@ -601,7 +644,10 @@ describe("Test initializeRound", () => { }, ]; const mockSettleFn = vi.fn(); - (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue(mockOrders); + (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ + nonZeroOutput: mockOrders, + zeroOutput: [], + }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); (mockSolver.processOrder as Mock).mockResolvedValue(mockSettleFn); (mockState.contracts.getAddressesForTrade as Mock).mockReturnValue(undefined); // simulate missing trade addresses diff --git a/src/core/process/round.ts b/src/core/process/round.ts index 098f65c1..92450b47 100644 --- a/src/core/process/round.ts +++ b/src/core/process/round.ts @@ -29,7 +29,10 @@ export async function initializeRound( roundSpanCtx?: SpanWithContext, shuffle = true, ) { - const orders = [...this.orderManager.getNextRoundOrders()]; + const nextRoundOrders = this.orderManager.getNextRoundOrders(); + const orders = [...nextRoundOrders.nonZeroOutput]; + const zeroOutputs = [...nextRoundOrders.zeroOutput]; + const totalLength = orders.length + zeroOutputs.length; const settlements: Settlement[] = []; const checkpointReports: PreAssembledSpan[] = []; @@ -49,6 +52,7 @@ export async function initializeRound( return { settlements, checkpointReports, + totalLength, }; } for (const orderDetails of iterOrders(orders, shuffle)) { @@ -89,9 +93,27 @@ export async function initializeRound( checkpointReports.push(checkpointReport); }); + // report zero output order pairs + for (let i = 0; i < zeroOutputs.length; i += 25) { + const zeroOutputReport = new PreAssembledSpan(`order_zero_output`, performance.now()); + zeroOutputs.slice(i, i + 25).forEach((p, j) => { + const pair = `${p.buyTokenSymbol}/${p.sellTokenSymbol}`; + const owner = p.takeOrder.struct.order.owner.toLowerCase(); + zeroOutputReport.extendAttrs({ + [`details.${j}.pair`]: pair, + [`details.${j}.owner`]: owner, + [`details.${j}.orderHash`]: p.takeOrder.id, + [`details.${j}.orderbook`]: p.orderbook, + }); + }); + zeroOutputReport.end(); + this.logger?.exportPreAssembledSpan(zeroOutputReport, roundSpanCtx?.context); + } + return { settlements, checkpointReports, + totalLength, }; } diff --git a/src/order/index.test.ts b/src/order/index.test.ts index 71f9925a..120f5cb0 100644 --- a/src/order/index.test.ts +++ b/src/order/index.test.ts @@ -585,11 +585,12 @@ describe("Test OrderManager", () => { await orderManager.addOrder(mockOrder as any); const result = orderManager.getNextRoundOrders(); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); + expect(Array.isArray(result.nonZeroOutput)).toBe(true); + expect(Array.isArray(result.zeroOutput)).toBe(true); + expect(result.nonZeroOutput.length).toBeGreaterThan(0); // check the structure of the first orderbook's bundled orders - const bundledOrders = result; + const bundledOrders = result.nonZeroOutput; expect(Array.isArray(bundledOrders)).toBe(true); expect(bundledOrders.length).toBeGreaterThan(0); @@ -953,7 +954,7 @@ describe("Test OrderManager", () => { // helper to get the order hashes returned in the round const getRoundHashes = () => { const roundOrders = orderManager.getNextRoundOrders(); - return roundOrders.map((o) => o.takeOrder.id); + return roundOrders.nonZeroOutput.map((o) => o.takeOrder.id); }; // first call: should return the first 3 orders @@ -981,7 +982,7 @@ describe("Test OrderManager", () => { // get the takeOrder object from getNextRoundOrders const roundOrders = orderManager.getNextRoundOrders(); - const orderDetails = roundOrders[0]; + const orderDetails = roundOrders.nonZeroOutput[0]; // update the quote field via the object from getNextRoundOrders orderDetails.takeOrder.quote = { maxOutput: 999n, ratio: 888n }; @@ -1048,7 +1049,7 @@ describe("Test OrderManager", () => { // should find orderB as opposing order for orderA in the same orderbook const opposing = orderManager.getCounterpartyOrders( - roundOrders[0], + roundOrders.nonZeroOutput[0], CounterpartySource.IntraOrderbook, ); expect(Array.isArray(opposing)).toBe(true); @@ -1124,7 +1125,7 @@ describe("Test OrderManager", () => { // should find orderB as opposing order for orderA across orderbooks const opposing = orderManager.getCounterpartyOrders( - roundOrders[0], + roundOrders.nonZeroOutput[0], CounterpartySource.InterOrderbook, ); for (const counteryparties of opposing) { diff --git a/src/order/index.ts b/src/order/index.ts index 583fc784..6255810f 100644 --- a/src/order/index.ts +++ b/src/order/index.ts @@ -71,6 +71,14 @@ export class OrderManager { */ ownerTokenVaultMap: OrderbookOwnerTokenVaultsMap; + /** Metadata details about orders currently being processed */ + metadata = { + totalCount: 0, + totalOwnersCount: 0, + totalPairsCount: 0, + totalDistinctPairsCount: 0, + }; + /** * Creates a new OrderManager instance * @param state - SharedState instance @@ -121,6 +129,7 @@ export class OrderManager { ); } } + this.getCurrentMetadata?.(); return Result.ok(report); } @@ -189,6 +198,8 @@ export class OrderManager { this.addToTokenVaultsMap(pairs[j]); } + this.getCurrentMetadata?.(); + return Result.ok(undefined); } @@ -339,6 +350,7 @@ export class OrderManager { } } } + this.getCurrentMetadata?.(); } /** @@ -428,10 +440,19 @@ export class OrderManager { * Prepares orders for the next round * @returns Array of bundled orders grouped by orderbook */ - getNextRoundOrders(): Pair[] { - const result: Pair[] = []; + getNextRoundOrders(): { + nonZeroOutput: Pair[]; + zeroOutput: Pair[]; + } { + const result: { + nonZeroOutput: Pair[]; + zeroOutput: Pair[]; + } = { + nonZeroOutput: [], + zeroOutput: [], + }; this.ownersMap.forEach((ownersProfileMap) => { - ownersProfileMap.forEach((ownerProfile) => { + ownersProfileMap.forEach((ownerProfile, owner) => { let remainingLimit = ownerProfile.limit; // consume orders limits @@ -447,7 +468,40 @@ export class OrderManager { ownerProfile.lastIndex += remainingConsumingOrders.length; consumingOrders.push(...remainingConsumingOrders); } - result.push(...consumingOrders); + + // get updated balance for the pairs from owner vaults map + for (const pair of consumingOrders) { + pair.sellTokenVaultBalance = + this.ownerTokenVaultMap + .get(pair.orderbook) + ?.get(owner) + ?.get(pair.sellToken) + ?.get( + BigInt( + pair.takeOrder.struct.order.validOutputs[ + pair.takeOrder.struct.outputIOIndex + ].vaultId, + ), + )?.balance ?? pair.sellTokenVaultBalance; + pair.buyTokenVaultBalance = + this.ownerTokenVaultMap + .get(pair.orderbook) + ?.get(owner) + ?.get(pair.buyToken) + ?.get( + BigInt( + pair.takeOrder.struct.order.validInputs[ + pair.takeOrder.struct.inputIOIndex + ].vaultId, + ), + )?.balance ?? pair.buyTokenVaultBalance; + + if (pair.sellTokenVaultBalance <= 0n) { + result.zeroOutput.push(pair); + } else { + result.nonZeroOutput.push(pair); + } + } }); }); return result; @@ -578,12 +632,13 @@ export class OrderManager { totalPairsCount += obPairs; totalDistinctPairsCount = distinctPairsSet.size; }); - return { + this.metadata = { totalCount, totalOwnersCount, totalPairsCount, totalDistinctPairsCount, }; + return this.metadata; } } diff --git a/src/wallet/index.test.ts b/src/wallet/index.test.ts index 5a537432..086577c2 100644 --- a/src/wallet/index.test.ts +++ b/src/wallet/index.test.ts @@ -358,23 +358,17 @@ describe("Test WalletManager", () => { expect(report.attributes["details.destination"]).toBe(walletManager.mainWallet.address); // verify token transfer details - expect(report.attributes["details.transfers.TEST.token"]).toBe(mockToken.address); - expect(report.attributes["details.transfers.TEST.tx"]).toBe( + expect(report.attributes["transfers"]).toContain(mockToken.address); + expect(report.attributes["transfers"]).toContain( "https://explorer.test/tx/0xtoken_hash", ); - expect(report.attributes["details.transfers.TEST.status"]).toBe( - "Transferred successfully", - ); - expect(report.attributes["details.transfers.TEST.amount"]).toBe("1"); + expect(report.attributes["transfers"]).toContain("Transferred successfully"); + expect(report.attributes["transfers"]).toContain("1"); // verify gas transfer details - expect(report.attributes["details.transfers.remainingGas.tx"]).toBe( - "https://explorer.test/tx/0xgas_hash", - ); - expect(report.attributes["details.transfers.remainingGas.status"]).toBe( - "Transferred successfully", - ); - expect(report.attributes["details.transfers.remainingGas.amount"]).toBe("0.1"); + expect(report.attributes["transfers"]).toContain("https://explorer.test/tx/0xgas_hash"); + expect(report.attributes["transfers"]).toContain("Transferred successfully"); + expect(report.attributes["transfers"]).toContain("0.1"); // verify successfully swept expect(report.status?.code).toBe(SpanStatusCode.OK); @@ -413,12 +407,10 @@ describe("Test WalletManager", () => { ); // verify token failure details - expect(report.attributes["details.transfers.TEST.tx"]).toBe( + expect(report.attributes["transfers"]).toContain( "https://explorer.test/tx/0xfailed_token", ); - expect(report.attributes["details.transfers.TEST.status"]).toContain( - "Token transfer failed", - ); + expect(report.attributes["transfers"]).toContain("Token transfer failed"); transferTokenFromSpy.mockRestore(); transferRemainingGasFromSpy.mockRestore(); @@ -447,9 +439,7 @@ describe("Test WalletManager", () => { expect(report.attributes["severity"]).toBe(ErrorSeverity.LOW); // verify gas failure details - expect(report.attributes["details.transfers.remainingGas.status"]).toContain( - "Gas transfer failed", - ); + expect(report.attributes["transfers"]).toContain("Gas transfer failed"); transferTokenFromSpy.mockRestore(); transferRemainingGasFromSpy.mockRestore(); @@ -481,17 +471,13 @@ describe("Test WalletManager", () => { ); // verify token failure details - expect(report.attributes["details.transfers.TEST.status"]).toContain( - "Failed to transfer", - ); + expect(report.attributes["transfers"]).toContain("Failed to transfer"); // verify gas failure details - expect(report.attributes["details.transfers.remainingGas.tx"]).toBe( + expect(report.attributes["transfers"]).toContain( "https://explorer.test/tx/0xfailed_gas", ); - expect(report.attributes["details.transfers.remainingGas.status"]).toContain( - "Gas transfer failed", - ); + expect(report.attributes["transfers"]).toContain("Gas transfer failed"); transferTokenFromSpy.mockRestore(); transferRemainingGasFromSpy.mockRestore(); @@ -523,9 +509,7 @@ describe("Test WalletManager", () => { expect(transferTokenFromSpy).not.toHaveBeenCalled(); // verify gas transfer was still attempted and successful - expect(report.attributes["details.transfers.remainingGas.status"]).toBe( - "Transferred successfully", - ); + expect(report.attributes["transfers"]).toContain("Transferred successfully"); transferRemainingGasFromSpy.mockRestore(); transferTokenFromSpy.mockRestore(); @@ -628,28 +612,24 @@ describe("Test WalletManager", () => { expect(report.attributes["details.wallet"]).toBe(walletManager.mainWallet.address); // verify TEST1 conversion details - expect(report.attributes["details.swaps.TEST1.token"]).toBe("0xtoken1"); - expect(report.attributes["details.swaps.TEST1.tx"]).toBe( - "https://explorer.test/tx/0xhash1", - ); - expect(report.attributes["details.swaps.TEST1.status"]).toBe("Successfully swapped"); - expect(report.attributes["details.swaps.TEST1.amount"]).toBe("100"); - expect(report.attributes["details.swaps.TEST1.receivedAmount"]).toBe("0.1"); - expect(report.attributes["details.swaps.TEST1.receivedAmountMin"]).toBe("0.095"); - expect(report.attributes["details.swaps.TEST1.expectedGasCost"]).toBe("0.01"); - expect(report.attributes["details.swaps.TEST1.route"]).toBe("TEST1 -> WETH"); + expect(report.attributes["swaps"]).toContain("0xtoken1"); + expect(report.attributes["swaps"]).toContain("https://explorer.test/tx/0xhash1"); + expect(report.attributes["swaps"]).toContain("Successfully swapped"); + expect(report.attributes["swaps"]).toContain("100"); + expect(report.attributes["swaps"]).toContain("0.1"); + expect(report.attributes["swaps"]).toContain("0.095"); + expect(report.attributes["swaps"]).toContain("0.01"); + expect(report.attributes["swaps"]).toContain("TEST1 -> WETH"); // verify TEST2 conversion details - expect(report.attributes["details.swaps.TEST2.token"]).toBe("0xtoken2"); - expect(report.attributes["details.swaps.TEST2.tx"]).toBe( - "https://explorer.test/tx/0xhash2", - ); - expect(report.attributes["details.swaps.TEST2.status"]).toBe("Successfully swapped"); - expect(report.attributes["details.swaps.TEST2.amount"]).toBe("50"); - expect(report.attributes["details.swaps.TEST2.receivedAmount"]).toBe("0.05"); - expect(report.attributes["details.swaps.TEST2.receivedAmountMin"]).toBe("0.0475"); - expect(report.attributes["details.swaps.TEST2.expectedGasCost"]).toBe("0.01"); - expect(report.attributes["details.swaps.TEST2.route"]).toBe("TEST2 -> WETH"); + expect(report.attributes["swaps"]).toContain("0xtoken2"); + expect(report.attributes["swaps"]).toContain("https://explorer.test/tx/0xhash2"); + expect(report.attributes["swaps"]).toContain("Successfully swapped"); + expect(report.attributes["swaps"]).toContain("50"); + expect(report.attributes["swaps"]).toContain("0.05"); + expect(report.attributes["swaps"]).toContain("0.0475"); + expect(report.attributes["swaps"]).toContain("0.01"); + expect(report.attributes["swaps"]).toContain("TEST2 -> WETH"); // verify spy calls expect(convertToGasSpy).toHaveBeenCalledTimes(2); @@ -679,20 +659,12 @@ describe("Test WalletManager", () => { const report = await walletManager.convertHoldingsToGas(2n); // verify TEST1 failure details - expect(report.attributes["details.swaps.TEST1.tx"]).toBe( - "https://explorer.test/tx/0xfailed", - ); - expect(report.attributes["details.swaps.TEST1.status"]).toContain( - "Swap failed due to slippage", - ); + expect(report.attributes["swaps"]).toContain("https://explorer.test/tx/0xfailed"); + expect(report.attributes["swaps"]).toContain("Swap failed due to slippage"); // verify TEST2 failure details - expect(report.attributes["details.swaps.TEST2.tx"]).toBe( - "https://explorer.test/tx/0xfailed", - ); - expect(report.attributes["details.swaps.TEST2.status"]).toContain( - "Swap failed due to slippage", - ); + expect(report.attributes["swaps"]).toContain("https://explorer.test/tx/0xfailed"); + expect(report.attributes["swaps"]).toContain("Swap failed due to slippage"); convertToGasSpy.mockRestore(); }); @@ -708,16 +680,12 @@ describe("Test WalletManager", () => { const report = await walletManager.convertHoldingsToGas(2n); // verify TEST1 failure details - expect(report.attributes["details.swaps.TEST1.status"]).toContain( - "Failed to convert token to gas", - ); - expect(report.attributes["details.swaps.TEST1.status"]).toContain("No route found"); + expect(report.attributes["swaps"]).toContain("Failed to convert token to gas"); + expect(report.attributes["swaps"]).toContain("No route found"); // verify TEST2 failure details - expect(report.attributes["details.swaps.TEST2.status"]).toContain( - "Failed to convert token to gas", - ); - expect(report.attributes["details.swaps.TEST2.status"]).toContain("No route found"); + expect(report.attributes["swaps"]).toContain("Failed to convert token to gas"); + expect(report.attributes["swaps"]).toContain("No route found"); convertToGasSpy.mockRestore(); }); @@ -742,16 +710,12 @@ describe("Test WalletManager", () => { const report = await walletManager.convertHoldingsToGas(2n); // verify TEST1 success details - expect(report.attributes["details.swaps.TEST1.tx"]).toBe( - "https://explorer.test/tx/0xsuccess", - ); - expect(report.attributes["details.swaps.TEST1.status"]).toBe("Successfully swapped"); + expect(report.attributes["swaps"]).toContain("https://explorer.test/tx/0xsuccess"); + expect(report.attributes["swaps"]).toContain("Successfully swapped"); // verify TEST2 failure details - expect(report.attributes["details.swaps.TEST2.status"]).toContain( - "Failed to convert token to gas", - ); - expect(report.attributes["details.swaps.TEST2.status"]).toContain("No route found"); + expect(report.attributes["swaps"]).toContain("Failed to convert token to gas"); + expect(report.attributes["swaps"]).toContain("No route found"); convertToGasSpy.mockRestore(); }); diff --git a/src/wallet/index.ts b/src/wallet/index.ts index a79c7c03..b2b7c879 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -316,77 +316,58 @@ export class WalletManager { report.setAttr("details.wallet", wallet.account.address); report.setAttr("details.destination", this.mainWallet.address); + const transfers: any[] = []; let hadFailures = false; // sweep erc20 tokens from the wallet for (const [, tokenDetails] of this.state.watchedTokens) { - report.setAttr(`details.transfers.${tokenDetails.symbol}.token`, tokenDetails.address); + const transfer: any = {}; + transfer.symbol = tokenDetails.symbol; + transfer.token = tokenDetails.address; try { const { amount, txHash } = await this.transferTokenFrom(wallet, tokenDetails); if (txHash) { - report.setAttr( - `details.transfers.${tokenDetails.symbol}.tx`, - this.state.chainConfig.blockExplorers?.default.url + "/tx/" + txHash, - ); + transfer.tx = + this.state.chainConfig.blockExplorers?.default.url + "/tx/" + txHash; } - report.setAttr( - `details.transfers.${tokenDetails.symbol}.status`, - "Transferred successfully", - ); - report.setAttr( - `details.transfers.${tokenDetails.symbol}.amount`, - formatUnits(amount, tokenDetails.decimals), - ); + transfer.status = "Transferred successfully"; + transfer.amount = formatUnits(amount, tokenDetails.decimals); } catch (error: any) { hadFailures = true; if ("txHash" in error) { - report.setAttr( - `details.transfers.${tokenDetails.symbol}.tx`, - this.state.chainConfig.blockExplorers?.default.url + "/tx/" + error.txHash, - ); - report.setAttr( - `details.transfers.${tokenDetails.symbol}.status`, - await errorSnapshot("", error.error), - ); + transfer.tx = + this.state.chainConfig.blockExplorers?.default.url + "/tx/" + error.txHash; + transfer.status = await errorSnapshot("", error.error); } else { - report.setAttr( - `details.transfers.${tokenDetails.symbol}.status`, - await errorSnapshot("Failed to transfer", error), - ); + transfer.status = await errorSnapshot("Failed to transfer", error); } } + transfers.push(transfer); } // sweep remaining gas from the wallet if (withdrawGas) { + const remainingGas: any = {}; try { const { amount, txHash } = await this.transferRemainingGasFrom(wallet); if (txHash) { - report.setAttr( - `details.transfers.remainingGas.tx`, - this.state.chainConfig.blockExplorers?.default.url + "/tx/" + txHash, - ); + remainingGas.tx = + this.state.chainConfig.blockExplorers?.default.url + "/tx/" + txHash; } - report.setAttr(`details.transfers.remainingGas.status`, "Transferred successfully"); - report.setAttr(`details.transfers.remainingGas.amount`, formatUnits(amount, 18)); + remainingGas.status = "Transferred successfully"; + remainingGas.amount = formatUnits(amount, 18); } catch (error: any) { hadFailures = true; if ("txHash" in error) { - report.setAttr( - `details.transfers.remainingGas.tx`, - this.state.chainConfig.blockExplorers?.default.url + "/tx/" + error.txHash, - ); - report.setAttr( - `details.transfers.remainingGas.status`, - await errorSnapshot("", error.error), - ); + remainingGas.tx = + this.state.chainConfig.blockExplorers?.default.url + "/tx/" + error.txHash; + remainingGas.status = await errorSnapshot("", error.error); } else { - report.setAttr( - `details.transfers.remainingGas.status`, - await errorSnapshot("", error), - ); + remainingGas.status = await errorSnapshot("", error); } } + + transfers.push(remainingGas); } // if there were failures, set the severity to low and set the status to error @@ -402,6 +383,7 @@ export class WalletManager { message: "Successfully swept wallet tokens", }); } + report.setAttr("transfers", JSON.stringify(transfers)); report.end(); return report; @@ -429,11 +411,14 @@ export class WalletManager { const report = new PreAssembledSpan("convert-to-gas"); report.setAttr("details.wallet", this.mainWallet.address); + const swaps: any[] = []; for (const [, tokenDetails] of this.state.watchedTokens) { - report.setAttr(`details.swaps.${tokenDetails.symbol}.token`, tokenDetails.address); + const swap: any = {}; + swap.symbol = tokenDetails.symbol; + swap.token = tokenDetails.address; if (this.state.appOptions.skipSweep.has(tokenDetails.address.toLowerCase())) { - report.setAttr(`details.swaps.${tokenDetails.symbol}.status`, "skipped"); + swap.status = "skipped"; continue; } @@ -450,59 +435,39 @@ export class WalletManager { // handle the result as report attributes if (txHash) { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.tx`, - this.state.chainConfig.blockExplorers?.default.url + "/tx/" + txHash, - ); + swap.tx = this.state.chainConfig.blockExplorers?.default.url + "/tx/" + txHash; } if (typeof status === "string") { - report.setAttr(`details.swaps.${tokenDetails.symbol}.status`, status); + swap.status = status; } if (typeof amount === "bigint") { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.amount`, - formatUnits(amount, tokenDetails.decimals), - ); + swap.amount = formatUnits(amount, tokenDetails.decimals); } if (typeof receivedAmount === "bigint") { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.receivedAmount`, - formatUnits(receivedAmount, 18), - ); + swap.receivedAmount = formatUnits(receivedAmount, 18); } if (typeof receivedAmountMin === "bigint") { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.receivedAmountMin`, - formatUnits(receivedAmountMin, 18), - ); + swap.receivedAmountMin = formatUnits(receivedAmountMin, 18); } if (typeof expectedGasCost === "bigint") { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.expectedGasCost`, - formatUnits(expectedGasCost, 18), - ); + swap.expectedGasCost = formatUnits(expectedGasCost, 18); } if (typeof route === "string") { - report.setAttr(`details.swaps.${tokenDetails.symbol}.route`, route); + swap.route = route; } } catch (error: any) { if ("txHash" in error) { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.tx`, - this.state.chainConfig.blockExplorers?.default.url + "/tx/" + error.txHash, - ); - report.setAttr( - `details.swaps.${tokenDetails.symbol}.status`, - await errorSnapshot("", error.error), - ); + swap.tx = + this.state.chainConfig.blockExplorers?.default.url + "/tx/" + error.txHash; + swap.status = await errorSnapshot("", error.error); } else { - report.setAttr( - `details.swaps.${tokenDetails.symbol}.status`, - await errorSnapshot("Failed to convert token to gas", error), - ); + swap.status = await errorSnapshot("Failed to convert token to gas", error); } } + + swaps.push(swap); } + report.setAttr("swaps", JSON.stringify(swaps)); report.end(); return report; diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index b9d93255..6ef15d57 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -352,6 +352,7 @@ for (let i = 0; i < testData.length; i++) { config.testBlockNumber = BigInt(blockNumber); config.testBlockNumberInc = BigInt(blockNumber); // increments during test updating to new block height config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -709,6 +710,7 @@ for (let i = 0; i < testData.length; i++) { config.orderbookAddress = orderbook1.address; config.testBlockNumber = BigInt(blockNumber); config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -735,7 +737,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1124,6 +1126,7 @@ for (let i = 0; i < testData.length; i++) { config.orderbookAddress = orderbook1.address; config.testBlockNumber = BigInt(blockNumber); config.gasCoveragePercentage = "0"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -1150,7 +1153,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1520,6 +1523,7 @@ for (let i = 0; i < testData.length; i++) { config.orderbookAddress = orderbook.address; config.testBlockNumber = BigInt(blockNumber); config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -1546,7 +1550,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1875,6 +1879,7 @@ for (let i = 0; i < testData.length; i++) { config.testBlockNumber = BigInt(blockNumber); config.testBlockNumberInc = BigInt(blockNumber); // increments during test updating to new block height config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -2183,6 +2188,7 @@ for (let i = 0; i < testData.length; i++) { config.testBlockNumber = BigInt(blockNumber); config.testBlockNumberInc = BigInt(blockNumber); // increments during test updating to new block height config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -2570,6 +2576,7 @@ for (let i = 0; i < testData.length; i++) { config.orderbookAddress = orderbook1.address; config.testBlockNumber = BigInt(blockNumber); config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -2596,7 +2603,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -2978,6 +2985,7 @@ for (let i = 0; i < testData.length; i++) { config.orderbookAddress = orderbook1.address; config.testBlockNumber = BigInt(blockNumber); config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -3004,7 +3012,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -3391,6 +3399,7 @@ for (let i = 0; i < testData.length; i++) { config.testBlockNumber = BigInt(blockNumber); config.gasCoveragePercentage = chainId === ChainId.BASE || chainId == ChainId.MATCHAIN ? "0" : "1"; + config.headroom = 101.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -3417,7 +3426,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -3769,6 +3778,7 @@ for (let i = 0; i < testData.length; i++) { config.testBlockNumber = BigInt(blockNumber); config.testBlockNumberInc = BigInt(blockNumber); // increments during test updating to new block height config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot; @@ -4100,6 +4110,7 @@ for (let i = 0; i < testData.length; i++) { config.testBlockNumber = BigInt(blockNumber); config.testBlockNumberInc = BigInt(blockNumber); // increments during test updating to new block height config.gasCoveragePercentage = "1"; + config.headroom = 1.5; config.viemClient = viemClient; config.accounts = []; config.mainAccount = bot;