From d5f26307c969b5da4a2d9aa7a8ae84ccd7e4304f Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 20 Jul 2026 22:05:04 +0000 Subject: [PATCH 1/8] init --- src/cli/index.test.ts | 2 +- src/cli/index.ts | 12 ++---- src/core/index.ts | 3 +- src/core/process/round.test.ts | 78 +++++++++++++++++++++++++++------- src/core/process/round.ts | 27 +++++++++++- src/order/index.test.ts | 15 ++++--- src/order/index.ts | 65 +++++++++++++++++++++++++--- test/e2e/e2e.test.js | 12 +++--- 8 files changed, 169 insertions(+), 45 deletions(-) diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 68f93c8e..a9056a28 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -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 = { @@ -724,7 +725,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..46989149 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -294,7 +294,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 +312,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], ); } } 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/process/round.test.ts b/src/core/process/round.test.ts index 8d19fb5f..33c0bb41 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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({ + noneZeroOutput: [], + 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({ + noneZeroOutput: [], + 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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({ + noneZeroOutput: mockOrders, + zeroOutput: [], + }); (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({ + noneZeroOutput: 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({ + noneZeroOutput: [], + 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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({ + noneZeroOutput: 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..69903fca 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.noneZeroOutput]; + 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,30 @@ export async function initializeRound( checkpointReports.push(checkpointReport); }); + // report zero output order pairs + const zeroOutputReport = new PreAssembledSpan(`order_zero_output`, performance.now()); + zeroOutputReport.setAttr( + "details", + JSON.stringify( + zeroOutputs.map((p) => { + const pair = `${p.buyTokenSymbol}/${p.sellTokenSymbol}`; + const owner = p.takeOrder.struct.order.owner.toLowerCase(); + return { + pair, + owner, + orderHash: p.takeOrder.id, + 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..e42379e8 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.noneZeroOutput)).toBe(true); + expect(Array.isArray(result.zeroOutput)).toBe(true); + expect(result.noneZeroOutput.length).toBeGreaterThan(0); // check the structure of the first orderbook's bundled orders - const bundledOrders = result; + const bundledOrders = result.noneZeroOutput; 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.noneZeroOutput.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.noneZeroOutput[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.noneZeroOutput[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.noneZeroOutput[0], CounterpartySource.InterOrderbook, ); for (const counteryparties of opposing) { diff --git a/src/order/index.ts b/src/order/index.ts index 583fc784..f486527b 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(): { + noneZeroOutput: Pair[]; + zeroOutput: Pair[]; + } { + const result: { + noneZeroOutput: Pair[]; + zeroOutput: Pair[]; + } = { + noneZeroOutput: [], + 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.noneZeroOutput.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/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index b9d93255..3033e46c 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -735,7 +735,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.noneZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1150,7 +1150,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.noneZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1546,7 +1546,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.noneZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -2596,7 +2596,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.noneZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -3004,7 +3004,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.noneZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -3417,7 +3417,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.forEach((pair) => { + orders.noneZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens From 7d6deb54cdce33da88aecc51032c41f2bacce44d Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 01:53:33 +0000 Subject: [PATCH 2/8] update --- src/cli/index.test.ts | 44 ++++++++++- src/cli/index.ts | 28 ++++--- src/core/modes/simulator.test.ts | 8 +- src/core/modes/simulator.ts | 4 +- src/core/process/round.test.ts | 30 ++++---- src/core/process/round.ts | 35 ++++----- src/order/index.test.ts | 14 ++-- src/order/index.ts | 8 +- src/wallet/index.test.ts | 120 +++++++++++------------------- src/wallet/index.ts | 123 +++++++++++-------------------- 10 files changed, 192 insertions(+), 222 deletions(-) diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index a9056a28..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"; @@ -199,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; @@ -656,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([]); @@ -670,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" }, @@ -686,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); }); }); diff --git a/src/cli/index.ts b/src/cli/index.ts index 46989149..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, @@ -341,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) => { @@ -350,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/core/modes/simulator.test.ts b/src/core/modes/simulator.test.ts index 5a97c917..0bd132f0 100644 --- a/src/core/modes/simulator.test.ts +++ b/src/core/modes/simulator.test.ts @@ -257,7 +257,7 @@ describe("Test TradeSimulatorBase", () => { Result.err(setTransactionDataError), ); const headroom = BigInt( - (Number(mockSolver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + (Number(mockSolver.appOptions.gasCoveragePercentage) * 101.5).toFixed(), ); const result = await mockSimulator.trySimulateTrade(); @@ -326,7 +326,7 @@ describe("Test TradeSimulatorBase", () => { }; (dryrun as Mock).mockResolvedValueOnce(Result.err(dryrunError)); const headroom = BigInt( - (Number(mockSolver.appOptions.gasCoveragePercentage) * 102.5).toFixed(), + (Number(mockSolver.appOptions.gasCoveragePercentage) * 101.5).toFixed(), ); const result = await mockSimulator.trySimulateTrade(); @@ -410,7 +410,7 @@ 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) * 101.5).toFixed(), ); // last call to setTransactionData fails const setTransactionDataError = { @@ -525,7 +525,7 @@ 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) * 101.5).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..aef954b9 100644 --- a/src/core/modes/simulator.ts +++ b/src/core/modes/simulator.ts @@ -153,9 +153,9 @@ 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) * 101.5).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 33c0bb41..c547eeec 100644 --- a/src/core/process/round.test.ts +++ b/src/core/process/round.test.ts @@ -113,7 +113,7 @@ describe("Test initializeRound", () => { const mockSettleFn = vi.fn(); (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -170,7 +170,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue({ @@ -215,7 +215,7 @@ describe("Test initializeRound", () => { describe("empty orders handling", () => { it("should return empty settlements and checkpointReports for empty orders", async () => { (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: [], + nonZeroOutput: [], zeroOutput: [], }); @@ -231,7 +231,7 @@ describe("Test initializeRound", () => { describe("method call verification", () => { it("should call getNextRoundOrders with correct parameter", async () => { (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: [], + nonZeroOutput: [], zeroOutput: [], }); @@ -261,7 +261,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -286,7 +286,7 @@ describe("Test initializeRound", () => { const mockOrders = [orderDetails]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -316,7 +316,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue({ @@ -363,7 +363,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -408,7 +408,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -434,7 +434,7 @@ describe("Test initializeRound", () => { }, ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -465,7 +465,7 @@ describe("Test initializeRound", () => { }, ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -483,7 +483,7 @@ describe("Test initializeRound", () => { describe("return value structure", () => { it("should always return object with settlements and checkpointReports arrays", async () => { (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: [], + nonZeroOutput: [], zeroOutput: [], }); @@ -511,7 +511,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue({ @@ -547,7 +547,7 @@ describe("Test initializeRound", () => { ]; const mockSettleFn = vi.fn(); (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); @@ -645,7 +645,7 @@ describe("Test initializeRound", () => { ]; const mockSettleFn = vi.fn(); (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ - noneZeroOutput: mockOrders, + nonZeroOutput: mockOrders, zeroOutput: [], }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); diff --git a/src/core/process/round.ts b/src/core/process/round.ts index 69903fca..95a423ad 100644 --- a/src/core/process/round.ts +++ b/src/core/process/round.ts @@ -30,7 +30,7 @@ export async function initializeRound( shuffle = true, ) { const nextRoundOrders = this.orderManager.getNextRoundOrders(); - const orders = [...nextRoundOrders.noneZeroOutput]; + const orders = [...nextRoundOrders.nonZeroOutput]; const zeroOutputs = [...nextRoundOrders.zeroOutput]; const totalLength = orders.length + zeroOutputs.length; const settlements: Settlement[] = []; @@ -94,24 +94,21 @@ export async function initializeRound( }); // report zero output order pairs - const zeroOutputReport = new PreAssembledSpan(`order_zero_output`, performance.now()); - zeroOutputReport.setAttr( - "details", - JSON.stringify( - zeroOutputs.map((p) => { - const pair = `${p.buyTokenSymbol}/${p.sellTokenSymbol}`; - const owner = p.takeOrder.struct.order.owner.toLowerCase(); - return { - pair, - owner, - orderHash: p.takeOrder.id, - orderbook: p.orderbook, - }; - }), - ), - ); - zeroOutputReport.end(); - this.logger?.exportPreAssembledSpan(zeroOutputReport, roundSpanCtx?.context); + 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, diff --git a/src/order/index.test.ts b/src/order/index.test.ts index e42379e8..120f5cb0 100644 --- a/src/order/index.test.ts +++ b/src/order/index.test.ts @@ -585,12 +585,12 @@ describe("Test OrderManager", () => { await orderManager.addOrder(mockOrder as any); const result = orderManager.getNextRoundOrders(); - expect(Array.isArray(result.noneZeroOutput)).toBe(true); + expect(Array.isArray(result.nonZeroOutput)).toBe(true); expect(Array.isArray(result.zeroOutput)).toBe(true); - expect(result.noneZeroOutput.length).toBeGreaterThan(0); + expect(result.nonZeroOutput.length).toBeGreaterThan(0); // check the structure of the first orderbook's bundled orders - const bundledOrders = result.noneZeroOutput; + const bundledOrders = result.nonZeroOutput; expect(Array.isArray(bundledOrders)).toBe(true); expect(bundledOrders.length).toBeGreaterThan(0); @@ -954,7 +954,7 @@ describe("Test OrderManager", () => { // helper to get the order hashes returned in the round const getRoundHashes = () => { const roundOrders = orderManager.getNextRoundOrders(); - return roundOrders.noneZeroOutput.map((o) => o.takeOrder.id); + return roundOrders.nonZeroOutput.map((o) => o.takeOrder.id); }; // first call: should return the first 3 orders @@ -982,7 +982,7 @@ describe("Test OrderManager", () => { // get the takeOrder object from getNextRoundOrders const roundOrders = orderManager.getNextRoundOrders(); - const orderDetails = roundOrders.noneZeroOutput[0]; + const orderDetails = roundOrders.nonZeroOutput[0]; // update the quote field via the object from getNextRoundOrders orderDetails.takeOrder.quote = { maxOutput: 999n, ratio: 888n }; @@ -1049,7 +1049,7 @@ describe("Test OrderManager", () => { // should find orderB as opposing order for orderA in the same orderbook const opposing = orderManager.getCounterpartyOrders( - roundOrders.noneZeroOutput[0], + roundOrders.nonZeroOutput[0], CounterpartySource.IntraOrderbook, ); expect(Array.isArray(opposing)).toBe(true); @@ -1125,7 +1125,7 @@ describe("Test OrderManager", () => { // should find orderB as opposing order for orderA across orderbooks const opposing = orderManager.getCounterpartyOrders( - roundOrders.noneZeroOutput[0], + roundOrders.nonZeroOutput[0], CounterpartySource.InterOrderbook, ); for (const counteryparties of opposing) { diff --git a/src/order/index.ts b/src/order/index.ts index f486527b..6255810f 100644 --- a/src/order/index.ts +++ b/src/order/index.ts @@ -441,14 +441,14 @@ export class OrderManager { * @returns Array of bundled orders grouped by orderbook */ getNextRoundOrders(): { - noneZeroOutput: Pair[]; + nonZeroOutput: Pair[]; zeroOutput: Pair[]; } { const result: { - noneZeroOutput: Pair[]; + nonZeroOutput: Pair[]; zeroOutput: Pair[]; } = { - noneZeroOutput: [], + nonZeroOutput: [], zeroOutput: [], }; this.ownersMap.forEach((ownersProfileMap) => { @@ -499,7 +499,7 @@ export class OrderManager { if (pair.sellTokenVaultBalance <= 0n) { result.zeroOutput.push(pair); } else { - result.noneZeroOutput.push(pair); + result.nonZeroOutput.push(pair); } } }); 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; From 645ae2c377d125ea2a664e90c25b16efacbafd6d Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 21:11:21 +0000 Subject: [PATCH 3/8] Update e2e.test.js --- test/e2e/e2e.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index 3033e46c..1e1f4b4f 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -735,7 +735,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.noneZeroOutput.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1150,7 +1150,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.noneZeroOutput.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -1546,7 +1546,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.noneZeroOutput.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -2596,7 +2596,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.noneZeroOutput.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -3004,7 +3004,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.noneZeroOutput.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens @@ -3417,7 +3417,7 @@ for (let i = 0; i < testData.length; i++) { orders = orderManager.getNextRoundOrders(false); // mock init quotes - orders.noneZeroOutput.forEach((pair) => { + orders.nonZeroOutput.forEach((pair) => { pair.takeOrder.quote = { ratio: ethers.constants.Zero.toBigInt(), maxOutput: tokens From 343d295a735e9e8aa27c43c19158b870bd8dbd91 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 21:14:18 +0000 Subject: [PATCH 4/8] Update round.ts --- src/core/process/round.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/process/round.ts b/src/core/process/round.ts index 95a423ad..92450b47 100644 --- a/src/core/process/round.ts +++ b/src/core/process/round.ts @@ -94,7 +94,7 @@ export async function initializeRound( }); // report zero output order pairs - for (let i = 0; i <= zeroOutputs.length; i += 25) { + 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}`; From 87d9e7c8dd9697081247237c2b69883f6237ff32 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 21:17:31 +0000 Subject: [PATCH 5/8] Update round.test.ts --- src/core/process/round.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/process/round.test.ts b/src/core/process/round.test.ts index c547eeec..ddb6a505 100644 --- a/src/core/process/round.test.ts +++ b/src/core/process/round.test.ts @@ -435,7 +435,7 @@ describe("Test initializeRound", () => { ]; (mockOrderManager.getNextRoundOrders as Mock).mockReturnValue({ nonZeroOutput: mockOrders, - zeroOutput: [], + zeroOutput: mockOrders, }); (mockWalletManager.getRandomSigner as Mock).mockResolvedValue(mockSigner); (mockSolver as any).logger = { From e8d77b25ee16b4e07d2de75d1fe15a91e30683f8 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 21:47:46 +0000 Subject: [PATCH 6/8] update --- config.env.yaml | 1 + config.example.yaml | 4 ++++ src/cli/commands/sweep.ts | 1 + src/config/yaml.test.ts | 3 +++ src/config/yaml.ts | 9 +++++++++ src/core/modes/simulator.test.ts | 21 +++++++++++++++++---- src/core/modes/simulator.ts | 5 ++++- 7 files changed, 39 insertions(+), 5 deletions(-) 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..26928ec4 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: %1.5 +headroom: 1.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/config/yaml.test.ts b/src/config/yaml.test.ts index 9ae3e9f8..f69d1cb2 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, 101.5); // ownerProfile const expectedOwnerProfile = { diff --git a/src/config/yaml.ts b/src/config/yaml.ts index f54132cb..a74718c8 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: 1.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", + "1.5", + ) + 100, } as AppOptions); } catch (error: any) { if (error instanceof AppOptionsError) { diff --git a/src/core/modes/simulator.test.ts b/src/core/modes/simulator.test.ts index 0bd132f0..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) * 101.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) * 101.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) * 101.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) * 101.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 aef954b9..0a3914f1 100644 --- a/src/core/modes/simulator.ts +++ b/src/core/modes/simulator.ts @@ -155,7 +155,10 @@ export abstract class TradeSimulatorBase { // examine the success of the trade with 1.5% headroom const headroom = BigInt( - (Number(this.tradeArgs.solver.appOptions.gasCoveragePercentage) * 101.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(); From 2aabe9522460cb02dc9cd076c0c8cd32f032e61f Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 21:56:46 +0000 Subject: [PATCH 7/8] Update e2e.test.js --- test/e2e/e2e.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index 1e1f4b4f..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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; From f9222903697198bc5c76bc0dcf8a529cededc6af Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 26 Jul 2026 22:06:27 +0000 Subject: [PATCH 8/8] update --- config.example.yaml | 4 ++-- src/config/yaml.test.ts | 2 +- src/config/yaml.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 26928ec4..e211ef77 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -99,8 +99,8 @@ poolUpdateInterval: 10 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: %1.5 -headroom: 1.5 +# 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/config/yaml.test.ts b/src/config/yaml.test.ts index f69d1cb2..31ef623f 100644 --- a/src/config/yaml.test.ts +++ b/src/config/yaml.test.ts @@ -280,7 +280,7 @@ orderbookTradeTypes: assert.deepEqual(result.timeout, 20000); assert.equal(result.maxRatio, true); assert.equal(result.maxConcurrency, 1); - assert.equal(result.headroom, 101.5); + assert.equal(result.headroom, 102.5); // ownerProfile const expectedOwnerProfile = { diff --git a/src/config/yaml.ts b/src/config/yaml.ts index a74718c8..ecc07968 100644 --- a/src/config/yaml.ts +++ b/src/config/yaml.ts @@ -108,7 +108,7 @@ export type AppOptions = { maxConcurrency: number; /** List of tokens to skip when sweeping bounty tokens */ skipSweep: Set; - /** Trade simulation profitablity headroom, default: 1.5 */ + /** Trade simulation profitablity headroom, default: 2.5 */ headroom: number; }; @@ -301,7 +301,7 @@ export namespace AppOptions { input.headroom, FLOAT_PATTERN, "invalid headroom value, must be a number greater than equal to 0", - "1.5", + "2.5", ) + 100, } as AppOptions); } catch (error: any) {